Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 758a5724e9 | |||
| 276433d635 | |||
| 7672531cdc | |||
| ec5f80e47f | |||
| da58a59f70 | |||
| 2796487b30 | |||
| dcea401ec2 | |||
| 25d851cc67 | |||
| 0f03edc8ef |
@@ -5,10 +5,179 @@ import { dirname, join } from 'node:path'
|
|||||||
import test from 'node:test'
|
import test from 'node:test'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
import type { AttributeKind, AttributeVocabulary } from './adf/attribute-vocabulary.ts'
|
||||||
|
import { blockDirectives } from './adf/block-directives.ts'
|
||||||
|
import { inlineDirectives } from './adf/inline-directives.ts'
|
||||||
|
import { markAttributes } from './adf/mark-attributes.ts'
|
||||||
|
import { blockArgument } from './markdown/block-directive-arguments.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 definitionReference = '#/definitions/'
|
||||||
|
const gaps = ['layoutSection.columnRuleStyle', 'link.collection', 'link.id', 'link.occurrenceKey', 'rule.color', 'rule.style', 'rule.weight']
|
||||||
|
const grammarOwn = ['doc', 'text']
|
||||||
|
const readKeywords = ['$ref', 'additionalProperties', 'allOf', 'anyOf', 'enum', 'items', 'maxItems', 'maximum', 'minItems', 'minLength', 'minimum', 'pattern', 'properties', 'required', 'type']
|
||||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..', 'spec', 'adf-schema')
|
const root = join(dirname(fileURLToPath(import.meta.url)), '..', 'spec', 'adf-schema')
|
||||||
|
const schemaFiles = ['full.json', 'stage-0.json']
|
||||||
|
|
||||||
test('the ADF JSON Schemas are @atlaskit/adf-schema 57.4.9, vendored byte-exact', () => {
|
test('the ADF JSON Schemas are @atlaskit/adf-schema 57.4.9, vendored byte-exact', () => {
|
||||||
const digest = (name: string) => createHash('sha256').update(readFileSync(join(root, name))).digest('hex')
|
const digest = (name: string) => createHash('sha256').update(readFileSync(join(root, name))).digest('hex')
|
||||||
assert.equal(digest('full.json'), '75f080928a970250eb8289e9cae5374e3c2a6c0ac3ca22478acaa9d3f39484a3')
|
assert.equal(digest('full.json'), '75f080928a970250eb8289e9cae5374e3c2a6c0ac3ca22478acaa9d3f39484a3')
|
||||||
assert.equal(digest('stage-0.json'), '56747e5a71c0d5f8c58f94180d69e4482f62c08020d66aaf327333681fcc1c8f')
|
assert.equal(digest('stage-0.json'), '56747e5a71c0d5f8c58f94180d69e4482f62c08020d66aaf327333681fcc1c8f')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('the tables spell the attribute names and kinds the ADF JSON Schemas give each type they spell, the pinned gaps apart', () => {
|
||||||
|
const held = schemaTypes()
|
||||||
|
const spelledTypes = spelled()
|
||||||
|
const found: string[] = []
|
||||||
|
const gapsHeld = new Set<string>()
|
||||||
|
for (const [type, spelledKinds] of spelledTypes) {
|
||||||
|
const kinds = held.get(type)
|
||||||
|
if (kinds === undefined) {
|
||||||
|
found.push(`${type}: the tables spell the type, the schema holds no definition of it`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (const [attribute, schemaKinds] of kinds) {
|
||||||
|
const kind = spelledKinds.get(attribute)
|
||||||
|
const holds = [...schemaKinds].sort().join(' or ')
|
||||||
|
if (kind === undefined && gaps.includes(`${type}.${attribute}`)) gapsHeld.add(`${type}.${attribute}`)
|
||||||
|
else if (kind === undefined) found.push(`${type}.${attribute}: the schema holds ${holds}, the tables spell nothing and the gaps list does not name it`)
|
||||||
|
else if (schemaKinds.size !== 1 || !schemaKinds.has(kind)) found.push(`${type}.${attribute}: the tables spell ${kind}, the schema holds ${holds}`)
|
||||||
|
}
|
||||||
|
for (const [attribute, kind] of spelledKinds) if (!kinds.has(attribute)) found.push(`${type}.${attribute}: the tables spell ${kind}, the schema holds nothing`)
|
||||||
|
}
|
||||||
|
for (const gap of gaps) {
|
||||||
|
if (gapsHeld.has(gap)) continue
|
||||||
|
const [type = '', attribute = ''] = gap.split('.')
|
||||||
|
const spelledKinds = spelledTypes.find(([name]) => name === type)?.[1]
|
||||||
|
const kind = spelledKinds?.get(attribute)
|
||||||
|
if (spelledKinds === undefined) found.push(`${gap}: the gaps list names it, the tables spell no ${type} type`)
|
||||||
|
else if (kind === undefined) found.push(`${gap}: the gaps list names it, the schema holds nothing`)
|
||||||
|
else found.push(`${gap}: the gaps list names it, the tables spell ${kind}`)
|
||||||
|
}
|
||||||
|
assert.deepEqual(found, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("the ADF JSON Schemas hold no type the tables leave unspelled, the pinned carried ones and the grammar's own apart", () => {
|
||||||
|
const held = schemaTypes()
|
||||||
|
const spelledNames = new Set(spelled().map(([type]) => type))
|
||||||
|
const found: string[] = []
|
||||||
|
for (const type of held.keys()) {
|
||||||
|
if (!spelledNames.has(type) && !carried.includes(type) && !grammarOwn.includes(type)) found.push(`${type}: the schema holds the type, the tables spell none of it and the carried list does not name it`)
|
||||||
|
}
|
||||||
|
for (const type of carried) {
|
||||||
|
if (!held.has(type)) found.push(`${type}: the carried list names the type, the schema holds no definition of it`)
|
||||||
|
if (spelledNames.has(type)) found.push(`${type}: the carried list names the type, and the tables spell it`)
|
||||||
|
}
|
||||||
|
assert.deepEqual(found, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
function spelled(): Spelled[] {
|
||||||
|
return [
|
||||||
|
...Object.entries(blockDirectives).map(([type, entry]) => spelledType(type, entry.attributes, blockArgument(type))),
|
||||||
|
...Object.entries(inlineDirectives).map(([type, entry]) => spelledType(type, entry.attributes)),
|
||||||
|
...Object.entries(markAttributes).map(([type, attributes]) => spelledType(type, attributes)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function spelledType(type: string, attributes: AttributeVocabulary, argument?: string): Spelled {
|
||||||
|
const kinds = new Map(Object.entries(attributes))
|
||||||
|
if (argument !== undefined) kinds.set(argument, 'string')
|
||||||
|
return [type, kinds]
|
||||||
|
}
|
||||||
|
|
||||||
|
function schemaTypes(): Map<string, Held> {
|
||||||
|
const held = new Map<string, Held>()
|
||||||
|
for (const file of schemaFiles) {
|
||||||
|
const definitions = schemaObject(schemaObject(JSON.parse(readFileSync(join(root, file), 'utf8')), file)['definitions'], `${file} definitions`)
|
||||||
|
for (const [name, definition] of Object.entries(definitions)) {
|
||||||
|
const where = `${file} ${name}`
|
||||||
|
for (const properties of alternatives(schemaObject(definition, where), definitions, where)) {
|
||||||
|
const attributes = (properties.get('attrs') ?? []).flatMap((attrs) => alternatives(attrs, definitions, `${where} attrs`)).flatMap((alternative) => [...alternative])
|
||||||
|
for (const type of (properties.get('type') ?? []).flatMap((schema) => enumStrings(schema, `${where} type`))) {
|
||||||
|
const kinds = held.get(type) ?? new Map<string, Set<AttributeKind>>()
|
||||||
|
held.set(type, kinds)
|
||||||
|
for (const [attribute, schemas] of attributes) {
|
||||||
|
const attributeKinds = kinds.get(attribute) ?? new Set<AttributeKind>()
|
||||||
|
kinds.set(attribute, attributeKinds)
|
||||||
|
for (const schema of schemas) for (const kind of propertyKinds(schema, `${where} ${type}.${attribute}`)) attributeKinds.add(kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return held
|
||||||
|
}
|
||||||
|
|
||||||
|
function alternatives(schema: SchemaObject, definitions: SchemaObject, where: string): Properties[] {
|
||||||
|
const own = Object.entries(schemaObject(readSchema(schema, where)['properties'] ?? {}, `${where} properties`))
|
||||||
|
let found: Properties[] = [new Map(own.map(([name, property]): [string, SchemaObject[]] => [name, [readSchema(schemaObject(property, `${where} ${name}`), `${where} ${name}`)]]))]
|
||||||
|
if (schema['$ref'] !== undefined) found = intersect(found, alternatives(referenced(schema['$ref'], definitions, where), definitions, `${where} ${String(schema['$ref'])}`))
|
||||||
|
for (const branch of branches(schema['allOf'], `${where} allOf`)) found = intersect(found, alternatives(branch, definitions, `${where} allOf`))
|
||||||
|
if (schema['anyOf'] !== undefined) found = intersect(found, branches(schema['anyOf'], `${where} anyOf`).flatMap((branch) => alternatives(branch, definitions, `${where} anyOf`)))
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSchema(schema: SchemaObject, where: string): SchemaObject {
|
||||||
|
const unread = Object.keys(schema).find((keyword) => !readKeywords.includes(keyword))
|
||||||
|
if (unread !== undefined) return assert.fail(`${where}: the schema holds the keyword ${unread}, which the gate does not read`)
|
||||||
|
const extra = schema['additionalProperties']
|
||||||
|
return extra === undefined || typeof extra === 'boolean' ? schema : assert.fail(`${where}: additionalProperties holds a schema, which the gate does not read`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function intersect(left: readonly Properties[], right: readonly Properties[]): Properties[] {
|
||||||
|
return left.flatMap((own) =>
|
||||||
|
right.map((other) => {
|
||||||
|
const merged = new Map(own)
|
||||||
|
for (const [name, schemas] of other) merged.set(name, [...(merged.get(name) ?? []), ...schemas])
|
||||||
|
return merged
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function referenced(reference: unknown, definitions: SchemaObject, where: string): SchemaObject {
|
||||||
|
const name = typeof reference === 'string' && reference.startsWith(definitionReference) ? reference.slice(definitionReference.length) : undefined
|
||||||
|
return schemaObject(name === undefined ? undefined : definitions[name], `${where}: the reference ${String(reference)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function branches(value: unknown, where: string): SchemaObject[] {
|
||||||
|
if (value === undefined) return []
|
||||||
|
return Array.isArray(value) ? value.map((branch, index) => schemaObject(branch, `${where} ${index}`)) : assert.fail(`${where} is no array of schemas`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function enumStrings(schema: SchemaObject, where: string): string[] {
|
||||||
|
const values = schema['enum']
|
||||||
|
return Array.isArray(values) ? values.filter((value: unknown): value is string => typeof value === 'string') : assert.fail(`${where}: the type property holds no enum naming the type`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function propertyKinds(property: SchemaObject, where: string): AttributeKind[] {
|
||||||
|
const type = property['type']
|
||||||
|
if (type === 'boolean') return ['boolean']
|
||||||
|
if (type === 'integer' || type === 'number') return ['number']
|
||||||
|
if (type === 'string') return ['string']
|
||||||
|
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)
|
||||||
|
if (combinator !== undefined) return assert.fail(`${where}: the schema holds the keyword ${combinator}, which the gate reads as no attribute kind`)
|
||||||
|
const values = property['enum']
|
||||||
|
return Array.isArray(values) ? values.map(valueKind) : ['json']
|
||||||
|
}
|
||||||
|
|
||||||
|
function valueKind(value: unknown): AttributeKind {
|
||||||
|
if (typeof value === 'boolean') return 'boolean'
|
||||||
|
if (typeof value === 'number') return 'number'
|
||||||
|
if (typeof value === 'string') return 'string'
|
||||||
|
return 'json'
|
||||||
|
}
|
||||||
|
|
||||||
|
function schemaObject(value: unknown, where: string): SchemaObject {
|
||||||
|
return isSchemaObject(value) ? value : assert.fail(`${where} is no JSON Schema object`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSchemaObject(value: unknown): value is SchemaObject {
|
||||||
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||||
|
}
|
||||||
|
|||||||
+17
-1
@@ -562,11 +562,27 @@ The done `todo.md` items in full, as they were written. `todo.md` keeps a one-li
|
|||||||
`instrumentisto/geckodriver`, currency over size — the leg's whole worth is a real
|
`instrumentisto/geckodriver`, currency over size — the leg's whole worth is a real
|
||||||
SpiderMonkey, which decays the moment the pin stops moving, and the smaller image was four
|
SpiderMonkey, which decays the moment the pin stops moving, and the smaller image was four
|
||||||
Firefox majors behind with a publisher that may go quiet while Renovate stays silent.
|
Firefox majors behind with a publisher that may go quiet while Renovate stays silent.
|
||||||
- [ ] **11 — Atlassian's ADF schema as the tables' truth (`0.2.0`).**
|
- [x] **11 — Atlassian's ADF schema as the tables' truth (`0.2.0`).** `@atlaskit/adf-schema`'s two
|
||||||
|
JSON Schemas vendored rather than the package installed (AGENTS.md §5), and the node tables
|
||||||
|
gated against them (§10). **Settled** (the maintainer, 2026-09-13): vendored at
|
||||||
|
`spec/adf-schema/` and re-pinned by hand when a need shows; the gate compares attribute names
|
||||||
|
and kinds, never value sets, over `full.json` and `stage-0.json` together.
|
||||||
- [x] **11a — The vendored schema.** `full.json` and `stage-0.json`, byte-exact from
|
- [x] **11a — The vendored schema.** `full.json` and `stage-0.json`, byte-exact from
|
||||||
`@atlaskit/adf-schema@57.4.9`'s `dist/json-schema/v1/`, at `spec/adf-schema/`, each pinned
|
`@atlaskit/adf-schema@57.4.9`'s `dist/json-schema/v1/`, at `spec/adf-schema/`, each pinned
|
||||||
by its SHA-256 in a test the way `spec.json` is. The version, the source and the Apache-2.0
|
by its SHA-256 in a test the way `spec.json` is. The version, the source and the Apache-2.0
|
||||||
attribution sit beside them with the licence text; no gate re-serializes either file.
|
attribution sit beside them with the licence text; no gate re-serializes either file.
|
||||||
|
- [x] **11b — The gate.** For each node and mark type the tables spell, the attribute names and
|
||||||
|
kinds equal the union over every definition in both files whose `type` enum names it,
|
||||||
|
`anyOf`/`allOf` branches included, the argument slot (`panelType`, `state`) counting as
|
||||||
|
spelled. Kinds: `string`; `number`, `integer` included; `boolean`; `json` for an object, an
|
||||||
|
array or an untyped value; an `enum`-only attribute takes its values' kind. What the schema
|
||||||
|
holds past the tables is pinned in two exact lists — an entry the schema no longer needs is
|
||||||
|
red, like a difference neither list names: gaps, attributes of a spelled type (57.4.9:
|
||||||
|
`link` `collection` `id` `occurrenceKey`, `rule` `color` `style` `weight`, `layoutSection`
|
||||||
|
`columnRuleStyle`), emptied by 13; and carried, types the tables do not spell (`alignment`
|
||||||
|
`annotation` `backgroundColor` `blockCard` `bodiedRule` `breakout` `dataConsumer`
|
||||||
|
`embedCard` `fontSize` `fragment` `indentation` `inlineExtension` `placeholder`), `doc` and
|
||||||
|
`text` counting as the grammar's own.
|
||||||
|
|
||||||
## 5 — Ship `0.1.0`
|
## 5 — Ship `0.1.0`
|
||||||
|
|
||||||
|
|||||||
@@ -55,11 +55,15 @@ proves 12, 13 spells 11's gaps in 12's grammar, and 12 rewrites code 4b and 4c c
|
|||||||
collision already fails the round-trip on the same fixtures; the fixture-duplicate test
|
collision already fails the round-trip on the same fixtures; the fixture-duplicate test
|
||||||
stays.
|
stays.
|
||||||
- [ ] **4.1 — Editor-normal and the node accessors.** `toEditorNormal(doc)` in
|
- [ ] **4.1 — Editor-normal and the node accessors.** `toEditorNormal(doc)` in
|
||||||
`src/adf/editor-normal.ts`, on 3i's merging: adjacent text nodes carrying identical marks
|
`src/adf/editor-normal.ts`, on 3i's merging: adjacent text nodes carrying identical marks and
|
||||||
merged, an empty `attrs`, `marks` or `content` the absent key (§2), and the round-trip
|
no attributes merged, an empty `attrs`, `marks` or `content` the absent key, `-0` read as `0`
|
||||||
tests compare through it. `nodeContent`/`nodeAttrs`/`nodeMarks` replace the 49 inline
|
(§2); the round-trip tests compare the parser's output through it, and `serializeCanonicalJson`
|
||||||
`?? []`/`?? {}` reads in `src/` (27 `content`, 12 `marks`, 10 `attrs`), and the branch floor
|
beneath it walks iteratively. `nodeContent`/`nodeAttrs`/`nodeMarks` replace the 46 inline
|
||||||
rises to what the suite then measures.
|
`?? []`/`?? {}` reads in `src/` (23 `content`, 12 `marks`, 11 `attrs`) and the `attrs?.[key]`
|
||||||
|
reads, and the branch floor rises to the integer floor of what the suite then measures.
|
||||||
|
**Settled** (the maintainer, 2026-09-14): a text node carrying attributes never merges —
|
||||||
|
`0.1.0` merged a carried one into its neighbour on read-back — and the fix lands here, as does
|
||||||
|
the iterative serializer.
|
||||||
- [ ] **4.2 — The ADF property.** `fast-check` joins `devDependencies`, AGENTS.md §5 naming what
|
- [ ] **4.2 — The ADF property.** `fast-check` joins `devDependencies`, AGENTS.md §5 naming what
|
||||||
it earns — shrinking a failing document to the nodes that break it — and §10 the properties
|
it earns — shrinking a failing document to the nodes that break it — and §10 the properties
|
||||||
beside the corpus. A generated editor-normal document either refuses in `adfToMarkdown`
|
beside the corpus. A generated editor-normal document either refuses in `adfToMarkdown`
|
||||||
@@ -163,66 +167,73 @@ proves 12, 13 spells 11's gaps in 12's grammar, and 12 rewrites code 4b and 4c c
|
|||||||
README's tagline and `package.json`'s `description` regain HTML (5g).
|
README's tagline and `package.json`'s `description` regain HTML (5g).
|
||||||
- [ ] **8 — CLI.** A later goal, shaped around the personas once the library exists.
|
- [ ] **8 — CLI.** A later goal, shaped around the personas once the library exists.
|
||||||
- [ ] **9 — The online sandbox.** A web page with two textboxes converting back and forth between ADF and markdown, powered by the library's browser build.
|
- [ ] **9 — The online sandbox.** A web page with two textboxes converting back and forth between ADF and markdown, powered by the library's browser build.
|
||||||
- [ ] **10 — Lossy conversion (`0.2.0`).** A direction that only converts what Markdown actually
|
- [ ] **10 — Lossy conversion (`0.2.0`).** Markdown other tools render readably, to and from ADF,
|
||||||
supports, keeping the ADF's data while dropping what markdown cannot hold — format, design
|
keeping the content while dropping what markdown cannot hold — format, design and the richer
|
||||||
and the richer nodes.
|
nodes.
|
||||||
**Settled** (the maintainer, 2026-09-13): `adfToPlainMarkdown(doc)` is one export whose body
|
**Settled** (the maintainer, 2026-09-14): two exports composed around the lossless pair, so §1's
|
||||||
reduces the document ADF→ADF in `src/adf/` and hands the result to `adfToMarkdown`, so §1's
|
four conversions stay four. `adfToPlainMarkdown(doc)` reduces the document ADF→ADF and hands it
|
||||||
four conversions stay four. Its markdown is the flavour without directives — CommonMark, the
|
to `adfToMarkdown`; `plainMarkdownToAdf(markdown)` hands the markdown to `markdownToAdf` and
|
||||||
pipe table and `~~` — and it refuses only what the document guard refuses
|
lifts the result ADF→ADF. Both carry markdown conventions, so the reduction sits in
|
||||||
(`not-an-adf-document`, `unsupported-document-version`, `unsupported-nesting-depth`); every
|
`src/markdown/emit/`, the lift in `src/markdown/parse/` and what both read in `src/markdown/`
|
||||||
other shape degrades. The reduction:
|
(§11). The markdown is the flavour without directives — CommonMark, the pipe table and `~~` —
|
||||||
- `panel`, `layoutSection`/`layoutColumn`, `bodiedExtension`, `bodiedSyncBlock`,
|
plus the conventions below, chosen for readability from a survey of GitHub, GitLab, Gitea,
|
||||||
`multiBodiedExtension` and `extensionFrame` unwrap to their body blocks in order; `expand`
|
Obsidian, Pandoc, MkDocs, Docusaurus, Typora, Joplin, Logseq, Bear, Notion, Azure DevOps and
|
||||||
and `nestedExpand` put their title first as a strong paragraph.
|
Discord, GitHub's renderer confirming each shape. Writing refuses only what the document guard
|
||||||
- The CommonMark blocks keep their spelling, attributes dropped.
|
refuses (`not-an-adf-document`, `unsupported-document-version`, `unsupported-nesting-depth`)
|
||||||
- `taskList` and `decisionList` become bullet lists, a task item's state leading its text as
|
and degrades every other shape; reading refuses what `markdownToAdf` refuses. A lifted node
|
||||||
`[x]` or `[ ]`, the way Obsidian and GFM write a checkbox: `- [x] Write the spec`.
|
carries no `localId`. The lift also reads other tools' spellings — type words in any case,
|
||||||
- `mention` and `status` become their text, `emoji` its text or else its `shortName`, and
|
Obsidian's aliases, `[X]` — since it reads their output and never writes those spellings.
|
||||||
`date` its ISO date in UTC (`2026-09-13`).
|
- A `panel` is an alert: the marker alone on the quote's first line, a blank `>`, then the body
|
||||||
- `inlineCard`, `blockCard` and `embedCard` become a link to their `url`, dropped when they
|
(`> [!WARNING]`), in GitHub's five words by colour — info `NOTE`, note `IMPORTANT`, tip and
|
||||||
carry only `data`; a `mediaSingle` holding an external image stays ``; `media`,
|
success `TIP`, warning `WARNING`, error `CAUTION`, custom `NOTE`. The lift reads those words
|
||||||
`mediaGroup` and `mediaInline` become their `alt` text or nothing; `caption` its text as a
|
back (`NOTE` info, `IMPORTANT` note, `TIP` tip, `WARNING` warning, `CAUTION` error) and
|
||||||
paragraph; `extension`, `inlineExtension` and `syncBlock` their `text` attribute or nothing;
|
Obsidian's by meaning (hint tip; success, check and done success; attention warning; danger,
|
||||||
`placeholder` nothing; a node no row names, or one standing where no spelling holds it, its
|
failure, fail, missing and bug error; any other word info). Text after a marker in its
|
||||||
blocks or its text.
|
paragraph is the panel's first body paragraph.
|
||||||
- A table stays a pipe table: the first row becomes the header, a cell's blocks join on one
|
- An `expand` or `nestedExpand` is Obsidian's folded callout, `> [!NOTE]- Title`, a blank `>`,
|
||||||
line with spaces, and spans and the cells they cover drop.
|
then the body. The lift reads a fold sign (`-` or `+`) as an expand whatever the word, the
|
||||||
- `code`, `em`, `link`, `strike` and `strong` stay and every other mark drops, keeping its text;
|
rest of the marker's paragraph as its title, and an expand inside an expand as a
|
||||||
a link no CommonMark escape writes becomes its text, and a mark run CommonMark's flanking or
|
`nestedExpand`.
|
||||||
matching cannot spell drops its mark.
|
- A `taskList` is a bullet list whose items lead with `[x]` or `[ ]` (`- [x] Write the spec`).
|
||||||
|
The lift reads a list whose every item is so marked back as a `taskList` — a `blockTaskItem`
|
||||||
|
where an item holds more than one block, a nested task list moved beside its item — and
|
||||||
|
leaves mixed and ordered lists plain. A `decisionList` is a plain bullet list.
|
||||||
|
- `backgroundColor` is `==text==`, and the lift gives `==text==` the Atlassian editor's default
|
||||||
|
highlight colour.
|
||||||
|
- `layoutSection`/`layoutColumn`, `bodiedExtension`, `bodiedSyncBlock`, `multiBodiedExtension`
|
||||||
|
and `extensionFrame` unwrap to their body blocks in order; the CommonMark blocks keep their
|
||||||
|
spelling, attributes dropped.
|
||||||
|
- `mention` and `status` become their text, the mention's `@` kept; `emoji` its text or else its
|
||||||
|
`shortName`; `date` its ISO date in UTC (`2026-09-13`); `inlineCard`, `blockCard` and
|
||||||
|
`embedCard` a link to their `url`, dropped when they carry only `data`; a `mediaSingle`
|
||||||
|
holding an external image stays ``; `media`, `mediaGroup` and `mediaInline` their
|
||||||
|
`alt` text or nothing; `caption` its text as a paragraph; `extension`, `inlineExtension` and
|
||||||
|
`syncBlock` their `text` attribute or nothing; `placeholder` nothing; a node no row names, or
|
||||||
|
one standing where no spelling holds it, its blocks or its text.
|
||||||
|
- A table stays a pipe table: the first row becomes the header, a cell's blocks join on one line
|
||||||
|
with spaces, and spans and the cells they cover drop.
|
||||||
|
- `code`, `em`, `link`, `strike` and `strong` stay and every other mark drops, keeping its text —
|
||||||
|
`subsup` too, since `~2~` is a strike on GitHub; a link no CommonMark escape writes becomes its
|
||||||
|
text, and a mark run CommonMark's flanking or matching cannot spell drops its mark.
|
||||||
- A newline in text becomes a hard break and edge whitespace is trimmed; carriage returns and
|
- A newline in text becomes a hard break and edge whitespace is trimmed; carriage returns and
|
||||||
null characters are removed; a paragraph line opening with a code span whose backticks would
|
null characters are removed; a paragraph line opening with a code span whose backticks would
|
||||||
read as a fence loses the code mark; an empty paragraph drops, and adjacent lists of one type
|
read as a fence loses the code mark; an empty paragraph drops, and adjacent lists of one type
|
||||||
merge.
|
merge.
|
||||||
- [ ] **10a — Obsidian's formats.** Look up the formats Obsidian-flavoured markdown adds —
|
- Rejected in the survey: `~sub~` and `^sup^`, underline and colour spellings, raw HTML
|
||||||
callouts, highlights, embeds, task states and whatever else it writes — and propose which
|
(`<details>`, `<mark>`), MkDocs `!!!` and the `:::` admonition family, footnotes, definition
|
||||||
of the reduction's rows should adopt one; the maintainer settles the proposal, revising the
|
lists, wikilinks, embeds, tags, comments, TOC tokens, spoilers, task states past `[x]`/`[ ]`,
|
||||||
rows above, before 10b starts (the maintainer's request, 2026-09-13).
|
and lifting bare URLs, `@name`, `:shortcode:` or ISO dates into nodes.
|
||||||
- [ ] **10b — The reduction.** The reduction in `src/adf/`, tests first, a test per row as 10a
|
- [ ] **10a — The reduction.** `adfToPlainMarkdown`'s ADF→ADF reduction, tests first, a test per
|
||||||
leaves them.
|
row above.
|
||||||
- [ ] **10c — `adfToPlainMarkdown`.** The export and its README section, and a property over
|
- [ ] **10b — The lift.** `plainMarkdownToAdf`'s ADF→ADF lift, tests first, a test per row it reads,
|
||||||
4.2's generators: it refuses only the guard's codes, and its output reads back through
|
other tools' spellings included; the editor's default highlight colour looked up and cited.
|
||||||
`markdownToAdf` holding no node or mark the flavour spells as a directive. AGENTS.md §1
|
- [ ] **10c — The exports.** `adfToPlainMarkdown` and `plainMarkdownToAdf` exported with their README
|
||||||
records the reduction as what keeps the conversions at four.
|
sections, and two properties over 4.2's generators: writing refuses only the guard's codes,
|
||||||
- [ ] **11 — Atlassian's ADF schema as the tables' truth (`0.2.0`).** `@atlaskit/adf-schema`'s two
|
and markdown `adfToPlainMarkdown` wrote reads back through `plainMarkdownToAdf` and writes
|
||||||
JSON Schemas vendored rather than the package installed (AGENTS.md §5), and the node tables
|
again byte for byte. AGENTS.md §1 records the pair as composed around the lossless one.
|
||||||
gated against them (§10). **Settled** (the maintainer, 2026-09-13): vendored at
|
- [x] **11 — Atlassian's ADF schema as the tables' truth.**
|
||||||
`spec/adf-schema/` and re-pinned by hand when a need shows; the gate compares attribute names
|
|
||||||
and kinds, never value sets, over `full.json` and `stage-0.json` together.
|
|
||||||
- [x] **11a — The vendored schema.**
|
- [x] **11a — The vendored schema.**
|
||||||
- [ ] **11b — The gate.** For each node and mark type the tables spell, the attribute names and
|
- [x] **11b — The gate.**
|
||||||
kinds equal the union over every definition in both files whose `type` enum names it,
|
|
||||||
`anyOf`/`allOf` branches included, the argument slot (`panelType`, `state`) counting as
|
|
||||||
spelled. Kinds: `string`; `number`, `integer` included; `boolean`; `json` for an object, an
|
|
||||||
array or an untyped value; an `enum`-only attribute takes its values' kind. What the schema
|
|
||||||
holds past the tables is pinned in two exact lists — an entry the schema no longer needs is
|
|
||||||
red, like a difference neither list names: gaps, attributes of a spelled type (57.4.9:
|
|
||||||
`link` `collection` `id` `occurrenceKey`, `rule` `color` `style` `weight`, `layoutSection`
|
|
||||||
`columnRuleStyle`), emptied by 13; and carried, types the tables do not spell (`alignment`
|
|
||||||
`annotation` `backgroundColor` `blockCard` `bodiedRule` `breakout` `dataConsumer`
|
|
||||||
`embedCard` `fontSize` `fragment` `indentation` `inlineExtension` `placeholder`), `doc` and
|
|
||||||
`text` counting as the grammar's own.
|
|
||||||
- [ ] **12 — The `!adf:` re-spelling (`0.2.0`).** Replace the colon directive grammar with the
|
- [ ] **12 — The `!adf:` re-spelling (`0.2.0`).** Replace the colon directive grammar with the
|
||||||
namespaced prefix, a breaking change to the emitted contract (shipped `0.1.0`, so §8 makes it
|
namespaced prefix, a breaking change to the emitted contract (shipped `0.1.0`, so §8 makes it
|
||||||
`0.2.0`). Forms: block container `!adf:name arg {attrs}` … `!adf:/name` — the `/` parts open
|
`0.2.0`). Forms: block container `!adf:name arg {attrs}` … `!adf:/name` — the `/` parts open
|
||||||
@@ -270,7 +281,9 @@ proves 12, 13 spells 11's gaps in 12's grammar, and 12 rewrites code 4b and 4c c
|
|||||||
- [ ] **12b — The inline form.** Inline nodes, directive marks, `text` and the inline carry
|
- [ ] **12b — The inline form.** Inline nodes, directive marks, `text` and the inline carry
|
||||||
`!adf:carry{json=…}` spelled and read as `!adf:name[content]{attrs}`, with the prefix claim
|
`!adf:carry{json=…}` spelled and read as `!adf:name[content]{attrs}`, with the prefix claim
|
||||||
and its escape; the round-trip, normalization and `errors/` fixtures holding inline forms
|
and its escape; the round-trip, normalization and `errors/` fixtures holding inline forms
|
||||||
re-spelled, and the gate green.
|
re-spelled, and the gate green. The content slot of `emoji`, `mention` and `status` refuses a
|
||||||
|
text node carrying attributes as `unsupported-node-shape`, which it drops silently today (the
|
||||||
|
maintainer, 2026-09-14).
|
||||||
- [ ] **12c — The block form.** Openers and `!adf:/name` closers, leaf vs container by content
|
- [ ] **12c — The block form.** Openers and `!adf:/name` closers, leaf vs container by content
|
||||||
model, empty pairs, `listBreak` and the `carry` fence, spelled and read; the fence-length
|
model, empty pairs, `listBreak` and the `carry` fence, spelled and read; the fence-length
|
||||||
rule and the corpus test's fence nesting check deleted; the remaining fixtures re-spelled
|
rule and the corpus test's fence nesting check deleted; the remaining fixtures re-spelled
|
||||||
|
|||||||
Reference in New Issue
Block a user