10a - adfToPlainMarkdown's ADF→ADF reduction #128

Merged
lilleman merged 20 commits from 10a into main 2026-09-25 19:42:37 +02:00
2 changed files with 32 additions and 12 deletions
Showing only changes of commit 37abc1ddc1 - Show all commits
+25 -7
View File
@@ -1,7 +1,7 @@
import type { AdfMark, AdfNode } from '../../adf/document.ts'
import type { InlineNodeModel } from '../../adf/inline-nodes.ts'
import type { LineContainer } from '../line-container.ts'
import { assembleInlineLine, isSyntax, type InlineEscaping, type InlineSegment, type NodeRange } from './line-escaping.ts'
import { assembleInlineLine, isSyntax, type InlineEscaping, type InlineSegment, type MarkRun, type NodeRange } from './line-escaping.ts'
import { carriedInline } from '../opaque-carry.ts'
import { claimsLine, holdsNullCharacter, trimTrailingSpace } from '../commonmark/grammar.ts'
import { commonMarkLink, linkHref, markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
@@ -35,6 +35,8 @@ type LineAttempt = { fallback: NodeRange | 'opening-link'; line?: undefined } |
type LineFallbacks = { carried: Set<number>; openingLinkAsDirective: boolean }
export type PlainLineFallback = { kind: 'claimed-line'; line: number } | { kind: 'opening-link' } | { kind: 'unspellable-run'; run: MarkRun }
export function emitInlineLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<string> {
const emitted = emitLine(nodes, container, path)
if (!emitted.ok) return emitted
@@ -47,6 +49,17 @@ export function openingLinkTakesDirective(nodes: readonly AdfNode[], path: Conve
return success(emitted.value.openingLinkAsDirective)
}
export function plainLineFallback(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<PlainLineFallback | undefined> {
const emission = lineSegments(nodes, container, path, { carried: new Set(), openingLinkAsDirective: false })
if (!emission.ok) return emission
if (emission.value.carry !== undefined) return failure('unsupported-node-shape', 'an inline node on a plain line has no spelling but the carry', path)
const assembled = assembleInlineLine(emission.value.segments, container)
if (assembled.openingLinkAsDirective) return success({ kind: 'opening-link' })
if (assembled.unspellableRun !== undefined) return success({ kind: 'unspellable-run', run: assembled.unspellableRun })
const claimed = claimedLine(assembled.line, container)
return success(claimed === undefined ? undefined : { kind: 'claimed-line', line: claimed })
}
export function tryPipeCell(nodes: readonly AdfNode[], path: ConvertErrorPath): string | undefined {
const emitted = emitLine(nodes, 'table-cell', path)
if (!emitted.ok) return undefined
@@ -108,14 +121,19 @@ function attemptLine(segments: readonly InlineSegment[], container: LineContaine
const assembled = assembleInlineLine(segments, container)
if (assembled.openingLinkAsDirective) return success({ fallback: 'opening-link' })
if (assembled.unspellableRun !== undefined) return success({ fallback: assembled.unspellableRun })
for (const [index, single] of assembled.line.split('\n').entries()) {
if (container === 'paragraph' && claimsLine(single, index === 0 ? 'first' : 'later')) {
return failure('unspellable-line-start', `block parsing would claim the emitted line ${JSON.stringify(single)}`, path)
}
const claimed = claimedLine(assembled.line, container)
if (claimed !== undefined) {
return failure('unspellable-line-start', `block parsing would claim the emitted line ${JSON.stringify(assembled.line.split('\n')[claimed])}`, path)
}
return success({ line: assembled.line })
}
function claimedLine(line: string, container: LineContainer): number | undefined {
if (container !== 'paragraph') return undefined
const index = line.split('\n').findIndex((single, index) => claimsLine(single, index === 0 ? 'first' : 'later'))
return index === -1 ? undefined : index
}
// spec/flavour.md, Inline nodes.
function carryStrippedWhitespace(segments: readonly InlineSegment[]): InlineSegment[] {
const carried: InlineSegment[] = []
@@ -275,9 +293,9 @@ function emitEmphasis(nodes: readonly AdfNode[], spelling: string, depth: number
const carried = carryStrippedWhitespace(inner.value.segments)
return success({
segments: [
{ emphasis: 'open', escaping: 'none', nodes: range, text: spelling },
{ emphasis: 'open', escaping: 'none', nodes: { ...range, depth }, text: spelling },
...carried,
{ emphasis: 'close', escaping: 'none', nodes: range, text: spelling },
{ emphasis: 'close', escaping: 'none', nodes: { ...range, depth }, text: spelling },
],
})
}
+7 -5
View File
@@ -13,12 +13,14 @@ export type InlineEscaping = 'backslash' | 'bracketed' | 'bracketed-link-target'
export type NodeRange = { first: number; last: number }
export type MarkRun = NodeRange & { depth: number }
export type InlineSegment =
| { emphasis: EmphasisRole; escaping: 'none'; nodes: NodeRange; text: string }
| { emphasis: EmphasisRole; escaping: 'none'; nodes: MarkRun; text: string }
| { emphasis?: undefined; escaping: 'none'; nodes: NodeRange; text: string }
| { emphasis?: undefined; escaping: InlineEscaping; nodes?: undefined; text: string }
export type AssembledLine = { line: string; openingLinkAsDirective?: true; unspellableRun: NodeRange | undefined }
export type AssembledLine = { line: string; openingLinkAsDirective?: true; unspellableRun: MarkRun | undefined }
type ScanLine = { position: LinePosition; start: number; text: string }
@@ -132,7 +134,7 @@ function escapeClosedRuns(scan: string, escapings: readonly InlineEscaping[], cl
return escaped
}
function unspellableRun(segments: readonly InlineSegment[], output: string, placements: readonly number[]): NodeRange | undefined {
function unspellableRun(segments: readonly InlineSegment[], output: string, placements: readonly number[]): MarkRun | undefined {
const { nodes, runs } = emittedRuns(segments, placements, output)
const pair = misflanked(runs) ?? unpaired(runs)
return pair === undefined ? undefined : nodes[pair]
@@ -168,9 +170,9 @@ function delimiterAt(run: EmittedRun, closes: boolean, offset: number, width: nu
return run.delimiters.find((delimiter) => delimiter.closes === closes && delimiter.offset === offset && delimiter.width === width)
}
function emittedRuns(segments: readonly InlineSegment[], placements: readonly number[], output: string): { nodes: NodeRange[]; runs: EmittedRun[] } {
function emittedRuns(segments: readonly InlineSegment[], placements: readonly number[], output: string): { nodes: MarkRun[]; runs: EmittedRun[] } {
const runs: EmittedRun[] = []
const nodes: NodeRange[] = []
const nodes: MarkRun[] = []
const open: number[] = []
let cursor = 0
for (const segment of segments) {