Files
orca/src/shared/markdown-code-span-runs.ts
T
OrcaWinandOrcaWin e988b5dd77 fix(mobile): preserve code, quotes and code blocks through rich Markdown edits (#35056)
* fix(mobile): preserve literal code through rich Markdown edits

* fix(mobile): preserve inline code containing backticks

* Preserve adjacent inline-code text when saving mobile Markdown

* Preserve quote paragraphs and blocks in mobile Markdown

* fix(mobile): preserve selected text when creating code blocks

* test(mobile): choose native history shortcut from browser host

* Preserve paragraph-owned mobile code blocks when saving

* Preserve code selected inside mobile headings after saving

* fix(mobile): keep code serialization space-normalized and trim review tests

- Inline code and code blocks keep turning browser-created NBSPs into spaces;
  only the reader-side entity fix is needed for literal code.
- Inline code line breaks save as spaces so table rows and headings stay intact.
- Code blocks keep trimming trailing newlines left by placeholder <br>s.
- Heading- and paragraph-owned code blocks join adjacent inline code runs.
- Empty nested quotes reopen as a plain empty paragraph.
- Trim synthetic and behavior-locking tests.

* fix(mobile): keep quoted text literal when block reading would rewrite it

Quoted list continuations and lazy lines were restructured on save because the list reader drops them; reopen quote blocks only when they save the source or the same bytes as the literal paragraph.

* test(mobile): use template literal for saved code-block markdown

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-29 17:38:16 -07:00

90 lines
2.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export type MarkdownCodeSpanScanner = {
/** Return the closing offset only for a run matching the opener’s exact length, as CommonMark requires. */
findSpanEnd: (index: number) => number | null
}
export function createRawMarkdownCodeSpanScanner(content: string): MarkdownCodeSpanScanner {
const starts: number[] = []
const lengths: number[] = []
const runsByLength = new Map<number, number[]>()
for (const match of content.matchAll(/`+/g)) {
const runLength = match[0].length
const candidates = runsByLength.get(runLength)
if (candidates) {
candidates.push(starts.length)
} else {
runsByLength.set(runLength, [starts.length])
}
starts.push(match.index)
lengths.push(runLength)
}
return createMarkdownBacktickRunLookup(starts, lengths, runsByLength)
}
export function createMarkdownBacktickRunLookup(
starts: number[],
lengths: number[],
runsByLength: Map<number, number[]>,
blocks?: number[],
escaped?: boolean[]
): MarkdownCodeSpanScanner {
function findRunAt(index: number): number {
let low = 0
let high = starts.length - 1
let atOrBefore = -1
while (low <= high) {
const mid = (low + high) >> 1
if (starts[mid] <= index) {
atOrBefore = mid
low = mid + 1
} else {
high = mid - 1
}
}
if (atOrBefore === -1 || index >= starts[atOrBefore] + lengths[atOrBefore]) {
return -1
}
return atOrBefore
}
function findFirstRunFrom(candidates: number[], offset: number): number {
let low = 0
let high = candidates.length - 1
let match = -1
while (low <= high) {
const mid = (low + high) >> 1
if (starts[candidates[mid]] >= offset) {
match = candidates[mid]
high = mid - 1
} else {
low = mid + 1
}
}
return match
}
return {
findSpanEnd(index: number): number | null {
const opener = findRunAt(index)
if (opener === -1) {
return null
}
// Resume within rejected runs; escaped backticks cannot open but marked permits them to close a span.
const openStart = escaped?.[opener] ? Math.max(index, starts[opener] + 1) : index
const openerEnd = starts[opener] + lengths[opener]
if (openStart >= openerEnd) {
return null
}
const candidates = runsByLength.get(openerEnd - openStart)
if (!candidates) {
return null
}
const closer = findFirstRunFrom(candidates, openerEnd)
if (closer === -1 || (blocks && blocks[closer] !== blocks[opener])) {
return null
}
return starts[closer] + lengths[closer]
}
}
}