mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
fix(editor): skip fenced blocks when scanning for code spans
A backtick inside a fenced block paired with a later prose backtick, producing a span that covered everything between and hid any <details> block in that stretch from the tokenizer.
This commit is contained in:
committed by
Neil
parent
4dbc6ffcae
commit
65ceead55b
@@ -0,0 +1,91 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { findDetailsBlockStart } from './details-markdown-html'
|
||||
import { markdownCodeSpanRanges, markdownFenceRanges } from './markdown-scan-ranges'
|
||||
|
||||
describe('markdownCodeSpanRanges', () => {
|
||||
it('reports a span whose backtick run closes on an equal run', () => {
|
||||
expect(markdownCodeSpanRanges('a `<details>` b')).toEqual([[2, 13]])
|
||||
})
|
||||
|
||||
it('keeps a span that contains a blank line', () => {
|
||||
expect(markdownCodeSpanRanges('text `a\n\nb` tail')).toEqual([[5, 11]])
|
||||
})
|
||||
|
||||
it('reports no span for backticks inside a fenced block', () => {
|
||||
expect(markdownCodeSpanRanges(['```', 'a `b` c', '```'].join('\n'))).toEqual([])
|
||||
})
|
||||
|
||||
it('does not pair a fence backtick with a later prose backtick', () => {
|
||||
const content = ['~~~', 'x = `abc', '~~~', '', 'prose with `code` here'].join('\n')
|
||||
const spans = markdownCodeSpanRanges(content)
|
||||
|
||||
expect(spans).toEqual([[29, 35]])
|
||||
expect(content.slice(29, 35)).toBe('`code`')
|
||||
})
|
||||
|
||||
it('does not run a span through a four-backtick fence closer', () => {
|
||||
const content = ['```', 'code', '````', '', 'tail', '', '```', 'more', '```'].join('\n')
|
||||
|
||||
expect(markdownCodeSpanRanges(content)).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('findDetailsBlockStart with fenced content', () => {
|
||||
it('finds a details block after a fence whose content holds an unpaired backtick', () => {
|
||||
const content = [
|
||||
'~~~',
|
||||
'x = `abc',
|
||||
'~~~',
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>S</summary>',
|
||||
'',
|
||||
'body',
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
'prose with `code` here'
|
||||
].join('\n')
|
||||
|
||||
expect(findDetailsBlockStart(content)).toBe(content.indexOf('<details>'))
|
||||
})
|
||||
|
||||
it('finds a details block between a four-backtick closer and a later fence', () => {
|
||||
const content = [
|
||||
'```',
|
||||
'code',
|
||||
'````',
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>S</summary>',
|
||||
'',
|
||||
'x',
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
'```',
|
||||
'more',
|
||||
'```'
|
||||
].join('\n')
|
||||
|
||||
expect(findDetailsBlockStart(content)).toBe(content.indexOf('<details>'))
|
||||
})
|
||||
|
||||
it('skips a details mention inside a code span that spans a blank line', () => {
|
||||
expect(findDetailsBlockStart('text `a\n\n<details>b` tail')).toBe(-1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('markdownFenceRanges', () => {
|
||||
it('covers the opening delimiter, content, and closing delimiter', () => {
|
||||
const content = ['```', 'x', '```', ''].join('\n')
|
||||
|
||||
expect(markdownFenceRanges(content)).toEqual([[0, 10]])
|
||||
})
|
||||
|
||||
it('runs an unterminated fence to the end of the content', () => {
|
||||
const content = ['```', 'x', 'y'].join('\n')
|
||||
|
||||
expect(markdownFenceRanges(content)).toEqual([[0, content.length]])
|
||||
})
|
||||
})
|
||||
@@ -46,14 +46,35 @@ export function isInsideRange(index: number, ranges: MarkdownFenceRanges): boole
|
||||
return ranges.some(([start, end]) => index >= start && index < end)
|
||||
}
|
||||
|
||||
function rangeEndAt(index: number, ranges: MarkdownFenceRanges): number {
|
||||
for (const [start, end] of ranges) {
|
||||
if (index >= start && index < end) {
|
||||
return end
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// CommonMark code spans: a backtick run only closes on a run of the same
|
||||
// length, so `` `<details>` `` is one span even though `<details>` alone
|
||||
// isn't. Mirrors the tick-matching in raw-markdown-html.ts's inline scan.
|
||||
export function markdownCodeSpanRanges(content: string): MarkdownFenceRanges {
|
||||
// Fenced blocks are skipped whole — their delimiters and content are not
|
||||
// inline code, and scanning them pairs a fence backtick with a later prose
|
||||
// one, swallowing everything between. Blank lines are not span boundaries.
|
||||
export function markdownCodeSpanRanges(
|
||||
content: string,
|
||||
fenceRanges: MarkdownFenceRanges = markdownFenceRanges(content)
|
||||
): MarkdownFenceRanges {
|
||||
const ranges: [number, number][] = []
|
||||
let index = 0
|
||||
|
||||
while (index < content.length) {
|
||||
const fenceEnd = rangeEndAt(index, fenceRanges)
|
||||
if (fenceEnd !== -1) {
|
||||
index = fenceEnd
|
||||
continue
|
||||
}
|
||||
|
||||
if (content[index] !== '`') {
|
||||
index += 1
|
||||
continue
|
||||
@@ -72,6 +93,10 @@ export function markdownCodeSpanRanges(content: string): MarkdownFenceRanges {
|
||||
if (candidate === -1) {
|
||||
break
|
||||
}
|
||||
if (rangeEndAt(candidate, fenceRanges) !== -1) {
|
||||
searchFrom = candidate + 1
|
||||
continue
|
||||
}
|
||||
if (
|
||||
(candidate === 0 || content[candidate - 1] !== '`') &&
|
||||
content[candidate + tickCount] !== '`'
|
||||
|
||||
Reference in New Issue
Block a user