Compare commits

..

3 Commits

4 changed files with 20 additions and 12 deletions
+4 -4
View File
@@ -38,7 +38,7 @@ type LineAttempt = { fallback: NodeRange | 'opening-link'; line?: undefined } |
type LineFallbacks = { carried: Set<number>; flavour: Flavour; openingLinkAsDirective: boolean } type LineFallbacks = { carried: Set<number>; flavour: Flavour; openingLinkAsDirective: boolean }
export type PlainLineFallback = { kind: 'claimed-line'; line: number; text: string } | { kind: 'opening-link' } | { kind: 'unspellable-run'; run: MarkRun; runs: MarkRun[] } export type PlainLineFallback = { kind: 'claimed-line'; line: number; text: string } | { kind: 'opening-link' } | { kind: 'unspellable-run'; runs: [MarkRun, ...MarkRun[]] }
export function emitInlineLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, flavour: Flavour): Result<string> { export function emitInlineLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, flavour: Flavour): Result<string> {
const emitted = emitLine(nodes, container, path, flavour) const emitted = emitLine(nodes, container, path, flavour)
@@ -120,7 +120,7 @@ function lineSegments(nodes: readonly AdfNode[], container: LineContainer, path:
function attemptLine(segments: readonly InlineSegment[], container: LineContainer, path: ConvertErrorPath, flavour: Flavour): Result<LineAttempt> { function attemptLine(segments: readonly InlineSegment[], container: LineContainer, path: ConvertErrorPath, flavour: Flavour): Result<LineAttempt> {
const verdict = lineVerdict(segments, container, flavour) const verdict = lineVerdict(segments, container, flavour)
if (verdict.kind === 'opening-link') return success({ fallback: 'opening-link' }) if (verdict.kind === 'opening-link') return success({ fallback: 'opening-link' })
if (verdict.kind === 'unspellable-run') return success({ fallback: verdict.run }) if (verdict.kind === 'unspellable-run') return success({ fallback: verdict.runs[0] })
if (verdict.kind === 'claimed-line') return failure('unspellable-line-start', `block parsing would claim the emitted line ${JSON.stringify(verdict.text)}`, path) if (verdict.kind === 'claimed-line') return failure('unspellable-line-start', `block parsing would claim the emitted line ${JSON.stringify(verdict.text)}`, path)
return success({ line: verdict.text }) return success({ line: verdict.text })
} }
@@ -129,8 +129,8 @@ function attemptLine(segments: readonly InlineSegment[], container: LineContaine
function lineVerdict(segments: readonly InlineSegment[], container: LineContainer, flavour: Flavour): PlainLineFallback | { kind: 'line'; text: string } { function lineVerdict(segments: readonly InlineSegment[], container: LineContainer, flavour: Flavour): PlainLineFallback | { kind: 'line'; text: string } {
const assembled = assembleInlineLine(segments, container, flavour) const assembled = assembleInlineLine(segments, container, flavour)
if (assembled.openingLinkAsDirective) return { kind: 'opening-link' } if (assembled.openingLinkAsDirective) return { kind: 'opening-link' }
const [run] = assembled.unspellableRuns const [run, ...others] = assembled.unspellableRuns
if (run !== undefined) return { kind: 'unspellable-run', run, runs: assembled.unspellableRuns } if (run !== undefined) return { kind: 'unspellable-run', runs: [run, ...others] }
const lines = assembled.line.split('\n') const lines = assembled.line.split('\n')
const claimed = container === 'paragraph' ? lines.findIndex((single, index) => claimsLine(single, index === 0 ? 'first' : 'later')) : -1 const claimed = container === 'paragraph' ? lines.findIndex((single, index) => claimsLine(single, index === 0 ? 'first' : 'later')) : -1
return claimed === -1 ? { kind: 'line', text: assembled.line } : { kind: 'claimed-line', line: claimed, text: lines[claimed] ?? '' } return claimed === -1 ? { kind: 'line', text: assembled.line } : { kind: 'claimed-line', line: claimed, text: lines[claimed] ?? '' }
@@ -125,6 +125,7 @@ test('spells a task list as a bullet list whose items lead with their state', ()
assert.equal(plain(node('taskList', {}, task('DONE', 'Write the spec'), nested, task('TODO', 'Ship it'))), '- [x] Write the spec\n - [ ] Review\n- [ ] Ship it\n') assert.equal(plain(node('taskList', {}, task('DONE', 'Write the spec'), nested, task('TODO', 'Ship it'))), '- [x] Write the spec\n - [ ] Review\n- [ ] Ship it\n')
assert.equal(plain(node('taskList', {}, nested, task('DONE', ''), said('Stray'))), '- - [ ] Review\n- [x]\n- Stray\n') assert.equal(plain(node('taskList', {}, nested, task('DONE', ''), said('Stray'))), '- - [ ] Review\n- [x]\n- Stray\n')
assert.equal(plain(node('taskList', {}, node('taskList', {}), task('DONE', 'a'), said('b'), node('taskList', {}, task('TODO', 'c')))), '- [x] a\n- b\n - [ ] c\n') assert.equal(plain(node('taskList', {}, node('taskList', {}), task('DONE', 'a'), said('b'), node('taskList', {}, task('TODO', 'c')))), '- [x] a\n- b\n - [ ] c\n')
assert.equal(plain(node('taskList', {}, paragraph(), task('DONE', 'a'))), '- [x] a\n')
assert.equal(plain(bulletList(item(said('x'))), node('taskList', {}, task('DONE', 'a'), node('taskList', {}, task('TODO', 'c')))), '- x\n- [x] a\n - [ ] c\n') assert.equal(plain(bulletList(item(said('x'))), node('taskList', {}, task('DONE', 'a'), node('taskList', {}, task('TODO', 'c')))), '- x\n- [x] a\n - [ ] c\n')
assert.equal(plain(node('taskList', {}), task('TODO', 'Loose')), 'Loose\n') assert.equal(plain(node('taskList', {}), task('TODO', 'Loose')), 'Loose\n')
const blockTask = node('blockTaskItem', { state: 'DONE' }, said('First.'), said('Second.')) const blockTask = node('blockTaskItem', { state: 'DONE' }, said('First.'), said('Second.'))
+11 -8
View File
@@ -274,26 +274,29 @@ function blankedLines(code: readonly AdfNode[]): AdfNode[] {
return blanked === '' ? [] : [text(blanked)] return blanked === '' ? [] : [text(blanked)]
} }
// A task list opening with a task and holding tasks and task lists alone keeps its spelling, a list nesting in the task before it; any other keeps its markers as text. // A task list opening with a task and holding tasks and task lists alone keeps its spelling, a list nesting in the task before it; any other keeps its markers as text. A child reducing to nothing counts for neither.
function reduceTaskList(node: AdfNode, reduction: Reduction): Result<AdfNode[]> { function reduceTaskList(node: AdfNode, reduction: Reduction): Result<AdfNode[]> {
const children = nodeContent(node) const kept: { blocks: AdfNode[]; child: AdfNode }[] = []
const regular = isTask(children[0]) && children.every((child) => isTask(child) || child.type === 'taskList') for (const [index, child] of nodeContent(node).entries()) {
const tasks: AdfNode[] = []
let nested: AdfNode[] = []
for (const [index, child] of children.entries()) {
const at = childReduction(reduction, index) const at = childReduction(reduction, index)
const reduced = isTask(child) ? reduceTask(child, at) : reduceStanding(child, at) const reduced = isTask(child) ? reduceTask(child, at) : reduceStanding(child, at)
if (!reduced.ok) return reduced if (!reduced.ok) return reduced
if (reduced.value.length > 0) kept.push({ blocks: reduced.value, child })
}
const regular = isTask(kept[0]?.child) && kept.every(({ child }) => isTask(child) || child.type === 'taskList')
const tasks: AdfNode[] = []
let nested: AdfNode[] = []
for (const { blocks, child } of kept) {
if (!regular) { if (!regular) {
const standsAlone = !isTask(child) && child.type !== 'taskList' const standsAlone = !isTask(child) && child.type !== 'taskList'
for (const block of standsAlone ? [{ content: reduced.value, type: 'listItem' }] : reduced.value) tasks.push(block) for (const block of standsAlone ? [{ content: blocks, type: 'listItem' }] : blocks) tasks.push(block)
continue continue
} }
if (isTask(child)) { if (isTask(child)) {
nestIn(tasks, nested) nestIn(tasks, nested)
nested = [] nested = []
} }
for (const block of reduced.value) (isTask(child) ? tasks : nested).push(block) for (const block of blocks) (isTask(child) ? tasks : nested).push(block)
} }
nestIn(tasks, nested) nestIn(tasks, nested)
if (regular) return success([{ content: tasks, type: 'taskList' }]) if (regular) return success([{ content: tasks, type: 'taskList' }])
+4
View File
@@ -34,6 +34,10 @@
paragraph of 2000 × `un` plus `**-r**`: each run its flanking cannot spell re-emits the whole line paragraph of 2000 × `un` plus `**-r**`: each run its flanking cannot spell re-emits the whole line
before riding the carry, quadratic in the runs (Goal 9), and the plain reduction's before riding the carry, quadratic in the runs (Goal 9), and the plain reduction's
`spellableLine` drops one mark per re-emit the same way. Make both linear. `spellableLine` drops one mark per re-emit the same way. Make both linear.
- **42 — Trim a text leaf's trailing blanks in linear time.** `plain-inline.ts`'s `leafEdges` finds
the trail with an unanchored `/[ \t]*$/`, quadratic in a run of blanks inside one leaf: a
paragraph of `a`, 80 000 spaces, `b` takes 6.5 s in `adfToPlainMarkdown` (Goal 9). Scan backward,
as the expand title's trim does.
- **34 — Read emphasis flanking by the whole character beside an astral symbol.** Check whether - **34 — Read emphasis flanking by the whole character beside an astral symbol.** Check whether
`line-escaping.ts`'s `charAt` and the parser's flanking read one UTF-16 unit beside an astral `line-escaping.ts`'s `charAt` and the parser's flanking read one UTF-16 unit beside an astral
symbol — a lone surrogate is neither punctuation nor symbol, where CommonMark reads `😀` as symbol — a lone surrogate is neither punctuation nor symbol, where CommonMark reads `😀` as