mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Answer stability round 1: 0 🔴, 2 🟡; a mark's malformed attrs name their cause, the docs name a value of the wrong type as a fallback
This commit is contained in:
+2
-2
@@ -68,8 +68,8 @@
|
||||
`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 a mark's
|
||||
type or attributes are ones the flavour does not list. Convert stored markdown per `MIGRATION.md`.
|
||||
`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`.
|
||||
- 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.
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ function migrateMarkdown(stored: string) {
|
||||
| `: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 a mark's type or attributes are ones the flavour does not list |
|
||||
| `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 |
|
||||
|
||||
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.
|
||||
|
||||
@@ -318,8 +318,9 @@ Ping !adf:mention[@Mikael]{id=5b10a2} — !adf:status[In review]{color=yellow}
|
||||
`[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 a mark's type or attributes are ones the flavour does not list. An empty `attrs`, `content`
|
||||
or `marks` key is spelled `{attrs=empty}`, `{content=empty}` or `{marks=empty}`.
|
||||
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}`.
|
||||
- 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.
|
||||
|
||||
+5
-6
@@ -215,9 +215,9 @@ 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 a
|
||||
mark's type is not one §Marks lists, or its attributes are not the ones listed there, the whole
|
||||
array is a `json` value instead: `marks="[{\"attrs\":{\"color\":\"#fff\"},\"type\":\"backgroundColor\"}]"`.
|
||||
`!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\"}]"`.
|
||||
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.
|
||||
|
||||
@@ -543,7 +543,6 @@ A block's `marks` key also holds these, which no inline spelling wraps:
|
||||
|
||||
- `alignment` — Attributes: `align` (`center` `end`).
|
||||
- `breakout` — Attributes: `mode` (`full-width` `wide`), `width` (number).
|
||||
- `dataConsumer` — Attributes: `sources` (json).
|
||||
- `fragment` — Attributes: `localId` (string), `name` (string).
|
||||
- `indentation` — Attributes: `level` (number, 1–6).
|
||||
|
||||
@@ -556,8 +555,8 @@ 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 first
|
||||
list does not hold, an attrs key its spelling does not list, a value that is not the spelling's type, an attribute
|
||||
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
|
||||
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
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { AttributeVocabulary } from './attribute-vocabulary.ts'
|
||||
export const blockMarkAttributes = {
|
||||
alignment: { align: 'string' },
|
||||
breakout: { mode: 'string', width: 'number' },
|
||||
dataConsumer: { sources: 'json' },
|
||||
fragment: { localId: 'string', name: 'string' },
|
||||
indentation: { level: 'number' },
|
||||
} satisfies Readonly<Record<string, AttributeVocabulary>>
|
||||
|
||||
@@ -16,7 +16,7 @@ type Properties = Map<string, SchemaObject[]>
|
||||
type SchemaObject = Readonly<Record<string, unknown>>
|
||||
type Spelled = [string, Map<string, AttributeKind>]
|
||||
|
||||
const carried = ['annotation', 'backgroundColor', 'blockCard', 'bodiedRule', 'embedCard', 'fontSize', 'inlineExtension', 'placeholder']
|
||||
const carried = ['annotation', 'backgroundColor', 'blockCard', 'bodiedRule', 'dataConsumer', 'embedCard', 'fontSize', 'inlineExtension', 'placeholder']
|
||||
const definitionReference = '#/definitions/'
|
||||
const gaps: string[] = []
|
||||
const grammarOwn = ['doc', 'text']
|
||||
|
||||
@@ -431,7 +431,6 @@ test('refuses marks and attributes nested deeper than the emitter carries', () =
|
||||
const markedCode: AdfNode = { content: [{ text: 'x', type: 'text' }], marks: [{ attrs: { deep: nested(largestNesting - 2) }, type: 'em' }], type: 'codeBlock' }
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(markedCode))), deeper('marks', 'codeBlock'))
|
||||
roundTrips(marked(largestNesting - 3))
|
||||
roundTrips({ ...marked(0), marks: [{ attrs: { sources: nested(largestNesting) }, type: 'dataConsumer' }] })
|
||||
})
|
||||
|
||||
test('escapes a literal delimiter that would merge with an emitted one', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ConvertFault } from '../../result.ts'
|
||||
import type { DirectiveAttributes, DirectiveValue } from '../directive-syntax.ts'
|
||||
import type { Elsewhere } from './directive-attributes.ts'
|
||||
import { attributeNestingMessage, isUnmarkedSpellableText } from '../../adf/document.ts'
|
||||
import { attributeValue, directivePrefix, readAttributes, readDirectiveName, unknownDirectiveFault } from '../directive-syntax.ts'
|
||||
import { attributeValue, directiveEscape, directivePrefix, readAttributes, readDirectiveName, unknownDirectiveFault } from '../directive-syntax.ts'
|
||||
import { blockArgument, blockDirectiveForm, marksAttribute, readMarkValues, spellBlockMarks } from '../block-directive.ts'
|
||||
import { blockNodeModel } from '../../adf/block-nodes.ts'
|
||||
import { carryFencePrefix, carryName } from '../opaque-carry.ts'
|
||||
@@ -132,10 +132,14 @@ 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}, a mark it spells only in its JSON array`, path)
|
||||
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)
|
||||
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) return failure('unsupported-node-shape', unnamedMarks(type), path)
|
||||
if (attributes.fault !== undefined) {
|
||||
// A quoted value holds no directive, so the fault's escape hint does not apply.
|
||||
const cause = attributes.fault.message.replace(`; ${directiveEscape}`, '')
|
||||
return failure('unsupported-node-shape', `the ${name} mark in the ${marksAttribute} attribute of ${type}: ${cause}`, path)
|
||||
}
|
||||
const mark = readMarkAttributes(name, attributes.value.attributes, vocabulary, path)
|
||||
return mark.ok ? success({ end: attributes.value.end, mark: mark.value }) : mark
|
||||
}
|
||||
|
||||
@@ -556,12 +556,13 @@ test('names the marks key no marks read back from', () => {
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="{}"}\n')), named)
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="em strong"}\n')), named)
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="em "}\n')), named)
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="breakout{width=1 mode=wide}"}\n')), named)
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="breakout{mode}"}\n')), named)
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="breakout{mode=wide"}\n')), named)
|
||||
const inBreakout = (cause: string): string => `unsupported-node-shape: the breakout mark in the marks attribute of rule: ${cause}`
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="breakout{width=1 mode=wide}"}\n')), inBreakout('the {attrs} keys read in alphabetical order: mode before width'))
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="breakout{mode}"}\n')), inBreakout('an attribute reads key=value, the value bare or double-quoted: this one does not'))
|
||||
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, a mark it spells only in its 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: spell an unlisted mark in the JSON array')
|
||||
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')
|
||||
})
|
||||
@@ -596,7 +597,6 @@ test('names the depth an attribute value nests past, never the kind the JSON rea
|
||||
assert.equal(content(losslessMarkdownToAdf(`!adf:extension {parameters="${nested(largestNesting + 1)}"}\n`)), deeper('parameters', 'extension'))
|
||||
assert.equal(content(losslessMarkdownToAdf(`!adf:rule {marks="${nested(largestNesting + 1)}"}\n`)), deeper('marks', 'rule'))
|
||||
assert.equal(content(losslessMarkdownToAdf(`!adf:rule {marks="[{\\"attrs\\":{\\"deep\\":${nested(largestNesting - 2)}},\\"type\\":\\"em\\"}]"}\n`)), deeper('marks', 'rule'))
|
||||
assert.equal(content(losslessMarkdownToAdf(`!adf:rule {marks="dataConsumer{sources=\\"${nested(largestNesting + 1)}\\"}"}\n`)), deeper('sources', 'dataConsumer'))
|
||||
assert.equal(content(losslessMarkdownToAdf(`!adf:media {width="${nested(largestNesting + 1)}"}\n`)), 'unsupported-node-shape: the width attribute of media is no number')
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user