mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-13 04:08:29 +01:00
Answer principles round 1: 0 🔴, 7 🟡; mint task ids within the engine limits, fold == into inline-tokens.ts, keep one frame per quote level, pass the list's path once
This commit is contained in:
+1
-1
@@ -5,5 +5,5 @@ export type { JsonValue } from './json-value.ts'
|
||||
export { adfToLosslessMarkdown } from './markdown/emit/adf-to-markdown.ts'
|
||||
export { adfToPortableMarkdown } from './markdown/portable/adf-to-portable-markdown.ts'
|
||||
export { commonMarkToAdf, losslessMarkdownToAdf } from './markdown/parse/markdown-to-adf.ts'
|
||||
export { portableMarkdownToAdf } from './markdown/portable/portable-markdown-to-adf.ts'
|
||||
export { isAdfDocument } from './adf/document.ts'
|
||||
export { portableMarkdownToAdf } from './markdown/portable/portable-markdown-to-adf.ts'
|
||||
|
||||
@@ -203,6 +203,7 @@ test('alternates the marker between adjacent lists of a kind, the marker change
|
||||
const ordered: AdfNode = { attrs: { order: 9 }, content: [{ content: [paragraph({ text: 'x', type: 'text' })], type: 'listItem' }, { content: [paragraph({ text: 'y', type: 'text' }), paragraph({ text: 'z', type: 'text' })], type: 'listItem' }], type: 'orderedList' }
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(ordered, ordered))), '9. x\n10. y\n\n z\n\n9) x\n10) y\n\n z\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(list, ordered, list))), '- x\n\n9. x\n10. y\n\n z\n\n- x\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(list, paragraph({ text: 'y', type: 'text' }), list))), '- x\n\ny\n\n- x\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ attrs: { panelType: 'info' }, content: [list, list], type: 'panel' }))), '!adf:panel info\n- x\n\n* x\n!adf:/panel\n')
|
||||
const nested: AdfNode = { content: [{ content: [list, list], type: 'listItem' }], type: 'bulletList' }
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(nested))), '- - x\n * x\n')
|
||||
|
||||
@@ -141,7 +141,7 @@ function readableBlock(node: AdfNode, path: ConvertErrorPath, depth: number, wri
|
||||
function spellReadableBlock(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing): Result<EmittedBlock> | undefined {
|
||||
const own = writing.flavour === 'portable' ? writing.blocks.spell(node, path, depth, writing) : undefined
|
||||
if (own !== undefined) return own
|
||||
if (node.type === 'blockquote') return tryBlockquote(node, path, depth, writing)
|
||||
if (node.type === 'blockquote') return holdsOnlyAttributes(node, []) ? quotedBlocks(node, path, depth, writing, (body) => alertEscaped(body, writing)) : undefined
|
||||
if (node.type === 'bulletList' || node.type === 'orderedList') return tryList(node, path, depth, writing)
|
||||
if (node.type === 'codeBlock') return readableText(tryCodeBlock(node, writing.flavour))
|
||||
if (node.type === 'heading') return tryHeading(node, path, writing.flavour)
|
||||
@@ -198,15 +198,15 @@ function emitDirectiveBody(node: AdfNode, model: BlockNodeModel, opener: string,
|
||||
return success(directivePair(node, opener, joinBlocks(walk.value.blocks, 'directive'), walk.value.headroom))
|
||||
}
|
||||
|
||||
function tryBlockquote(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing): Result<EmittedBlock> | undefined {
|
||||
if (!holdsOnlyAttributes(node, [])) return undefined
|
||||
return quotedBlocks(node, path, depth, writing, (body) => (writing.flavour === 'portable' && writing.blocks.claimsQuote(body) ? `\\${body}` : body))
|
||||
function alertEscaped(body: string, writing: Writing): string {
|
||||
return writing.flavour === 'portable' && writing.blocks.claimsQuote(body) ? `\\${body}` : body
|
||||
}
|
||||
|
||||
export function quotedBlocks(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing, text: (body: string) => string): Result<EmittedBlock> {
|
||||
// Each quote speller calls it directly, not through a wrapper, so a quote level spends one frame between walks (docs/decisions.md §Nothing recurses unbounded).
|
||||
export function quotedBlocks(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing, bodyText: (body: string) => string): Result<EmittedBlock> {
|
||||
const inner = walkBlocks(nodeContent(node), path, depth + 1, writing)
|
||||
if (!inner.ok) return inner
|
||||
return success(commonMarkText(quoted(text(joinBlocks(inner.value.blocks, 'document'))), inner.value.headroom))
|
||||
return success(commonMarkText(quoted(bodyText(joinBlocks(inner.value.blocks, 'document'))), inner.value.headroom))
|
||||
}
|
||||
|
||||
function tryCodeBlock(node: AdfNode, flavour: WrittenFlavour): string | undefined {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { commonMarkLink, linkHref, markSpelling, spellMarkAttributes } from '../
|
||||
import { escapeUnbalanced, spellDestination, spellLinkTarget } from '../commonmark/link-syntax.ts'
|
||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { flavourClaims } from '../flavour.ts'
|
||||
import { highlightDelimiter } from '../highlight-syntax.ts'
|
||||
import { highlightDelimiter } from '../inline-tokens.ts'
|
||||
import { holdsNullCharacter, trimTrailingSpace } from '../commonmark/grammar.ts'
|
||||
import { identicalMark, isSpellableText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||
import { inlineNodeModel, takenMarkTypes } from '../../adf/inline-nodes.ts'
|
||||
|
||||
@@ -2,10 +2,9 @@ import type { Claims, WrittenFlavour } from '../flavour.ts'
|
||||
import type { InlineSource, InlineToken } from '../inline-tokens.ts'
|
||||
import type { LineContainer } from '../line-container.ts'
|
||||
import { delimiterFlags, isWordCharacter, matchEmphasis, runLength } from '../commonmark/emphasis-matching.ts'
|
||||
import { delimiterRunToken, inlineSource, readInlineToken } from '../inline-tokens.ts'
|
||||
import { delimiterRunToken, highlightFlanking, inlineSource, readInlineToken } from '../inline-tokens.ts'
|
||||
import { escapesLineClaim, type LinePosition } from '../commonmark/grammar.ts'
|
||||
import { flavourClaims } from '../flavour.ts'
|
||||
import { highlightFlanking } from '../highlight-syntax.ts'
|
||||
import { isBareDelimiterRow } from '../pipe-table-syntax.ts'
|
||||
import { opensLinkDefinition } from '../commonmark/link-reference-definitions.ts'
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ export type Flavour = 'commonmark' | 'lossless' | 'portable'
|
||||
|
||||
export type WrittenFlavour = Exclude<Flavour, 'commonmark'>
|
||||
|
||||
// What a reader reads past CommonMark, and so what its writer escapes (docs/decisions.md §Each call names its flavour).
|
||||
// What a reader reads past CommonMark, and so what its writer escapes, but for the portable callouts and task lists that `FlavourBlockReaders` and `FlavourBlockWriters` carry (docs/decisions.md §Each call names its flavour).
|
||||
export type Claims = {
|
||||
carryFence: boolean
|
||||
directives: boolean
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { isWordCharacter } from './commonmark/emphasis-matching.ts'
|
||||
|
||||
export const highlightDelimiter = '=='
|
||||
|
||||
// ー, ー and the kana voicing marks sit outside the kana scripts; Script_Extensions would also take Latin combining marks.
|
||||
const boundingScript = /^[゙゚ーー゙゚\p{Script=Han}\p{Script=Hangul}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Khmer}\p{Script=Lao}\p{Script=Myanmar}\p{Script=Thai}]$/u
|
||||
|
||||
// A delimiter is bounded outside by the code point beyond it, and flanks by the character inside it.
|
||||
export function highlightFlanking(source: string, index: number): { closes: boolean; opens: boolean } {
|
||||
const end = index + highlightDelimiter.length
|
||||
const before = Array.from(source.slice(Math.max(0, index - 2), index)).at(-1) ?? ''
|
||||
const after = Array.from(source.slice(end, end + 2))[0] ?? ''
|
||||
return { closes: flanks(before) && bounds(after, before), opens: flanks(after) && bounds(before, after) }
|
||||
}
|
||||
|
||||
function bounds(outside: string, inside: string): boolean {
|
||||
return !isWordCharacter(outside) || boundingScript.test(outside) || boundingScript.test(inside)
|
||||
}
|
||||
|
||||
function flanks(character: string): boolean {
|
||||
return character !== '' && !/\s/.test(character)
|
||||
}
|
||||
@@ -4,8 +4,7 @@ import type { CodeSpanCloser } from './commonmark/backtick-runs.ts'
|
||||
import { backslashEscape, inlineHtmlConstruct, readBracketedAutolink, readEmailAutolink } from './commonmark/grammar.ts'
|
||||
import { backtickCloser, backtickRun } from './commonmark/backtick-runs.ts'
|
||||
import { claimDirectivePrefix } from './directive/syntax.ts'
|
||||
import { delimiterFlags, runLength } from './commonmark/emphasis-matching.ts'
|
||||
import { highlightDelimiter, highlightFlanking } from './highlight-syntax.ts'
|
||||
import { delimiterFlags, isWordCharacter, runLength } from './commonmark/emphasis-matching.ts'
|
||||
import { readEntityReference } from './commonmark/entity-references.ts'
|
||||
|
||||
// `directive` and `html` carry no `width`: a directive reads by its own grammar, and the reader refuses HTML.
|
||||
@@ -27,6 +26,11 @@ export type InlineSource = {
|
||||
readonly text: string
|
||||
}
|
||||
|
||||
// ー, ー and the kana voicing marks sit outside the kana scripts; Script_Extensions would also take Latin combining marks.
|
||||
const boundingScript = /^[\u3099\u309a\u30fc\uff70\uff9e\uff9f\p{Script=Han}\p{Script=Hangul}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Khmer}\p{Script=Lao}\p{Script=Myanmar}\p{Script=Thai}]$/u
|
||||
|
||||
export const highlightDelimiter = '=='
|
||||
|
||||
export function inlineSource(text: string): InlineSource {
|
||||
return { codeSpanCloser: backtickCloser(text), terminators: new Map(), text }
|
||||
}
|
||||
@@ -102,3 +106,19 @@ function lastStart(inline: InlineSource, terminator: string): number {
|
||||
function text(width: number): InlineToken {
|
||||
return { kind: 'text', width }
|
||||
}
|
||||
|
||||
// A delimiter is bounded outside by the code point beyond it, and flanks by the character inside it.
|
||||
export function highlightFlanking(source: string, index: number): { closes: boolean; opens: boolean } {
|
||||
const end = index + highlightDelimiter.length
|
||||
const before = Array.from(source.slice(Math.max(0, index - 2), index)).at(-1) ?? ''
|
||||
const after = Array.from(source.slice(end, end + 2))[0] ?? ''
|
||||
return { closes: flanks(before) && bounds(after, before), opens: flanks(after) && bounds(before, after) }
|
||||
}
|
||||
|
||||
function bounds(outside: string, inside: string): boolean {
|
||||
return !isWordCharacter(outside) || boundingScript.test(outside) || boundingScript.test(inside)
|
||||
}
|
||||
|
||||
function flanks(character: string): boolean {
|
||||
return character !== '' && !/\s/.test(character)
|
||||
}
|
||||
|
||||
@@ -10,10 +10,9 @@ import { commonMarkLink, linkHref, linkMark } from '../mark-spellings.ts'
|
||||
import { decodeTextEscapes, trimTrailingSpace } from '../commonmark/grammar.ts'
|
||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { flavourClaims } from '../flavour.ts'
|
||||
import { highlightDelimiter } from '../highlight-syntax.ts'
|
||||
import { highlightDelimiter, inlineSource, readInlineToken } from '../inline-tokens.ts'
|
||||
import { identicalMarks, mergeAdjacentText, nodeAttrs, nodeMarks } from '../../adf/document.ts'
|
||||
import { inlineNodeModel, takesMark } from '../../adf/inline-nodes.ts'
|
||||
import { inlineSource, readInlineToken } from '../inline-tokens.ts'
|
||||
import { joinsWhenRead, textBreakName, textBreakSpelling } from '../adjacent-text.ts'
|
||||
import { matchEmphasis } from '../commonmark/emphasis-matching.ts'
|
||||
import { normalizeLabel, readInlineTarget, readLabel } from '../commonmark/link-syntax.ts'
|
||||
|
||||
@@ -19,9 +19,9 @@ import { spellStringAttribute, unsupportedNodeShape } from '../directive/syntax.
|
||||
import { spellsEmpty } from '../directive/empty-keys.ts'
|
||||
import { withinEngineLimits } from '../length-bound.ts'
|
||||
|
||||
// A flavour's own reading of a quote and of a bullet list, which may read to several blocks.
|
||||
// A flavour's own reading of a quote and of a bullet list, which may read to several blocks; `path` is the container's, `index` the list's among its blocks.
|
||||
export type FlavourBlockReaders = {
|
||||
bulletList: (items: readonly Block[][], reading: FlavourReading, path: ConvertErrorPath, depth: number, listIndex: number) => Result<AdfNode[]>
|
||||
bulletList: (items: readonly Block[][], reading: FlavourReading, path: ConvertErrorPath, depth: number, index: number) => Result<AdfNode[]>
|
||||
quote: (blocks: readonly Block[], reading: FlavourReading, path: ConvertErrorPath, depth: number) => Result<AdfNode>
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export function readBlocks(blocks: readonly Block[], reading: Reading, path: Con
|
||||
return positioned(faulted(fault, nodePath), block.position)
|
||||
}
|
||||
if (block.kind === 'bulletList' && reading.flavour === 'portable') {
|
||||
const listed = positioned(reading.blocks.bulletList(block.items, reading, nodePath, depth, content.length), block.position)
|
||||
const listed = positioned(reading.blocks.bulletList(block.items, reading, path, depth, content.length), block.position)
|
||||
if (!listed.ok) return listed
|
||||
for (const node of listed.value) content.push(node)
|
||||
continue
|
||||
|
||||
@@ -20,17 +20,17 @@ export const portableWriting: WrittenAs = { blocks: portableBlockWriters, flavou
|
||||
|
||||
// The portable flavour's nodes, in the shapes the portable reduction leaves them.
|
||||
function spellPortableBlock(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing): Result<EmittedBlock> | undefined {
|
||||
if (node.type === 'panel') return quotedUnder(alertMarker(nodeAttrs(node)['panelType']), node, path, depth, writing)
|
||||
if (node.type === 'panel') return quotedBlocks(node, path, depth, writing, headed(alertMarker(nodeAttrs(node)['panelType'])))
|
||||
if (node.type === 'taskList') return tryTaskList(node, path, depth, writing)
|
||||
if (node.type !== 'expand' && node.type !== 'nestedExpand') return undefined
|
||||
const title = nodeAttrs(node)['title']
|
||||
if (typeof title !== 'string') return quotedUnder(foldedAlertMarker, node, path, depth, writing)
|
||||
if (typeof title !== 'string') return quotedBlocks(node, path, depth, writing, headed(foldedAlertMarker))
|
||||
const line = emitInlineLine([{ text: title, type: 'text' }], 'paragraph', path, writing.flavour)
|
||||
return line.ok ? quotedUnder(`${foldedAlertMarker} ${line.value}`, node, path, depth, writing) : line
|
||||
return line.ok ? quotedBlocks(node, path, depth, writing, headed(`${foldedAlertMarker} ${line.value}`)) : line
|
||||
}
|
||||
|
||||
function quotedUnder(head: string, node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing): Result<EmittedBlock> {
|
||||
return quotedBlocks(node, path, depth, writing, (body) => (body === '' ? head : `${head}\n\n${body}`))
|
||||
function headed(head: string): (body: string) => string {
|
||||
return (body) => (body === '' ? head : `${head}\n\n${body}`)
|
||||
}
|
||||
|
||||
function tryTaskList(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing): Result<EmittedBlock> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
||||
import type { Block } from '../parse/blocks.ts'
|
||||
import type { ConvertError, ConvertErrorPath, ParseError, Result, SourcePosition } from '../../result.ts'
|
||||
import type { FlavourBlockReaders, FlavourReading, Reading } from '../parse/markdown-to-adf.ts'
|
||||
import type { FlavourBlockReaders, FlavourReading } from '../parse/markdown-to-adf.ts'
|
||||
import { containerNode, contentNode, listNode, readBlocks, readDocument, tooDeep } from '../parse/markdown-to-adf.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
import { leadingMarker, readAlertMarker, readTaskMarker } from './conventions.ts'
|
||||
@@ -9,24 +9,25 @@ import { mintTaskIds } from './task-ids.ts'
|
||||
import { parseInlineContent } from '../parse/inline-content.ts'
|
||||
import { pathTo, splitTaskList } from './task-list.ts'
|
||||
import { positioned, success } from '../../result.ts'
|
||||
import { withinEngineLimits } from '../length-bound.ts'
|
||||
|
||||
type Paragraph = Extract<Block, { kind: 'paragraph' }>
|
||||
|
||||
// The paragraph a task marker opens: a task item holds no image, so a lone one there reads as its linked alt text.
|
||||
type TaskParagraph = { kind: 'taskParagraph'; position: SourcePosition; text: string }
|
||||
|
||||
// inExpand: whether an expand holds the blocks, which makes a folded callout a nestedExpand.
|
||||
function blockReaders(inExpand: boolean): FlavourBlockReaders {
|
||||
return { bulletList: bulletBlocks, quote: (blocks, reading, path, depth) => quoteNode(blocks, reading, path, depth, inExpand) }
|
||||
}
|
||||
|
||||
const expandBlockReaders = blockReaders(true)
|
||||
const portableBlockReaders = blockReaders(false)
|
||||
// Inside an expand, a folded callout reads as a nestedExpand.
|
||||
const expandBlockReaders: FlavourBlockReaders = { bulletList: bulletBlocks, quote: quoteReader('nestedExpand') }
|
||||
const portableBlockReaders: FlavourBlockReaders = { bulletList: bulletBlocks, quote: quoteReader('expand') }
|
||||
|
||||
export function portableMarkdownToAdf(markdown: string): Result<AdfDocument, ParseError> {
|
||||
const read = readDocument(markdown, { blocks: portableBlockReaders, flavour: 'portable' })
|
||||
if (read.ok) mintTaskIds(read.value, markdown)
|
||||
return read
|
||||
if (!read.ok) return read
|
||||
const minted = withinEngineLimits(() => {
|
||||
mintTaskIds(read.value, markdown)
|
||||
return read
|
||||
})
|
||||
return positioned(minted, { line: 1, offset: 0 })
|
||||
}
|
||||
|
||||
function markerLed<T extends { length: number }>(block: Block | undefined, read: (text: string) => T | undefined): { marker: T; position: SourcePosition; text: string } | undefined {
|
||||
@@ -47,18 +48,20 @@ function paragraphsOf(position: SourcePosition, ...texts: string[]): Paragraph[]
|
||||
return texts.filter((text) => text !== '').map((text) => ({ kind: 'paragraph', position, text }))
|
||||
}
|
||||
|
||||
function quoteNode(blocks: readonly Block[], reading: FlavourReading, path: ConvertErrorPath, depth: number, inExpand: boolean): Result<AdfNode> {
|
||||
const [first, ...body] = blocks
|
||||
const led = markerLed(first, readAlertMarker)
|
||||
if (led === undefined) return containerNode({ type: 'blockquote' }, blocks, reading, path, depth)
|
||||
const { folded, panelType } = led.marker
|
||||
const { line, rest } = markerLine(led.text)
|
||||
if (!folded) return filledNode({ attrs: { panelType }, type: 'panel' }, readBlocks([...paragraphsOf(led.position, line, rest), ...body], reading, path, depth + 1))
|
||||
const title = parseInlineContent(line, reading.definitions, path, 'paragraph', reading.flavour)
|
||||
if (!title.ok) return title
|
||||
const text = titleText(title.value.nodes)
|
||||
const type = inExpand ? 'nestedExpand' : 'expand'
|
||||
return filledNode(text === '' ? { type } : { attrs: { title: text }, type }, readBlocks([...paragraphsOf(led.position, rest), ...body], { ...reading, blocks: expandBlockReaders }, path, depth + 1))
|
||||
// The closure is the reader rather than a wrapper around one, so a quote level spends one stack frame (docs/decisions.md §Nothing recurses unbounded).
|
||||
function quoteReader(foldedType: 'expand' | 'nestedExpand'): FlavourBlockReaders['quote'] {
|
||||
return (blocks, reading, path, depth) => {
|
||||
const [first, ...body] = blocks
|
||||
const led = markerLed(first, readAlertMarker)
|
||||
if (led === undefined) return containerNode({ type: 'blockquote' }, blocks, reading, path, depth)
|
||||
const { folded, panelType } = led.marker
|
||||
const { line, rest } = markerLine(led.text)
|
||||
if (!folded) return filledNode({ attrs: { panelType }, type: 'panel' }, readBlocks([...paragraphsOf(led.position, line, rest), ...body], reading, path, depth + 1))
|
||||
const title = parseInlineContent(line, reading.definitions, path, 'paragraph', reading.flavour)
|
||||
if (!title.ok) return title
|
||||
const text = titleText(title.value.nodes)
|
||||
return filledNode(text === '' ? { type: foldedType } : { attrs: { title: text }, type: foldedType }, readBlocks([...paragraphsOf(led.position, rest), ...body], { ...reading, blocks: expandBlockReaders }, path, depth + 1))
|
||||
}
|
||||
}
|
||||
|
||||
// docs/decisions.md, A callout title keeps its link targets.
|
||||
@@ -97,10 +100,11 @@ function taskItems(items: readonly Block[][]) {
|
||||
return led
|
||||
}
|
||||
|
||||
function bulletBlocks(items: readonly Block[][], reading: Reading, path: ConvertErrorPath, depth: number, listIndex: number): Result<AdfNode[]> {
|
||||
const tasks = taskBlocks(items, reading, path, depth)
|
||||
function bulletBlocks(items: readonly Block[][], reading: FlavourReading, path: ConvertErrorPath, depth: number, index: number): Result<AdfNode[]> {
|
||||
const listPath = [...path, 'content', index]
|
||||
const tasks = taskBlocks(items, reading, listPath, depth)
|
||||
if (tasks === undefined) {
|
||||
const list = listNode({ type: 'bulletList' }, items, reading, path, depth)
|
||||
const list = listNode({ type: 'bulletList' }, items, reading, listPath, depth)
|
||||
return list.ok ? success([list.value]) : list
|
||||
}
|
||||
if (!tasks.ok) return tasks
|
||||
@@ -110,7 +114,7 @@ function bulletBlocks(items: readonly Block[][], reading: Reading, path: Convert
|
||||
// task-list.test.ts pins that the split keeps the marker; were it lost, the error keeps its pre-split path, since nothing here throws.
|
||||
if (place === undefined) return { error: refusal.error, ok: false }
|
||||
const [blockIndex, ...steps] = place
|
||||
return { error: { ...refusal.error, path: [...path.slice(0, -1), listIndex + blockIndex, ...steps.slice(0, -2), ...refusal.within] }, ok: false }
|
||||
return { error: { ...refusal.error, path: [...path, 'content', index + blockIndex, ...steps.slice(0, -2), ...refusal.within] }, ok: false }
|
||||
}
|
||||
|
||||
// A block a task list refused. Its placeholder holds `marker` as its one child and stands where the block's node would, so the one split that builds the list places it; `within` is the error's path below that node.
|
||||
@@ -119,7 +123,7 @@ type Refusal = { error: ConvertError; marker: AdfNode; within: ConvertErrorPath
|
||||
// Nodes read from task blocks and the first refusal among them; a list is read up to the task holding its refusal.
|
||||
type TaskNodes = { nodes: AdfNode[]; refusal: Refusal | undefined }
|
||||
|
||||
function taskBlocks(items: readonly Block[][], reading: Reading, path: ConvertErrorPath, depth: number): Result<TaskNodes> | undefined {
|
||||
function taskBlocks(items: readonly Block[][], reading: FlavourReading, path: ConvertErrorPath, depth: number): Result<TaskNodes> | undefined {
|
||||
const led = taskItems(items)
|
||||
if (led === undefined) return undefined
|
||||
if (depth + 1 > largestNesting) return tooDeep([...path, 'content', 0])
|
||||
@@ -141,18 +145,19 @@ function taskBlocks(items: readonly Block[][], reading: Reading, path: ConvertEr
|
||||
}
|
||||
|
||||
// A refused block reads to a placeholder. The split tells only paragraphs and task lists from other blocks, so a paragraph that failed to read stays a paragraph.
|
||||
function readTaskBlock(block: Block | TaskParagraph, reading: Reading, path: ConvertErrorPath, depth: number): TaskNodes {
|
||||
function readTaskBlock(block: Block | TaskParagraph, reading: FlavourReading, path: ConvertErrorPath, depth: number): TaskNodes {
|
||||
const nodePath = [...path, 'content', 0]
|
||||
const nested = block.kind === 'bulletList' ? taskBlocks(block.items, reading, nodePath, depth) : undefined
|
||||
if (nested?.ok) return nested.value
|
||||
const read = positioned(nested ?? (block.kind === 'taskParagraph' ? taskParagraph(block, reading, nodePath) : readBlocks([block], reading, path, depth)), block.position)
|
||||
const read = positioned(nested ?? readTaskChild(block, reading, path, depth), block.position)
|
||||
if (read.ok) return { nodes: read.value, refusal: undefined }
|
||||
const marker: AdfNode = { text: '', type: 'text' }
|
||||
const type = block.kind === 'taskParagraph' ? 'paragraph' : nested === undefined ? block.kind : 'taskList'
|
||||
return { nodes: [{ content: [marker], type }], refusal: { error: read.error, marker, within: read.error.path.slice(nodePath.length) } }
|
||||
}
|
||||
|
||||
function taskParagraph(paragraph: TaskParagraph, reading: Reading, path: ConvertErrorPath): Result<AdfNode[]> {
|
||||
const node = contentNode({ type: 'paragraph' }, paragraph.text, reading, path, 'paragraph')
|
||||
function readTaskChild(block: Block | TaskParagraph, reading: FlavourReading, path: ConvertErrorPath, depth: number): Result<AdfNode[]> {
|
||||
if (block.kind !== 'taskParagraph') return readBlocks([block], reading, path, depth)
|
||||
const node = contentNode({ type: 'paragraph' }, block.text, reading, [...path, 'content', 0], 'paragraph')
|
||||
return node.ok ? success([node.value]) : node
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user