Name the adf: fence among the carve-outs, and the way a fence stays code in its errors
This commit is contained in:
@@ -175,7 +175,7 @@ Parsing — `markdownToAdf` and `plainMarkdownToAdf`, and `htmlToAdf` at `0.2.0`
|
|||||||
|
|
||||||
| Code | Fires when | What you can do |
|
| Code | Fires when | What you can do |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `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 escape the prefix — `\!adf:`, block and inline alike — to keep it literal text |
|
| `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 wrap a code fence whose info string opens `adf:` in `!adf:codeBlock {language="adf:…"}` with a bare fence |
|
||||||
| `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 |
|
| `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 |
|
||||||
| `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 |
|
| `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-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 |
|
||||||
@@ -212,8 +212,9 @@ Serves Goals 1, 3 and 4.
|
|||||||
`0.2.0`, well-formed HTML means what the HTML standard says, read or written. The bullets below
|
`0.2.0`, well-formed HTML means what the HTML standard says, read or written. The bullets below
|
||||||
name every exception.
|
name every exception.
|
||||||
- Plain CommonMark is valid input to `markdownToAdf` apart from the raw HTML `unmappable-html`
|
- Plain CommonMark is valid input to `markdownToAdf` apart from the raw HTML `unmappable-html`
|
||||||
names, with three carve-outs — literal text matching directive, pipe-table or strikethrough
|
names, with four carve-outs — literal text matching directive, pipe-table or strikethrough
|
||||||
syntax is claimed (escapable — `spec/flavour.md`) — and one gap: a CommonMark image fits only as
|
syntax, and a code fence whose info string opens `adf:`, is claimed (escapable — `spec/flavour.md`)
|
||||||
|
— and one gap: a CommonMark image fits only as
|
||||||
its own title-less paragraph; mid-text and titled images are error results, save an image inside
|
its own title-less paragraph; mid-text and titled images are error results, save an image inside
|
||||||
another's description, which flattens into the alt text. Converting back yields the library's
|
another's description, which flattens into the alt text. Converting back yields the library's
|
||||||
canonical spelling, which round-trips byte-identically — where it converts back at all: a parse
|
canonical spelling, which round-trips byte-identically — where it converts back at all: a parse
|
||||||
|
|||||||
+4
-3
@@ -1,9 +1,10 @@
|
|||||||
# The markdown flavour
|
# The markdown flavour
|
||||||
|
|
||||||
The grammar of the extended markdown `adfToMarkdown` emits and `markdownToAdf` parses. Plain
|
The grammar of the extended markdown `adfToMarkdown` emits and `markdownToAdf` parses. Plain
|
||||||
CommonMark is a subset apart from raw HTML (below), with three carve-outs: literal text that matches
|
CommonMark is a subset apart from raw HTML (below), with four carve-outs: literal text that matches
|
||||||
directive syntax below or reads as a pipe table is claimed by the flavour, and a matched `~~` pair
|
directive syntax below or reads as a pipe table is claimed by the flavour, a matched `~~` pair
|
||||||
spells `strike` (escape the `!adf:`, `|` or `~` to keep it literal) — and one gap: a CommonMark
|
spells `strike` (escape the `!adf:`, `|` or `~` to keep it literal), and a code fence whose info
|
||||||
|
string opens `adf:` is the opaque carry (The opaque carry says how to keep it code) — and one gap: a CommonMark
|
||||||
image fits only as its own title-less paragraph — mid-text and titled images are named errors. The
|
image fits only as its own title-less paragraph — mid-text and titled images are named errors. The
|
||||||
emitted form is contract (`docs/decisions.md` §The formats are API). Per-node syntaxes build on this
|
emitted form is contract (`docs/decisions.md` §The formats are API). Per-node syntaxes build on this
|
||||||
grammar in the sections below.
|
grammar in the sections below.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { infoStringCarries } from './commonmark/grammar.ts'
|
|||||||
import { isAdfNode } from '../adf/document.ts'
|
import { isAdfNode } from '../adf/document.ts'
|
||||||
import { isJsonValue, nestingDepth, overNested } from '../json-value.ts'
|
import { isJsonValue, nestingDepth, overNested } from '../json-value.ts'
|
||||||
import { largestNesting } from '../nesting.ts'
|
import { largestNesting } from '../nesting.ts'
|
||||||
import { malformedDirective, readSoleStringAttribute, spellAttributes, spellInlineLeafDirective, spellStringAttribute, unsupportedNodeShape } from './directive-syntax.ts'
|
import { directiveEscape, malformedDirective, readSoleStringAttribute, spellAttributes, spellDirectiveOpener, spellInlineLeafDirective, spellStringAttribute, unsupportedNodeShape } from './directive-syntax.ts'
|
||||||
import { serializeCanonicalJson } from '../canonical-json.ts'
|
import { serializeCanonicalJson } from '../canonical-json.ts'
|
||||||
|
|
||||||
export const carryFencePrefix = 'adf:'
|
export const carryFencePrefix = 'adf:'
|
||||||
@@ -58,8 +58,9 @@ function carriedJson(node: AdfNode, spelled: object, spelling: JsonSpelling, pat
|
|||||||
|
|
||||||
// `type` is the one the fence's info string names, `undefined` for the inline carry.
|
// `type` is the one the fence's info string names, `undefined` for the inline carry.
|
||||||
function readCarriedJson(raw: string, spelling: JsonSpelling, levels: number, type?: string): Read<AdfNode> {
|
function readCarriedJson(raw: string, spelling: JsonSpelling, levels: number, type?: string): Read<AdfNode> {
|
||||||
|
const wayOut = type === undefined ? directiveEscape : fenceEscape(type)
|
||||||
const parsed = parseJsonText(raw)
|
const parsed = parseJsonText(raw)
|
||||||
if (parsed === undefined) return { fault: malformedDirective('the opaque carry holds invalid JSON') }
|
if (parsed === undefined) return { fault: malformedDirective(`the opaque carry holds invalid JSON; ${wayOut}`) }
|
||||||
const { value } = parsed
|
const { value } = parsed
|
||||||
if (!isJsonValue(value)) return { fault: unsupportedNodeShape('the opaque carry holds a number JSON cannot spell') }
|
if (!isJsonValue(value)) return { fault: unsupportedNodeShape('the opaque carry holds a number JSON cannot spell') }
|
||||||
const typed = type === undefined || type === '' ? { value } : typedValue(value, type)
|
const typed = type === undefined || type === '' ? { value } : typedValue(value, type)
|
||||||
@@ -70,12 +71,17 @@ function readCarriedJson(raw: string, spelling: JsonSpelling, levels: number, ty
|
|||||||
}
|
}
|
||||||
if (serializeCanonicalJson(value, spelling) !== raw) {
|
if (serializeCanonicalJson(value, spelling) !== raw) {
|
||||||
const shape = spelling === 'compact' ? 'compact, keys sorted' : 'two-space indent, keys sorted'
|
const shape = spelling === 'compact' ? 'compact, keys sorted' : 'two-space indent, keys sorted'
|
||||||
return { fault: unsupportedNodeShape(`the opaque carry spells its node's JSON canonically: ${shape}`) }
|
return { fault: unsupportedNodeShape(`the opaque carry spells its node's JSON canonically: ${shape}; ${wayOut}`) }
|
||||||
}
|
}
|
||||||
if (!isAdfNode(held)) return { fault: unsupportedNodeShape("the opaque carry holds one ADF node's JSON: this JSON is no ADF node") }
|
if (!isAdfNode(held)) return { fault: unsupportedNodeShape(`the opaque carry holds one ADF node's JSON: this JSON is no ADF node; ${wayOut}`) }
|
||||||
return { value: held }
|
return { value: held }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A code fence the carry claims stays code under the directive, whose language rides the attribute.
|
||||||
|
function fenceEscape(type: string): string {
|
||||||
|
return `${spellDirectiveOpener('codeBlock', undefined, spellAttributes([['language', spellStringAttribute(`${carryFencePrefix}${type}`)]]))} around a bare fence keeps it a code block`
|
||||||
|
}
|
||||||
|
|
||||||
function typedValue(value: JsonValue, type: string): Read<JsonValue> {
|
function typedValue(value: JsonValue, type: string): Read<JsonValue> {
|
||||||
if (value === null || typeof value !== 'object' || Array.isArray(value)) return { value }
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) return { value }
|
||||||
if ('type' in value) return { fault: unsupportedNodeShape(`the ${carryFencePrefix}${type} fence names its node's type: this JSON holds a type as well`) }
|
if ('type' in value) return { fault: unsupportedNodeShape(`the ${carryFencePrefix}${type} fence names its node's type: this JSON holds a type as well`) }
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ test('reads the carry fence back to the node its info string names and its JSON
|
|||||||
"unsupported-node-shape: the adf:blockCard fence names its node's type: this JSON holds a type as well",
|
"unsupported-node-shape: the adf:blockCard fence names its node's type: this JSON holds a type as well",
|
||||||
)
|
)
|
||||||
assert.equal(content(markdownToAdf('```adf:\n{\n "type": "blockCard"\n}\n```\n')), 'unsupported-node-shape: the carry fence names a type its info string carries: spell it adf:blockCard')
|
assert.equal(content(markdownToAdf('```adf:\n{\n "type": "blockCard"\n}\n```\n')), 'unsupported-node-shape: the carry fence names a type its info string carries: spell it adf:blockCard')
|
||||||
assert.equal(content(markdownToAdf('```adf:\n{}\n```\n')), "unsupported-node-shape: the opaque carry holds one ADF node's JSON: this JSON is no ADF node")
|
assert.equal(content(markdownToAdf('```adf:\n{}\n```\n')), "unsupported-node-shape: the opaque carry holds one ADF node's JSON: this JSON is no ADF node; !adf:codeBlock {language=\"adf:\"} around a bare fence keeps it a code block")
|
||||||
const unnamed = 'unsupported-node-shape: the carry fence names a type no info string carries back: spell it adf: with the type in the JSON'
|
const unnamed = 'unsupported-node-shape: the carry fence names a type no info string carries back: spell it adf: with the type in the JSON'
|
||||||
assert.equal(content(markdownToAdf('```adf:\\\\\n{}\n```\n')), unnamed)
|
assert.equal(content(markdownToAdf('```adf:\\\\\n{}\n```\n')), unnamed)
|
||||||
})
|
})
|
||||||
@@ -385,26 +385,31 @@ test('reads the inline carry back to the node its json attribute holds', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('names the invalid JSON no opaque carry holds', () => {
|
test('names the invalid JSON no opaque carry holds, and the spelling that keeps it literal', () => {
|
||||||
const invalid = 'malformed-directive: the opaque carry holds invalid JSON'
|
const invalid = 'malformed-directive: the opaque carry holds invalid JSON; '
|
||||||
assert.equal(content(markdownToAdf('```adf:x\n{"attrs":\n```\n')), invalid)
|
const fence = `${invalid}!adf:codeBlock {language="adf:x"} around a bare fence keeps it a code block`
|
||||||
assert.equal(content(markdownToAdf('```adf:x\n```\n')), invalid)
|
assert.equal(content(markdownToAdf('```adf:x\n{"attrs":\n```\n')), fence)
|
||||||
assert.equal(content(markdownToAdf('!adf:carry{json="{"}\n')), invalid)
|
assert.equal(content(markdownToAdf('```adf:x\n```\n')), fence)
|
||||||
assert.equal(content(markdownToAdf('!adf:carry{json=abc}\n')), invalid)
|
assert.equal(content(markdownToAdf('!adf:carry{json="{"}\n')), `${invalid}\\!adf: keeps the prefix literal`)
|
||||||
|
assert.equal(content(markdownToAdf('!adf:carry{json=abc}\n')), `${invalid}\\!adf: keeps the prefix literal`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('names the canonical spelling a carried JSON reads alone', () => {
|
test('names the canonical spelling a carried JSON reads alone', () => {
|
||||||
const canonically = "unsupported-node-shape: the opaque carry spells its node's JSON canonically: "
|
const canonically = "unsupported-node-shape: the opaque carry spells its node's JSON canonically: "
|
||||||
assert.equal(content(markdownToAdf('```adf:blockCard\n{"attrs":{}}\n```\n')), `${canonically}two-space indent, keys sorted`)
|
const literal = '; \\!adf: keeps the prefix literal'
|
||||||
assert.equal(content(markdownToAdf('!adf:carry{json="{\\"type\\": \\"blockCard\\"}"}\n')), `${canonically}compact, keys sorted`)
|
assert.equal(
|
||||||
assert.equal(content(markdownToAdf('!adf:carry{json="{\\"type\\":\\"blockCard\\",\\"attrs\\":{}}"}\n')), `${canonically}compact, keys sorted`)
|
content(markdownToAdf('```adf:blockCard\n{"attrs":{}}\n```\n')),
|
||||||
|
`${canonically}two-space indent, keys sorted; !adf:codeBlock {language="adf:blockCard"} around a bare fence keeps it a code block`,
|
||||||
|
)
|
||||||
|
assert.equal(content(markdownToAdf('!adf:carry{json="{\\"type\\": \\"blockCard\\"}"}\n')), `${canonically}compact, keys sorted${literal}`)
|
||||||
|
assert.equal(content(markdownToAdf('!adf:carry{json="{\\"type\\":\\"blockCard\\",\\"attrs\\":{}}"}\n')), `${canonically}compact, keys sorted${literal}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('names the node JSON an opaque carry restores alone', () => {
|
test('names the node JSON an opaque carry restores alone', () => {
|
||||||
const node = "unsupported-node-shape: the opaque carry holds one ADF node's JSON: this JSON is no ADF node"
|
const node = "unsupported-node-shape: the opaque carry holds one ADF node's JSON: this JSON is no ADF node; "
|
||||||
assert.equal(content(markdownToAdf('```adf:x\n[]\n```\n')), node)
|
assert.equal(content(markdownToAdf('```adf:x\n[]\n```\n')), `${node}!adf:codeBlock {language="adf:x"} around a bare fence keeps it a code block`)
|
||||||
assert.equal(content(markdownToAdf('!adf:carry{json=null}\n')), node)
|
assert.equal(content(markdownToAdf('!adf:carry{json=null}\n')), `${node}\\!adf: keeps the prefix literal`)
|
||||||
assert.equal(content(markdownToAdf('!adf:carry{json="{\\"kind\\":\\"x\\"}"}\n')), node)
|
assert.equal(content(markdownToAdf('!adf:carry{json="{\\"kind\\":\\"x\\"}"}\n')), `${node}\\!adf: keeps the prefix literal`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('names the shape the inline carry reads alone', () => {
|
test('names the shape the inline carry reads alone', () => {
|
||||||
@@ -421,7 +426,7 @@ test('holds a carried JSON value to the nesting its position leaves', () => {
|
|||||||
assert.equal(content(markdownToAdf(`!adf:carry{json="${nested(largestNesting + 2)}"}\n`)), deeper(largestNesting))
|
assert.equal(content(markdownToAdf(`!adf:carry{json="${nested(largestNesting + 2)}"}\n`)), deeper(largestNesting))
|
||||||
assert.equal(
|
assert.equal(
|
||||||
content(markdownToAdf(fence('', largestNesting + 1))),
|
content(markdownToAdf(fence('', largestNesting + 1))),
|
||||||
"unsupported-node-shape: the opaque carry spells its node's JSON canonically: two-space indent, keys sorted",
|
'unsupported-node-shape: the opaque carry spells its node\'s JSON canonically: two-space indent, keys sorted; !adf:codeBlock {language="adf:x"} around a bare fence keeps it a code block',
|
||||||
)
|
)
|
||||||
assert.equal(content(markdownToAdf(fence('> ', largestNesting + 1))), deeper(largestNesting - 1))
|
assert.equal(content(markdownToAdf(fence('> ', largestNesting + 1))), deeper(largestNesting - 1))
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user