Name the reader's own-marks items for what they hold, and drop the carry flag the items already say

This commit is contained in:
2026-10-03 16:21:49 +02:00
parent d56e5b4f57
commit fd3452b124
+21 -21
View File
@@ -21,17 +21,17 @@ import { readDirectiveMark } from './directive-marks.ts'
import { readInlineDirectiveNode } from './directive-nodes.ts' import { readInlineDirectiveNode } from './directive-nodes.ts'
import { readTextDirective } from '../text-directive.ts' import { readTextDirective } from '../text-directive.ts'
export type InlineContent = { carry?: undefined; image: AdfNode; nodes?: undefined } | { carry: boolean; image?: undefined; nodes: AdfNode[] } export type InlineContent = { image: AdfNode; nodes?: undefined } | { image?: undefined; nodes: AdfNode[] }
// The text break leaf: it holds no marks, and stands between the nodes it parts until the outermost content checks and drops it. // The text break leaf: it holds no marks, and stands between the nodes it parts until the outermost content checks and drops it.
type TextBreak = { kind: 'textBreak' } type TextBreak = { kind: 'textBreak' }
// A node whose marks are its own — an opaque carry, or an inline node spelling marks=empty: no mark spelling wraps it, and it joins no neighbour. // A node whose marks are its own — an opaque carry, or an inline node spelling marks=empty: no mark spelling wraps it, and it joins no neighbour.
type Carried = { kind: 'carry'; node: AdfNode } type OwnMarks = { kind: 'ownMarks'; node: AdfNode }
type Inline = AdfNode | Carried | TextBreak type Inline = AdfNode | OwnMarks | TextBreak
type Scanned = { carry?: undefined; image: AdfNode; nodes?: undefined } | { carry: boolean; image?: undefined; nodes: Inline[] } type Scanned = { image: AdfNode; nodes?: undefined } | { image?: undefined; nodes: Inline[] }
export type LinkDefinitions = ReadonlyMap<string, LinkDefinition> export type LinkDefinitions = ReadonlyMap<string, LinkDefinition>
@@ -43,7 +43,7 @@ type Pairing = EmphasisPairing<Run>
type Piece = type Piece =
| Bracket | Bracket
| Carried | OwnMarks
| { alt: string; kind: 'image'; node: AdfNode } | { alt: string; kind: 'image'; node: AdfNode }
| { kind: 'nodes'; nodes: Inline[] } | { kind: 'nodes'; nodes: Inline[] }
| TextBreak | TextBreak
@@ -70,9 +70,9 @@ type Scan = {
// What a directive's content slot inherits from the content holding it. // What a directive's content slot inherits from the content holding it.
type SharedScan = Pick<Scan, 'definitions' | 'path'> type SharedScan = Pick<Scan, 'definitions' | 'path'>
type SlotContent = { carry: boolean; nodes: Inline[] } type SlotContent = { nodes: Inline[] }
const carriedInMark = 'move the opaque carry, or the node spelling marks=empty, out of the mark spelling: it holds its own marks' const ownMarksInMark = 'move the opaque carry, or the node spelling marks=empty, out of the mark spelling: it holds its own marks'
const editorHighlight: AdfMark = { attrs: { color: '#f8e6a0' }, type: 'backgroundColor' } const editorHighlight: AdfMark = { attrs: { color: '#f8e6a0' }, type: 'backgroundColor' }
const hreflessLink = 'the link mark spells its href: this one spells none' const hreflessLink = 'the link mark spells its href: this one spells none'
const imageAlone = 'an image fits only as a paragraph of its own: this one sits inside other content' const imageAlone = 'an image fits only as a paragraph of its own: this one sits inside other content'
@@ -93,7 +93,7 @@ export function parseInlineContent(source: string, definitions: LinkDefinitions,
if (!takesDirective.ok) return takesDirective if (!takesDirective.ok) return takesDirective
if (!takesDirective.value) return failure('unsupported-node-shape', spellableLink, path) if (!takesDirective.value) return failure('unsupported-node-shape', spellableLink, path)
} }
return success({ carry: scanned.value.carry, nodes: nodes.value }) return success({ nodes: nodes.value })
} }
function freshScan(source: string, shared: SharedScan, own: Pick<Scan, 'container' | 'highlights' | 'spans'>): Scan { function freshScan(source: string, shared: SharedScan, own: Pick<Scan, 'container' | 'highlights' | 'spans'>): Scan {
@@ -236,7 +236,7 @@ function directivePiece(scan: Scan, span: DirectiveSpan, index: number): Result<
const carried = readCarriedInline(span) const carried = readCarriedInline(span)
if (carried !== undefined) { if (carried !== undefined) {
if (carried.fault !== undefined) return faulted(carried.fault, scan.path) if (carried.fault !== undefined) return faulted(carried.fault, scan.path)
return success({ kind: 'carry', node: carried.value }) return success({ kind: 'ownMarks', node: carried.value })
} }
if (span.name === textBreakName) return span.content === undefined && span.attributes.size === 0 ? success(textBreak) : failure('unsupported-node-shape', `${textBreakName} spells the bare leaf form, ${textBreakSpelling}: this one spells more`, scan.path) if (span.name === textBreakName) return span.content === undefined && span.attributes.size === 0 ? success(textBreak) : failure('unsupported-node-shape', `${textBreakName} spells the bare leaf form, ${textBreakSpelling}: this one spells more`, scan.path)
const text = readTextDirective(span) const text = readTextDirective(span)
@@ -250,14 +250,14 @@ function directivePiece(scan: Scan, span: DirectiveSpan, index: number): Result<
if (content?.some(isTextBreak) === true) return failure('unsupported-node-shape', `delete ${textBreakSpelling} from this content slot: a slot holds one text node`, scan.path) if (content?.some(isTextBreak) === true) return failure('unsupported-node-shape', `delete ${textBreakSpelling} from this content slot: a slot holds one text node`, scan.path)
const node = readInlineDirectiveNode(span.name, span.attributes, content === undefined ? undefined : adfNodes(content), scan.path) const node = readInlineDirectiveNode(span.name, span.attributes, content === undefined ? undefined : adfNodes(content), scan.path)
if (!node.ok) return node if (!node.ok) return node
return success(node.value.marks === undefined ? { kind: 'nodes', nodes: [node.value] } : { kind: 'carry', node: node.value }) return success(node.value.marks === undefined ? { kind: 'nodes', nodes: [node.value] } : { kind: 'ownMarks', node: node.value })
} }
function directiveMarkPiece(scan: Scan, name: string, mark: AdfMark, slot: SlotContent | undefined, index: number): Result<Piece> { function directiveMarkPiece(scan: Scan, name: string, mark: AdfMark, slot: SlotContent | undefined, index: number): Result<Piece> {
if (slot === undefined || slot.nodes.length === 0) { if (slot === undefined || slot.nodes.length === 0) {
return failure('unsupported-node-shape', `the ${name} mark wraps the [content] it marks: this one wraps none`, scan.path) return failure('unsupported-node-shape', `the ${name} mark wraps the [content] it marks: this one wraps none`, scan.path)
} }
if (slot.carry) return failure('unsupported-node-shape', carriedInMark, scan.path) if (slot.nodes.some(holdsOwnMarks)) return failure('unsupported-node-shape', ownMarksInMark, scan.path)
const refused = mark.type === 'link' ? refuseLinkDirective(scan, mark, slot.nodes, index) : undefined const refused = mark.type === 'link' ? refuseLinkDirective(scan, mark, slot.nodes, index) : undefined
if (refused !== undefined) return refused if (refused !== undefined) return refused
return success({ kind: 'nodes', nodes: applyMark(slot.nodes, mark) }) return success({ kind: 'nodes', nodes: applyMark(slot.nodes, mark) })
@@ -299,7 +299,7 @@ function assemble(scan: Scan): Result<Scanned> {
if (holdsImage(scan.pieces)) return failure('unmappable-image', imageAlone, scan.path) if (holdsImage(scan.pieces)) return failure('unmappable-image', imageAlone, scan.path)
const nodes = resolveNodes(scan.pieces, scan, true) const nodes = resolveNodes(scan.pieces, scan, true)
if (!nodes.ok) return nodes if (!nodes.ok) return nodes
return success({ carry: holdsCarry(scan.pieces), nodes: nodes.value }) return success({ nodes: nodes.value })
} }
// spec/flavour.md, Inline nodes: the leaf builds no node, so only the pair it parts spells it. // spec/flavour.md, Inline nodes: the leaf builds no node, so only the pair it parts spells it.
@@ -327,7 +327,7 @@ function isTextBreak(item: Inline): item is TextBreak {
return 'kind' in item && item.kind === 'textBreak' return 'kind' in item && item.kind === 'textBreak'
} }
// The nodes the items hold, carried ones unwrapped and text breaks dropped. // The nodes the items hold, own-marks ones unwrapped and text breaks dropped.
function adfNodes(items: readonly Inline[]): AdfNode[] { function adfNodes(items: readonly Inline[]): AdfNode[] {
const nodes: AdfNode[] = [] const nodes: AdfNode[] = []
for (const item of items) { for (const item of items) {
@@ -337,15 +337,15 @@ function adfNodes(items: readonly Inline[]): AdfNode[] {
return nodes return nodes
} }
function holdsCarry(pieces: readonly Piece[]): boolean { function holdsOwnMarks(item: Inline | Piece): boolean {
return pieces.some((piece) => piece.kind === 'carry') return 'kind' in item && item.kind === 'ownMarks'
} }
function holdsImage(pieces: readonly Piece[]): boolean { function holdsImage(pieces: readonly Piece[]): boolean {
return pieces.some((piece) => piece.kind === 'image') return pieces.some((piece) => piece.kind === 'image')
} }
// A carry rides its own piece: the carried node's marks restore with it rather than riding a spelling, so the guard below answers for it. // A node whose marks are its own rides its own piece, its marks restoring with it rather than riding a spelling, so the guard below answers for it.
function holdsLink(pieces: readonly Piece[]): boolean { function holdsLink(pieces: readonly Piece[]): boolean {
return pieces.some((piece) => piece.kind === 'nodes' && marksLink(piece.nodes)) return pieces.some((piece) => piece.kind === 'nodes' && marksLink(piece.nodes))
} }
@@ -447,7 +447,7 @@ function closeLink(scan: Scan, at: number, inner: readonly Piece[], definition:
return success(false) return success(false)
} }
if (holdsImage(inner)) return failure('unmappable-image', imageAlone, scan.path) if (holdsImage(inner)) return failure('unmappable-image', imageAlone, scan.path)
if (holdsCarry(inner)) return failure('unsupported-node-shape', carriedInMark, scan.path) if (inner.some(holdsOwnMarks)) return failure('unsupported-node-shape', ownMarksInMark, scan.path)
const resolved = resolveNodes(inner, scan, true) const resolved = resolveNodes(inner, scan, true)
if (!resolved.ok) return resolved if (!resolved.ok) return resolved
const nodes = resolved.value const nodes = resolved.value
@@ -506,12 +506,12 @@ function resolveNodes(pieces: readonly Piece[], scan: Scan, highlights: boolean)
const runs = delimiterRuns(pieces) const runs = delimiterRuns(pieces)
const pairings = matchEmphasis(runs) const pairings = matchEmphasis(runs)
writeUnpaired(nodes, runs, pairings) writeUnpaired(nodes, runs, pairings)
if (!markPairings(pieces, nodes, pairings)) return failure('unsupported-node-shape', carriedInMark, scan.path) if (!markPairings(pieces, nodes, pairings)) return failure('unsupported-node-shape', ownMarksInMark, scan.path)
markHighlights(pieces, nodes, highlights ? pairedHighlights(pieces, nodes) : []) markHighlights(pieces, nodes, highlights ? pairedHighlights(pieces, nodes) : [])
return success(mergeReadText(nodes.flat())) return success(mergeReadText(nodes.flat()))
} }
// A text break and a carried node are walls: the text on either side joins only its own side. // A text break and a node whose marks are its own are walls: the text on either side joins only its own side.
function mergeReadText(items: readonly Inline[]): Inline[] { function mergeReadText(items: readonly Inline[]): Inline[] {
const merged: Inline[] = [] const merged: Inline[] = []
let run: AdfNode[] = [] let run: AdfNode[] = []
@@ -532,7 +532,7 @@ function mergeReadText(items: readonly Inline[]): Inline[] {
// A highlight delimiter holds an empty text node until it pairs, so the emphasis around it marks it. // A highlight delimiter holds an empty text node until it pairs, so the emphasis around it marks it.
function pieceNodes(piece: Piece): Inline[] { function pieceNodes(piece: Piece): Inline[] {
switch (piece.kind) { switch (piece.kind) {
case 'carry': case 'ownMarks':
return [piece] return [piece]
case 'highlight': case 'highlight':
return [{ text: '', type: 'text' }] return [{ text: '', type: 'text' }]
@@ -577,7 +577,7 @@ function markPairings(pieces: readonly Piece[], nodes: Inline[][], pairings: rea
for (const pairing of pairings) { for (const pairing of pairings) {
const mark: AdfMark = { type: markType(pairing.opener.character, pairing.used) } const mark: AdfMark = { type: markType(pairing.opener.character, pairing.used) }
for (let index = pairing.opener.index + 1; index < pairing.closer.index; index += 1) { for (let index = pairing.opener.index + 1; index < pairing.closer.index; index += 1) {
if (pieces[index]?.kind === 'carry') return false if (pieces[index]?.kind === 'ownMarks') return false
nodes[index] = applyMark(nodes[index] ?? [], mark) nodes[index] = applyMark(nodes[index] ?? [], mark)
} }
} }