diff --git a/src/renderer/src/components/editor/editor-panel-render-model.ts b/src/renderer/src/components/editor/editor-panel-render-model.ts index 0ccf9053c3f..144264b5753 100644 --- a/src/renderer/src/components/editor/editor-panel-render-model.ts +++ b/src/renderer/src/components/editor/editor-panel-render-model.ts @@ -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 diff --git a/src/renderer/src/components/editor/markdown-rich-mode-eligibility-cache.test.ts b/src/renderer/src/components/editor/markdown-rich-mode-eligibility-cache.test.ts index f94f0a09cd2..6510c11eaf9 100644 --- a/src/renderer/src/components/editor/markdown-rich-mode-eligibility-cache.test.ts +++ b/src/renderer/src/components/editor/markdown-rich-mode-eligibility-cache.test.ts @@ -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: 'edited\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 }) diff --git a/src/renderer/src/components/editor/markdown-rich-mode-eligibility-cache.ts b/src/renderer/src/components/editor/markdown-rich-mode-eligibility-cache.ts index 9d54df171fa..0528f2a7489 100644 --- a/src/renderer/src/components/editor/markdown-rich-mode-eligibility-cache.ts +++ b/src/renderer/src/components/editor/markdown-rich-mode-eligibility-cache.ts @@ -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 } diff --git a/src/renderer/src/components/editor/markdown-rich-mode.ts b/src/renderer/src/components/editor/markdown-rich-mode.ts index 82a86171a3b..8644362ec55 100644 --- a/src/renderer/src/components/editor/markdown-rich-mode.ts +++ b/src/renderer/src/components/editor/markdown-rich-mode.ts @@ -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 }) } }