···5151 expect( wrappers.length ).toBe( 2 );
5252 expect( out ).toContain( '<p>mid</p>' );
5353 } );
5454+5555+ it( 'converts <br> line breaks in code to real newlines (never literal tag text)', () => {
5656+ // Some stored code blocks use <br /> as the line separator (render.ts passes it
5757+ // through; the sanitiser keeps it as a real break). Highlighting must tokenise
5858+ // clean multi-line source, not turn the breaks into literal "<br>" text.
5959+ const out = highlightCodeBlocks(
6060+ block( '{<br /> "name": "potato",<br /> "value": 42<br />}' )
6161+ );
6262+ expect( out ).not.toContain( '<br' ); // no raw <br> passed through to hljs
6363+ expect( out ).not.toContain( '<' ); // and not escaped into literal "<br>" text
6464+ expect( out ).toContain( '\n' ); // line breaks preserved as real newlines
6565+ expect( out ).toContain( 'class="hljs' ); // still highlighted
6666+ } );
5467} );
+5-1
src/lib/reader/highlight.ts
···5555const CODE_BLOCK = /<pre class="wp-block-code"><code>([\s\S]*?)<\/code><\/pre>/g;
56565757function highlightOne( escapedSource: string ): string {
5858- const raw = decodeEntities( escapedSource );
5858+ // Stored code may use <br> for line breaks (render.ts passes it through and the
5959+ // sanitiser keeps it as a real break). Turn those into real newlines BEFORE tokenising,
6060+ // or the highlighter treats "<br>" as an xml tag and emits literal "<br>" text,
6161+ // collapsing the code's line structure.
6262+ const raw = decodeEntities( escapedSource ).replace( /<br\s*\/?>/gi, '\n' );
5963 try {
6064 const { value, language, relevance } = hljs.highlightAuto( raw );
6165 // Zero relevance / no detected language → no useful tokens; keep it plain.