From be9d6749ff787b698245c9d9f14b201e39318223 Mon Sep 17 00:00:00 2001 From: NeoVand Date: Fri, 19 Jun 2026 09:51:07 -0500 Subject: [PATCH] fix(content): stripRichTextMarkup must not corrupt Mermaid node syntax MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regression from the bare-ref strip change: Mermaid reuses {{…}} (hexagon) and [[…]] (subroutine) for node *shapes* — e.g. a story diagram with `T3{{"Middlebox"}}`. The new fallback turned that into `T3"Middlebox"`, invalid Mermaid, so the diagram failed to render ("Diagram unavailable"). Fix: only resolve a bare ref when its id is slug-shaped (REF_ID: lowercase kebab/colon). Non-slug `{{…}}` / `[[…]]` (quotes, spaces, capitals = Mermaid syntax) pass through untouched. This also fixes the *pre-existing* silent loss of those labels — the "Middlebox" hexagon now renders correctly. Added regression tests; verified the Transport category-story diagram renders again, console clean. Co-Authored-By: Claude Fable 5 --- src/lib/utils/text-parser.test.ts | 7 +++++++ src/lib/utils/text-parser.ts | 23 +++++++++++++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/lib/utils/text-parser.test.ts b/src/lib/utils/text-parser.test.ts index d2ad7e1..672c5a6 100644 --- a/src/lib/utils/text-parser.test.ts +++ b/src/lib/utils/text-parser.test.ts @@ -117,4 +117,11 @@ describe('stripRichTextMarkup', () => { expect(out).not.toContain('{{'); expect(out.toLowerCase()).toContain('handshake'); }); + + it('leaves Mermaid node-shape syntax untouched (non-slug ids)', () => { + // Mermaid reuses {{…}} (hexagon) and [[…]] (subroutine) — these are not + // our refs and must survive stripping so diagram labels stay valid. + expect(stripRichTextMarkup('T3{{"Middlebox"}}')).toBe('T3{{"Middlebox"}}'); + expect(stripRichTextMarkup('N[[Subroutine Step]]')).toBe('N[[Subroutine Step]]'); + }); }); diff --git a/src/lib/utils/text-parser.ts b/src/lib/utils/text-parser.ts index a9859a9..0a2ceca 100644 --- a/src/lib/utils/text-parser.ts +++ b/src/lib/utils/text-parser.ts @@ -243,6 +243,10 @@ function bracketFallbackLabel(rawId: string): string { } } +/** A real cross-reference id is a lowercase slug (optionally `prefix:id` or + * `part/chapter`). Used to tell our refs apart from Mermaid shape syntax. */ +const REF_ID = /^[a-z0-9][a-z0-9:/-]*$/; + /** * Strip rich-text atoms ([[…]] / {{…}} / **…**) down to their label * text. For surfaces that render strings as raw DOM text (screen-reader @@ -261,12 +265,19 @@ export function stripRichTextMarkup(raw: string): string { // abbreviation, concept term, …) rather than vanishing — otherwise a bare // `[[tcp]]` in a stripped field (tooltip / aria-label) would silently drop // the word. + // + // IMPORTANT: only resolve when the id is slug-shaped (lowercase kebab/colon). + // Mermaid reuses `{{…}}` (hexagon) and `[[…]]` (subroutine) for node *shapes*, + // e.g. `T3{{"Middlebox"}}`. Those aren't our refs — left untouched so this + // function (which also cleans Mermaid node labels) doesn't corrupt diagrams. return raw - .replace(/\[\[([^\][|]+)(?:\|([^\]]+))?\]\]/g, (_m, id, label) => - label != null ? label : bracketFallbackLabel(id) - ) - .replace(/\{\{([^}{|]+)(?:\|([^}]+))?\}\}/g, (_m, id, label) => - label != null ? label : (getConceptById(id)?.term ?? id) - ) + .replace(/\[\[([^\][|]+)(?:\|([^\]]+))?\]\]/g, (m, id, label) => { + if (label != null) return label; + return REF_ID.test(id) ? bracketFallbackLabel(id) : m; + }) + .replace(/\{\{([^}{|]+)(?:\|([^}]+))?\}\}/g, (m, id, label) => { + if (label != null) return label; + return REF_ID.test(id) ? (getConceptById(id)?.term ?? id) : m; + }) .replace(/\*\*([^*]+)\*\*/g, '$1'); }