From 273950d1f740feac30d0d4a61e89fdb8b103c69a Mon Sep 17 00:00:00 2001 From: xiaoheiai4719-collab Date: Tue, 6 Oct 2026 00:18:12 -0700 Subject: [PATCH] fix: preserve literal Markdown inside inline code --- .../app/utils/markdown-parser.util.spec.ts | 75 +++++++++++++++++++ .../src/app/utils/markdown-parser.util.ts | 14 +++- 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts b/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts index 0fd04defa..0ffbdbb87 100644 --- a/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +++ b/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts @@ -68,3 +68,78 @@ describe('markdown-parser.util verification & check lines', () => { expect(html).toContain('status is ok'); }); }); + +describe('renderMarkdownToHtml inline code', () => { + for (const literal of ['**literal**', '__name__', 'a*b*c', '_literal_', '***literal***', '___literal___', '~~literal~~']) { + it(`should preserve ${literal} inside inline code`, () => { + expect(renderMarkdownToHtml('`' + literal + '`')) + .toBe(`
${literal}
`); + }); + } + + it('should format surrounding text while preserving multiple code spans', () => { + expect(renderMarkdownToHtml('**bold** `**first**` _italic_ `__second__` ~~removed~~')) + .toBe('
bold **first** italic __second__ removed
'); + }); + + for (const [delimiter, open, close] of [ + ['***', '', ''], + ['___', '', ''], + ['**', '', ''], + ['__', '', ''], + ['*', '', ''], + ['_', '', ''], + ['~~', '', ''], + ]) { + it(`should preserve ${delimiter} formatting around inline code`, () => { + expect(renderMarkdownToHtml(delimiter + 'before `a*b_c~d` after' + delimiter)) + .toBe(`
${open}before a*b_c~d after${close}
`); + }); + } + + it('should not pair formatting delimiters across code spans', () => { + expect(renderMarkdownToHtml('`a*` plain `*b` and `a_` plain `_b` and `a~~` plain `~~b`')) + .toBe('
a* plain *b and a_ plain _b and a~~ plain ~~b
'); + }); + + it('should handle adjacent code spans', () => { + expect(renderMarkdownToHtml('`**first**``__second__`')) + .toBe('
**first**__second__
'); + }); + + it('should keep HTML escaped inside and outside inline code', () => { + expect(renderMarkdownToHtml('outside ` & **literal**`')) + .toBe('
<b>outside</b> <img src=x onerror="alert(1)"> & </code> **literal**
'); + }); + + it('should leave empty and unmatched backticks unchanged', () => { + expect(renderMarkdownToHtml('``')).toBe('
``
'); + expect(renderMarkdownToHtml('`unfinished **bold**')) + .toBe('
`unfinished bold
'); + }); + + it('should preserve underscore word boundaries next to code', () => { + expect(renderMarkdownToHtml('`code`_after_ and _before_`code` and _`code`_')) + .toBe('
codeafter and beforecode and code
'); + }); + + it('should not pair outside delimiters with code contents', () => { + expect(renderMarkdownToHtml('*before `x*` and `*x` after*')) + .toBe('
before x* and *x after
'); + }); + + it('should preserve placeholder-like text and replacement patterns literally', () => { + expect(renderMarkdownToHtml(' ` $& $1 $$ **literal**`')) + .toBe('
<inline-code-0> <inline-code-1> $& $1 $$ **literal**
'); + }); + + it('should protect code in headings, lists, and blockquotes', () => { + expect(renderMarkdownToHtml('# `**heading**`\n- `__item__`\n> `~~quote~~`')) + .toBe('

**heading**

\n
    \n
  • __item__
  • \n
\n
~~quote~~
'); + }); + + it('should leave fenced code formatting unchanged', () => { + expect(renderMarkdownToHtml('```text\n`**literal**` &\n```')) + .toBe('
`**literal**` <b>&</b>
'); + }); +}); diff --git a/apps/showcase_ui/src/app/utils/markdown-parser.util.ts b/apps/showcase_ui/src/app/utils/markdown-parser.util.ts index 0b3e377a2..030430d4c 100644 --- a/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +++ b/apps/showcase_ui/src/app/utils/markdown-parser.util.ts @@ -213,9 +213,14 @@ export function extractCheckerResult(text: string): CheckerResult | null { */ function formatInlineMarkdown(text: string): string { if (!text) return ''; - return text - // Inline code first (so asterisks inside code aren't formatted) - .replace(/`([^`]+)`/g, '$1') + const codeSpans: string[] = []; + const formatted = text + // Input is already HTML-escaped, so these placeholders cannot collide with user text. + // Restore code after formatting so its contents remain literal. + .replace(/`([^`]+)`/g, (_, code: string) => { + codeSpans.push(code); + return ``; + }) // Bold + Italic .replace(/\*\*\*([^*]+)\*\*\*/g, '$1') .replace(/___([^_]+)___/g, '$1') @@ -227,6 +232,9 @@ function formatInlineMarkdown(text: string): string { .replace(/\b_([^_]+)_\b/g, '$1') // Strikethrough .replace(/~~([^~]+)~~/g, '$1'); + + return formatted.replace(//g, (_, index: string) => + `${codeSpans[Number(index)]}`); } /**