From 69c76b5ec715ea2f05d41328b27011e64109e718 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 12 Sep 2026 18:07:23 -0700 Subject: [PATCH] perf: bound rich Markdown comment detection and preservation scans (#20287) --- .../rich-markdown-comment-scan-benchmark.mjs | 104 ++++++++++++++++++ .../editor/markdown-rich-comment-scan.test.ts | 61 ++++++++++ .../components/editor/markdown-rich-mode.ts | 12 +- 3 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 config/scripts/rich-markdown-comment-scan-benchmark.mjs create mode 100644 src/renderer/src/components/editor/markdown-rich-comment-scan.test.ts diff --git a/config/scripts/rich-markdown-comment-scan-benchmark.mjs b/config/scripts/rich-markdown-comment-scan-benchmark.mjs new file mode 100644 index 00000000000..29c52e4b761 --- /dev/null +++ b/config/scripts/rich-markdown-comment-scan-benchmark.mjs @@ -0,0 +1,104 @@ +import assert from 'node:assert/strict' +import { execFileSync } from 'node:child_process' +import { readFileSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { performance } from 'node:perf_hooks' +import { build } from 'esbuild' +import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs' + +const baseline = process.argv[2] ?? '20ab9950654' +const file = 'src/renderer/src/components/editor/markdown-rich-mode.ts' +async function load(contents) { + const result = await build({ + stdin: { contents, loader: 'ts', resolveDir: dirname(resolve(file)) }, + bundle: true, + platform: 'node', + format: 'esm', + write: false, + plugins: [ + { + name: 'cached-round-trip', + setup(bundler) { + bundler.onResolve({ filter: /markdown-round-trip$|^@\/i18n\/i18n$/ }, (args) => ({ + path: args.path, + namespace: 'bench' + })) + bundler.onLoad({ filter: /.*/, namespace: 'bench' }, (args) => ({ + contents: args.path.endsWith('markdown-round-trip') + ? 'export const getRichMarkdownRoundTripOutput = (content) => content' + : 'export const translate = (_key, fallback) => fallback', + loader: 'js' + })) + } + } + ] + }) + return import( + `data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}` + ) +} +const arms = { + before: await load(execFileSync('git', ['show', `${baseline}:${file}`], { encoding: 'utf8' })), + after: await load(readFileSync(file, 'utf8')) +} +const results = [] +for (const [name, content] of [ + ['plain', '# Heading\nOrdinary prose with .'], + ['complete', 'text'], + ['unclosed-1000', '', null], + ['', 'html-or-jsx'], + ['tail', 'html-or-jsx'], + ['">', 'html-or-jsx'], + ['``', null], + ['```html\n\n```', null], + ['\n[a]: https://example.com', 'reference-links'], + ['\n[^a]: footnote', 'reference-links'] + ] as const)('preserves the decision for %j', (content, expected) => { + vi.mocked(getRichMarkdownRoundTripOutput).mockReturnValue(null) + expect(getMarkdownRichModeUnsupportedReason(content)).toBe(expected) + }) + + it('does not pass unclosed comment openers through a comment regex', () => { + const input = '' + ).length + expect(result).toBeNull() + expect(closerSearches).toBeLessThanOrEqual(1) + vi.mocked(getRichMarkdownRoundTripOutput).mockReturnValue( + input.replace('after', 'after') + ) + expect(getMarkdownRichModeUnsupportedReason(input)).toBe('html-or-jsx') + }) +}) diff --git a/src/renderer/src/components/editor/markdown-rich-mode.ts b/src/renderer/src/components/editor/markdown-rich-mode.ts index 142e0cea560..c8c7021e333 100644 --- a/src/renderer/src/components/editor/markdown-rich-mode.ts +++ b/src/renderer/src/components/editor/markdown-rich-mode.ts @@ -48,7 +48,7 @@ const UNSUPPORTED_PATTERNS: UnsupportedMatch[] = [ // Why: the rich editor preserves common embedded markup via placeholder // tokens before parsing, but any HTML shape that still fails round-trip // must fall back instead of risking silent source corruption. - pattern: /<\/?[A-Za-z][\w.:-]*(?:\s[^<>]*)?\/?>|/ + pattern: /<\/?[A-Za-z][\w.:-]*(?:\s[^<>]*)?\/?>/ }, { reason: 'reference-links', @@ -157,6 +157,11 @@ export function getMarkdownRichModeEligibility(params: { } function hasHtmlOrJsx(content: string, pattern: RegExp): boolean { + // A missing closer after the first opener rules out every later opener. + const commentStart = content.indexOf('', commentStart + 4)) { + return true + } for (const match of content.matchAll(new RegExp(pattern, 'g'))) { if (isHtmlOrJsxFragment(match[0])) { return true @@ -166,7 +171,7 @@ function hasHtmlOrJsx(content: string, pattern: RegExp): boolean { } function isHtmlOrJsxFragment(fragment: string): boolean { - if (fragment.startsWith('') for (let index = 0; index < content.length; index++) { if (content.charCodeAt(index) !== 60) { continue @@ -231,7 +237,7 @@ function forEachEmbeddedHtmlFragment( let fragmentEnd: number | null = null if (content.startsWith('', index + 4) + const commentEnd = index + 4 <= lastCommentClose ? content.indexOf('-->', index + 4) : -1 fragmentEnd = commentEnd === -1 ? null : commentEnd + 3 } else { fragmentEnd = getHtmlTagEnd(content, index)