Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da58a59f70 | |||
| 2796487b30 | |||
| dcea401ec2 | |||
| 25d851cc67 | |||
| 0f03edc8ef | |||
| 37f59cec40 | |||
| aa34d6e7fd | |||
| 3c1bacace6 | |||
| f8a5d5e9b3 | |||
| c9fb85f9db | |||
| 24b2e8cbbe | |||
| f66efabcbb | |||
| e424b35fd6 |
@@ -63,8 +63,11 @@ module, so entity decoding is complete without one. The CommonMark spec suite is
|
|||||||
data and ships vendored at `corpus/commonmark-spec/` rather than as the `commonmark-spec` dev
|
data and ships vendored at `corpus/commonmark-spec/` rather than as the `commonmark-spec` dev
|
||||||
dependency — that package is CommonJS-only, and Renovate auto-bumping a spec version would silently
|
dependency — that package is CommonJS-only, and Renovate auto-bumping a spec version would silently
|
||||||
point the vendored exception list's example numbers at a renumbered suite. A spec bump is a
|
point the vendored exception list's example numbers at a renumbered suite. A spec bump is a
|
||||||
deliberate re-pin, exceptions re-derived by hand beside it. `devDependencies`: few, each earning its
|
deliberate re-pin, exceptions re-derived by hand beside it. Atlassian's ADF JSON Schemas ship
|
||||||
keep; they never reach a consumer.
|
vendored the same way, at `spec/adf-schema/`, rather than as the `@atlaskit/adf-schema` dev
|
||||||
|
dependency — CommonJS-only, some fifty packages with React among them, and a release most days for
|
||||||
|
Renovate to automerge — re-pinned by hand when a payload or a report shows the need.
|
||||||
|
`devDependencies`: few, each earning its keep; they never reach a consumer.
|
||||||
|
|
||||||
## 6. The package contract
|
## 6. The package contract
|
||||||
|
|
||||||
@@ -220,8 +223,7 @@ functions, and a branch floor that only ever moves upward. It sits below 100 bec
|
|||||||
compared against `undefined` — have a half no valid document reaches.
|
compared against `undefined` — have a half no valid document reaches.
|
||||||
|
|
||||||
The corpus, all checked in: hand-built fixtures per node and combination; real sanitized ADF from
|
The corpus, all checked in: hand-built fixtures per node and combination; real sanitized ADF from
|
||||||
live Atlassian APIs; property-generated ADF trees; the CommonMark spec suite against
|
live Atlassian APIs; the CommonMark spec suite against `markdownToAdf` and `markdownToHtml`.
|
||||||
`markdownToAdf` and `markdownToHtml`.
|
|
||||||
|
|
||||||
`spec/flavour.md` is read as a source too, so the node tables cannot drift from the prose they
|
`spec/flavour.md` is read as a source too, so the node tables cannot drift from the prose they
|
||||||
copy: each `- ` bullet in `## Block nodes`, `## Inline nodes` and `## Marks` declares the nodes
|
copy: each `- ` bullet in `## Block nodes`, `## Inline nodes` and `## Marks` declares the nodes
|
||||||
@@ -231,6 +233,12 @@ of a bullet; fenced examples are skipped. It guards the attributes alone: nodes
|
|||||||
content model share a bullet, and the argument attribute is spelled ahead of `Attributes: `, so
|
content model share a bullet, and the argument attribute is spelled ahead of `Attributes: `, so
|
||||||
both answer to the round-trip corpus and to nothing else where a node has no fixture.
|
both answer to the round-trip corpus and to nothing else where a node has no fixture.
|
||||||
|
|
||||||
|
The tables answer to Atlassian's schema too (§5): for every node and mark they spell, the attribute
|
||||||
|
names and kinds equal what `full.json` and `stage-0.json` hold between them. Value sets stay
|
||||||
|
documentation, since any value round-trips. What the schema holds and the tables do not spell is
|
||||||
|
pinned by name — an attribute as a gap, a type as carried — so a re-pin adding either goes red until
|
||||||
|
someone spells it or pins it.
|
||||||
|
|
||||||
## 11. Code rules
|
## 11. Code rules
|
||||||
|
|
||||||
- Two-space indent, strict TypeScript, English everywhere. Alphabetical order wherever order
|
- Two-space indent, strict TypeScript, English everywhere. Alphabetical order wherever order
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
Copyright 2019 Atlassian Pty Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Atlassian's ADF JSON Schemas
|
||||||
|
|
||||||
|
`full.json` and `stage-0.json` are byte-exact from `dist/json-schema/v1/` in
|
||||||
|
[`@atlaskit/adf-schema` 57.4.9](https://registry.npmjs.org/@atlaskit/adf-schema/-/adf-schema-57.4.9.tgz)
|
||||||
|
(© Atlassian Pty Ltd, Apache-2.0: `LICENSE` is the package's own notice, `LICENSE-2.0.txt` the
|
||||||
|
licence it names).
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,183 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { createHash } from 'node:crypto'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { dirname, join } from 'node:path'
|
||||||
|
import test from 'node:test'
|
||||||
|
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 schemaFiles = ['full.json', 'stage-0.json']
|
||||||
|
|
||||||
|
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')
|
||||||
|
assert.equal(digest('full.json'), '75f080928a970250eb8289e9cae5374e3c2a6c0ac3ca22478acaa9d3f39484a3')
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -562,6 +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.
|
||||||
|
- [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
|
||||||
|
`@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
|
||||||
|
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`
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ milestone. A done item shrinks to its title here; its full text moves to `todo-h
|
|||||||
|
|
||||||
## Milestones
|
## Milestones
|
||||||
|
|
||||||
Shipping order: 3h, 3i, 3j, 5a, 5b, 5c, 5d, 5 → `0.1.0` (shipped 2026-09-05); 5e before 2027-01; 3k, 4, 4b, 4c, 5g, 10, 11, 12 → `0.2.0`; 4d, 5f → `0.2.1`;
|
Shipping order: 3h, 3i, 3j, 5a, 5b, 5c, 5d, 5 → `0.1.0` (shipped 2026-09-05); 3k, 11, 4, 12, 13, 4b, 4c, 10, 5g → `0.2.0`; 4d, 5f → `0.2.1`;
|
||||||
6, 7 → `0.3.0`; 9 → TBD.
|
6, 7 → `0.3.0`; 9 → TBD; 5e last.
|
||||||
The numbering is the order the work was planned in, not the order it ships.
|
The numbering is the order the work was planned in, not the order it ships. `0.2.0`'s order is settled
|
||||||
|
(the maintainer, 2026-09-13): 11 makes the tables 4 generates from answer to Atlassian's schema, 4
|
||||||
|
proves 12, 13 spells 11's gaps in 12's grammar, and 12 rewrites code 4b and 4c change.
|
||||||
|
|
||||||
- [x] **0 — Scaffold.**
|
- [x] **0 — Scaffold.**
|
||||||
- [x] **1a — The directive grammar.**
|
- [x] **1a — The directive grammar.**
|
||||||
@@ -42,19 +44,37 @@ The numbering is the order the work was planned in, not the order it ships.
|
|||||||
- [x] **3j — The carry and the combinations.**
|
- [x] **3j — The carry and the combinations.**
|
||||||
- [x] **3k — The CommonMark spec suite.**
|
- [x] **3k — The CommonMark spec suite.**
|
||||||
- [ ] **4 — Round-trip property tests (`0.2.0`)**, widening 3j's corpus round-trip past the
|
- [ ] **4 — Round-trip property tests (`0.2.0`)**, widening 3j's corpus round-trip past the
|
||||||
documents a human wrote — the thing that proves 2 and 3 beyond them. Editor-normal (§2) is
|
documents a human wrote — the thing that proves 2 and 3 beyond them.
|
||||||
finished here, on 3i's merging — `toEditorNormal(doc)` and the equality the round-trip
|
**Settled** (the maintainer, 2026-09-13): `fast-check` generates and shrinks. The gate runs a
|
||||||
asserts, which over normalized input is the canonical serializer's compact spelling —
|
fixed seed, the properties together adding about five seconds per engine; an environment
|
||||||
rather than staying spelled inline as `?? []` at every reader. The
|
variable raises the runs and randomizes the seed for local digging, and a counterexample
|
||||||
reading half is `nodeContent`/`nodeAttrs`/`nodeMarks` over the ~28 sites spelling it
|
found becomes a round-trip fixture. The generators draw from the node tables — each node's
|
||||||
inline today, which also lifts the branch floor §10 keeps below 100 for exactly those
|
content model and attribute vocabulary as `adf/` records them, which 11 holds to Atlassian's
|
||||||
halves.
|
schema — and misplace a share of nodes so the carry (§3) is exercised; no JSON Schema walker
|
||||||
Generators emit editor-normal ADF (§2). Real sanitized ADF from live Atlassian APIs lands
|
enters the tests. `toEditorNormal` stays internal. 2e5's collision test goes, since a
|
||||||
here too (§10), in `corpus/real-payloads/`: an ADF→markdown→ADF check with no expected
|
collision already fails the round-trip on the same fixtures; the fixture-duplicate test
|
||||||
markdown, the payloads supplied by the maintainer. This subsumes 2e5's collision property —
|
stays.
|
||||||
a document that round-trips proves no other document shares its spelling — so decide here
|
- [ ] **4.1 — Editor-normal and the node accessors.** `toEditorNormal(doc)` in
|
||||||
whether that gate stays as the parser-free, faster-failing signal or goes; the half holding
|
`src/adf/editor-normal.ts`, on 3i's merging: adjacent text nodes carrying identical marks and
|
||||||
no fixture duplicates is hygiene rather than a round-trip claim, and stays either way.
|
no attributes merged, an empty `attrs`, `marks` or `content` the absent key, `-0` read as `0`
|
||||||
|
(§2); the round-trip tests compare the parser's output through it, and `serializeCanonicalJson`
|
||||||
|
beneath it walks iteratively. `nodeContent`/`nodeAttrs`/`nodeMarks` replace the 46 inline
|
||||||
|
`?? []`/`?? {}` 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
|
||||||
|
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`
|
||||||
|
with a `ConvertError` or reads back through `markdownToAdf` to an equal document, and
|
||||||
|
nothing throws, under Node, Deno and Bun alike. 2e5's collision test is deleted.
|
||||||
|
- [ ] **4.3 — The markdown property.** Generated markdown through `markdownToAdf` never throws,
|
||||||
|
and the runs fit the budget; where it parses and `adfToMarkdown` spells the result, that
|
||||||
|
spelling parses and emits to itself byte for byte (§2).
|
||||||
|
- [ ] **4.4 — The real payloads.** `corpus/real-payloads/` holds the maintainer's sanitized
|
||||||
|
payloads, each round-tripped ADF→markdown→ADF with no expected markdown. It waits on the
|
||||||
|
maintainer placing the files.
|
||||||
- [ ] **4b — The block walk's retry (`0.2.0`).** `emitBlock` walks a subtree twice wherever
|
- [ ] **4b — The block walk's retry (`0.2.0`).** `emitBlock` walks a subtree twice wherever
|
||||||
`readableBlock` reads it whole and then gives up — a list item whose first line reads back
|
`readableBlock` reads it whole and then gives up — a list item whose first line reads back
|
||||||
as a thematic break — and the walk below does the same, so the cost doubles per level:
|
as a thematic break — and the walk below does the same, so the cost doubles per level:
|
||||||
@@ -98,15 +118,18 @@ The numbering is the order the work was planned in, not the order it ships.
|
|||||||
timing is what turns "slow or hung" from a guess into a reading; the browser leg's own
|
timing is what turns "slow or hung" from a guess into a reading; the browser leg's own
|
||||||
5.4–7.9s against a 17s warm gate is the number that made it obviously cheap.
|
5.4–7.9s against a 17s warm gate is the number that made it obviously cheap.
|
||||||
- [x] **5 — Ship `0.1.0`.**
|
- [x] **5 — Ship `0.1.0`.**
|
||||||
- [ ] **5e — The publish token's deadline (before 2027-01).** `0.1.0` published only once the npm
|
- [ ] **5e — The publish token's deadline.** `0.1.0` published only once the npm
|
||||||
token carried **Bypass 2FA**: the account requiring no 2FA on writes was not enough, and npm
|
token carried **Bypass 2FA**: the account requiring no 2FA on writes was not enough, and npm
|
||||||
answered `EOTP` until the token itself bypassed. npm retires bypass-2FA tokens for direct
|
answered `EOTP` until the token itself bypassed. npm retires bypass-2FA tokens for direct
|
||||||
publishing around January 2027, and its replacement — trusted publishing over OIDC —
|
publishing around January 2027, leaving them `npm stage publish`, which a maintainer
|
||||||
supports GitHub Actions, GitLab CI, CircleCI and Buildkite, not Gitea or self-hosted
|
approves with 2FA; its replacement — trusted publishing over OIDC — supports GitHub-hosted
|
||||||
runners. So the release path has an expiry date and no drop-in successor yet. Revisit before
|
Actions, GitLab.com's shared runners and CircleCI's cloud, self-hosted runners planned
|
||||||
the deadline: whether npm has added Gitea or self-hosted OIDC, and otherwise whether the
|
without a date. So the release path has an expiry date and no drop-in successor yet. Revisit:
|
||||||
release moves to a human-approved staged publish — which fits badly with publish-on-merge,
|
whether npm has added Gitea or self-hosted OIDC, and otherwise whether the
|
||||||
|
release moves to the staged publish — which fits badly with publish-on-merge,
|
||||||
and is the trade to weigh rather than discover on a red release run.
|
and is the trade to weigh rather than discover on a red release run.
|
||||||
|
**Settled** (the maintainer, 2026-09-13): last of the known work, clear of `0.2.0`, placed
|
||||||
|
there knowing the cutoff may land before `0.2.0` ships.
|
||||||
- [ ] **5f — Publish the bundle size (`0.2.1`).** Measure the shipped artifact and put the number in the
|
- [ ] **5f — Publish the bundle size (`0.2.1`).** Measure the shipped artifact and put the number in the
|
||||||
README, kept honest by the release pipeline rather than by a human re-reading it. The
|
README, kept honest by the release pipeline rather than by a human re-reading it. The
|
||||||
quantity is what a consumer downloads and loads: the tarball `npm pack` produces, its
|
quantity is what a consumer downloads and loads: the tarball `npm pack` produces, its
|
||||||
@@ -120,12 +143,15 @@ The numbering is the order the work was planned in, not the order it ships.
|
|||||||
- [ ] **5g — Reweight the README for the reader (`0.2.0`).** It opens with the pre-launch rationale —
|
- [ ] **5g — Reweight the README for the reader (`0.2.0`).** It opens with the pre-launch rationale —
|
||||||
Atlassian's REST APIs, `pf-editor-service/convert` being decommissioned, a link to
|
Atlassian's REST APIs, `pf-editor-service/convert` being decommissioned, a link to
|
||||||
JRACLOUD-77436 — where a shipped package should answer what it is, what it does and for whom
|
JRACLOUD-77436 — where a shipped package should answer what it is, what it does and for whom
|
||||||
first, then the shortest runnable example; the reader's top seconds go to "why this exists"
|
first, then the shortest runnable example.
|
||||||
instead of "what I can do with it". Demote the Jira/endpoint background to a later "why
|
**Settled** (the maintainer, 2026-09-13): the background goes entirely, no endpoint, ticket or
|
||||||
losslessness" note or drop it — the internal references (the `jira.atlassian.com` URL,
|
"why" note left. The top follows the package-README order: an npm version badge and the Gitea
|
||||||
`pf-editor-service/convert`) don't belong in published text at all, no ticket IDs or internal
|
Actions badge, a tagline that is also `package.json`'s `description`, a feature list and a
|
||||||
URLs. The `0.3.0` HTML future should read as an aside, not the lede: the package reads as a
|
one-line table of contents, then install and the shortest runnable example; a table of
|
||||||
shipped `0.1.0`, not a work-in-progress.
|
everything exported sits near the bottom. The HTML directions are one aside line under the API
|
||||||
|
until `0.3.0` ships them, the `// 0.3.0` signatures and the `0.3.0` guarantee going until then.
|
||||||
|
The tagline and `description` read "Lossless conversion between Atlassian Document Format and
|
||||||
|
extended markdown" until 7 restores HTML.
|
||||||
- [x] **5a — Rename to `@larvit/adf-codec`.**
|
- [x] **5a — Rename to `@larvit/adf-codec`.**
|
||||||
- [x] **5b — The consumer's error surface.**
|
- [x] **5b — The consumer's error surface.**
|
||||||
- [x] **5b1 — The error's source position.**
|
- [x] **5b1 — The error's source position.**
|
||||||
@@ -137,11 +163,77 @@ The numbering is the order the work was planned in, not the order it ships.
|
|||||||
- [ ] **6 — The HTML dialect spec (`0.3.0`).** Element-by-element mapping, the `data-*` fidelity
|
- [ ] **6 — The HTML dialect spec (`0.3.0`).** Element-by-element mapping, the `data-*` fidelity
|
||||||
scheme, the opaque-carry form, and the documented foreign-element set `htmlToAdf` accepts.
|
scheme, the opaque-carry form, and the documented foreign-element set `htmlToAdf` accepts.
|
||||||
- [ ] **7 — HTML, ship `0.3.0`.** `adfToHtml`, `htmlToAdf`, the composed `markdownToHtml` /
|
- [ ] **7 — HTML, ship `0.3.0`.** `adfToHtml`, `htmlToAdf`, the composed `markdownToHtml` /
|
||||||
`htmlToMarkdown`. CommonMark spec suite runs against `markdownToHtml` from here (§10).
|
`htmlToMarkdown`. CommonMark spec suite runs against `markdownToHtml` from here (§10). The
|
||||||
|
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 supports, keeping the ADF's data while dropping what markdown cannot hold — format, design and the richer nodes.
|
- [ ] **10 — Lossy conversion (`0.2.0`).** Markdown other tools render readably, to and from ADF,
|
||||||
- [ ] **11 — Evaluate `@atlaskit/adf-schema` (`0.2.0`).** Whether to add `@atlaskit/adf-schema` as a dev dependency to use as truth for the ADF schema.
|
keeping the content while dropping what markdown cannot hold — format, design and the richer
|
||||||
|
nodes.
|
||||||
|
**Settled** (the maintainer, 2026-09-14): two exports composed around the lossless pair, so §1's
|
||||||
|
four conversions stay four. `adfToPlainMarkdown(doc)` reduces the document ADF→ADF and hands it
|
||||||
|
to `adfToMarkdown`; `plainMarkdownToAdf(markdown)` hands the markdown to `markdownToAdf` and
|
||||||
|
lifts the result ADF→ADF. Both carry markdown conventions, so the reduction sits in
|
||||||
|
`src/markdown/emit/`, the lift in `src/markdown/parse/` and what both read in `src/markdown/`
|
||||||
|
(§11). The markdown is the flavour without directives — CommonMark, the pipe table and `~~` —
|
||||||
|
plus the conventions below, chosen for readability from a survey of GitHub, GitLab, Gitea,
|
||||||
|
Obsidian, Pandoc, MkDocs, Docusaurus, Typora, Joplin, Logseq, Bear, Notion, Azure DevOps and
|
||||||
|
Discord, GitHub's renderer confirming each shape. Writing refuses only what the document guard
|
||||||
|
refuses (`not-an-adf-document`, `unsupported-document-version`, `unsupported-nesting-depth`)
|
||||||
|
and degrades every other shape; reading refuses what `markdownToAdf` refuses. A lifted node
|
||||||
|
carries no `localId`. The lift also reads other tools' spellings — type words in any case,
|
||||||
|
Obsidian's aliases, `[X]` — since it reads their output and never writes those spellings.
|
||||||
|
- A `panel` is an alert: the marker alone on the quote's first line, a blank `>`, then the body
|
||||||
|
(`> [!WARNING]`), in GitHub's five words by colour — info `NOTE`, note `IMPORTANT`, tip and
|
||||||
|
success `TIP`, warning `WARNING`, error `CAUTION`, custom `NOTE`. The lift reads those words
|
||||||
|
back (`NOTE` info, `IMPORTANT` note, `TIP` tip, `WARNING` warning, `CAUTION` error) and
|
||||||
|
Obsidian's by meaning (hint tip; success, check and done success; attention warning; danger,
|
||||||
|
failure, fail, missing and bug error; any other word info). Text after a marker in its
|
||||||
|
paragraph is the panel's first body paragraph.
|
||||||
|
- An `expand` or `nestedExpand` is Obsidian's folded callout, `> [!NOTE]- Title`, a blank `>`,
|
||||||
|
then the body. The lift reads a fold sign (`-` or `+`) as an expand whatever the word, the
|
||||||
|
rest of the marker's paragraph as its title, and an expand inside an expand as a
|
||||||
|
`nestedExpand`.
|
||||||
|
- 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
|
||||||
|
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
|
||||||
|
merge.
|
||||||
|
- Rejected in the survey: `~sub~` and `^sup^`, underline and colour spellings, raw HTML
|
||||||
|
(`<details>`, `<mark>`), MkDocs `!!!` and the `:::` admonition family, footnotes, definition
|
||||||
|
lists, wikilinks, embeds, tags, comments, TOC tokens, spoilers, task states past `[x]`/`[ ]`,
|
||||||
|
and lifting bare URLs, `@name`, `:shortcode:` or ISO dates into nodes.
|
||||||
|
- [ ] **10a — The reduction.** `adfToPlainMarkdown`'s ADF→ADF reduction, tests first, a test per
|
||||||
|
row above.
|
||||||
|
- [ ] **10b — The lift.** `plainMarkdownToAdf`'s ADF→ADF lift, tests first, a test per row it reads,
|
||||||
|
other tools' spellings included; the editor's default highlight colour looked up and cited.
|
||||||
|
- [ ] **10c — The exports.** `adfToPlainMarkdown` and `plainMarkdownToAdf` exported with their README
|
||||||
|
sections, and two properties over 4.2's generators: writing refuses only the guard's codes,
|
||||||
|
and markdown `adfToPlainMarkdown` wrote reads back through `plainMarkdownToAdf` and writes
|
||||||
|
again byte for byte. AGENTS.md §1 records the pair as composed around the lossless one.
|
||||||
|
- [x] **11 — Atlassian's ADF schema as the tables' truth.**
|
||||||
|
- [x] **11a — The vendored schema.**
|
||||||
|
- [x] **11b — The gate.**
|
||||||
- [ ] **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
|
||||||
@@ -149,29 +241,71 @@ The numbering is the order the work was planned in, not the order it ships.
|
|||||||
length rule go and every container opens the constant `!adf:`; block leaf `!adf:name arg
|
length rule go and every container opens the constant `!adf:`; block leaf `!adf:name arg
|
||||||
{attrs}` with no closer; inline node `!adf:name[content]{attrs}`; directive marks
|
{attrs}` with no closer; inline node `!adf:name[content]{attrs}`; directive marks
|
||||||
`!adf:border`/`subsup`/`textColor`/`underline` `[content]{attrs}`. Attributes and their
|
`!adf:border`/`subsup`/`textColor`/`underline` `[content]{attrs}`. Attributes and their
|
||||||
escaping stay `{key=value}`; the literal escape is `\!adf:`; a line opening `!adf:` claims as
|
escaping stay `{key=value}`; the literal escape is `\!adf:`. Leaf vs container is decided by
|
||||||
today's colon-run does. Leaf vs container is decided by the node's content model rather than
|
the node's content model rather than syntax — the `::`/`:::` split goes, a simplification the
|
||||||
syntax — the `::`/`:::` split and §4's name-set-independent recognition go, a simplification
|
carry makes safe (an unknown *block* node already rides the fence, not the directive). The
|
||||||
the carry makes safe (an unknown *block* node already rides the fence, not the directive).
|
carry's reserved name becomes `carry`, both spellings — the block fence info string `carry`
|
||||||
The carry's reserved name becomes `carry`, both spellings — the block fence info string
|
and the inline `!adf:carry{json="…"}` — named for what it does: it carries a node verbatim,
|
||||||
`` `carry` `` and the inline `!adf:carry{json="…"}` — named for what it does: it carries a node
|
never "unknown-node", since a known node no section spells where it stands rides it too. No
|
||||||
verbatim, never "unknown-node", since a known node no section spells where it stands rides it
|
`ConvertErrorCode` is added, removed or renamed, and the round-trip guarantee and the carry
|
||||||
too. A spelling change, not a semantic one: no `ConvertErrorCode` is added, removed or renamed,
|
both hold through it. Mechanical surface: the grammar in `spec/flavour.md`,
|
||||||
the round-trip guarantee and the carry both hold through it. Mechanical surface: the grammar in
|
`src/adf/block-directives.ts` + `inline-directives.ts`, `src/markdown/`'s
|
||||||
`spec/flavour.md`, `src/adf/block-directives.ts` + `inline-directives.ts`, `src/markdown/`'s
|
|
||||||
`directive-syntax.ts`, `opaque-carry.ts` and the `emit/` + `parse/` readers, every corpus
|
`directive-syntax.ts`, `opaque-carry.ts` and the `emit/` + `parse/` readers, every corpus
|
||||||
fixture (round-trip, normalization and `errors/`), `spec.test.ts`'s prose reader, and the
|
fixture (round-trip, normalization and `errors/`), the prose reader over `spec/flavour.md`,
|
||||||
README's examples.
|
and the README's examples.
|
||||||
- [ ] **12a — The spec and the decision.** Rewrite `spec/flavour.md` to the `!adf:` grammar, and
|
**Settled** (the maintainer, 2026-09-13):
|
||||||
record the departures in `AGENTS.md` §4 (leaf/container by content model, carry renamed
|
- A line opening `!adf:name` is a block line when a space or the line's end follows the name,
|
||||||
`carry`).
|
and a paragraph when `[` or `{` does. Claiming stays syntactic and structure comes from the
|
||||||
- [ ] **12b — The emit side.** `adfToMarkdown` spells `!adf:` / `!adf:/name` / `!adf:carry`; its
|
tables: an unknown name is `unknown-directive-name` at the opener, whatever follows it.
|
||||||
fixtures re-spelled, green.
|
- An unescaped `!adf:` claims on its own anywhere inline: one completing no directive is
|
||||||
- [ ] **12c — The parse side and the round-trip.** `markdownToAdf` reads it back; the round-trip
|
`malformed-directive`, the emitter escapes every literal `!adf:`, and `!adf:hardBreak{}`
|
||||||
corpus, the `errors/` fixtures and the CommonMark spec suite re-spelled,
|
keeps its braces. Block and inline share the one `\!adf:` escape hint.
|
||||||
`markdownToAdf(adfToMarkdown(doc))` still equals `doc`.
|
- A closer names the innermost open container, crosses no list-item or blockquote edge,
|
||||||
- [ ] **12d — The README and the sweep.** The README's examples follow; sweep docs and fixtures
|
indents as a fence does and carries nothing after the name; anything else is
|
||||||
for any stale `::`/`:name` spelling.
|
`malformed-directive`.
|
||||||
|
- A node holding no content whose content model takes some is an empty opener–closer pair,
|
||||||
|
never a leaf.
|
||||||
|
- A spelled node's content model is frozen with its spelling: changing it is MAJOR (§8).
|
||||||
|
- The colon spellings are dropped, not refused: `0.1.0` markdown reads back as prose, `adf`
|
||||||
|
is no longer a reserved language, and `MIGRATION.md` tells a consumer to convert stored
|
||||||
|
markdown through `0.1.0`'s parser and `0.2.0`'s emitter.
|
||||||
|
- Inputs moving between codes ride the break: a leaf given a body, a container missing its
|
||||||
|
closer and `listBreak` with a body are `malformed-directive`, and an empty inline-body
|
||||||
|
container parses.
|
||||||
|
- Split by construct, each sub-item both directions: 55 of 78 round-trip fixtures feed both
|
||||||
|
the emit and the read-back test, so an emit-only chunk cannot land green.
|
||||||
|
- [ ] **12a — The spec and the decision.** `spec/flavour.md` rewritten to the `!adf:` grammar and
|
||||||
|
the settled answers above, no colon directive form left in it; AGENTS.md §4's directive
|
||||||
|
bullet and prior-art line, and §8's escape hints and `::adf`/`::listBreak` examples, name
|
||||||
|
the new forms, §8 gaining the frozen content model.
|
||||||
|
- [ ] **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
|
||||||
|
and its escape; the round-trip, normalization and `errors/` fixtures holding inline forms
|
||||||
|
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
|
||||||
|
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
|
||||||
|
and `errors/` re-derived under the shifted codes, and the gate green.
|
||||||
|
- [ ] **12d — The README, `MIGRATION.md` and the sweep.** The README's examples and error tables
|
||||||
|
follow, `MIGRATION.md` linked from one README line; docs and fixtures swept for any stale
|
||||||
|
`::`/`:name` spelling.
|
||||||
|
- [ ] **13 — The schema's gap attributes (`0.2.0`).** Spell the attributes 11b pins as gaps, in
|
||||||
|
12's grammar, and empty the list.
|
||||||
|
**Settled** (the maintainer, 2026-09-13): a link `[text](url "title")` cannot hold takes the
|
||||||
|
directive mark `!adf:link[text]{attrs}` — one carrying `collection`, `id` or `occurrenceKey`,
|
||||||
|
or an `href` or `title` no CommonMark escape writes — and a directive link CommonMark could
|
||||||
|
spell is `unsupported-node-shape`. That leaves `unspellable-link` no cause, so it leaves
|
||||||
|
`ConvertErrorCode` in `0.2.0`, §8 recording the removal.
|
||||||
|
- [ ] **13a — `rule` and `layoutSection`.** `rule`'s `color`, `style` and `weight` and
|
||||||
|
`layoutSection`'s `columnRuleStyle` join their tables and `spec/flavour.md` bullets, with
|
||||||
|
round-trip fixtures; their gap entries go.
|
||||||
|
- [ ] **13b — The directive link.** `link` spelled as above in both directions, with a round-trip
|
||||||
|
fixture per trigger, `spec/flavour.md`'s Marks section following; `unspellable-link` removed
|
||||||
|
from the code list, its `errors/` fixtures and the CommonMark suite's `unspellable`
|
||||||
|
exceptions it cures re-derived, and the README's code table and its "not every document
|
||||||
|
converts back" guarantee following; the gap list is empty.
|
||||||
|
|
||||||
## The ADF inventory to cover
|
## The ADF inventory to cover
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user