mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Keep the one-fallback-per-pass loop in the properties, out of the writers' signatures
This commit is contained in:
@@ -2,13 +2,16 @@ import assert from 'node:assert/strict'
|
||||
import fc from 'fast-check'
|
||||
import test from 'node:test'
|
||||
|
||||
import { emitInlineLine } from './inline-line.ts'
|
||||
import type { AdfNode } from '../../adf/document.ts'
|
||||
import type { LineContainer } from '../line-container.ts'
|
||||
import { emitInlineLine, linePass, takeFallback, withheldMarkFallbacks } from './inline-line.ts'
|
||||
import { lineContainer, markedLine, propertyRuns } from '../../conformance/property-harness.ts'
|
||||
import { success, type Result } from '../../result.ts'
|
||||
|
||||
test('spells a line alike taking every fallback a pass finds or the first alone', () => {
|
||||
fc.assert(
|
||||
fc.property(markedLine, lineContainer, (nodes, container) => {
|
||||
assert.deepEqual(emitInlineLine(nodes, container, [], 'lossless'), emitInlineLine(nodes, container, [], 'lossless', 'first'))
|
||||
assert.deepEqual(emitInlineLine(nodes, container, [], 'lossless'), firstFallbackLine(nodes, container))
|
||||
}),
|
||||
// Lines the fixed seed rarely generates: carrying one run lets a neighbour or its holder read as written, or one pass carries every copy of a repeated mark run.
|
||||
{
|
||||
@@ -27,3 +30,16 @@ test('spells a line alike taking every fallback a pass finds or the first alone'
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
// Takes the first fallback a pass finds alone, and no cap.
|
||||
function firstFallbackLine(nodes: readonly AdfNode[], container: LineContainer): Result<string> {
|
||||
const fallbacks = withheldMarkFallbacks(nodes, 'lossless')
|
||||
for (;;) {
|
||||
const attempt = linePass(nodes, container, [], fallbacks)
|
||||
if (!attempt.ok) return attempt
|
||||
const { fallback } = attempt.value
|
||||
if (fallback === undefined) return success(attempt.value.line)
|
||||
const taken = takeFallback(fallbacks, fallback === 'opening-link' ? fallback : [fallback[0]], [])
|
||||
if (!taken.ok) return taken
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,6 @@ type Emission = { carries: [NodeRange, ...NodeRange[]]; segments?: undefined } |
|
||||
|
||||
type Fallback = [NodeRange, ...NodeRange[]] | 'opening-link'
|
||||
|
||||
// How many fallbacks one pass applies: `every`, or the `first` alone and with no cap; a property checks that both write the same line.
|
||||
export type FallbacksPerPass = 'every' | 'first'
|
||||
|
||||
type InlineContext = {
|
||||
atBlockEnd: boolean
|
||||
bracketed: boolean
|
||||
@@ -43,7 +40,7 @@ type InlineContext = {
|
||||
|
||||
type InlineRun = { index: number; kind: 'marked'; mark: AdfMark; nodes: AdfNode[] } | { index: number; kind: 'plain'; node: AdfNode }
|
||||
|
||||
type LineAttempt = { fallback: Fallback; line?: 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 }
|
||||
|
||||
@@ -52,28 +49,28 @@ export const readBackPasses = 32
|
||||
|
||||
export type PortableLineFallback = { kind: 'opening-link' } | { kind: 'unspellable-run'; runs: [MarkRun, ...MarkRun[]] }
|
||||
|
||||
export function emitInlineLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, flavour: WrittenFlavour, perPass: FallbacksPerPass = 'every'): Result<string> {
|
||||
const emitted = emitLine(nodes, container, path, flavour, perPass)
|
||||
export function emitInlineLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, flavour: WrittenFlavour): Result<string> {
|
||||
const emitted = emitLine(nodes, container, path, flavour)
|
||||
if (!emitted.ok) return emitted
|
||||
return success(emitted.value.line)
|
||||
}
|
||||
|
||||
export function openingLinkTakesDirective(nodes: readonly AdfNode[], path: ConvertErrorPath): Result<boolean> {
|
||||
const emitted = emitLine(nodes, 'paragraph', path, 'lossless', 'every')
|
||||
const emitted = emitLine(nodes, 'paragraph', path, 'lossless')
|
||||
if (!emitted.ok) return emitted
|
||||
return success(emitted.value.openingLinkAsDirective)
|
||||
}
|
||||
|
||||
export function portableLineFallback(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, alone: (run: MarkRun) => boolean): Result<PortableLineFallback | undefined> {
|
||||
export function portableLineFallback(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<PortableLineFallback | undefined> {
|
||||
const emission = lineSegments(nodes, container, path, withheldMarkFallbacks(nodes, 'portable'))
|
||||
if (!emission.ok) return emission
|
||||
if (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', { alone, kind: 'drop' })
|
||||
const verdict = lineVerdict(emission.value.segments, container, 'portable', { kind: 'drop', nodes })
|
||||
return success(verdict.kind === 'line' ? undefined : verdict)
|
||||
}
|
||||
|
||||
export function tryPipeCell(nodes: readonly AdfNode[], path: ConvertErrorPath, flavour: WrittenFlavour): string | undefined {
|
||||
const emitted = emitLine(nodes, 'table-cell', path, flavour, 'every')
|
||||
const emitted = emitLine(nodes, 'table-cell', path, flavour)
|
||||
if (!emitted.ok) return undefined
|
||||
// spec/flavour.md, Tables; the portable reduction leaves a `|` in syntax only escaped, in a link title.
|
||||
if (flavour === 'lossless' && emitted.value.segments.some((segment) => isSyntax(segment.escaping) && segment.text.includes('|'))) return undefined
|
||||
@@ -92,31 +89,33 @@ export function tryImageLine(alt: string | undefined, url: string, flavour: Writ
|
||||
return attemptLine([syntax('[, bracketedLinkTarget(destination), syntax(`)](${target})`)], 'paragraph', 'lossless').line
|
||||
}
|
||||
|
||||
function emitLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, flavour: WrittenFlavour, perPass: FallbacksPerPass): Result<EmittedLine> {
|
||||
function emitLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, flavour: WrittenFlavour): Result<EmittedLine> {
|
||||
const fallbacks = withheldMarkFallbacks(nodes, flavour)
|
||||
// Terminates because takeFallback refuses a pass that took no new fallback.
|
||||
for (let pass = 1; ; pass += 1) {
|
||||
const emission = lineSegments(nodes, container, path, fallbacks)
|
||||
if (!emission.ok) return emission
|
||||
const { carries, segments } = emission.value
|
||||
const attempt = segments === undefined ? { fallback: carries } : attemptLine(segments, container, flavour)
|
||||
if (attempt.fallback === undefined) return success({ line: attempt.line, openingLinkAsDirective: fallbacks.openingLinkAsDirective, segments: attempt.segments })
|
||||
const taken = takeFallback(fallbacks, takenThisPass(attempt.fallback, perPass), path)
|
||||
const attempt = linePass(nodes, container, path, fallbacks)
|
||||
if (!attempt.ok) return attempt
|
||||
const { fallback, line, segments } = attempt.value
|
||||
if (fallback === undefined) return success({ line, openingLinkAsDirective: fallbacks.openingLinkAsDirective, segments })
|
||||
const taken = takeFallback(fallbacks, fallback, path)
|
||||
if (!taken.ok) return taken
|
||||
if (perPass === 'every' && pass === readBackPasses) carryEveryEmphasis(fallbacks, nodes)
|
||||
if (pass === readBackPasses) carryEveryEmphasis(fallbacks, nodes)
|
||||
}
|
||||
}
|
||||
|
||||
export function linePass(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, fallbacks: LineFallbacks): Result<LineAttempt> {
|
||||
const emission = lineSegments(nodes, container, path, fallbacks)
|
||||
if (!emission.ok) return emission
|
||||
const { carries, segments } = emission.value
|
||||
return success(segments === undefined ? { fallback: carries } : attemptLine(segments, container, fallbacks.flavour))
|
||||
}
|
||||
|
||||
function carryEveryEmphasis(fallbacks: LineFallbacks, nodes: readonly AdfNode[]): void {
|
||||
for (const [index, node] of nodes.entries()) if (nodeMarks(node).some(isEmphasis)) fallbacks.carried.add(index)
|
||||
}
|
||||
|
||||
function takenThisPass(fallback: Fallback, perPass: FallbacksPerPass): Fallback {
|
||||
return fallback === 'opening-link' || perPass === 'every' ? fallback : [fallback[0]]
|
||||
}
|
||||
|
||||
// Seeds the withheld-mark carries once per line, so the run at each mark depth does not rescan every node's marks.
|
||||
function withheldMarkFallbacks(nodes: readonly AdfNode[], flavour: WrittenFlavour): LineFallbacks {
|
||||
export function withheldMarkFallbacks(nodes: readonly AdfNode[], flavour: WrittenFlavour): LineFallbacks {
|
||||
const carried = new Set<number>()
|
||||
for (const [index, node] of nodes.entries()) {
|
||||
const taken = takenMarkTypes(node)
|
||||
@@ -125,7 +124,7 @@ function withheldMarkFallbacks(nodes: readonly AdfNode[], flavour: WrittenFlavou
|
||||
return { carried, flavour, openingLinkAsDirective: false }
|
||||
}
|
||||
|
||||
function takeFallback(fallbacks: LineFallbacks, fallback: Fallback, path: ConvertErrorPath): Result<null> {
|
||||
export function takeFallback(fallbacks: LineFallbacks, fallback: Fallback, path: ConvertErrorPath): Result<null> {
|
||||
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)
|
||||
fallbacks.openingLinkAsDirective = true
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { AdfNode } from '../../adf/document.ts'
|
||||
import type { Claims, WrittenFlavour } from '../flavour.ts'
|
||||
import type { InlineReading } from '../inline-content.ts'
|
||||
import type { InlineSource, InlineToken } from '../inline-tokens.ts'
|
||||
import type { LineContainer } from '../line-container.ts'
|
||||
import { delimiterFlags, isWordCharacter, runLength } from '../commonmark/emphasis-matching.ts'
|
||||
import { delimiterRunToken, highlightFlanking, inlineSource, readInlineToken } from '../inline-tokens.ts'
|
||||
import { escapesLineClaim, type LinePosition } from '../commonmark/grammar.ts'
|
||||
import { flavourClaims } from '../flavour.ts'
|
||||
import { isBareDelimiterRow } from '../pipe-table-syntax.ts'
|
||||
import { delimiterFlags, isWordCharacter, runLength } from '../commonmark/emphasis-matching.ts'
|
||||
import { nodeMarks } from '../../adf/document.ts'
|
||||
import { opensLinkDefinition } from '../commonmark/link-reference-definitions.ts'
|
||||
import { readLineBack } from '../inline-content.ts'
|
||||
|
||||
@@ -28,8 +30,8 @@ export type InlineSegment =
|
||||
|
||||
export type AssembledLine = { line: string; openingLinkAsDirective?: true; unspellableRuns: MarkRun[] }
|
||||
|
||||
// How the caller takes a run back: a carry swaps its nodes for one carry directive, splitting every run holding it; a drop removes its mark, moving no other delimiter where `alone` holds.
|
||||
export type RunFallback = { kind: 'carry' } | { alone: (run: MarkRun) => boolean; kind: 'drop' }
|
||||
// How the caller takes a run back: a carry swaps its nodes for one carry directive, splitting every run holding it; a drop removes its mark from the line's `nodes`.
|
||||
export type RunFallback = { kind: 'carry' } | { kind: 'drop'; nodes: readonly AdfNode[] }
|
||||
|
||||
type ScanLine = { position: LinePosition; start: number; text: string }
|
||||
|
||||
@@ -297,7 +299,7 @@ function unspellableRuns(placed: readonly PlacedSegment[], reading: InlineReadin
|
||||
const firstPair = pairs[first]
|
||||
if (firstPair === undefined) return misflankedHighlights(placed, output)
|
||||
const merging = new Set<number>()
|
||||
if (fallback.kind === 'drop') for (const { nodes } of pairs) if (!fallback.alone(nodes)) merging.add(nodes.first).add(nodes.last + 1)
|
||||
if (fallback.kind === 'drop') for (const { nodes } of pairs) if (!dropsAlone(fallback.nodes, nodes)) merging.add(nodes.first).add(nodes.last + 1)
|
||||
const sweep: PairSweep = { fallback, fallen: new Uint8Array(pairs.length), first, merging, output, pairAt, pairs, placed }
|
||||
sweep.fallen[first] = 1
|
||||
// Each verdict reads the verdicts settled before it, so the sweep settles outward from the first pair.
|
||||
@@ -308,6 +310,18 @@ function unspellableRuns(placed: readonly PlacedSegment[], reading: InlineReadin
|
||||
return runs
|
||||
}
|
||||
|
||||
// Dropping the run merges no runs and moves no run's end, whatever else drops: at each end, the node outside holds no mark type the end node holds inside the run's mark.
|
||||
function dropsAlone(nodes: readonly AdfNode[], run: MarkRun): boolean {
|
||||
return endsAlone(nodes[run.first], nodes[run.first - 1], run.depth) && endsAlone(nodes[run.last], nodes[run.last + 1], run.depth)
|
||||
}
|
||||
|
||||
function endsAlone(edge: AdfNode | undefined, beside: AdfNode | undefined, depth: number): boolean {
|
||||
const besideTypes = new Set(nodeMarks(beside ?? {}).map((mark) => mark.type))
|
||||
return nodeMarks(edge ?? {})
|
||||
.slice(depth + 1)
|
||||
.every((mark) => !besideTypes.has(mark.type))
|
||||
}
|
||||
|
||||
// The pairs in closing order.
|
||||
function emphasisPairs(placed: readonly PlacedSegment[]): { pairAt: Int32Array; pairs: EmphasisPair[] } {
|
||||
const pairAt = new Int32Array(placed.length).fill(-1)
|
||||
|
||||
@@ -412,11 +412,11 @@ test('gives up every highlight run whose own delimiter cannot flank in one fallb
|
||||
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 runs = [{ depth: 0, first: 1, last: 1 }, { depth: 0, first: 3, last: 3 }, { depth: 0, first: 5, last: 5 }]
|
||||
assert.deepEqual(portableLineFallback(line, 'paragraph', [], () => false), { ok: true, value: { kind: 'unspellable-run', runs } })
|
||||
assert.deepEqual(portableLineFallback(line, 'paragraph', []), { ok: true, value: { kind: 'unspellable-run', runs } })
|
||||
})
|
||||
|
||||
test('refuses a portable line holding a node only the carry spells, at the line', () => {
|
||||
const refused = portableLineFallback([text('x', { type: 'odd' })], 'paragraph', ['content', 0], () => false)
|
||||
const refused = portableLineFallback([text('x', { type: 'odd' })], 'paragraph', ['content', 0])
|
||||
assert.deepEqual(refused.ok ? refused : [refused.error.code, refused.error.path], ['unsupported-node-shape', ['content', 0]])
|
||||
})
|
||||
|
||||
|
||||
@@ -2,13 +2,19 @@ import assert from 'node:assert/strict'
|
||||
import fc from 'fast-check'
|
||||
import test from 'node:test'
|
||||
|
||||
import type { AdfNode } from '../../adf/document.ts'
|
||||
import type { LineContainer } from '../line-container.ts'
|
||||
import type { PortableLineFallback } from '../emit/inline-line.ts'
|
||||
import { lineContainer, markedLine, propertyRuns } from '../../conformance/property-harness.ts'
|
||||
import { reduceInline } from './inline-reduction.ts'
|
||||
import { nodeMarks } from '../../adf/document.ts'
|
||||
import { portableLeaves, reduceInline, trimmedEdges, withoutFallback } from './inline-reduction.ts'
|
||||
import { portableLineFallback } from '../emit/inline-line.ts'
|
||||
import { success, type Result } from '../../result.ts'
|
||||
|
||||
test('reduces a line alike dropping every run a pass may or the first alone', () => {
|
||||
fc.assert(
|
||||
fc.property(markedLine, lineContainer, (nodes, container) => {
|
||||
assert.deepEqual(reduceInline(nodes, container, [], 0), reduceInline(nodes, container, [], 0, 'first'))
|
||||
assert.deepEqual(reduceInline(nodes, container, [], 0), firstRunLine(nodes, container))
|
||||
}),
|
||||
// Lines the fixed seed rarely generates: a later run must not drop with the first, or one pass drops every copy of a repeated mark run.
|
||||
{
|
||||
@@ -29,3 +35,23 @@ test('reduces a line alike dropping every run a pass may or the first alone', ()
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
// Drops the first run a pass finds alone, with every misflanked highlight, and takes no cap.
|
||||
function firstRunLine(nodes: readonly AdfNode[], container: LineContainer): Result<AdfNode[]> {
|
||||
const leaves = portableLeaves(nodes, container, [], 0)
|
||||
if (!leaves.ok) return leaves
|
||||
for (let current = leaves.value; ; ) {
|
||||
const fallback = portableLineFallback(current, container, [])
|
||||
if (!fallback.ok) return fallback
|
||||
if (fallback.value === undefined) return success(current)
|
||||
const fixed = withoutFallback(current, firstRun(current, fallback.value), [])
|
||||
if (!fixed.ok) return fixed
|
||||
current = trimmedEdges(fixed.value)
|
||||
}
|
||||
}
|
||||
|
||||
function firstRun(leaves: readonly AdfNode[], fallback: PortableLineFallback): PortableLineFallback {
|
||||
if (fallback.kind === 'opening-link') return fallback
|
||||
const [first, ...others] = fallback.runs
|
||||
return { kind: 'unspellable-run', runs: [first, ...others.filter((run) => nodeMarks(leaves[run.first] ?? {})[run.depth]?.type === 'backgroundColor')] }
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { joinsWhenEditorNormal, sameMarkWhenEditorNormal } from './editor-normal
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
import { mergeAdjacentText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||
import { partsShownText, referenceName, shownImage, shownLink, shownText, shownUrl } from '../../adf/shown-text.ts'
|
||||
import { portableLineFallback, readBackPasses, type FallbacksPerPass, type PortableLineFallback } from '../emit/inline-line.ts'
|
||||
import { portableLineFallback, readBackPasses, type PortableLineFallback } from '../emit/inline-line.ts'
|
||||
import { spellDestination } from '../commonmark/link-syntax.ts'
|
||||
|
||||
const highlight = 'backgroundColor'
|
||||
@@ -17,10 +17,16 @@ const highlightMark: AdfMark = { type: highlight }
|
||||
const edgeStrippingMarks: readonly string[] = [highlight, 'em', 'strike', 'strong']
|
||||
const keptMarks: readonly string[] = [...edgeStrippingMarks, 'code', 'link']
|
||||
|
||||
export function reduceInline(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, depth: number, perPass: FallbacksPerPass = 'every'): Result<AdfNode[]> {
|
||||
export function reduceInline(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, depth: number): Result<AdfNode[]> {
|
||||
const leaves = portableLeaves(nodes, container, path, depth)
|
||||
if (!leaves.ok) return leaves
|
||||
return spellableLine(leaves.value, container, path)
|
||||
}
|
||||
|
||||
export function portableLeaves(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, depth: number): Result<AdfNode[]> {
|
||||
const leaves = inlineLeaves(nodes, container, path, depth)
|
||||
if (!leaves.ok) return leaves
|
||||
return spellableLine(trimmedEdges(highlightsInsideSharedMarks(trimmedEdges(leaves.value))), container, path, perPass)
|
||||
return success(trimmedEdges(highlightsInsideSharedMarks(trimmedEdges(leaves.value))))
|
||||
}
|
||||
|
||||
export function inlineLeaves(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, depth: number): Result<AdfNode[]> {
|
||||
@@ -155,7 +161,7 @@ function withMarks(leaf: AdfNode, marks: readonly AdfMark[]): AdfNode {
|
||||
return marks.length === 0 ? unmarked : { ...unmarked, marks: [...marks] }
|
||||
}
|
||||
|
||||
function trimmedEdges(leaves: readonly AdfNode[]): AdfNode[] {
|
||||
export function trimmedEdges(leaves: readonly AdfNode[]): AdfNode[] {
|
||||
for (let current = leaves; ; ) {
|
||||
const merged = withoutEdgeBreaks(mergeAdjacentText(current, joinsWhenEditorNormal))
|
||||
let changed = false
|
||||
@@ -219,19 +225,23 @@ function sameMarkAt(marks: readonly AdfMark[], others: readonly AdfMark[], index
|
||||
return mark !== undefined && other !== undefined && sameMarkWhenEditorNormal(mark, other)
|
||||
}
|
||||
|
||||
function spellableLine(leaves: AdfNode[], container: LineContainer, path: ConvertErrorPath, perPass: FallbacksPerPass): Result<AdfNode[]> {
|
||||
function spellableLine(leaves: AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<AdfNode[]> {
|
||||
for (let current = leaves, pass = 1; ; pass += 1) {
|
||||
const fallback = portableLineFallback(current, container, path, (run) => dropsAlone(current, run))
|
||||
const fallback = portableLineFallback(current, container, path)
|
||||
if (!fallback.ok) return fallback
|
||||
if (fallback.value === undefined) return success(current)
|
||||
const fixed = withoutFallback(current, fallback.value, perPass)
|
||||
if (fixed === undefined) return failure('unsupported-node-shape', 'a portable line keeps a spelling that dropping a mark does not change', path)
|
||||
current = trimmedEdges(perPass === 'every' && pass === readBackPasses ? withoutEmphasis(fixed) : fixed)
|
||||
const fixed = withoutFallback(current, fallback.value, path)
|
||||
if (!fixed.ok) return fixed
|
||||
current = trimmedEdges(pass === readBackPasses ? withoutEmphasis(fixed.value) : fixed.value)
|
||||
}
|
||||
}
|
||||
|
||||
function withoutFallback(leaves: readonly AdfNode[], fallback: PortableLineFallback, perPass: FallbacksPerPass): AdfNode[] | undefined {
|
||||
if (fallback.kind === 'unspellable-run') return withoutMarks(leaves, droppedRuns(leaves, fallback.runs, perPass))
|
||||
export function withoutFallback(leaves: readonly AdfNode[], fallback: PortableLineFallback, path: ConvertErrorPath): Result<AdfNode[]> {
|
||||
const fixed = fallback.kind === 'unspellable-run' ? withoutMarks(leaves, fallback.runs) : withoutOpeningLinkCode(leaves)
|
||||
return fixed === undefined ? failure('unsupported-node-shape', 'a portable line keeps a spelling that dropping a mark does not change', path) : success(fixed)
|
||||
}
|
||||
|
||||
function withoutOpeningLinkCode(leaves: readonly AdfNode[]): AdfNode[] | undefined {
|
||||
const mark = nodeMarks(leaves[0] ?? {})[0]
|
||||
if (mark === undefined || mark.type !== 'link') return undefined
|
||||
let last = 0
|
||||
@@ -245,7 +255,7 @@ function withoutEmphasis(leaves: readonly AdfNode[]): AdfNode[] {
|
||||
return leaves.map((leaf) => withMarks(leaf, nodeMarks(leaf).filter((mark) => !isEmphasis(mark))))
|
||||
}
|
||||
|
||||
// The runs may nest, so a leaf drops the mark at every depth a run holds it.
|
||||
// Pairs that fall together may hold one another, so a leaf drops its mark at each depth a run over it names.
|
||||
function withoutMarks(leaves: readonly AdfNode[], runs: readonly MarkRun[]): AdfNode[] {
|
||||
const depths = new Map<number, number[]>()
|
||||
for (const run of runs) {
|
||||
@@ -260,20 +270,3 @@ function withoutMarks(leaves: readonly AdfNode[], runs: readonly MarkRun[]): Adf
|
||||
return dropped === undefined ? leaf : withMarks(leaf, nodeMarks(leaf).filter((_, held) => !dropped.includes(held)))
|
||||
})
|
||||
}
|
||||
|
||||
// Every misflanked highlight drops with the first run; with `every`, so does every emphasis run that falls with it.
|
||||
function droppedRuns(leaves: readonly AdfNode[], runs: readonly [MarkRun, ...MarkRun[]], perPass: FallbacksPerPass): readonly MarkRun[] {
|
||||
return perPass === 'every' ? runs : runs.filter((run, index) => index === 0 || nodeMarks(leaves[run.first] ?? {})[run.depth]?.type === highlight)
|
||||
}
|
||||
|
||||
// Dropping the run merges no runs and moves no run's end, whatever else drops: at each end, the leaf outside holds no mark type the end leaf holds inside the run's mark.
|
||||
function dropsAlone(leaves: readonly AdfNode[], run: MarkRun): boolean {
|
||||
return endsAlone(leaves[run.first], leaves[run.first - 1], run.depth) && endsAlone(leaves[run.last], leaves[run.last + 1], run.depth)
|
||||
}
|
||||
|
||||
function endsAlone(edge: AdfNode | undefined, beside: AdfNode | undefined, depth: number): boolean {
|
||||
const besideTypes = new Set(nodeMarks(beside ?? {}).map((mark) => mark.type))
|
||||
return nodeMarks(edge ?? {})
|
||||
.slice(depth + 1)
|
||||
.every((mark) => !besideTypes.has(mark.type))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user