Optimize nested toggle validation by sharing fence ranges across sibling

When validating nested details elements, computing fence ranges once and
reusing across siblings eliminates redundant body rescans. Export
MarkdownFenceRanges type and add precomputedFenceRanges parameter to
matchDetailsHtmlBlock.
This commit is contained in:
Jinjing
2026-08-27 16:59:38 -07:00
parent 1320a2a953
commit f2b25b776f
2 changed files with 32 additions and 5 deletions
@@ -91,6 +91,22 @@ describe('details markdown html', () => {
expect(isEditableHtml(nestedToggles(16))).toBe(true)
})
it('bounds each of many sibling nested toggles independently', () => {
const siblings = (count: number, extra = ''): string =>
Array.from(
{ length: count },
(_, index) =>
`<details class="orca-details"${extra}>\n<summary>sibling ${index}</summary>\n\nBody\n\n</details>`
).join('\n\n')
const wrap = (body: string): string =>
`<details class="orca-details">\n<summary>Outer</summary>\n\n${body}\n\n</details>`
expect(isEditableHtml(wrap(siblings(40)))).toBe(true)
// A single non-editable sibling must still reject, so sharing fence ranges
// cannot make later siblings inherit an earlier sibling's boundaries.
expect(isEditableHtml(wrap(`${siblings(20)}\n\n${siblings(1, ' id="x"')}`))).toBe(false)
})
it('rejects a toggle whose nested toggle is not itself editable', () => {
const block: DetailsHtmlBlock = {
raw: '',
@@ -36,6 +36,10 @@ export type DetailsHtmlBlock = {
inner: string
}
// Fence ranges depend only on the scanned string, so callers scanning one body
// repeatedly compute them once and share them across sibling matches.
export type MarkdownFenceRanges = readonly (readonly [number, number])[]
export type DetailsSummaryHtml = {
attributes: string
content: string
@@ -87,7 +91,7 @@ export function renderDetailsAttributes(attrs: Record<string, unknown> | undefin
return attributes.join(' ')
}
function markdownFenceRanges(content: string): [number, number][] {
function markdownFenceRanges(content: string): MarkdownFenceRanges {
const ranges: [number, number][] = []
let offset = 0
let openFence: { marker: '`' | '~'; length: number; start: number } | null = null
@@ -129,11 +133,15 @@ function markdownFenceRanges(content: string): [number, number][] {
return ranges
}
function isInsideRange(index: number, ranges: [number, number][]): boolean {
function isInsideRange(index: number, ranges: MarkdownFenceRanges): boolean {
return ranges.some(([start, end]) => index >= start && index < end)
}
export function matchDetailsHtmlBlock(content: string, start: number): DetailsHtmlBlock | null {
export function matchDetailsHtmlBlock(
content: string,
start: number,
precomputedFenceRanges?: MarkdownFenceRanges
): DetailsHtmlBlock | null {
const openingMatch = content.slice(start).match(/^<details\b[^>]*>/i)
if (!openingMatch) {
return null
@@ -141,7 +149,7 @@ export function matchDetailsHtmlBlock(content: string, start: number): DetailsHt
const detailsTagPattern = /<\/?details\b[^>]*>/gi
detailsTagPattern.lastIndex = start
const fenceRanges = markdownFenceRanges(content)
const fenceRanges = precomputedFenceRanges ?? markdownFenceRanges(content)
let depth = 0
@@ -270,6 +278,8 @@ const MAX_DETAILS_NESTING_LEVELS = 16
function stripEditableNestedDetails(bodyHtml: string, nestingLevel: number): string | null {
let result = ''
let index = 0
// Why: without sharing this, N sibling toggles rescan the whole body N times.
let fenceRanges: MarkdownFenceRanges | null = null
for (;;) {
const nestedStart = indexOfAsciiIgnoreCase(bodyHtml, '<details', index)
@@ -281,7 +291,8 @@ function stripEditableNestedDetails(bodyHtml: string, nestingLevel: number): str
return null
}
const nested = matchDetailsHtmlBlock(bodyHtml, nestedStart)
fenceRanges ??= markdownFenceRanges(bodyHtml)
const nested = matchDetailsHtmlBlock(bodyHtml, nestedStart, fenceRanges)
if (!nested || !isEditableDetailsHtmlBlock(nested, nestingLevel + 1)) {
return null
}