diff --git a/src/markdown/emit/inline-line.ts b/src/markdown/emit/inline-line.ts index 90be255..432fde4 100644 --- a/src/markdown/emit/inline-line.ts +++ b/src/markdown/emit/inline-line.ts @@ -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; 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 { 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 { + 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 }, ], }) } diff --git a/src/markdown/emit/line-escaping.ts b/src/markdown/emit/line-escaping.ts index 69b73af..34e000f 100644 --- a/src/markdown/emit/line-escaping.ts +++ b/src/markdown/emit/line-escaping.ts @@ -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) {