mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Answer architecture round 1: 2 🔴, 5 🟡; nesting counts the JSON fallback alone, one mark attrs read and spell, one mark table, a malformed mark is unsupported-node-shape, the old colwidth names the comma list, README and notes name the attrs 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 for a block
|
||||
mark 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 a mark's
|
||||
type or attributes are ones the flavour does not list. 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 for a mark the flavour does not list |
|
||||
| `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 |
|
||||
|
||||
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.
|
||||
|
||||
@@ -316,7 +316,8 @@ 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. An empty `attrs`, `content` or `marks` key is spelled `{attrs=empty}`,
|
||||
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}"`. 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
|
||||
|
||||
@@ -9,11 +9,17 @@ export type VocabularyValue =
|
||||
| { kind: 'boolean'; value: boolean }
|
||||
| { kind: 'json'; value: JsonValue }
|
||||
| { kind: 'number'; value: number }
|
||||
| { kind: 'numbers'; value: number[] }
|
||||
| { kind: 'numbers'; value: NumberList }
|
||||
| { kind: 'string'; value: string }
|
||||
|
||||
export type VocabularyPair = VocabularyValue & { key: string }
|
||||
|
||||
export type NumberList = [number, ...number[]]
|
||||
|
||||
export function isNumberList(value: unknown): value is NumberList {
|
||||
return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === 'number')
|
||||
}
|
||||
|
||||
export function vocabularyPairs(attrs: AdfAttributes, vocabulary: AttributeVocabulary, spelledElsewhere: readonly string[]): VocabularyPair[] | undefined {
|
||||
const pairs: VocabularyPair[] = []
|
||||
for (const [key, value] of Object.entries(attrs)) {
|
||||
@@ -30,7 +36,7 @@ export function vocabularyPairs(attrs: AdfAttributes, vocabulary: AttributeVocab
|
||||
function vocabularyPair(key: string, value: JsonValue, kind: AttributeKind): VocabularyPair | undefined {
|
||||
if (kind === 'boolean') return typeof value === 'boolean' ? { key, kind, value } : undefined
|
||||
if (kind === 'number') return typeof value === 'number' ? { key, kind, value } : undefined
|
||||
if (kind === 'numbers') return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === 'number') ? { key, kind, value } : undefined
|
||||
if (kind === 'numbers') return isNumberList(value) ? { key, kind, value } : undefined
|
||||
if (kind === 'string') return typeof value === 'string' ? { key, kind, value } : undefined
|
||||
return { key, kind, value }
|
||||
}
|
||||
|
||||
@@ -21,14 +21,10 @@ export const markAttributes = {
|
||||
underline: {},
|
||||
} satisfies Readonly<Record<string, AttributeVocabulary>>
|
||||
|
||||
const blockMarkVocabularies: ReadonlyMap<string, AttributeVocabulary> = new Map(Object.entries(blockMarkAttributes))
|
||||
export const markVocabularies: ReadonlyMap<string, AttributeVocabulary> = new Map([...Object.entries(blockMarkAttributes), ...Object.entries(markAttributes)])
|
||||
|
||||
export type MarkType = keyof typeof markAttributes
|
||||
|
||||
export function isMarkType(type: string): type is MarkType {
|
||||
return Object.hasOwn(markAttributes, type)
|
||||
}
|
||||
|
||||
export function markVocabulary(type: string): AttributeVocabulary | undefined {
|
||||
return isMarkType(type) ? markAttributes[type] : blockMarkVocabularies.get(type)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { AttributeKind, AttributeVocabulary } from '../adf/attribute-vocabu
|
||||
import { blockArgument } from '../markdown/block-directive.ts'
|
||||
import { blockNodes } from '../adf/block-nodes.ts'
|
||||
import { inlineNodes } from '../adf/inline-nodes.ts'
|
||||
import { blockMarkAttributes, markAttributes } from '../adf/mark-attributes.ts'
|
||||
import { markVocabularies } from '../adf/mark-attributes.ts'
|
||||
|
||||
type Held = Map<string, Set<AttributeKind>>
|
||||
type Properties = Map<string, SchemaObject[]>
|
||||
@@ -85,7 +85,7 @@ function spelled(): Spelled[] {
|
||||
return [
|
||||
...Object.entries(blockNodes).map(([type, model]) => spelledType(type, model.attributes, blockArgument(type))),
|
||||
...Object.entries(inlineNodes).map(([type, model]) => spelledType(type, model.attributes)),
|
||||
...Object.entries({ ...blockMarkAttributes, ...markAttributes }).map(([type, attributes]) => spelledType(type, attributes)),
|
||||
...[...markVocabularies].map(([type, attributes]) => spelledType(type, attributes)),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import test from 'node:test'
|
||||
import type { AttributeKind, AttributeVocabulary } from '../adf/attribute-vocabulary.ts'
|
||||
import { blockNodes } from '../adf/block-nodes.ts'
|
||||
import { inlineNodes } from '../adf/inline-nodes.ts'
|
||||
import { blockMarkAttributes, markAttributes } from '../adf/mark-attributes.ts'
|
||||
import { blockMarkAttributes, markAttributes, markVocabularies } from '../adf/mark-attributes.ts'
|
||||
import { textDirectiveName } from '../markdown/text-directive.ts'
|
||||
|
||||
type Declared = { attributes: AttributeVocabulary }
|
||||
@@ -104,5 +104,9 @@ test('no name is spelled in more than one position', () => {
|
||||
})
|
||||
|
||||
test('the mark table holds the attributes spec/flavour.md gives each mark', () => {
|
||||
assert.deepEqual(declarations('Marks'), vocabularies(Object.fromEntries(Object.entries({ ...blockMarkAttributes, ...markAttributes }).map(([type, attributes]) => [type, { attributes }]))))
|
||||
assert.deepEqual(declarations('Marks'), vocabularies(Object.fromEntries([...markVocabularies].map(([type, attributes]) => [type, { attributes }]))))
|
||||
})
|
||||
|
||||
test('no mark is named in both mark tables', () => {
|
||||
assert.equal(markVocabularies.size, Object.keys(blockMarkAttributes).length + Object.keys(markAttributes).length)
|
||||
})
|
||||
|
||||
@@ -2,12 +2,12 @@ import type { AdfMark } from '../adf/document.ts'
|
||||
import type { BlockType } from '../adf/block-nodes.ts'
|
||||
import type { JsonValue } from '../json-value.ts'
|
||||
import { blockNodeModel } from '../adf/block-nodes.ts'
|
||||
import { isAdfMark, nodeAttrs } from '../adf/document.ts'
|
||||
import { markVocabulary } from '../adf/mark-attributes.ts'
|
||||
import { isAdfMark } from '../adf/document.ts'
|
||||
import { markVocabularies } from '../adf/mark-attributes.ts'
|
||||
import { overNested } from '../json-value.ts'
|
||||
import { serializeCanonicalJson } from '../canonical-json.ts'
|
||||
import { spellAttributes, spellDirectiveOpener, spellJsonAttribute, spellStringAttribute, spellVocabulary } from './directive-syntax.ts'
|
||||
import { spellEmptyKeys } from './empty-keys.ts'
|
||||
import { vocabularyPairs } from '../adf/attribute-vocabulary.ts'
|
||||
import { spellAttributes, spellDirectiveOpener, spellJsonAttribute, spellStringAttribute } from './directive-syntax.ts'
|
||||
import { spellMarkVocabulary } from './mark-spellings.ts'
|
||||
|
||||
const argumentByType = new Map(
|
||||
Object.entries({
|
||||
@@ -41,14 +41,14 @@ 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 where one mark's type or attrs no table types.
|
||||
export function spellBlockMarks(marks: readonly AdfMark[]): string {
|
||||
// spec/flavour.md, Block nodes: each mark by name, else the whole array as JSON, `undefined` where that array nests past largestNesting.
|
||||
export function spellBlockMarks(marks: readonly AdfMark[]): string | undefined {
|
||||
const named: string[] = []
|
||||
for (const mark of marks) {
|
||||
const vocabulary = markVocabulary(mark.type)
|
||||
const pairs = vocabulary === undefined ? undefined : vocabularyPairs(nodeAttrs(mark), vocabulary, [])
|
||||
if (pairs === undefined) return spellJsonAttribute(markValues(marks))
|
||||
named.push(`${mark.type}${spellAttributes([...spellVocabulary(pairs), ...spellEmptyKeys(mark)])}`)
|
||||
const vocabulary = markVocabularies.get(mark.type)
|
||||
const attributes = vocabulary === undefined ? undefined : spellMarkVocabulary(mark, vocabulary)
|
||||
if (attributes === undefined) return overNested(markValues(marks)) ? undefined : spellJsonAttribute(markValues(marks))
|
||||
named.push(`${mark.type}${attributes}`)
|
||||
}
|
||||
return spellStringAttribute(named.join(' '))
|
||||
}
|
||||
|
||||
@@ -3,16 +3,17 @@ import type { ConvertFault } from '../result.ts'
|
||||
import type { JsonValue } from '../json-value.ts'
|
||||
import { backslashEscape, holdsNullCharacter } from './commonmark/grammar.ts'
|
||||
import { backtickRun, closingBacktickRun } from './commonmark/backtick-runs.ts'
|
||||
import { isNumberList } from '../adf/attribute-vocabulary.ts'
|
||||
import { overNested } from '../json-value.ts'
|
||||
import { largestNesting } from '../nesting.ts'
|
||||
import { parseJsonText } from '../json-text.ts'
|
||||
import { serializeCanonicalJson } from '../canonical-json.ts'
|
||||
|
||||
export type Attributes = { attributes: DirectiveAttributes; end: number }
|
||||
|
||||
export type AttributeReading = { refusal: 'kind' | 'nesting'; value?: undefined } | { refusal?: undefined; value: VocabularyValue }
|
||||
|
||||
// Proof that `!adf:` stands at `index`; only `claimDirectivePrefix` builds one.
|
||||
export type Attributes = { attributes: DirectiveAttributes; end: number }
|
||||
|
||||
export type ClaimedPrefix = { readonly [claimedPrefix]: true; readonly index: number }
|
||||
|
||||
export type DirectiveValue = { decoded: string; spelling: string }
|
||||
@@ -65,9 +66,10 @@ const prefixFault = `an unescaped ${directivePrefix} completes no directive; ${d
|
||||
export function attributeValue(text: string, kind: AttributeKind): AttributeReading {
|
||||
if (kind === 'string') return { value: { kind, value: text } }
|
||||
if (kind === 'boolean') return text === 'true' || text === 'false' ? { value: { kind, value: text === 'true' } } : { refusal: 'kind' }
|
||||
// A JSON array reads only so the respelling check names the comma list to write.
|
||||
if (kind === 'numbers') {
|
||||
const values = text.split(',').map((part) => parseJsonText(part).value)
|
||||
return values.every((value) => typeof value === 'number') ? { value: { kind, value: values } } : { refusal: 'kind' }
|
||||
const values = text.startsWith('[') ? parseJsonText(text).value : text.split(',').map((part) => parseJsonText(part).value)
|
||||
return isNumberList(values) ? { value: { kind, value: values } } : { refusal: 'kind' }
|
||||
}
|
||||
const parsed = parseJsonText(text).value
|
||||
if (parsed === undefined) return { refusal: 'kind' }
|
||||
|
||||
@@ -431,6 +431,7 @@ 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', () => {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { AdfNode } from '../../adf/document.ts'
|
||||
import type { BlockNodeModel } from '../../adf/block-nodes.ts'
|
||||
import { attributeNestingMessage, nodeAttrs, nodeMarks } from '../../adf/document.ts'
|
||||
import { blockArgument, markValues, marksAttribute, spellBlockMarks } from '../block-directive.ts'
|
||||
import { blockArgument, marksAttribute, spellBlockMarks } from '../block-directive.ts'
|
||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { isBareToken, spellAttributes, spellDirectiveOpener, spellVocabulary } from '../directive-syntax.ts'
|
||||
import { overNested } from '../../json-value.ts'
|
||||
import { spellEmptyKeys } from '../empty-keys.ts'
|
||||
import { vocabularyPairs } from '../../adf/attribute-vocabulary.ts'
|
||||
|
||||
@@ -18,8 +17,9 @@ export function spellBlockDirectiveOpener(node: AdfNode, model: BlockNodeModel,
|
||||
const spelledPairs = [...spellVocabulary(pairs), ...spellEmptyKeys(node)]
|
||||
const marks = nodeMarks(node)
|
||||
if (marks.length > 0) {
|
||||
if (overNested(markValues(marks))) return failure('unsupported-nesting-depth', attributeNestingMessage(marksAttribute, node.type), path)
|
||||
spelledPairs.push([marksAttribute, spellBlockMarks(marks)])
|
||||
const spelling = spellBlockMarks(marks)
|
||||
if (spelling === undefined) return failure('unsupported-nesting-depth', attributeNestingMessage(marksAttribute, node.type), path)
|
||||
spelledPairs.push([marksAttribute, spelling])
|
||||
}
|
||||
return success(spellDirectiveOpener(node.type, slot.argument, spellAttributes(spelledPairs)))
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ import type { AdfAttributes, AdfMark, AdfNode } from '../adf/document.ts'
|
||||
import type { AttributeVocabulary } from '../adf/attribute-vocabulary.ts'
|
||||
import type { LinkDefinition } from './commonmark/link-syntax.ts'
|
||||
import type { MarkType } from '../adf/mark-attributes.ts'
|
||||
import { emptyKeys, nodeAttrs, nodeMarks } from '../adf/document.ts'
|
||||
import { escapeUnbalanced, spellLinkTarget } from './commonmark/link-syntax.ts'
|
||||
import { holdsDirectivePrefix, spellAttributes, spellVocabulary } from './directive-syntax.ts'
|
||||
import { holdsEntityReference } from './commonmark/entity-references.ts'
|
||||
import { isAutolink } from './commonmark/grammar.ts'
|
||||
import { isMarkType, markAttributes } from '../adf/mark-attributes.ts'
|
||||
import { nodeAttrs, nodeMarks } from '../adf/document.ts'
|
||||
import { spellEmptyKeys } from './empty-keys.ts'
|
||||
import { vocabularyPairs } from '../adf/attribute-vocabulary.ts'
|
||||
|
||||
@@ -71,8 +71,11 @@ function isBareLink(nodes: readonly AdfNode[], href: string, marksInside: number
|
||||
|
||||
// `undefined` where the spelling holds no such attrs: only a directive spells attrs=empty.
|
||||
export function spellMarkAttributes(mark: AdfMark, spelling: MarkSpelling): string | undefined {
|
||||
const pairs = vocabularyPairs(nodeAttrs(mark), spelling.attributes, [])
|
||||
const empty = spellEmptyKeys(mark)
|
||||
if (pairs === undefined || (empty.length > 0 && spelling.kind !== 'directive')) return undefined
|
||||
return spellAttributes([...spellVocabulary(pairs), ...empty])
|
||||
return emptyKeys(mark).length > 0 && spelling.kind !== 'directive' ? undefined : spellMarkVocabulary(mark, spelling.attributes)
|
||||
}
|
||||
|
||||
// `undefined` where an attribute is one the vocabulary does not type.
|
||||
export function spellMarkVocabulary(mark: AdfMark, vocabulary: AttributeVocabulary): string | undefined {
|
||||
const pairs = vocabularyPairs(nodeAttrs(mark), vocabulary, [])
|
||||
return pairs === undefined ? undefined : spellAttributes([...spellVocabulary(pairs), ...spellEmptyKeys(mark)])
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export function readVocabulary(
|
||||
if (kind === undefined) return failure('unsupported-node-shape', `${type} holds no ${key} attribute: this one spells it`, path)
|
||||
const read = attributeValue(spelled.decoded, kind)
|
||||
if (read.refusal === 'nesting') return failure('unsupported-nesting-depth', attributeNestingMessage(key, type), path)
|
||||
if (read.value === undefined) return failure('unsupported-node-shape', `the ${key} attribute of ${type} is no ${kind}`, path)
|
||||
if (read.value === undefined) return failure('unsupported-node-shape', `the ${key} attribute of ${type} is no ${kind === 'numbers' ? 'comma list of numbers' : kind}`, path)
|
||||
const spelling = spellAttributeValue(read.value)
|
||||
if (spelling !== spelled.spelling) return failure('unsupported-node-shape', `${type} spells its ${key} attribute as ${key}=${spelling}`, path)
|
||||
attrs[key] = read.value.value
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AdfMark } from '../../adf/document.ts'
|
||||
import type { AttributeVocabulary } from '../../adf/attribute-vocabulary.ts'
|
||||
import type { ConvertFault } from '../../result.ts'
|
||||
import type { DirectiveAttributes } from '../directive-syntax.ts'
|
||||
import type { MarkSpelling } from '../mark-spellings.ts'
|
||||
@@ -13,12 +14,16 @@ export function readDirectiveMark(name: string, attributes: DirectiveAttributes,
|
||||
if (spelling === undefined) return undefined
|
||||
const markdown = markdownForm(spelling)
|
||||
if (markdown !== undefined) return failure('unsupported-node-shape', `${name} is spelled ${markdown}, never as a directive`, path)
|
||||
const empty = readEmptyKeys(name, attributes, ['attrs'], false)
|
||||
return readMarkAttributes(name, attributes, spelling.attributes, path)
|
||||
}
|
||||
|
||||
export function readMarkAttributes(type: string, attributes: DirectiveAttributes, vocabulary: AttributeVocabulary, path: ConvertErrorPath): Result<AdfMark> {
|
||||
const empty = readEmptyKeys(type, attributes, ['attrs'], false)
|
||||
if (empty.fault !== undefined) return faulted(empty.fault, path)
|
||||
const attrs = readVocabulary(name, empty.value.rest, spelling.attributes, undefined, path)
|
||||
const attrs = readVocabulary(type, empty.value.rest, vocabulary, undefined, path)
|
||||
if (!attrs.ok) return attrs
|
||||
if (empty.value.empty.has('attrs')) return success({ attrs: {}, type: name })
|
||||
return success(Object.keys(attrs.value).length === 0 ? { type: name } : { attrs: attrs.value, type: name })
|
||||
if (empty.value.empty.has('attrs')) return success({ attrs: {}, type })
|
||||
return success(Object.keys(attrs.value).length === 0 ? { type } : { attrs: attrs.value, type })
|
||||
}
|
||||
|
||||
export function inlineMarkSpellingFault(name: string): ConvertFault | undefined {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { AdfAttributes, AdfMark, AdfNode, EmptyKey } from '../../adf/document.ts'
|
||||
import type { AttributeVocabulary } from '../../adf/attribute-vocabulary.ts'
|
||||
import type { BlockNodeModel } from '../../adf/block-nodes.ts'
|
||||
import type { ConvertFault } from '../../result.ts'
|
||||
import type { DirectiveAttributes, DirectiveValue } from '../directive-syntax.ts'
|
||||
@@ -10,9 +9,9 @@ import { blockArgument, blockDirectiveForm, marksAttribute, readMarkValues, spel
|
||||
import { blockNodeModel } from '../../adf/block-nodes.ts'
|
||||
import { carryFencePrefix, carryName } from '../opaque-carry.ts'
|
||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { inlineMarkSpellingFault } from './directive-marks.ts'
|
||||
import { inlineMarkSpellingFault, readMarkAttributes } from './directive-marks.ts'
|
||||
import { inlineNodeModel } from '../../adf/inline-nodes.ts'
|
||||
import { markVocabulary } from '../../adf/mark-attributes.ts'
|
||||
import { markVocabularies } from '../../adf/mark-attributes.ts'
|
||||
import { readEmptyKeys, spellsEmpty } from '../empty-keys.ts'
|
||||
import { readVocabulary } from './directive-attributes.ts'
|
||||
import { slotFault } from '../directive-syntax.ts'
|
||||
@@ -119,33 +118,30 @@ function readNamedMarks(type: string, text: string, path: ConvertErrorPath): Res
|
||||
const marks: AdfMark[] = []
|
||||
let cursor = 0
|
||||
for (;;) {
|
||||
const name = readDirectiveName(text, cursor)
|
||||
if (name === undefined) return failure('unsupported-node-shape', unnamedMarks(type), path)
|
||||
const vocabulary = markVocabulary(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)
|
||||
cursor += name.length
|
||||
const attributes = text.charAt(cursor) === '{' ? readAttributes(text, cursor) : { value: { attributes: new Map<string, DirectiveValue>(), end: cursor } }
|
||||
if (attributes.fault !== undefined) return faulted(attributes.fault, path)
|
||||
const mark = readNamedMark(name, attributes.value.attributes, vocabulary, path)
|
||||
const mark = readNamedMark(type, text, cursor, path)
|
||||
if (!mark.ok) return mark
|
||||
marks.push(mark.value)
|
||||
cursor = attributes.value.end
|
||||
marks.push(mark.value.mark)
|
||||
cursor = mark.value.end
|
||||
if (cursor === text.length) return success(marks)
|
||||
if (text.charAt(cursor) !== ' ') return failure('unsupported-node-shape', unnamedMarks(type), path)
|
||||
cursor += 1
|
||||
}
|
||||
}
|
||||
|
||||
function unnamedMarks(type: string): string {
|
||||
return `the ${marksAttribute} attribute of ${type} names each mark with its {attrs}, one space apart: this one does not`
|
||||
function readNamedMark(type: string, text: string, index: number, path: ConvertErrorPath): Result<{ end: number; mark: AdfMark }> {
|
||||
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)
|
||||
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)
|
||||
const mark = readMarkAttributes(name, attributes.value.attributes, vocabulary, path)
|
||||
return mark.ok ? success({ end: attributes.value.end, mark: mark.value }) : mark
|
||||
}
|
||||
|
||||
function readNamedMark(type: string, attributes: DirectiveAttributes, vocabulary: AttributeVocabulary, path: ConvertErrorPath): Result<AdfMark> {
|
||||
const empty = readEmptyKeys(type, attributes, ['attrs'], false)
|
||||
if (empty.fault !== undefined) return faulted(empty.fault, path)
|
||||
const attrs = readVocabulary(type, empty.value.rest, vocabulary, undefined, path)
|
||||
if (!attrs.ok) return attrs
|
||||
return success(Object.keys(attrs.value).length > 0 || empty.value.empty.has('attrs') ? { attrs: attrs.value, type } : { type })
|
||||
function unnamedMarks(type: string): string {
|
||||
return `the ${marksAttribute} attribute of ${type} names each mark with its {attrs}, one space apart: this one does not`
|
||||
}
|
||||
|
||||
function namedNode(type: string, attrs: AdfAttributes, marks: readonly AdfMark[] | undefined, empty: ReadonlySet<EmptyKey>): AdfNode {
|
||||
|
||||
@@ -556,6 +556,10 @@ 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)
|
||||
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="breakout{width=wide}"}\n')), 'unsupported-node-shape: the width attribute of breakout is no number')
|
||||
@@ -577,9 +581,10 @@ test('names the attribute a node holds no reading for', () => {
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:media {width=wide}\n')), 'unsupported-node-shape: the width attribute of media is no number')
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:table {isNumberColumnEnabled=yes}\n!adf:/table\n')), 'unsupported-node-shape: the isNumberColumnEnabled attribute of table is no boolean')
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:media {width=true}\n')), 'unsupported-node-shape: the width attribute of media is no number')
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:tableCell {colwidth="340,"}\n!adf:/tableCell\n')), 'unsupported-node-shape: the colwidth attribute of tableCell is no numbers')
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:tableCell {colwidth="[340,420]"}\n!adf:/tableCell\n')), 'unsupported-node-shape: the colwidth attribute of tableCell is no numbers')
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:tableCell {colwidth=""}\n!adf:/tableCell\n')), 'unsupported-node-shape: the colwidth attribute of tableCell is no numbers')
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:tableCell {colwidth="340,"}\n!adf:/tableCell\n')), 'unsupported-node-shape: the colwidth attribute of tableCell is no comma list of numbers')
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:tableCell {colwidth="[]"}\n!adf:/tableCell\n')), 'unsupported-node-shape: the colwidth attribute of tableCell is no comma list of numbers')
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:tableCell {colwidth="[340,\\"420\\"]"}\n!adf:/tableCell\n')), 'unsupported-node-shape: the colwidth attribute of tableCell is no comma list of numbers')
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:tableCell {colwidth=""}\n!adf:/tableCell\n')), 'unsupported-node-shape: the colwidth attribute of tableCell is no comma list of numbers')
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:panel info {panelType=note}\nx\n!adf:/panel\n')), 'unsupported-node-shape: panel spells its panelType attribute as the directive argument, never in {attrs}')
|
||||
assert.equal(content(losslessMarkdownToAdf('Part !adf:mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: mention spells its text attribute in the content slot, never in {attrs}')
|
||||
})
|
||||
@@ -600,6 +605,7 @@ test('names the attribute value spelled outside the canonical form', () => {
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:media {width="20.0"}\n')), 'unsupported-node-shape: media spells its width attribute as width=20')
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:tableCell {colwidth="340, 420"}\n!adf:/tableCell\n')), 'unsupported-node-shape: tableCell spells its colwidth attribute as colwidth="340,420"')
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:tableCell {colwidth="340"}\n!adf:/tableCell\n')), 'unsupported-node-shape: tableCell spells its colwidth attribute as colwidth=340')
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:tableCell {colwidth="[340,420]"}\n!adf:/tableCell\n')), 'unsupported-node-shape: tableCell spells its colwidth attribute as colwidth="340,420"')
|
||||
})
|
||||
|
||||
test('names the argument and the body a node takes no reading for', () => {
|
||||
|
||||
Reference in New Issue
Block a user