12b: the content slot refuses a carried text node's content too, and the reserved-name message names the live fence
CI / gate (push) Successful in 24s
CI / publish (push) Waiting to run

This commit was merged in pull request #84.
This commit is contained in:
2026-09-16 10:26:12 +02:00
parent 65c09bdbae
commit 9ba2d09ee3
3 changed files with 18 additions and 14 deletions
+10 -7
View File
@@ -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` **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 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 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: `\!adf:` is the literal, block and inline alike. A construct CommonMark binds first — a code span,
the blockquote or list item whose paragraph CommonMark would fold it into closes instead. 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. **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 `[content]` is inline markdown; brackets inside balance as in CommonMark link text, `\]` for a
@@ -416,11 +419,11 @@ Right.
## Inline nodes ## Inline nodes
Attributes and the carry fallback read as in the block sections, the carry in its inline form. Of 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 the nodes below, `emoji`, `mention` and `status` spell their `text` attribute in the content slot as
as plain text: `[]` is the empty string, absent content is the absent attribute, non-empty content 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 parsing to anything but one text node carrying neither marks, attributes nor content — adjacent text
with identical marks and no attributes merged first — is a named error, and so is a `text` key in nodes with identical marks and no attributes merged first — is a named error, and so is a `text` key
`{attrs}`. An enclosing mark spelling does not reach into the slot. The rest take no content, 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. `!adf:text` included; content on a node that takes none is a named error.
- `date` — Attributes: `localId` (string), `timestamp` (string, epoch milliseconds). - `date` — Attributes: `localId` (string), `timestamp` (string, epoch milliseconds).
+5 -5
View File
@@ -3,11 +3,11 @@ import type { BlockDirective } from '../../adf/block-directives.ts'
import type { ConvertFault } from '../../result.ts' import type { ConvertFault } from '../../result.ts'
import type { DirectiveAttributes, DirectiveValue } from '../directive-syntax.ts' import type { DirectiveAttributes, DirectiveValue } from '../directive-syntax.ts'
import type { Elsewhere } from './directive-attributes.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 { attributeValue, directiveLineEscape, directivePrefix, inlineDirectiveEscape, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
import { blockArgument } from '../block-directive-arguments.ts' import { blockArgument } from '../block-directive-arguments.ts'
import { blockDirective } from '../../adf/block-directives.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 { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
import { inlineDirective } from '../../adf/inline-directives.ts' import { inlineDirective } from '../../adf/inline-directives.ts'
import { inlineMarkSpellingFault } from './directive-marks.ts' import { inlineMarkSpellingFault } from './directive-marks.ts'
@@ -26,7 +26,7 @@ export function readBlockDirectiveNode(
path: ConvertErrorPath, path: ConvertErrorPath,
): Result<BlockDirectiveNode> { ): Result<BlockDirectiveNode> {
if (name === carryName) { 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) const directive = blockDirective(name)
if (directive === undefined) return faulted(inlineSpellingFault(name) ?? unknownDirectiveFault(name, directiveLineEscape), path) if (directive === undefined) return faulted(inlineSpellingFault(name) ?? unknownDirectiveFault(name, directiveLineEscape), path)
@@ -62,7 +62,7 @@ export function readInlineDirectiveNode(
if (slot !== undefined && content !== undefined) { if (slot !== undefined && content !== undefined) {
const text = slotText(content) const text = slotText(content)
if (text === undefined) { 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) const spans = slotLineEndingFault(name, text)
if (spans !== undefined) return faulted(spans, path) if (spans !== undefined) return faulted(spans, path)
@@ -91,7 +91,7 @@ function blockSpellingFault(name: string): ConvertFault | undefined {
function slotText(content: readonly AdfNode[]): string | undefined { function slotText(content: readonly AdfNode[]): string | undefined {
if (content.length === 0) return '' if (content.length === 0) return ''
const only = content.length === 1 ? content[0] : undefined 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 return only.text
} }
+3 -2
View File
@@ -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', () => { 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\n')), reserved)
assert.equal(content(markdownToAdf(':::carry\nx\n:::\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' }]) 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', () => { 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[**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="{\\"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[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(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') 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')