mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-13 04:08:29 +01:00
Answer architecture round 2: portableSequence walks a stack in linear time, the * escape and the alternation are worded and named for what they do, item 51 names no count, and item 94 files the link-definition join
This commit is contained in:
@@ -20,13 +20,13 @@ import { tryPipeTable } from './pipe-table.ts'
|
||||
type BlockContainer = 'directive' | 'document' | 'list-item'
|
||||
// The usual delimiter, then the one a list beside another of its kind takes.
|
||||
type Delimiters = readonly [string, string]
|
||||
// CommonMark starts a new list where the marker changes, so a list beside one of its kind takes `alternateText`.
|
||||
type SpelledBlock = { spelling: 'commonmark' | 'directive'; text: string } | { alternateText: string; spelling: 'list'; text: string }
|
||||
// headroom: the nesting levels the deepest node below may still spend before `largestNesting` refuses it.
|
||||
type EmittedBlock = SpelledBlock & { headroom: number }
|
||||
type KeptSpelling = { block: EmittedBlock | undefined; depth: number }
|
||||
type PlacedBlock = SpelledBlock & { node: AdfNode }
|
||||
type PlacedBlocks = { blocks: readonly PlacedBlock[]; headroom: number }
|
||||
// CommonMark starts a new list where the marker changes, so a list beside one of its kind takes `alternateText`.
|
||||
type SpelledBlock = { spelling: 'commonmark' | 'directive'; text: string } | { alternateText: string; spelling: 'list'; text: string }
|
||||
// Keyed by reference: only a caller building one object per position (the parse, the portable reduction) passes one; a consumer's document may share a node.
|
||||
export type SpellingMemo = Map<AdfNode, KeptSpelling>
|
||||
type WalkedItem = { node: AdfNode; walk: PlacedBlocks }
|
||||
@@ -34,9 +34,9 @@ export type Writing = { flavour: WrittenFlavour; memo: SpellingMemo | undefined
|
||||
|
||||
const bulletDelimiters: Delimiters = ['-', '*']
|
||||
export const largestListMarker = 999999999
|
||||
const orderedDelimiters: Delimiters = ['.', ')']
|
||||
// Bare because tryList admits no item carrying attributes, marks or text.
|
||||
const listItemOpener = spellDirectiveOpener('listItem', undefined, '')
|
||||
const orderedDelimiters: Delimiters = ['.', ')']
|
||||
|
||||
export function adfToLosslessMarkdown(document: AdfDocument): Result<string> {
|
||||
return writeMarkdown(document, 'lossless')
|
||||
@@ -76,14 +76,15 @@ function joinBlocks(blocks: readonly PlacedBlock[], container: BlockContainer):
|
||||
for (const [index, block] of blocks.entries()) {
|
||||
const previous = blocks[index - 1]
|
||||
if (previous !== undefined) text += separationBetween(previous, block, container)
|
||||
alternated = !alternated && previous?.spelling === 'list' && block.spelling === 'list' && isOrdered(previous.node) === isOrdered(block.node)
|
||||
alternated = !alternated && previous !== undefined && sharesMarker(previous, block)
|
||||
text += alternated && block.spelling === 'list' ? block.alternateText : block.text
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
function isOrdered(node: AdfNode): boolean {
|
||||
return node.type === 'orderedList'
|
||||
// A task list is spelled as a bullet list.
|
||||
function sharesMarker(previous: PlacedBlock, next: PlacedBlock): boolean {
|
||||
return previous.spelling === 'list' && next.spelling === 'list' && (previous.node.type === 'orderedList') === (next.node.type === 'orderedList')
|
||||
}
|
||||
|
||||
function separationBetween(previous: PlacedBlock, next: PlacedBlock, container: BlockContainer): string {
|
||||
@@ -208,7 +209,7 @@ function taskBlocks(task: AdfNode, path: ConvertErrorPath, depth: number, writin
|
||||
const walk = walkBlocks(nodeContent(task), path, depth, writing)
|
||||
if (!walk.ok) return walk
|
||||
const [first, ...rest] = walk.value.blocks
|
||||
const blocks = first?.node.type === 'paragraph' ? [{ ...first, text: `${marker} ${first.text}` }, ...rest] : [markerBlock, ...walk.value.blocks]
|
||||
const blocks = first?.node.type === 'paragraph' && first.spelling !== 'list' ? [{ ...first, text: `${marker} ${first.text}` }, ...rest] : [markerBlock, ...walk.value.blocks]
|
||||
return success({ blocks, headroom: walk.value.headroom })
|
||||
}
|
||||
|
||||
@@ -366,7 +367,7 @@ function tryListItemLines(inner: string, marker: string): string | undefined {
|
||||
const lines = body.map((line, index) => (index === 0 ? `${marker}${line}` : line === '' ? '' : `${indent}${line}`))
|
||||
const first = lines[0] ?? ''
|
||||
if (!isThematicBreak(first)) return lines.join('\n')
|
||||
// A first line of `*` alone beside the `* ` marker is text, since a rule is spelled `---`: a backslash keeps it from reading as one.
|
||||
// Only text opens an item with `*`, a rule being spelled `---`; where `* ` before it reads as a rule, a backslash keeps it text.
|
||||
return inner.startsWith('*') ? [`${marker}\\${first.slice(marker.length)}`, ...lines.slice(1)].join('\n') : undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -116,16 +116,18 @@ function concatenated(results: readonly Result<AdfNode[]>[]): Result<AdfNode[]>
|
||||
|
||||
// A list still taking the directive form gives way to its items' blocks.
|
||||
function portableSequence(blocks: readonly AdfNode[], reduction: Reduction): Result<AdfNode[]> {
|
||||
let sequence = blocks.filter((block) => block.type !== 'paragraph' || nodeContent(block).length > 0)
|
||||
for (let index = 0; index < sequence.length; index += 1) {
|
||||
const listed = sequence[index]
|
||||
if (listed === undefined || (listed.type !== 'bulletList' && listed.type !== 'orderedList')) continue
|
||||
const block = numberedPastMarkers(listed)
|
||||
const spelled = block === listed && commonMarkSpelling(block, reduction.path, reduction.depth, { flavour: 'portable', memo: reduction.memo })?.ok === true
|
||||
if (spelled) continue
|
||||
sequence = [...sequence.slice(0, index), ...(block === listed ? nodeContent(block).flatMap(nodeContent) : [block]), ...sequence.slice(index + 1)]
|
||||
// The replacement's first block takes this index, and may itself take the directive form.
|
||||
index -= 1
|
||||
const pending = blocks.filter((block) => block.type !== 'paragraph' || nodeContent(block).length > 0).reverse()
|
||||
const sequence: AdfNode[] = []
|
||||
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
|
||||
const block = numberedPastMarkers(next)
|
||||
const listed = block.type === 'bulletList' || block.type === 'orderedList'
|
||||
if (!listed || (block === next && commonMarkSpelling(block, reduction.path, reduction.depth, { flavour: 'portable', memo: reduction.memo })?.ok === true)) {
|
||||
sequence.push(block)
|
||||
continue
|
||||
}
|
||||
// Read next, since a replacement may take the directive form itself.
|
||||
const replacement = block === next ? nodeContent(block).flatMap(nodeContent) : [block]
|
||||
for (const node of replacement.reverse()) pending.push(node)
|
||||
}
|
||||
return success(sequence)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user