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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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 {
|
||||
const before = markdown.slice(0, offset)
|
||||
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' }
|
||||
| { held: string[]; kind: 'indented-code'; lines: 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[][]] }
|
||||
)
|
||||
|
||||
@@ -377,8 +377,12 @@ function readLineBlock(walk: Walk, opener: string): boolean {
|
||||
function appendParagraph(walk: Walk, line: string): void {
|
||||
const leaf = walk.leaf
|
||||
const text = line.replace(/^[ \t]+/, '')
|
||||
if (leaf?.kind === 'paragraph') leaf.lines.push(text)
|
||||
else walk.leaf = { kind: 'paragraph', lines: [text], position: walk.position }
|
||||
if (leaf?.kind !== 'paragraph') {
|
||||
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 {
|
||||
@@ -413,32 +417,33 @@ function faultedBlock(message: string, position: SourcePosition): Block {
|
||||
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 {
|
||||
const leaf = walk.leaf
|
||||
if (leaf?.kind !== 'paragraph') return undefined
|
||||
walk.leaf = undefined
|
||||
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[] {
|
||||
return walk.stack.at(-1)?.blocks ?? walk.blocks
|
||||
}
|
||||
|
||||
function sourceLines(markdown: string): { position: SourcePosition; text: string }[] {
|
||||
const lines: { position: SourcePosition; text: string }[] = []
|
||||
function* sourceLines(markdown: string): Generator<{ position: SourcePosition; text: string }> {
|
||||
let line = 1
|
||||
let start = 0
|
||||
for (let index = 0; index < markdown.length; index += 1) {
|
||||
const character = markdown.charAt(index)
|
||||
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
|
||||
line += 1
|
||||
start = index + 1
|
||||
}
|
||||
if (start < markdown.length) lines.push(sourceLine(markdown, line, start, markdown.length))
|
||||
return lines
|
||||
if (start < markdown.length) yield sourceLine(markdown, line, start, markdown.length)
|
||||
}
|
||||
|
||||
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 })
|
||||
})
|
||||
|
||||
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', () => {
|
||||
assert.deepEqual(content(markdownToAdf('[a]: /url\n')), [])
|
||||
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
|
||||
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.
|
||||
The block walk mints it and the node walk attaches it as results return, one call in
|
||||
`blockNodes`, so the innermost block wins and the emitter's own refusals — which the
|
||||
parser re-enters for the CommonMark spelling — get an input coordinate too. 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 both framings the review settled beside it:
|
||||
`unsupported-node-shape` stays one code across the two directions, and `unmappable-html`
|
||||
names the version rather than the element.
|
||||
The block walk mints it and the node walk attaches it as results return — at `blockNodes`,
|
||||
and at the inline body a directive holds — so the innermost block wins and the emitter's
|
||||
own refusals, which the parser re-enters for the CommonMark spelling, get an input
|
||||
coordinate too. `markdownToAdf` wraps the walk once more, which is what turns the wide
|
||||
`Result<T>` into the `Result<T, ParseError>` its signature promises rather than guarding
|
||||
anything: the depth guard under it cannot fire at depth 0. A paragraph names the line its
|
||||
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