From 9ba2d09ee37ce82d88dac08ca34595ac7043c168 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Wed, 16 Sep 2026 10:26:12 +0200 Subject: [PATCH] 12b: the content slot refuses a carried text node's content too, and the reserved-name message names the live fence --- spec/flavour.md | 17 ++++++++++------- src/markdown/parse/directive-nodes.ts | 10 +++++----- src/markdown/parse/markdown-to-adf.test.ts | 5 +++-- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/spec/flavour.md b/spec/flavour.md index d38046b..5328870 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -87,8 +87,11 @@ info string, and `listBreak` for the leaf that parts two adjacent lists (Canonic **Claiming**: an unescaped `!adf:` claims wherever it stands. What follows picks the form: `/name` closes a container, and a name picks by what follows it in turn — a space or the line's end a block line, `[` or `{` an inline directive. A `!adf:` completing none of the three is a named error, and -`\!adf:` is the literal, block and inline alike. A claimed block line also ends a lazy continuation: -the blockquote or list item whose paragraph CommonMark would fold it into closes instead. +`\!adf:` is the literal, block and inline alike. A construct CommonMark binds first — a code span, +an autolink URI, a link destination or title — holds its `!adf:` as content; a directive's +`[content]`, which binds ahead of bracket matching, does not. A claimed block line also ends a lazy +continuation: the blockquote or list item whose paragraph CommonMark would fold it into closes +instead. **Inline**: `!adf:name[content]{attrs}`, on one line — an inline directive never spans lines. `[content]` is inline markdown; brackets inside balance as in CommonMark link text, `\]` for a @@ -416,11 +419,11 @@ Right. ## Inline nodes Attributes and the carry fallback read as in the block sections, the carry in its inline form. Of -the nodes below, `emoji`, `mention` and `status` spell their `text` attribute in the content slot -as plain text: `[]` is the empty string, absent content is the absent attribute, non-empty content -parsing to anything but one text node carrying neither marks nor attributes — adjacent text nodes -with identical marks and no attributes merged first — is a named error, and so is a `text` key in -`{attrs}`. An enclosing mark spelling does not reach into the slot. The rest take no content, +the nodes below, `emoji`, `mention` and `status` spell their `text` attribute in the content slot as +plain text: `[]` is the empty string, absent content is the absent attribute, non-empty content +parsing to anything but one text node carrying neither marks, attributes nor content — adjacent text +nodes with identical marks and no attributes merged first — is a named error, and so is a `text` key +in `{attrs}`. An enclosing mark spelling does not reach into the slot. The rest take no content, `!adf:text` included; content on a node that takes none is a named error. - `date` — Attributes: `localId` (string), `timestamp` (string, epoch milliseconds). diff --git a/src/markdown/parse/directive-nodes.ts b/src/markdown/parse/directive-nodes.ts index 020b89c..eda3b72 100644 --- a/src/markdown/parse/directive-nodes.ts +++ b/src/markdown/parse/directive-nodes.ts @@ -3,11 +3,11 @@ import type { BlockDirective } from '../../adf/block-directives.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, nodeMarks } from '../../adf/document.ts' +import { attributeNestingMessage, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts' import { attributeValue, directiveLineEscape, directivePrefix, inlineDirectiveEscape, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts' import { blockArgument } from '../block-directive-arguments.ts' import { blockDirective } from '../../adf/block-directives.ts' -import { carryName } from '../opaque-carry.ts' +import { carryFence, carryName } from '../opaque-carry.ts' import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts' import { inlineDirective } from '../../adf/inline-directives.ts' import { inlineMarkSpellingFault } from './directive-marks.ts' @@ -26,7 +26,7 @@ export function readBlockDirectiveNode( path: ConvertErrorPath, ): Result { if (name === carryName) { - return failure('malformed-directive', `the name ${carryName} is reserved for the opaque carry, whose block form is the fence`, path) + return failure('malformed-directive', `the name ${carryName} is reserved for the opaque carry, whose block form is the ${carryFence} fence`, path) } const directive = blockDirective(name) if (directive === undefined) return faulted(inlineSpellingFault(name) ?? unknownDirectiveFault(name, directiveLineEscape), path) @@ -62,7 +62,7 @@ export function readInlineDirectiveNode( if (slot !== undefined && content !== undefined) { const text = slotText(content) if (text === undefined) { - return failure('unsupported-node-shape', `the ${name} content slot holds one text node carrying neither marks nor attributes: this one holds something else`, path) + return failure('unsupported-node-shape', `the ${name} content slot holds one text node carrying neither marks, attributes nor content: this one holds something else`, path) } const spans = slotLineEndingFault(name, text) if (spans !== undefined) return faulted(spans, path) @@ -91,7 +91,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 || typeof only.text !== 'string') return 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 return only.text } diff --git a/src/markdown/parse/markdown-to-adf.test.ts b/src/markdown/parse/markdown-to-adf.test.ts index 81f527a..2fe973a 100644 --- a/src/markdown/parse/markdown-to-adf.test.ts +++ b/src/markdown/parse/markdown-to-adf.test.ts @@ -312,7 +312,7 @@ test('names the position a directive name the other one spells belongs to', () = }) test('names the reserved carry name a block directive spells', () => { - const reserved = 'malformed-directive: the name carry is reserved for the opaque carry, whose block form is the fence' + const reserved = 'malformed-directive: the name carry is reserved for the opaque carry, whose block form is the adf fence' assert.equal(content(markdownToAdf('::carry\n')), reserved) assert.equal(content(markdownToAdf(':::carry\nx\n:::\n')), reserved) assert.deepEqual(content(markdownToAdf('```adfx\nx\n```\n')), [{ attrs: { language: 'adfx' }, content: [text('x')], type: 'codeBlock' }]) @@ -876,9 +876,10 @@ test('reads the content slot as the text attribute the node spells there', () => }) test('names the content slot no lone plain text node reads back from', () => { - const named = 'unsupported-node-shape: the status content slot holds one text node carrying neither marks nor attributes: this one holds something else' + const named = 'unsupported-node-shape: the status content slot holds one text node carrying neither marks, attributes nor content: this one holds something else' assert.equal(content(markdownToAdf('!adf:status[**A**]{color=yellow}\n')), named) assert.equal(content(markdownToAdf('!adf:status[!adf:carry{json="{\\"attrs\\":{\\"localId\\":\\"a\\"},\\"text\\":\\"A\\",\\"type\\":\\"text\\"}"}]{color=yellow}\n')), named) + assert.equal(content(markdownToAdf('!adf:status[!adf:carry{json="{\\"content\\":[{\\"text\\":\\"B\\",\\"type\\":\\"text\\"}],\\"text\\":\\"A\\",\\"type\\":\\"text\\"}"}]{color=yellow}\n')), named) assert.equal(code(markdownToAdf('!adf:status[a`b`]{color=yellow}\n')), 'unsupported-node-shape') assert.equal(code(markdownToAdf('!adf:status[!adf:date{timestamp=1}]{color=yellow}\n')), 'unsupported-node-shape') assert.equal(content(markdownToAdf('!adf:status[![a](/u)]{color=yellow}\n')), 'unmappable-image: an image fits only as a paragraph of its own: this one sits inside other content')