diff --git a/src/renderer/src/components/editor/rich-markdown-code-block-markdown.ts b/src/renderer/src/components/editor/rich-markdown-code-block-markdown.ts new file mode 100644 index 00000000000..d03acf924fb --- /dev/null +++ b/src/renderer/src/components/editor/rich-markdown-code-block-markdown.ts @@ -0,0 +1,38 @@ +import type { JSONContent } from '@tiptap/core' + +type MarkdownCodeBlockRenderHelpers = { + renderChildren: (nodes: JSONContent[]) => string +} + +function longestFenceRun(text: string, character: '`' | '~'): number { + let longest = 0 + let current = 0 + for (const value of text) { + if (value === character) { + current += 1 + longest = Math.max(longest, current) + } else { + current = 0 + } + } + return longest +} + +function chooseFence(text: string): { character: '`' | '~'; length: number } { + const backtickLength = Math.max(3, longestFenceRun(text, '`') + 1) + const tildeLength = Math.max(3, longestFenceRun(text, '~') + 1) + return tildeLength < backtickLength + ? { character: '~', length: tildeLength } + : { character: '`', length: backtickLength } +} + +export function renderRichMarkdownCodeBlock( + node: JSONContent, + helpers: MarkdownCodeBlockRenderHelpers +): string { + const language = typeof node.attrs?.language === 'string' ? node.attrs.language : '' + const body = helpers.renderChildren(node.content ?? []) + const fence = chooseFence(body) + const marker = fence.character.repeat(fence.length) + return [`${marker}${language}`, body, marker].join('\n') +} diff --git a/src/renderer/src/components/editor/rich-markdown-extensions.ts b/src/renderer/src/components/editor/rich-markdown-extensions.ts index f6e69f00862..147b8b8fa14 100644 --- a/src/renderer/src/components/editor/rich-markdown-extensions.ts +++ b/src/renderer/src/components/editor/rich-markdown-extensions.ts @@ -45,6 +45,7 @@ import { RichMarkdownInlineMath } from './rich-markdown-inline-math' import { RichMarkdownCodeBlockLowlight } from './rich-markdown-lowlight' import { RichMarkdownTaskList } from './rich-markdown-task-list' import { createCachedLowlight } from './rich-markdown-lowlight-cache' +import { renderRichMarkdownCodeBlock } from './rich-markdown-code-block-markdown' const lowlight = createCachedLowlight(createLowlight(common)) @@ -83,6 +84,7 @@ export function createRichMarkdownExtensions({ RichMarkdownParagraph, RichMarkdownCode, RichMarkdownCodeBlockLowlight.extend({ + renderMarkdown: renderRichMarkdownCodeBlock, addNodeView() { // Why: RichMarkdownCodeBlock never reads getPos, so it must not re-render // just because earlier edits shifted this block's document position. diff --git a/src/renderer/src/components/editor/rich-markdown-list-item.test.ts b/src/renderer/src/components/editor/rich-markdown-list-item.test.ts index 0e3fd7e5113..bf4aa92cc92 100644 --- a/src/renderer/src/components/editor/rich-markdown-list-item.test.ts +++ b/src/renderer/src/components/editor/rich-markdown-list-item.test.ts @@ -60,4 +60,16 @@ describe('ordered list continuation serialization', () => { } expect(current).toBe('1. one\n ```ts\n const value = 1\n ```\n2. two') }) + + it('preserves meaningful indentation inside an ordered-item fence', () => { + const source = '1. one\n\n ```\n indented\n ```\n2. two' + expect(roundTrip(source)).toBe('1. one\n ```\n indented\n ```\n2. two') + }) + + it('chooses a non-colliding fence for fence-shaped code content', () => { + const source = '~~~\n```\n~~~' + const canonical = roundTrip(source) + expect(canonical).toBe(source) + expect(roundTrip(canonical)).toBe(canonical) + }) })