From 211dc054009cdded1a5d284ec80906ff75e7364e Mon Sep 17 00:00:00 2001 From: orca-bug-scan-bot Date: Fri, 26 Jun 2026 21:08:47 -0700 Subject: [PATCH] fix: address pr-bug-scan validated finding from #6315 Protect fenced/inline code spans from the HTML stripTags pass in normalizeMobileMarkdownPreviewHtml; blocks stripTags(next) at mobile-markdown-preview-html.ts:95 from deleting <...> inside code. --- mobile/src/components/MobileMarkdown.test.ts | 12 +++++++ .../mobile-markdown-preview-html.ts | 35 +++++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/mobile/src/components/MobileMarkdown.test.ts b/mobile/src/components/MobileMarkdown.test.ts index cfc7a0df410..ff8a9e6a09a 100644 --- a/mobile/src/components/MobileMarkdown.test.ts +++ b/mobile/src/components/MobileMarkdown.test.ts @@ -49,4 +49,16 @@ describe('parseMobileMarkdown', () => { normalizeMobileMarkdownPreviewHtml('

Use &lt;button&gt;

') ).toBe('Use `<button>`') }) + + it('preserves angle brackets and generics inside fenced and inline code', () => { + expect(normalizeMobileMarkdownPreviewHtml('```html\n
x
\n```')).toBe( + '```html\n
x
\n```' + ) + expect(normalizeMobileMarkdownPreviewHtml('```ts\nconst x: Array = []\n```')).toBe( + '```ts\nconst x: Array = []\n```' + ) + expect(normalizeMobileMarkdownPreviewHtml('Use `Array` here')).toBe( + 'Use `Array` here' + ) + }) }) diff --git a/mobile/src/components/mobile-markdown-preview-html.ts b/mobile/src/components/mobile-markdown-preview-html.ts index 14965a4ff82..68ee0ed6339 100644 --- a/mobile/src/components/mobile-markdown-preview-html.ts +++ b/mobile/src/components/mobile-markdown-preview-html.ts @@ -74,8 +74,39 @@ function normalizeInlineHtml(value: string): string { }) } +// Why: the HTML cleanup pipeline below strips every `<...>` run and decodes +// entities. Markdown code (fenced ```...``` and inline `...`) is literal source +// the user wants verbatim — e.g. `
x
` or `Array` — so it must +// not pass through stripTags. Protect those spans with placeholder tokens (no +// `<`, `>`, or entity text, so the pipeline leaves them untouched) and restore +// them after. Fenced blocks are matched before inline spans so a fence's +// backticks can't be misread as inline code. +const CODE_PLACEHOLDER_PREFIX = 'ORCA_MD_CODE_' +const CODE_PLACEHOLDER_SUFFIX = '' + +function protectMarkdownCode(content: string): { protectedText: string; codeSpans: string[] } { + const codeSpans: string[] = [] + const store = (match: string): string => { + const token = `${CODE_PLACEHOLDER_PREFIX}${codeSpans.length}${CODE_PLACEHOLDER_SUFFIX}` + codeSpans.push(match) + return token + } + const protectedText = content + .replace(/```[\s\S]*?```/g, store) + .replace(/`[^`\n]+`/g, store) + return { protectedText, codeSpans } +} + +function restoreMarkdownCode(value: string, codeSpans: string[]): string { + return value.replace( + new RegExp(`${CODE_PLACEHOLDER_PREFIX}(\\d+)${CODE_PLACEHOLDER_SUFFIX}`, 'g'), + (_token, index) => codeSpans[Number(index)] ?? _token + ) +} + export function normalizeMobileMarkdownPreviewHtml(content: string): string { - let next = content.replace(/\r\n?/g, '\n') + const { protectedText, codeSpans } = protectMarkdownCode(content.replace(/\r\n?/g, '\n')) + let next = protectedText // Why: repository Markdown often uses small HTML islands for centered README // headers and badges. Preview mode should read like Markdown, while Source @@ -94,5 +125,5 @@ export function normalizeMobileMarkdownPreviewHtml(content: string): string { next = normalizeInlineHtml(next) next = stripTags(next) - return restoreEscapedHtmlEntities(next) + return restoreMarkdownCode(restoreEscapedHtmlEntities(next), codeSpans) }