perf(editor): avoid HTML round trips in source mode

This commit is contained in:
Neil
2026-09-19 00:28:01 -07:00
parent 4d30ed2c29
commit cb0e9336fc
4 changed files with 46 additions and 9 deletions
@@ -133,7 +133,10 @@ export function getEditorPanelRenderModel({
const richModeEligibility = canRenderInlineMarkdown
? getCachedMarkdownRichModeEligibility({
content: inlineMarkdownContent,
sizeOverridden: markdownRichModeSizeOverridden
sizeOverridden: markdownRichModeSizeOverridden,
// Source mode only needs a conservative affordance decision. The full
// HTML round trip is reserved for rich rendering so typing stays cheap.
validateHtmlRoundTrip: mdViewMode === 'rich'
})
: null
const richModeUnsupportedMessage = richModeEligibility?.unsupportedMessage ?? null
@@ -1,5 +1,7 @@
// @vitest-environment happy-dom
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import * as htmlValidation from './markdown-rich-html-validation'
import * as roundTrip from './markdown-round-trip'
import { i18n } from '@/i18n/i18n'
import { getMarkdownRichModeEligibility } from './markdown-rich-mode'
import {
@@ -73,6 +75,21 @@ describe('getCachedMarkdownRichModeEligibility', () => {
)
})
it('supports a conservative source-mode decision without HTML round trips', () => {
const htmlValidationSpy = vi.spyOn(htmlValidation, 'getRichMarkdownHtmlValidationOutput')
const roundTripSpy = vi.spyOn(roundTrip, 'getRichMarkdownRoundTripOutput')
const result = getCachedMarkdownRichModeEligibility({
content: '<span>edited</span>\n',
sizeOverridden: false,
validateHtmlRoundTrip: false
})
expect(result.unsupportedMessage).not.toBeNull()
expect(htmlValidationSpy).not.toHaveBeenCalled()
expect(roundTripSpy).not.toHaveBeenCalled()
})
it('stays correct once the corpus exceeds the cache capacity', () => {
const expected = CORPUS.map(({ content }) =>
getMarkdownRichModeEligibility({ content, sizeOverridden: false })
@@ -8,6 +8,7 @@ import {
type EligibilityCacheEntry = {
content: string
sizeOverridden: boolean
validateHtmlRoundTrip: boolean
decision: MarkdownRichModeEligibilityDecision
}
@@ -36,6 +37,7 @@ const entries: EligibilityCacheEntry[] = []
export function getCachedMarkdownRichModeEligibility(params: {
content: string
sizeOverridden: boolean
validateHtmlRoundTrip?: boolean
}): MarkdownRichModeEligibility {
const decision = getCachedMarkdownRichModeEligibilityDecision(params)
return {
@@ -47,11 +49,16 @@ export function getCachedMarkdownRichModeEligibility(params: {
function getCachedMarkdownRichModeEligibilityDecision(params: {
content: string
sizeOverridden: boolean
validateHtmlRoundTrip?: boolean
}): MarkdownRichModeEligibilityDecision {
const { content, sizeOverridden } = params
const { content, sizeOverridden, validateHtmlRoundTrip = true } = params
for (let index = 0; index < entries.length; index += 1) {
const entry = entries[index]
if (entry.sizeOverridden !== sizeOverridden || entry.content !== content) {
if (
entry.sizeOverridden !== sizeOverridden ||
entry.validateHtmlRoundTrip !== validateHtmlRoundTrip ||
entry.content !== content
) {
continue
}
if (index > 0) {
@@ -61,8 +68,12 @@ function getCachedMarkdownRichModeEligibilityDecision(params: {
return entry.decision
}
const decision = getMarkdownRichModeEligibilityDecision({ content, sizeOverridden })
entries.unshift({ content, sizeOverridden, decision })
const decision = getMarkdownRichModeEligibilityDecision({
content,
sizeOverridden,
validateHtmlRoundTrip
})
entries.unshift({ content, sizeOverridden, validateHtmlRoundTrip, decision })
if (entries.length > MAX_ENTRIES) {
entries.length = MAX_ENTRIES
}
@@ -112,7 +112,8 @@ export function resolveMarkdownRichModeUnsupportedMessage(
}
export function getMarkdownRichModeUnsupportedReason(
content: string
content: string,
{ validateHtmlRoundTrip = true }: { validateHtmlRoundTrip?: boolean } = {}
): MarkdownRichModeUnsupportedReason | null {
// Why: front-matter is handled externally — stripped before the rich editor
// sees the content and displayed as a read-only block. Only the body needs
@@ -145,6 +146,9 @@ export function getMarkdownRichModeUnsupportedReason(
}
if (hasHtml) {
if (!validateHtmlRoundTrip) {
return htmlMatcher!.reason
}
// The source codec recognizes multiline code spans that the cheap scan can misclassify.
const htmlOutput = getRichMarkdownHtmlValidationOutput(body)
if (htmlOutput && preservesEmbeddedHtml(body, htmlOutput)) {
@@ -163,14 +167,16 @@ export function getMarkdownRichModeUnsupportedReason(
export function getMarkdownRichModeEligibilityDecision({
content,
sizeOverridden
sizeOverridden,
validateHtmlRoundTrip = true
}: {
content: string
sizeOverridden: boolean
validateHtmlRoundTrip?: boolean
}): MarkdownRichModeEligibilityDecision {
return {
exceedsSizeLimit: !sizeOverridden && exceedsMarkdownRichModeSizeLimit(content),
unsupportedReason: getMarkdownRichModeUnsupportedReason(content)
unsupportedReason: getMarkdownRichModeUnsupportedReason(content, { validateHtmlRoundTrip })
}
}