From e4f80badd4bbdcfd52436ac2fc543bc21d68439b Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Sat, 4 Jul 2026 00:34:47 +0700 Subject: [PATCH] feat(markdown-editor): decode named HTML entities outside code instead of freezing them The `literalAmpersand` extension pre-encoded every `&` so @tiptap/markdown's decodeHtmlEntities netted to a no-op, keeping `<`/`>`/`&`/`"` byte-verbatim. But bare-prose entities in the knowledge corpus are HTML-encoding artifacts from ingestion (e.g. `Time difference > 5 min` meaning `>`), and freezing them as `>` is not what the author wrote. Drop the extension so marked decodes the 4 named entities in prose (`<`->`<`, etc.). Verified scoped and safe: numeric refs (`{`, `(`) and any entity inside code / inline-code are NOT decoded (code content bypasses inline tokenizers), a bare `&` survives as `&` (only valid entities decode), raw `` unaffected, and the identity serializer keeps the decoded `<` from being re-encoded on save. Full corpus (378 real knowledge docs): zero new non-convergence, zero bare-&->& regressions. Live on :8000: `> 5 min` -> `> 5 min` on load+save while numeric/code entities stay put. Co-Authored-By: Claude Opus 4.8 --- .../markdown-editor-content-integrity.test.ts | 9 +++--- .../markdown-editor-extensions.test.ts | 21 +++++++++---- .../markdown-editor/markdown-editor-marked.ts | 30 +++++-------------- 3 files changed, 29 insertions(+), 31 deletions(-) diff --git a/frontend/src/components/shared/markdown-editor/markdown-editor-content-integrity.test.ts b/frontend/src/components/shared/markdown-editor/markdown-editor-content-integrity.test.ts index 1da12c70..532c1584 100644 --- a/frontend/src/components/shared/markdown-editor/markdown-editor-content-integrity.test.ts +++ b/frontend/src/components/shared/markdown-editor/markdown-editor-content-integrity.test.ts @@ -13,8 +13,9 @@ const mulberry32 = (seed: number) => () => { }; // Content atoms that MUST survive load→serialize verbatim: identifiers/dunders, Go-template variables, -// xml-like tags, regex/path backslashes, HTML entities, C++. Whitespace/formatting may reflow; these -// bytes may not disappear or mutate. +// xml-like tags, regex/path backslashes, numeric HTML entities + a bare `&`, C++. Whitespace/formatting may +// reflow; these bytes may not disappear or mutate. (Named entities like `<` decode to `<` by design — +// see markdown-editor-marked.ts — so they are NOT survive-verbatim atoms.) const ATOMS = [ '__init__', '__call__', @@ -32,9 +33,9 @@ const ATOMS = [ 'regex \\.php files', 'glob \\* and \\?', 'escaped \\[ \\| \\+ here', - '<script>', + '(paren)', 'AT&T', - 'x & y', + '2>&1 redirect', 'C++ then C++', '~10% left', ]; diff --git a/frontend/src/components/shared/markdown-editor/markdown-editor-extensions.test.ts b/frontend/src/components/shared/markdown-editor/markdown-editor-extensions.test.ts index e6b5a6e1..3717c7aa 100644 --- a/frontend/src/components/shared/markdown-editor/markdown-editor-extensions.test.ts +++ b/frontend/src/components/shared/markdown-editor/markdown-editor-extensions.test.ts @@ -109,16 +109,27 @@ describe('backslash before punctuation survives — escape tokenizer neutralized }); }); -describe('HTML entities stay literal — decode neutralized (a pentest doc teaching <script> keeps its source)', () => { +describe('named HTML entities decode outside code; numeric refs, bare & and raw survive', () => { it.each([ - 'encode <script> as text', - 'ampersand AT&T and a & b', - 'quote "value" and > alone and < too', + ['encode <script> as text', 'encode