Read the inline nodes and the marks back, and keep the whitespace CommonMark does not strip #41
+3
-1
@@ -62,7 +62,9 @@ node and mark names the sections below spell as directives. Recognition is synta
|
|||||||
name-set-independent: anything matching the forms below parses as a directive regardless of
|
name-set-independent: anything matching the forms below parses as a directive regardless of
|
||||||
whether the name is known, and an unknown name is an error result naming it — so output an old
|
whether the name is known, and an unknown name is an error result naming it — so output an old
|
||||||
emitter escaped stays escaped, and erroring input gaining meaning later is MINOR, never a reparse
|
emitter escaped stays escaped, and erroring input gaining meaning later is MINOR, never a reparse
|
||||||
(§8). The name `adf` is reserved for the opaque carry, as both directive name and fence info
|
(§8). Each name belongs to one position, and a name the other one spells — a mark or an inline
|
||||||
|
node written as a block directive, a block node written inline — is a different error, naming the
|
||||||
|
spelling it takes. The name `adf` is reserved for the opaque carry, as both directive name and fence info
|
||||||
string.
|
string.
|
||||||
|
|
||||||
**Inline**: `:name[content]{attrs}`, on one line — an inline directive never spans lines.
|
**Inline**: `:name[content]{attrs}`, on one line — an inline directive never spans lines.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { AdfMark } from '../../adf/document.ts'
|
import type { AdfMark } from '../../adf/document.ts'
|
||||||
|
import type { ConvertFault } from '../../result.ts'
|
||||||
import type { DirectiveAttributes } from '../directive-syntax.ts'
|
import type { DirectiveAttributes } from '../directive-syntax.ts'
|
||||||
import type { MarkSpelling } from '../mark-spellings.ts'
|
import type { MarkSpelling } from '../mark-spellings.ts'
|
||||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
@@ -15,6 +16,12 @@ export function readDirectiveMark(name: string, attributes: DirectiveAttributes,
|
|||||||
return success(Object.keys(attrs.value).length === 0 ? { type: name } : { attrs: attrs.value, type: name })
|
return success(Object.keys(attrs.value).length === 0 ? { type: name } : { attrs: attrs.value, type: name })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function markSpellingFault(name: string): ConvertFault | undefined {
|
||||||
|
const spelling = markSpelling(name)
|
||||||
|
if (spelling === undefined) return undefined
|
||||||
|
return { code: 'unsupported-node-shape', message: `${name} is spelled ${markdownForm(spelling) ?? `:${name}[…]`}, never as a block directive` }
|
||||||
|
}
|
||||||
|
|
||||||
function markdownForm(spelling: MarkSpelling): string | undefined {
|
function markdownForm(spelling: MarkSpelling): string | undefined {
|
||||||
switch (spelling.kind) {
|
switch (spelling.kind) {
|
||||||
case 'code':
|
case 'code':
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { AdfAttributes, AdfMark, AdfNode } from '../../adf/document.ts'
|
import type { AdfAttributes, AdfMark, AdfNode } from '../../adf/document.ts'
|
||||||
import type { BlockDirective } from '../../adf/block-directives.ts'
|
import type { BlockDirective } from '../../adf/block-directives.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 { attributeValue, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
|
import { attributeValue, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
|
||||||
@@ -8,9 +9,11 @@ import { blockDirective } from '../../adf/block-directives.ts'
|
|||||||
import { carryName } from '../opaque-carry.ts'
|
import { 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 { markSpellingFault } from './directive-marks.ts'
|
||||||
import { marksAttribute, readMarkValues } from '../block-directive-marks.ts'
|
import { marksAttribute, readMarkValues } from '../block-directive-marks.ts'
|
||||||
import { readVocabulary } from './directive-attributes.ts'
|
import { readVocabulary } from './directive-attributes.ts'
|
||||||
import { slotLineEndingFault } from '../directive-syntax.ts'
|
import { slotLineEndingFault } from '../directive-syntax.ts'
|
||||||
|
import { textDirectiveName } from '../text-directive.ts'
|
||||||
|
|
||||||
export type BlockDirectiveNode = { contentModel: BlockDirective['contentModel']; node: AdfNode }
|
export type BlockDirectiveNode = { contentModel: BlockDirective['contentModel']; node: AdfNode }
|
||||||
|
|
||||||
@@ -24,7 +27,7 @@ export function readBlockDirectiveNode(
|
|||||||
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 fence`, path)
|
||||||
}
|
}
|
||||||
const directive = blockDirective(name)
|
const directive = blockDirective(name)
|
||||||
if (directive === undefined) return faulted(unknownDirectiveFault(name), path)
|
if (directive === undefined) return faulted(inlineSpellingFault(name) ?? unknownDirectiveFault(name), path)
|
||||||
const argumentKey = blockArgument(name)
|
const argumentKey = blockArgument(name)
|
||||||
const rest = new Map(attributes)
|
const rest = new Map(attributes)
|
||||||
rest.delete(marksAttribute)
|
rest.delete(marksAttribute)
|
||||||
@@ -48,7 +51,7 @@ export function readInlineDirectiveNode(
|
|||||||
path: ConvertErrorPath,
|
path: ConvertErrorPath,
|
||||||
): Result<AdfNode> {
|
): Result<AdfNode> {
|
||||||
const directive = inlineDirective(name)
|
const directive = inlineDirective(name)
|
||||||
if (directive === undefined) return faulted(unknownDirectiveFault(name), path)
|
if (directive === undefined) return faulted(blockSpellingFault(name) ?? unknownDirectiveFault(name), path)
|
||||||
const slot = directive.textAttribute
|
const slot = directive.textAttribute
|
||||||
if (slot === undefined && content !== undefined) return failure('unsupported-node-shape', `${name} takes no content`, path)
|
if (slot === undefined && content !== undefined) return failure('unsupported-node-shape', `${name} takes no content`, path)
|
||||||
const elsewhere: Elsewhere | undefined = slot === undefined ? undefined : { key: slot, slot: 'content' }
|
const elsewhere: Elsewhere | undefined = slot === undefined ? undefined : { key: slot, slot: 'content' }
|
||||||
@@ -64,6 +67,19 @@ export function readInlineDirectiveNode(
|
|||||||
return success(namedNode(name, attrs.value, undefined))
|
return success(namedNode(name, attrs.value, undefined))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A name the other position spells names that spelling, never the code a later MINOR may fill (AGENTS.md §8).
|
||||||
|
function inlineSpellingFault(name: string): ConvertFault | undefined {
|
||||||
|
const mark = markSpellingFault(name)
|
||||||
|
if (mark !== undefined) return mark
|
||||||
|
if (inlineDirective(name) === undefined && name !== textDirectiveName) return undefined
|
||||||
|
return { code: 'unsupported-node-shape', message: `${name} takes the inline form, :${name}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
function blockSpellingFault(name: string): ConvertFault | undefined {
|
||||||
|
if (blockDirective(name) === undefined) return undefined
|
||||||
|
return { code: 'unsupported-node-shape', message: `${name} takes the block form, ::${name}` }
|
||||||
|
}
|
||||||
|
|
||||||
// spec/flavour.md, Inline nodes: the slot is plain text, its adjacent nodes already merged.
|
// spec/flavour.md, Inline nodes: the slot is plain text, its adjacent nodes already merged.
|
||||||
function slotText(content: readonly AdfNode[]): string | undefined {
|
function slotText(content: readonly AdfNode[]): string | undefined {
|
||||||
if (content.length === 0) return ''
|
if (content.length === 0) return ''
|
||||||
|
|||||||
@@ -251,6 +251,16 @@ test('names the directive name no node reads back to', () => {
|
|||||||
assert.deepEqual(path(markdownToAdf('Part.\n:::x\n')), ['content', 1])
|
assert.deepEqual(path(markdownToAdf('Part.\n:::x\n')), ['content', 1])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('names the position a directive name the other one spells belongs to', () => {
|
||||||
|
assert.equal(content(markdownToAdf(':::em\na\n:::\n')), 'unsupported-node-shape: em is spelled _x_, never as a block directive')
|
||||||
|
assert.equal(content(markdownToAdf('::underline\n')), 'unsupported-node-shape: underline is spelled :underline[…], never as a block directive')
|
||||||
|
assert.equal(content(markdownToAdf('::text {text=" "}\n')), 'unsupported-node-shape: text takes the inline form, :text')
|
||||||
|
assert.equal(content(markdownToAdf('::date {timestamp=1}\n')), 'unsupported-node-shape: date takes the inline form, :date')
|
||||||
|
assert.equal(content(markdownToAdf(':paragraph[a]\n')), 'unsupported-node-shape: paragraph takes the block form, ::paragraph')
|
||||||
|
assert.equal(code(markdownToAdf(':::widget\na\n:::\n')), 'unknown-directive-name')
|
||||||
|
assert.equal(code(markdownToAdf(':widget[a]\n')), 'unknown-directive-name')
|
||||||
|
})
|
||||||
|
|
||||||
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 adf is reserved for the opaque carry, whose block form is the fence'
|
const reserved = 'malformed-directive: the name adf is reserved for the opaque carry, whose block form is the fence'
|
||||||
assert.equal(content(markdownToAdf('::adf\n')), reserved)
|
assert.equal(content(markdownToAdf('::adf\n')), reserved)
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ import type { ConvertFault } from '../result.ts'
|
|||||||
import type { DirectiveSpan, Read } from './directive-syntax.ts'
|
import type { DirectiveSpan, Read } from './directive-syntax.ts'
|
||||||
import { spellAttributes, spellLeafDirective, spellStringAttribute } from './directive-syntax.ts'
|
import { spellAttributes, spellLeafDirective, spellStringAttribute } from './directive-syntax.ts'
|
||||||
|
|
||||||
const name = 'text'
|
export const textDirectiveName = 'text'
|
||||||
|
|
||||||
const whitespaceRun = /^(?:[ \t]+|\n+)$/
|
const whitespaceRun = /^(?:[ \t]+|\n+)$/
|
||||||
|
|
||||||
export function spellTextDirective(text: string): string {
|
export function spellTextDirective(text: string): string {
|
||||||
|
const name = textDirectiveName
|
||||||
return spellLeafDirective(name, spellAttributes([[name, spellStringAttribute(text)]]))
|
return spellLeafDirective(name, spellAttributes([[name, spellStringAttribute(text)]]))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readTextDirective(span: DirectiveSpan): Read<string> | undefined {
|
export function readTextDirective(span: DirectiveSpan): Read<string> | undefined {
|
||||||
|
const name = textDirectiveName
|
||||||
if (span.name !== name) return undefined
|
if (span.name !== name) return undefined
|
||||||
if (span.content !== undefined) return { fault: unsupported(`${name} takes no content`) }
|
if (span.content !== undefined) return { fault: unsupported(`${name} takes no content`) }
|
||||||
const spelled = span.attributes.get(name)
|
const spelled = span.attributes.get(name)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { AttributeKind, AttributeVocabulary } from './adf/attribute-vocabul
|
|||||||
import { blockDirectives } from './adf/block-directives.ts'
|
import { blockDirectives } from './adf/block-directives.ts'
|
||||||
import { inlineDirectives } from './adf/inline-directives.ts'
|
import { inlineDirectives } from './adf/inline-directives.ts'
|
||||||
import { markAttributes } from './adf/mark-attributes.ts'
|
import { markAttributes } from './adf/mark-attributes.ts'
|
||||||
|
import { textDirectiveName } from './markdown/text-directive.ts'
|
||||||
|
|
||||||
type Declared = { attributes: AttributeVocabulary }
|
type Declared = { attributes: AttributeVocabulary }
|
||||||
|
|
||||||
@@ -96,6 +97,12 @@ test('the inline node table holds the attributes spec/flavour.md gives each node
|
|||||||
assert.deepEqual(declarations('Inline nodes'), vocabularies(inlineDirectives))
|
assert.deepEqual(declarations('Inline nodes'), vocabularies(inlineDirectives))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// A name in two tables would make the position a directive is read in ambiguous.
|
||||||
|
test('no name is spelled in more than one position', () => {
|
||||||
|
const names = [...Object.keys(blockDirectives), ...Object.keys(inlineDirectives), ...Object.keys(markAttributes), textDirectiveName]
|
||||||
|
assert.equal(new Set(names).size, names.length)
|
||||||
|
})
|
||||||
|
|
||||||
test('the mark table holds the attributes spec/flavour.md gives each mark', () => {
|
test('the mark table holds the attributes spec/flavour.md gives each mark', () => {
|
||||||
assert.deepEqual(declarations('Marks'), vocabularies(Object.fromEntries(Object.entries(markAttributes).map(([type, attributes]) => [type, { attributes }]))))
|
assert.deepEqual(declarations('Marks'), vocabularies(Object.fromEntries(Object.entries(markAttributes).map(([type, attributes]) => [type, { attributes }]))))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -364,6 +364,12 @@ Under **3 — `markdownToAdf` (`0.1.0`)**:
|
|||||||
fixtures, its cause now a marked slot rather than a slot at all. The slot's own whitespace
|
fixtures, its cause now a marked slot rather than a slot at all. The slot's own whitespace
|
||||||
answers the rule the spelling does: `:text{text="\n"}` and ` ` alike reach a slot the
|
answers the rule the spelling does: `:text{text="\n"}` and ` ` alike reach a slot the
|
||||||
emitter refuses a line ending in, so one function answers both directions.
|
emitter refuses a line ending in, so one function answers both directions.
|
||||||
|
**Settled** (the maintainer, 2026-09-01): a name the other position spells names that
|
||||||
|
spelling rather than reading as unknown — `:::em` and `::date` take
|
||||||
|
`unsupported-node-shape` naming the inline form, `:paragraph[a]` the block one — leaving
|
||||||
|
`unknown-directive-name` for a name no table holds, which is the meaning §8 gives it. The
|
||||||
|
two readers lean on the tables being disjoint, so that is a test beside the spec drift
|
||||||
|
guard now.
|
||||||
The same read found the hole the other way: `attemptLine` refused a line edged with a
|
The same read found the hole the other way: `attemptLine` refused a line edged with a
|
||||||
vertical tab or a form feed, where CommonMark strips spaces and tabs alone, so valid
|
vertical tab or a form feed, where CommonMark strips spaces and tabs alone, so valid
|
||||||
CommonMark parsed to a document `adfToMarkdown` then refused. The edges that check covered
|
CommonMark parsed to a document `adfToMarkdown` then refused. The edges that check covered
|
||||||
|
|||||||
Reference in New Issue
Block a user