12b - The inline directive forms read and spell the !adf: prefix #84

Merged
lilleman merged 3 commits from inline-prefix-12b into main 2026-09-16 10:27:44 +02:00
3 changed files with 18 additions and 14 deletions
Showing only changes of commit 9ba2d09ee3 - Show all commits
+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`
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).
+5 -5
View File
@@ -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<BlockDirectiveNode> {
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
}
+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', () => {
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')