mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 08:02:35 +00:00
perf: reuse Markdown fence scans across sibling toggles (#26324)
This commit is contained in:
@@ -92,6 +92,14 @@ export function renderDetailsAttributes(attrs: Record<string, unknown> | undefin
|
||||
return attributes.join(' ')
|
||||
}
|
||||
|
||||
export function createDetailsMatcher(content: string): (start: number) => DetailsHtmlBlock | null {
|
||||
let fenceRanges: MarkdownFenceRanges | undefined
|
||||
return (start) =>
|
||||
/^<details\b[^>]*>/i.test(content.slice(start))
|
||||
? matchDetailsHtmlBlock(content, start, (fenceRanges ??= getMarkdownFenceRanges(content)))
|
||||
: null
|
||||
}
|
||||
|
||||
export function matchDetailsHtmlBlock(
|
||||
content: string,
|
||||
start: number,
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { encodeRawMarkdownHtmlForRichEditor } from './raw-markdown-html'
|
||||
import { createRichMarkdownEditorCodec } from './rich-markdown-source-transport'
|
||||
import * as fenceScanner from './markdown-fence-scanner'
|
||||
|
||||
vi.mock('./markdown-fence-scanner', { spy: true })
|
||||
|
||||
const key = '0123456789abcdef0123456789abcdef'
|
||||
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
describe('raw Markdown details fence scanning', () => {
|
||||
it('scans the document once for sibling toggles', () => {
|
||||
const content = Array.from(
|
||||
{ length: 50 },
|
||||
(_, index) => `<details><summary>Section ${index}</summary>\n\nBody ${index}\n\n</details>`
|
||||
).join('\n\n')
|
||||
const scanFences = vi.mocked(fenceScanner.getMarkdownFenceRanges)
|
||||
scanFences.mockClear()
|
||||
|
||||
expect(encodeRawMarkdownHtmlForRichEditor(content, createRichMarkdownEditorCodec(key))).toBe(
|
||||
content
|
||||
)
|
||||
expect(scanFences).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'Plain text\n\nMore plain text',
|
||||
'<details',
|
||||
'<details open\nStill no opening tag terminator',
|
||||
'```html\n<details><summary>Code</summary>\n\nBody\n\n</details>\n```'
|
||||
])('does not scan fences when no opening tag is matched: %j', (content) => {
|
||||
const scanFences = vi.mocked(fenceScanner.getMarkdownFenceRanges)
|
||||
scanFences.mockClear()
|
||||
expect(encodeRawMarkdownHtmlForRichEditor(content, createRichMarkdownEditorCodec(key))).toBe(
|
||||
content
|
||||
)
|
||||
expect(scanFences).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
it('preserves nested and mixed-case editable toggles', () => {
|
||||
const content =
|
||||
'<DeTaIlS open><SuMmArY>Outer</SuMmArY>\n\n<details><summary>Inner</summary>\n\nBody\n\n</details>\n\n</DeTaIlS>'
|
||||
expect(encodeRawMarkdownHtmlForRichEditor(content, createRichMarkdownEditorCodec(key))).toBe(
|
||||
content
|
||||
)
|
||||
})
|
||||
|
||||
it('uses each document’s own fences when the codec is reused', () => {
|
||||
const codec = createRichMarkdownEditorCodec(key)
|
||||
const first = '<details><summary>First</summary>\n\nBody\n\n</details>'
|
||||
const second =
|
||||
'<details><summary>Second</summary>\n\n```html\n</details>\n```\n\nBody\n\n</details>'
|
||||
expect(encodeRawMarkdownHtmlForRichEditor(first, codec)).toBe(first)
|
||||
expect(encodeRawMarkdownHtmlForRichEditor(second, codec)).toBe(
|
||||
codec.transport.create('block-html', second)
|
||||
)
|
||||
expect(encodeRawMarkdownHtmlForRichEditor(first, codec)).toBe(first)
|
||||
})
|
||||
|
||||
it('preserves fenced code before a non-editable sibling', () => {
|
||||
const codec = createRichMarkdownEditorCodec(key)
|
||||
const editable = '<details><summary>Editable</summary>\n\nBody\n\n</details>'
|
||||
const raw = '<details class="custom"><summary>Raw</summary>\n\nBody\n\n</details>'
|
||||
const prefix = '~~~html\n<details><summary>Code</summary></details>\n~~~\n\n'
|
||||
expect(encodeRawMarkdownHtmlForRichEditor(`${prefix}${editable}\n\n${raw}`, codec)).toBe(
|
||||
`${prefix}${editable}\n\n${codec.transport.create('block-html', raw)}`
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,9 @@
|
||||
import { isEditableDetailsHtmlBlock, matchDetailsHtmlBlock } from './details-markdown-html'
|
||||
import { createDetailsMatcher, isEditableDetailsHtmlBlock } from './details-markdown-html'
|
||||
import { formatMarkdownDocLinkBody, parseMarkdownDocLink } from './markdown-doc-links'
|
||||
import { normalizeMarkdownReferenceLinks } from './markdown-reference-link-normalization'
|
||||
import type { RichMarkdownEditorCodec } from './rich-markdown-source-transport'
|
||||
import { isReservedRichMarkdownTransportBody } from './rich-markdown-source-transport'
|
||||
import { matchHtmlSuperscriptLinkSource } from './rich-markdown-html-superscript-link-source'
|
||||
import { getMarkdownFenceRanges } from './markdown-fence-scanner'
|
||||
|
||||
const INLINE_HTML_PATTERN = /^<!--[\s\S]*?-->|^<\/?[A-Za-z][\w.:-]*(?:\s[^<>]*?)?\/?>/
|
||||
|
||||
@@ -46,7 +45,7 @@ export function encodeRawMarkdownHtmlForRichEditor(
|
||||
{ htmlSuperscriptLinks = false }: { htmlSuperscriptLinks?: boolean } = {}
|
||||
): string {
|
||||
const normalizedContent = normalizeMarkdownReferenceLinks(content)
|
||||
const fenceRanges = getMarkdownFenceRanges(normalizedContent)
|
||||
const matchDetails = createDetailsMatcher(normalizedContent)
|
||||
const lastCommentClose = normalizedContent.lastIndexOf('-->')
|
||||
const { transport } = codec
|
||||
let index = 0
|
||||
@@ -123,7 +122,7 @@ export function encodeRawMarkdownHtmlForRichEditor(
|
||||
}
|
||||
|
||||
if (isLineStart) {
|
||||
const detailsHtml = matchDetailsHtmlBlock(normalizedContent, index, fenceRanges)
|
||||
const detailsHtml = matchDetails(index)
|
||||
if (detailsHtml && isEditableDetailsHtmlBlock(detailsHtml)) {
|
||||
// Why: <details>/<summary> is an editable rich-mode node; raw passthrough
|
||||
// would make toggle blocks reopen as inert HTML instead.
|
||||
|
||||
Reference in New Issue
Block a user