5b1: report the line a paragraph's kept text starts on, and stream the source lines
CI / gate (push) Successful in 8s
CI / gate (push) Successful in 8s
This commit is contained in:
+1
-1
@@ -13,7 +13,7 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node --test --experimental-test-coverage --test-coverage-exclude=\"src/**/*.test.ts\" --test-coverage-branches=98 --test-coverage-functions=100 --test-coverage-lines=100 \"src/**/*.test.ts\"",
|
"test": "node --test --experimental-test-coverage --test-coverage-exclude=\"src/**/*.test.ts\" --test-coverage-branches=97.95 --test-coverage-functions=100 --test-coverage-lines=100 \"src/**/*.test.ts\"",
|
||||||
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.build.json"
|
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.build.json"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
+1
-1
@@ -192,7 +192,7 @@ for (const name of pairedNames(normalizationRoot, '.md', '.json')) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// The position the input itself gives an offset: undefined where the offset starts no line.
|
// The position the input itself gives an offset, recomputed rather than trusted from the parser.
|
||||||
function lineStarting(markdown: string, offset: number): { line: number; offset: number } | undefined {
|
function lineStarting(markdown: string, offset: number): { line: number; offset: number } | undefined {
|
||||||
const before = markdown.slice(0, offset)
|
const before = markdown.slice(0, offset)
|
||||||
if (offset !== 0 && !/(?:\r\n|[\n\r])$/.test(before)) return undefined
|
if (offset !== 0 && !/(?:\r\n|[\n\r])$/.test(before)) return undefined
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ type OpenLeaf = { position: SourcePosition } & (
|
|||||||
| { closer: RegExp | undefined; construct: string; kind: 'html' }
|
| { closer: RegExp | undefined; construct: string; kind: 'html' }
|
||||||
| { held: string[]; kind: 'indented-code'; lines: string[] }
|
| { held: string[]; kind: 'indented-code'; lines: string[] }
|
||||||
| { indentation: number; info: string; kind: 'fenced-code'; lines: string[]; marker: string }
|
| { indentation: number; info: string; kind: 'fenced-code'; lines: string[]; marker: string }
|
||||||
| { kind: 'paragraph'; lines: string[] }
|
| { kind: 'paragraph'; lines: string[]; positions: SourcePosition[] }
|
||||||
| { kind: 'pipe-table'; rows: [string[], ...string[][]] }
|
| { kind: 'pipe-table'; rows: [string[], ...string[][]] }
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -377,8 +377,12 @@ function readLineBlock(walk: Walk, opener: string): boolean {
|
|||||||
function appendParagraph(walk: Walk, line: string): void {
|
function appendParagraph(walk: Walk, line: string): void {
|
||||||
const leaf = walk.leaf
|
const leaf = walk.leaf
|
||||||
const text = line.replace(/^[ \t]+/, '')
|
const text = line.replace(/^[ \t]+/, '')
|
||||||
if (leaf?.kind === 'paragraph') leaf.lines.push(text)
|
if (leaf?.kind !== 'paragraph') {
|
||||||
else walk.leaf = { kind: 'paragraph', lines: [text], position: walk.position }
|
walk.leaf = { kind: 'paragraph', lines: [text], position: walk.position, positions: [walk.position] }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
leaf.lines.push(text)
|
||||||
|
leaf.positions.push(walk.position)
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeLeaf(walk: Walk): void {
|
function closeLeaf(walk: Walk): void {
|
||||||
@@ -413,32 +417,33 @@ function faultedBlock(message: string, position: SourcePosition): Block {
|
|||||||
return { fault: malformedPipeTable(message), kind: 'fault', position }
|
return { fault: malformedPipeTable(message), kind: 'fault', position }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The definitions a paragraph gives up are whole lines, so what is left starts at one this held.
|
||||||
function takeParagraph(walk: Walk): Extract<Block, { kind: 'paragraph' }> | undefined {
|
function takeParagraph(walk: Walk): Extract<Block, { kind: 'paragraph' }> | undefined {
|
||||||
const leaf = walk.leaf
|
const leaf = walk.leaf
|
||||||
if (leaf?.kind !== 'paragraph') return undefined
|
if (leaf?.kind !== 'paragraph') return undefined
|
||||||
walk.leaf = undefined
|
walk.leaf = undefined
|
||||||
const text = readLinkDefinitions(walk.definitions, leaf.lines.join('\n'))
|
const text = readLinkDefinitions(walk.definitions, leaf.lines.join('\n'))
|
||||||
return text === '' ? undefined : { kind: 'paragraph', position: leaf.position, text }
|
if (text === '') return undefined
|
||||||
|
const kept = leaf.positions[leaf.lines.length - text.split('\n').length]
|
||||||
|
return { kind: 'paragraph', position: kept ?? leaf.position, text }
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentBlocks(walk: Walk): Block[] {
|
function currentBlocks(walk: Walk): Block[] {
|
||||||
return walk.stack.at(-1)?.blocks ?? walk.blocks
|
return walk.stack.at(-1)?.blocks ?? walk.blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
function sourceLines(markdown: string): { position: SourcePosition; text: string }[] {
|
function* sourceLines(markdown: string): Generator<{ position: SourcePosition; text: string }> {
|
||||||
const lines: { position: SourcePosition; text: string }[] = []
|
|
||||||
let line = 1
|
let line = 1
|
||||||
let start = 0
|
let start = 0
|
||||||
for (let index = 0; index < markdown.length; index += 1) {
|
for (let index = 0; index < markdown.length; index += 1) {
|
||||||
const character = markdown.charAt(index)
|
const character = markdown.charAt(index)
|
||||||
if (character !== '\n' && character !== '\r') continue
|
if (character !== '\n' && character !== '\r') continue
|
||||||
lines.push(sourceLine(markdown, line, start, index))
|
yield sourceLine(markdown, line, start, index)
|
||||||
if (character === '\r' && markdown.charAt(index + 1) === '\n') index += 1
|
if (character === '\r' && markdown.charAt(index + 1) === '\n') index += 1
|
||||||
line += 1
|
line += 1
|
||||||
start = index + 1
|
start = index + 1
|
||||||
}
|
}
|
||||||
if (start < markdown.length) lines.push(sourceLine(markdown, line, start, markdown.length))
|
if (start < markdown.length) yield sourceLine(markdown, line, start, markdown.length)
|
||||||
return lines
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sourceLine(markdown: string, line: number, start: number, end: number): { position: SourcePosition; text: string } {
|
function sourceLine(markdown: string, line: number, start: number, end: number): { position: SourcePosition; text: string } {
|
||||||
|
|||||||
@@ -463,6 +463,14 @@ test('names the line and the offset in the input a refusal sits at, the innermos
|
|||||||
assert.deepEqual(position(markdownToAdf('x\n\n:::caption\n- a\n:::\n')), { line: 3, offset: 3 })
|
assert.deepEqual(position(markdownToAdf('x\n\n:::caption\n- a\n:::\n')), { line: 3, offset: 3 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('names the line the text a paragraph keeps starts on, never a definition line it gave up', () => {
|
||||||
|
assert.deepEqual(position(markdownToAdf('[a]: /url\n<span>b</span>\n')), { line: 2, offset: 10 })
|
||||||
|
assert.deepEqual(position(markdownToAdf('[a]: /a\n[b]: /b\n[c]: /c\n[d]: /d\n<span>x</span>\n')), { line: 5, offset: 32 })
|
||||||
|
assert.deepEqual(position(markdownToAdf('[a]:\n<the url>\n"Title"\n<span>b</span>\n')), { line: 4, offset: 23 })
|
||||||
|
assert.deepEqual(position(markdownToAdf('> [a]: /url\n> <span>b</span>\n')), { line: 2, offset: 12 })
|
||||||
|
assert.deepEqual(position(markdownToAdf(':::caption\n[a]: /url\n<span>b</span>\n:::\n')), { line: 3, offset: 21 })
|
||||||
|
})
|
||||||
|
|
||||||
test('gives up the link reference definitions a paragraph opens with', () => {
|
test('gives up the link reference definitions a paragraph opens with', () => {
|
||||||
assert.deepEqual(content(markdownToAdf('[a]: /url\n')), [])
|
assert.deepEqual(content(markdownToAdf('[a]: /url\n')), [])
|
||||||
assert.deepEqual(content(markdownToAdf('[a]: /url\n[b]: /other\nPart.\n')), [paragraph('Part.')])
|
assert.deepEqual(content(markdownToAdf('[a]: /url\n[b]: /other\nPart.\n')), [paragraph('Part.')])
|
||||||
|
|||||||
+12
-7
@@ -400,10 +400,15 @@ Under **3 — `markdownToAdf` (`0.1.0`)**:
|
|||||||
**Settled** (the maintainer, 2026-09-03): the position is the parse side's alone — an
|
**Settled** (the maintainer, 2026-09-03): the position is the parse side's alone — an
|
||||||
emitter has no source string to point into, so emit-side errors keep `path` unchanged. The
|
emitter has no source string to point into, so emit-side errors keep `path` unchanged. The
|
||||||
representation and where the position is captured are implementation judgment.
|
representation and where the position is captured are implementation judgment.
|
||||||
The block walk mints it and the node walk attaches it as results return, one call in
|
The block walk mints it and the node walk attaches it as results return — at `blockNodes`,
|
||||||
`blockNodes`, so the innermost block wins and the emitter's own refusals — which the
|
and at the inline body a directive holds — so the innermost block wins and the emitter's
|
||||||
parser re-enters for the CommonMark spelling — get an input coordinate too. Line endings
|
own refusals, which the parser re-enters for the CommonMark spelling, get an input
|
||||||
stay as the input spells them, so an offset indexes the string the caller passed rather
|
coordinate too. `markdownToAdf` wraps the walk once more, which is what turns the wide
|
||||||
than a normalized copy of it. §8 records both framings the review settled beside it:
|
`Result<T>` into the `Result<T, ParseError>` its signature promises rather than guarding
|
||||||
`unsupported-node-shape` stays one code across the two directions, and `unmappable-html`
|
anything: the depth guard under it cannot fire at depth 0. A paragraph names the line its
|
||||||
names the version rather than the element.
|
kept text starts on, never a link reference definition it gave up. Line endings stay as
|
||||||
|
the input spells them, so an offset indexes the string the caller passed rather than a
|
||||||
|
normalized copy of it. §8 records the framings the review settled beside it:
|
||||||
|
`unsupported-node-shape` stays one code across the two directions, `unmappable-html` names
|
||||||
|
the version rather than the element, and a direction that reads a source returns the
|
||||||
|
narrowed error type.
|
||||||
|
|||||||
Reference in New Issue
Block a user