mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 19:58:28 +01:00
Ask once whether a line spans, keep base's fallback loop, and decide carries once per pass
This commit is contained in:
+5
-3
@@ -721,9 +721,11 @@ the maintainer decides. After the fifth ruling they gave the restructure to a su
|
|||||||
current design for a better reading rather than answer the ruling's places one by one (2026-11-04). A ruling judges the code, never the behaviour: item 91's spanning stays, since the
|
current design for a better reading rather than answer the ruling's places one by one (2026-11-04). A ruling judges the code, never the behaviour: item 91's spanning stays, since the
|
||||||
paragraphs it spells, such as bold around a code span, occur in documents all the time
|
paragraphs it spells, such as bold around a code span, occur in documents all the time
|
||||||
(2026-11-03). The maintainer's call on how: refactor first, ruled alone (item 166), then make spanning part of
|
(2026-11-03). The maintainer's call on how: refactor first, ruled alone (item 166), then make spanning part of
|
||||||
grouping, with the span rule in one home. `inlineRuns` in `emit/inline-runs.ts` groups a line, `runCrosses` says which
|
grouping, with the span rule in one home. `inlineRuns` in `emit/inline-runs.ts` groups a line spanned or parted, `runCrosses`
|
||||||
node a run passes, and a run ends at its last holder. Parting is a line's first fallback, so each pass walks once. The reducer
|
says which node a run passes, and a run ends at its last holder. The writer tries the spanned line once and, where it needs any
|
||||||
judges the spanned line, then the parted line, each trimmed by its own runs through that same grouping. `withoutOpeningLinkCode`
|
fallback, runs its fallback loop on the parted line, so `takeFallback` takes what it is handed; whether a node is carried is
|
||||||
|
decided once per pass, on its `RunNode` (the restructure of 2026-11-04). The reducer judges the same two lines, each trimmed by
|
||||||
|
its own runs, which `runsSharedWithNext` counts beside `inlineRuns`. `withoutOpeningLinkCode`
|
||||||
stays on `insideRun` alone, since it answers the parted line. A refactor that merges as its own chunk took its own ruling,
|
stays on `insideRun` alone, since it answers the parted line. A refactor that merges as its own chunk took its own ruling,
|
||||||
so the chunk's later rulings read against that merge, the commit `larv-review ruling-base` prints. A restructure past a
|
so the chunk's later rulings read against that merge, the commit `larv-review ruling-base` prints. A restructure past a
|
||||||
pass's round cap still goes unreviewed, so the PR summary names it.
|
pass's round cap still goes unreviewed, so the PR summary names it.
|
||||||
|
|||||||
@@ -31,11 +31,20 @@ test('spells a line alike taking every fallback a pass finds or the first alone'
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Takes only the first fallback each pass finds, with no cap.
|
test('carries the range it is handed on its first call', () => {
|
||||||
|
const fallbacks = withheldMarkFallbacks([{ text: 'a', type: 'text' }], 'lossless')
|
||||||
|
assert.deepEqual(takeFallback(fallbacks, [{ first: 0, last: 0 }], []), success(null))
|
||||||
|
assert.deepEqual([...fallbacks.carried], [0])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Writes the spanned line where it reads back, else the parted line taking only the first fallback each pass finds, with no cap.
|
||||||
function firstFallbackLine(nodes: readonly AdfNode[], container: LineContainer): Result<string> {
|
function firstFallbackLine(nodes: readonly AdfNode[], container: LineContainer): Result<string> {
|
||||||
const fallbacks = withheldMarkFallbacks(nodes, 'lossless')
|
const fallbacks = withheldMarkFallbacks(nodes, 'lossless')
|
||||||
|
const spanned = linePass(nodes, container, [], fallbacks, 'spanned')
|
||||||
|
if (!spanned.ok) return spanned
|
||||||
|
if (spanned.value.fallback === undefined) return success(spanned.value.line)
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const attempt = linePass(nodes, container, [], fallbacks)
|
const attempt = linePass(nodes, container, [], fallbacks, 'parted')
|
||||||
if (!attempt.ok) return attempt
|
if (!attempt.ok) return attempt
|
||||||
const { fallback } = attempt.value
|
const { fallback } = attempt.value
|
||||||
if (fallback === undefined) return success(attempt.value.line)
|
if (fallback === undefined) return success(attempt.value.line)
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
||||||
import type { InlineNodeModel } from '../../adf/inline-nodes.ts'
|
import type { InlineNodeModel } from '../../adf/inline-nodes.ts'
|
||||||
import type { InlineRun, RunGrouping, RunNode } from './inline-runs.ts'
|
import type { InlineRun, PlainRun, RunGrouping, RunNode } from './inline-runs.ts'
|
||||||
import type { LineContainer } from '../line-container.ts'
|
import type { LineContainer } from '../line-container.ts'
|
||||||
import type { LinkDefinition, TitleEscapes } from '../commonmark/link-syntax.ts'
|
import type { LinkDefinition, TitleEscapes } from '../commonmark/link-syntax.ts'
|
||||||
import type { RunFallback } from './unspellable-runs.ts'
|
import type { RunFallback } from './unspellable-runs.ts'
|
||||||
import type { WrittenFlavour } from '../flavour.ts'
|
import type { WrittenFlavour } from '../flavour.ts'
|
||||||
import { assembleInlineLine, isSyntax, type InlineEscaping, type InlineSegment, type MarkRun, 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 { carriedInline } from '../opaque-carry.ts'
|
||||||
import { carries, inlineRuns } from './inline-runs.ts'
|
|
||||||
import { commonMarkLink, isEmphasis, linkHref, markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
|
import { commonMarkLink, isEmphasis, linkHref, markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
|
||||||
import { escapeUnbalanced, spellDestination, spellLinkTarget } from '../commonmark/link-syntax.ts'
|
import { escapeUnbalanced, spellDestination, spellLinkTarget } from '../commonmark/link-syntax.ts'
|
||||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
@@ -15,6 +14,7 @@ import { flavourClaims } from '../flavour.ts'
|
|||||||
import { highlightDelimiter } from '../inline-tokens.ts'
|
import { highlightDelimiter } from '../inline-tokens.ts'
|
||||||
import { holdsNullCharacter, trimTrailingSpace } from '../commonmark/grammar.ts'
|
import { holdsNullCharacter, trimTrailingSpace } from '../commonmark/grammar.ts'
|
||||||
import { inlineNodeModel, takenMarkTypes } from '../../adf/inline-nodes.ts'
|
import { inlineNodeModel, takenMarkTypes } from '../../adf/inline-nodes.ts'
|
||||||
|
import { inlineRuns } from './inline-runs.ts'
|
||||||
import { isSpellableText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
import { isSpellableText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||||
import { joinsWhenRead, textBreakSpelling } from '../adjacent-text.ts'
|
import { joinsWhenRead, textBreakSpelling } from '../adjacent-text.ts'
|
||||||
import { largestNesting } from '../../nesting.ts'
|
import { largestNesting } from '../../nesting.ts'
|
||||||
@@ -30,11 +30,12 @@ type Emission = { carries: [NodeRange, ...NodeRange[]]; segments?: undefined } |
|
|||||||
|
|
||||||
type Fallback = [NodeRange, ...NodeRange[]] | 'opening-link'
|
type Fallback = [NodeRange, ...NodeRange[]] | 'opening-link'
|
||||||
|
|
||||||
type InlineContext = RunGrouping & {
|
type InlineContext = {
|
||||||
atBlockEnd: boolean
|
atBlockEnd: boolean
|
||||||
bracketed: boolean
|
bracketed: boolean
|
||||||
container: LineContainer
|
container: LineContainer
|
||||||
flavour: WrittenFlavour
|
flavour: WrittenFlavour
|
||||||
|
grouping: RunGrouping
|
||||||
lineNodes: readonly AdfNode[]
|
lineNodes: readonly AdfNode[]
|
||||||
openingLinkAsDirective: boolean
|
openingLinkAsDirective: boolean
|
||||||
path: ConvertErrorPath
|
path: ConvertErrorPath
|
||||||
@@ -43,7 +44,7 @@ type InlineContext = RunGrouping & {
|
|||||||
|
|
||||||
type LineAttempt = { fallback: Fallback; line?: undefined; segments?: undefined } | { fallback?: undefined; line: string; segments: InlineSegment[] }
|
type LineAttempt = { fallback: Fallback; line?: undefined; segments?: undefined } | { fallback?: undefined; line: string; segments: InlineSegment[] }
|
||||||
|
|
||||||
type LineFallbacks = { carried: Set<number>; flavour: WrittenFlavour; openingLinkAsDirective: boolean; parted: boolean }
|
type LineFallbacks = { carried: Set<number>; flavour: WrittenFlavour; openingLinkAsDirective: boolean }
|
||||||
|
|
||||||
// docs/decisions.md §After 32 passes, a line carries or drops every em, strike and strong mark.
|
// docs/decisions.md §After 32 passes, a line carries or drops every em, strike and strong mark.
|
||||||
export const readBackPasses = 32
|
export const readBackPasses = 32
|
||||||
@@ -62,9 +63,9 @@ export function openingLinkTakesDirective(nodes: readonly AdfNode[], path: Conve
|
|||||||
return success(emitted.value.openingLinkAsDirective)
|
return success(emitted.value.openingLinkAsDirective)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function portableLineFallback(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, { parted }: Pick<RunGrouping, 'parted'>): Result<PortableLineFallback | undefined> {
|
export function portableLineFallback(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, grouping: RunGrouping): Result<PortableLineFallback | undefined> {
|
||||||
const fallbacks = { ...withheldMarkFallbacks(nodes, 'portable'), parted }
|
const fallbacks = withheldMarkFallbacks(nodes, 'portable')
|
||||||
const emission = lineSegments(nodes, container, path, fallbacks)
|
const emission = lineSegments(nodes, container, path, fallbacks, grouping)
|
||||||
if (!emission.ok) return emission
|
if (!emission.ok) return emission
|
||||||
if (fallbacks.carried.size > 0 || emission.value.carries !== undefined) return failure('unsupported-node-shape', 'an inline node on a portable line has no spelling but the carry', path)
|
if (fallbacks.carried.size > 0 || emission.value.carries !== undefined) return failure('unsupported-node-shape', 'an inline node on a portable line has no spelling but the carry', path)
|
||||||
const verdict = lineVerdict(emission.value.segments, container, 'portable', { kind: 'drop', nodes })
|
const verdict = lineVerdict(emission.value.segments, container, 'portable', { kind: 'drop', nodes })
|
||||||
@@ -93,9 +94,18 @@ export function tryImageLine(alt: string | undefined, url: string, flavour: Writ
|
|||||||
|
|
||||||
function emitLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, flavour: WrittenFlavour): Result<EmittedLine> {
|
function emitLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, flavour: WrittenFlavour): Result<EmittedLine> {
|
||||||
const fallbacks = withheldMarkFallbacks(nodes, flavour)
|
const fallbacks = withheldMarkFallbacks(nodes, flavour)
|
||||||
// Terminates because takeFallback refuses a pass that took no new fallback. Pass 0 only parts the line, so `readBackPasses` counts what a parted line takes.
|
// spec/flavour.md, Marks: the spanned line where it reads back, else the parted line with every fallback it needs.
|
||||||
for (let pass = 0; ; pass += 1) {
|
const spanned = linePass(nodes, container, path, fallbacks, 'spanned')
|
||||||
const attempt = linePass(nodes, container, path, fallbacks)
|
if (!spanned.ok) return spanned
|
||||||
|
const { fallback, line, segments } = spanned.value
|
||||||
|
if (fallback === undefined) return success({ line, openingLinkAsDirective: false, segments })
|
||||||
|
return partedLine(nodes, container, path, fallbacks)
|
||||||
|
}
|
||||||
|
|
||||||
|
function partedLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, fallbacks: LineFallbacks): Result<EmittedLine> {
|
||||||
|
// Terminates because takeFallback refuses a pass that took no new fallback.
|
||||||
|
for (let pass = 1; ; pass += 1) {
|
||||||
|
const attempt = linePass(nodes, container, path, fallbacks, 'parted')
|
||||||
if (!attempt.ok) return attempt
|
if (!attempt.ok) return attempt
|
||||||
const { fallback, line, segments } = attempt.value
|
const { fallback, line, segments } = attempt.value
|
||||||
if (fallback === undefined) return success({ line, openingLinkAsDirective: fallbacks.openingLinkAsDirective, segments })
|
if (fallback === undefined) return success({ line, openingLinkAsDirective: fallbacks.openingLinkAsDirective, segments })
|
||||||
@@ -105,8 +115,8 @@ function emitLine(nodes: readonly AdfNode[], container: LineContainer, path: Con
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function linePass(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, fallbacks: LineFallbacks): Result<LineAttempt> {
|
export function linePass(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, fallbacks: LineFallbacks, grouping: RunGrouping): Result<LineAttempt> {
|
||||||
const emission = lineSegments(nodes, container, path, fallbacks)
|
const emission = lineSegments(nodes, container, path, fallbacks, grouping)
|
||||||
if (!emission.ok) return emission
|
if (!emission.ok) return emission
|
||||||
const { carries, segments } = emission.value
|
const { carries, segments } = emission.value
|
||||||
return success(segments === undefined ? { fallback: carries } : attemptLine(segments, container, fallbacks.flavour))
|
return success(segments === undefined ? { fallback: carries } : attemptLine(segments, container, fallbacks.flavour))
|
||||||
@@ -123,15 +133,20 @@ export function withheldMarkFallbacks(nodes: readonly AdfNode[], flavour: Writte
|
|||||||
const taken = takenMarkTypes(node)
|
const taken = takenMarkTypes(node)
|
||||||
if (taken !== undefined && nodeMarks(node).some((mark) => !taken.includes(mark.type))) carried.add(index)
|
if (taken !== undefined && nodeMarks(node).some((mark) => !taken.includes(mark.type))) carried.add(index)
|
||||||
}
|
}
|
||||||
return { carried, flavour, openingLinkAsDirective: false, parted: false }
|
return { carried, flavour, openingLinkAsDirective: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decides the text-node and slot-fault carries up front, `withheldMarkFallbacks` the withheld-mark ones, so a line holding many of them is emitted once.
|
||||||
|
function carries(node: AdfNode, carried: ReadonlySet<number>, index: number): boolean {
|
||||||
|
if (carried.has(index)) return true
|
||||||
|
if (node.type === 'text') return !isSpellableText(node) || typeof node.text !== 'string' || node.text === ''
|
||||||
|
const model = inlineNodeModel(node.type)
|
||||||
|
if (model === undefined) return true
|
||||||
|
const slot = model.textAttribute === undefined ? undefined : nodeAttrs(node)[model.textAttribute]
|
||||||
|
return typeof slot === 'string' && slotFault(node.type, slot) !== undefined && nodeContent(node).length === 0 && node.text === undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export function takeFallback(fallbacks: LineFallbacks, fallback: Fallback, path: ConvertErrorPath): Result<null> {
|
export function takeFallback(fallbacks: LineFallbacks, fallback: Fallback, path: ConvertErrorPath): Result<null> {
|
||||||
// spec/flavour.md, Marks: a line needing a fallback first parts its runs, and takes what the parted line still needs.
|
|
||||||
if (!fallbacks.parted) {
|
|
||||||
fallbacks.parted = true
|
|
||||||
return success(null)
|
|
||||||
}
|
|
||||||
if (fallback === 'opening-link') {
|
if (fallback === 'opening-link') {
|
||||||
if (fallbacks.openingLinkAsDirective) return failure('unsupported-node-shape', 'an opening link spelled as a directive still reads as a link definition, so the line has no spelling left', path)
|
if (fallbacks.openingLinkAsDirective) return failure('unsupported-node-shape', 'an opening link spelled as a directive still reads as a link definition, so the line has no spelling left', path)
|
||||||
fallbacks.openingLinkAsDirective = true
|
fallbacks.openingLinkAsDirective = true
|
||||||
@@ -143,12 +158,12 @@ export function takeFallback(fallbacks: LineFallbacks, fallback: Fallback, path:
|
|||||||
return success(null)
|
return success(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
function lineSegments(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, fallbacks: LineFallbacks): Result<Emission> {
|
function lineSegments(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, { carried, flavour, openingLinkAsDirective }: LineFallbacks, grouping: RunGrouping): Result<Emission> {
|
||||||
const context: InlineContext = { atBlockEnd: true, bracketed: false, ...fallbacks, container, lineNodes: nodes, path, spansLines: container === 'paragraph' }
|
const context: InlineContext = { atBlockEnd: true, bracketed: false, container, flavour, grouping, lineNodes: nodes, openingLinkAsDirective, path, spansLines: container === 'paragraph' }
|
||||||
const emission = emitRun(nodes.map((node) => ({ node, outer: 0 })), 0, 0, context)
|
const emission = emitRun(nodes.map((node, index) => ({ carried: carries(node, carried, index), node, outer: 0 })), 0, 0, context)
|
||||||
if (!emission.ok) return emission
|
if (!emission.ok) return emission
|
||||||
if (emission.value.carries !== undefined) return emission
|
if (emission.value.carries !== undefined) return emission
|
||||||
return success({ segments: spellEdgeWhitespace(emission.value.segments, fallbacks.flavour) })
|
return success({ segments: spellEdgeWhitespace(emission.value.segments, flavour) })
|
||||||
}
|
}
|
||||||
|
|
||||||
function attemptLine(segments: InlineSegment[], container: LineContainer, flavour: WrittenFlavour): LineAttempt {
|
function attemptLine(segments: InlineSegment[], container: LineContainer, flavour: WrittenFlavour): LineAttempt {
|
||||||
@@ -219,13 +234,13 @@ function emitRun(nodes: readonly RunNode[], depth: number, firstIndex: number, c
|
|||||||
if (depth > largestNesting) {
|
if (depth > largestNesting) {
|
||||||
return failure('unsupported-nesting-depth', `the marks nest deeper than the ${largestNesting} levels the emitter carries`, context.path)
|
return failure('unsupported-nesting-depth', `the marks nest deeper than the ${largestNesting} levels the emitter carries`, context.path)
|
||||||
}
|
}
|
||||||
const runs = inlineRuns(nodes, firstIndex, context)
|
const runs = inlineRuns(nodes, firstIndex, context.grouping)
|
||||||
const carries: NodeRange[] = []
|
const carries: NodeRange[] = []
|
||||||
const segments: InlineSegment[] = []
|
const segments: InlineSegment[] = []
|
||||||
for (const [offset, run] of runs.entries()) {
|
for (const [offset, run] of runs.entries()) {
|
||||||
if (takesTextBreak(runs[offset - 1], run, context.carried)) segments.push(syntax(textBreakSpelling))
|
if (takesTextBreak(runs[offset - 1], run)) segments.push(syntax(textBreakSpelling))
|
||||||
const runContext = { ...context, atBlockEnd: context.atBlockEnd && offset === runs.length - 1 }
|
const runContext = { ...context, atBlockEnd: context.atBlockEnd && offset === runs.length - 1 }
|
||||||
const emitted = run.kind === 'plain' ? emitLeaf(run.node, runContext, run.index) : emitMarkedRun(run.nodes, run.mark, depth, run.index, runContext)
|
const emitted = run.kind === 'plain' ? emitLeaf(run, runContext) : emitMarkedRun(run.nodes, run.mark, depth, run.index, runContext)
|
||||||
if (!emitted.ok) return emitted
|
if (!emitted.ok) return emitted
|
||||||
if (emitted.value.carries !== undefined) for (const carry of emitted.value.carries) carries.push(carry)
|
if (emitted.value.carries !== undefined) for (const carry of emitted.value.carries) carries.push(carry)
|
||||||
else for (const segment of emitted.value.segments) segments.push(segment)
|
else for (const segment of emitted.value.segments) segments.push(segment)
|
||||||
@@ -234,9 +249,8 @@ function emitRun(nodes: readonly RunNode[], depth: number, firstIndex: number, c
|
|||||||
return success(carry === undefined ? { segments } : { carries: [carry, ...others] })
|
return success(carry === undefined ? { segments } : { carries: [carry, ...others] })
|
||||||
}
|
}
|
||||||
|
|
||||||
function takesTextBreak(previous: InlineRun | undefined, run: InlineRun, carried: ReadonlySet<number>): boolean {
|
function takesTextBreak(previous: InlineRun | undefined, run: InlineRun): boolean {
|
||||||
if (previous?.kind !== 'plain' || run.kind !== 'plain') return false
|
if (previous?.kind !== 'plain' || run.kind !== 'plain' || previous.carried || run.carried) return false
|
||||||
if (carries(previous.node, carried, previous.index) || carries(run.node, carried, run.index)) return false
|
|
||||||
return joinsWhenRead(previous.node, run.node)
|
return joinsWhenRead(previous.node, run.node)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,11 +259,11 @@ function nodePath(context: InlineContext, index: number): ConvertErrorPath {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Builds the node's path only to refuse it: a path copies every level above, and a line may hold a million leaves.
|
// Builds the node's path only to refuse it: a path copies every level above, and a line may hold a million leaves.
|
||||||
function emitLeaf(node: AdfNode, context: InlineContext, index: number): Result<Emission> {
|
function emitLeaf({ carried, index, node }: PlainRun, context: InlineContext): Result<Emission> {
|
||||||
if (carries(node, context.carried, index)) {
|
if (carried) {
|
||||||
const carried = carriedInline(node, nodePath(context, index))
|
const carry = carriedInline(node, nodePath(context, index))
|
||||||
if (!carried.ok) return carried
|
if (!carry.ok) return carry
|
||||||
return success({ segments: [syntax(carried.value)] })
|
return success({ segments: [syntax(carry.value)] })
|
||||||
}
|
}
|
||||||
const types = nodeMarks(node).map((mark) => mark.type)
|
const types = nodeMarks(node).map((mark) => mark.type)
|
||||||
if (new Set(types).size !== types.length) return failure('unsupported-node-shape', `a ${node.type} node carries one mark type twice`, nodePath(context, index))
|
if (new Set(types).size !== types.length) return failure('unsupported-node-shape', `a ${node.type} node carries one mark type twice`, nodePath(context, index))
|
||||||
@@ -294,7 +308,7 @@ function textSegments(node: AdfNode, context: InlineContext): InlineSegment[] |
|
|||||||
return parts.map((part) => (part === '\r' ? syntax(' ') : /^[\n\0]/.test(part) ? textDirectiveSegment(part) : { escaping, text: part }))
|
return parts.map((part) => (part === '\r' ? syntax(' ') : /^[\n\0]/.test(part) ? textDirectiveSegment(part) : { escaping, text: part }))
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitMarkedRun(nodes: readonly RunNode[], mark: AdfMark, depth: number, index: number, context: InlineContext): Result<Emission> {
|
function emitMarkedRun(nodes: readonly [RunNode, ...RunNode[]], mark: AdfMark, depth: number, index: number, context: InlineContext): Result<Emission> {
|
||||||
const range: NodeRange = { first: index, last: index + nodes.length - 1 }
|
const range: NodeRange = { first: index, last: index + nodes.length - 1 }
|
||||||
if (mark.type === 'backgroundColor' && flavourClaims[context.flavour].highlights) return emitHighlight(nodes, depth, { ...range, mark }, context)
|
if (mark.type === 'backgroundColor' && flavourClaims[context.flavour].highlights) return emitHighlight(nodes, depth, { ...range, mark }, context)
|
||||||
const spelling = markSpelling(mark.type)
|
const spelling = markSpelling(mark.type)
|
||||||
@@ -348,11 +362,11 @@ function needsPadding(text: string): boolean {
|
|||||||
return text.startsWith(' ') && text.endsWith(' ') && /[^ ]/.test(text)
|
return text.startsWith(' ') && text.endsWith(' ') && /[^ ]/.test(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryLink(nodes: readonly RunNode[], mark: AdfMark, depth: number, range: NodeRange, context: InlineContext): Result<Emission> | undefined {
|
function tryLink(nodes: readonly [RunNode, ...RunNode[]], mark: AdfMark, depth: number, range: NodeRange, context: InlineContext): Result<Emission> | undefined {
|
||||||
const href = linkHref(nodeAttrs(mark))
|
const href = linkHref(nodeAttrs(mark))
|
||||||
if (href === undefined) return carriedRun(range, context)
|
if (href === undefined) return carriedRun(range, context)
|
||||||
const opening = depth === 0 && range.first === 0 && context.openingLinkAsDirective
|
const opening = depth === 0 && range.first === 0 && context.openingLinkAsDirective
|
||||||
const commonMark = opening ? undefined : commonMarkLink(nodeAttrs(mark), href, nodes.map(({ node }) => node), nodes[0]?.outer ?? 0, context.bracketed, titleEscapes(context.flavour, context.container))
|
const commonMark = opening ? undefined : commonMarkLink(nodeAttrs(mark), href, nodes.map(({ node }) => node), nodes[0].outer, context.bracketed, titleEscapes(context.flavour, context.container))
|
||||||
if (commonMark === undefined) return undefined
|
if (commonMark === undefined) return undefined
|
||||||
if (commonMark.form === 'autolink') return success({ segments: [syntax(`<${href}>`)] })
|
if (commonMark.form === 'autolink') return success({ segments: [syntax(`<${href}>`)] })
|
||||||
const inner = emitRun(nodes, depth + 1, range.first, { ...context, bracketed: true })
|
const inner = emitRun(nodes, depth + 1, range.first, { ...context, bracketed: true })
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
||||||
|
import { runsSharedWithNext } from './inline-runs.ts'
|
||||||
|
|
||||||
|
const em: AdfMark = { type: 'em' }
|
||||||
|
const strong: AdfMark = { type: 'strong' }
|
||||||
|
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||||
|
|
||||||
|
test('counts a run crossing a code span to its next holder where the line is spanned, and none where it is parted', () => {
|
||||||
|
const line = [marked('Run ', strong), marked('npm ci', { type: 'code' }), marked(' first', strong)]
|
||||||
|
assert.deepEqual(runsSharedWithNext(line, 'spanned'), [1, 1, 0])
|
||||||
|
assert.deepEqual(runsSharedWithNext(line, 'parted'), [0, 0, 0])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('counts every nested run crossing a hard break, and none crossing a hard break holding an empty marks', () => {
|
||||||
|
assert.deepEqual(runsSharedWithNext([marked('a', em, strong), { type: 'hardBreak' }, marked('b', em, strong)], 'spanned'), [2, 2, 0])
|
||||||
|
assert.deepEqual(runsSharedWithNext([marked('a', em), { marks: [], type: 'hardBreak' }, marked('b', em)], 'spanned'), [0, 0, 0])
|
||||||
|
})
|
||||||
@@ -1,54 +1,53 @@
|
|||||||
import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
||||||
import { identicalMark, isSpellableText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
import { identicalMark, nodeMarks } from '../../adf/document.ts'
|
||||||
import { inlineNodeModel, takesMark } from '../../adf/inline-nodes.ts'
|
|
||||||
import { markSpelling } from '../mark-spellings.ts'
|
import { markSpelling } from '../mark-spellings.ts'
|
||||||
import { slotFault } from '../directive/syntax.ts'
|
import { takesMark } from '../../adf/inline-nodes.ts'
|
||||||
|
|
||||||
export type InlineRun = { index: number; kind: 'marked'; mark: AdfMark; nodes: RunNode[] } | { index: number; kind: 'plain'; node: AdfNode }
|
export type InlineRun = MarkedRun | PlainRun
|
||||||
|
|
||||||
// `parted` keeps a run from crossing any node.
|
type MarkedRun = { index: number; kind: 'marked'; mark: AdfMark; nodes: [RunNode, ...RunNode[]] }
|
||||||
export type RunGrouping = { carried: ReadonlySet<number>; parted: boolean }
|
|
||||||
|
|
||||||
// An inline node inside the runs of its first `outer` marks. A node a run crosses holds no mark of that run, so its `outer` stays.
|
export type PlainRun = { carried: boolean; index: number; kind: 'plain'; node: AdfNode }
|
||||||
export type RunNode = { node: AdfNode; outer: number }
|
|
||||||
|
|
||||||
export const partedLine: RunGrouping = { carried: new Set(), parted: true }
|
// spec/flavour.md, Marks: spanned, an em, strike, strong or link run crosses a node that cannot hold its mark; parted, as a line needing a fallback is, every run breaks there.
|
||||||
export const spannedLine: RunGrouping = { carried: new Set(), parted: false }
|
export type RunGrouping = 'parted' | 'spanned'
|
||||||
|
|
||||||
|
// An inline node inside the runs of its first `outer` marks, `carried` where the carry spells it. A node a run crosses holds no mark of that run, so its `outer` stays.
|
||||||
|
export type RunNode = { carried: boolean; node: AdfNode; outer: number }
|
||||||
|
|
||||||
// spec/flavour.md, Marks. The portable reducer must ask this too, or it trims edge whitespace by runs the writers do not spell.
|
|
||||||
export function inlineRuns(nodes: readonly RunNode[], firstIndex: number, grouping: RunGrouping): InlineRun[] {
|
export function inlineRuns(nodes: readonly RunNode[], firstIndex: number, grouping: RunGrouping): InlineRun[] {
|
||||||
const runs: InlineRun[] = []
|
const runs: InlineRun[] = []
|
||||||
let end = 0
|
let end = 0
|
||||||
for (const [offset, { node, outer }] of nodes.entries()) {
|
for (const [offset, { carried, node, outer }] of nodes.entries()) {
|
||||||
if (offset < end) continue
|
if (offset < end) continue
|
||||||
const index = firstIndex + offset
|
const index = firstIndex + offset
|
||||||
// A carried node spells its marks inside the carry.
|
// A carried node spells its marks inside the carry.
|
||||||
const mark = carries(node, grouping.carried, index) ? undefined : nodeMarks(node)[outer]
|
const mark = carried ? undefined : nodeMarks(node)[outer]
|
||||||
if (mark === undefined) {
|
if (mark === undefined) {
|
||||||
runs.push({ index, kind: 'plain', node })
|
runs.push({ carried, index, kind: 'plain', node })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
end = runEnd(nodes, offset, mark, firstIndex, grouping)
|
end = runEnd(nodes, offset, mark, grouping)
|
||||||
runs.push({ index, kind: 'marked', mark, nodes: nodes.slice(offset, end).map((runNode) => insideRun(runNode, mark) ?? runNode) })
|
runs.push({ index, kind: 'marked', mark, nodes: [{ carried, node, outer: outer + 1 }, ...nodes.slice(offset + 1, end).map((runNode) => insideRun(runNode, mark) ?? runNode)] })
|
||||||
}
|
}
|
||||||
return runs
|
return runs
|
||||||
}
|
}
|
||||||
|
|
||||||
// A run opens and closes at a node holding its mark, so a node it would cross past the last such node stays outside.
|
// A run opens and closes at a node holding its mark, so a node it would cross past the last such node stays outside.
|
||||||
function runEnd(nodes: readonly RunNode[], first: number, mark: AdfMark, firstIndex: number, { carried, parted }: RunGrouping): number {
|
function runEnd(nodes: readonly RunNode[], first: number, mark: AdfMark, grouping: RunGrouping): number {
|
||||||
let end = first + 1
|
let end = first + 1
|
||||||
for (let offset = end; offset < nodes.length; offset += 1) {
|
for (let offset = end; offset < nodes.length; offset += 1) {
|
||||||
const runNode = nodes[offset]
|
const runNode = nodes[offset]
|
||||||
if (runNode === undefined || carries(runNode.node, carried, firstIndex + offset)) break
|
if (runNode === undefined || runNode.carried) break
|
||||||
if (insideRun(runNode, mark) !== undefined) end = offset + 1
|
if (insideRun(runNode, mark) !== undefined) end = offset + 1
|
||||||
else if (parted || !runCrosses(runNode.node, mark)) break
|
else if (grouping === 'parted' || !runCrosses(runNode.node, mark)) break
|
||||||
}
|
}
|
||||||
return end
|
return end
|
||||||
}
|
}
|
||||||
|
|
||||||
export function insideRun({ node, outer }: RunNode, mark: AdfMark): RunNode | undefined {
|
export function insideRun(runNode: RunNode, mark: AdfMark): RunNode | undefined {
|
||||||
const held = nodeMarks(node)[outer]
|
const held = nodeMarks(runNode.node)[runNode.outer]
|
||||||
return held !== undefined && identicalMark(held, mark) ? { node, outer: outer + 1 } : undefined
|
return held !== undefined && identicalMark(held, mark) ? { ...runNode, outer: runNode.outer + 1 } : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// The readers refuse `marks=empty` inside a mark's spelling.
|
// The readers refuse `marks=empty` inside a mark's spelling.
|
||||||
@@ -57,12 +56,18 @@ function runCrosses(node: AdfNode, mark: AdfMark): boolean {
|
|||||||
return (kind === 'emphasis' || kind === 'link') && !takesMark(node, mark.type) && node.marks?.length !== 0
|
return (kind === 'emphasis' || kind === 'link') && !takesMark(node, mark.type) && node.marks?.length !== 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decides the text-node and slot-fault carries up front, `withheldMarkFallbacks` the withheld-mark ones, so a line holding many of them is emitted once.
|
// For each leaf but the last, how many runs hold both it and the next leaf, grouped as the writers group them, so the reducer trims edge whitespace by runs they spell.
|
||||||
export function carries(node: AdfNode, carried: ReadonlySet<number>, index: number): boolean {
|
export function runsSharedWithNext(leaves: readonly AdfNode[], grouping: RunGrouping): number[] {
|
||||||
if (carried.has(index)) return true
|
const shared = leaves.map(() => 0)
|
||||||
if (node.type === 'text') return !isSpellableText(node) || typeof node.text !== 'string' || node.text === ''
|
countSharedRuns(leaves.map((node) => ({ carried: false, node, outer: 0 })), 0, grouping, shared)
|
||||||
const model = inlineNodeModel(node.type)
|
return shared
|
||||||
if (model === undefined) return true
|
}
|
||||||
const slot = model.textAttribute === undefined ? undefined : nodeAttrs(node)[model.textAttribute]
|
|
||||||
return typeof slot === 'string' && slotFault(node.type, slot) !== undefined && nodeContent(node).length === 0 && node.text === undefined
|
// Recurses once per mark of a leaf, and the reducer's leaves hold one mark of each kept type.
|
||||||
|
function countSharedRuns(nodes: readonly RunNode[], firstIndex: number, grouping: RunGrouping, shared: number[]): void {
|
||||||
|
for (const run of inlineRuns(nodes, firstIndex, grouping)) {
|
||||||
|
if (run.kind === 'plain') continue
|
||||||
|
for (let index = run.index; index < run.index + run.nodes.length - 1; index += 1) shared[index] = (shared[index] ?? 0) + 1
|
||||||
|
countSharedRuns(run.nodes, run.index, grouping, shared)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from '../../adf/doc
|
|||||||
import { adfToPortableMarkdown, reduceToPortable } from './adf-to-portable-markdown.ts'
|
import { adfToPortableMarkdown, reduceToPortable } from './adf-to-portable-markdown.ts'
|
||||||
import { largestNesting } from '../../nesting.ts'
|
import { largestNesting } from '../../nesting.ts'
|
||||||
import { portableLineFallback } from '../emit/inline-line.ts'
|
import { portableLineFallback } from '../emit/inline-line.ts'
|
||||||
import { spannedLine } from '../emit/inline-runs.ts'
|
|
||||||
import { portableMarkdownToAdf } from './portable-markdown-to-adf.ts'
|
import { portableMarkdownToAdf } from './portable-markdown-to-adf.ts'
|
||||||
|
|
||||||
const code: AdfMark = { type: 'code' }
|
const code: AdfMark = { type: 'code' }
|
||||||
@@ -413,12 +412,12 @@ test('gives up every highlight run whose own delimiter cannot flank in one fallb
|
|||||||
const highlight: AdfMark = { type: 'backgroundColor' }
|
const highlight: AdfMark = { type: 'backgroundColor' }
|
||||||
const line = [text('a '), text('x', highlight), text('a '), text('x', highlight), text('a '), text('x', highlight), text('a')]
|
const line = [text('a '), text('x', highlight), text('a '), text('x', highlight), text('a '), text('x', highlight), text('a')]
|
||||||
const runs = [{ first: 1, last: 1, mark: highlight }, { first: 3, last: 3, mark: highlight }, { first: 5, last: 5, mark: highlight }]
|
const runs = [{ first: 1, last: 1, mark: highlight }, { first: 3, last: 3, mark: highlight }, { first: 5, last: 5, mark: highlight }]
|
||||||
assert.deepEqual(portableLineFallback(line, 'paragraph', [], spannedLine), { ok: true, value: { kind: 'unspellable-run', runs } })
|
assert.deepEqual(portableLineFallback(line, 'paragraph', [], 'spanned'), { ok: true, value: { kind: 'unspellable-run', runs } })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('refuses a portable line holding a node only the carry spells, at the line', () => {
|
test('refuses a portable line holding a node only the carry spells, at the line', () => {
|
||||||
for (const mark of [{ attrs: { a: 1 }, type: 'em' }, { type: 'odd' }]) {
|
for (const mark of [{ attrs: { a: 1 }, type: 'em' }, { type: 'odd' }]) {
|
||||||
const refused = portableLineFallback([text('x', mark)], 'paragraph', ['content', 0], spannedLine)
|
const refused = portableLineFallback([text('x', mark)], 'paragraph', ['content', 0], 'spanned')
|
||||||
assert.deepEqual(refused.ok ? refused : [refused.error.code, refused.error.path], ['unsupported-node-shape', ['content', 0]])
|
assert.deepEqual(refused.ok ? refused : [refused.error.code, refused.error.path], ['unsupported-node-shape', ['content', 0]])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import type { LineContainer } from '../line-container.ts'
|
|||||||
import type { PortableLineFallback } from '../emit/inline-line.ts'
|
import type { PortableLineFallback } from '../emit/inline-line.ts'
|
||||||
import { lineContainer, markedLine, propertyRuns } from '../../conformance/property-harness.ts'
|
import { lineContainer, markedLine, propertyRuns } from '../../conformance/property-harness.ts'
|
||||||
import { judgedLine, portableLeaves, reduceInline, trimmedEdges, withoutFallback } from './inline-reduction.ts'
|
import { judgedLine, portableLeaves, reduceInline, trimmedEdges, withoutFallback } from './inline-reduction.ts'
|
||||||
import { spannedLine } from '../emit/inline-runs.ts'
|
|
||||||
import { success, type Result } from '../../result.ts'
|
import { success, type Result } from '../../result.ts'
|
||||||
|
|
||||||
test('reduces a line alike dropping every run a pass may or the first alone', () => {
|
test('reduces a line alike dropping every run a pass may or the first alone', () => {
|
||||||
@@ -70,7 +69,7 @@ function firstRunLine(nodes: readonly AdfNode[], container: LineContainer): Resu
|
|||||||
if (fallback === undefined) return success(line)
|
if (fallback === undefined) return success(line)
|
||||||
const fixed = withoutFallback(line, firstRun(fallback), [])
|
const fixed = withoutFallback(line, firstRun(fallback), [])
|
||||||
if (!fixed.ok) return fixed
|
if (!fixed.ok) return fixed
|
||||||
current = trimmedEdges(fixed.value, spannedLine)
|
current = trimmedEdges(fixed.value, 'spanned')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
||||||
import type { LineContainer } from '../line-container.ts'
|
import type { LineContainer } from '../line-container.ts'
|
||||||
import type { MarkRun } from '../emit/line-escaping.ts'
|
import type { MarkRun } from '../emit/line-escaping.ts'
|
||||||
import type { RunGrouping, RunNode } from '../emit/inline-runs.ts'
|
import type { RunGrouping } from '../emit/inline-runs.ts'
|
||||||
import { codeTextMarkTypes } from '../../adf/inline-nodes.ts'
|
import { codeTextMarkTypes } from '../../adf/inline-nodes.ts'
|
||||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
import { identicalMark, mergeAdjacentText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
import { identicalMark, mergeAdjacentText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||||
import { imageAside } from '../image-aside.ts'
|
import { imageAside } from '../image-aside.ts'
|
||||||
import { inlineRuns, insideRun, partedLine, spannedLine } from '../emit/inline-runs.ts'
|
import { insideRun, runsSharedWithNext } from '../emit/inline-runs.ts'
|
||||||
import { isEmphasis } from '../mark-spellings.ts'
|
import { isEmphasis } from '../mark-spellings.ts'
|
||||||
import { joinsWhenEditorNormal } from './editor-normal.ts'
|
import { joinsWhenEditorNormal } from './editor-normal.ts'
|
||||||
import { largestNesting } from '../../nesting.ts'
|
import { largestNesting } from '../../nesting.ts'
|
||||||
@@ -34,7 +34,7 @@ export function reduceInline(nodes: readonly AdfNode[], container: LineContainer
|
|||||||
export function portableLeaves(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, depth: number): Result<AdfNode[]> {
|
export function portableLeaves(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, depth: number): Result<AdfNode[]> {
|
||||||
const leaves = inlineLeaves(nodes, container, path, depth)
|
const leaves = inlineLeaves(nodes, container, path, depth)
|
||||||
if (!leaves.ok) return leaves
|
if (!leaves.ok) return leaves
|
||||||
return success(trimmedEdges(highlightsInsideSharedMarks(trimmedEdges(leaves.value, spannedLine)), spannedLine))
|
return success(trimmedEdges(highlightsInsideSharedMarks(trimmedEdges(leaves.value, 'spanned')), 'spanned'))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function inlineLeaves(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, depth: number): Result<AdfNode[]> {
|
export function inlineLeaves(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, depth: number): Result<AdfNode[]> {
|
||||||
@@ -198,22 +198,6 @@ function withoutEdgeBreaks(leaves: readonly AdfNode[]): AdfNode[] {
|
|||||||
return leaves.slice(first, last + 1)
|
return leaves.slice(first, last + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each leaf but the last, how many runs of the line hold both it and the next leaf.
|
|
||||||
function runsSharedWithNext(leaves: readonly AdfNode[], grouping: RunGrouping): number[] {
|
|
||||||
const shared = leaves.map(() => 0)
|
|
||||||
countSharedRuns(leaves.map((node) => ({ node, outer: 0 })), 0, grouping, shared)
|
|
||||||
return shared
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recurses once per mark, and a portable leaf holds one mark of each kept type.
|
|
||||||
function countSharedRuns(nodes: readonly RunNode[], firstIndex: number, grouping: RunGrouping, shared: number[]): void {
|
|
||||||
for (const run of inlineRuns(nodes, firstIndex, grouping)) {
|
|
||||||
if (run.kind === 'plain') continue
|
|
||||||
for (let index = run.index; index < run.index + run.nodes.length - 1; index += 1) shared[index] = (shared[index] ?? 0) + 1
|
|
||||||
countSharedRuns(run.nodes, run.index, grouping, shared)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function beside(neighbour: AdfNode | undefined, sharedRuns = 0): Beside {
|
function beside(neighbour: AdfNode | undefined, sharedRuns = 0): Beside {
|
||||||
return { lineEdge: neighbour === undefined || neighbour.type === 'hardBreak', sharedRuns }
|
return { lineEdge: neighbour === undefined || neighbour.type === 'hardBreak', sharedRuns }
|
||||||
}
|
}
|
||||||
@@ -256,17 +240,17 @@ function spellableLine(leaves: AdfNode[], container: LineContainer, path: Conver
|
|||||||
if (fallback === undefined) return success(line)
|
if (fallback === undefined) return success(line)
|
||||||
const fixed = withoutFallback(line, fallback, path)
|
const fixed = withoutFallback(line, fallback, path)
|
||||||
if (!fixed.ok) return fixed
|
if (!fixed.ok) return fixed
|
||||||
current = trimmedEdges(pass === readBackPasses ? withoutEmphasis(fixed.value) : fixed.value, spannedLine)
|
current = trimmedEdges(pass === readBackPasses ? withoutEmphasis(fixed.value) : fixed.value, 'spanned')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// spec/flavour.md, Marks: the lines in the order the writer tries them, each trimmed by its own runs.
|
// Judged as the writer writes a line: spanned where that reads back, else parted and trimmed by the parted runs, the line a fallback's indexes name.
|
||||||
export function judgedLine(spanned: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<JudgedLine> {
|
export function judgedLine(spanned: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<JudgedLine> {
|
||||||
const spannedFallback = portableLineFallback(spanned, container, path, spannedLine)
|
const spannedFallback = portableLineFallback(spanned, container, path, 'spanned')
|
||||||
if (!spannedFallback.ok) return spannedFallback
|
if (!spannedFallback.ok) return spannedFallback
|
||||||
if (spannedFallback.value === undefined) return success({ fallback: undefined, leaves: [...spanned] })
|
if (spannedFallback.value === undefined) return success({ fallback: undefined, leaves: [...spanned] })
|
||||||
const parted = trimmedEdges(spanned, partedLine)
|
const parted = trimmedEdges(spanned, 'parted')
|
||||||
const partedFallback = portableLineFallback(parted, container, path, partedLine)
|
const partedFallback = portableLineFallback(parted, container, path, 'parted')
|
||||||
if (!partedFallback.ok) return partedFallback
|
if (!partedFallback.ok) return partedFallback
|
||||||
return success({ fallback: partedFallback.value, leaves: parted })
|
return success({ fallback: partedFallback.value, leaves: parted })
|
||||||
}
|
}
|
||||||
@@ -279,7 +263,7 @@ export function withoutFallback(leaves: readonly AdfNode[], fallback: PortableLi
|
|||||||
function withoutOpeningLinkCode(leaves: readonly AdfNode[]): AdfNode[] | undefined {
|
function withoutOpeningLinkCode(leaves: readonly AdfNode[]): AdfNode[] | undefined {
|
||||||
const mark = nodeMarks(leaves[0] ?? {})[0]
|
const mark = nodeMarks(leaves[0] ?? {})[0]
|
||||||
if (mark === undefined || mark.type !== 'link') return undefined
|
if (mark === undefined || mark.type !== 'link') return undefined
|
||||||
const past = leaves.findIndex((leaf) => insideRun({ node: leaf, outer: 0 }, mark) === undefined)
|
const past = leaves.findIndex((leaf) => insideRun({ carried: false, node: leaf, outer: 0 }, mark) === undefined)
|
||||||
const end = past === -1 ? leaves.length : past
|
const end = past === -1 ? leaves.length : past
|
||||||
// A code span is what binds the `]` a link definition reads, and dropping it keeps the link target.
|
// A code span is what binds the `]` a link definition reads, and dropping it keeps the link target.
|
||||||
const spans = leaves.slice(0, end).some((leaf) => nodeMarks(leaf).length > 1 && nodeMarks(leaf).at(-1)?.type === 'code')
|
const spans = leaves.slice(0, end).some((leaf) => nodeMarks(leaf).length > 1 && nodeMarks(leaf).at(-1)?.type === 'code')
|
||||||
|
|||||||
Reference in New Issue
Block a user