mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Span a run inside inlineRuns, so runs group in one place
This commit is contained in:
@@ -6,19 +6,18 @@ import type { RunFallback } from './unspellable-runs.ts'
|
||||
import type { WrittenFlavour } from '../flavour.ts'
|
||||
import { assembleInlineLine, isSyntax, type InlineEscaping, type InlineSegment, type MarkRun, type NodeRange } from './line-escaping.ts'
|
||||
import { carriedInline } from '../opaque-carry.ts'
|
||||
import { commonMarkLink, insideRun, isEmphasis, linkHref, markSpelling, spellMarkAttributes, type RunNode } from '../mark-spellings.ts'
|
||||
import { commonMarkLink, isEmphasis, linkHref, markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
|
||||
import { directivePrefix, slotFault, spellInlineDirectiveOpener, spellInlineLeafDirective } from '../directive/syntax.ts'
|
||||
import { escapeUnbalanced, spellDestination, spellLinkTarget } from '../commonmark/link-syntax.ts'
|
||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { flavourClaims } from '../flavour.ts'
|
||||
import { highlightDelimiter } from '../inline-tokens.ts'
|
||||
import { holdsNullCharacter, trimTrailingSpace } from '../commonmark/grammar.ts'
|
||||
import { inlineNodeModel, takenMarkTypes } from '../../adf/inline-nodes.ts'
|
||||
import { isSpellableText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||
import { identicalMark, isSpellableText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||
import { inlineNodeModel, takenMarkTypes, takesMark } from '../../adf/inline-nodes.ts'
|
||||
import { joinsWhenRead, textBreakSpelling } from '../adjacent-text.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
import { longestBacktickRun } from '../commonmark/backtick-runs.ts'
|
||||
import { slotFault, spellInlineDirectiveOpener, spellInlineLeafDirective } from '../directive/syntax.ts'
|
||||
import { spanSkippedNodes } from './skipped-nodes.ts'
|
||||
import { spellInlineNodeAttributes } from '../directive/inline-spelling.ts'
|
||||
import { spellTextDirective } from '../directive/text.ts'
|
||||
|
||||
@@ -118,12 +117,12 @@ export function linePass(nodes: readonly AdfNode[], container: LineContainer, pa
|
||||
|
||||
// spec/flavour.md, Marks: a pass keeps the spanned line only where it needs no fallback; otherwise it writes the parted line and takes that line's fallbacks.
|
||||
function spannedLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, fallbacks: LineFallbacks): LineAttempt | undefined {
|
||||
const spanned = spanSkippedNodes(nodes, (node, index) => carries(node, fallbacks.carried, index))
|
||||
if (spanned === nodes) return undefined
|
||||
const spanned = spannedNodes(nodes, fallbacks.carried)
|
||||
if (spanned === undefined) return undefined
|
||||
const emission = lineSegments(spanned, container, path, fallbacks)
|
||||
if (!emission.ok || emission.value.segments === undefined) return undefined
|
||||
// A span that leaves whitespace at a line edge spells it `!adf:text`, which portable markdown lacks.
|
||||
if (fallbacks.flavour === 'portable' && emission.value.segments.some((segment) => segment.escaping === 'none' && segment.text.startsWith('!adf:'))) return undefined
|
||||
// A span can leave whitespace at a line edge, which only `!adf:text` spells.
|
||||
if (!flavourClaims[fallbacks.flavour].directives && emission.value.segments.some((segment) => segment.escaping === 'none' && segment.text.startsWith(directivePrefix))) return undefined
|
||||
const attempt = attemptLine(emission.value.segments, container, fallbacks.flavour)
|
||||
return attempt.fallback === undefined ? attempt : undefined
|
||||
}
|
||||
@@ -229,7 +228,7 @@ function emitRun(nodes: readonly RunNode[], depth: number, firstIndex: number, c
|
||||
if (depth > largestNesting) {
|
||||
return failure('unsupported-nesting-depth', `the marks nest deeper than the ${largestNesting} levels the emitter carries`, context.path)
|
||||
}
|
||||
const runs = inlineRuns(nodes, firstIndex, context.carried)
|
||||
const runs = inlineRuns(nodes, depth, firstIndex, context.carried, false)
|
||||
const carries: NodeRange[] = []
|
||||
const segments: InlineSegment[] = []
|
||||
for (const [offset, run] of runs.entries()) {
|
||||
@@ -244,25 +243,70 @@ function emitRun(nodes: readonly RunNode[], depth: number, firstIndex: number, c
|
||||
return success(carry === undefined ? { segments } : { carries: [carry, ...others] })
|
||||
}
|
||||
|
||||
function inlineRuns(nodes: readonly RunNode[], firstIndex: number, carried: ReadonlySet<number>): InlineRun[] {
|
||||
// Where `spans`, an emphasis or link run also takes in the nodes between two of its own that cannot hold its mark, lending them the mark.
|
||||
function inlineRuns(nodes: readonly AdfNode[], depth: number, firstIndex: number, carried: ReadonlySet<number>, spans: boolean): InlineRun[] {
|
||||
const runs: InlineRun[] = []
|
||||
for (const [offset, runNode] of nodes.entries()) {
|
||||
let spanEnd = 0
|
||||
for (const [offset, node] of nodes.entries()) {
|
||||
const index = firstIndex + offset
|
||||
const { node, outer } = runNode
|
||||
// A carried node spells its marks inside the carry.
|
||||
const mark = carries(node, carried, index) ? undefined : nodeMarks(node)[outer]
|
||||
if (mark === undefined) {
|
||||
runs.push({ index, kind: 'plain', node })
|
||||
const previous = runs[runs.length - 1]
|
||||
if (offset < spanEnd && previous?.kind === 'marked') {
|
||||
previous.nodes.push(withMark(node, previous.mark, depth))
|
||||
continue
|
||||
}
|
||||
const previous = runs[runs.length - 1]
|
||||
const inside = previous?.kind === 'marked' ? insideRun(runNode, previous.mark) : undefined
|
||||
if (previous?.kind === 'marked' && inside !== undefined) previous.nodes.push(inside)
|
||||
else runs.push({ index, kind: 'marked', mark, nodes: [{ node, outer: outer + 1 }] })
|
||||
const mark = heldMark(node, index, depth, carried)
|
||||
if (mark === undefined) runs.push({ index, kind: 'plain', node })
|
||||
else if (previous?.kind === 'marked' && identicalMark(previous.mark, mark)) previous.nodes.push(node)
|
||||
else runs.push({ index, kind: 'marked', mark, nodes: [node] })
|
||||
if (spans && mark !== undefined) spanEnd = spannedTo(nodes, offset + 1, firstIndex, depth, mark, carried)
|
||||
}
|
||||
return runs
|
||||
}
|
||||
|
||||
// `depth` indexes each node's marks, outermost first; a carried node spells its marks inside the carry.
|
||||
function heldMark(node: AdfNode, index: number, depth: number, carried: ReadonlySet<number>): AdfMark | undefined {
|
||||
return carries(node, carried, index) ? undefined : nodeMarks(node)[depth]
|
||||
}
|
||||
|
||||
// The offset where the nodes from `start` that a run of `mark` spans end: `start` itself unless a node holding the mark follows them.
|
||||
function spannedTo(nodes: readonly AdfNode[], start: number, firstIndex: number, depth: number, mark: AdfMark, carried: ReadonlySet<number>): number {
|
||||
let end = start
|
||||
while (spansNode(nodes[end], firstIndex + end, mark, carried)) end += 1
|
||||
const next = nodes[end]
|
||||
const held = next === undefined ? undefined : heldMark(next, firstIndex + end, depth, carried)
|
||||
return held !== undefined && identicalMark(held, mark) ? end : start
|
||||
}
|
||||
|
||||
// spec/flavour.md, Marks: the readers give no node a mark Atlassian's schema withholds from it, and a lent mark would erase the `marks=empty` of an empty marks array.
|
||||
function spansNode(node: AdfNode | undefined, index: number, mark: AdfMark, carried: ReadonlySet<number>): boolean {
|
||||
const kind = markSpelling(mark.type)?.kind
|
||||
return node !== undefined && (kind === 'emphasis' || kind === 'link') && !carries(node, carried, index) && !takesMark(node, mark.type) && node.marks?.length !== 0
|
||||
}
|
||||
|
||||
function withMark(node: AdfNode, mark: AdfMark, depth: number): AdfNode {
|
||||
const marks = nodeMarks(node)
|
||||
return { ...node, marks: [...marks.slice(0, depth), mark, ...marks.slice(depth)] }
|
||||
}
|
||||
|
||||
// The nodes as the spanned line writes them, `undefined` where no run spans a node.
|
||||
export function spannedNodes(nodes: readonly AdfNode[], carried: ReadonlySet<number>): AdfNode[] | undefined {
|
||||
const spanned: AdfNode[] = []
|
||||
lendMarks(nodes, 0, 0, carried, spanned)
|
||||
return spanned.some((node, index) => node !== nodes[index]) ? spanned : undefined
|
||||
}
|
||||
|
||||
function lendMarks(nodes: readonly AdfNode[], depth: number, firstIndex: number, carried: ReadonlySet<number>, spanned: AdfNode[]): void {
|
||||
// Past this depth the spanned line is refused as the parted one is.
|
||||
if (depth > largestNesting) {
|
||||
for (const node of nodes) spanned.push(node)
|
||||
return
|
||||
}
|
||||
for (const run of inlineRuns(nodes, depth, firstIndex, carried, true)) {
|
||||
if (run.kind === 'plain') spanned.push(run.node)
|
||||
else lendMarks(run.nodes, depth + 1, run.index, carried, spanned)
|
||||
}
|
||||
}
|
||||
|
||||
function takesTextBreak(previous: InlineRun | undefined, run: InlineRun, carried: ReadonlySet<number>): boolean {
|
||||
if (previous?.kind !== 'plain' || run.kind !== 'plain') return false
|
||||
if (carries(previous.node, carried, previous.index) || carries(run.node, carried, run.index)) return false
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
||||
import { identicalMark, nodeMarks } from '../../adf/document.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
import { markSpelling } from '../mark-spellings.ts'
|
||||
import { takesMark } from '../../adf/inline-nodes.ts'
|
||||
|
||||
type Carries = (node: AdfNode, index: number) => boolean
|
||||
|
||||
// Lends each emphasis or link run's mark to the nodes inside the run that cannot hold it; returns `nodes` itself where no run skips a node.
|
||||
export function spanSkippedNodes(nodes: readonly AdfNode[], carries: Carries): readonly AdfNode[] {
|
||||
const spanned = [...nodes]
|
||||
return spanRuns(spanned, 0, spanned.length, 0, carries) ? spanned : nodes
|
||||
}
|
||||
|
||||
// Groups the runs as `inlineRuns` does.
|
||||
function spanRuns(spanned: AdfNode[], first: number, end: number, depth: number, carries: Carries): boolean {
|
||||
if (depth > largestNesting) return false
|
||||
let lent = false
|
||||
for (let start = first; start < end; ) {
|
||||
const mark = heldMark(spanned, start, depth, carries)
|
||||
if (mark === undefined) {
|
||||
start += 1
|
||||
continue
|
||||
}
|
||||
let last = start
|
||||
for (;;) {
|
||||
let next = last + 1
|
||||
while (next < end && skipsMark(spanned, next, mark, carries)) next += 1
|
||||
const held = next < end ? heldMark(spanned, next, depth, carries) : undefined
|
||||
if (held === undefined || !identicalMark(held, mark)) break
|
||||
for (const [offset, node] of spanned.slice(last + 1, next).entries()) spanned[last + 1 + offset] = withMark(node, mark, depth)
|
||||
lent ||= next > last + 1
|
||||
last = next
|
||||
}
|
||||
lent = spanRuns(spanned, start, last + 1, depth + 1, carries) || lent
|
||||
start = last + 1
|
||||
}
|
||||
return lent
|
||||
}
|
||||
|
||||
function heldMark(spanned: readonly AdfNode[], index: number, depth: number, carries: Carries): AdfMark | undefined {
|
||||
const node = spanned[index]
|
||||
return node === undefined || carries(node, index) ? undefined : nodeMarks(node)[depth]
|
||||
}
|
||||
|
||||
// spec/flavour.md, Marks: the readers give no node a mark Atlassian's schema withholds from it.
|
||||
function skipsMark(spanned: readonly AdfNode[], index: number, mark: AdfMark, carries: Carries): boolean {
|
||||
const node = spanned[index]
|
||||
const kind = markSpelling(mark.type)?.kind
|
||||
return node !== undefined && (kind === 'emphasis' || kind === 'link') && !carries(node, index) && !takesMark(node, mark.type) && !holdsEmptyMarks(node)
|
||||
}
|
||||
|
||||
// An empty marks array is spelled `marks=empty`, which a lent mark would erase.
|
||||
function holdsEmptyMarks(node: AdfNode): boolean {
|
||||
return Array.isArray(node.marks) && node.marks.length === 0
|
||||
}
|
||||
|
||||
function withMark(node: AdfNode, mark: AdfMark, depth: number): AdfNode {
|
||||
const marks = nodeMarks(node)
|
||||
return { ...node, marks: [...marks.slice(0, depth), mark, ...marks.slice(depth)] }
|
||||
}
|
||||
@@ -9,8 +9,7 @@ import { insideRun, isEmphasis, type RunNode } from '../mark-spellings.ts'
|
||||
import { joinsWhenEditorNormal } from './editor-normal.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
import { partsShownText, referenceName, shownImage, shownLink, shownText, shownUrl } from '../../adf/shown-text.ts'
|
||||
import { portableLineFallback, readBackPasses, type PortableLineFallback } from '../emit/inline-line.ts'
|
||||
import { spanSkippedNodes } from '../emit/skipped-nodes.ts'
|
||||
import { portableLineFallback, readBackPasses, spannedNodes, type PortableLineFallback } from '../emit/inline-line.ts'
|
||||
import { spellDestination } from '../commonmark/link-syntax.ts'
|
||||
|
||||
const highlight = 'backgroundColor'
|
||||
@@ -166,8 +165,8 @@ export function trimmedEdges(leaves: readonly AdfNode[]): AdfNode[] {
|
||||
// Terminates: a pass that changes a leaf drops whitespace from the leaf's edge or takes a mark off that whitespace, and merging adds no whitespace or mark.
|
||||
for (let current = leaves; ; ) {
|
||||
const merged = withoutEdgeBreaks(mergeAdjacentText(current, joinsWhenEditorNormal))
|
||||
// Whitespace beside a code span inside a run keeps the run's mark, since the writer spans the run across the code.
|
||||
const spanned = spanSkippedNodes(merged, () => false)
|
||||
// Judged beside the spanned line's nodes, so whitespace beside a code span inside a run keeps the run's mark.
|
||||
const spanned = spannedNodes(merged, new Set()) ?? merged
|
||||
let changed = false
|
||||
const trimmed: AdfNode[] = []
|
||||
for (const [index, leaf] of merged.entries()) {
|
||||
|
||||
Reference in New Issue
Block a user