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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user