From a565d49c24859082e9aca113fb33cd6d269f6510 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sat, 3 Oct 2026 14:44:00 +0200 Subject: [PATCH] Spell 'a text node holding its text and marks alone' once, as isPlainText --- corpus/errors/directive-slot-carried-marks.error | 1 + corpus/errors/directive-slot-carried-marks.md | 1 + src/adf/document.ts | 5 +++++ src/markdown/emit/adf-to-markdown.ts | 4 ++-- src/markdown/emit/inline-line.ts | 10 +++++----- src/markdown/parse/directive-nodes.ts | 4 ++-- src/markdown/text-break.ts | 8 ++------ 7 files changed, 18 insertions(+), 15 deletions(-) create mode 100644 corpus/errors/directive-slot-carried-marks.error create mode 100644 corpus/errors/directive-slot-carried-marks.md diff --git a/corpus/errors/directive-slot-carried-marks.error b/corpus/errors/directive-slot-carried-marks.error new file mode 100644 index 0000000..a811539 --- /dev/null +++ b/corpus/errors/directive-slot-carried-marks.error @@ -0,0 +1 @@ +unsupported-node-shape diff --git a/corpus/errors/directive-slot-carried-marks.md b/corpus/errors/directive-slot-carried-marks.md new file mode 100644 index 0000000..c593b3e --- /dev/null +++ b/corpus/errors/directive-slot-carried-marks.md @@ -0,0 +1 @@ +!adf:status[!adf:carry{json="{\"marks\":[],\"text\":\"x\",\"type\":\"text\"}"}] diff --git a/src/adf/document.ts b/src/adf/document.ts index 752e517..1ae004c 100644 --- a/src/adf/document.ts +++ b/src/adf/document.ts @@ -64,6 +64,11 @@ export function emptyKeys(held: { attrs?: AdfAttributes; content?: AdfNode[]; ma return keys } +// A text node holding its text and marks alone: a format spells it as text, where anything more rides a carry. +export function isPlainText(node: AdfNode): boolean { + return node.type === 'text' && node.attrs === undefined && node.content === undefined && node.marks?.length !== 0 +} + export function identicalMark(left: AdfMark, right: AdfMark): boolean { return marksKey([left]) === marksKey([right]) } diff --git a/src/markdown/emit/adf-to-markdown.ts b/src/markdown/emit/adf-to-markdown.ts index 1a63f78..867dfc8 100644 --- a/src/markdown/emit/adf-to-markdown.ts +++ b/src/markdown/emit/adf-to-markdown.ts @@ -1,7 +1,7 @@ import type { AdfDocument, AdfNode } from '../../adf/document.ts' import type { BlockNodeModel } from '../../adf/block-nodes.ts' import type { Flavour } from '../plain-conventions.ts' -import { adfDocumentFault, carriesOnly, nodeAttrs, nodeContent } from '../../adf/document.ts' +import { adfDocumentFault, carriesOnly, isPlainText, nodeAttrs, nodeContent } from '../../adf/document.ts' import { alertMarker, foldedAlertMarker, leadingMarker, readAlertMarker, readTaskMarker, taskMarker } from '../plain-conventions.ts' import { blockDirectiveForm, documentSpelling, listBreakSpelling } from '../block-directive.ts' import { blockNodeModel, blockNodes } from '../../adf/block-nodes.ts' @@ -278,7 +278,7 @@ function emitCodeDirective(node: AdfNode, model: BlockNodeModel, path: ConvertEr // The text each fence holds, or `undefined` where a child is no plain text node, which the carry holds instead. function fencedTexts(node: AdfNode, path: ConvertErrorPath): Result { if (node.content === undefined) return success(['']) - if (node.content.some((child) => child.type !== 'text' || child.attrs !== undefined || child.content !== undefined || child.marks !== undefined)) return success(undefined) + if (node.content.some((child) => !isPlainText(child) || child.marks !== undefined)) return success(undefined) const texts: string[] = [] for (const [index, child] of node.content.entries()) { const childPath = [...path, 'content', index] diff --git a/src/markdown/emit/inline-line.ts b/src/markdown/emit/inline-line.ts index 5d4c3d2..6be52b0 100644 --- a/src/markdown/emit/inline-line.ts +++ b/src/markdown/emit/inline-line.ts @@ -6,7 +6,7 @@ import { assembleInlineLine, isSyntax, type InlineEscaping, type InlineSegment, import { carriedInline } from '../opaque-carry.ts' import { claimsLine, holdsNullCharacter, trimTrailingSpace } from '../commonmark/grammar.ts' import { commonMarkLink, linkHref, markSpelling, spellMarkAttributes } from '../mark-spellings.ts' -import { emptyKeys, identicalMark, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts' +import { identicalMark, isPlainText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts' import { escapeUnbalanced, spellDestination } from '../commonmark/link-syntax.ts' import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts' import { highlightDelimiter } from '../plain-conventions.ts' @@ -268,9 +268,9 @@ function emitInlineDirective(node: AdfNode, model: InlineNodeModel, index: numbe } function emitText(node: AdfNode, context: InlineContext, index: number, path: ConvertErrorPath): Result { - if (node.attrs !== undefined || emptyKeys(node).length > 0) return success({ carry: { first: index, last: index } }) - if (typeof node.text !== 'string' || node.text === '') return failure('unsupported-node-shape', 'a text node holds text: this one has none', path) if (nodeContent(node).length > 0) return failure('unsupported-node-shape', 'a text node holds no content: this one holds some', path) + if (!isPlainText(node)) return success({ carry: { first: index, last: index } }) + if (typeof node.text !== 'string' || node.text === '') return failure('unsupported-node-shape', 'a text node holds text: this one has none', path) if (/\r/.test(node.text)) return failure('unspellable-character', 'a text node holds a carriage return CommonMark rewrites', path) if (holdsNullCharacter(node.text)) return failure('unspellable-character', 'a text node holds a null character CommonMark replaces', path) const escaping: InlineEscaping = context.bracketed ? 'bracketed' : 'backslash' @@ -323,10 +323,10 @@ function emitHighlight(nodes: readonly AdfNode[], depth: number, range: NodeRang function emitCodeSpan(nodes: readonly AdfNode[], depth: number, range: NodeRange, path: ConvertErrorPath): Result { const spans: string[] = [] for (const node of nodes) { - if (node.type !== 'text' || nodeMarks(node).length !== depth + 1 || node.attrs !== undefined || emptyKeys(node).length > 0) return success({ carry: range }) + if (node.type === 'text' && nodeContent(node).length > 0) return failure('unsupported-node-shape', 'a text node holds no content: this one holds some', path) + if (!isPlainText(node) || nodeMarks(node).length !== depth + 1) return success({ carry: range }) const { text } = node if (typeof text !== 'string' || text === '') return failure('unsupported-node-shape', 'a text node holds text: this one has none', path) - if (nodeContent(node).length > 0) return failure('unsupported-node-shape', 'a text node holds no content: this one holds some', path) if (/[\n\r]/.test(text)) return success({ carry: range }) if (holdsNullCharacter(text)) return failure('unspellable-character', 'a code span holds a null character CommonMark replaces', path) const fence = '`'.repeat(longestBacktickRun(text) + 1) diff --git a/src/markdown/parse/directive-nodes.ts b/src/markdown/parse/directive-nodes.ts index c738fb1..3aab5db 100644 --- a/src/markdown/parse/directive-nodes.ts +++ b/src/markdown/parse/directive-nodes.ts @@ -3,7 +3,7 @@ import type { BlockNodeModel } from '../../adf/block-nodes.ts' import type { ConvertFault } from '../../result.ts' import type { DirectiveAttributes, DirectiveValue } from '../directive-syntax.ts' import type { Elsewhere } from './directive-attributes.ts' -import { attributeNestingMessage, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts' +import { attributeNestingMessage, isPlainText } from '../../adf/document.ts' import { attributeValue, directivePrefix, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts' import { blockArgument, blockDirectiveForm, marksAttribute, readMarkValues } from '../block-directive.ts' import { blockNodeModel } from '../../adf/block-nodes.ts' @@ -95,7 +95,7 @@ function blockSpellingFault(name: string): ConvertFault | undefined { function slotText(content: readonly AdfNode[]): string | undefined { if (content.length === 0) return '' const only = content.length === 1 ? content[0] : undefined - if (only?.type !== 'text' || nodeMarks(only).length > 0 || Object.keys(nodeAttrs(only)).length > 0 || nodeContent(only).length > 0 || typeof only.text !== 'string') return undefined + if (only === undefined || !isPlainText(only) || only.marks !== undefined || typeof only.text !== 'string') return undefined return only.text } diff --git a/src/markdown/text-break.ts b/src/markdown/text-break.ts index 994ae19..2055539 100644 --- a/src/markdown/text-break.ts +++ b/src/markdown/text-break.ts @@ -1,5 +1,5 @@ import type { AdfNode } from '../adf/document.ts' -import { identicalMarks, nodeMarks } from '../adf/document.ts' +import { identicalMarks, isPlainText, nodeMarks } from '../adf/document.ts' import { spellInlineLeafDirective } from './directive-syntax.ts' export const textBreakName = 'textBreak' @@ -8,9 +8,5 @@ export const textBreakSpelling = spellInlineLeafDirective(textBreakName, '') // Whether CommonMark reads the pair back as one text node, where neither rides the carry (spec/flavour.md, Inline nodes). export function readsAsOne(previous: AdfNode, node: AdfNode): boolean { - return spelledAsText(previous) && spelledAsText(node) && identicalMarks(nodeMarks(previous), nodeMarks(node)) -} - -function spelledAsText(node: AdfNode): boolean { - return node.type === 'text' && node.attrs === undefined && node.content === undefined && node.marks?.length !== 0 + return isPlainText(previous) && isPlainText(node) && identicalMarks(nodeMarks(previous), nodeMarks(node)) }