diff --git a/website/app/page.ts b/website/app/page.ts index aa73ac910..b00ee9442 100644 --- a/website/app/page.ts +++ b/website/app/page.ts @@ -105,26 +105,9 @@ export default function LandingPage() { @media (prefers-color-scheme: dark) { :root:not([data-theme='light']) { --code-tag: oklch(0.78 0.13 250); --code-attr: oklch(0.66 0.16 150); --code-str: oklch(0.80 0.15 145); } } - .t-com { color: var(--fg-subtle); font-style: italic; } - .t-str { color: oklch(0.52 0.13 150); } - .t-kw { color: oklch(0.52 0.16 295); font-weight: 600; } - .t-fn { color: oklch(0.52 0.15 250); } - .t-type{ color: oklch(0.52 0.10 200); } - .t-num { color: oklch(0.55 0.12 215); } - .t-punc{ color: var(--fg-muted); } - .t-id { color: var(--fg); } - :root[data-theme='dark'] .t-str { color: oklch(0.80 0.14 150); } - :root[data-theme='dark'] .t-kw { color: oklch(0.76 0.14 295); } - :root[data-theme='dark'] .t-fn { color: oklch(0.75 0.13 250); } - :root[data-theme='dark'] .t-type{ color: oklch(0.80 0.10 200); } - :root[data-theme='dark'] .t-num { color: oklch(0.82 0.12 215); } - @media (prefers-color-scheme: dark) { - :root:not([data-theme='light']) .t-str { color: oklch(0.80 0.14 150); } - :root:not([data-theme='light']) .t-kw { color: oklch(0.76 0.14 295); } - :root:not([data-theme='light']) .t-fn { color: oklch(0.75 0.13 250); } - :root:not([data-theme='light']) .t-type{ color: oklch(0.80 0.10 200); } - :root:not([data-theme='light']) .t-num { color: oklch(0.82 0.12 215); } - } + /* Syntax-highlight token colors (.t-kw / .t-str / ...) are defined + globally in public/input.css so every surface (this page and the + blog code fences) shares one palette. */ /* The live like-button demo: a bare light-DOM button the page styles into a pill (tag-prefixed, per the light-DOM CSS rule). */ like-button button { diff --git a/website/lib/highlight.ts b/website/lib/highlight.ts index 6b058b300..bf9ec9429 100644 --- a/website/lib/highlight.ts +++ b/website/lib/highlight.ts @@ -121,3 +121,23 @@ export function highlight(code: string): TemplateResult[] { return cls ? html`${tok.v}` : html`${tok.v}`; }); } + +function esc(s: string): string { + return s.replace(/&/g, '&').replace(//g, '>'); +} + +/** + * Highlight a code sample into an HTML string of styled spans, for callers + * that build markup as a string rather than an `html` TemplateResult (the + * blog markdown renderer, `modules/blog/utils/render-post.ts`). Same + * tokenizer and token classes as `highlight()`, so the colors match every + * other WebJs surface. Token text is HTML-escaped, so a sample can contain + * `<`, `&`, and backticks safely. + */ +export function highlightToHtml(code: string): string { + return tokenize(code.replace(/^\n+|\n+$/g, '')).map((tok) => { + const cls = CLASS[tok.t] ?? ''; + const v = esc(tok.v); + return cls ? `${v}` : v; + }).join(''); +} diff --git a/website/modules/blog/utils/render-post.ts b/website/modules/blog/utils/render-post.ts index 0a11c8635..f513f7aa1 100644 --- a/website/modules/blog/utils/render-post.ts +++ b/website/modules/blog/utils/render-post.ts @@ -20,6 +20,8 @@ * screen-reader cue); an internal `/path` link navigates in place. */ +import { highlightToHtml } from '#lib/highlight.ts'; + function inline(s: string): string { let out = s .replace(/&/g, '&') @@ -75,9 +77,16 @@ export function renderPostBody(md: string): string { for (const raw of lines) { if (inCode) { if (raw.trim().startsWith('```')) { - const escaped = codeBuf.join('\n') - .replace(/&/g, '&').replace(//g, '>'); - out.push(`
${escaped}`);
+ const src = codeBuf.join('\n');
+ // JS/TS fences get SSR syntax highlighting via the shared tokenizer
+ // (colors match the home page and the ui docs). Every other fence,
+ // INCLUDING a bare no-language fence (sh, plain command output, JSON),
+ // stays plain escaped text: JS-tokenizing shell output mis-colors it.
+ const HIGHLIGHTED = new Set(['ts', 'tsx', 'js', 'jsx', 'javascript', 'typescript']);
+ const body = HIGHLIGHTED.has(codeLang.toLowerCase())
+ ? highlightToHtml(src)
+ : src.replace(/&/g, '&').replace(//g, '>');
+ out.push(`${body}`);
codeBuf = [];
codeLang = '';
inCode = false;
diff --git a/website/public/input.css b/website/public/input.css
index d021e236e..a5bbf3806 100644
--- a/website/public/input.css
+++ b/website/public/input.css
@@ -42,3 +42,30 @@
@font-face{font-family:'Inter Tight';font-style:normal;font-weight:100 900;font-display:swap;src:url('/public/fonts/inter-tight.woff2') format('woff2');}
@font-face{font-family:'Inter';font-style:normal;font-weight:100 900;font-display:swap;src:url('/public/fonts/inter.woff2') format('woff2');}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:100 800;font-display:swap;src:url('/public/fonts/jetbrains-mono.woff2') format('woff2');}
+
+/*
+ * Syntax-highlight token colors (theme-aware). Emitted at SSR by
+ * lib/highlight.ts (home page code windows and blog code fences share the
+ * one tokenizer, so the palette is defined once here, globally, rather than
+ * inline per page). Class names: t-com/t-str/t-kw/t-fn/t-type/t-num/t-punc/t-id.
+ */
+.t-com { color: var(--fg-subtle); font-style: italic; }
+.t-str { color: oklch(0.52 0.13 150); }
+.t-kw { color: oklch(0.52 0.16 295); font-weight: 600; }
+.t-fn { color: oklch(0.52 0.15 250); }
+.t-type{ color: oklch(0.52 0.10 200); }
+.t-num { color: oklch(0.55 0.12 215); }
+.t-punc{ color: var(--fg-muted); }
+.t-id { color: var(--fg); }
+:root[data-theme='dark'] .t-str { color: oklch(0.80 0.14 150); }
+:root[data-theme='dark'] .t-kw { color: oklch(0.76 0.14 295); }
+:root[data-theme='dark'] .t-fn { color: oklch(0.75 0.13 250); }
+:root[data-theme='dark'] .t-type{ color: oklch(0.80 0.10 200); }
+:root[data-theme='dark'] .t-num { color: oklch(0.82 0.12 215); }
+@media (prefers-color-scheme: dark) {
+ :root:not([data-theme='light']) .t-str { color: oklch(0.80 0.14 150); }
+ :root:not([data-theme='light']) .t-kw { color: oklch(0.76 0.14 295); }
+ :root:not([data-theme='light']) .t-fn { color: oklch(0.75 0.13 250); }
+ :root:not([data-theme='light']) .t-type{ color: oklch(0.80 0.10 200); }
+ :root:not([data-theme='light']) .t-num { color: oklch(0.82 0.12 215); }
+}
diff --git a/website/test/highlight/highlight.test.ts b/website/test/highlight/highlight.test.ts
index f13005b4b..040516500 100644
--- a/website/test/highlight/highlight.test.ts
+++ b/website/test/highlight/highlight.test.ts
@@ -14,21 +14,23 @@ import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { html } from '@webjsdev/core';
import { renderToString } from '@webjsdev/core/server';
-import { highlight } from '#lib/highlight.ts';
+import { highlight, highlightToHtml } from '#lib/highlight.ts';
+import { renderPostBody } from '#modules/blog/utils/render-post.ts';
const render = (code: string) => renderToString(html`${highlight(code)}`);
-test('every token class highlight() emits is styled in app/page.ts', () => {
- // highlight.ts emits t-* classes; the only consumer that styles them is the
- // inline