67 Commits

Author SHA1 Message Date
lilleman 2f23c13528 Tick 12b, moving its text to todo-history.md
CI / gate (push) Successful in 25s
CI / publish (push) Successful in 4s
2026-09-16 10:28:08 +02:00
lilleman 9ba2d09ee3 12b: the content slot refuses a carried text node's content too, and the reserved-name message names the live fence
CI / gate (push) Successful in 24s
CI / publish (push) Waiting to run
2026-09-16 10:26:12 +02:00
lilleman 65c09bdbae 12b: the inline opener speller takes its reader's name, and todo.md records 12c's collapses
CI / gate (push) Successful in 25s
CI / publish (push) Has been skipped
2026-09-16 10:03:49 +02:00
lilleman 302804cc4e 12b: the inline directive forms read and spell the !adf: prefix
CI / gate (push) Successful in 26s
CI / publish (push) Has been skipped
2026-09-16 09:52:52 +02:00
lilleman fe064efdb9 Tick 12a, moving its text to todo-history.md
CI / gate (push) Successful in 25s
CI / publish (push) Successful in 4s
2026-09-16 09:19:51 +02:00
lilleman 16af0b9592 12a: spec/flavour.md and AGENTS.md spell the !adf: grammar
CI / gate (push) Successful in 24s
CI / publish (push) Successful in 4s
2026-09-16 09:16:13 +02:00
lilleman 4ac4a119e4 Tick 4.3 and 4, moving their text to todo-history.md
CI / gate (push) Successful in 25s
CI / publish (push) Successful in 4s
2026-09-15 22:49:53 +02:00
lilleman ad1f3e996b 4.3: todo.md records the joined-run family and the generalized backtick pass
CI / gate (push) Successful in 25s
CI / publish (push) Successful in 4s
2026-09-15 22:35:48 +02:00
lilleman b6ba440410 4.3: a bare backtick run escapes wherever a later string of its length forms around an escape 2026-09-15 22:28:51 +02:00
lilleman 7d20f3e895 4.3: the ADF generator draws backtick runs beside code spans, and the directive floor's message names what it counts 2026-09-15 22:22:46 +02:00
lilleman 5fe2e6ec42 4.3: the directive floor counts directive-only and carried types in the parsed document, and run parameters name the gate
CI / gate (push) Successful in 25s
CI / publish (push) Has been skipped
2026-09-15 21:48:06 +02:00
lilleman ef71d55c13 4.3: todo.md records the settled harness, floors and breaks, and 12's and 13b's follow-ups
CI / gate (push) Successful in 25s
CI / publish (push) Has been skipped
2026-09-15 21:36:55 +02:00
lilleman a3073c69b2 4.3: under the gate seed the markdown property asserts floors on fixpoint and directive-shaped runs 2026-09-15 21:36:55 +02:00
lilleman 8e7d429379 4.3: the shared property module is property-harness.ts, and the canonical arm filters emit refusals 2026-09-15 21:31:41 +02:00
lilleman 755b5cbe01 4.3: the markdown generator splits into inline, leaf-block and block builders 2026-09-15 21:28:32 +02:00
lilleman fe47e6bc7b 4.3: a link opening a paragraph that reads as a link reference definition rides the carry 2026-09-15 21:22:34 +02:00
lilleman aeb54d98d1 4.3: an escaped backtick run escapes whole, and a lone backtick before one escapes too
CI / gate (push) Successful in 25s
CI / publish (push) Has been skipped
2026-09-15 21:05:16 +02:00
lilleman 4735933d61 4.3: a paragraph line reading as a link reference definition escapes its opening bracket 2026-09-15 21:05:16 +02:00
lilleman ec5c92d4fd 4.3: the markdown property 2026-09-15 21:05:16 +02:00
lilleman fb10a1dc04 Tick 4.4, moving its text to todo-history.md
CI / publish (push) Successful in 3s
CI / gate (push) Successful in 23s
2026-09-15 20:59:55 +02:00
lilleman 2901a28fcb 4.4: a review comment as a real payload
CI / gate (push) Successful in 22s
CI / publish (push) Successful in 4s
2026-09-15 19:21:53 +02:00
lilleman 047827fd86 4.4: the real payloads 2026-09-15 19:18:49 +02:00
lilleman e23fd9495e Settle 4.4's payloads as editor-written on the test site, and let 4.4 follow 12
CI / publish (push) Successful in 4s
CI / gate (push) Successful in 25s
2026-09-15 17:51:57 +02:00
lilleman 10d63ff8e6 Tick 4.2, moving its text to todo-history.md, and give 13b the autolink fixture
CI / gate (push) Successful in 23s
CI / publish (push) Successful in 3s
2026-09-14 23:45:18 +02:00
lilleman f8149321f2 4.2: the review's runner and weight nits
CI / gate (push) Successful in 23s
CI / publish (push) Successful in 4s
2026-09-14 23:43:49 +02:00
lilleman a581e454ae 4.2: the review's generator, runner and pattern fixes
CI / gate (push) Successful in 23s
CI / publish (push) Has been skipped
2026-09-14 23:25:13 +02:00
lilleman 342c2027d8 4.2: a link target's would-be directive escapes inside a directive's content 2026-09-14 23:25:13 +02:00
lilleman fade9f51ea 4.2: generated keys avoid V8's JSON.parse key fault 2026-09-14 23:25:13 +02:00
lilleman b6930b45a4 4.2: an autolink holding a backtick gives way inside a directive's content 2026-09-14 23:25:13 +02:00
lilleman e166e969cb 4.2: the ADF property 2026-09-14 23:25:13 +02:00
lilleman 507f68aa13 4.2: a link target's backtick escapes where it would open a code span inside a directive's content 2026-09-14 23:25:13 +02:00
lilleman a2672bdc5b 4.2: a list whose item holds a whitespace-only line takes the directive form 2026-09-14 23:25:13 +02:00
lilleman 9303235434 4.2: a link's brackets balance inside a directive's content 2026-09-14 23:25:13 +02:00
lilleman eafd863a32 4.2: an empty link destination beside a title spells <> 2026-09-14 23:25:13 +02:00
lilleman 1125715371 Record the autolink backtick answer and the accepted V8 JSON.parse fault in 4.2
CI / publish (push) Successful in 4s
CI / gate (push) Successful in 19s
2026-09-14 22:34:05 +02:00
lilleman 31156f4914 Settle 4.2's generator mix and how the breaks it finds get fixed
CI / publish (push) Successful in 3s
CI / gate (push) Successful in 18s
2026-09-14 20:50:17 +02:00
lilleman 0185879943 Tick 4.1, moving its text to todo-history.md, and record where 4.4's payloads land
CI / publish (push) Successful in 3s
CI / gate (push) Successful in 18s
2026-09-14 20:23:41 +02:00
lilleman 9cd4b33020 4.1: the browser leg compares through toEditorNormal
CI / gate (push) Successful in 17s
CI / publish (push) Successful in 4s
2026-09-14 20:21:32 +02:00
lilleman 758a5724e9 4.1: an iterative canonical serializer
CI / gate (push) Successful in 19s
CI / publish (push) Has been skipped
2026-09-14 20:11:25 +02:00
lilleman 276433d635 4.1: the review's test and comment fixes 2026-09-14 20:11:25 +02:00
lilleman 7672531cdc 4.1: editor-normal and the node accessors 2026-09-14 20:11:25 +02:00
lilleman ec5f80e47f 4.1: a text node carrying attributes never merges 2026-09-14 20:11:25 +02:00
lilleman da58a59f70 Revise 10: an alert-and-callout lossy pair, to and from markdown
CI / publish (push) Successful in 3s
CI / gate (push) Successful in 17s
2026-09-14 19:34:30 +02:00
lilleman 2796487b30 Tick 11, moving its text to todo-history.md, and correct 4.1's text
CI / gate (push) Successful in 16s
CI / publish (push) Has been cancelled
2026-09-14 19:34:02 +02:00
lilleman dcea401ec2 11b: fail on combinators an attribute kind cannot read
CI / gate (push) Successful in 17s
CI / publish (push) Successful in 3s
2026-09-14 17:33:13 +02:00
lilleman 25d851cc67 11b: guard the gate against empty gaps and unread keywords
CI / gate (push) Successful in 17s
CI / publish (push) Has been skipped
2026-09-14 16:54:32 +02:00
lilleman 0f03edc8ef 11b: the gate
CI / gate (push) Successful in 17s
CI / publish (push) Has been skipped
2026-09-14 16:16:49 +02:00
lilleman 37f59cec40 Design 5g: the README's order, badges and tagline
CI / gate (push) Successful in 17s
CI / publish (push) Successful in 3s
2026-09-14 16:11:14 +02:00
lilleman aa34d6e7fd Design 10: adfToPlainMarkdown as a reduction, its mapping and Obsidian's formats
CI / gate (push) Successful in 17s
CI / publish (push) Waiting to run
2026-09-14 15:09:48 +02:00
lilleman 3c1bacace6 Design 12 and 13: settle the !adf: grammar and split both by construct
CI / gate (push) Successful in 17s
CI / publish (push) Has been cancelled
2026-09-14 15:02:16 +02:00
lilleman f8a5d5e9b3 Design 4: fast-check generators on a fixed seed, in four sub-items
CI / gate (push) Successful in 17s
CI / publish (push) Has been cancelled
2026-09-14 15:02:15 +02:00
lilleman c9fb85f9db Tick 11a, moving its text to todo-history.md
CI / gate (push) Successful in 18s
CI / publish (push) Successful in 3s
2026-09-14 15:02:15 +02:00
lilleman 24b2e8cbbe 11a: the vendored schema
CI / gate (push) Successful in 25s
CI / publish (push) Successful in 4s
2026-09-13 15:40:34 +02:00
lilleman f66efabcbb Design 11: vendor Atlassian's ADF schema and gate the node tables against it
CI / publish (push) Successful in 3s
CI / gate (push) Successful in 17s
2026-09-13 15:34:32 +02:00
lilleman e424b35fd6 Settle 0.2.0's order and move 5e to last
CI / gate (push) Successful in 17s
CI / publish (push) Successful in 4s
2026-09-13 15:08:45 +02:00
lilleman 5c62b07530 Extend the merge grant through 0.2.0 and drop the finished reserved acts
CI / publish (push) Successful in 3s
CI / gate (push) Successful in 17s
2026-09-13 11:43:48 +02:00
lilleman 1e20b0b6cc Tick 3k and milestone 3, moving their text to todo-history.md
CI / gate (push) Successful in 17s
CI / publish (push) Successful in 4s
2026-09-13 11:10:41 +02:00
lilleman e989cc3edd Name the pending CommonMark divergences and complete the spec attribution
CI / gate (push) Successful in 17s
CI / publish (push) Successful in 4s
2026-09-12 20:18:41 +02:00
lilleman f81bebe8d0 Spell the check set once; fix the doubled spec version and a stale filename
CI / gate (push) Successful in 18s
CI / publish (push) Has been skipped
2026-09-09 15:40:29 +02:00
lilleman d1331a6612 Delete the unreachable entity decoder, hash-pin spec.json, and fold the exception pin loop
CI / gate (push) Successful in 24s
CI / publish (push) Has been skipped
2026-09-09 14:08:30 +02:00
lilleman 6e3b7e692c Cite §14 instead of the ticked 3e item in the count comment
CI / gate (push) Successful in 17s
CI / publish (push) Has been skipped
2026-09-06 23:15:16 +02:00
lilleman dfbb92b7ab Pin refusal codes and divergences; fix the text oracle and count by heading level
CI / gate (push) Successful in 18s
CI / publish (push) Has been skipped
2026-09-06 23:09:01 +02:00
lilleman 12b57a06a9 Pin the suite run, gate the exception list, and trim the corpus README
CI / gate (push) Successful in 19s
CI / publish (push) Has been skipped
2026-09-06 21:58:58 +02:00
lilleman e6610d7057 Check in the CommonMark spec suite and pin its exception list
CI / gate (push) Successful in 19s
CI / publish (push) Has been skipped
2026-09-05 18:56:28 +02:00
lilleman 026ea5e1b6 Rework the roadmap: fold perf and docs fixes into 0.2.0, add 0.2.1
CI / gate (push) Successful in 18s
CI / publish (push) Successful in 4s
2026-09-05 18:14:05 +02:00
lilleman 5d19bdbae7 Add the online sandbox, lossy conversion and @atlaskit/adf-schema evaluation
CI / gate (push) Successful in 17s
CI / publish (push) Successful in 3s
2026-09-05 16:41:34 +02:00
lilleman 75f35eef0b Tick the 0.1.0 release and record the publish token's deadline
CI / gate (push) Successful in 18s
CI / publish (push) Successful in 3s
2026-09-05 14:16:29 +02:00
131 changed files with 19849 additions and 658 deletions
+50 -24
View File
@@ -17,12 +17,14 @@ When losslessness and readability conflict, losslessness wins.
The other direction is a canonical fixpoint, not byte-identity: human markdown normalizes, the way The other direction is a canonical fixpoint, not byte-identity: human markdown normalizes, the way
back yields the library's canonical spelling, and that spelling round-trips byte-identically — back yields the library's canonical spelling, and that spelling round-trips byte-identically —
where there is a way back. CommonMark spells link destinations the flavour has no escape for, so a where there is a way back. CommonMark spells some things the flavour has no escape for — a link
parse succeeding does not imply a spellable document; `todo.md` 3k's exception list names those. destination or title holding a backslash or newline, a paragraph opening with a code span whose
backticks read back as a fence — so a parse succeeding does not imply a spellable document;
`corpus/commonmark-spec/exceptions.json` names those.
"Equals" is structural equality over editor-normal ADF — adjacent text nodes with identical marks "Equals" is structural equality over editor-normal ADF — adjacent text nodes with identical marks
merged, JSON number semantics, an empty attrs object, marks array or content array the absent and no attributes merged, JSON number semantics, an empty attrs object, marks array or content
key — the only domain markdown can restore. array the absent key — the only domain markdown can restore.
Round-trip equality is a property tested over a corpus, not a claim made in prose. Round-trip equality is a property tested over a corpus, not a claim made in prose.
@@ -41,8 +43,11 @@ Round-trip equality is a property tested over a corpus, not a claim made in pros
## 4. The flavour ## 4. The flavour
- Directives, one grammar for everything markdown lacks: `:::panel info` … `:::` blocks, - Directives, one grammar for everything markdown lacks, namespaced under `!adf:`: `!adf:panel info`
`:mention[@Mikael]{id=5b10a2}` inline. Prior art: CommonMark's generic-directives proposal. … `!adf:/panel` blocks, `!adf:mention[@Mikael]{id=5b10a2}` inline, `\!adf:` the one escape. Not
CommonMark's generic-directives proposal: its `:::` claims a form prose writes, and its
fence-length discipline ties a container's opener to its own body, where closing from the opener
nests by itself and leaf versus container falls out of the node's content model.
- Plain CommonMark is a subset, with carve-outs (`spec/flavour.md`): literal text shaped like a - Plain CommonMark is a subset, with carve-outs (`spec/flavour.md`): literal text shaped like a
directive, a pipe table or a `~~` pair is claimed — plus one image gap. directive, a pipe table or a `~~` pair is claimed — plus one image gap.
- Tables: one header row plus plain inline cells → pipe table; anything richer → directive form. - Tables: one header row plus plain inline cells → pipe table; anything richer → directive form.
@@ -57,8 +62,16 @@ Round-trip equality is a property tested over a corpus, not a claim made in pros
why ~20 lines of own code cannot do the job, who maintains it, and what auditing it costs. So the why ~20 lines of own code cannot do the job, who maintains it, and what auditing it costs. So the
CommonMark and HTML parsers are written in this repo. A table a standard fixes is data rather than CommonMark and HTML parsers are written in this repo. A table a standard fixes is data rather than
a dependency: HTML5's 2125 semicolon-terminated character references ship packed in their own a dependency: HTML5's 2125 semicolon-terminated character references ship packed in their own
module, so entity decoding is complete without one. `devDependencies`: few, each earning its keep; module, so entity decoding is complete without one. The CommonMark spec suite is the same shape of
they never reach a consumer. 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
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. Atlassian's ADF JSON Schemas ship
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. `fast-check` earns its
place shrinking a failing generated document to the nodes that break it.
## 6. The package contract ## 6. The package contract
@@ -88,15 +101,17 @@ against the README's personas.
The emitted markdown and HTML are contracts. After 1.0: previously-emitted output parsing The emitted markdown and HTML are contracts. After 1.0: previously-emitted output parsing
differently, or not at all, is MAJOR; new syntax while old output still round-trips is MINOR. differently, or not at all, is MAJOR; new syntax while old output still round-trips is MINOR.
Pre-1.0, normal 0.x rules. Pre-1.0, normal 0.x rules. A spelled node's content model is part of that contract — leaf or
container is the model, not the syntax — so giving a spelled node's model content it had not, or
taking it away, is MAJOR whatever ADF's own schema does.
The error surface is a contract too. `ConvertError` is `{ code, message, path, position? }` — the The error surface is a contract too. `ConvertError` is `{ code, message, path, position? }` — the
code from a closed list a consumer may switch exhaustively, the message free text, the path the code from a closed list a consumer may switch exhaustively, the message free text, the path the
node's place from the document root, the position where a parse read the refusal in its input. node's place from the document root, the position where a parse read the refusal in its input.
A message names the violation, not the rule alone — a rule by itself states a truth the reader A message names the violation, not the rule alone — a rule by itself states a truth the reader
must invert before it reads as a failure — and where the flavour's claim refuses ordinary prose it must invert before it reads as a failure — and where the flavour's claim refuses ordinary prose it
names the escape that unclaims the form claimed: `\:::` for a directive line, `\|` for every pipe names the escape that unclaims the form claimed: `\!adf:` for a directive, block line and inline
row, `\:` for an inline directive. alike, `\|` for every pipe row.
Adding, removing or renaming a code is breaking, so a milestone meeting a new failure cause Adding, removing or renaming a code is breaking, so a milestone meeting a new failure cause
reuses a code where one fits; the list is complete at `0.1.0`. A code names the reuses a code where one fits; the list is complete at `0.1.0`. A code names the
cause; where one cause recurs across node types, across one mark's attributes or across cause; where one cause recurs across node types, across one mark's attributes or across
@@ -106,7 +121,7 @@ direction hits it, `unspellable-link` the destination and the title alike. Where
apart, the line between them is what they name: `unspellable-character` is a character CommonMark apart, the line between them is what they name: `unspellable-character` is a character CommonMark
rewrites wherever text holds it, `unspellable-whitespace` the newline no inline directive's rewrites wherever text holds it, `unspellable-whitespace` the newline no inline directive's
content slot spans, in either direction. A claim code names the spelling claimed, never the node that spelling would have built: content slot spans, in either direction. A claim code names the spelling claimed, never the node that spelling would have built:
a malformed `:::table` is a `malformed-directive`, and an alignment colon a `malformed-pipe-table` — a malformed `!adf:table` is a `malformed-directive`, and an alignment colon a `malformed-pipe-table` —
the flavour's own delimiter row is `-` runs, so the grammar refuses the colon rather than ADF's the flavour's own delimiter row is `-` runs, so the grammar refuses the colon rather than ADF's
missing column model doing it. A refusal no spelling recovers from is a gap in the flavour rather missing column model doing it. A refusal no spelling recovers from is a gap in the flavour rather
than a code: give the flavour the spelling and the code goes, which the freeze is the last moment than a code: give the flavour the spelling and the code goes, which the freeze is the last moment
@@ -115,8 +130,8 @@ spelling writes rides the carry with its node. A directive whose name reads back
`unknown-directive-name` rather than a claim code — the spelling is well formed, and telling that `unknown-directive-name` rather than a claim code — the spelling is well formed, and telling that
apart from a typo is what a consumer switches on when a later MINOR gives the name meaning. A apart from a typo is what a consumer switches on when a later MINOR gives the name meaning. A
reserved name is a known name, so never that code, and the two the flavour reserves part on form: reserved name is a known name, so never that code, and the two the flavour reserves part on form:
a form the grammar does not have is a claim code — `::adf`, whose carry is the fence — and a a form the grammar does not have is a claim code — `!adf:carry`, whose carry is the fence — and a
well-formed form in the wrong place is `unsupported-node-shape`, `::listBreak` parting anything well-formed form in the wrong place is `unsupported-node-shape`, `!adf:listBreak` parting anything
but two adjacent lists of one type. What but two adjacent lists of one type. What
the grammar itself refuses stays a claim code, key order among it; a well-formed directive the the grammar itself refuses stays a claim code, key order among it; a well-formed directive the
node tables refuse — an attribute a node does not hold or spells elsewhere, a value outside its node tables refuse — an attribute a node does not hold or spells elsewhere, a value outside its
@@ -198,8 +213,10 @@ resolver maps them, under `NodeNext` alone; a `.d.ts` reader that is not `tsc` s
`node-floor.js` round-trips the installed package under a Node pinned to `engines.node`'s floor. `node-floor.js` round-trips the installed package under a Node pinned to `engines.node`'s floor.
A fourth engine reads the build rather than the source: a headless Firefox loads `dist/index.js` A fourth engine reads the build rather than the source: a headless Firefox loads `dist/index.js`
over HTTP and converts the whole corpus, which is §6's browser half and the only SpiderMonkey over HTTP and converts the round-trip, normalization and error fixtures and the real payloads —
there is — the gate's other three engines are two V8s and a JavaScriptCore that is not Safari's. the `commonmark-spec` sort is the Node suite's to check — which is §6's browser half and the only
SpiderMonkey there is — the gate's other three engines are two V8s and a JavaScriptCore that is
not Safari's.
A WebDriver session is what carries a verdict back out, the driver and the page's server sharing A WebDriver session is what carries a verdict back out, the driver and the page's server sharing
one network namespace so each is the other's `127.0.0.1`; `--headless --screenshot` has no such one network namespace so each is the other's `127.0.0.1`; `--headless --screenshot` has no such
channel, and loading `dist/index.js` in a globals-stripped realm buys one by not running a browser. channel, and loading `dist/index.js` in a globals-stripped realm buys one by not running a browser.
@@ -212,9 +229,13 @@ functions, and a branch floor that only ever moves upward. It sits below 100 bec
`noUncheckedIndexedAccess` and ADF's optional keys force — `?? []`, `?? {}`, `?.`, an index `noUncheckedIndexedAccess` and ADF's optional keys force — `?? []`, `?? {}`, `?.`, an index
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 ADF Atlassian's
live Atlassian APIs; property-generated ADF trees; the CommonMark spec suite against editor wrote; the CommonMark spec suite against `markdownToAdf` and `markdownToHtml`.
`markdownToAdf` and `markdownToHtml`.
Beside the corpus, properties run over documents generated from the node tables and over generated
markdown, on a fixed seed in the gate; `PROPERTY_RUNS=<runs>` raises the runs and randomizes the
seed for local digging, and a counterexample found becomes a round-trip fixture. The generators and
run parameters properties share live in `src/property-harness.ts`, outside the build and coverage.
`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
@@ -224,6 +245,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
@@ -316,17 +343,16 @@ worth deliberating; what matters is that nothing is left undone in the end. Per
gate result (commit and outcome); a reviewer does not re-run `ci.sh` or the tests when a gate result (commit and outcome); a reviewer does not re-run `ci.sh` or the tests when a
result exists for the commit under review, or when the diff since that result cannot affect result exists for the commit under review, or when the diff since that result cannot affect
it (docs-only) — re-run only what its own findings or fixes invalidate. it (docs-only) — re-run only what its own findings or fixes invalidate.
3. Merge the PR (standing authorization, this repo only, granted through the `0.1.0` release — 3. Merge the PR (standing authorization, this repo only, granted through the `0.2.0` release —
PR #3), check the box in `todo.md` and move the item's text to `todo-history.md`, leaving its the maintainer, 2026-09-13), check the box in `todo.md` and move the item's text to
title behind, report, stop. The next chunk gets a fresh session. `todo-history.md`, leaving its title behind, report, stop. The next chunk gets a fresh session.
Ask, don't guess: any choice where what the maintainer would pick is not near-certain gets asked, Ask, don't guess: any choice where what the maintainer would pick is not near-certain gets asked,
and the answer lands as a decision in this file. The confidence bar is very high — asking too and the answer lands as a decision in this file. The confidence bar is very high — asking too
often is the accepted cost, guessing wrong is not. often is the accepted cost, guessing wrong is not.
Reserved for the maintainer, never the agent: changing `version` in `package.json` (a bump on Reserved for the maintainer, never the agent: changing `version` in `package.json` (a bump on
`main` publishes, §9 — every release including `0.1.0` is the maintainer's), making the repo `main` publishes, §9 — every release is the maintainer's) and the `NPM_TOKEN` secret.
public, and creating the `NPM_TOKEN` secret.
A continuous loop session (`/loop`) counts as a chain of sessions: one chunk per iteration, each A continuous loop session (`/loop`) counts as a chain of sessions: one chunk per iteration, each
iteration starting by re-reading `AGENTS.md` and `todo.md` and trusting them over anything iteration starting by re-reading `AGENTS.md` and `todo.md` and trusting them over anything
+7 -2
View File
@@ -3,8 +3,8 @@
Lossless conversion between **Atlassian Document Format** (ADF), an extended markdown flavour, and Lossless conversion between **Atlassian Document Format** (ADF), an extended markdown flavour, and
an HTML dialect. an HTML dialect.
**Status: pre-release — the markdown round-trip (`adfToMarkdown`, `markdownToAdf`); HTML not **Status: published — the markdown round-trip (`adfToMarkdown`, `markdownToAdf`); HTML at
yet.** `0.3.0`.**
Plan: `todo.md`. Decisions: `AGENTS.md`. The flavour's grammar: Plan: `todo.md`. Decisions: `AGENTS.md`. The flavour's grammar:
[`spec/flavour.md`](spec/flavour.md). [`spec/flavour.md`](spec/flavour.md).
@@ -115,6 +115,11 @@ emit refuses:
library's canonical spelling, which round-trips byte-identically — where it converts back at library's canonical spelling, which round-trips byte-identically — where it converts back at
all: a parse succeeding is no promise of that, so keep the source until the way back succeeds. all: a parse succeeding is no promise of that, so keep the source until the way back succeeds.
`[a](/a\b)`, `<http://x?a=1&amp;b=2>` and `[a](/x&#10;y)` read cleanly and then refuse. `[a](/a\b)`, `<http://x?a=1&amp;b=2>` and `[a](/x&#10;y)` read cleanly and then refuse.
- Three CommonMark spellings parse without an error and build a document the reference renders
differently: `[](/url)` and `[]()` stay literal text against CommonMark's empty link, a list
continuing past a marker change stays one list against CommonMark's two, and a shortcut
reference matching its definition only under Unicode case folding stays unresolved. Each is
pinned `pending` in `corpus/commonmark-spec/exceptions.json`.
- Raw HTML in markdown input is an error result, never a silent drop — a tag, a comment and a - Raw HTML in markdown input is an error result, never a silent drop — a tag, a comment and a
processing instruction alike. ADF holds no raw-HTML node; the element mapping ships at `0.3.0`. processing instruction alike. ADF holds no raw-HTML node; the element mapping ships at `0.3.0`.
- Not every document converts back: `adfToMarkdown` is partial on valid ADF — a text node holding - Not every document converts back: `adfToMarkdown` is partial on valid ADF — a text node holding
+5
View File
@@ -3,6 +3,11 @@ try {
window.convertCorpus = (corpus) => ({ window.convertCorpus = (corpus) => ({
errors: corpus.errors.map(({ markdown, name }) => ({ name, parsed: markdownToAdf(markdown) })), errors: corpus.errors.map(({ markdown, name }) => ({ name, parsed: markdownToAdf(markdown) })),
normalization: corpus.normalization.map(({ markdown, name }) => ({ name, parsed: markdownToAdf(markdown) })), normalization: corpus.normalization.map(({ markdown, name }) => ({ name, parsed: markdownToAdf(markdown) })),
realPayloads: corpus.realPayloads.map(({ json, name }) => {
const adf = JSON.parse(json)
const emitted = adfToMarkdown(adf)
return { emitted, isDocument: isAdfDocument(adf), name, parsed: emitted.ok ? markdownToAdf(emitted.value) : undefined }
}),
roundTrip: corpus.roundTrip.map(({ json, markdown, name }) => { roundTrip: corpus.roundTrip.map(({ json, markdown, name }) => {
const adf = JSON.parse(json) const adf = JSON.parse(json)
return { emitted: adfToMarkdown(adf), isDocument: isAdfDocument(adf), name, parsed: markdownToAdf(markdown) } return { emitted: adfToMarkdown(adf), isDocument: isAdfDocument(adf), name, parsed: markdownToAdf(markdown) }
+17 -4
View File
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict'
import { readFileSync, readdirSync } from 'node:fs' import { readFileSync, readdirSync } from 'node:fs'
import { createServer } from 'node:http' import { createServer } from 'node:http'
import { extname, join } from 'node:path' import { extname, join } from 'node:path'
import { toEditorNormal } from '../dist/adf/editor-normal.js'
const contentTypes = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript' } const contentTypes = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript' }
const driver = 'http://127.0.0.1:4444' const driver = 'http://127.0.0.1:4444'
@@ -66,14 +67,16 @@ async function whenDriverAnswers() {
const corpus = { const corpus = {
errors: fixtureNames('errors', '.md').map((name) => ({ markdown: fixture(name, '.md'), name })), errors: fixtureNames('errors', '.md').map((name) => ({ markdown: fixture(name, '.md'), name })),
normalization: fixtureNames('normalization', '.md').map((name) => ({ markdown: fixture(name, '.md'), name })), normalization: fixtureNames('normalization', '.md').map((name) => ({ markdown: fixture(name, '.md'), name })),
realPayloads: fixtureNames('real-payloads', '.json').map((name) => ({ json: fixture(name, '.json'), name })),
roundTrip: fixtureNames('round-trip', '.json').map((name) => ({ json: fixture(name, '.json'), markdown: fixture(name, '.md'), name })), roundTrip: fixtureNames('round-trip', '.json').map((name) => ({ json: fixture(name, '.json'), markdown: fixture(name, '.md'), name })),
} }
assert.deepEqual( assert.deepEqual(
readdirSync(corpusRoot, { withFileTypes: true }) readdirSync(corpusRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory()) .filter((entry) => entry.isDirectory())
.map((entry) => entry.name) .map((entry) => entry.name)
.filter((name) => name !== 'commonmark-spec')
.sort(), .sort(),
['errors', 'normalization', 'round-trip'], ['errors', 'normalization', 'real-payloads', 'round-trip'],
'a corpus kind the browser leg does not convert', 'a corpus kind the browser leg does not convert',
) )
for (const [kind, fixtures] of Object.entries(corpus)) assert.ok(fixtures.length > 0, `the browser leg found no ${kind} fixture to convert`) for (const [kind, fixtures] of Object.entries(corpus)) assert.ok(fixtures.length > 0, `the browser leg found no ${kind} fixture to convert`)
@@ -104,7 +107,7 @@ for (const [index, { json, markdown, name }] of corpus.roundTrip.entries()) {
assert.ok(result.emitted.ok, `it did not emit — ${refusal(result.emitted)}`) assert.ok(result.emitted.ok, `it did not emit — ${refusal(result.emitted)}`)
assert.equal(result.emitted.value, markdown) assert.equal(result.emitted.value, markdown)
assert.ok(result.parsed.ok, `it did not parse — ${refusal(result.parsed)}`) assert.ok(result.parsed.ok, `it did not parse — ${refusal(result.parsed)}`)
assert.deepEqual(result.parsed.value, JSON.parse(json)) assert.deepEqual(toEditorNormal(result.parsed.value), JSON.parse(json))
}) })
} }
@@ -112,7 +115,17 @@ for (const [index, { name }] of corpus.normalization.entries()) {
const result = results.normalization[index] const result = results.normalization[index]
checking(name, result, () => { checking(name, result, () => {
assert.ok(result.parsed.ok, `it did not parse — ${refusal(result.parsed)}`) assert.ok(result.parsed.ok, `it did not parse — ${refusal(result.parsed)}`)
assert.deepEqual(result.parsed.value, JSON.parse(fixture(name, '.json'))) assert.deepEqual(toEditorNormal(result.parsed.value), JSON.parse(fixture(name, '.json')))
})
}
for (const [index, { json, name }] of corpus.realPayloads.entries()) {
const result = results.realPayloads[index]
checking(name, result, () => {
assert.ok(result.isDocument, `${name}.json is no ADF document`)
assert.ok(result.emitted.ok, `it did not emit — ${refusal(result.emitted)}`)
assert.ok(result.parsed.ok, `it did not parse back — ${refusal(result.parsed)}`)
assert.deepEqual(toEditorNormal(result.parsed.value), JSON.parse(json))
}) })
} }
@@ -125,5 +138,5 @@ for (const [index, { name }] of corpus.errors.entries()) {
} }
console.log( console.log(
`Firefox ${session.capabilities.browserVersion} converted ${corpus.roundTrip.length} round-trip, ${corpus.normalization.length} normalization and ${corpus.errors.length} error fixtures`, `Firefox ${session.capabilities.browserVersion} converted ${corpus.roundTrip.length} round-trip, ${corpus.normalization.length} normalization and ${corpus.errors.length} error fixtures and ${corpus.realPayloads.length} real payloads`,
) )
+1 -1
View File
@@ -16,7 +16,7 @@ if printf '%s' "$test_output" | grep -q 'ℹ tests 0'; then
exit 1 exit 1
fi fi
in_image "$deno_image" deno test --allow-read --no-check src/ in_image "$deno_image" deno test --allow-env=PROPERTY_RUNS --allow-read --no-check src/
in_image "$bun_image" bun test src/ in_image "$bun_image" bun test src/
in_image "$node_image" npm run build in_image "$node_image" npm run build
+13 -3
View File
@@ -10,7 +10,17 @@ One directory per contract kind, each landing with its milestone:
markdown is not canonical. markdown is not canonical.
- `errors/` — `<name>.md`: markdown input that must not convert. A `<name>.error` beside it - `errors/` — `<name>.md`: markdown input that must not convert. A `<name>.error` beside it
pins which error. pins which error.
- `real-payloads/` — `<name>.json`: sanitized live ADF, round-tripped ADF→markdown→ADF. No - `real-payloads/` — `<name>.json`: ADF Atlassian's editor wrote, round-tripped ADF→markdown→ADF.
expected markdown. No expected markdown.
- `commonmark-spec/` — the CommonMark suite run against `markdownToAdf` by three checks. `spec.json`
is the suite; `refusals.json` pins each refusing example to its error `code`; `exceptions.json`
pins each known divergence by `check`, `example`, `kind` and the exact `divergence`, with a
`reason`. `kind` is `mark-model` (the permanent count divergence from ADF's mark-per-text-node
model), `unspellable` (parses but the flavour has no spelling) or `pending` (a parser gap a later
milestone may close).
JSON is editor-normal (AGENTS.md §2), two-space indent, keys sorted. JSON is editor-normal (AGENTS.md §2), two-space indent, keys sorted. `spec.json` is the vendored,
upstream machine-readable suite, byte-exact from
[spec.commonmark.org](https://spec.commonmark.org/0.31.2/spec.json) (CommonMark 0.31.2, © John
MacFarlane, [CC-BY-SA-4.0](https://creativecommons.org/licenses/by-sa/4.0/)), and is not
re-serialized by the corpus gate.
+436
View File
@@ -0,0 +1,436 @@
[
{
"check": "fixpoint",
"divergence": "unspellable-link",
"example": 196,
"kind": "unspellable",
"reason": "The link title holds literal newlines no escape spells."
},
{
"check": "fixpoint",
"divergence": "unspellable-link",
"example": 202,
"kind": "unspellable",
"reason": "The link destination holds a backslash the flavour cannot escape."
},
{
"check": "count",
"divergence": "ul 2/1",
"example": 301,
"kind": "pending",
"reason": "A list continuing past a marker change renders as two lists, the parser opens one."
},
{
"check": "count",
"divergence": "ol 2/1",
"example": 302,
"kind": "pending",
"reason": "A list continuing past a marker change renders as two lists, the parser opens one."
},
{
"check": "fixpoint",
"divergence": "unspellable-line-start",
"example": 330,
"kind": "unspellable",
"reason": "A paragraph opens with a code span whose backticks read back as a fence."
},
{
"check": "fixpoint",
"divergence": "unspellable-line-start",
"example": 331,
"kind": "unspellable",
"reason": "A paragraph opens with a code span whose backticks read back as a fence."
},
{
"check": "fixpoint",
"divergence": "unspellable-line-start",
"example": 340,
"kind": "unspellable",
"reason": "A paragraph opens with a code span whose backticks read back as a fence."
},
{
"check": "count",
"divergence": "em 2/1",
"example": 369,
"kind": "mark-model",
"reason": "CommonMark nests same-kind elements; the single mark collapses them to one."
},
{
"check": "count",
"divergence": "em 2/1",
"example": 373,
"kind": "mark-model",
"reason": "CommonMark nests same-kind elements; the single mark collapses them to one."
},
{
"check": "count",
"divergence": "strong 2/1",
"example": 389,
"kind": "mark-model",
"reason": "CommonMark nests same-kind elements; the single mark collapses them to one."
},
{
"check": "count",
"divergence": "em 1/3",
"example": 393,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "strong 1/5",
"example": 394,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "strong 1/3",
"example": 395,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "em 1/3",
"example": 399,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "em 1/2",
"example": 404,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "em 1/3",
"example": 406,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "em 2/1",
"example": 407,
"kind": "mark-model",
"reason": "CommonMark nests same-kind elements; the single mark collapses them to one."
},
{
"check": "count",
"divergence": "em 2/1",
"example": 408,
"kind": "mark-model",
"reason": "CommonMark nests same-kind elements; the single mark collapses them to one."
},
{
"check": "count",
"divergence": "em 2/1",
"example": 409,
"kind": "mark-model",
"reason": "CommonMark nests same-kind elements; the single mark collapses them to one."
},
{
"check": "count",
"divergence": "em 1/3",
"example": 410,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "em 1/3",
"example": 411,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "em 1/2",
"example": 413,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "em 1/2",
"example": 414,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "em 1/2",
"example": 415,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "strong 3/1",
"example": 417,
"kind": "mark-model",
"reason": "CommonMark nests same-kind elements; the single mark collapses them to one."
},
{
"check": "count",
"divergence": "em 2/5 strong 1/3",
"example": 418,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "strong 1/2",
"example": 422,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "strong 1/3",
"example": 424,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "strong 2/1",
"example": 425,
"kind": "mark-model",
"reason": "CommonMark nests same-kind elements; the single mark collapses them to one."
},
{
"check": "count",
"divergence": "strong 2/1",
"example": 426,
"kind": "mark-model",
"reason": "CommonMark nests same-kind elements; the single mark collapses them to one."
},
{
"check": "count",
"divergence": "strong 2/1",
"example": 427,
"kind": "mark-model",
"reason": "CommonMark nests same-kind elements; the single mark collapses them to one."
},
{
"check": "count",
"divergence": "strong 1/3",
"example": 428,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "strong 1/3",
"example": 429,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "strong 1/2",
"example": 430,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "strong 1/2",
"example": 431,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "em 1/3 strong 2/5",
"example": 432,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "strong 1/2",
"example": 433,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "em 2/1",
"example": 461,
"kind": "mark-model",
"reason": "CommonMark nests same-kind elements; the single mark collapses them to one."
},
{
"check": "count",
"divergence": "em 2/1",
"example": 463,
"kind": "mark-model",
"reason": "CommonMark nests same-kind elements; the single mark collapses them to one."
},
{
"check": "count",
"divergence": "strong 2/1",
"example": 464,
"kind": "mark-model",
"reason": "CommonMark nests same-kind elements; the single mark collapses them to one."
},
{
"check": "count",
"divergence": "strong 2/1",
"example": 465,
"kind": "mark-model",
"reason": "CommonMark nests same-kind elements; the single mark collapses them to one."
},
{
"check": "count",
"divergence": "strong 3/1",
"example": 466,
"kind": "mark-model",
"reason": "CommonMark nests same-kind elements; the single mark collapses them to one."
},
{
"check": "count",
"divergence": "strong 2/1",
"example": 468,
"kind": "mark-model",
"reason": "CommonMark nests same-kind elements; the single mark collapses them to one."
},
{
"check": "count",
"divergence": "em 1/3",
"example": 470,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "em 1/2",
"example": 478,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "em 1/2",
"example": 479,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "a 1/0",
"example": 484,
"kind": "pending",
"reason": "An empty link text stays literal text; CommonMark renders an empty link."
},
{
"check": "text",
"divergence": "\"\" against \"[](./target.md)\"",
"example": 484,
"kind": "pending",
"reason": "An empty link text stays literal text; CommonMark renders an empty link."
},
{
"check": "count",
"divergence": "a 1/0",
"example": 487,
"kind": "pending",
"reason": "An empty link text stays literal text; CommonMark renders an empty link."
},
{
"check": "text",
"divergence": "\"\" against \"[]()\"",
"example": 487,
"kind": "pending",
"reason": "An empty link text stays literal text; CommonMark renders an empty link."
},
{
"check": "fixpoint",
"divergence": "unspellable-link",
"example": 502,
"kind": "unspellable",
"reason": "The link destination holds a backslash the flavour cannot escape."
},
{
"check": "count",
"divergence": "a 1/5 em 1/4",
"example": 516,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "em 1/3",
"example": 519,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "a 1/5 em 1/4",
"example": 530,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "em 1/2",
"example": 533,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "a 1/0",
"example": 540,
"kind": "pending",
"reason": "The case-folding shortcut reference is unresolved; CommonMark folds case and links."
},
{
"check": "text",
"divergence": "\"ẞ\" against \"[ẞ]\"",
"example": 540,
"kind": "pending",
"reason": "A case-folding shortcut reference is unresolved; CommonMark folds case and links."
},
{
"check": "count",
"divergence": "a 1/2",
"example": 554,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "a 1/2",
"example": 558,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "a 1/2",
"example": 559,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "em 1/2",
"example": 638,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
},
{
"check": "count",
"divergence": "em 1/2",
"example": 639,
"kind": "mark-model",
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
}
]
+346
View File
@@ -0,0 +1,346 @@
[
{
"code": "unmappable-html",
"example": 21
},
{
"code": "unmappable-html",
"example": 31
},
{
"code": "unmappable-html",
"example": 148
},
{
"code": "unmappable-html",
"example": 149
},
{
"code": "unmappable-html",
"example": 150
},
{
"code": "unmappable-html",
"example": 151
},
{
"code": "unmappable-html",
"example": 152
},
{
"code": "unmappable-html",
"example": 153
},
{
"code": "unmappable-html",
"example": 154
},
{
"code": "unmappable-html",
"example": 155
},
{
"code": "unmappable-html",
"example": 156
},
{
"code": "unmappable-html",
"example": 157
},
{
"code": "unmappable-html",
"example": 158
},
{
"code": "unmappable-html",
"example": 159
},
{
"code": "unmappable-html",
"example": 160
},
{
"code": "unmappable-html",
"example": 161
},
{
"code": "unmappable-html",
"example": 162
},
{
"code": "unmappable-html",
"example": 163
},
{
"code": "unmappable-html",
"example": 164
},
{
"code": "unmappable-html",
"example": 165
},
{
"code": "unmappable-html",
"example": 166
},
{
"code": "unmappable-html",
"example": 167
},
{
"code": "unmappable-html",
"example": 168
},
{
"code": "unmappable-html",
"example": 169
},
{
"code": "unmappable-html",
"example": 170
},
{
"code": "unmappable-html",
"example": 171
},
{
"code": "unmappable-html",
"example": 172
},
{
"code": "unmappable-html",
"example": 173
},
{
"code": "unmappable-html",
"example": 174
},
{
"code": "unmappable-html",
"example": 175
},
{
"code": "unmappable-html",
"example": 176
},
{
"code": "unmappable-html",
"example": 177
},
{
"code": "unmappable-html",
"example": 178
},
{
"code": "unmappable-html",
"example": 179
},
{
"code": "unmappable-html",
"example": 180
},
{
"code": "unmappable-html",
"example": 181
},
{
"code": "unmappable-html",
"example": 182
},
{
"code": "unmappable-html",
"example": 183
},
{
"code": "unmappable-html",
"example": 184
},
{
"code": "unmappable-html",
"example": 185
},
{
"code": "unmappable-html",
"example": 186
},
{
"code": "unmappable-html",
"example": 187
},
{
"code": "unmappable-html",
"example": 188
},
{
"code": "unmappable-html",
"example": 189
},
{
"code": "unmappable-html",
"example": 190
},
{
"code": "unmappable-html",
"example": 191
},
{
"code": "unmappable-html",
"example": 201
},
{
"code": "unmappable-html",
"example": 308
},
{
"code": "unmappable-html",
"example": 309
},
{
"code": "unmappable-html",
"example": 344
},
{
"code": "unmappable-html",
"example": 475
},
{
"code": "unmappable-html",
"example": 476
},
{
"code": "unmappable-html",
"example": 477
},
{
"code": "unmappable-html",
"example": 491
},
{
"code": "unmappable-html",
"example": 494
},
{
"code": "unmappable-image",
"example": 517
},
{
"code": "unmappable-html",
"example": 524
},
{
"code": "unmappable-image",
"example": 531
},
{
"code": "unmappable-html",
"example": 536
},
{
"code": "unmappable-image",
"example": 572
},
{
"code": "unmappable-image",
"example": 573
},
{
"code": "unmappable-image",
"example": 576
},
{
"code": "unmappable-image",
"example": 577
},
{
"code": "unmappable-image",
"example": 579
},
{
"code": "unmappable-image",
"example": 584
},
{
"code": "unmappable-image",
"example": 585
},
{
"code": "unmappable-image",
"example": 586
},
{
"code": "unmappable-image",
"example": 587
},
{
"code": "unmappable-image",
"example": 588
},
{
"code": "unmappable-image",
"example": 589
},
{
"code": "unmappable-image",
"example": 591
},
{
"code": "unmappable-html",
"example": 613
},
{
"code": "unmappable-html",
"example": 614
},
{
"code": "unmappable-html",
"example": 615
},
{
"code": "unmappable-html",
"example": 616
},
{
"code": "unmappable-html",
"example": 617
},
{
"code": "unmappable-html",
"example": 623
},
{
"code": "unmappable-html",
"example": 625
},
{
"code": "unmappable-html",
"example": 626
},
{
"code": "unmappable-html",
"example": 627
},
{
"code": "unmappable-html",
"example": 628
},
{
"code": "unmappable-html",
"example": 629
},
{
"code": "unmappable-html",
"example": 630
},
{
"code": "unmappable-html",
"example": 631
},
{
"code": "unmappable-html",
"example": 642
},
{
"code": "unmappable-html",
"example": 643
}
]
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
_a :adf{json="{\"type\":\"blockCard\"}"} b_ _a !adf:carry{json="{\"type\":\"blockCard\"}"} b_
+1 -1
View File
@@ -1 +1 @@
:adf{json="{"} !adf:carry{json="{"}
+1 -1
View File
@@ -1 +1 @@
:adf{json="[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"} !adf:carry{json="[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"}
@@ -1,3 +1,3 @@
:::adf :::carry
{"type":"rule"} {"type":"rule"}
::: :::
+1 -1
View File
@@ -1 +1 @@
Part :mention[**@Mikael**]{id=b1c2}. Part !adf:mention[**@Mikael**]{id=b1c2}.
+1 -1
View File
@@ -1 +1 @@
Part :mention[@A and more. Part !adf:mention[@A and more.
+1 -1
View File
@@ -1 +1 @@
Part :em[emphasised] here. Part !adf:em[emphasised] here.
@@ -0,0 +1 @@
malformed-directive
+1
View File
@@ -0,0 +1 @@
Part !adf: here.
@@ -0,0 +1 @@
unsupported-node-shape
@@ -0,0 +1 @@
!adf:status[!adf:carry{json="{\"attrs\":{\"localId\":\"x\"},\"text\":\"ok\",\"type\":\"text\"}"}]{color=red}
+1 -1
View File
@@ -1 +1 @@
Part :text{text=plain} here. Part !adf:text{text=plain} here.
File diff suppressed because one or more lines are too long
+173
View File
@@ -0,0 +1,173 @@
{
"content": [
{
"attrs": {
"level": 2,
"localId": "0d42e7ff24ba"
},
"content": [
{
"text": "Code blocks",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "a971506228e7"
},
"content": [
{
"text": "Run the setup once, then start the server with ",
"type": "text"
},
{
"marks": [
{
"type": "code"
}
],
"text": "npm start",
"type": "text"
},
{
"text": " and open the page.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"language": "javascript",
"localId": "eaf42046-6454-451d-ab13-8bd5ed2333bb",
"wrap": true
},
"content": [
{
"text": "function greet(name) {\n return `Hello, ${name}!`\n}",
"type": "text"
}
],
"type": "codeBlock"
},
{
"attrs": {
"localId": "d69616dee641"
},
"content": [
{
"text": "The same idea in Python:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"language": "python",
"localId": "2094b559-280a-49d5-9be3-182572e5d2d8",
"wrap": true
},
"content": [
{
"text": "def total(prices):\n return sum(prices)\n\nprint(total([3, 4.5]))",
"type": "text"
}
],
"type": "codeBlock"
},
{
"attrs": {
"localId": "abe1971d1f18"
},
"content": [
{
"text": "A query that lists visitors by city:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"language": "sql",
"localId": "5551dc8e-8fe0-455e-a9cf-22e48b589fe8",
"wrap": true
},
"content": [
{
"text": "SELECT name, city\nFROM visitors\nWHERE city = 'Lund'\nORDER BY name;",
"type": "text"
}
],
"type": "codeBlock"
},
{
"attrs": {
"localId": "51efdd2f1f02"
},
"content": [
{
"text": "Plain output, no language chosen:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "d0f83c92-c252-4a70-9e31-dfb5ac255cd1",
"wrap": true
},
"content": [
{
"text": "$ ./setup\nChecking folders... done\n 3 files copied",
"type": "text"
}
],
"type": "codeBlock"
},
{
"attrs": {
"localId": "3dac57e146fa"
},
"content": [
{
"text": "Keep ",
"type": "text"
},
{
"marks": [
{
"type": "code"
}
],
"text": ".env",
"type": "text"
},
{
"text": " files out of version control, and never paste a ",
"type": "text"
},
{
"marks": [
{
"type": "code"
}
],
"text": "password",
"type": "text"
},
{
"text": " into a ticket.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
+254
View File
@@ -0,0 +1,254 @@
{
"content": [
{
"attrs": {
"level": 2,
"localId": "0fcbc98aec1f"
},
"content": [
{
"text": "Where things stand",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "32b6266eb05c"
},
"content": [
{
"text": "Office move ",
"type": "text"
},
{
"attrs": {
"color": "blue",
"localId": "1ab19511-a0fa-41a1-b6d0-6729813c0526",
"style": "mixedCase",
"text": "In progress"
},
"type": "status"
},
{
"text": " since ",
"type": "text"
},
{
"attrs": {
"localId": "493624162706",
"timestamp": "1790812800000"
},
"type": "date"
},
{
"text": " , owned by ",
"type": "text"
},
{
"attrs": {
"accessLevel": "",
"id": "712020:78bf5058-bc76-4783-99af-6d70cb593cee",
"localId": "604060e1-e533-4019-aa28-21952dfdea19",
"text": "@Larvit Developer"
},
"type": "mention"
},
{
"text": " .",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "73a94328c89f"
},
"content": [
{
"text": "Furniture ",
"type": "text"
},
{
"attrs": {
"color": "green",
"localId": "7b27ff33-1483-452b-8982-311cf9429023",
"style": "mixedCase",
"text": "Done"
},
"type": "status"
},
{
"text": " , cabling ",
"type": "text"
},
{
"attrs": {
"color": "red",
"localId": "b6eb05a9-e0e7-4269-9cbd-e912640364a6",
"style": "mixedCase",
"text": "Blocked"
},
"type": "status"
},
{
"text": " , signage ",
"type": "text"
},
{
"attrs": {
"color": "yellow",
"localId": "ff7ed7fb-1846-48cb-b5ea-8be6b598ccc6",
"style": "mixedCase",
"text": "Waiting"
},
"type": "status"
},
{
"text": " , parking ",
"type": "text"
},
{
"attrs": {
"color": "purple",
"localId": "e9218ff4-6fcb-4e43-b36a-b1bef8a19376",
"style": "mixedCase",
"text": "Maybe"
},
"type": "status"
},
{
"text": " and keys ",
"type": "text"
},
{
"attrs": {
"color": "neutral",
"localId": "f1455340-2469-4bc6-a371-ed6b1f54386c",
"style": "mixedCase",
"text": "Not started"
},
"type": "status"
},
{
"text": " ",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "8d9e2dc70265"
},
"content": [
{
"attrs": {
"accessLevel": "",
"id": "712020:78bf5058-bc76-4783-99af-6d70cb593cee",
"localId": "ab5b7997-43ce-4238-b540-db3864e0965c",
"text": "@Larvit Developer"
},
"type": "mention"
},
{
"text": " please confirm the handover on ",
"type": "text"
},
{
"attrs": {
"localId": "77e2bdefa7fd",
"timestamp": "1795132800000"
},
"type": "date"
},
{
"text": " before lunch.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "f7fb8a7527a3"
},
"content": [
{
"text": "Great work so far ",
"type": "text"
},
{
"attrs": {
"id": "1f44d",
"localId": "8fe9a02e94ce",
"shortName": ":thumbsup:",
"text": "👍"
},
"type": "emoji"
},
{
"text": " and thanks for the snacks ",
"type": "text"
},
{
"attrs": {
"id": "1f389",
"localId": "c19496cc742f",
"shortName": ":tada:",
"text": "🎉"
},
"type": "emoji"
},
{
"text": " ",
"type": "text"
},
{
"attrs": {
"id": "1f600",
"localId": "c05a6757d0ee",
"shortName": ":grinning:",
"text": "😀"
},
"type": "emoji"
},
{
"text": " ",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "b75918031ebd"
},
"content": [
{
"text": "Deliveries go to:",
"type": "text"
},
{
"type": "hardBreak"
},
{
"text": "Building C, third floor",
"type": "text"
},
{
"type": "hardBreak"
},
{
"text": "Ring the bell twice",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
+527
View File
@@ -0,0 +1,527 @@
{
"content": [
{
"attrs": {
"level": 1,
"localId": "cfdcc54363d9"
},
"content": [
{
"text": "Project handbook",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "91177a27b0ff"
},
"content": [
{
"text": "Welcome to the handbook for the spring project.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"level": 2,
"localId": "7e7d9113a89b"
},
"content": [
{
"text": "Getting started",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "e65711ca96b6"
},
"content": [
{
"text": "Pack the following before the first day:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "b5c36c57fe79"
},
"content": [
{
"attrs": {
"localId": "1f4aa02de452"
},
"content": [
{
"attrs": {
"localId": "8993e22a13aa"
},
"content": [
{
"text": "Laptop",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "2b335a4504ab"
},
"content": [
{
"attrs": {
"localId": "c9180347caf0"
},
"content": [
{
"attrs": {
"localId": "60990e48da32"
},
"content": [
{
"text": "Charger",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "fc5d4c8a875d"
},
"content": [
{
"attrs": {
"localId": "3b8ec5318463"
},
"content": [
{
"text": "Mouse",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "bulletList"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "90f5f37a8a8f"
},
"content": [
{
"attrs": {
"localId": "90fab5a310c5"
},
"content": [
{
"text": "Notebook",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "46840b8bd24b",
"order": 1
},
"content": [
{
"attrs": {
"localId": "b4f06fbd15ef"
},
"content": [
{
"attrs": {
"localId": "93db4583bdba"
},
"content": [
{
"text": "Lined for notes",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "6904c80dd06d"
},
"content": [
{
"attrs": {
"localId": "0bc1c819d38d"
},
"content": [
{
"text": "Squared for sketches",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "orderedList"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "65bd0afce26e"
},
"content": [
{
"attrs": {
"localId": "a753e5ecdad3"
},
"content": [
{
"text": "Water bottle",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "bulletList"
},
{
"attrs": {
"level": 3,
"localId": "5b6a671499c5"
},
"content": [
{
"text": "Unpacking",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "f460abd3a962"
},
"content": [
{
"text": "The first two steps happen at reception, so ours start at three:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "baf0046a0432",
"order": 3
},
"content": [
{
"attrs": {
"localId": "b4df865a31a9"
},
"content": [
{
"attrs": {
"localId": "3f1b32f952d0"
},
"content": [
{
"text": "Unpack the boxes",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "f64930d87d4e"
},
"content": [
{
"attrs": {
"localId": "d953389c865a"
},
"content": [
{
"text": "Label the shelves",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "840daad9cb43"
},
"content": [
{
"attrs": {
"localId": "8708b8c7aeec"
},
"content": [
{
"text": "Recycle the cardboard",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "orderedList"
},
{
"attrs": {
"level": 4,
"localId": "27d1d408ae3e"
},
"content": [
{
"text": "Shelf labels",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "8b5cf239dbb5",
"order": 1
},
"content": [
{
"attrs": {
"localId": "0948562abdc3"
},
"content": [
{
"attrs": {
"localId": "4da9d4f45923"
},
"content": [
{
"text": "Top shelf",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "36edf97d8e2c"
},
"content": [
{
"attrs": {
"localId": "7915e252429c"
},
"content": [
{
"attrs": {
"localId": "9b15cc9fa8a1"
},
"content": [
{
"text": "Binders",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "efe607ab9e41"
},
"content": [
{
"attrs": {
"localId": "d559a2f29321"
},
"content": [
{
"text": "Archive boxes",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "bulletList"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "229692097876"
},
"content": [
{
"attrs": {
"localId": "928b1862c273"
},
"content": [
{
"text": "Bottom shelf",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "orderedList"
},
{
"attrs": {
"level": 5,
"localId": "353a84ef9126"
},
"content": [
{
"text": "Quiet hours",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "9c11b949ed48"
},
"content": [
{
"attrs": {
"localId": "bd576489e8b6"
},
"content": [
{
"text": "Good tools make good work, and quiet rooms make good thinking.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "727fda576a9d"
},
"content": [
{
"text": "Please keep calls in the booths.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "blockquote"
},
{
"attrs": {
"level": 6,
"localId": "b12277440bc7"
},
"content": [
{
"text": "Small print",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "9f6fbb5a19af"
},
"type": "rule"
},
{
"attrs": {
"localId": "8a6e253c110e"
},
"content": [
{
"text": "Office hours:",
"type": "text"
},
{
"type": "hardBreak"
},
{
"text": "Monday to Thursday, eight to five",
"type": "text"
},
{
"type": "hardBreak"
},
{
"text": "Friday, eight to three",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "b9c3f5daa373"
},
"content": [
{
"text": "That is all for now.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
+106
View File
@@ -0,0 +1,106 @@
{
"content": [
{
"attrs": {
"level": 2,
"localId": "d5c3a2503683"
},
"content": [
{
"text": "Media and attachments",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "f71287760586"
},
"content": [
{
"text": "A photo of the meadow behind the office:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"layout": "align-start",
"localId": "95de25bcdecc",
"width": 320,
"widthType": "pixel"
},
"content": [
{
"attrs": {
"alt": "Green meadow under a yellow sun",
"collection": "",
"height": 180,
"id": "8c586460-fb0d-44f6-949f-d800d5dd0f45",
"localId": "630d0d0457b8",
"type": "file",
"width": 320
},
"marks": [
{
"attrs": {
"color": "#091e4224",
"size": 2
},
"type": "border"
}
],
"type": "media"
}
],
"type": "mediaSingle"
},
{
"attrs": {
"localId": "b4bfee3ad3ec"
},
"type": "paragraph"
},
{
"attrs": {
"localId": "ffe5710ea995"
},
"content": [
{
"text": "The packing list is attached as a file:",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"attrs": {
"collection": "",
"id": "8dfd7046-2382-4377-b3ba-ec31e34cd01a",
"type": "file"
},
"type": "media"
}
],
"type": "mediaGroup"
},
{
"attrs": {
"localId": "77fcb609f709"
},
"content": [
{
"text": "Both were uploaded from the editor.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1,292 @@
{
"content": [
{
"attrs": {
"level": 2,
"localId": "d483ec27f809"
},
"content": [
{
"text": "Panels",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "cb02f8e9812b"
},
"content": [
{
"text": "Each colour of panel below holds a short note for the visiting team.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "b6d76c21e77e",
"panelType": "info"
},
"content": [
{
"attrs": {
"localId": "2d871d80f6e1"
},
"content": [
{
"text": "The workshop starts at nine in the morning.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "panel"
},
{
"attrs": {
"localId": "cbaab515445c",
"panelType": "note"
},
"content": [
{
"attrs": {
"localId": "13cafc26c42d"
},
"content": [
{
"text": "Bring your own laptop charger.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "panel"
},
{
"attrs": {
"localId": "c5fa8b34e2aa",
"panelType": "success"
},
"content": [
{
"attrs": {
"localId": "e186fca354d9"
},
"content": [
{
"text": "All rooms are booked for the week.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "panel"
},
{
"attrs": {
"localId": "ee223a0f886e",
"panelType": "warning"
},
"content": [
{
"attrs": {
"localId": "e0006fb2b972"
},
"content": [
{
"text": "The east entrance closes early on Fridays.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "ea6a56b9c863"
},
"content": [
{
"text": "Use the main door after four.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "panel"
},
{
"attrs": {
"localId": "fedbadc6d1c7",
"panelType": "error"
},
"content": [
{
"attrs": {
"localId": "c0f6122d81b7"
},
"content": [
{
"text": "The projector in room two is out of order.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "panel"
},
{
"attrs": {
"level": 2,
"localId": "afe638de11c5"
},
"content": [
{
"text": "Expands",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "e635bf2eac43",
"title": "Agenda for the first day"
},
"content": [
{
"attrs": {
"localId": "c8beb4c392ee"
},
"content": [
{
"attrs": {
"localId": "6d6ff472d878"
},
"content": [
{
"attrs": {
"localId": "d1186101c5a1"
},
"content": [
{
"text": "Welcome and introductions",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "01d203b97343"
},
"content": [
{
"attrs": {
"localId": "61bfec91218d"
},
"content": [
{
"text": "Tour of the building",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "004e47604c2a"
},
"content": [
{
"attrs": {
"localId": "726aa40e1e38"
},
"content": [
{
"text": "Lunch in the courtyard",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "bulletList"
}
],
"type": "expand"
},
{
"attrs": {
"localId": "cb6e48b9d746"
},
"content": [
{
"text": "Questions go to the organisers before the event.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "32a8832682b2",
"title": "Travel details"
},
"content": [
{
"attrs": {
"localId": "00b05814cccb"
},
"content": [
{
"text": "Trains leave the central station every twenty minutes.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "eec0df0ed4f1",
"panelType": "note"
},
"content": [
{
"attrs": {
"localId": "712c49fa7ecc"
},
"content": [
{
"text": "Tickets are cheaper before seven.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "panel"
}
],
"type": "expand"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1,630 @@
{
"content": [
{
"attrs": {
"localId": "3b911072ce78"
},
"content": [
{
"text": "Summary of the planning session on Thursday. Attendees: ",
"type": "text"
},
{
"attrs": {
"accessLevel": "",
"id": "712020:78bf5058-bc76-4783-99af-6d70cb593cee",
"localId": "c8b64b9f-d396-4525-9456-38cf690ade1e",
"text": "@Larvit Developer"
},
"type": "mention"
},
{
"text": " and two guests from the design team.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"level": 2,
"localId": "3e97ef36cbc3"
},
"content": [
{
"text": "Where we are",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "530e0e3aa621"
},
"content": [
{
"text": "The beta is ",
"type": "text"
},
{
"attrs": {
"color": "green",
"localId": "ac0975eb-fafe-44ad-8293-8a12fdbcaebc",
"style": "mixedCase",
"text": "On track"
},
"type": "status"
},
{
"text": " and the public launch is set for ",
"type": "text"
},
{
"attrs": {
"localId": "84d9a1cde860",
"timestamp": "1793664000000"
},
"type": "date"
},
{
"text": " .",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "ffcd53578640",
"panelType": "warning"
},
"content": [
{
"attrs": {
"localId": "0f0e8b9e13bb"
},
"content": [
{
"text": "Translation files are still ",
"type": "text"
},
{
"marks": [
{
"type": "strong"
}
],
"text": "missing",
"type": "text"
},
{
"text": " for two languages.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "panel"
},
{
"attrs": {
"level": 2,
"localId": "d9b590d479ad"
},
"content": [
{
"text": "Decisions",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "69dc2d52-34f7-43bb-8d6c-36d52a47179e"
},
"content": [
{
"attrs": {
"localId": "852a35a7-f660-4161-85dd-2d54adaf662b",
"state": "DECIDED"
},
"content": [
{
"text": "Ship the beta to internal users first",
"type": "text"
}
],
"type": "decisionItem"
},
{
"attrs": {
"localId": "2df16457-331f-4d26-90ba-539f82949d70",
"state": "DECIDED"
},
"content": [
{
"text": "Keep the old export for one more release",
"type": "text"
}
],
"type": "decisionItem"
}
],
"type": "decisionList"
},
{
"attrs": {
"level": 2,
"localId": "7813370b5e89"
},
"content": [
{
"text": "Next steps",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "21a7a33c-199d-4396-a148-c8404f70554f"
},
"content": [
{
"attrs": {
"localId": "5343dfdb-e70d-4ce6-bca5-8a382123c7bd",
"state": "TODO"
},
"content": [
{
"text": "Draft the release notes",
"type": "text"
}
],
"type": "taskItem"
},
{
"attrs": {
"localId": "67d65eb8-17e9-4cd6-93cb-7d495f84e01d",
"state": "TODO"
},
"content": [
{
"text": "Book the demo room",
"type": "text"
}
],
"type": "taskItem"
}
],
"type": "taskList"
},
{
"attrs": {
"level": 2,
"localId": "e5806b037d61"
},
"content": [
{
"text": "Numbers",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"isNumberColumnEnabled": false,
"layout": "align-start",
"localId": "cbae6bea-3c35-42b9-9a8f-3b37749e1a87"
},
"content": [
{
"attrs": {
"localId": "e4d343447bf5"
},
"content": [
{
"attrs": {
"localId": "4fcec7ad0a0f"
},
"content": [
{
"attrs": {
"localId": "974849d6e63c"
},
"content": [
{
"marks": [
{
"type": "strong"
}
],
"text": "Metric",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableHeader"
},
{
"attrs": {
"localId": "42df5203769e"
},
"content": [
{
"attrs": {
"localId": "8b0da0854a0a"
},
"content": [
{
"marks": [
{
"type": "strong"
}
],
"text": "Beta",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableHeader"
},
{
"attrs": {
"localId": "cbdae6c8508d"
},
"content": [
{
"attrs": {
"localId": "2584f222d6c3"
},
"content": [
{
"marks": [
{
"type": "strong"
}
],
"text": "Goal",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableHeader"
}
],
"type": "tableRow"
},
{
"attrs": {
"localId": "65e4d7a700c0"
},
"content": [
{
"attrs": {
"localId": "a37175350eea"
},
"content": [
{
"attrs": {
"localId": "7efe32972497"
},
"content": [
{
"text": "Sign-ups",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
},
{
"attrs": {
"localId": "e0a95cdbd3c5"
},
"content": [
{
"attrs": {
"localId": "4628a3afe71b"
},
"content": [
{
"text": "140",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
},
{
"attrs": {
"localId": "b802fc652961"
},
"content": [
{
"attrs": {
"localId": "94c6ec93acb2"
},
"content": [
{
"text": "200",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
}
],
"type": "tableRow"
},
{
"attrs": {
"localId": "bd6465cc546d"
},
"content": [
{
"attrs": {
"localId": "b6dcef3c6ac5"
},
"content": [
{
"attrs": {
"localId": "ba5c99353d30"
},
"content": [
{
"text": "Crash reports",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
},
{
"attrs": {
"localId": "554b7b5a8e71"
},
"content": [
{
"attrs": {
"localId": "569a134488bc"
},
"content": [
{
"text": "3",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
},
{
"attrs": {
"localId": "fb03db2017b1"
},
"content": [
{
"attrs": {
"localId": "c0ce0ada482c"
},
"content": [
{
"text": "0",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
}
],
"type": "tableRow"
}
],
"type": "table"
},
{
"attrs": {
"localId": "b4eb1362eb9f"
},
"content": [
{
"text": "The flag that turns the beta on:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"language": "yaml",
"localId": "14298a36-6632-458f-a548-7fccd4b7186b",
"wrap": true
},
"content": [
{
"text": "beta:\n enabled: true\n audience: internal",
"type": "text"
}
],
"type": "codeBlock"
},
{
"attrs": {
"localId": "a98274e22eaa"
},
"content": [
{
"text": "Risks we talked about:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "fb3e387688ff"
},
"content": [
{
"attrs": {
"localId": "fdeee4ce0678"
},
"content": [
{
"attrs": {
"localId": "5ff2e05f1c7e"
},
"content": [
{
"text": "Late translations",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "043a7ded718e"
},
"content": [
{
"attrs": {
"localId": "73a4d8e4ef0d"
},
"content": [
{
"text": "The holiday week",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "3e32c1b868a1"
},
"content": [
{
"attrs": {
"localId": "98969cd5548e"
},
"content": [
{
"attrs": {
"localId": "3b9989413325"
},
"content": [
{
"text": "Half the team is away",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "bulletList"
}
],
"type": "listItem"
}
],
"type": "bulletList"
},
{
"attrs": {
"localId": "ba7d8422f4c6"
},
"content": [
{
"attrs": {
"localId": "a6837cfad3f5"
},
"content": [
{
"text": "Small steps, shipped often.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "blockquote"
},
{
"attrs": {
"localId": "6ec9ef8df224"
},
"content": [
{
"text": "Thanks, everyone ",
"type": "text"
},
{
"attrs": {
"id": "1f389",
"localId": "2481c60576e0",
"shortName": ":tada:",
"text": "🎉"
},
"type": "emoji"
},
{
"text": " ",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "782129bf30c1",
"title": "Raw notes"
},
"content": [
{
"attrs": {
"localId": "e9614e389875"
},
"content": [
{
"text": "We spent twenty minutes on the colour of the launch banner.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "expand"
}
],
"type": "doc",
"version": 1
}
+118
View File
@@ -0,0 +1,118 @@
{
"content": [
{
"attrs": {
"localId": "3e9517d02129"
},
"content": [
{
"text": "Looks good to me. Two small things:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "5738867bf15d",
"order": 1
},
"content": [
{
"attrs": {
"localId": "ccd8d283e36d"
},
"content": [
{
"attrs": {
"localId": "638111edfab4"
},
"content": [
{
"text": "Rename ",
"type": "text"
},
{
"marks": [
{
"type": "code"
}
],
"text": "beta.enabled",
"type": "text"
},
{
"text": " before the launch",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "0adfb25bb321"
},
"content": [
{
"attrs": {
"localId": "980750fad0fe"
},
"content": [
{
"text": "Move the demo to the ",
"type": "text"
},
{
"marks": [
{
"type": "em"
}
],
"text": "large",
"type": "text"
},
{
"text": " room",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "orderedList"
},
{
"attrs": {
"localId": "40e080c7acd4"
},
"content": [
{
"text": "Thanks ",
"type": "text"
},
{
"attrs": {
"accessLevel": "",
"id": "712020:78bf5058-bc76-4783-99af-6d70cb593cee",
"localId": "c4e073cd-8b3a-48b2-93a0-ade57b4ec2cf",
"text": "@Larvit Developer"
},
"type": "mention"
},
{
"text": " !",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
+124
View File
@@ -0,0 +1,124 @@
{
"content": [
{
"attrs": {
"level": 2,
"localId": "e632994a13ae"
},
"content": [
{
"text": "Smart links",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "dcfa975dde87"
},
"content": [
{
"text": "An inline card sits in a sentence, like ",
"type": "text"
},
{
"attrs": {
"localId": "042b9a548674",
"url": "https://en.wikipedia.org/wiki/Markdown"
},
"type": "inlineCard"
},
{
"text": " which keeps the text flowing.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "c138d931a131"
},
"content": [
{
"text": "A block card stands on its own line:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "9e527ae5f2bd"
},
"type": "paragraph"
},
{
"attrs": {
"localId": "88bd10974ec6",
"url": "https://spec.commonmark.org/"
},
"type": "blockCard"
},
{
"attrs": {
"localId": "9879bb1368ea"
},
"content": [
{
"text": " An embed shows the page itself, where the site allows it:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "ccbed18139bb"
},
"type": "paragraph"
},
{
"attrs": {
"layout": "wide",
"localId": "8320cf243168",
"originalHeight": 480,
"originalWidth": 853.344,
"url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
"width": 100
},
"type": "embedCard"
},
{
"attrs": {
"localId": "22f60b236c1c"
},
"content": [
{
"text": " And a plain address stays a link: ",
"type": "text"
},
{
"marks": [
{
"attrs": {
"href": "https://www.markdownguide.org/"
},
"type": "link"
}
],
"text": "https://www.markdownguide.org/",
"type": "text"
},
{
"text": " for reference.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
+124
View File
@@ -0,0 +1,124 @@
{
"content": [
{
"attrs": {
"level": 2,
"localId": "3265660b5792"
},
"content": [
{
"text": "Shared checklist",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "c7d94ae59a47"
},
"content": [
{
"text": "The block below is synced, so an edit shows up wherever it is used.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "72651b28-757c-44a1-be0b-586c3e6df546",
"resourceId": "03a667ab-4c6b-402d-adae-0c55582e32eb"
},
"content": [
{
"attrs": {
"localId": "0eb44ec3924b"
},
"content": [
{
"text": "Before every release:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "4beeefa0c6fa"
},
"content": [
{
"attrs": {
"localId": "07d19212447f"
},
"content": [
{
"attrs": {
"localId": "2fd9fc0e5d71"
},
"content": [
{
"text": "Run the tests",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "7f5f50f7642c"
},
"content": [
{
"attrs": {
"localId": "14d3435ce5f4"
},
"content": [
{
"text": "Update the changelog",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "bulletList"
},
{
"attrs": {
"localId": "e405ebea5e00"
},
"content": [
{
"text": "Ask for a second pair of eyes.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "bodiedSyncBlock"
},
{
"attrs": {
"localId": "73b5f0a1ef05"
},
"content": [
{
"text": "Anything specific to this release goes below the synced block.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
+620
View File
@@ -0,0 +1,620 @@
{
"content": [
{
"attrs": {
"level": 2,
"localId": "a0c750451c14"
},
"content": [
{
"text": "A simple table",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "944306a6da33"
},
"content": [
{
"text": "Rooms for the week:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"isNumberColumnEnabled": false,
"layout": "align-start",
"localId": "92b28760-e51b-436d-82f0-45c6b6bf5b60"
},
"content": [
{
"attrs": {
"localId": "d53d5ad740cb"
},
"content": [
{
"attrs": {
"localId": "a8c82e01543e"
},
"content": [
{
"attrs": {
"localId": "00c1bde1a44d"
},
"content": [
{
"marks": [
{
"type": "strong"
}
],
"text": "Room",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableHeader"
},
{
"attrs": {
"localId": "c736c185bcd0"
},
"content": [
{
"attrs": {
"localId": "3dc846d8aaf9"
},
"content": [
{
"marks": [
{
"type": "strong"
}
],
"text": "Capacity",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableHeader"
},
{
"attrs": {
"localId": "b484834a890d"
},
"content": [
{
"attrs": {
"localId": "5b0671aadeed"
},
"content": [
{
"marks": [
{
"type": "strong"
}
],
"text": "Notes",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableHeader"
}
],
"type": "tableRow"
},
{
"attrs": {
"localId": "45395ea36f04"
},
"content": [
{
"attrs": {
"localId": "8b19b08b6a93"
},
"content": [
{
"attrs": {
"localId": "363cf87d44be"
},
"content": [
{
"text": "North",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
},
{
"attrs": {
"localId": "e189034484f3"
},
"content": [
{
"attrs": {
"localId": "42a5bad514ea"
},
"content": [
{
"text": "12",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
},
{
"attrs": {
"localId": "9cfc78f9a553"
},
"content": [
{
"attrs": {
"localId": "2ae747c00211"
},
"content": [
{
"text": "Projector",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
}
],
"type": "tableRow"
},
{
"attrs": {
"localId": "8bc7abba3229"
},
"content": [
{
"attrs": {
"localId": "0e7980a31697"
},
"content": [
{
"attrs": {
"localId": "a001411b475e"
},
"content": [
{
"text": "South",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
},
{
"attrs": {
"localId": "e61493f2b3d9"
},
"content": [
{
"attrs": {
"localId": "79c522faf7e8"
},
"content": [
{
"text": "8",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
},
{
"attrs": {
"localId": "db911772081a"
},
"content": [
{
"attrs": {
"localId": "6721e0a4f29f"
},
"content": [
{
"text": "Quiet corner",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
}
],
"type": "tableRow"
}
],
"type": "table"
},
{
"attrs": {
"level": 2,
"localId": "c21221cbaeda"
},
"content": [
{
"text": "A richer table",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "b9cdc4e693ca"
},
"content": [
{
"text": "Budget by season, with merged, coloured and resized cells:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"isNumberColumnEnabled": false,
"layout": "align-start",
"localId": "ac1b1f4b-3d7a-4cd2-a0a7-05ece633ba69"
},
"content": [
{
"attrs": {
"localId": "c411338ed251"
},
"content": [
{
"attrs": {
"background": "#b3d4ff",
"colwidth": [
135
],
"localId": "798f1dea331f"
},
"content": [
{
"attrs": {
"localId": "b605b3c1f598"
},
"content": [
{
"marks": [
{
"type": "strong"
}
],
"text": "Item",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableHeader"
},
{
"attrs": {
"colwidth": [
233
],
"localId": "db4768c076d4"
},
"content": [
{
"attrs": {
"localId": "b4890b2d833e"
},
"content": [
{
"marks": [
{
"type": "strong"
}
],
"text": "Spring",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableHeader"
},
{
"attrs": {
"colwidth": [
184
],
"localId": "4dde2fbfb921"
},
"content": [
{
"attrs": {
"localId": "7986b41ed947"
},
"content": [
{
"marks": [
{
"type": "strong"
}
],
"text": "Autumn",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableHeader"
}
],
"type": "tableRow"
},
{
"attrs": {
"localId": "b4ac97bec992"
},
"content": [
{
"attrs": {
"colwidth": [
135
],
"localId": "ba92b57558d8"
},
"content": [
{
"attrs": {
"localId": "f23f030e9233"
},
"content": [
{
"text": "Venue",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableHeader"
},
{
"attrs": {
"background": "#abf5d1",
"colspan": 2,
"colwidth": [
233,
184
],
"localId": "51af31d44baa"
},
"content": [
{
"attrs": {
"localId": "ba55f45dc035"
},
"content": [
{
"text": "Booked for both",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
}
],
"type": "tableRow"
},
{
"attrs": {
"localId": "e911af70b656"
},
"content": [
{
"attrs": {
"colwidth": [
135
],
"localId": "50a820456456"
},
"content": [
{
"attrs": {
"localId": "b04d0749aee7"
},
"content": [
{
"text": "Catering",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableHeader"
},
{
"attrs": {
"colwidth": [
233
],
"localId": "d0cc276de239"
},
"content": [
{
"attrs": {
"localId": "90211ae7a504"
},
"content": [
{
"text": "400",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
},
{
"attrs": {
"colwidth": [
184
],
"localId": "1962d05e8763"
},
"content": [
{
"attrs": {
"localId": "221cf059e38d"
},
"content": [
{
"text": "450",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
}
],
"type": "tableRow"
},
{
"attrs": {
"localId": "2c8a06942ac4"
},
"content": [
{
"attrs": {
"colwidth": [
135
],
"localId": "ba4e1cb93e92"
},
"content": [
{
"attrs": {
"localId": "e6bb346c5fd4"
},
"content": [
{
"text": "Notes",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableHeader"
},
{
"attrs": {
"colwidth": [
233
],
"localId": "d7a1dec97b03"
},
"content": [
{
"attrs": {
"localId": "5cbd8813deab"
},
"content": [
{
"text": "Spring is confirmed.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
},
{
"attrs": {
"colwidth": [
184
],
"localId": "85f9a8dab00b"
},
"content": [
{
"attrs": {
"localId": "da90346e9a26",
"title": "Payment terms"
},
"content": [
{
"attrs": {
"localId": "ef6c6f374bca"
},
"content": [
{
"text": "Invoices arrive monthly.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "nestedExpand"
}
],
"type": "tableCell"
}
],
"type": "tableRow"
}
],
"type": "table"
},
{
"attrs": {
"localId": "10d03e65eff2"
},
"content": [
{
"text": "Figures are estimates.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1,195 @@
{
"content": [
{
"attrs": {
"level": 2,
"localId": "26d50a5240c3"
},
"content": [
{
"text": "Action items",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "f8b236b2-3b7c-412a-a0df-6c523af43a76"
},
"content": [
{
"attrs": {
"localId": "eebe3cb9-5773-4753-b6a3-5a5df104b6e8",
"state": "DONE"
},
"content": [
{
"text": "Book the meeting room",
"type": "text"
}
],
"type": "taskItem"
},
{
"attrs": {
"localId": "2263a39a-c49c-4711-8f00-cca6e8f3ea35",
"state": "TODO"
},
"content": [
{
"text": "Order name badges",
"type": "text"
}
],
"type": "taskItem"
},
{
"attrs": {
"localId": "e323e762-8880-43c0-945d-17a7341fdefb"
},
"content": [
{
"attrs": {
"localId": "55523903-9f79-4711-b276-ec1bc98be977",
"state": "DONE"
},
"content": [
{
"text": "Print a spare set for the front desk",
"type": "text"
}
],
"type": "taskItem"
}
],
"type": "taskList"
},
{
"attrs": {
"localId": "680d665a-a859-4345-b032-d5a46fd6db0d",
"state": "TODO"
},
"content": [
{
"text": "Send the agenda to everyone",
"type": "text"
}
],
"type": "taskItem"
},
{
"attrs": {
"localId": "48d45b93-2180-4b80-b0ed-f9eb458ed1ed",
"state": "TODO"
},
"content": [
{
"text": "Ask ",
"type": "text"
},
{
"attrs": {
"accessLevel": "",
"id": "712020:78bf5058-bc76-4783-99af-6d70cb593cee",
"localId": "09321e9b-b551-48ee-8868-5339e8b86863",
"text": "@Larvit Developer"
},
"type": "mention"
},
{
"text": " about parking by ",
"type": "text"
},
{
"attrs": {
"localId": "16c6b2a1718e",
"timestamp": "1792022400000"
},
"type": "date"
},
{
"text": " ",
"type": "text"
}
],
"type": "taskItem"
}
],
"type": "taskList"
},
{
"attrs": {
"localId": "90a7d38e3ed7"
},
"type": "paragraph"
},
{
"attrs": {
"localId": "8c510497da2e"
},
"type": "paragraph"
},
{
"attrs": {
"level": 2,
"localId": "3b52fc685167"
},
"content": [
{
"text": "Decisions",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "82dbc28b-49c8-428c-8c21-65cc000ef7c5"
},
"content": [
{
"attrs": {
"localId": "2f7a7680-2c41-4b45-9162-29d97f922a7b",
"state": "DECIDED"
},
"content": [
{
"text": "Hold the kickoff on the second floor",
"type": "text"
}
],
"type": "decisionItem"
},
{
"attrs": {
"localId": "c94ef59b-6db4-4ddc-8d22-576821020306",
"state": "DECIDED"
},
"content": [
{
"text": "Keep the budget under the agreed limit",
"type": "text"
}
],
"type": "decisionItem"
}
],
"type": "decisionList"
},
{
"attrs": {
"localId": "562121f961a8"
},
"content": [
{
"text": "Done items are ticked; the rest are still open.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
+288
View File
@@ -0,0 +1,288 @@
{
"content": [
{
"attrs": {
"level": 2,
"localId": "ef39f25be439"
},
"content": [
{
"text": "Text formatting",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "4a24d2441f4b"
},
"content": [
{
"text": "This sentence has ",
"type": "text"
},
{
"marks": [
{
"type": "strong"
}
],
"text": "bold",
"type": "text"
},
{
"text": ", ",
"type": "text"
},
{
"marks": [
{
"type": "em"
}
],
"text": "italic",
"type": "text"
},
{
"text": ", ",
"type": "text"
},
{
"marks": [
{
"type": "underline"
}
],
"text": "underlined",
"type": "text"
},
{
"text": ", ",
"type": "text"
},
{
"marks": [
{
"type": "strike"
}
],
"text": "struck",
"type": "text"
},
{
"text": " and ",
"type": "text"
},
{
"marks": [
{
"type": "code"
}
],
"text": "inline code",
"type": "text"
},
{
"text": " words in it.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "707e9aa736b2"
},
"content": [
{
"text": "Water is H",
"type": "text"
},
{
"marks": [
{
"attrs": {
"type": "sub"
},
"type": "subsup"
}
],
"text": "2",
"type": "text"
},
{
"text": "O and the room measures 20 m",
"type": "text"
},
{
"marks": [
{
"attrs": {
"type": "sup"
},
"type": "subsup"
}
],
"text": "2",
"type": "text"
},
{
"text": ".",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "a03bd1fe3442"
},
"content": [
{
"text": "Some words are ",
"type": "text"
},
{
"marks": [
{
"attrs": {
"color": "#ff5630"
},
"type": "textColor"
}
],
"text": "red",
"type": "text"
},
{
"text": ", some are ",
"type": "text"
},
{
"marks": [
{
"attrs": {
"color": "#0747a6"
},
"type": "textColor"
}
],
"text": "bold blue",
"type": "text"
},
{
"text": ", and one is ",
"type": "text"
},
{
"marks": [
{
"attrs": {
"color": "#f8e6a0"
},
"type": "backgroundColor"
}
],
"text": "highlighted",
"type": "text"
},
{
"text": ".",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "f3a744347f56"
},
"content": [
{
"text": "A ",
"type": "text"
},
{
"marks": [
{
"type": "em"
},
{
"type": "strong"
}
],
"text": "bold italic",
"type": "text"
},
{
"text": " phrase, and a ",
"type": "text"
},
{
"marks": [
{
"type": "strike"
},
{
"type": "underline"
}
],
"text": "struck underlined",
"type": "text"
},
{
"text": " one.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "643a019d97c1"
},
"content": [
{
"text": "Read the ",
"type": "text"
},
{
"marks": [
{
"attrs": {
"href": "https://spec.commonmark.org/0.31.2/"
},
"type": "link"
}
],
"text": "CommonMark spec",
"type": "text"
},
{
"text": " or the ",
"type": "text"
},
{
"marks": [
{
"attrs": {
"href": "https://www.markdownguide.org/basic-syntax/"
},
"type": "link"
}
],
"text": "basic syntax guide",
"type": "text"
},
{
"text": " before you edit.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
+2 -2
View File
@@ -2,5 +2,5 @@
| --- | --- | | --- | --- |
| Bolt M8 | Grade `8.8` | | Bolt M8 | Grade `8.8` |
| Nut \| washer | Sold as a pair | | Nut \| washer | Sold as a pair |
| Washer M8 | 100 pcs:hardBreak{}zinc-plated | | Washer M8 | 100 pcs!adf:hardBreak{}zinc-plated |
| :text{text=" "}Spare | | | !adf:text{text=" "}Spare | |
@@ -1,7 +1,7 @@
a `b :inlineCard{url="c\u0060d"} a `b !adf:inlineCard{url="c\u0060d"}
:inlineCard{url="https://example.com/s?q=a\u0026amp;b\u003cc"} !adf:inlineCard{url="https://example.com/s?q=a\u0026amp;b\u003cc"}
::extension {extensionKey=jira parameters="{\"jql\":\"a\u007cb \u0060c\u0060 \u0026 d \u003c e\"}"} ::extension {extensionKey=jira parameters="{\"jql\":\"a\u007cb \u0060c\u0060 \u0026 d \u003c e\"}"}
[a:inlineCard{url="b](c)"} [a!adf:inlineCard{url="b](c)"}
@@ -38,4 +38,4 @@
} }
``` ```
See :adf{json="{\"attrs\":{\"localId\":5,\"text\":\"In review\"},\"type\":\"status\"}"} now. See !adf:carry{json="{\"attrs\":{\"localId\":5,\"text\":\"In review\"},\"type\":\"status\"}"} now.
@@ -1,11 +1,11 @@
Reviewed :adf{json="{\"marks\":[{\"attrs\":{\"annotationType\":\"inlineComment\",\"id\":\"0198f3a2-7c41-7f2e-9b3a-4d8e2c1a6b90\"},\"type\":\"annotation\"}],\"text\":\"audited\",\"type\":\"text\"}"} twice. Reviewed !adf:carry{json="{\"marks\":[{\"attrs\":{\"annotationType\":\"inlineComment\",\"id\":\"0198f3a2-7c41-7f2e-9b3a-4d8e2c1a6b90\"},\"type\":\"annotation\"}],\"text\":\"audited\",\"type\":\"text\"}"} twice.
:adf{json="{\"marks\":[{\"attrs\":{\"colour\":\"red\"},\"type\":\"em\"}],\"text\":\"urgent\",\"type\":\"text\"}"} !adf:carry{json="{\"marks\":[{\"attrs\":{\"colour\":\"red\"},\"type\":\"em\"}],\"text\":\"urgent\",\"type\":\"text\"}"}
:adf{json="{\"marks\":[{\"type\":\"code\"},{\"type\":\"strong\"}],\"text\":\"flag\",\"type\":\"text\"}"} !adf:carry{json="{\"marks\":[{\"type\":\"code\"},{\"type\":\"strong\"}],\"text\":\"flag\",\"type\":\"text\"}"}
:adf{json="{\"marks\":[{\"type\":\"code\"}],\"text\":\"a\\nb\",\"type\":\"text\"}"} !adf:carry{json="{\"marks\":[{\"type\":\"code\"}],\"text\":\"a\\nb\",\"type\":\"text\"}"}
:adf{json="{\"marks\":[{\"type\":\"link\"}],\"text\":\"Release notes\",\"type\":\"text\"}"} !adf:carry{json="{\"marks\":[{\"type\":\"link\"}],\"text\":\"Release notes\",\"type\":\"text\"}"}
a:adf{json="{\"marks\":[{\"type\":\"em\"}],\"text\":\".\",\"type\":\"text\"}"}b:adf{json="{\"marks\":[{\"type\":\"strong\"}],\"text\":\",\",\"type\":\"text\"}"}c a!adf:carry{json="{\"marks\":[{\"type\":\"em\"}],\"text\":\".\",\"type\":\"text\"}"}b!adf:carry{json="{\"marks\":[{\"type\":\"strong\"}],\"text\":\",\",\"type\":\"text\"}"}c
@@ -0,0 +1,57 @@
{
"content": [
{
"content": [
{
"attrs": {
"localId": "01a0a067-68cf-78af-abd6-c660ec0d189b"
},
"text": "Owner",
"type": "text"
},
{
"text": " signs off.",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"text": "Signed by ",
"type": "text"
},
{
"attrs": {
"localId": "01a0a067-68d2-787e-afc5-2459776aa029"
},
"text": "the owner",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"attrs": {
"localId": "01a0a067-68d5-7372-9962-29a039654056"
},
"text": "One ",
"type": "text"
},
{
"attrs": {
"localId": "01a0a067-68d5-7372-9962-29a039654056"
},
"text": "anchor",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1,5 @@
!adf:carry{json="{\"attrs\":{\"localId\":\"01a0a067-68cf-78af-abd6-c660ec0d189b\"},\"text\":\"Owner\",\"type\":\"text\"}"} signs off.
Signed by !adf:carry{json="{\"attrs\":{\"localId\":\"01a0a067-68d2-787e-afc5-2459776aa029\"},\"text\":\"the owner\",\"type\":\"text\"}"}
!adf:carry{json="{\"attrs\":{\"localId\":\"01a0a067-68d5-7372-9962-29a039654056\"},\"text\":\"One \",\"type\":\"text\"}"}!adf:carry{json="{\"attrs\":{\"localId\":\"01a0a067-68d5-7372-9962-29a039654056\"},\"text\":\"anchor\",\"type\":\"text\"}"}
@@ -3,7 +3,16 @@
{ {
"content": [ "content": [
{ {
"text": "Write :mention[@Mikael]{id=5b10a2} to tag someone.", "text": "Write !adf:mention[@Mikael]{id=5b10a2} to tag someone.",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"text": "Namespaced under !adf: alone.",
"type": "text" "type": "text"
} }
], ],
@@ -54,7 +63,7 @@
{ {
"content": [ "content": [
{ {
"text": ":status[nope]{color=red} vs ", "text": "!adf:status[nope]{color=red} vs ",
"type": "text" "type": "text"
}, },
{ {
@@ -1,4 +1,6 @@
Write \:mention[@Mikael]{id=5b10a2} to tag someone. Write \!adf:mention[@Mikael]{id=5b10a2} to tag someone.
Namespaced under \!adf: alone.
\::syncBlock {localId=x} \::syncBlock {localId=x}
@@ -6,6 +8,6 @@ Write \:mention[@Mikael]{id=5b10a2} to tag someone.
Stand-up at 10:30, shipped :tada:, {maxLevel} deep, a ::syncBlock mid-line. Stand-up at 10:30, shipped :tada:, {maxLevel} deep, a ::syncBlock mid-line.
:status[In review]\{color} decides. !adf:status[In review]\{color} decides.
\:status[nope]{color=red} vs :status[yep]{color=red} \!adf:status[nope]{color=red} vs !adf:status[yep]{color=red}
@@ -0,0 +1,73 @@
{
"content": [
{
"content": [
{
"text": "`` ``",
"type": "text"
},
{
"marks": [
{
"type": "code"
}
],
"text": "a",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"marks": [
{
"type": "underline"
}
],
"text": "``",
"type": "text"
},
{
"text": "``",
"type": "text"
},
{
"marks": [
{
"type": "code"
}
],
"text": "a",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"text": "``\\`",
"type": "text"
},
{
"marks": [
{
"type": "code"
}
],
"text": "`",
"type": "text"
},
{
"text": "`",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1,5 @@
\`\` \`\``a`
!adf:underline[\`\`]\`\``a`
\`\`\\\``` ` ``\`
@@ -1,6 +1,6 @@
# Release 2.4 # Release 2.4
Shipped :emoji[🚀]{shortName=":rocket:"} on :date{timestamp=1756080000000} — :mention[@Mikael]{id=01a032c3-7a7c-775f-a730-2d79351338b4} owns the rollout, status :status[In review]{color=yellow}. Shipped !adf:emoji[🚀]{shortName=":rocket:"} on !adf:date{timestamp=1756080000000} — !adf:mention[@Mikael]{id=01a032c3-7a7c-775f-a730-2d79351338b4} owns the rollout, status !adf:status[In review]{color=yellow}.
| Part | Qty | | Part | Qty |
| --- | --- | | --- | --- |
@@ -11,7 +11,7 @@ Shipped :emoji[🚀]{shortName=":rocket:"} on :date{timestamp=1756080000000} —
- Check the collation: - Check the collation:
- `mysqldump --default-character-set=utf8mb4` - `mysqldump --default-character-set=utf8mb4`
> Rolled back once, see the :underline[postmortem]. > Rolled back once, see the !adf:underline[postmortem].
--- ---
@@ -0,0 +1,50 @@
{
"content": [
{
"content": [
{
"marks": [
{
"type": "underline"
},
{
"attrs": {
"href": "http://a`b"
},
"type": "link"
}
],
"text": "http://a`b",
"type": "text"
},
{
"text": "`c",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"marks": [
{
"type": "underline"
},
{
"attrs": {
"href": "http://d`e"
},
"type": "link"
}
],
"text": "http://d`e",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1,3 @@
!adf:underline[[http://a\`b](http://a\`b)]`c
!adf:underline[[http://d\`e](http://d`e)]
@@ -0,0 +1,83 @@
{
"content": [
{
"content": [
{
"marks": [
{
"attrs": {
"color": ""
},
"type": "textColor"
},
{
"attrs": {
"href": "`"
},
"type": "link"
}
],
"text": " ",
"type": "text"
},
{
"text": "`a",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"marks": [
{
"type": "underline"
},
{
"attrs": {
"href": "c",
"title": "`"
},
"type": "link"
}
],
"text": "b",
"type": "text"
},
{
"marks": [
{
"type": "code"
}
],
"text": "c",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"marks": [
{
"type": "underline"
},
{
"attrs": {
"href": "e`f"
},
"type": "link"
}
],
"text": "d",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1,5 @@
!adf:textColor[[ ](\`)]{color=""}`a
!adf:underline[[b](c "\`")]`c`
!adf:underline[[d](e`f)]
@@ -0,0 +1,111 @@
{
"content": [
{
"content": [
{
"marks": [
{
"type": "subsup"
},
{
"attrs": {
"href": "["
},
"type": "link"
}
],
"text": "a",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"marks": [
{
"type": "underline"
},
{
"attrs": {
"href": "]"
},
"type": "link"
}
],
"text": "b",
"type": "text"
},
{
"marks": [
{
"type": "underline"
}
],
"text": " ",
"type": "text"
},
{
"marks": [
{
"type": "underline"
},
{
"attrs": {
"href": "x",
"title": "["
},
"type": "link"
}
],
"text": "c",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"marks": [
{
"type": "underline"
},
{
"attrs": {
"href": "e[f]"
},
"type": "link"
}
],
"text": "d",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"marks": [
{
"type": "underline"
},
{
"attrs": {
"href": "http://a[b"
},
"type": "link"
}
],
"text": "http://a[b",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1,7 @@
!adf:subsup[[a](\[)]
!adf:underline[[b](\]) [c](x "\[")]
!adf:underline[[d](e[f])]
!adf:underline[[http://a\[b](http://a\[b)]
@@ -0,0 +1,127 @@
{
"content": [
{
"content": [
{
"marks": [
{
"type": "underline"
},
{
"attrs": {
"href": "!adf:a{"
},
"type": "link"
}
],
"text": "x",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"marks": [
{
"type": "underline"
},
{
"attrs": {
"href": "y",
"title": "!adf:a{"
},
"type": "link"
}
],
"text": "x",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"marks": [
{
"type": "underline"
},
{
"attrs": {
"href": "!adf:a[]{"
},
"type": "link"
}
],
"text": "x",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"marks": [
{
"type": "underline"
},
{
"attrs": {
"href": "https://example.com/?q=!adf:emoji{"
},
"type": "link"
}
],
"text": "x",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"marks": [
{
"type": "underline"
},
{
"attrs": {
"href": "ab:!adf:c{"
},
"type": "link"
}
],
"text": "ab:!adf:c{",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"marks": [
{
"type": "underline"
},
{
"attrs": {
"href": "ab:!adf:d"
},
"type": "link"
}
],
"text": "ab:!adf:d",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1,11 @@
!adf:underline[[x](\!adf:a{)]
!adf:underline[[x](y "\!adf:a{")]
!adf:underline[[x](\!adf:a[]{)]
!adf:underline[[x](https://example.com/?q=\!adf:emoji{)]
!adf:underline[[ab:\!adf:c{](ab:\!adf:c{)]
!adf:underline[[ab:\!adf:d](ab:\!adf:d)]
@@ -0,0 +1,45 @@
{
"content": [
{
"content": [
{
"marks": [
{
"attrs": {
"href": "/u"
},
"type": "link"
},
{
"type": "code"
}
],
"text": "]: a",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"attrs": {
"timestamp": "]:a"
},
"marks": [
{
"attrs": {
"href": "/u"
},
"type": "link"
}
],
"type": "date"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1,3 @@
!adf:carry{json="{\"marks\":[{\"attrs\":{\"href\":\"/u\"},\"type\":\"link\"},{\"type\":\"code\"}],\"text\":\"]: a\",\"type\":\"text\"}"}
!adf:carry{json="{\"attrs\":{\"timestamp\":\"]:a\"},\"marks\":[{\"attrs\":{\"href\":\"/u\"},\"type\":\"link\"}],\"type\":\"date\"}"}
@@ -0,0 +1,76 @@
{
"content": [
{
"content": [
{
"text": "[",
"type": "text"
},
{
"attrs": {
"timestamp": "]:a"
},
"type": "date"
}
],
"type": "paragraph"
},
{
"content": [
{
"content": [
{
"text": "[",
"type": "text"
},
{
"marks": [
{
"type": "code"
}
],
"text": "]: a",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "blockquote"
},
{
"content": [
{
"content": [
{
"content": [
{
"text": "[",
"type": "text"
},
{
"attrs": {
"timestamp": "]:a"
},
"type": "date"
},
{
"type": "hardBreak"
},
{
"text": "b",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "bulletList"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1,6 @@
\[!adf:date{timestamp="]:a"}
> \[`]: a`
- \[!adf:date{timestamp="]:a"}\
b
@@ -0,0 +1,57 @@
{
"content": [
{
"content": [
{
"content": [
{
"content": [
{
"text": " \na",
"type": "text"
}
],
"type": "codeBlock"
}
],
"type": "listItem"
}
],
"type": "bulletList"
},
{
"attrs": {
"order": 1
},
"content": [
{
"content": [
{
"content": [
{
"content": [
{
"content": [
{
"text": "\t",
"type": "text"
}
],
"type": "codeBlock"
}
],
"type": "listItem"
}
],
"type": "bulletList"
}
],
"type": "listItem"
}
],
"type": "orderedList"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1,20 @@
::::bulletList
:::listItem
```
a
```
:::
::::
::::::orderedList {order=1}
:::::listItem
::::bulletList
:::listItem
```
```
:::
::::
:::::
::::::
@@ -1 +1 @@
un:adf{json="{\"marks\":[{\"type\":\"strong\"}],\"text\":\"-real\",\"type\":\"text\"}"}istic un!adf:carry{json="{\"marks\":[{\"type\":\"strong\"}],\"text\":\"-real\",\"type\":\"text\"}"}istic
@@ -1 +1 @@
_**Ready**_:adf{json="{\"attrs\":{\"extensionKey\":\"status-lozenge\",\"extensionType\":\"com.atlassian.confluence.macro.core\"},\"marks\":[{\"type\":\"em\"},{\"type\":\"strong\"}],\"type\":\"inlineExtension\"}"}_**to ship**_ _**Ready**_!adf:carry{json="{\"attrs\":{\"extensionKey\":\"status-lozenge\",\"extensionType\":\"com.atlassian.confluence.macro.core\"},\"marks\":[{\"type\":\"em\"},{\"type\":\"strong\"}],\"type\":\"inlineExtension\"}"}_**to ship**_
@@ -1,5 +1,5 @@
un:adf{json="{\"marks\":[{\"type\":\"em\"}],\"text\":\"a\",\"type\":\"text\"}"}:adf{json="{\"marks\":[{\"type\":\"em\"},{\"type\":\"strong\"}],\"text\":\"b\",\"type\":\"text\"}"}**c**istic un!adf:carry{json="{\"marks\":[{\"type\":\"em\"}],\"text\":\"a\",\"type\":\"text\"}"}!adf:carry{json="{\"marks\":[{\"type\":\"em\"},{\"type\":\"strong\"}],\"text\":\"b\",\"type\":\"text\"}"}**c**istic
***a*b**:adf{json="{\"marks\":[{\"type\":\"strong\"},{\"type\":\"em\"}],\"text\":\"c\",\"type\":\"text\"}"} ***a*b**!adf:carry{json="{\"marks\":[{\"type\":\"strong\"},{\"type\":\"em\"}],\"text\":\"c\",\"type\":\"text\"}"}
~~un~~:adf{json="{\"marks\":[{\"type\":\"strike\"},{\"type\":\"em\"}],\"text\":\"a\",\"type\":\"text\"}"}:adf{json="{\"marks\":[{\"type\":\"strike\"},{\"type\":\"em\"},{\"type\":\"strong\"}],\"text\":\"b\",\"type\":\"text\"}"}~~**c**istic~~ ~~un~~!adf:carry{json="{\"marks\":[{\"type\":\"strike\"},{\"type\":\"em\"}],\"text\":\"a\",\"type\":\"text\"}"}!adf:carry{json="{\"marks\":[{\"type\":\"strike\"},{\"type\":\"em\"},{\"type\":\"strong\"}],\"text\":\"b\",\"type\":\"text\"}"}~~**c**istic~~
+2 -2
View File
@@ -1,5 +1,5 @@
:textColor[Overdue since :status[Blocked]{color=red} on Tuesday]{color="#ae2e24"} !adf:textColor[Overdue since !adf:status[Blocked]{color=red} on Tuesday]{color="#ae2e24"}
_Torque the **M8 bolt** to **25 Nm**_ _Torque the **M8 bolt** to **25 Nm**_
[Release notes :emoji[🚀]{shortName=":rocket:"}](https://example.com/release) [Release notes !adf:emoji[🚀]{shortName=":rocket:"}](https://example.com/release)
@@ -1,4 +1,4 @@
| Cell | Note | | Cell | Note |
| --- | --- | | --- | --- |
| :text{text=" "}lead | | | !adf:text{text=" "}lead | |
| :status[ok]{style="a\u007cb"} | trail:text{text=" "} | | !adf:status[ok]{style="a\u007cb"} | trail!adf:text{text=" "} |
@@ -1,6 +1,6 @@
Line one\ Line one\
Line two Line two
Trailing:hardBreak{} Trailing!adf:hardBreak{}
## Two:hardBreak{}lines ## Two!adf:hardBreak{}lines
@@ -0,0 +1,24 @@
{
"content": [
{
"content": [
{
"marks": [
{
"attrs": {
"href": "",
"title": ""
},
"type": "link"
}
],
"text": "a",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1 @@
[a](<> "")
@@ -0,0 +1,31 @@
{
"content": [
{
"content": [
{
"text": "`a ``b``",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"text": "`a",
"type": "text"
},
{
"type": "hardBreak"
},
{
"text": "```b``",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1,4 @@
\`a \`\`b``
\`a\
\`\`\`b``
@@ -1,5 +1,5 @@
:status[In review]{color=yellow} carries the text in the slot. !adf:status[In review]{color=yellow} carries the text in the slot.
:status{color=neutral} carries no text attribute. !adf:status{color=neutral} carries no text attribute.
:status[]{color=neutral} carries the empty string. !adf:status[]{color=neutral} carries the empty string.
+1 -1
View File
@@ -1 +1 @@
Due :date{timestamp=1756080000000}, reviewed :date{localId=01a033ab-781c-7f79-9c8a-fb63356c1cad timestamp=1735689600000}. Due !adf:date{timestamp=1756080000000}, reviewed !adf:date{localId=01a033ab-781c-7f79-9c8a-fb63356c1cad timestamp=1735689600000}.
+1 -1
View File
@@ -1 +1 @@
Shipped :emoji[🎉]{id=1f389 localId=01a033ad-f00e-7acb-a009-37ddd25d7949 shortName=":tada:"} on Friday. Shipped !adf:emoji[🎉]{id=1f389 localId=01a033ad-f00e-7acb-a009-37ddd25d7949 shortName=":tada:"} on Friday.
@@ -1,3 +1,3 @@
One:hardBreak{localId=01a0431d-201e-7400-a175-9c04d3628f95}Two One!adf:hardBreak{localId=01a0431d-201e-7400-a175-9c04d3628f95}Two
Three:hardBreak{text="\n"}Four Three!adf:hardBreak{text="\n"}Four
@@ -1,3 +1,3 @@
Tracking :inlineCard{url="https://example.com/browse/ABC-123"} this week. Tracking !adf:inlineCard{url="https://example.com/browse/ABC-123"} this week.
:inlineCard{data="{\"@type\":\"Link\",\"name\":\"Torque spec\",\"url\":\"https://example.com/torque\"}" localId=01a033ad-1fa5-76fa-aad0-2e56a17d76aa} !adf:inlineCard{data="{\"@type\":\"Link\",\"name\":\"Torque spec\",\"url\":\"https://example.com/torque\"}" localId=01a033ad-1fa5-76fa-aad0-2e56a17d76aa}
@@ -1 +1 @@
**:mention[@Mikael]{id=01a032c3-7a7c-775f-a730-2d79351338b4}** owns the rebuild. **!adf:mention[@Mikael]{id=01a032c3-7a7c-775f-a730-2d79351338b4}** owns the rebuild.
@@ -1,3 +1,3 @@
_:underline[Rewired]_ at the mains. _!adf:underline[Rewired]_ at the mains.
:underline[_Rewound_] by hand. !adf:underline[_Rewound_] by hand.
+2 -2
View File
@@ -1,3 +1,3 @@
:textColor[Over]{color="#ae2e24"}:textColor[due]{color="#a54800"} since the H:subsup[2]{type=sub}O test, :underline[signed by :mention[@Mikael]{id=01a032c3-7a7c-775f-a730-2d79351338b4}] on the 2:subsup[nd]{type=sup} of May. !adf:textColor[Over]{color="#ae2e24"}!adf:textColor[due]{color="#a54800"} since the H!adf:subsup[2]{type=sub}O test, !adf:underline[signed by !adf:mention[@Mikael]{id=01a032c3-7a7c-775f-a730-2d79351338b4}] on the 2!adf:subsup[nd]{type=sup} of May.
:border[:mediaInline{collection=contentId-98237 id=01a032c3-7a90-70c9-88f6-c60f710eda07}]{color="#091e42" size=2} !adf:border[!adf:mediaInline{collection=contentId-98237 id=01a032c3-7a90-70c9-88f6-c60f710eda07}]{color="#091e42" size=2}
@@ -1,3 +1,3 @@
See :mediaInline{alt="Torque chart" collection=contentId-98237 height=24 id=01a033ae-69a3-7411-b388-a37f9d103ded type=file width=24} mid-sentence. See !adf:mediaInline{alt="Torque chart" collection=contentId-98237 height=24 id=01a033ae-69a3-7411-b388-a37f9d103ded type=file width=24} mid-sentence.
Or :mediaInline{data="{\"fileName\":\"budget.pdf\",\"fileSize\":18432}" localId=01a033b1-b5a0-7a98-9e9c-48192bd30ac0 occurrenceKey=01a033ab-bab8-74b0-9ec8-b3419b000189 type=image} beside the text. Or !adf:mediaInline{data="{\"fileName\":\"budget.pdf\",\"fileSize\":18432}" localId=01a033b1-b5a0-7a98-9e9c-48192bd30ac0 occurrenceKey=01a033ab-bab8-74b0-9ec8-b3419b000189 type=image} beside the text.
+1 -1
View File
@@ -1 +1 @@
Ping :mention[@Mikael]{accessLevel=CONTAINER id=01a032c3-7a7c-775f-a730-2d79351338b4 localId=01a033ad-ecb7-7530-aa1c-ff25031e96e2 userType=DEFAULT} before merging. Ping !adf:mention[@Mikael]{accessLevel=CONTAINER id=01a032c3-7a7c-775f-a730-2d79351338b4 localId=01a033ad-ecb7-7530-aa1c-ff25031e96e2 userType=DEFAULT} before merging.
+1 -1
View File
@@ -1 +1 @@
The rebuild is :status[In review]{color=yellow localId=01a033a7-d68a-71a9-a58a-edf5757ad176 style=bold} until Friday. The rebuild is !adf:status[In review]{color=yellow localId=01a033a7-d68a-71a9-a58a-edf5757ad176 style=bold} until Friday.
@@ -1,15 +1,15 @@
:text{text=" "}Two leading spaces held, and one text node split:text{text="\n"}over two lines. !adf:text{text=" "}Two leading spaces held, and one text node split!adf:text{text="\n"}over two lines.
Trailing space held:text{text=" "} Trailing space held!adf:text{text=" "}
:text{text="\t"}Tabbed at the block edge. !adf:text{text="\t"}Tabbed at the block edge.
Space before the break:text{text=" "}\ Space before the break!adf:text{text=" "}\
survives. survives.
Space after the break\ Space after the break\
:text{text=" "}survives too. !adf:text{text=" "}survives too.
Kept _:text{text=" "}inside:text{text=" "}_, exactly. Kept _!adf:text{text=" "}inside!adf:text{text=" "}_, exactly.
Edged with the whitespace CommonMark keeps Edged with the whitespace CommonMark keeps
@@ -56,7 +56,7 @@
{ {
"content": [ "content": [
{ {
"text": ":", "text": "!adf:",
"type": "text" "type": "text"
}, },
{ {
@@ -1,7 +1,7 @@
Draft: :adf{json="{\"attrs\":{\"text\":\"Add a summary\"},\"type\":\"placeholder\"}"} — expected Friday. Draft: !adf:carry{json="{\"attrs\":{\"text\":\"Add a summary\"},\"type\":\"placeholder\"}"} — expected Friday.
**Blocked** on :adf{json="{\"attrs\":{\"extensionKey\":\"jira-issue\",\"extensionType\":\"com.atlassian.jira.macro\"},\"marks\":[{\"type\":\"strong\"}],\"type\":\"inlineExtension\"}"} until the rebuild lands. **Blocked** on !adf:carry{json="{\"attrs\":{\"extensionKey\":\"jira-issue\",\"extensionType\":\"com.atlassian.jira.macro\"},\"marks\":[{\"type\":\"strong\"}],\"type\":\"inlineExtension\"}"} until the rebuild lands.
\::adf{json="{\"type\":\"blockCard\"}"} \!adf:!adf:carry{json="{\"type\":\"blockCard\"}"}
**a**:adf{json="{\"marks\":[{\"type\":\"strong\"}],\"type\":\"placeholder\"}"}**b** **a**!adf:carry{json="{\"marks\":[{\"type\":\"strong\"}],\"type\":\"placeholder\"}"}**b**
+1 -1
View File
@@ -7,7 +7,7 @@ node_image=node:24.19.0-alpine3.24
in_image() { in_image() {
local image=$1 entrypoint=$2 local image=$1 entrypoint=$2
shift 2 shift 2
docker run --rm -u "$(id -u):$(id -g)" -e HOME=/tmp ${in_image_network:+--network "$in_image_network"} -v "$PWD:/app" -w /app --entrypoint "$entrypoint" "$image" "$@" docker run --rm -u "$(id -u):$(id -g)" -e HOME=/tmp ${PROPERTY_RUNS+-e PROPERTY_RUNS} ${in_image_network:+--network "$in_image_network"} -v "$PWD:/app" -w /app --entrypoint "$entrypoint" "$image" "$@"
} }
with_firefox() { with_firefox() {
+41
View File
@@ -10,6 +10,7 @@
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "24.13.3", "@types/node": "24.13.3",
"fast-check": "4.10.0",
"typescript": "7.0.2" "typescript": "7.0.2"
}, },
"engines": { "engines": {
@@ -366,6 +367,46 @@
"node": ">=16.20.0" "node": ">=16.20.0"
} }
}, },
"node_modules/fast-check": {
"version": "4.10.0",
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.10.0.tgz",
"integrity": "sha512-hhqQL+IJllZi3aM4TKvmCj3bywLEcycNTTLZeLhA9ttMxBrCqM07q7Di4kl+j9EWSTXvJH1+EpIgsDbF/+8H5Q==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT",
"dependencies": {
"pure-rand": "^8.0.0"
},
"engines": {
"node": ">=12.17.0"
}
},
"node_modules/pure-rand": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz",
"integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT"
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "7.0.2", "version": "7.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
+2 -1
View File
@@ -23,11 +23,12 @@
}, },
"scripts": { "scripts": {
"build": "tsc -p tsconfig.build.json", "build": "tsc -p tsconfig.build.json",
"test": "node --test --experimental-test-coverage --test-coverage-exclude=\"src/**/*.test.ts\" --test-coverage-branches=98 --test-coverage-functions=100 --test-coverage-lines=100 \"src/**/*.test.ts\"", "test": "node --test --experimental-test-coverage --test-coverage-exclude=\"src/**/*.test.ts\" --test-coverage-exclude=src/property-harness.ts --test-coverage-branches=98 --test-coverage-functions=100 --test-coverage-lines=100 \"src/**/*.test.ts\"",
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.build.json" "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.build.json"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "24.13.3", "@types/node": "24.13.3",
"fast-check": "4.10.0",
"typescript": "7.0.2" "typescript": "7.0.2"
} }
} }
+13
View File
@@ -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.
+202
View File
@@ -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.
+6
View File
@@ -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
+140 -122
View File
@@ -3,7 +3,7 @@
The grammar of the extended markdown `adfToMarkdown` emits and `markdownToAdf` parses. Plain The grammar of the extended markdown `adfToMarkdown` emits and `markdownToAdf` parses. Plain
CommonMark is a subset apart from raw HTML (below), with three carve-outs: literal text that CommonMark is a subset apart from raw HTML (below), with three carve-outs: literal text that
matches directive syntax below or reads as a pipe table is claimed by the flavour, and a matched matches directive syntax below or reads as a pipe table is claimed by the flavour, and a matched
`~~` pair spells `strike` (escape the `:`, `|` or `~` to keep it literal) — and one gap: a `~~` pair spells `strike` (escape the `!adf:`, `|` or `~` to keep it literal) — and one gap: a
CommonMark image fits only as its own CommonMark image fits only as its own
title-less paragraph — mid-text and titled images are named errors. The emitted form is contract title-less paragraph — mid-text and titled images are named errors. The emitted form is contract
(AGENTS.md §8). Per-node syntaxes build on this grammar in the sections below. (AGENTS.md §8). Per-node syntaxes build on this grammar in the sections below.
@@ -25,10 +25,11 @@ normalizes to it through the round-trip.
whose first item is empty), whatever block sits above it. Blank lines between items normalize whose first item is empty), whatever block sits above it. Blank lines between items normalize
away, and no list opens beside one of its own kind — the marker change CommonMark starts a away, and no list opens beside one of its own kind — the marker change CommonMark starts a
second list on merges instead: ADF records no tightness, so one `- ` spelling reads two second list on merges instead: ADF records no tightness, so one `- ` spelling reads two
adjacent lists of a kind back as one. The leaf `::listBreak` parts them, taking the separation adjacent lists of a kind back as one. The leaf `!adf:listBreak` parts them, taking the separation
any directive block takes where it sits. It builds no node, and it reads only between two any directive block takes where it sits. It builds no node, and it reads only between two
adjacent lists of one type: elsewhere, or carrying an argument, `{attrs}` or a body, it is a adjacent lists of one type: elsewhere, or carrying an argument, `{attrs}` or a body, it is a
named error. named error. A list whose item holds a line of spaces or tabs alone, which a list item reads
back empty, takes the directive form.
- Blockquotes prefix lines with `> `; a blank line inside a blockquote is a bare `>`. - Blockquotes prefix lines with `> `; a blank line inside a blockquote is a bare `>`.
- ATX headings (`#` … `######`); setext input normalizes to ATX. - ATX headings (`#` … `######`); setext input normalizes to ATX.
- Code fences ``` with the node's language as info string, the fence lengthened past any backtick - Code fences ``` with the node's language as info string, the fence lengthened past any backtick
@@ -42,15 +43,25 @@ normalizes to it through the round-trip.
- Hard break: backslash at end of line (survives editors that trim trailing spaces). Where - Hard break: backslash at end of line (survives editors that trim trailing spaces). Where
CommonMark admits no spelling — the end of a block, inside an ATX heading — or where the node CommonMark admits no spelling — the end of a block, inside an ATX heading — or where the node
carries an attribute, it is the inline directive. carries an attribute, it is the inline directive.
- An empty paragraph — real payloads carry them — is `::paragraph`. - An empty paragraph — real payloads carry them — is an `!adf:paragraph` … `!adf:/paragraph` pair
- Links `[text](url)`; `<…>` around a destination containing spaces; title in double quotes. A holding nothing.
backslash escapes a parenthesis the destination leaves unbalanced, and a quote inside the title; - Links `[text](url)`; `<…>` around a destination containing spaces, `<>` an empty one beside a
a balanced pair stays bare. `<url>` autolink form only when the text equals the destination and title; title in double quotes. A backslash escapes a parenthesis the destination leaves
the destination is a valid CommonMark autolink (absolute URI). unbalanced, and a quote inside the title; a balanced pair stays bare. `<url>` autolink form only
when the text equals the destination and the destination is a valid CommonMark autolink
(absolute URI) — inside an inline directive's `[content]`, one holding no backtick, no
unbalanced bracket and no inline directive opener.
- Paragraphs on one line — no soft wrapping; a soft line break in input becomes a single space. - Paragraphs on one line — no soft wrapping; a soft line break in input becomes a single space.
- Entity references in input decode to their characters; output backslash-escapes only where text - Entity references in input decode to their characters; output backslash-escapes only where text
would otherwise parse as syntax, scanning the assembled line rather than each text node: escape would otherwise parse as syntax, scanning the assembled line rather than each text node: escape
the leading delimiter of a construct that would otherwise open, re-scan from there, and repeat. the leading delimiter of a construct that would otherwise open, re-scan from there, and repeat.
A backtick run escapes whole, and a bare one escapes wherever a later backtick string of its
length forms around an escape in the same inline content — an escaped backtick alone, one joined
to the bare run after it, or a bare run an escape splits off: CommonMark reads no escape inside a
code span, so such a string still closes one.
Where a paragraph opens with what reads as a link reference definition, which resolves before
any inline construct binds (a `]` inside a code span or `{attrs}` counts), an opening text `[`
escapes and an opening link's nodes ride the carry.
An emphasis delimiter run in text escapes where CommonMark can open **or** close with it, so An emphasis delimiter run in text escapes where CommonMark can open **or** close with it, so
`*not emphasis*` is `\*not emphasis\*` — no delimiter the emitter did not write reaches the `*not emphasis*` is `\*not emphasis\*` — no delimiter the emitter did not write reaches the
matching below, which is what lets the emitter decide its own pairings. matching below, which is what lets the emitter decide its own pairings.
@@ -62,56 +73,62 @@ normalizes to it through the round-trip.
## Directives ## Directives
One grammar for everything CommonMark lacks. A directive name is `[a-z][A-Za-z0-9]*` — the ADF One grammar for everything CommonMark lacks, namespaced: every directive opens with the literal
node and mark names the sections below spell as directives. Recognition is syntactic and `!adf:`. A directive name is `[a-z][A-Za-z0-9]*` — the ADF node and mark names the sections below
name-set-independent: anything matching the forms below parses as a directive regardless of spell as directives. Recognition is syntactic and name-set-independent: anything matching the forms
whether the name is known, and an unknown name is an error result naming it — so output an old below parses as a directive regardless of whether the name is known, and an unknown name is an
emitter escaped stays escaped, and erroring input gaining meaning later is MINOR, never a reparse error result naming it at the opener, whatever follows it — so output an old emitter escaped stays
(§8). Each name belongs to one position, and a name the other one spells — a mark or an inline escaped, and erroring input gaining meaning later is MINOR, never a reparse (§8). Each name belongs
node written as a block directive, a block node written inline — is a different error, naming the to one position, and a name the other one spells — a mark or an inline node written as a block
spelling it takes. Two reserved names read back to no node: `adf` for the opaque carry, as both directive, a block node written inline — is a different error, naming the spelling it takes. Two
directive name and fence info string, and `listBreak` for the leaf that parts two adjacent lists reserved names read back to no node: `carry` for the opaque carry, as both directive name and fence
(Canonical form). info string, and `listBreak` for the leaf that parts two adjacent lists (Canonical form).
**Inline**: `:name[content]{attrs}`, on one line — an inline directive never spans lines. **Claiming**: an unescaped `!adf:` claims wherever it stands. What follows picks the form: `/name`
closes a container, and a name picks by what follows it in turn — a space or the line's end a block
line, `[` or `{` an inline directive. A `!adf:` completing none of the three is a named error, and
`\!adf:` is the literal, block and inline alike. A construct CommonMark binds first — a code span,
an autolink URI, a link destination or title — holds its `!adf:` as content; a directive's
`[content]`, which binds ahead of bracket matching, does not. A claimed block line also ends a lazy
continuation: the blockquote or list item whose paragraph CommonMark would fold it into closes
instead.
**Inline**: `!adf:name[content]{attrs}`, on one line — an inline directive never spans lines.
`[content]` is inline markdown; brackets inside balance as in CommonMark link text, `\]` for a `[content]` is inline markdown; brackets inside balance as in CommonMark link text, `\]` for a
literal bracket. Whitespace at either edge of `[content]`, space or tab, is part of it and literal bracket. Whitespace at either edge of `[content]`, space or tab, is part of it and
survives inline parsing. Each section below says whether content is required. `:` opens a survives inline parsing. Each section below says whether content is required, and `{attrs}` must
directive only when the name is followed immediately by `[` or `{`, and `{attrs}` must follow follow `]` (or the name) with no gap. An inline directive binds as a unit before bracket matching,
`]` (or the name) with no gap — anything else (`10:30`, `:smile:`, a stray `{…}` in text) is the way a code span does: a `]` or `(` inside its `{attrs}` is the directive's, never the enclosing
literal text. An inline directive binds as a unit before bracket matching, the way a code span content's, and a `(` after its closing `]` opens no link.
does: a `]` or `(` inside its `{attrs}` is the directive's, never the enclosing content's, and a
`(` after its closing `]` opens no link.
**Container block**: **Block container**:
``` ```
:::name arg {attrs} !adf:name arg {attrs}
block content block content
::: !adf:/name
``` ```
The fence is three or more colons. `arg` is one optional bare token whose meaning each node `arg` is one optional bare token whose meaning each node defines (e.g. the panel type). The body is
defines (e.g. the panel type). The body is block markdown. The closing fence is a line of at block markdown. The closer names the innermost open container and carries nothing after the name;
least the opening's length and closes the innermost open container however long its run, and a one naming another node, or standing where no container is open, is a named error. Opening and
container's fence is longer than every directive fence line anywhere in its body, however deeply a list item or blockquote nests it; a colon run inside a code closing is what nests, so the opener reads the same at every depth, and a closer crosses no list
fence or opaque carry is content. Canonical form uses minimal lengths. item or blockquote edge — a container opened inside one closes inside it. Directive block lines
Directive fence lines follow code-fence indentation (up to three spaces relative to their follow code-fence indentation (up to three spaces relative to their container); an `!adf:` inside a
container). code fence or opaque carry is content.
**Leaf block**: `::name arg {attrs}` — a block-position node with no body, `arg` reading as **Block leaf**: `!adf:name arg {attrs}` — the same opener with no closer, `arg` reading as above.
above.
Which of the two a node takes is its content model, never the spelling: a model taking content is
written as an opener–closer pair and one taking none as a leaf, so a leaf given a body and a
container missing its closer are each a named error. A node holding no content whose model takes
some is an empty pair. A spelled node's content model is contract in consequence — changing one is
MAJOR (AGENTS.md §8).
Canonical spacing is the only spacing input reads: one space parts the name, `arg` and `{attrs}`, Canonical spacing is the only spacing input reads: one space parts the name, `arg` and `{attrs}`,
and one parts each attribute pair, with no padding inside the braces. Trailing whitespace on a and one parts each attribute pair, with no padding inside the braces. Trailing whitespace on a
directive block line is tolerated in input, never emitted. directive block line is tolerated in input, never emitted.
**Claiming at block level**, symmetric with inline: a line whose leading run of two or more
colons is followed immediately by a name character is claimed and must parse fully as a container
opening or a leaf, else it is a named error. A bare colon-run line is a closing fence while a
container is open, a named error otherwise. A claimed line also ends a lazy continuation: the
blockquote or list item whose paragraph CommonMark would fold it into closes instead.
**Attributes**: `{key=value key2="two words"}`. `{attrs}` is optional in every form, and `{}` is **Attributes**: `{key=value key2="two words"}`. `{attrs}` is optional in every form, and `{}` is
valid — no attributes. A bare value matches `[A-Za-z0-9_-]+`; any other value is double-quoted valid — no attributes. A bare value matches `[A-Za-z0-9_-]+`; any other value is double-quoted
with JSON string escaping (`\"` `\\` `\n` `\t` `\uXXXX`, …) — total over with JSON string escaping (`\"` `\\` `\n` `\t` `\uXXXX`, …) — total over
@@ -121,22 +138,23 @@ after a directive binds and stay raw. The closing `}` is the first one outside q
quoted value holds `}` unescaped. All values are strings at the grammar level; each node's section quoted value holds `}` unescaped. All values are strings at the grammar level; each node's section
assigns types. assigns types.
Canonical form orders keys alphabetically, spells values bare wherever allowed, escapes inside Canonical form orders keys alphabetically, spells values bare wherever allowed, escapes inside
quotes in the shortest form each escape has, and omits empty `{attrs}` except where the `{` itself quotes in the shortest form each escape has, and omits empty `{attrs}` except where the `{` is what
claims the directive (`:hardBreak{}`). Input reads that spelling alone: keys out of order, a value ends the name (`!adf:hardBreak{}`). Input reads that spelling alone: keys out of order, a value
quoted where bare carries it, an escape longer than it need be, an empty `{attrs}` the name or the quoted where bare carries it, an escape longer than it need be, an empty `{attrs}` on a block line
`[content]` already claims, and a number or `json` value outside its canonical JSON spelling are or after a `[content]`, and a number or `json` value outside its canonical JSON spelling are each a
each a named error naming the spelling to write instead. named error naming the spelling to write instead.
**Escaping**: the emitter backslash-escapes whatever literal text would otherwise parse as **Escaping**: the emitter backslash-escapes whatever literal text would otherwise parse as
directive syntax — the leading `:` of a would-be directive, `]` inside content, a `{` right directive syntax — every literal `!adf:`, `]` inside content, a bracket a link's destination and
after a directive's closing `]`, which would otherwise be read as the attributes it has none title inside content leave unbalanced, a backtick there that would open a code span, a `{` right
of; outside code spans and code blocks, a backslash before `:` in input yields a literal colon. after a directive's closing `]`, which would otherwise be read as the attributes it has none of;
outside code spans and code blocks, `\!adf:` in input yields the literal text.
**Malformed directives are error results**, named: an unclosed container at end of input, a body **Malformed directives are error results**, named: an unclosed container at end of input, a closer
fence line of the container's length or longer, a bare colon-run line outside any container or naming no open container or a node other than the innermost open one, a leaf given a body, an
shorter than the fence it would close, an inline `[content]` or `{attrs}` left unclosed at end of `!adf:` completing no directive, an inline `[content]` or `{attrs}` left unclosed at end of line,
line, unparseable or duplicate-keyed attrs, invalid JSON in an opaque carry. Never a silent unparseable or duplicate-keyed attrs, invalid JSON in an opaque carry. Never a silent literal-text
literal-text fallback — a typo that reparses as prose is the silent loss §2 refuses. fallback — a typo that reparses as prose is the silent loss §2 refuses.
## The opaque carry (AGENTS.md §3) ## The opaque carry (AGENTS.md §3)
@@ -146,16 +164,15 @@ a node the emitter spells natively: it restores unreinterpreted, and the next em
canonically (AGENTS.md §2). Block and inline positions canonicalize differently, each fitting canonically (AGENTS.md §2). Block and inline positions canonicalize differently, each fitting
where it sits: where it sits:
- **Block position**: a fenced code block with info string `adf`, body = the node's JSON — - **Block position**: a fenced code block with info string `carry`, body = the node's JSON —
two-space indent, object keys sorted. two-space indent, object keys sorted.
- **Inline position**: `:adf{json="…"}` — compact serialization (keys sorted, no whitespace), - **Inline position**: `!adf:carry{json="…"}` — compact serialization (keys sorted, no whitespace),
JSON-string-escaped into the attribute. JSON-string-escaped into the attribute.
The info string `adf` is reserved: a genuine `codeBlock` whose `language` is exactly `adf` takes The info string `carry` is reserved: a genuine `codeBlock` whose `language` is exactly `carry` takes
the attribute the section below keeps for a language no info string holds, so the reservation the attribute the section below keeps for a language no info string holds, so the reservation
stays absolute. stays absolute.
In block-directive positions (`::adf`, `:::adf`) the reserved name is a named error — the In block-directive position `!adf:carry` is a named error — the carry's block form is the fence.
carry's block form is the fence.
## Raw HTML in input ## Raw HTML in input
@@ -181,9 +198,9 @@ editor-normal ADF reads an empty attrs object, marks array or content array as t
(AGENTS.md §2) — the grammar's empty-`{attrs}` omission already collapses the two spellings. (AGENTS.md §2) — the grammar's empty-`{attrs}` omission already collapses the two spellings.
Marks on a block node ride the reserved attribute key `marks` — the node's marks array as a Marks on a block node ride the reserved attribute key `marks` — the node's marks array as a
`json` value: `::::layoutSection {marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"}`. `json` value: `!adf:layoutSection {marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"}`.
A section saying its body is inline takes at most one paragraph, whose inline content becomes A section saying its body is inline takes at most one paragraph, whose inline content becomes
the node's `content`; any other body is a named error, and a node holding no content is the leaf. the node's `content`; any other body is a named error, and an empty pair is a node holding none.
A node the sections cannot spell rides the opaque carry: an attrs key its section does not A node the sections cannot spell rides the opaque carry: an attrs key its section does not
list, a value that is not the section's type, or an arg-slot value that is no bare token. In list, a value that is not the section's type, or an arg-slot value that is no bare token. In
@@ -200,7 +217,7 @@ form.
- `codeBlock` — container, body one fenced code block whose info string is the language and whose - `codeBlock` — container, body one fenced code block whose info string is the language and whose
content is the node's. Attributes: `hideLineNumbers` (boolean), `language` (string), `localId` content is the node's. Attributes: `hideLineNumbers` (boolean), `language` (string), `localId`
(string), `uniqueId` (string), `wrap` (boolean). A language no info string carries back — empty, (string), `uniqueId` (string), `wrap` (boolean). A language no info string carries back — empty,
the reserved `adf`, or holding a backtick, a backslash, a control character, edge whitespace or the reserved `carry`, or holding a backtick, a backslash, a control character, edge whitespace or
an entity reference — rides the `language` attribute instead and the fence carries no info an entity reference — rides the `language` attribute instead and the fence carries no info
string; writing it in the slot that rule leaves empty, or in both, is a named error. The body is string; writing it in the slot that rule leaves empty, or in both, is a named error. The body is
one ordinary code block, and a fence's info string decodes escapes and entity references as any one ordinary code block, and a fence's info string decodes escapes and entity references as any
@@ -215,11 +232,11 @@ form.
- `rule` — leaf. Attributes: `localId` (string). - `rule` — leaf. Attributes: `localId` (string).
```` ````
:::codeBlock {localId=01a03d5c-9b21-73f4-8e6a-0c47b1d9e2f8 wrap=true} !adf:codeBlock {localId=01a03d5c-9b21-73f4-8e6a-0c47b1d9e2f8 wrap=true}
```rust ```rust
fn main() {} fn main() {}
``` ```
::: !adf:/codeBlock
```` ````
### Panel ### Panel
@@ -230,9 +247,9 @@ fn main() {}
panels. panels.
``` ```
:::panel warning !adf:panel warning
Check the collation before importing. Check the collation before importing.
::: !adf:/panel
``` ```
### Expand ### Expand
@@ -241,9 +258,9 @@ Check the collation before importing.
which. Attributes: `localId` (string), `title` (string). which. Attributes: `localId` (string), `title` (string).
``` ```
:::expand {title="Full build log"} !adf:expand {title="Full build log"}
… …
::: !adf:/expand
``` ```
### The media family ### The media family
@@ -252,19 +269,19 @@ Check the collation before importing.
(string), `localId` (string), `occurrenceKey` (string), `type` (`external` `file` `link`), (string), `localId` (string), `occurrenceKey` (string), `type` (`external` `file` `link`),
`url` (string), `width` (number). `file` and `link` media carry `collection` + `id`; `url` (string), `width` (number). `file` and `link` media carry `collection` + `id`;
`external` media carry `url`. `external` media carry `url`.
- `mediaSingle` — container: one `::media`, then optionally one `:::caption`. Attributes: - `mediaSingle` — container: one `!adf:media`, then optionally one `!adf:caption`. Attributes:
`layout` (`align-end` `align-start` `center` `full-width` `wide` `wrap-left` `wrap-right`), `layout` (`align-end` `align-start` `center` `full-width` `wide` `wrap-left` `wrap-right`),
`localId` (string), `width` (number), `widthType` (`percentage` `pixel`). `localId` (string), `width` (number), `widthType` (`percentage` `pixel`).
- `caption` — container, inline body. Attributes: `localId` (string). - `caption` — container, inline body. Attributes: `localId` (string).
- `mediaGroup` — container of `::media` leaves. Attributes: none. - `mediaGroup` — container of `!adf:media` leaves. Attributes: none.
``` ```
::::mediaSingle {layout=center width=50} !adf:mediaSingle {layout=center width=50}
::media {collection=MediaServicesSample id=4478e39c-cf9b-41d1-ba92-68589487cd75 type=file} !adf:media {collection=MediaServicesSample id=4478e39c-cf9b-41d1-ba92-68589487cd75 type=file}
:::caption !adf:caption
The moon, at night. The moon, at night.
::: !adf:/caption
:::: !adf:/mediaSingle
``` ```
**The CommonMark image.** A paragraph whose entire inline content is one image `![alt](url)` is **The CommonMark image.** A paragraph whose entire inline content is one image `![alt](url)` is
@@ -307,27 +324,27 @@ inline layer's ordinary CommonMark escaping yields the pipe; each cell is the in
one paragraph, trimmed; canonical form pads cells with single spaces and ends rows with `|` one paragraph, trimmed; canonical form pads cells with single spaces and ends rows with `|`
(optional in input). Named errors: a delimiter or body row whose cell count differs from the (optional in input). Named errors: a delimiter or body row whose cell count differs from the
header's, and an alignment colon in the delimiter row — ADF holds no column alignment. In a header's, and an alignment colon in the delimiter row — ADF holds no column alignment. In a
pipe cell a hard break is `:hardBreak{}` and a literal `|` is `\|`; a `|` inside a quoted pipe cell a hard break is `!adf:hardBreak{}` and a literal `|` is `\|`; a `|` inside a quoted
attribute value is already `\u007c`, so the split never reaches it. attribute value is already `\u007c`, so the split never reaches it.
The directive form nests cells as containers of block content inside `tableRow` containers: The directive form nests cells as containers of block content inside `tableRow` containers:
``` ```
:::::table {isNumberColumnEnabled=true width=760} !adf:table {isNumberColumnEnabled=true width=760}
::::tableRow !adf:tableRow
:::tableHeader {colspan=2 colwidth="[340,420]"} !adf:tableHeader {colspan=2 colwidth="[340,420]"}
Assembly Assembly
::: !adf:/tableHeader
:::: !adf:/tableRow
::::tableRow !adf:tableRow
:::tableCell {background="#deebff"} !adf:tableCell {background="#deebff"}
Bolt M8 Bolt M8
::: !adf:/tableCell
:::tableCell {valign=top} !adf:tableCell {valign=top}
40 40
::: !adf:/tableCell
:::: !adf:/tableRow
::::: !adf:/table
``` ```
- `table` — container of `tableRow` containers. Attributes: `displayMode` (`default` `fixed`), - `table` — container of `tableRow` containers. Attributes: `displayMode` (`default` `fixed`),
@@ -351,14 +368,14 @@ Bolt M8
free-form; the editor writes `DECIDED`). free-form; the editor writes `DECIDED`).
``` ```
::::taskList {localId=0198f3a2-7c41-7f2e-9b3a-4d8e2c1a6b90} !adf:taskList {localId=0198f3a2-7c41-7f2e-9b3a-4d8e2c1a6b90}
:::taskItem DONE {localId=0198f3a2-8d52-70b1-8c4f-5e9f3d2b7ca1} !adf:taskItem DONE {localId=0198f3a2-8d52-70b1-8c4f-5e9f3d2b7ca1}
Write the spec Write the spec
::: !adf:/taskItem
:::taskItem TODO {localId=0198f3a2-9e63-7d80-a15b-6fa04e3c8db2} !adf:taskItem TODO {localId=0198f3a2-9e63-7d80-a15b-6fa04e3c8db2}
Ship it Ship it
::: !adf:/taskItem
:::: !adf:/taskList
``` ```
### Layout ### Layout
@@ -368,14 +385,14 @@ Ship it
`middle` `top`), `width` (number — percent). `middle` `top`), `width` (number — percent).
``` ```
::::layoutSection !adf:layoutSection
:::layoutColumn {width=50} !adf:layoutColumn {width=50}
Left. Left.
::: !adf:/layoutColumn
:::layoutColumn {width=50} !adf:layoutColumn {width=50}
Right. Right.
::: !adf:/layoutColumn
:::: !adf:/layoutSection
``` ```
### Extensions ### Extensions
@@ -387,7 +404,7 @@ Right.
- `extensionFrame` — container, block body. Attributes: none. - `extensionFrame` — container, block body. Attributes: none.
``` ```
::extension {extensionKey=toc extensionType="com.atlassian.confluence.macro.core" parameters="{\"maxLevel\":2}"} !adf:extension {extensionKey=toc extensionType="com.atlassian.confluence.macro.core" parameters="{\"maxLevel\":2}"}
``` ```
### Sync blocks ### Sync blocks
@@ -396,18 +413,18 @@ Right.
`localId` (string), `resourceId` (string). `localId` (string), `resourceId` (string).
``` ```
::syncBlock {localId=0198f3a2-af74-7e91-b26c-70b15f4d9ec3 resourceId="ari:cloud:confluence:site/page/123"} !adf:syncBlock {localId=0198f3a2-af74-7e91-b26c-70b15f4d9ec3 resourceId="ari:cloud:confluence:site/page/123"}
``` ```
## Inline nodes ## Inline nodes
Attributes and the carry fallback read as in the block sections, the carry in its inline form. Of Attributes and the carry fallback read as in the block sections, the carry in its inline form. Of
the nodes below, `emoji`, `mention` and `status` spell their `text` attribute in the content slot the nodes below, `emoji`, `mention` and `status` spell their `text` attribute in the content slot as
as plain text: `[]` is the empty string, absent content is the absent attribute, non-empty content plain text: `[]` is the empty string, absent content is the absent attribute, non-empty content
parsing to anything but one unmarked text node — adjacent identical-mark text nodes merged first — parsing to anything but one text node carrying neither marks, attributes nor content — adjacent text
is a named error, and so is a `text` key in `{attrs}`. An enclosing mark spelling does not reach nodes with identical marks and no attributes merged first — is a named error, and so is a `text` key
into the slot. The rest take no content, `:text` included; content on a node that takes none is a in `{attrs}`. An enclosing mark spelling does not reach into the slot. The rest take no content,
named error. `!adf:text` included; content on a node that takes none is a named error.
- `date` — Attributes: `localId` (string), `timestamp` (string, epoch milliseconds). - `date` — Attributes: `localId` (string), `timestamp` (string, epoch milliseconds).
- `emoji` — Attributes: `id` (string), `localId` (string), `shortName` (string, `:name:`), `text` - `emoji` — Attributes: `id` (string), `localId` (string), `shortName` (string, `:name:`), `text`
@@ -424,30 +441,30 @@ named error.
(string), `style` (string), `text` (string). (string), `style` (string), `text` (string).
``` ```
:status[In review]{color=yellow} — :mention[@Mikael]{id=01a032c3-7a7c-775f-a730-2d79351338b4} !adf:status[In review]{color=yellow} — !adf:mention[@Mikael]{id=01a032c3-7a7c-775f-a730-2d79351338b4}
Shipped :emoji[🎉]{shortName=":tada:"} on :date{timestamp=1756080000000}. Shipped !adf:emoji[🎉]{shortName=":tada:"} on !adf:date{timestamp=1756080000000}.
``` ```
**Whitespace CommonMark cannot hold.** A newline inside a text node, and a space or tab where **Whitespace CommonMark cannot hold.** A newline inside a text node, and a space or tab where
CommonMark strips or refuses one — a block's inline content edges, either side of a line break, CommonMark strips or refuses one — a block's inline content edges, either side of a line break,
an em, strong or strike spelling's inner edges, a pipe cell's edges — is spelled an em, strong or strike spelling's inner edges, a pipe cell's edges — is spelled
`:text{text="…"}`, the reserved key carrying the node's text, escaped by the attribute grammar `!adf:text{text="…"}`, the reserved key carrying the node's text, escaped by the attribute grammar
and never literal: pipe cells trim and pad. The emitter wraps the whitespace run alone and leaves and never literal: pipe cells trim and pad. The emitter wraps the whitespace run alone and leaves
the rest plain text; `markdownToAdf` merges adjacent text nodes carrying identical marks the rest plain text; `markdownToAdf` merges adjacent text nodes carrying identical marks and no
(AGENTS.md §2). Input reads that spelling alone: the value is one run of spaces and tabs, or one attributes (AGENTS.md §2). Input reads that spelling alone: the value is one run of spaces and
run of newlines, and anything else — a mixed run, or text CommonMark carries plainly — is a named tabs, or one run of newlines, and anything else — a mixed run, or text CommonMark carries plainly —
error. is a named error.
``` ```
:text{text=" "}Two leading spaces held, and one text node split:text{text="\n"}over two lines. !adf:text{text=" "}Two leading spaces held, and one text node split!adf:text{text="\n"}over two lines.
``` ```
## Marks ## Marks
An inline node's marks ride the spelling wrapped around them, never the block sections' reserved An inline node's marks ride the spelling wrapped around them, never the block sections' reserved
`marks` key. `code`, `em`, `link`, `strike` and `strong` keep their markdown spellings, and are `marks` key. `code`, `em`, `link`, `strike` and `strong` keep their markdown spellings, and are
not directive names: `:em[x]` is a named error. `border`, `subsup`, `textColor` and `underline` not directive names: `!adf:em[x]` is a named error. `border`, `subsup`, `textColor` and `underline`
are inline directives, content required non-empty. are inline directives, content required non-empty.
- `border` — Attributes: `color` (string, `#rrggbb` or `#rrggbbaa`), `size` (number, 1–3). - `border` — Attributes: `color` (string, `#rrggbb` or `#rrggbbaa`), `size` (number, 1–3).
@@ -458,7 +475,8 @@ are inline directives, content required non-empty.
- `underline` — Attributes: none. - `underline` — Attributes: none.
A spelling adds its mark to every inline node it wraps, and nesting is the marks array in order, A spelling adds its mark to every inline node it wraps, and nesting is the marks array in order,
outermost first: `_:underline[x]_` gives marks `[em, underline]`, `:underline[_x_]` the reverse. outermost first: `_!adf:underline[x]_` gives marks `[em, underline]`, `!adf:underline[_x_]` the
reverse.
`adfToMarkdown` nests in the order the array holds rather than sorting it — §2's equality `adfToMarkdown` nests in the order the array holds rather than sorting it — §2's equality
restores the array, not a set — and opens each spelling once over the longest run of adjacent restores the array, not a set — and opens each spelling once over the longest run of adjacent
inline nodes carrying an identical mark, attributes included, at that depth. A run breaks at every inline nodes carrying an identical mark, attributes included, at that depth. A run breaks at every
@@ -476,7 +494,7 @@ opaque carry inside a mark spelling is a named error in input: the carry restore
exactly, marks included (AGENTS.md §3). exactly, marks included (AGENTS.md §3).
``` ```
:textColor[**Overdue**]{color="#ae2e24"}, H:subsup[2]{type=sub}O, :underline[signed]. !adf:textColor[**Overdue**]{color="#ae2e24"}, H!adf:subsup[2]{type=sub}O, !adf:underline[signed].
:border[:mediaInline{collection=contentId-98237 id=01a032c3-7a90-70c9-88f6-c60f710eda07}]{color="#091e42" size=2} !adf:border[!adf:mediaInline{collection=contentId-98237 id=01a032c3-7a90-70c9-88f6-c60f710eda07}]{color="#091e42" size=2}
``` ```
+23
View File
@@ -0,0 +1,23 @@
import fc from 'fast-check'
import assert from 'node:assert/strict'
import test from 'node:test'
import { adfDocument, propertyRuns, propertyTimeout } from './property-harness.ts'
import { adfToMarkdown } from './markdown/emit/adf-to-markdown.ts'
import { markdownToAdf } from './markdown/parse/markdown-to-adf.ts'
import { toEditorNormal } from './adf/editor-normal.ts'
const gateRuns = 1600
test('a generated document refuses to emit, or its markdown reads back to it', { timeout: propertyTimeout }, () => {
fc.assert(
fc.property(adfDocument, (document) => {
const emitted = adfToMarkdown(document)
if (!emitted.ok) return
const read = markdownToAdf(emitted.value)
assert.ok(read.ok, read.ok ? '' : `${read.error.code}: ${read.error.message} — reading ${JSON.stringify(emitted.value)}`)
assert.deepEqual(toEditorNormal(read.value), document, `reading ${JSON.stringify(emitted.value)}`)
}),
propertyRuns(gateRuns),
)
})
+183
View File
@@ -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)
}
+21 -9
View File
@@ -52,8 +52,8 @@ export function attributeNestingMessage(key: string, type: string, levels: numbe
} }
export function carriesOnly(node: AdfNode, attributes: readonly string[]): boolean { export function carriesOnly(node: AdfNode, attributes: readonly string[]): boolean {
if ((node.marks ?? []).length > 0 || node.text !== undefined) return false if (nodeMarks(node).length > 0 || node.text !== undefined) return false
return holdsOnly(node.attrs ?? {}, attributes) return holdsOnly(nodeAttrs(node), attributes)
} }
// Depth is the walks' business, not the shape's: the guard waves a deep document through as blocks and marks do. // Depth is the walks' business, not the shape's: the guard waves a deep document through as blocks and marks do.
@@ -72,6 +72,18 @@ export function isAdfMark(value: unknown): value is AdfMark {
return !('attrs' in value) || isAttributes(value['attrs']) return !('attrs' in value) || isAttributes(value['attrs'])
} }
export function nodeAttrs(node: { attrs?: AdfAttributes }): Readonly<AdfAttributes> {
return node.attrs ?? {}
}
export function nodeContent(node: { content?: AdfNode[] }): readonly AdfNode[] {
return node.content ?? []
}
export function nodeMarks(node: { marks?: AdfMark[] }): readonly AdfMark[] {
return node.marks ?? []
}
function isNodeArray(value: readonly unknown[]): value is readonly AdfNode[] { function isNodeArray(value: readonly unknown[]): value is readonly AdfNode[] {
const pending: unknown[] = [...value] const pending: unknown[] = [...value]
while (pending.length > 0) { while (pending.length > 0) {
@@ -95,23 +107,23 @@ function nestingFault(nodes: readonly AdfNode[]): ConvertFault | undefined {
while (pending.length > 0) { while (pending.length > 0) {
const node = pending.pop() const node = pending.pop()
if (node === undefined) continue if (node === undefined) continue
const fault = attributesFault(node.attrs, node.type) ?? marksFault(node.marks) const fault = attributesFault(nodeAttrs(node), node.type) ?? marksFault(nodeMarks(node))
if (fault !== undefined) return fault if (fault !== undefined) return fault
pending.push(...(node.content ?? [])) pending.push(...nodeContent(node))
} }
return undefined return undefined
} }
function marksFault(marks: readonly AdfMark[] | undefined): ConvertFault | undefined { function marksFault(marks: readonly AdfMark[]): ConvertFault | undefined {
for (const mark of marks ?? []) { for (const mark of marks) {
const fault = attributesFault(mark.attrs, mark.type, markAttributeNesting) const fault = attributesFault(nodeAttrs(mark), mark.type, markAttributeNesting)
if (fault !== undefined) return fault if (fault !== undefined) return fault
} }
return undefined return undefined
} }
function attributesFault(attrs: AdfAttributes | undefined, type: string, levels: number = largestNesting): ConvertFault | undefined { function attributesFault(attrs: AdfAttributes, type: string, levels: number = largestNesting): ConvertFault | undefined {
for (const [key, value] of Object.entries(attrs ?? {})) { for (const [key, value] of Object.entries(attrs)) {
if (overNested(value, levels)) return { code: 'unsupported-nesting-depth', message: attributeNestingMessage(key, type, levels) } if (overNested(value, levels)) return { code: 'unsupported-nesting-depth', message: attributeNestingMessage(key, type, levels) }
} }
return undefined return undefined
+77
View File
@@ -0,0 +1,77 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import type { AdfNode } from './document.ts'
import type { JsonValue } from '../json-value.ts'
import { toEditorNormal } from './editor-normal.ts'
test('merges adjacent text nodes carrying identical marks, at every level', () => {
const content: AdfNode[] = [
{ text: 'a', type: 'text' },
{ text: 'b', type: 'text' },
{ marks: [{ type: 'strong' }], text: 'c', type: 'text' },
{ marks: [{ attrs: {}, type: 'strong' }], text: 'd', type: 'text' },
{ type: 'hardBreak' },
{ text: 'e', type: 'text' },
{ attrs: { localId: '01a0a06b-5281-7f27-9022-8d3a74b0ab0d' }, text: 'f', type: 'text' },
{ text: 'g', type: 'text' },
{ attrs: {}, text: 'h', type: 'text' },
]
assert.deepEqual(toEditorNormal({ content: [{ attrs: { panelType: 'info' }, content: [{ content, type: 'paragraph' }], type: 'panel' }], type: 'doc', version: 1 }), {
content: [
{
attrs: { panelType: 'info' },
content: [
{
content: [
{ text: 'ab', type: 'text' },
{ marks: [{ type: 'strong' }], text: 'cd', type: 'text' },
{ type: 'hardBreak' },
{ text: 'e', type: 'text' },
{ attrs: { localId: '01a0a06b-5281-7f27-9022-8d3a74b0ab0d' }, text: 'f', type: 'text' },
{ text: 'gh', type: 'text' },
],
type: 'paragraph',
},
],
type: 'panel',
},
],
type: 'doc',
version: 1,
})
})
test('reads negative zero as zero, as JSON does', () => {
const marks = [{ attrs: { size: -0 }, type: 'border' }]
assert.deepEqual(
toEditorNormal({ content: [{ attrs: { a: -0, b: [{ c: -0 }, null, 'd'] }, content: [{ marks, text: 'x', type: 'text' }], type: 'paragraph' }], type: 'doc', version: -0 }),
{ content: [{ attrs: { a: 0, b: [{ c: 0 }, null, 'd'] }, content: [{ marks: [{ attrs: { size: 0 }, type: 'border' }], text: 'x', type: 'text' }], type: 'paragraph' }], type: 'doc', version: 0 },
)
})
test('reads an empty attrs object, marks array or content array as the absent key', () => {
const paragraph: AdfNode = { attrs: {}, content: [{ attrs: {}, marks: [], text: 'a', type: 'text' }, { marks: [{ attrs: {}, type: 'em' }], text: 'b', type: 'text' }], marks: [], type: 'paragraph' }
assert.deepEqual(toEditorNormal({ content: [paragraph, { content: [], type: 'rule' }], type: 'doc', version: 1 }), {
content: [{ content: [{ text: 'a', type: 'text' }, { marks: [{ type: 'em' }], text: 'b', type: 'text' }], type: 'paragraph' }, { type: 'rule' }],
type: 'doc',
version: 1,
})
assert.deepEqual(toEditorNormal({ content: [], type: 'doc', version: 1 }), { type: 'doc', version: 1 })
})
test('normalizes blocks and mark attributes nesting far past the levels a recursive walk survives', () => {
const levels = 100000
let node: AdfNode = { content: [], type: 'paragraph' }
for (let level = 0; level < levels; level += 1) node = { content: [node], type: 'blockquote' }
let normal = toEditorNormal({ content: [node], type: 'doc', version: 1 }).content?.[0]
let depth = 0
for (; normal?.content !== undefined; depth += 1) normal = normal.content[0]
assert.equal(depth, levels)
assert.deepEqual(normal, { type: 'paragraph' })
let deep: JsonValue = 1
for (let level = 0; level < 2 * levels; level += 1) deep = [deep]
const marks = [{ attrs: { deep }, type: 'textColor' }]
const merged = toEditorNormal({ content: [{ content: [{ marks, text: 'a', type: 'text' }, { marks, text: 'b', type: 'text' }], type: 'paragraph' }], type: 'doc', version: 1 })
assert.deepEqual(merged.content?.[0]?.content?.map((text) => text.text), ['ab'])
})
+63 -5
View File
@@ -1,16 +1,21 @@
import type { AdfMark, AdfNode } from './document.ts' import type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from './document.ts'
import type { JsonValue } from '../json-value.ts'
import { nodeAttrs, nodeContent, nodeMarks } from './document.ts'
import { serializeCanonicalJson } from '../canonical-json.ts' import { serializeCanonicalJson } from '../canonical-json.ts'
type JsonContainer = JsonValue[] | { [key: string]: JsonValue }
type NodeHolder = { content?: AdfNode[] }
export function sameMark(candidate: AdfMark, mark: AdfMark): boolean { export function sameMark(candidate: AdfMark, mark: AdfMark): boolean {
return markKey(candidate) === markKey(mark) return markKey(candidate) === markKey(mark)
} }
// AGENTS.md §2: adjacent text nodes carrying identical marks are one node.
export function mergeAdjacentText(nodes: readonly AdfNode[]): AdfNode[] { export function mergeAdjacentText(nodes: readonly AdfNode[]): AdfNode[] {
const merged: AdfNode[] = [] const merged: AdfNode[] = []
for (const node of nodes) { for (const node of nodes) {
const previous = merged[merged.length - 1] const previous = merged[merged.length - 1]
if (previous !== undefined && previous.type === 'text' && node.type === 'text' && sameMarks(previous, node)) { if (previous !== undefined && mergesText(previous) && mergesText(node) && sameMarks(previous, node)) {
merged[merged.length - 1] = { ...previous, text: `${previous.text ?? ''}${node.text ?? ''}` } merged[merged.length - 1] = { ...previous, text: `${previous.text ?? ''}${node.text ?? ''}` }
continue continue
} }
@@ -19,8 +24,61 @@ export function mergeAdjacentText(nodes: readonly AdfNode[]): AdfNode[] {
return merged return merged
} }
export function toEditorNormal(document: AdfDocument): AdfDocument {
const normal: AdfDocument = { type: document.type, version: Object.is(document.version, -0) ? 0 : document.version }
const pending: { holder: NodeHolder; source: NodeHolder }[] = [{ holder: normal, source: document }]
for (let entry = pending.pop(); entry !== undefined; entry = pending.pop()) {
const content = mergeAdjacentText(nodeContent(entry.source))
if (content.length === 0) continue
entry.holder.content = content.map((source) => {
const holder = normalNode(source)
pending.push({ holder, source })
return holder
})
}
return normal
}
function normalNode(node: AdfNode): AdfNode {
const normal: AdfNode = { type: node.type }
const attrs = normalAttributes(nodeAttrs(node))
if (attrs !== undefined) normal.attrs = attrs
const marks = nodeMarks(node).map(normalMark)
if (marks.length > 0) normal.marks = marks
if (node.text !== undefined) normal.text = node.text
return normal
}
function normalMark(mark: AdfMark): AdfMark {
const attrs = normalAttributes(nodeAttrs(mark))
return attrs === undefined ? { type: mark.type } : { attrs, type: mark.type }
}
function normalAttributes(attrs: AdfAttributes): AdfAttributes | undefined {
if (Object.keys(attrs).length === 0) return undefined
const normal = { ...attrs }
const pending: JsonContainer[] = [normal]
for (let held = pending.pop(); held !== undefined; held = pending.pop()) {
if (Array.isArray(held)) for (const [index, value] of held.entries()) held[index] = normalValue(value, pending)
else for (const [key, value] of Object.entries(held)) held[key] = normalValue(value, pending)
}
return normal
}
function normalValue(value: JsonValue, pending: JsonContainer[]): JsonValue {
if (Object.is(value, -0)) return 0
if (value === null || typeof value !== 'object') return value
const copy = Array.isArray(value) ? [...value] : { ...value }
pending.push(copy)
return copy
}
function mergesText(node: AdfNode): boolean {
return node.type === 'text' && Object.keys(nodeAttrs(node)).length === 0
}
function sameMarks(previous: AdfNode, node: AdfNode): boolean { function sameMarks(previous: AdfNode, node: AdfNode): boolean {
return marksKey(previous.marks ?? []) === marksKey(node.marks ?? []) return marksKey(nodeMarks(previous)) === marksKey(nodeMarks(node))
} }
function marksKey(marks: readonly AdfMark[]): string { function marksKey(marks: readonly AdfMark[]): string {
@@ -28,5 +86,5 @@ function marksKey(marks: readonly AdfMark[]): string {
} }
function markKey(mark: AdfMark): string { function markKey(mark: AdfMark): string {
return `${mark.type} ${serializeCanonicalJson(mark.attrs ?? {}, 'compact')}` return `${mark.type} ${serializeCanonicalJson(nodeAttrs(mark), 'compact')}`
} }
+13
View File
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import test from 'node:test' import test from 'node:test'
import type { JsonValue } from './json-value.ts'
import { serializeCanonicalJson } from './canonical-json.ts' import { serializeCanonicalJson } from './canonical-json.ts'
test('sorts object keys recursively', () => { test('sorts object keys recursively', () => {
@@ -39,3 +40,15 @@ test('leaves non-ASCII raw', () => {
test('spells scalars in canonical JSON', () => { test('spells scalars in canonical JSON', () => {
assert.equal(serializeCanonicalJson([null, true, false, 0, -1.5, 'a"b'], 'compact'), '[null,true,false,0,-1.5,"a\\"b"]') assert.equal(serializeCanonicalJson([null, true, false, 0, -1.5, 'a"b'], 'compact'), '[null,true,false,0,-1.5,"a\\"b"]')
}) })
test('spells a value nesting far past the levels a recursive walk survives', () => {
const levels = 200000
let array: JsonValue = 1
let object: JsonValue = 1
for (let level = 0; level < levels; level += 1) {
array = [array]
object = { a: object }
}
assert.equal(serializeCanonicalJson(array, 'compact'), `${'['.repeat(levels)}1${']'.repeat(levels)}`)
assert.equal(serializeCanonicalJson(object, 'compact'), `${'{"a":'.repeat(levels)}1${'}'.repeat(levels)}`)
})
+32 -18
View File
@@ -2,28 +2,42 @@ import type { JsonValue } from './json-value.ts'
export type JsonSpelling = 'compact' | 'two-space' export type JsonSpelling = 'compact' | 'two-space'
type Member = { label: string; value: JsonValue }
type Pending = string | { depth: number; value: JsonValue }
export function serializeCanonicalJson(value: JsonValue, spelling: JsonSpelling): string { export function serializeCanonicalJson(value: JsonValue, spelling: JsonSpelling): string {
return serialize(value, spelling === 'compact' ? '' : ' ', 0) const indent = spelling === 'compact' ? '' : ' '
const text: string[] = []
const pending: Pending[] = [{ depth: 0, value }]
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
if (typeof next === 'string') {
text.push(next)
continue
}
const { depth, value: held } = next
if (Array.isArray(held)) schedule(pending, '[', held.map((item) => ({ label: '', value: item })), ']', indent, depth)
else if (held !== null && typeof held === 'object') schedule(pending, '{', objectMembers(held, indent), '}', indent, depth)
else text.push(JSON.stringify(held))
}
return text.join('')
} }
function serialize(value: JsonValue, indent: string, depth: number): string { function objectMembers(value: { [key: string]: JsonValue }, indent: string): Member[] {
if (Array.isArray(value)) {
if (value.length === 0) return '[]'
const items = value.map((item) => serialize(item, indent, depth + 1))
return `[${join(items, indent, depth)}]`
}
if (value !== null && typeof value === 'object') {
const keys = Object.keys(value).sort()
if (keys.length === 0) return '{}'
const separator = indent === '' ? ':' : ': ' const separator = indent === '' ? ':' : ': '
const entries = keys.map((key) => `${JSON.stringify(key)}${separator}${serialize(value[key] ?? null, indent, depth + 1)}`) return Object.keys(value)
return `{${join(entries, indent, depth)}}` .sort()
} .map((key) => ({ label: `${JSON.stringify(key)}${separator}`, value: value[key] ?? null }))
return JSON.stringify(value)
} }
function join(parts: readonly string[], indent: string, depth: number): string { function schedule(pending: Pending[], open: string, members: readonly Member[], close: string, indent: string, depth: number): void {
if (indent === '') return parts.join(',') if (members.length === 0) {
const inner = `\n${indent.repeat(depth + 1)}` pending.push(`${open}${close}`)
return `${inner}${parts.join(`,${inner}`)}\n${indent.repeat(depth)}` return
}
const inner = indent === '' ? '' : `\n${indent.repeat(depth + 1)}`
const scheduled: Pending[] = []
for (const [index, member] of members.entries()) scheduled.push(`${index === 0 ? open : ','}${inner}${member.label}`, { depth: depth + 1, value: member.value })
scheduled.push(indent === '' ? close : `\n${indent.repeat(depth)}${close}`)
for (const item of scheduled.reverse()) pending.push(item)
} }

Some files were not shown because too many files have changed in this diff Show More