3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-12 11:48:29 +01:00

Answer prose round 1: 0 🔴, 6 🟡; the changelog names what breaks, the fallback reads as untyped mark, attribute or value, the spec's value list and marks paragraph split, messages name the JSON array to write

This commit is contained in:
2026-10-14 10:28:50 +02:00
committed by Lillem4n
parent 168ca1cea2
commit aefcd4b9e3
10 changed files with 37 additions and 35 deletions
+4 -4
View File
@@ -66,10 +66,10 @@
- **Breaking:** `adfToLosslessMarkdown` writes a centered, uncaptioned external image whose one
mark is a link holding only `href` and `title` as `[![alt](url)](href "title")`, where it wrote the
`mediaSingle` directive. Convert stored markdown per `MIGRATION.md`.
- **Breaking:** `colwidth` is spelled as its widths joined by commas, `colwidth="340,420"`, where it
was a JSON array, and a block's `marks` names each mark with its attributes,
`marks="breakout{mode=wide}"`, where it was a JSON array. The JSON array stays only where the
flavour does not list a mark's type, one of its attributes, or the type of one's value. Convert stored markdown per `MIGRATION.md`.
- **Breaking:** `adfToLosslessMarkdown` writes `colwidth` as its widths joined by commas,
`colwidth="340,420"`, and a block's `marks` as each mark's type and attributes,
`marks="breakout{mode=wide}"`, where it wrote both as JSON arrays, and `losslessMarkdownToAdf`
refuses the arrays. Convert stored markdown per `MIGRATION.md`.
- Fix the time `adfToLosslessMarkdown` and `adfToPortableMarkdown` take on text holding a long run of
`*`, `_`, `~` or backticks, or a heading ending in a long run of `#`: the time is linear in the
run, and a run of 20000 `*` takes milliseconds where it took seconds.
+2 -2
View File
@@ -67,8 +67,8 @@ function migrateMarkdown(stored: string) {
| `\:` keeps a directive literal | `\!adf:` keeps a directive literal |
| `:adf{json="…"}` carrying a link for its `collection`, `id` or `occurrenceKey` | `!adf:link[text]{attrs}` |
| `::listBreak` between two lists of a kind | no separator: adjacent lists of a kind alternate `- ` and `* `, or `1.` and `1)` |
| `colwidth="[340,420]"` | `colwidth="340,420"` |
| `marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"` on a block | `marks="breakout{mode=wide}"`; the JSON array stays where the flavour does not list a mark's type, one of its attributes, or the type of one's value |
| `colwidth="[340,420]"` | `colwidth="340,420"`; a `colwidth` holding no number, or a value that is not a number, rides the `adf:tableCell` or `adf:tableHeader` carry |
| `marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"` on a block | `marks="breakout{mode=wide}"`; `marks` stays a JSON array where the flavour does not list a mark's type or one of its attributes, or a value holds another type than the flavour gives it |
A colon run and `:name[` are plain text now, and `adf` an ordinary code block language; text
holding an unescaped `!adf:` and a code fence whose info string opens `adf:` are claimed instead.
+5 -5
View File
@@ -316,11 +316,11 @@ Ping !adf:mention[@Mikael]{id=5b10a2} — !adf:status[In review]{color=yellow}
- Attributes are `{key=value key2="two words"}`: keys alphabetical, a value bare where it is
`[A-Za-z0-9_-]+`, else double-quoted with JSON string escaping. A number or JSON value is spelled as
canonical JSON, but `colwidth` is its widths joined by commas, `colwidth="340,420"`, and a block's
`marks` names each mark with its attributes, `marks="breakout{mode=wide}"`, keeping the JSON array
where the flavour does not list a mark's type, one of its attributes, or the type of one's value.
An empty `attrs`, `content` or `marks` key is spelled `{attrs=empty}`, `{content=empty}` or
`{marks=empty}`.
canonical JSON, with two exceptions. `colwidth` joins its widths with commas: `colwidth="340,420"`.
A block's `marks` names each mark followed by its attributes, `marks="breakout{mode=wide}"`, and
stays a JSON array where the flavour does not list a mark's type or one of its attributes, or a
value holds another type than the flavour gives it. An empty `attrs`, `content` or `marks` key is
spelled `{attrs=empty}`, `{content=empty}` or `{marks=empty}`.
- Three reserved names build no node: `!adf:carry` is the inline opaque carry, `!adf:doc {content=none}`
spells a document holding no `content` key, and `!adf:textBreak{}` parts two adjacent text nodes
CommonMark would read back as one.
+4 -3
View File
@@ -109,9 +109,10 @@ managers, engineers and support agents. Goal 6. Valid while ADF types these attr
`colwidth` is its widths joined by commas, `colwidth="340,420"`, where it was a JSON array. A
block's `marks` names each mark with its `{attrs}`, in order,
`marks="indentation{level=2} fragment{localId=f1 name=\"Price list\"}"`, chosen over the escaped
JSON array and an unquoted one. All three writers first sketched a key per mark, `{align=center}`,
which loses the marks' order and can clash with the node's own attributes. A mark `src/adf/` does not
type keeps the whole array as JSON, the general form (§Readable spellings take the `try` prefix).
JSON array and the JSON array unquoted. All three writers first sketched a key per mark,
`{align=center}`, which loses the marks' order and can clash with the node's own attributes. Where
`src/adf/` does not type a mark or one of its attributes, or a value holds another type, the whole
array stays JSON, the general form (§Readable spellings take the `try` prefix).
The grammar: `spec/flavour.md` §Block nodes.
## Empty markdown is a document of no blocks
+16 -14
View File
@@ -204,20 +204,22 @@ node the emitter would have written as CommonMark is a named error.
Each section lists attributes as `name (type)`. A parenthesized value set documents what real
payloads hold; the type stays string and any value round-trips verbatim. Values map to attrs by
type: strings verbatim, numbers and booleans in canonical JSON spelling — quoted where not bare
(`width="33.33"`) — `numbers`, a list of one number or more, as those spellings joined by commas
(`colwidth="340,420"`), and `json` values as the inline carry's serialization (compact, keys
sorted), quoted, `-0` spelled `-0`. `losslessMarkdownToAdf` builds an `attrs`, `content` or `marks` key only
where the markdown spells one, an empty one through its reserved key (Attributes), so a document
type: strings verbatim; numbers and booleans in canonical JSON spelling, quoted where not bare
(`width="33.33"`); `numbers`, a list of one number or more, as those spellings joined by commas
(`colwidth="340,420"`); and `json` values as the inline carry's serialization (compact, keys
sorted), quoted. `-0` is spelled `-0`. `losslessMarkdownToAdf` builds an `attrs`, `content` or
`marks` key only where the markdown spells one, an empty one through its reserved key (Attributes), so a document
reads back deep-equal (`docs/decisions.md` §Equality is deep). A node CommonMark spells takes the
directive form to hold an empty key.
Marks on a block node ride the reserved attribute key `marks`: each mark's type followed by its
`{attrs}` spelled as a directive's, one space apart in the array's order, and `marks=empty` where
the array is empty: `!adf:layoutSection {marks="breakout{mode=wide}"}`,
`!adf:paragraph {marks="indentation{level=2} fragment{localId=f1 name=\"Price list\"}"}`. Where §Marks
does not list a mark's type, one of its attributes, or the type of one's value, the whole array is
a `json` value instead: `marks="[{\"attrs\":{\"color\":\"#fff\"},\"type\":\"backgroundColor\"}]"`.
Marks on a block node ride the reserved attribute key `marks`. It holds the marks in the array's
order, one space apart, each as its type followed by its `{attrs}` as a directive spells them:
`!adf:layoutSection {marks="breakout{mode=wide}"}`,
`!adf:paragraph {marks="indentation{level=2} fragment{localId=f1 name=\"Price list\"}"}`. Where
§Marks does not list a mark's type or one of its attributes, or a value holds another type than
§Marks gives it, the whole array is a `json` value instead:
`marks="[{\"attrs\":{\"color\":\"#fff\"},\"type\":\"backgroundColor\"}]"`. An empty array is
`marks=empty`.
A section saying its body is inline takes at most one paragraph, whose inline content becomes
the node's `content`; any other body is a named error, and an empty pair is a node holding none.
@@ -555,9 +557,9 @@ carrying an identical mark, attributes included, at that depth: `attrs: {}` diff
and a directive spells it `{attrs=empty}`. A run breaks at every node the emitter carries, so no
emitted carry sits inside a mark spelling.
An inline node whose marks no nesting spells — a hard break holding any mark, a mark type the inline
list above does not hold, an attrs key its spelling does not list, a value that is not the spelling's type, an attribute
the spelling needs and the mark lacks, an empty `attrs` on a mark CommonMark spells, an order
An inline node whose marks no nesting spells — a hard break holding any mark, a mark type the first
list above does not hold, an attrs key its spelling does not list, a value that is not the
spelling's type, an attribute the spelling needs and the mark lacks, an empty `attrs` on a mark CommonMark spells, an order
putting a code span outside another mark, `code` over anything but a text node holding text, or over
text holding a line ending or a null character, or a spelling CommonMark's flanking rules cannot
open or close where the run sits (`un**-real**istic`), or one CommonMark's matching pairs elsewhere
-1
View File
@@ -1,6 +1,5 @@
import type { AttributeVocabulary } from './attribute-vocabulary.ts'
// Marks only a block's marks key holds, so no inline spelling names them.
export const blockMarkAttributes = {
alignment: { align: 'string' },
breakout: { mode: 'string', width: 'number' },
+1 -1
View File
@@ -41,7 +41,7 @@ export function markValues(marks: readonly AdfMark[]): JsonValue {
return marks.map((mark) => (mark.attrs === undefined ? { type: mark.type } : { attrs: mark.attrs, type: mark.type }))
}
// spec/flavour.md, Block nodes: each mark by name, else the whole array as JSON, `undefined` where that array nests past largestNesting.
// `undefined` where the JSON array nests past largestNesting.
export function spellBlockMarks(marks: readonly AdfMark[]): string | undefined {
const named: string[] = []
for (const mark of marks) {
+1 -1
View File
@@ -74,7 +74,7 @@ export function spellMarkAttributes(mark: AdfMark, spelling: MarkSpelling): stri
return emptyKeys(mark).length > 0 && spelling.kind !== 'directive' ? undefined : spellMarkVocabulary(mark, spelling.attributes)
}
// `undefined` where an attribute is one the vocabulary does not type.
// `undefined` where the vocabulary lacks an attribute or types its value otherwise.
export function spellMarkVocabulary(mark: AdfMark, vocabulary: AttributeVocabulary): string | undefined {
const pairs = vocabularyPairs(nodeAttrs(mark), vocabulary, [])
return pairs === undefined ? undefined : spellAttributes([...spellVocabulary(pairs), ...spellEmptyKeys(mark)])
+2 -2
View File
@@ -132,7 +132,7 @@ function readNamedMark(type: string, text: string, index: number, path: ConvertE
const name = readDirectiveName(text, index)
if (name === undefined) return failure('unsupported-node-shape', unnamedMarks(type), path)
const vocabulary = markVocabularies.get(name)
if (vocabulary === undefined) return failure('unsupported-node-shape', `the ${marksAttribute} attribute of ${type} names ${name}, no mark the flavour lists: spell an unlisted mark in the JSON array`, path)
if (vocabulary === undefined) return failure('unsupported-node-shape', `the ${marksAttribute} attribute of ${type} names ${name}, no mark the flavour lists: write the marks as their JSON array, marks="[{\\"type\\":\\"${name}\\"}]"`, path)
const start = index + name.length
const attributes = text.charAt(start) === '{' ? readAttributes(text, start) : { value: { attributes: new Map<string, DirectiveValue>(), end: start } }
if (attributes.fault !== undefined) {
@@ -145,7 +145,7 @@ function readNamedMark(type: string, text: string, index: number, path: ConvertE
}
function unnamedMarks(type: string): string {
return `the ${marksAttribute} attribute of ${type} names each mark with its {attrs}, one space apart: this one does not`
return `the ${marksAttribute} attribute of ${type} names its marks one space apart, each as its type and any {attrs}: this one does not`
}
function namedNode(type: string, attrs: AdfAttributes, marks: readonly AdfMark[] | undefined, empty: ReadonlySet<EmptyKey>): AdfNode {
+2 -2
View File
@@ -550,7 +550,7 @@ test('reads the reserved marks key as a JSON array where a mark is one the table
test('names the marks key no marks read back from', () => {
const array = 'unsupported-node-shape: the marks attribute of rule is its marks array in canonical JSON: this one is not'
const named = 'unsupported-node-shape: the marks attribute of rule names each mark with its {attrs}, one space apart: this one does not'
const named = 'unsupported-node-shape: the marks attribute of rule names its marks one space apart, each as its type and any {attrs}: this one does not'
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="[]"}\n')), array)
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="[1]"}\n')), array)
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="{}"}\n')), named)
@@ -562,7 +562,7 @@ test('names the marks key no marks read back from', () => {
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="breakout{mode=wide"}\n')), inBreakout('the {attrs} closing brace is missing'))
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="em{attrs=empty}strong"}\n')), named)
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="em{attrs=x}"}\n')), 'unsupported-node-shape: the reserved key attrs takes only the value empty: this one spells attrs=x')
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks=x}\n')), 'unsupported-node-shape: the marks attribute of rule names x, no mark the flavour lists: spell an unlisted mark in the JSON array')
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks=x}\n')), 'unsupported-node-shape: the marks attribute of rule names x, no mark the flavour lists: write the marks as their JSON array, marks="[{\\"type\\":\\"x\\"}]"')
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="breakout{width=wide}"}\n')), 'unsupported-node-shape: the width attribute of breakout is no number')
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="breakout{bogus=1}"}\n')), 'unsupported-node-shape: breakout holds no bogus attribute: this one spells it')
})