import fs from 'node:fs'; import vm from 'node:vm'; // ---- Regression guard for the fenced-code renderer ------------------------- // Chat markdown is rendered through marked with the custom renderer in // window.MARKED_OPTIONS (src/js/helpers.js). marked >= 13 hands renderer methods // the TOKEN as their single argument, so the renderer's old positional // `language` parameter was always undefined. Two consequences, both visible on // every code block the model streams (handleMessageStream.js re-parses the whole // message on every delta): // // * the fence's declared language was ignored, so every block went through // hljs.highlightAuto() — labelling ```js as "cpp" and ```html as // "php-template", relabelling itself as the block grew, and costing a full // multi-grammar detection pass each time; // * `code.text || code` fell through to the token OBJECT for an EMPTY block, // rendering a literal "[object Object]" — which is exactly what a fence // looks like for the moment between "```js" arriving and its first // character. // // Runs the REAL renderer from helpers.js against the REAL vendored marked + // highlight.js (zero drift). let failures = 0; function check(name, cond) { if (cond) console.log('ok - ' + name); else { console.error('FAIL - ' + name); failures++; } } const url = (p) => new URL(p, import.meta.url); const sandbox = vm.createContext({ console, out: {} }); vm.runInContext('var window = this; var self = this;', sandbox); vm.runInContext(fs.readFileSync(url('../src/vendor/marked.umd.min.js'), 'utf8'), sandbox); vm.runInContext(fs.readFileSync(url('../src/vendor/highlight.min.js'), 'utf8'), sandbox); // Pull MARKED_OPTIONS (plus the escape helpers it calls) straight out of helpers.js. const src = fs.readFileSync(url('../src/js/helpers.js'), 'utf8'); // From the syntax-highlight memo (which the renderer calls) through the end of // MARKED_OPTIONS, so the real renderer runs against its real helper. const a = src.indexOf('// ---- Syntax-highlight memo'); const b = src.indexOf('function prepareHistoryForAI('); if (a < 0 || b < 0 || b <= a) throw new Error('could not extract MARKED_OPTIONS'); const e0 = src.indexOf('function htmlEscape('); const e1 = src.indexOf('function generateChatId('); if (e0 < 0 || e1 < 0 || e1 <= e0) throw new Error('could not extract htmlEscape/htmlUnescape'); vm.runInContext(src.slice(e0, e1), sandbox); vm.runInContext(src.slice(a, b), sandbox); vm.runInContext('marked.use(MARKED_OPTIONS);', sandbox); // Render markdown the way the app does: escape first, then parse. function render(md) { sandbox.out.md = md; vm.runInContext('out.html = marked.parse(htmlEscape(out.md));', sandbox); return sandbox.out.html; } const fence = (lang, body) => '```' + lang + '\n' + body + '\n```'; const labelOf = (html) => (html.match(/class="code-language">([^<]*)) || [])[1]; // === The declared language wins =========================================== check('```js is labelled js', labelOf(render(fence('js', 'const x = 1;'))) === 'js'); check('```js gets the js grammar', /class="hljs language-js"/.test(render(fence('js', 'const x = 1;')))); check('```html is labelled html', labelOf(render(fence('html', '