mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Spell colwidth as a comma list and a block's marks by name
This commit is contained in:
@@ -1,2 +1 @@
|
||||
!adf:tableCell {colwidth="[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[1]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"}
|
||||
!adf:/tableCell
|
||||
!adf:extension {parameters="[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[1]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
!adf:layoutSection {localId=01a033d0-1393-790b-9d44-c572033ea4aa marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"}
|
||||
!adf:layoutSection {localId=01a033d0-1393-790b-9d44-c572033ea4aa marks="breakout{mode=wide}"}
|
||||
!adf:layoutColumn {width=100}
|
||||
Full width.
|
||||
!adf:/layoutColumn
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Release checklist
|
||||
!adf:/heading
|
||||
|
||||
!adf:paragraph {marks="[{\"attrs\":{\"align\":\"center\"},\"type\":\"alignment\"}]"}
|
||||
!adf:paragraph {marks="alignment{align=center}"}
|
||||
Centred.
|
||||
!adf:/paragraph
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
!adf:table {displayMode=fixed isNumberColumnEnabled=true layout=default localId=01a0339e-c34f-7658-9d98-eb059401b7ab width=760}
|
||||
!adf:tableRow
|
||||
!adf:tableHeader {colspan=2 colwidth="[340,420]"}
|
||||
!adf:tableHeader {colspan=2 colwidth="340,420"}
|
||||
Assembly
|
||||
!adf:/tableHeader
|
||||
!adf:/tableRow
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
!adf:table {isNumberColumnEnabled=true layout=wide width="760.5"}
|
||||
!adf:tableRow
|
||||
!adf:tableHeader {colspan=2 colwidth="[340,420]"}
|
||||
!adf:tableHeader {colspan=2 colwidth="340,420"}
|
||||
Assembly
|
||||
!adf:/tableHeader
|
||||
!adf:/tableRow
|
||||
|
||||
+21
-9
@@ -205,15 +205,19 @@ 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"`) — 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
|
||||
(`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
|
||||
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` — the node's marks array as a
|
||||
`json` value, `marks=empty` where it is empty:
|
||||
`!adf:layoutSection {marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"}`.
|
||||
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\"}]"`.
|
||||
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.
|
||||
|
||||
@@ -366,7 +370,7 @@ The directive form nests cells as containers of block content inside `tableRow`
|
||||
```
|
||||
!adf:table {isNumberColumnEnabled=true width=760}
|
||||
!adf:tableRow
|
||||
!adf:tableHeader {colspan=2 colwidth="[340,420]"}
|
||||
!adf:tableHeader {colspan=2 colwidth="340,420"}
|
||||
Assembly
|
||||
!adf:/tableHeader
|
||||
!adf:/tableRow
|
||||
@@ -386,7 +390,7 @@ Bolt M8
|
||||
`full-width` `wide`), `localId` (string), `width` (number, pixels).
|
||||
- `tableRow` — container of cells. Attributes: `localId` (string).
|
||||
- `tableCell`, `tableHeader` — containers, block body. Attributes: `background` (string),
|
||||
`colspan` (number), `colwidth` (json, one pixel width per spanned column), `localId` (string),
|
||||
`colspan` (number), `colwidth` (numbers, one pixel width per spanned column), `localId` (string),
|
||||
`rowspan` (number), `valign` (`bottom` `middle` `top`).
|
||||
|
||||
### Task and decision lists
|
||||
@@ -535,6 +539,14 @@ breaks reads the same, followed by its hard breaks.
|
||||
- `textColor` — Attributes: `color` (string, `#rrggbb`).
|
||||
- `underline` — Attributes: none.
|
||||
|
||||
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).
|
||||
|
||||
A spelling adds its mark to every inline node it wraps but a hard break, which Atlassian's schema
|
||||
gives no marks, and nesting is the marks array in order, outermost first: `_!adf:underline[x]_`
|
||||
gives marks `[em, underline]`, `!adf:underline[_x_]` the reverse. `adfToLosslessMarkdown` nests in
|
||||
@@ -544,8 +556,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 not listed
|
||||
here, 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 first
|
||||
list 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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AdfAttributes } from './document.ts'
|
||||
import type { JsonValue } from '../json-value.ts'
|
||||
|
||||
export type AttributeKind = 'boolean' | 'json' | 'number' | 'string'
|
||||
export type AttributeKind = 'boolean' | 'json' | 'number' | 'numbers' | 'string'
|
||||
|
||||
export type AttributeVocabulary = Readonly<Record<string, AttributeKind>>
|
||||
|
||||
@@ -9,6 +9,7 @@ export type VocabularyValue =
|
||||
| { kind: 'boolean'; value: boolean }
|
||||
| { kind: 'json'; value: JsonValue }
|
||||
| { kind: 'number'; value: number }
|
||||
| { kind: 'numbers'; value: number[] }
|
||||
| { kind: 'string'; value: string }
|
||||
|
||||
export type VocabularyPair = VocabularyValue & { key: string }
|
||||
@@ -29,6 +30,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 === 'string') return typeof value === 'string' ? { key, kind, value } : undefined
|
||||
return { key, kind, value }
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export type BlockNodeModel = {
|
||||
const cellAttributes: AttributeVocabulary = {
|
||||
background: 'string',
|
||||
colspan: 'number',
|
||||
colwidth: 'json',
|
||||
colwidth: 'numbers',
|
||||
localId: 'string',
|
||||
rowspan: 'number',
|
||||
valign: 'string',
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
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' },
|
||||
dataConsumer: { sources: 'json' },
|
||||
fragment: { localId: 'string', name: 'string' },
|
||||
indentation: { level: 'number' },
|
||||
} satisfies Readonly<Record<string, AttributeVocabulary>>
|
||||
|
||||
export const markAttributes = {
|
||||
border: { color: 'string', size: 'number' },
|
||||
code: {},
|
||||
@@ -12,8 +21,14 @@ export const markAttributes = {
|
||||
underline: {},
|
||||
} satisfies Readonly<Record<string, AttributeVocabulary>>
|
||||
|
||||
const blockMarkVocabularies: ReadonlyMap<string, AttributeVocabulary> = new Map(Object.entries(blockMarkAttributes))
|
||||
|
||||
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,14 +9,14 @@ 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 { markAttributes } from '../adf/mark-attributes.ts'
|
||||
import { blockMarkAttributes, markAttributes } from '../adf/mark-attributes.ts'
|
||||
|
||||
type Held = Map<string, Set<AttributeKind>>
|
||||
type Properties = Map<string, SchemaObject[]>
|
||||
type SchemaObject = Readonly<Record<string, unknown>>
|
||||
type Spelled = [string, Map<string, AttributeKind>]
|
||||
|
||||
const carried = ['alignment', 'annotation', 'backgroundColor', 'blockCard', 'bodiedRule', 'breakout', 'dataConsumer', 'embedCard', 'fontSize', 'fragment', 'indentation', 'inlineExtension', 'placeholder']
|
||||
const carried = ['annotation', 'backgroundColor', 'blockCard', 'bodiedRule', 'embedCard', 'fontSize', 'inlineExtension', 'placeholder']
|
||||
const definitionReference = '#/definitions/'
|
||||
const gaps: string[] = []
|
||||
const grammarOwn = ['doc', 'text']
|
||||
@@ -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(markAttributes).map(([type, attributes]) => spelledType(type, attributes)),
|
||||
...Object.entries({ ...blockMarkAttributes, ...markAttributes }).map(([type, attributes]) => spelledType(type, attributes)),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -166,6 +166,7 @@ function propertyKinds(property: SchemaObject, where: string): AttributeKind[] {
|
||||
if (type === 'boolean') return ['boolean']
|
||||
if (type === 'integer' || type === 'number') return ['number']
|
||||
if (type === 'string') return ['string']
|
||||
if (type === 'array' && isSchemaObject(property['items']) && property['items']['type'] === 'number') return ['numbers']
|
||||
if (type === 'array' || type === 'object') return ['json']
|
||||
if (type !== undefined) return assert.fail(`${where}: the schema types it ${JSON.stringify(type)}, which reads as no attribute kind`)
|
||||
const combinator = ['$ref', 'allOf', 'anyOf'].find((keyword) => property[keyword] !== undefined)
|
||||
|
||||
@@ -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 { markAttributes } from '../adf/mark-attributes.ts'
|
||||
import { blockMarkAttributes, markAttributes } from '../adf/mark-attributes.ts'
|
||||
import { textDirectiveName } from '../markdown/text-directive.ts'
|
||||
|
||||
type Declared = { attributes: AttributeVocabulary }
|
||||
@@ -80,7 +80,7 @@ function attributeList(heading: string, prose: string): AttributeVocabulary {
|
||||
|
||||
function attributeKind(heading: string, parenthesized: string): AttributeKind {
|
||||
const first = parenthesized.split(/[\s,]/)[0] ?? ''
|
||||
if (first === 'boolean' || first === 'json' || first === 'number' || first === 'string') return first
|
||||
if (first === 'boolean' || first === 'json' || first === 'number' || first === 'numbers' || first === 'string') return first
|
||||
assert.ok(first.startsWith('`'), `${heading}: ${first} is neither an attribute kind nor a value set`)
|
||||
return 'string'
|
||||
}
|
||||
@@ -104,5 +104,5 @@ 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(markAttributes).map(([type, attributes]) => [type, { attributes }]))))
|
||||
assert.deepEqual(declarations('Marks'), vocabularies(Object.fromEntries(Object.entries({ ...blockMarkAttributes, ...markAttributes }).map(([type, attributes]) => [type, { attributes }]))))
|
||||
})
|
||||
|
||||
@@ -8,13 +8,13 @@ import type { AttributeKind, AttributeVocabulary } from '../adf/attribute-vocabu
|
||||
import type { JsonValue } from '../json-value.ts'
|
||||
import type { LinkDefinition } from '../markdown/commonmark/link-syntax.ts'
|
||||
import { blockArgument } from '../markdown/block-directive.ts'
|
||||
import { blockMarkAttributes, markAttributes } from '../adf/mark-attributes.ts'
|
||||
import { blockNodes } from '../adf/block-nodes.ts'
|
||||
import { centeredImage, externalMedia } from '../markdown/external-image.ts'
|
||||
import { directivePrefix } from '../markdown/directive-syntax.ts'
|
||||
import { emptyKeys, mergeAdjacentText, nodeContent } from '../adf/document.ts'
|
||||
import { inlineNodes } from '../adf/inline-nodes.ts'
|
||||
import { joinsWhenRead } from '../markdown/adjacent-text.ts'
|
||||
import { markAttributes } from '../adf/mark-attributes.ts'
|
||||
|
||||
type Positions = { block: AdfNode; inline: AdfNode }
|
||||
|
||||
@@ -61,6 +61,7 @@ const valueByKind: Readonly<Record<AttributeKind, Arbitrary<JsonValue>>> = {
|
||||
boolean: fc.boolean(),
|
||||
json: jsonValue,
|
||||
number: numberValue,
|
||||
numbers: fc.array(numberValue, { maxLength: 3 }),
|
||||
string: textOf(0),
|
||||
}
|
||||
|
||||
@@ -108,6 +109,7 @@ const mark: Arbitrary<AdfMark> = occasionallyEmpty(fc.oneof(
|
||||
{ arbitrary: fc.record({ attrs: fc.dictionary(jsonKey, jsonValue, { maxKeys: 2, noNullPrototype: true }), type: unknownType }), weight: 1 },
|
||||
))
|
||||
const marks = fc.uniqueArray(mark, { maxLength: 3, selector: (held) => held.type })
|
||||
const blockMark: Arbitrary<AdfMark> = occasionallyEmpty(fc.oneof(...Object.entries(blockMarkAttributes).map(([type, vocabulary]) => attributes(vocabulary).map((attrs) => ({ attrs, type })))))
|
||||
|
||||
const textNode = occasionallyEmpty(fc.record({ marks, text: fc.oneof({ arbitrary: text, weight: 19 }, { arbitrary: fc.constant(''), weight: 1 }) }).map((held): AdfNode => ({ ...held, type: 'text' })))
|
||||
|
||||
@@ -136,7 +138,11 @@ const positions = fc.letrec<Positions>((tie) => {
|
||||
inline: inlineContent,
|
||||
none: fc.constant<AdfNode[]>([]),
|
||||
}
|
||||
const blockMarks = fc.oneof({ arbitrary: fc.constant<AdfMark[]>([]), weight: 4 }, { arbitrary: marks, weight: 1 })
|
||||
const blockMarks = fc.oneof(
|
||||
{ arbitrary: fc.constant<AdfMark[]>([]), weight: 4 },
|
||||
{ arbitrary: marks, weight: 1 },
|
||||
{ arbitrary: fc.uniqueArray(fc.oneof(blockMark, mark), { maxLength: 3, selector: (held) => held.type }), weight: 1 },
|
||||
)
|
||||
const blockArbitraries = Object.entries(blockNodes).map(([type, model]) => {
|
||||
const argument = blockArgument(type)
|
||||
const vocabulary: AttributeVocabulary = argument === undefined ? model.attributes : { ...model.attributes, [argument]: 'string' }
|
||||
|
||||
@@ -2,9 +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 } from '../adf/document.ts'
|
||||
import { isAdfMark, nodeAttrs } from '../adf/document.ts'
|
||||
import { markVocabulary } from '../adf/mark-attributes.ts'
|
||||
import { serializeCanonicalJson } from '../canonical-json.ts'
|
||||
import { spellAttributes, spellDirectiveOpener } from './directive-syntax.ts'
|
||||
import { spellAttributes, spellDirectiveOpener, spellJsonAttribute, spellStringAttribute, spellVocabulary } from './directive-syntax.ts'
|
||||
import { spellEmptyKeys } from './empty-keys.ts'
|
||||
import { vocabularyPairs } from '../adf/attribute-vocabulary.ts'
|
||||
|
||||
const argumentByType = new Map(
|
||||
Object.entries({
|
||||
@@ -38,6 +41,18 @@ 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 {
|
||||
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)])}`)
|
||||
}
|
||||
return spellStringAttribute(named.join(' '))
|
||||
}
|
||||
|
||||
export function readMarkValues(value: JsonValue): AdfMark[] | undefined {
|
||||
if (!Array.isArray(value) || value.length === 0) return undefined
|
||||
const marks: AdfMark[] = []
|
||||
|
||||
@@ -8,6 +8,8 @@ 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.
|
||||
@@ -28,8 +30,6 @@ export type NestedSpans = ReadonlyMap<number, DirectiveSpan>
|
||||
|
||||
export type Read<T> = { fault: ConvertFault; value?: undefined } | { fault?: undefined; value: T }
|
||||
|
||||
type Attributes = { attributes: DirectiveAttributes; end: number }
|
||||
|
||||
type AttributePair = { end: number; key: string; value: DirectiveValue }
|
||||
|
||||
type Content = { content: string | undefined; end: number; spans: NestedSpans }
|
||||
@@ -65,6 +65,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' }
|
||||
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 parsed = parseJsonText(text).value
|
||||
if (parsed === undefined) return { refusal: 'kind' }
|
||||
if (kind === 'number') return typeof parsed === 'number' ? { value: { kind, value: parsed } } : { refusal: 'kind' }
|
||||
@@ -145,6 +149,7 @@ export function spellAttributeValue(value: VocabularyValue): string {
|
||||
if (value.kind === 'boolean') return `${value.value}`
|
||||
if (value.kind === 'json') return spellJsonAttribute(value.value)
|
||||
if (value.kind === 'number') return spellStringAttribute(serializeCanonicalJson(value.value, 'compact'))
|
||||
if (value.kind === 'numbers') return spellStringAttribute(value.value.map((item) => serializeCanonicalJson(item, 'compact')).join(','))
|
||||
return spellStringAttribute(value.value)
|
||||
}
|
||||
|
||||
@@ -200,7 +205,7 @@ function readOpenerLine(line: string): Read<DirectiveLine> | undefined {
|
||||
return { value: { argument: opener.value.argument, attributes: opener.value.attributes, kind: 'opener', name } }
|
||||
}
|
||||
|
||||
function readDirectiveName(text: string, index: number): string | undefined {
|
||||
export function readDirectiveName(text: string, index: number): string | undefined {
|
||||
directiveName.lastIndex = index
|
||||
return directiveName.exec(text)?.[0]
|
||||
}
|
||||
@@ -289,7 +294,7 @@ function readCodeSpanEnd(text: string, index: number): number | undefined {
|
||||
return text.slice(index, closer + opener).includes('\n') ? undefined : closer + opener
|
||||
}
|
||||
|
||||
function readAttributes(text: string, index: number): Read<Attributes> {
|
||||
export function readAttributes(text: string, index: number): Read<Attributes> {
|
||||
const attributes = new Map<string, DirectiveValue>()
|
||||
let cursor = index + 1
|
||||
let previous = ''
|
||||
|
||||
@@ -90,7 +90,7 @@ test('spells a CommonMark block as a directive where its own spelling holds neit
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ attrs: { localId: 'a' }, type: 'paragraph' }))), '!adf:paragraph {localId=a}\n!adf:/paragraph\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ attrs: { wrap: true }, type: 'codeBlock' }))), '!adf:codeBlock {wrap=true}\n```\n```\n!adf:/codeBlock\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(paragraph({ attrs: { localId: 'a' }, type: 'hardBreak' }, { text: 'x', type: 'text' })))), '!adf:hardBreak{localId=a}x\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ marks: [{ type: 'border' }], type: 'blockquote' }))), '!adf:blockquote {marks="[{\\"type\\":\\"border\\"}]"}\n!adf:/blockquote\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ marks: [{ type: 'border' }], type: 'blockquote' }))), '!adf:blockquote {marks=border}\n!adf:/blockquote\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ type: 'listItem' }))), '!adf:listItem\n!adf:/listItem\n')
|
||||
})
|
||||
|
||||
@@ -615,11 +615,36 @@ test('carries an arg slot value no bare token spells', () => {
|
||||
|
||||
test('carries a block node mark in the reserved attribute', () => {
|
||||
const section = (...marks: AdfMark[]): AdfDocument => document({ marks, type: 'layoutSection' })
|
||||
assert.equal(markdown(adfToLosslessMarkdown(section({ type: 'breakout' }))), '!adf:layoutSection {marks="[{\\"type\\":\\"breakout\\"}]"}\n!adf:/layoutSection\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(section({ attrs: {}, type: 'breakout' }))), '!adf:layoutSection {marks="[{\\"attrs\\":{},\\"type\\":\\"breakout\\"}]"}\n!adf:/layoutSection\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(section({ type: 'breakout' }))), '!adf:layoutSection {marks=breakout}\n!adf:/layoutSection\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(section({ attrs: {}, type: 'breakout' }))), '!adf:layoutSection {marks="breakout{attrs=empty}"}\n!adf:/layoutSection\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(section())), '!adf:layoutSection {marks=empty}\n!adf:/layoutSection\n')
|
||||
})
|
||||
|
||||
test('spells each block mark by name and attrs, in order, where every mark is one the tables type', () => {
|
||||
const section = (...marks: AdfMark[]): AdfDocument => document({ marks, type: 'layoutSection' })
|
||||
assert.equal(markdown(adfToLosslessMarkdown(section({ attrs: { mode: 'wide' }, type: 'breakout' }))), '!adf:layoutSection {marks="breakout{mode=wide}"}\n!adf:/layoutSection\n')
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(section({ attrs: { level: 2 }, type: 'indentation' }, { attrs: { localId: 'f1', name: 'Price list' }, type: 'fragment' }))),
|
||||
'!adf:layoutSection {marks="indentation{level=2} fragment{localId=f1 name=\\"Price list\\"}"}\n!adf:/layoutSection\n',
|
||||
)
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(section({ type: 'em' }, { attrs: { color: '#fff' }, type: 'backgroundColor' }))),
|
||||
'!adf:layoutSection {marks="[{\\"type\\":\\"em\\"},{\\"attrs\\":{\\"color\\":\\"#fff\\"},\\"type\\":\\"backgroundColor\\"}]"}\n!adf:/layoutSection\n',
|
||||
)
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(section({ attrs: { level: '2' }, type: 'indentation' }))),
|
||||
'!adf:layoutSection {marks="[{\\"attrs\\":{\\"level\\":\\"2\\"},\\"type\\":\\"indentation\\"}]"}\n!adf:/layoutSection\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('spells colwidth as its widths, comma-separated, and carries a cell whose colwidth no list of numbers spells', () => {
|
||||
const cell = (colwidth: JsonValue): AdfDocument => document({ attrs: { colwidth }, type: 'tableCell' })
|
||||
assert.equal(markdown(adfToLosslessMarkdown(cell([340, 420.5]))), '!adf:tableCell {colwidth="340,420.5"}\n!adf:/tableCell\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(cell([340]))), '!adf:tableCell {colwidth=340}\n!adf:/tableCell\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(cell([]))), '```adf:tableCell\n{\n "attrs": {\n "colwidth": []\n }\n}\n```\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(cell(['340']))), '```adf:tableCell\n{\n "attrs": {\n "colwidth": [\n "340"\n ]\n }\n}\n```\n')
|
||||
})
|
||||
|
||||
test('refuses the content a directive body has no room for', () => {
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ content: [paragraph()], type: 'media' }))), 'unsupported-node-shape: a media holds no content: this one holds some')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ text: 'x', type: 'panel' }))), 'unsupported-node-shape: a panel carries no text: this one holds text')
|
||||
@@ -685,7 +710,7 @@ test('spells a mediaSingle the image form does not fit as a directive', () => {
|
||||
const single: AdfNode = { attrs: { layout: 'center' }, content: [media], marks: [{ type: 'border' }], type: 'mediaSingle' }
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(document(single))),
|
||||
'!adf:mediaSingle {layout=center marks="[{\\"type\\":\\"border\\"}]"}\n!adf:media {type=external url="https://example.com/moon.png"}\n!adf:/mediaSingle\n',
|
||||
'!adf:mediaSingle {layout=center marks=border}\n!adf:media {type=external url="https://example.com/moon.png"}\n!adf:/mediaSingle\n',
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,9 +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 } from '../block-directive.ts'
|
||||
import { blockArgument, markValues, marksAttribute, spellBlockMarks } from '../block-directive.ts'
|
||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { isBareToken, spellAttributes, spellDirectiveOpener, spellJsonAttribute, spellVocabulary } from '../directive-syntax.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,9 +18,8 @@ export function spellBlockDirectiveOpener(node: AdfNode, model: BlockNodeModel,
|
||||
const spelledPairs = [...spellVocabulary(pairs), ...spellEmptyKeys(node)]
|
||||
const marks = nodeMarks(node)
|
||||
if (marks.length > 0) {
|
||||
const values = markValues(marks)
|
||||
if (overNested(values)) return failure('unsupported-nesting-depth', attributeNestingMessage(marksAttribute, node.type), path)
|
||||
spelledPairs.push([marksAttribute, spellJsonAttribute(values)])
|
||||
if (overNested(markValues(marks))) return failure('unsupported-nesting-depth', attributeNestingMessage(marksAttribute, node.type), path)
|
||||
spelledPairs.push([marksAttribute, spellBlockMarks(marks)])
|
||||
}
|
||||
return success(spellDirectiveOpener(node.type, slot.argument, spellAttributes(spelledPairs)))
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
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'
|
||||
import type { Elsewhere } from './directive-attributes.ts'
|
||||
import { attributeNestingMessage, isUnmarkedSpellableText } from '../../adf/document.ts'
|
||||
import { attributeValue, directivePrefix, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
|
||||
import { blockArgument, blockDirectiveForm, marksAttribute, readMarkValues } from '../block-directive.ts'
|
||||
import { attributeValue, 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'
|
||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { inlineMarkSpellingFault } from './directive-marks.ts'
|
||||
import { inlineNodeModel } from '../../adf/inline-nodes.ts'
|
||||
import { markVocabulary } from '../../adf/mark-attributes.ts'
|
||||
import { readEmptyKeys, spellsEmpty } from '../empty-keys.ts'
|
||||
import { readVocabulary } from './directive-attributes.ts'
|
||||
import { slotFault } from '../directive-syntax.ts'
|
||||
@@ -97,15 +99,55 @@ function slotText(content: readonly AdfNode[]): string | undefined {
|
||||
}
|
||||
|
||||
function readMarks(type: string, spelled: DirectiveValue, path: ConvertErrorPath): Result<AdfMark[]> {
|
||||
const read = attributeValue(spelled.decoded, 'json')
|
||||
const marks = spelled.decoded.startsWith('[') ? readMarkArray(type, spelled.decoded, path) : readNamedMarks(type, spelled.decoded, path)
|
||||
if (!marks.ok) return marks
|
||||
const spelling = spellBlockMarks(marks.value)
|
||||
return spelling === spelled.spelling ? marks : failure('unsupported-node-shape', `${type} spells its ${marksAttribute} attribute as ${marksAttribute}=${spelling}`, path)
|
||||
}
|
||||
|
||||
function readMarkArray(type: string, text: string, path: ConvertErrorPath): Result<AdfMark[]> {
|
||||
const read = attributeValue(text, 'json')
|
||||
if (read.refusal === 'nesting') return failure('unsupported-nesting-depth', attributeNestingMessage(marksAttribute, type), path)
|
||||
const marks = read.value === undefined || spellAttributeValue(read.value) !== spelled.spelling ? undefined : readMarkValues(read.value.value)
|
||||
const marks = read.value === undefined ? undefined : readMarkValues(read.value.value)
|
||||
if (marks === undefined) {
|
||||
return failure('unsupported-node-shape', `the ${marksAttribute} attribute of ${type} is its marks array in canonical JSON: this one is not`, path)
|
||||
}
|
||||
return success(marks)
|
||||
}
|
||||
|
||||
function readNamedMarks(type: string, text: string, path: ConvertErrorPath): Result<AdfMark[]> {
|
||||
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)
|
||||
if (!mark.ok) return mark
|
||||
marks.push(mark.value)
|
||||
cursor = attributes.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, 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 namedNode(type: string, attrs: AdfAttributes, marks: readonly AdfMark[] | undefined, empty: ReadonlySet<EmptyKey>): AdfNode {
|
||||
const node: AdfNode = Object.keys(attrs).length > 0 || empty.has('attrs') ? { attrs, type } : { type }
|
||||
if (empty.has('content')) node.content = []
|
||||
|
||||
@@ -525,26 +525,51 @@ test('reads each attribute value as the type its section assigns', () => {
|
||||
{ attrs: { height: 10, id: 'a-1', type: 'file', url: '/x y', width: 20.5 }, type: 'media' },
|
||||
])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:table {isNumberColumnEnabled=true}\n!adf:/table\n')), [{ attrs: { isNumberColumnEnabled: true }, type: 'table' }])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:tableCell {colwidth="[340,420]"}\n!adf:/tableCell\n')), [{ attrs: { colwidth: [340, 420] }, type: 'tableCell' }])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:tableCell {colwidth="340,420.5"}\n!adf:/tableCell\n')), [{ attrs: { colwidth: [340, 420.5] }, type: 'tableCell' }])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:tableCell {colwidth=340}\n!adf:/tableCell\n')), [{ attrs: { colwidth: [340] }, type: 'tableCell' }])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:rule {localId=a-1}\n')), [{ attrs: { localId: 'a-1' }, type: 'rule' }])
|
||||
})
|
||||
|
||||
test('reads the reserved marks key as the node array it spells', () => {
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:rule {marks="[{\\"type\\":\\"em\\"}]"}\n')), [{ marks: [em], type: 'rule' }])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:rule {localId=a-1 marks="[{\\"attrs\\":{\\"mode\\":\\"wide\\"},\\"type\\":\\"breakout\\"}]"}\n')), [
|
||||
test('reads the reserved marks key as the marks it names, in order', () => {
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:rule {marks=em}\n')), [{ marks: [em], type: 'rule' }])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:rule {localId=a-1 marks="breakout{mode=wide}"}\n')), [
|
||||
{ attrs: { localId: 'a-1' }, marks: [{ attrs: { mode: 'wide' }, type: 'breakout' }], type: 'rule' },
|
||||
])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:rule {marks="indentation{level=2} fragment{localId=f1 name=\\"Price list\\"}"}\n')), [
|
||||
{ marks: [{ attrs: { level: 2 }, type: 'indentation' }, { attrs: { localId: 'f1', name: 'Price list' }, type: 'fragment' }], type: 'rule' },
|
||||
])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:rule {marks="em{attrs=empty}"}\n')), [{ marks: [{ attrs: {}, type: 'em' }], type: 'rule' }])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:rule {marks=empty}\n')), [{ marks: [], type: 'rule' }])
|
||||
})
|
||||
|
||||
test('reads the reserved marks key as a JSON array where a mark is one the tables do not type', () => {
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:rule {marks="[{\\"type\\":\\"em\\"},{\\"attrs\\":{\\"color\\":\\"#fff\\"},\\"type\\":\\"backgroundColor\\"}]"}\n')), [
|
||||
{ marks: [em, { attrs: { color: '#fff' }, type: 'backgroundColor' }], type: 'rule' },
|
||||
])
|
||||
})
|
||||
|
||||
test('names the marks key no marks array reads back from', () => {
|
||||
const named = 'unsupported-node-shape: the marks attribute of rule is its marks array in canonical JSON: this one is not'
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="[]"}\n')), named)
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="[1]"}\n')), named)
|
||||
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'
|
||||
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)
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks=x}\n')), named)
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="[{\\"type\\":\\"em\\",\\"attrs\\":{}}]"}\n')), named)
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:rule {marks="[{\\"attrs\\":{},\\"type\\":\\"em\\"}]"}\n')), [{ marks: [{ attrs: {}, type: 'em' }], type: 'rule' }])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:rule {marks=empty}\n')), [{ marks: [], type: 'rule' }])
|
||||
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="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')
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="breakout{bogus=1}"}\n')), 'unsupported-node-shape: breakout holds no bogus attribute: this one spells it')
|
||||
})
|
||||
|
||||
test('names the marks key spelled outside the canonical form', () => {
|
||||
const spells = (spelling: string): string => `unsupported-node-shape: rule spells its marks attribute as marks=${spelling}`
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="[{\\"type\\":\\"em\\"}]"}\n')), spells('em'))
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="[{\\"attrs\\":{},\\"type\\":\\"em\\"}]"}\n')), spells('"em{attrs=empty}"'))
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="[{\\"type\\":\\"em\\",\\"attrs\\":{}}]"}\n')), spells('"em{attrs=empty}"'))
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="em{}"}\n')), spells('em'))
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="em"}\n')), spells('em'))
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:rule {marks="breakout{mode=\\"wide\\"}"}\n')), 'unsupported-node-shape: breakout spells its mode attribute as mode=wide')
|
||||
})
|
||||
|
||||
test('names the attribute a node holds no reading for', () => {
|
||||
@@ -552,7 +577,9 @@ 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 json')
|
||||
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: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}')
|
||||
})
|
||||
@@ -561,16 +588,18 @@ test('names the depth an attribute value nests past, never the kind the JSON rea
|
||||
const nested = (levels: number): string => `${'['.repeat(levels)}1${']'.repeat(levels)}`
|
||||
const deeper = (key: string, type: string): string =>
|
||||
`unsupported-nesting-depth: the ${key} attribute of ${type} nests deeper than the ${largestNesting} levels an attribute carries`
|
||||
assert.equal(content(losslessMarkdownToAdf(`!adf:tableCell {colwidth="${nested(largestNesting + 1)}"}\n!adf:/tableCell\n`)), deeper('colwidth', 'tableCell'))
|
||||
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')
|
||||
})
|
||||
|
||||
test('names the attribute value spelled outside the canonical form', () => {
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:rule {localId="a-1"}\n')), 'unsupported-node-shape: rule spells its localId attribute as localId=a-1')
|
||||
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, 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')
|
||||
})
|
||||
|
||||
test('names the argument and the body a node takes no reading for', () => {
|
||||
|
||||
Reference in New Issue
Block a user