From a62cd398091c6ea09fccccc0bca8a07a6abd1167 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 5 Oct 2026 09:32:11 +0200 Subject: [PATCH] Answer architecture round 2: rename isBareText to isSpellableText, say which link holdsLink counts, the README names the type passed --- README.md | 2 +- src/adf/document.ts | 6 +++--- src/markdown/adjacent-text.ts | 4 ++-- src/markdown/emit/adf-to-markdown.ts | 4 ++-- src/markdown/emit/inline-line.ts | 8 ++++---- src/markdown/parse/directive-nodes.ts | 4 ++-- src/markdown/parse/inline-content.ts | 3 +-- 7 files changed, 15 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 64e2e50..15fef1a 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ Parsing — `markdownToAdf` and `plainMarkdownToAdf`, and `htmlToAdf` at `0.2.0` | --- | --- | --- | | `malformed-directive` | an `!adf:` the grammar cannot read — a prefix completing no directive, an unclosed container, `[content]` or `{attrs}`, a closer with no container of its name open, a leaf given a body, `{attrs}` out of order or duplicated, invalid JSON in an opaque carry | write the spelling the message names, or keep it literal: escape the prefix — `\!adf:`, block and inline alike — or, for a code fence whose info string opens `adf:`, drop the info string and wrap the fence in `!adf:codeBlock {language="adf:…"}` | | `malformed-pipe-table` | a pipe row that is no pipe table — a missing or ragged `---` delimiter row, an alignment colon in it, or a row not opening with a pipe | open every row with a pipe and give the delimiter row the header's cell count; to keep the lines literal text instead, escape the leading pipe of every one — escaping a single row leaves the next to open a fresh table and fail the same way | -| `not-a-string` | the value handed in is not a string: `null`, `undefined`, a number, an object | pass the markdown as a string; the message names what was passed | +| `not-a-string` | the value handed in is not a string: `null`, `undefined`, a number, an object | pass the markdown as a string; the message names the type passed | | `unknown-directive-name` | a directive whose name is no node or mark this version spells | check the name in `spec/flavour.md`, or escape the prefix as `\!adf:`; the spelling itself is well formed, so a later minor may give the name meaning | | `unmappable-html` | the input holds an HTML construct the documented element set does not map, a comment and a processing instruction among them — at this version that is every raw HTML construct in markdown, the element set landing at `0.2.0` | remove the construct, or write what it holds in the lossless flavour | | `unmappable-image` | an image sits inside other content that is not another image's description, or carries a title | give the image a paragraph of its own and drop the title | diff --git a/src/adf/document.ts b/src/adf/document.ts index 5945edb..c759b6c 100644 --- a/src/adf/document.ts +++ b/src/adf/document.ts @@ -66,12 +66,12 @@ export function emptyKeys(held: { attrs?: AdfAttributes; content?: AdfNode[]; ma } // No attrs, content or empty marks; a format may spell this node as text, and anything more rides a carry. -export function isBareText(node: AdfNode): boolean { +export function isSpellableText(node: AdfNode): boolean { return node.type === 'text' && node.attrs === undefined && node.content === undefined && node.marks?.length !== 0 } -export function isUnmarkedBareText(node: AdfNode): boolean { - return isBareText(node) && node.marks === undefined +export function isUnmarkedSpellableText(node: AdfNode): boolean { + return isSpellableText(node) && node.marks === undefined } export function identicalMark(left: AdfMark, right: AdfMark): boolean { diff --git a/src/markdown/adjacent-text.ts b/src/markdown/adjacent-text.ts index 851f2a3..7bfe097 100644 --- a/src/markdown/adjacent-text.ts +++ b/src/markdown/adjacent-text.ts @@ -1,5 +1,5 @@ import type { AdfNode } from '../adf/document.ts' -import { identicalMarks, isBareText, nodeMarks } from '../adf/document.ts' +import { identicalMarks, isSpellableText, nodeMarks } from '../adf/document.ts' import { spellInlineLeafDirective } from './directive-syntax.ts' export const textBreakName = 'textBreak' @@ -8,5 +8,5 @@ export const textBreakSpelling = spellInlineLeafDirective(textBreakName, '') // The lossless reader's rule: CommonMark reads the pair back as one text run (spec/flavour.md, Inline nodes). export function joinsWhenRead(previous: AdfNode, node: AdfNode): boolean { - return isBareText(previous) && isBareText(node) && identicalMarks(nodeMarks(previous), nodeMarks(node)) + return isSpellableText(previous) && isSpellableText(node) && identicalMarks(nodeMarks(previous), nodeMarks(node)) } diff --git a/src/markdown/emit/adf-to-markdown.ts b/src/markdown/emit/adf-to-markdown.ts index 5b2a9c7..d5cd980 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, holdsOnlyAttributes, isUnmarkedBareText, nodeAttrs, nodeContent } from '../../adf/document.ts' +import { adfDocumentFault, holdsOnlyAttributes, isUnmarkedSpellableText, 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' @@ -277,7 +277,7 @@ function fencedTexts(node: AdfNode): string[] | undefined { if (node.content === undefined) return [''] const texts: string[] = [] for (const child of node.content) { - if (!isUnmarkedBareText(child) || typeof child.text !== 'string' || child.text === '' || /\r/.test(child.text) || holdsNullCharacter(child.text)) return undefined + if (!isUnmarkedSpellableText(child) || typeof child.text !== 'string' || child.text === '' || /\r/.test(child.text) || holdsNullCharacter(child.text)) return undefined texts.push(child.text) } return texts diff --git a/src/markdown/emit/inline-line.ts b/src/markdown/emit/inline-line.ts index f8ac5d3..411dd66 100644 --- a/src/markdown/emit/inline-line.ts +++ b/src/markdown/emit/inline-line.ts @@ -9,7 +9,7 @@ import { escapeUnbalanced, spellDestination } from '../commonmark/link-syntax.ts import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts' import { highlightDelimiter } from '../plain/conventions.ts' import { holdsNullCharacter, trimTrailingSpace } from '../commonmark/grammar.ts' -import { identicalMark, isBareText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts' +import { identicalMark, isSpellableText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts' import { inlineNodeModel } from '../../adf/inline-nodes.ts' import { joinsWhenRead, textBreakSpelling } from '../adjacent-text.ts' import { largestNesting } from '../../nesting.ts' @@ -220,7 +220,7 @@ function nodePath(context: InlineContext, index: number): ConvertErrorPath { // Decides the text-node and slot-fault carries up front, so a line holding many of them is emitted once. function carries(node: AdfNode, carried: ReadonlySet, index: number): boolean { if (carried.has(index)) return true - if (node.type === 'text') return !isBareText(node) || typeof node.text !== 'string' || node.text === '' + 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] @@ -265,7 +265,7 @@ function emitInlineDirective(node: AdfNode, model: InlineNodeModel, index: numbe // CommonMark reads a raw carriage return as a line ending and a null character as U+FFFD, so neither is written raw. function emitText(node: AdfNode, context: InlineContext, index: number): Emission { - if (!isBareText(node) || typeof node.text !== 'string' || node.text === '') return { carry: { first: index, last: index } } + if (!isSpellableText(node) || typeof node.text !== 'string' || node.text === '') return { carry: { first: index, last: index } } const escaping: InlineEscaping = context.bracketed ? 'bracketed' : 'backslash' const parts = node.text.split(/(\n+|\0+|\r)/).filter((part) => part !== '') return { segments: parts.map((part) => (part === '\r' ? syntax(' ') : /^[\n\0]/.test(part) ? textDirectiveSegment(part) : { escaping, text: part })) } @@ -316,7 +316,7 @@ function emitCodeSpan(nodes: readonly AdfNode[], depth: number, range: NodeRange const spans: string[] = [] for (const node of nodes) { const { text } = node - if (!isBareText(node) || nodeMarks(node).length !== depth + 1 || typeof text !== 'string' || text === '' || /[\n\r]/.test(text) || holdsNullCharacter(text)) return { carry: range } + if (!isSpellableText(node) || nodeMarks(node).length !== depth + 1 || typeof text !== 'string' || text === '' || /[\n\r]/.test(text) || holdsNullCharacter(text)) return { carry: range } const fence = '`'.repeat(longestBacktickRun(text) + 1) spans.push(`${fence}${needsPadding(text) ? ` ${text} ` : text}${fence}`) } diff --git a/src/markdown/parse/directive-nodes.ts b/src/markdown/parse/directive-nodes.ts index efeae1f..6dac22d 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, isUnmarkedBareText } from '../../adf/document.ts' +import { attributeNestingMessage, isUnmarkedSpellableText } 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' @@ -92,7 +92,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 === undefined || !isUnmarkedBareText(only) || typeof only.text !== 'string') return undefined + if (only === undefined || !isUnmarkedSpellableText(only) || typeof only.text !== 'string') return undefined return only.text } diff --git a/src/markdown/parse/inline-content.ts b/src/markdown/parse/inline-content.ts index 9e4410d..4eba6d9 100644 --- a/src/markdown/parse/inline-content.ts +++ b/src/markdown/parse/inline-content.ts @@ -327,7 +327,6 @@ function isTextBreak(item: Inline): item is TextBreak { return 'kind' in item && item.kind === 'textBreak' } -// The nodes the items hold, with text breaks dropped. function adfNodes(items: readonly Inline[]): AdfNode[] { const nodes: AdfNode[] = [] for (const item of items) { @@ -345,7 +344,7 @@ function holdsImage(pieces: readonly Piece[]): boolean { return pieces.some((piece) => piece.kind === 'image') } -// 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. +// An `ownMarks` piece restores its link with its node, so only a link a `nodes` piece spells counts. function holdsLink(pieces: readonly Piece[]): boolean { return pieces.some((piece) => piece.kind === 'nodes' && marksLink(piece.nodes)) }