3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-12 11:48:29 +01:00

Pair highlights over the inline reader's pieces before building nodes

This commit is contained in:
2026-11-09 00:54:13 +01:00
committed by Lillem4n
parent 673121ac8a
commit ef6599e5b2
2 changed files with 28 additions and 42 deletions
+28 -34
View File
@@ -50,7 +50,7 @@ type Nodes = { kind: 'nodes'; nodes: Inline[] }
type Resolved = ImageLink | Nodes
type HighlightDelimiter = { closes: boolean; holder: AdfNode; index: number; opens: boolean; position: number; stretch: number }
type HighlightDelimiter = { closes: boolean; index: number; marks: readonly AdfMark[]; opens: boolean; position: number; stretch: number }
type HighlightPair = { closer: number; opener: number }
@@ -514,17 +514,19 @@ function resolveNodes(pieces: readonly Piece[], scan: Scan, highlights: boolean)
return nodes.ok ? success(mergeReadText(nodes.value.flat())) : nodes
}
// `nodes[index]` holds piece `index`'s nodes, which each step below rewrites in place.
// The result's `[index]` holds piece `index`'s nodes.
function markedNodes(pieces: readonly Piece[], scan: Scan, highlights: boolean): Result<Inline[][]> {
const nodes = pieces.map(pieceNodes)
const runs = delimiterRuns(pieces)
const pairings = matchEmphasis(runs)
for (const { closer, closerOffset, opener, openerOffset, used } of pairings) record(scan.shared, { at: opener.offset + openerOffset, closer: closer.offset + closerOffset, kind: 'emphasis', width: used })
const marks = pairingMarks(pieces, pairings)
if (marks === undefined) return failure('unsupported-node-shape', ownMarksInMark, scan.shared.path)
const nodes = pieces.map(pieceNodes)
writeUnpaired(nodes, runs, pairings)
if (!markPairings(pieces, nodes, pairings)) return failure('unsupported-node-shape', ownMarksInMark, scan.shared.path)
const paired = highlights ? pairedHighlights(pieces, nodes) : []
markHighlights(pieces, nodes, paired)
return success(nodes)
const paired = highlights ? pairedHighlights(pieces, nodes, marks) : []
const marked = nodes.map((held, index) => (marks[index] ?? []).reduceRight((inner, mark) => applyMark(inner, mark), held))
markHighlights(marked, paired)
return success(marked)
}
// A text break and a node whose marks are its own are walls: the text on either side joins only its own side.
@@ -545,13 +547,13 @@ function mergeReadText(items: readonly Inline[]): Inline[] {
}
// The image arm is an image inside an image's description or a link's text, which reads as its plain alt text.
// A highlight delimiter holds an empty text node until it pairs, so the emphasis around it marks it.
// A highlight delimiter is the text `==` until it pairs, so the emphasis around it marks it.
function pieceNodes(piece: Piece): Inline[] {
switch (piece.kind) {
case 'ownMarks':
return [piece]
case 'highlight':
return [{ text: '', type: 'text' }]
return [{ text: highlightDelimiter, type: 'text' }]
case 'image':
return piece.alt === '' ? [] : [{ text: piece.alt, type: 'text' }]
case 'imageLink':
@@ -589,17 +591,17 @@ function writeUnpaired(nodes: Inline[][], runs: readonly Run[], pairings: readon
}
}
// Pairings nest, so one walk holding the open ones on a stack marks each piece once, the marks outermost first (spec/flavour.md, Marks).
function markPairings(pieces: readonly Piece[], nodes: Inline[][], pairings: readonly Pairing[]): boolean {
// Pairings nest, so one walk holding the open ones on a stack gives each piece its marks, outermost first (spec/flavour.md, Marks).
// It is `undefined` where an own-marks node stands inside a mark.
function pairingMarks(pieces: readonly Piece[], pairings: readonly Pairing[]): AdfMark[][] | undefined {
const opening = pairingsByOpener(pairings)
const open: { closer: number; marks: AdfMark[] }[] = []
const marks: AdfMark[][] = []
for (const [index, piece] of pieces.entries()) {
while (open.at(-1)?.closer === index) open.pop()
const marks = open.at(-1)?.marks
if (marks !== undefined) {
if (piece.kind === 'ownMarks') return false
nodes[index] = marks.reduceRight((marked, mark) => applyMark(marked, mark), nodes[index] ?? [])
}
const here = open.at(-1)?.marks ?? []
if (here.length > 0 && piece.kind === 'ownMarks') return undefined
marks.push(here)
for (const pairing of opening.get(index) ?? []) {
const type = markType(pairing.opener.character, pairing.used)
// Each type once, so a piece's marks stay at most three however deep the pairings nest.
@@ -607,7 +609,7 @@ function markPairings(pieces: readonly Piece[], nodes: Inline[][], pairings: rea
open.push({ closer: pairing.closer.index, marks: [...outer, { type }] })
}
}
return true
return marks
}
// `matchEmphasis` lists a run's pairings innermost first, so each list here is reversed to stack the outermost first.
@@ -622,20 +624,14 @@ function pairingsByOpener(pairings: readonly Pairing[]): Map<number, Pairing[]>
return opening
}
function highlightDelimiters(pieces: readonly Piece[], nodes: readonly Inline[][]): HighlightDelimiter[] {
function highlightDelimiters(pieces: readonly Piece[], nodes: readonly Inline[][], marks: readonly AdfMark[][]): HighlightDelimiter[] {
const found: HighlightDelimiter[] = []
// A stretch is a run of text no other inline node breaks.
let stretch = 0
let position = 0
for (const [index, piece] of pieces.entries()) {
const held = adfNodes(nodes[index] ?? [])
const [holder] = held
if (piece.kind === 'highlight' && holder !== undefined) {
found.push({ closes: piece.closes, holder, index, opens: piece.opens, position, stretch })
position += highlightDelimiter.length
continue
}
for (const node of held) {
if (piece.kind === 'highlight') found.push({ closes: piece.closes, index, marks: marks[index] ?? [], opens: piece.opens, position, stretch })
for (const node of adfNodes(nodes[index] ?? [])) {
if (node.type === 'text') position += node.text?.length ?? 0
else stretch += 1
}
@@ -644,8 +640,8 @@ function highlightDelimiters(pieces: readonly Piece[], nodes: readonly Inline[][
}
// Each opener pairs with the next closer at least one character after it, and only when both sit in one stretch under the same marks.
function pairedHighlights(pieces: readonly Piece[], nodes: readonly Inline[][]): HighlightPair[] {
const found = highlightDelimiters(pieces, nodes)
function pairedHighlights(pieces: readonly Piece[], nodes: readonly Inline[][], marks: readonly AdfMark[][]): HighlightPair[] {
const found = highlightDelimiters(pieces, nodes, marks)
const paired: HighlightPair[] = []
let closer = 0
let resume = 0
@@ -658,18 +654,15 @@ function pairedHighlights(pieces: readonly Piece[], nodes: readonly Inline[][]):
candidate = found[closer]
}
if (candidate === undefined) break
if (candidate.stretch !== opener.stretch || !identicalMarks(nodeMarks(opener.holder), nodeMarks(candidate.holder))) continue
if (candidate.stretch !== opener.stretch || !identicalMarks(opener.marks, candidate.marks)) continue
paired.push({ closer: candidate.index, opener: opener.index })
resume = candidate.position + highlightDelimiter.length
}
return paired
}
// A node holds one highlight, and a rebuilt node would lose its attributes.
function markHighlights(pieces: readonly Piece[], nodes: Inline[][], paired: readonly HighlightPair[]): void {
for (const [index, piece] of pieces.entries()) {
if (piece.kind === 'highlight') nodes[index] = adfNodes(nodes[index] ?? []).map((holder) => ({ ...holder, text: highlightDelimiter }))
}
// A pair's delimiters leave, and the nodes between them take the highlight.
function markHighlights(nodes: Inline[][], paired: readonly HighlightPair[]): void {
for (const { closer, opener } of paired) {
nodes[opener] = []
nodes[closer] = []
@@ -677,6 +670,7 @@ function markHighlights(pieces: readonly Piece[], nodes: Inline[][], paired: rea
}
}
// A node holds one highlight, and a rebuilt node would lose its attributes.
function highlighted(node: Inline): Inline {
if (!isNode(node)) return node
const marks = nodeMarks(node)
-8
View File
@@ -13,7 +13,6 @@
| ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 153 | 0.4.0 | | question | **Decide whether `src/markdown/` root may import `emit/`, `parse/` or `portable/`, and hold the answer in `.oxlintrc.json` and §The source parts by ADF and format.** | 1 | 2 | 2 | 3 | 2 | code health by 3 | 26.2 |
| 154 | 0.4.0 | | | **Pair highlights over the inline reader's pieces by their offsets before building nodes, so no placeholder text node stands in for a delimiter.** | 2 | 1 | 2 | 3 | 4 | code health by 4 | 17.4 |
| 155 | 0.4.0 | | | **Have `closeBracket` return the link, image or literal bracket it builds, and apply it to the scan in one caller.** | 2 | 1 | 2 | 3 | 4 | code health by 4 | 17.4 |
| 162 | 0.4.0 | | | **Pin each rule of the emphasis sweep with a hand-worked line, so every mutant that lets a pair fall where the one-run-per-pass reference keeps it fails a test.** | 2 | 1 | 1 | 2 | 4 | code health by 4 | 17.4 |
| 175 | 0.4.0 | | | **Delete the guards in `tryTaskList` that `reduceToPortable` makes unreachable, once a test shows `adfToPortableMarkdown` reaches none of them.** | 1 | 1 | 1 | 1 | 2 | code health by 2 | 17.4 |
@@ -74,13 +73,6 @@ a paragraph opening with a spellable `!adf:link` (§Nothing recurses unbounded).
`src/markdown/**` bans only `html/`; banning `emit/`, `parse/` and `portable/` for the root's own
files, with its `ci.sh` probe, extends the decision.
### 154. Pair highlights over the inline reader's pieces by their offsets before building nodes, so no placeholder text node stands in for a delimiter.
A comprehension seat found `src/markdown/inline-content.ts` `resolveNodes` / `pairedHighlights`
hard: an empty text node stands in for each `==` until pairing, then a cursor walk overwrites it.
The pairing reads each `==`'s position, stretch and marks off the built nodes; give the pieces what
it reads, so it can run on them first. Make that place easier to read.
### 155. Have `closeBracket` return the link, image or literal bracket it builds, and apply it to the scan in one caller.
A comprehension seat found `src/markdown/inline-content.ts` `closeBracket` hard: it writes