A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

Syntax-highlight the lexicon page's JSON schema dump

The lexicon page printed the blog.skypress.content.gutenberg schema as
flat monospace, while the reader's code blocks already get coloured. The
existing highlighter only exposed highlightCodeBlocks, which auto-detects
the language on rendered wp-block-code HTML — wrong fit for a schema
string we already know is JSON.

Add highlightSource(source, language) to the same module (keeping
highlight.js imported in one place) to tokenise a known-language string
directly, falling back to plain entity-escaped text if the grammar
throws. The page highlights its schema as JSON in frontmatter, so it
stays server-side with no client JS, like the reader.

The lexicon code block sits on a light --paper-raised surface, not the
reader's dark <pre>, so its token palette is built fresh: brand tokens
that flip with the theme for keys/keywords, plus two string/number hues
overridden under prefers-color-scheme: dark. Verified legible in both
light and dark themes.

+94 -2
+27 -1
src/lib/reader/highlight.test.ts
··· 1 1 import { describe, expect, it } from 'vitest'; 2 - import { highlightCodeBlocks } from './highlight'; 2 + import { highlightCodeBlocks, highlightSource } from './highlight'; 3 3 4 4 const block = ( inner: string ) => 5 5 `<pre class="wp-block-code"><code>${ inner }</code></pre>`; ··· 75 75 expect( out ).toContain( 'class="hljs' ); // still highlighted 76 76 } ); 77 77 } ); 78 + 79 + describe( 'highlightSource', () => { 80 + it( 'tokenises a known-language source string into hljs spans', () => { 81 + // Unlike the reader blocks, static callers (e.g. the lexicon schema dump) know 82 + // the language up front, so we highlight directly instead of auto-detecting. 83 + const out = highlightSource( '{ "name": "potato", "value": 42 }', 'json' ); 84 + expect( out ).toContain( '<span class="hljs-' ); 85 + expect( out ).toContain( 'potato' ); 86 + } ); 87 + 88 + it( 'escapes angle brackets, never injecting raw markup', () => { 89 + const out = highlightSource( '{ "tag": "<b>" }', 'json' ); 90 + expect( out ).not.toContain( '<b>' ); 91 + expect( out ).toContain( '&lt;b&gt;' ); 92 + } ); 93 + 94 + it( 'falls back to plain escaped text for an unregistered language', () => { 95 + // An unknown grammar makes hljs throw; the source must still render as safe, 96 + // entity-escaped text — never raw markup, never an error bubbling to the page. 97 + const out = highlightSource( 'a < b && c > d', 'no-such-lang' ); 98 + expect( out ).not.toContain( 'hljs-' ); 99 + expect( out ).toContain( '&lt;' ); 100 + expect( out ).toContain( '&amp;' ); 101 + expect( out ).toContain( '&gt;' ); 102 + } ); 103 + } );
+23
src/lib/reader/highlight.ts
··· 79 79 export function highlightCodeBlocks( html: string ): string { 80 80 return html.replace( CODE_BLOCK, ( _full, inner: string ) => highlightOne( inner ) ); 81 81 } 82 + 83 + function escapeHtml( source: string ): string { 84 + return source 85 + .replace( /&/g, '&amp;' ) 86 + .replace( /</g, '&lt;' ) 87 + .replace( />/g, '&gt;' ); 88 + } 89 + 90 + /** 91 + * Highlight a known-language source string into hljs token HTML, for static pages 92 + * (e.g. the lexicon's JSON schema dump) that know their language up front and so 93 + * skip the reader's auto-detection. Returns highlight.js's already-escaped token 94 + * HTML, safe to inject; on any error (unregistered language, etc.) it falls back to 95 + * plain entity-escaped text — the source still renders, never as raw markup. Keeping 96 + * this here preserves the rule that highlight.js is imported in this module only. 97 + */ 98 + export function highlightSource( source: string, language: string ): string { 99 + try { 100 + return hljs.highlight( source, { language } ).value; 101 + } catch { 102 + return escapeHtml( source ); 103 + } 104 + }
+44 -1
src/pages/lexicon.astro
··· 9 9 contentSchemaFields, 10 10 } from '../lib/lexicon/schema-doc'; 11 11 import { parseInlineCode } from '../lib/lexicon/inline-code'; 12 + import { highlightSource } from '../lib/reader/highlight'; 12 13 13 14 const contentFields = contentSchemaFields(); 14 15 const schemaJson = JSON.stringify( CONTENT_LEXICON, null, 2 ); 16 + // The schema is JSON we control, so highlight it directly (known language) rather 17 + // than auto-detecting. Server-side at build time → no client JS, like the reader. 18 + const schemaJsonHtml = highlightSource( schemaJson, 'json' ); 15 19 16 20 // Hand-authored interop tables (site.standard.* are community-owned; see Decision 0005). 17 21 const documentFields = [ ··· 100 104 <code>textContent</code>, so your article is never unreadable. Here&rsquo;s 101 105 the full schema: 102 106 </p> 103 - <pre class="lex-code"><code>{ schemaJson }</code></pre> 107 + <pre class="lex-code"><code class="hljs language-json" set:html={ schemaJsonHtml } /></pre> 104 108 </section> 105 109 106 110 <section class="lex-section"> ··· 260 264 overflow: auto; 261 265 font-size: 0.85rem; 262 266 line-height: 1.55; 267 + /* String/number hues that read on the light code surface; the warm-orange 268 + and muted token colours below reuse brand tokens that already flip. */ 269 + --code-string: #3f7d4f; 270 + --code-number: #2f6f86; 263 271 } 264 272 .lex-code code { 265 273 font-family: var( --font-mono ); 266 274 background: none; 267 275 color: var( --ink ); 268 276 padding: 0; 277 + } 278 + /* highlight.js token colours, scoped to this light code block (unlike the 279 + reader's dark <pre>). The spans arrive via set:html, so they need :global. 280 + No background/padding on .hljs, so the .lex-code chrome above is untouched. */ 281 + .lex-code :global( .hljs-comment ), 282 + .lex-code :global( .hljs-quote ), 283 + .lex-code :global( .hljs-punctuation ) { 284 + color: var( --muted ); 285 + } 286 + .lex-code :global( .hljs-comment ), 287 + .lex-code :global( .hljs-quote ) { 288 + font-style: italic; 289 + } 290 + .lex-code :global( .hljs-attr ), 291 + .lex-code :global( .hljs-keyword ), 292 + .lex-code :global( .hljs-selector-tag ), 293 + .lex-code :global( .hljs-name ) { 294 + color: var( --sun-strong ); 295 + } 296 + .lex-code :global( .hljs-string ), 297 + .lex-code :global( .hljs-symbol ), 298 + .lex-code :global( .hljs-meta .hljs-string ) { 299 + color: var( --code-string ); 300 + } 301 + .lex-code :global( .hljs-number ), 302 + .lex-code :global( .hljs-literal ), 303 + .lex-code :global( .hljs-built_in ) { 304 + color: var( --code-number ); 305 + } 306 + 307 + @media ( prefers-color-scheme: dark ) { 308 + .lex-code { 309 + --code-string: #8fc98f; 310 + --code-number: #7fb2c9; 311 + } 269 312 } 270 313 271 314 .lex-actions {