A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1import { describe, expect, it } from 'vitest';
2import { highlightCodeBlocks, highlightJson, highlightSource } from './highlight';
3
4const block = ( inner: string ) =>
5 `<pre class="wp-block-code"><code>${ inner }</code></pre>`;
6
7describe( 'highlightCodeBlocks', () => {
8 it( 'tokenises a detected language into hljs spans', () => {
9 // A clearly-JavaScript snippet; auto-detect should produce tokens.
10 const out = highlightCodeBlocks( block( 'const answer = 42;' ) );
11 expect( out ).toContain( 'class="hljs' );
12 expect( out ).toContain( '<span class="hljs-' );
13 // Still a single wp-block-code pre.
14 expect( out ).toContain( '<pre class="wp-block-code"><code class="hljs' );
15 expect( out ).toContain( '</code></pre>' );
16 } );
17
18 it( 'keeps escaped angle brackets escaped (no raw HTML injected)', () => {
19 // render.ts stores code entity-escaped; markup must never be un-escaped into live tags.
20 const out = highlightCodeBlocks( block( '<div class="x">hi</div>' ) );
21 expect( out ).toContain( '<' );
22 expect( out ).not.toContain( '<div class="x">' );
23 } );
24
25 it( 'leaves inline <code> (not in a wp-block-code pre) untouched', () => {
26 const html = '<p>Use <code>npm run dev</code> to start.</p>';
27 expect( highlightCodeBlocks( html ) ).toBe( html );
28 } );
29
30 it( 'leaves non-code HTML untouched', () => {
31 const html = '<h2 class="wp-block-heading">Title</h2><p>Body.</p>';
32 expect( highlightCodeBlocks( html ) ).toBe( html );
33 } );
34
35 it( 'falls back to a plain wp-block-code block when nothing is highlightable', () => {
36 // Empty content can yield zero-relevance auto-detect; must not throw or corrupt markup.
37 const out = highlightCodeBlocks( block( '' ) );
38 expect( out ).toContain( '<pre class="wp-block-code">' );
39 expect( out ).toContain( '</code></pre>' );
40 expect( out ).not.toContain( 'hljs' );
41 expect( out ).toContain( '<pre class="wp-block-code"><code></code></pre>' );
42 } );
43
44 it( 'highlights every block when there are several', () => {
45 const out = highlightCodeBlocks(
46 block( 'const a = 1;' ) + '<p>mid</p>' + block( 'let b = 2;' )
47 );
48 // One highlighted wrapper per code block (token spans also carry hljs-* classes,
49 // so count the <code> wrapper specifically, not every hljs class).
50 const wrappers = out.match( /<code class="hljs/g ) ?? [];
51 expect( wrappers.length ).toBe( 2 );
52 expect( out ).toContain( '<p>mid</p>' );
53 } );
54
55 it( 'converts <br> line breaks in code to real newlines (never literal tag text)', () => {
56 // Some stored code blocks use <br /> as the line separator (render.ts passes it
57 // through; the sanitiser keeps it as a real break). Highlighting must tokenise
58 // clean multi-line source, not turn the breaks into literal "<br>" text.
59 const out = highlightCodeBlocks(
60 block( '{<br /> "name": "potato",<br /> "value": 42<br />}' )
61 );
62 expect( out ).not.toContain( '<br' ); // no raw <br> passed through to hljs
63 expect( out ).not.toContain( '<' ); // and not escaped into literal "<br>" text
64 expect( out ).toContain( '\n' ); // line breaks preserved as real newlines
65 expect( out ).toContain( 'class="hljs' ); // still highlighted
66 } );
67
68 it( 'keeps a literal <br> typed in a code sample as text, not a newline', () => {
69 // A literal `<br>` in a code sample is stored escaped (`<br>`), unlike the
70 // real break tags above. It must survive as escaped tag text after highlighting,
71 // not be collapsed into a newline along with the genuine separators.
72 const out = highlightCodeBlocks( block( 'const html = "<br>";' ) );
73 expect( out ).toContain( '<br>' ); // literal example tag preserved as text
74 expect( out ).not.toContain( '\n' ); // not turned into a line break
75 expect( out ).toContain( 'class="hljs' ); // still highlighted
76 } );
77} );
78
79describe( 'highlightJson', () => {
80 it( 'tokenises JSON into hljs spans inside a language-json code element', () => {
81 const out = highlightJson( '{\n "answer": 42\n}' );
82 expect( out ).toContain( '<code class="hljs language-json">' );
83 expect( out ).toContain( '<span class="hljs-' );
84 expect( out ).toContain( '</code>' );
85 // No <pre> wrapper — the caller owns the chrome.
86 expect( out ).not.toContain( '<pre' );
87 } );
88
89 it( 'keeps HTML in string values escaped (no raw tags from untrusted records)', () => {
90 const out = highlightJson( '{\n "x": "</script><img src=x onerror=alert(1)>"\n}' );
91 expect( out ).toContain( '<' );
92 expect( out ).not.toContain( '<img' );
93 expect( out ).not.toContain( '</script>' );
94 } );
95
96 it( 'handles an empty object without throwing', () => {
97 const out = highlightJson( '{}' );
98 expect( out ).toContain( '<code class="hljs language-json">' );
99 expect( out ).toContain( '</code>' );
100 } );
101} );
102
103describe( 'highlightSource', () => {
104 it( 'tokenises a known-language source string into hljs spans', () => {
105 // Unlike the reader blocks, static callers (e.g. the lexicon schema dump) know
106 // the language up front, so we highlight directly instead of auto-detecting.
107 const out = highlightSource( '{ "name": "potato", "value": 42 }', 'json' );
108 expect( out ).toContain( '<span class="hljs-' );
109 expect( out ).toContain( 'potato' );
110 } );
111
112 it( 'escapes angle brackets, never injecting raw markup', () => {
113 const out = highlightSource( '{ "tag": "<b>" }', 'json' );
114 expect( out ).not.toContain( '<b>' );
115 expect( out ).toContain( '<b>' );
116 } );
117
118 it( 'falls back to plain escaped text for an unregistered language', () => {
119 // An unknown grammar makes hljs throw; the source must still render as safe,
120 // entity-escaped text — never raw markup, never an error bubbling to the page.
121 const out = highlightSource( 'a < b && c > d', 'no-such-lang' );
122 expect( out ).not.toContain( 'hljs-' );
123 expect( out ).toContain( '<' );
124 expect( out ).toContain( '&' );
125 expect( out ).toContain( '>' );
126 } );
127} );