Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ba2d09ee3 | |||
| 65c09bdbae | |||
| 302804cc4e | |||
| fe064efdb9 | |||
| 16af0b9592 | |||
| 4ac4a119e4 | |||
| ad1f3e996b | |||
| b6ba440410 | |||
| 7d20f3e895 | |||
| 5fe2e6ec42 | |||
| ef71d55c13 | |||
| a3073c69b2 | |||
| 8e7d429379 | |||
| 755b5cbe01 | |||
| fe47e6bc7b | |||
| aeb54d98d1 | |||
| 4735933d61 | |||
| ec5c92d4fd | |||
| fb10a1dc04 | |||
| 2901a28fcb | |||
| 047827fd86 | |||
| e23fd9495e | |||
| 10d63ff8e6 | |||
| f8149321f2 | |||
| a581e454ae | |||
| 342c2027d8 | |||
| fade9f51ea | |||
| b6930b45a4 | |||
| e166e969cb | |||
| 507f68aa13 | |||
| a2672bdc5b | |||
| 9303235434 | |||
| eafd863a32 | |||
| 1125715371 | |||
| 31156f4914 | |||
| 0185879943 | |||
| 9cd4b33020 | |||
| 758a5724e9 | |||
| 276433d635 | |||
| 7672531cdc | |||
| ec5f80e47f | |||
| da58a59f70 | |||
| 2796487b30 | |||
| dcea401ec2 | |||
| 25d851cc67 | |||
| 0f03edc8ef | |||
| 37f59cec40 | |||
| aa34d6e7fd | |||
| 3c1bacace6 | |||
| f8a5d5e9b3 | |||
| c9fb85f9db | |||
| 24b2e8cbbe | |||
| f66efabcbb | |||
| e424b35fd6 |
@@ -23,8 +23,8 @@ backticks read back as a fence — so a parse succeeding does not imply a spella
|
||||
`corpus/commonmark-spec/exceptions.json` names those.
|
||||
|
||||
"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
|
||||
key — the only domain markdown can restore.
|
||||
and no attributes merged, JSON number semantics, an empty attrs object, marks array or content
|
||||
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.
|
||||
|
||||
@@ -43,8 +43,11 @@ Round-trip equality is a property tested over a corpus, not a claim made in pros
|
||||
|
||||
## 4. The flavour
|
||||
|
||||
- Directives, one grammar for everything markdown lacks: `:::panel info` … `:::` blocks,
|
||||
`:mention[@Mikael]{id=5b10a2}` inline. Prior art: CommonMark's generic-directives proposal.
|
||||
- Directives, one grammar for everything markdown lacks, namespaced under `!adf:`: `!adf:panel info`
|
||||
… `!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
|
||||
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.
|
||||
@@ -63,8 +66,12 @@ module, so entity decoding is complete without one. The CommonMark spec suite is
|
||||
data and ships vendored at `corpus/commonmark-spec/` rather than as the `commonmark-spec` dev
|
||||
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. `devDependencies`: few, each earning its
|
||||
keep; they never reach a consumer.
|
||||
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
|
||||
|
||||
@@ -94,15 +101,17 @@ against the README's personas.
|
||||
|
||||
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.
|
||||
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
|
||||
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.
|
||||
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
|
||||
names the escape that unclaims the form claimed: `\:::` for a directive line, `\|` for every pipe
|
||||
row, `\:` for an inline directive.
|
||||
names the escape that unclaims the form claimed: `\!adf:` for a directive, block line and inline
|
||||
alike, `\|` for every pipe row.
|
||||
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
|
||||
cause; where one cause recurs across node types, across one mark's attributes or across
|
||||
@@ -112,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
|
||||
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:
|
||||
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
|
||||
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
|
||||
@@ -121,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
|
||||
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:
|
||||
a form the grammar does not have is a claim code — `::adf`, whose carry is the fence — and a
|
||||
well-formed form in the wrong place is `unsupported-node-shape`, `::listBreak` parting anything
|
||||
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`, `!adf:listBreak` parting anything
|
||||
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
|
||||
node tables refuse — an attribute a node does not hold or spells elsewhere, a value outside its
|
||||
@@ -204,9 +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.
|
||||
|
||||
A fourth engine reads the build rather than the source: a headless Firefox loads `dist/index.js`
|
||||
over HTTP and converts the round-trip, normalization and error fixtures — 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.
|
||||
over HTTP and converts the round-trip, normalization and error fixtures and the real payloads —
|
||||
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
|
||||
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.
|
||||
@@ -219,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
|
||||
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
|
||||
live Atlassian APIs; property-generated ADF trees; the CommonMark spec suite against
|
||||
`markdownToAdf` and `markdownToHtml`.
|
||||
The corpus, all checked in: hand-built fixtures per node and combination; real ADF Atlassian's
|
||||
editor wrote; the CommonMark spec suite against `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
|
||||
copy: each `- ` bullet in `## Block nodes`, `## Inline nodes` and `## Marks` declares the nodes
|
||||
@@ -231,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
|
||||
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
|
||||
|
||||
- Two-space indent, strict TypeScript, English everywhere. Alphabetical order wherever order
|
||||
|
||||
@@ -3,6 +3,11 @@ try {
|
||||
window.convertCorpus = (corpus) => ({
|
||||
errors: corpus.errors.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 }) => {
|
||||
const adf = JSON.parse(json)
|
||||
return { emitted: adfToMarkdown(adf), isDocument: isAdfDocument(adf), name, parsed: markdownToAdf(markdown) }
|
||||
|
||||
+16
-4
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict'
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
import { createServer } from 'node:http'
|
||||
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 driver = 'http://127.0.0.1:4444'
|
||||
@@ -66,6 +67,7 @@ async function whenDriverAnswers() {
|
||||
const corpus = {
|
||||
errors: fixtureNames('errors', '.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 })),
|
||||
}
|
||||
assert.deepEqual(
|
||||
@@ -74,7 +76,7 @@ assert.deepEqual(
|
||||
.map((entry) => entry.name)
|
||||
.filter((name) => name !== 'commonmark-spec')
|
||||
.sort(),
|
||||
['errors', 'normalization', 'round-trip'],
|
||||
['errors', 'normalization', 'real-payloads', 'round-trip'],
|
||||
'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`)
|
||||
@@ -105,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.equal(result.emitted.value, markdown)
|
||||
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))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -113,7 +115,17 @@ for (const [index, { name }] of corpus.normalization.entries()) {
|
||||
const result = results.normalization[index]
|
||||
checking(name, result, () => {
|
||||
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))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -126,5 +138,5 @@ for (const [index, { name }] of corpus.errors.entries()) {
|
||||
}
|
||||
|
||||
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`,
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ if printf '%s' "$test_output" | grep -q 'ℹ tests 0'; then
|
||||
exit 1
|
||||
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 "$node_image" npm run build
|
||||
|
||||
+2
-2
@@ -10,8 +10,8 @@ One directory per contract kind, each landing with its milestone:
|
||||
markdown is not canonical.
|
||||
- `errors/` — `<name>.md`: markdown input that must not convert. A `<name>.error` beside it
|
||||
pins which error.
|
||||
- `real-payloads/` — `<name>.json`: sanitized live ADF, round-tripped ADF→markdown→ADF. No
|
||||
expected markdown.
|
||||
- `real-payloads/` — `<name>.json`: ADF Atlassian's editor wrote, round-tripped ADF→markdown→ADF.
|
||||
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
|
||||
|
||||
@@ -1 +1 @@
|
||||
_a :adf{json="{\"type\":\"blockCard\"}"} b_
|
||||
_a !adf:carry{json="{\"type\":\"blockCard\"}"} b_
|
||||
|
||||
@@ -1 +1 @@
|
||||
:adf{json="{"}
|
||||
!adf:carry{json="{"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
:adf{json="[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"}
|
||||
!adf:carry{json="[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
:::adf
|
||||
:::carry
|
||||
{"type":"rule"}
|
||||
:::
|
||||
@@ -1 +1 @@
|
||||
Part :mention[**@Mikael**]{id=b1c2}.
|
||||
Part !adf:mention[**@Mikael**]{id=b1c2}.
|
||||
|
||||
@@ -1 +1 @@
|
||||
Part :mention[@A and more.
|
||||
Part !adf:mention[@A and more.
|
||||
|
||||
@@ -1 +1 @@
|
||||
Part :em[emphasised] here.
|
||||
Part !adf:em[emphasised] here.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
malformed-directive
|
||||
@@ -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 @@
|
||||
Part :text{text=plain} here.
|
||||
Part !adf:text{text=plain} here.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,5 +2,5 @@
|
||||
| --- | --- |
|
||||
| Bolt M8 | Grade `8.8` |
|
||||
| Nut \| washer | Sold as a pair |
|
||||
| Washer M8 | 100 pcs:hardBreak{}zinc-plated |
|
||||
| :text{text=" "}Spare | |
|
||||
| Washer M8 | 100 pcs!adf:hardBreak{}zinc-plated |
|
||||
| !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\"}"}
|
||||
|
||||
[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": [
|
||||
{
|
||||
"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"
|
||||
}
|
||||
],
|
||||
@@ -54,7 +63,7 @@
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": ":status[nope]{color=red} vs ",
|
||||
"text": "!adf:status[nope]{color=red} vs ",
|
||||
"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}
|
||||
|
||||
@@ -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.
|
||||
|
||||
: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
|
||||
|
||||
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 |
|
||||
| --- | --- |
|
||||
@@ -11,7 +11,7 @@ Shipped :emoji[🚀]{shortName=":rocket:"} on :date{timestamp=1756080000000} —
|
||||
- Check the collation:
|
||||
- `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~~
|
||||
|
||||
@@ -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**_
|
||||
|
||||
[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 |
|
||||
| --- | --- |
|
||||
| :text{text=" "}lead | |
|
||||
| :status[ok]{style="a\u007cb"} | trail:text{text=" "} |
|
||||
| !adf:text{text=" "}lead | |
|
||||
| !adf:status[ok]{style="a\u007cb"} | trail!adf:text{text=" "} |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Line one\
|
||||
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 @@
|
||||
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 @@
|
||||
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.
|
||||
|
||||
@@ -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 @@
|
||||
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 @@
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": ":",
|
||||
"text": "!adf:",
|
||||
"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
@@ -7,7 +7,7 @@ node_image=node:24.19.0-alpine3.24
|
||||
in_image() {
|
||||
local image=$1 entrypoint=$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() {
|
||||
|
||||
Generated
+41
@@ -10,6 +10,7 @@
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "24.13.3",
|
||||
"fast-check": "4.10.0",
|
||||
"typescript": "7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -366,6 +367,46 @@
|
||||
"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": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
|
||||
|
||||
+2
-1
@@ -23,11 +23,12 @@
|
||||
},
|
||||
"scripts": {
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "24.13.3",
|
||||
"fast-check": "4.10.0",
|
||||
"typescript": "7.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
Copyright 2019 Atlassian Pty Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Atlassian's ADF JSON Schemas
|
||||
|
||||
`full.json` and `stage-0.json` are byte-exact from `dist/json-schema/v1/` in
|
||||
[`@atlaskit/adf-schema` 57.4.9](https://registry.npmjs.org/@atlaskit/adf-schema/-/adf-schema-57.4.9.tgz)
|
||||
(© Atlassian Pty Ltd, Apache-2.0: `LICENSE` is the package's own notice, `LICENSE-2.0.txt` the
|
||||
licence it names).
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+140
-122
@@ -3,7 +3,7 @@
|
||||
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
|
||||
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
|
||||
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.
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
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 `>`.
|
||||
- ATX headings (`#` … `######`); setext input normalizes to ATX.
|
||||
- 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
|
||||
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.
|
||||
- An empty paragraph — real payloads carry them — is `::paragraph`.
|
||||
- Links `[text](url)`; `<…>` around a destination containing spaces; title in double quotes. A
|
||||
backslash escapes a parenthesis the destination leaves 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).
|
||||
- An empty paragraph — real payloads carry them — is an `!adf:paragraph` … `!adf:/paragraph` pair
|
||||
holding nothing.
|
||||
- Links `[text](url)`; `<…>` around a destination containing spaces, `<>` an empty one beside a
|
||||
title; title in double quotes. A backslash escapes a parenthesis the destination leaves
|
||||
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.
|
||||
- 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
|
||||
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
|
||||
`*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.
|
||||
@@ -62,56 +73,62 @@ normalizes to it through the round-trip.
|
||||
|
||||
## Directives
|
||||
|
||||
One grammar for everything CommonMark lacks. A directive name is `[a-z][A-Za-z0-9]*` — the ADF
|
||||
node and mark names the sections below spell as directives. Recognition is syntactic and
|
||||
name-set-independent: anything matching the forms below parses as a directive regardless of
|
||||
whether the name is known, and an unknown name is an error result naming it — so output an old
|
||||
emitter escaped stays escaped, and erroring input gaining meaning later is MINOR, never a reparse
|
||||
(§8). Each name belongs to one position, and a name the other one spells — a mark or an inline
|
||||
node written as a block directive, a block node written inline — is a different error, naming the
|
||||
spelling it takes. Two reserved names read back to no node: `adf` for the opaque carry, as both
|
||||
directive name and fence info string, and `listBreak` for the leaf that parts two adjacent lists
|
||||
(Canonical form).
|
||||
One grammar for everything CommonMark lacks, namespaced: every directive opens with the literal
|
||||
`!adf:`. A directive name is `[a-z][A-Za-z0-9]*` — the ADF node and mark names the sections below
|
||||
spell as directives. Recognition is syntactic and name-set-independent: anything matching the forms
|
||||
below parses as a directive regardless of whether the name is known, and an unknown name is an
|
||||
error result naming it at the opener, whatever follows it — so output an old emitter escaped stays
|
||||
escaped, and erroring input gaining meaning later is MINOR, never a reparse (§8). Each name belongs
|
||||
to one position, and a name the other one spells — a mark or an inline node written as a block
|
||||
directive, a block node written inline — is a different error, naming the spelling it takes. Two
|
||||
reserved names read back to no node: `carry` for the opaque carry, as both directive name and fence
|
||||
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
|
||||
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
|
||||
directive only when the name is followed immediately by `[` or `{`, and `{attrs}` must follow
|
||||
`]` (or the name) with no gap — anything else (`10:30`, `:smile:`, a stray `{…}` in text) is
|
||||
literal text. An inline directive binds as a unit before bracket matching, the way a code span
|
||||
does: a `]` or `(` inside its `{attrs}` is the directive's, never the enclosing content's, and a
|
||||
`(` after its closing `]` opens no link.
|
||||
survives inline parsing. Each section below says whether content is required, and `{attrs}` must
|
||||
follow `]` (or the name) with no gap. An inline directive binds as a unit before bracket matching,
|
||||
the way a code span 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
|
||||
:::
|
||||
!adf:/name
|
||||
```
|
||||
|
||||
The fence is three or more colons. `arg` is one optional bare token whose meaning each node
|
||||
defines (e.g. the panel type). The body is block markdown. The closing fence is a line of at
|
||||
least the opening's length and closes the innermost open container however long its run, and a
|
||||
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
|
||||
fence or opaque carry is content. Canonical form uses minimal lengths.
|
||||
Directive fence lines follow code-fence indentation (up to three spaces relative to their
|
||||
container).
|
||||
`arg` is one optional bare token whose meaning each node defines (e.g. the panel type). The body is
|
||||
block markdown. The closer names the innermost open container and carries nothing after the name;
|
||||
one naming another node, or standing where no container is open, is a named error. Opening and
|
||||
closing is what nests, so the opener reads the same at every depth, and a closer crosses no list
|
||||
item or blockquote edge — a container opened inside one closes inside it. Directive block lines
|
||||
follow code-fence indentation (up to three spaces relative to their container); an `!adf:` inside a
|
||||
code fence or opaque carry is content.
|
||||
|
||||
**Leaf block**: `::name arg {attrs}` — a block-position node with no body, `arg` reading as
|
||||
above.
|
||||
**Block leaf**: `!adf:name arg {attrs}` — the same opener with no closer, `arg` reading as 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}`,
|
||||
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.
|
||||
|
||||
**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
|
||||
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
|
||||
@@ -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
|
||||
assigns types.
|
||||
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
|
||||
claims the directive (`: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
|
||||
`[content]` already claims, and a number or `json` value outside its canonical JSON spelling are
|
||||
each a named error naming the spelling to write instead.
|
||||
quotes in the shortest form each escape has, and omits empty `{attrs}` except where the `{` is what
|
||||
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}` on a block line
|
||||
or after a `[content]`, and a number or `json` value outside its canonical JSON spelling are each a
|
||||
named error naming the spelling to write instead.
|
||||
|
||||
**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
|
||||
after a directive's closing `]`, which would otherwise be read as the attributes it has none
|
||||
of; outside code spans and code blocks, a backslash before `:` in input yields a literal colon.
|
||||
directive syntax — every literal `!adf:`, `]` inside content, a bracket a link's destination and
|
||||
title inside content leave unbalanced, a backtick there that would open a code span, a `{` right
|
||||
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
|
||||
fence line of the container's length or longer, a bare colon-run line outside any container or
|
||||
shorter than the fence it would close, an inline `[content]` or `{attrs}` left unclosed at end of
|
||||
line, unparseable or duplicate-keyed attrs, invalid JSON in an opaque carry. Never a silent
|
||||
literal-text fallback — a typo that reparses as prose is the silent loss §2 refuses.
|
||||
**Malformed directives are error results**, named: an unclosed container at end of input, a closer
|
||||
naming no open container or a node other than the innermost open one, a leaf given a body, an
|
||||
`!adf:` completing no directive, an inline `[content]` or `{attrs}` left unclosed at end of line,
|
||||
unparseable or duplicate-keyed attrs, invalid JSON in an opaque carry. Never a silent literal-text
|
||||
fallback — a typo that reparses as prose is the silent loss §2 refuses.
|
||||
|
||||
## 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
|
||||
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.
|
||||
- **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.
|
||||
|
||||
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
|
||||
stays absolute.
|
||||
In block-directive positions (`::adf`, `:::adf`) the reserved name is a named error — the
|
||||
carry's block form is the fence.
|
||||
In block-directive position `!adf:carry` is a named error — the carry's block form is the fence.
|
||||
|
||||
## 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.
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
content is the node's. Attributes: `hideLineNumbers` (boolean), `language` (string), `localId`
|
||||
(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
|
||||
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
|
||||
@@ -215,11 +232,11 @@ form.
|
||||
- `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
|
||||
fn main() {}
|
||||
```
|
||||
:::
|
||||
!adf:/codeBlock
|
||||
````
|
||||
|
||||
### Panel
|
||||
@@ -230,9 +247,9 @@ fn main() {}
|
||||
panels.
|
||||
|
||||
```
|
||||
:::panel warning
|
||||
!adf:panel warning
|
||||
Check the collation before importing.
|
||||
:::
|
||||
!adf:/panel
|
||||
```
|
||||
|
||||
### Expand
|
||||
@@ -241,9 +258,9 @@ Check the collation before importing.
|
||||
which. Attributes: `localId` (string), `title` (string).
|
||||
|
||||
```
|
||||
:::expand {title="Full build log"}
|
||||
!adf:expand {title="Full build log"}
|
||||
…
|
||||
:::
|
||||
!adf:/expand
|
||||
```
|
||||
|
||||
### The media family
|
||||
@@ -252,19 +269,19 @@ Check the collation before importing.
|
||||
(string), `localId` (string), `occurrenceKey` (string), `type` (`external` `file` `link`),
|
||||
`url` (string), `width` (number). `file` and `link` media carry `collection` + `id`;
|
||||
`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`),
|
||||
`localId` (string), `width` (number), `widthType` (`percentage` `pixel`).
|
||||
- `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}
|
||||
::media {collection=MediaServicesSample id=4478e39c-cf9b-41d1-ba92-68589487cd75 type=file}
|
||||
:::caption
|
||||
!adf:mediaSingle {layout=center width=50}
|
||||
!adf:media {collection=MediaServicesSample id=4478e39c-cf9b-41d1-ba92-68589487cd75 type=file}
|
||||
!adf:caption
|
||||
The moon, at night.
|
||||
:::
|
||||
::::
|
||||
!adf:/caption
|
||||
!adf:/mediaSingle
|
||||
```
|
||||
|
||||
**The CommonMark image.** A paragraph whose entire inline content is one image `` 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 `|`
|
||||
(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
|
||||
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.
|
||||
|
||||
The directive form nests cells as containers of block content inside `tableRow` containers:
|
||||
|
||||
```
|
||||
:::::table {isNumberColumnEnabled=true width=760}
|
||||
::::tableRow
|
||||
:::tableHeader {colspan=2 colwidth="[340,420]"}
|
||||
!adf:table {isNumberColumnEnabled=true width=760}
|
||||
!adf:tableRow
|
||||
!adf:tableHeader {colspan=2 colwidth="[340,420]"}
|
||||
Assembly
|
||||
:::
|
||||
::::
|
||||
::::tableRow
|
||||
:::tableCell {background="#deebff"}
|
||||
!adf:/tableHeader
|
||||
!adf:/tableRow
|
||||
!adf:tableRow
|
||||
!adf:tableCell {background="#deebff"}
|
||||
Bolt M8
|
||||
:::
|
||||
:::tableCell {valign=top}
|
||||
!adf:/tableCell
|
||||
!adf:tableCell {valign=top}
|
||||
40
|
||||
:::
|
||||
::::
|
||||
:::::
|
||||
!adf:/tableCell
|
||||
!adf:/tableRow
|
||||
!adf:/table
|
||||
```
|
||||
|
||||
- `table` — container of `tableRow` containers. Attributes: `displayMode` (`default` `fixed`),
|
||||
@@ -351,14 +368,14 @@ Bolt M8
|
||||
free-form; the editor writes `DECIDED`).
|
||||
|
||||
```
|
||||
::::taskList {localId=0198f3a2-7c41-7f2e-9b3a-4d8e2c1a6b90}
|
||||
:::taskItem DONE {localId=0198f3a2-8d52-70b1-8c4f-5e9f3d2b7ca1}
|
||||
!adf:taskList {localId=0198f3a2-7c41-7f2e-9b3a-4d8e2c1a6b90}
|
||||
!adf:taskItem DONE {localId=0198f3a2-8d52-70b1-8c4f-5e9f3d2b7ca1}
|
||||
Write the spec
|
||||
:::
|
||||
:::taskItem TODO {localId=0198f3a2-9e63-7d80-a15b-6fa04e3c8db2}
|
||||
!adf:/taskItem
|
||||
!adf:taskItem TODO {localId=0198f3a2-9e63-7d80-a15b-6fa04e3c8db2}
|
||||
Ship it
|
||||
:::
|
||||
::::
|
||||
!adf:/taskItem
|
||||
!adf:/taskList
|
||||
```
|
||||
|
||||
### Layout
|
||||
@@ -368,14 +385,14 @@ Ship it
|
||||
`middle` `top`), `width` (number — percent).
|
||||
|
||||
```
|
||||
::::layoutSection
|
||||
:::layoutColumn {width=50}
|
||||
!adf:layoutSection
|
||||
!adf:layoutColumn {width=50}
|
||||
Left.
|
||||
:::
|
||||
:::layoutColumn {width=50}
|
||||
!adf:/layoutColumn
|
||||
!adf:layoutColumn {width=50}
|
||||
Right.
|
||||
:::
|
||||
::::
|
||||
!adf:/layoutColumn
|
||||
!adf:/layoutSection
|
||||
```
|
||||
|
||||
### Extensions
|
||||
@@ -387,7 +404,7 @@ Right.
|
||||
- `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
|
||||
@@ -396,18 +413,18 @@ Right.
|
||||
`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
|
||||
|
||||
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
|
||||
as 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 —
|
||||
is a named error, and so is a `text` key in `{attrs}`. An enclosing mark spelling does not reach
|
||||
into the slot. The rest take no content, `:text` included; content on a node that takes none is a
|
||||
named error.
|
||||
the nodes below, `emoji`, `mention` and `status` spell their `text` attribute in the content slot as
|
||||
plain text: `[]` is the empty string, absent content is the absent attribute, non-empty content
|
||||
parsing to anything but one text node carrying neither marks, attributes nor content — adjacent text
|
||||
nodes with identical marks and no attributes merged first — is a named error, and so is a `text` key
|
||||
in `{attrs}`. An enclosing mark spelling does not reach into the slot. The rest take no content,
|
||||
`!adf:text` included; content on a node that takes none is a named error.
|
||||
|
||||
- `date` — Attributes: `localId` (string), `timestamp` (string, epoch milliseconds).
|
||||
- `emoji` — Attributes: `id` (string), `localId` (string), `shortName` (string, `:name:`), `text`
|
||||
@@ -424,30 +441,30 @@ named error.
|
||||
(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
|
||||
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
|
||||
`: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
|
||||
the rest plain text; `markdownToAdf` merges adjacent text nodes carrying identical marks
|
||||
(AGENTS.md §2). Input reads that spelling alone: the value is one run of spaces and tabs, or one
|
||||
run of newlines, and anything else — a mixed run, or text CommonMark carries plainly — is a named
|
||||
error.
|
||||
the rest plain text; `markdownToAdf` merges adjacent text nodes carrying identical marks and no
|
||||
attributes (AGENTS.md §2). Input reads that spelling alone: the value is one run of spaces and
|
||||
tabs, or one run of newlines, and anything else — a mixed run, or text CommonMark carries plainly —
|
||||
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
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
- `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.
|
||||
|
||||
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
|
||||
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
|
||||
@@ -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).
|
||||
|
||||
```
|
||||
: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}
|
||||
```
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
})
|
||||
@@ -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
@@ -52,8 +52,8 @@ export function attributeNestingMessage(key: string, type: string, levels: numbe
|
||||
}
|
||||
|
||||
export function carriesOnly(node: AdfNode, attributes: readonly string[]): boolean {
|
||||
if ((node.marks ?? []).length > 0 || node.text !== undefined) return false
|
||||
return holdsOnly(node.attrs ?? {}, attributes)
|
||||
if (nodeMarks(node).length > 0 || node.text !== undefined) return false
|
||||
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.
|
||||
@@ -72,6 +72,18 @@ export function isAdfMark(value: unknown): value is AdfMark {
|
||||
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[] {
|
||||
const pending: unknown[] = [...value]
|
||||
while (pending.length > 0) {
|
||||
@@ -95,23 +107,23 @@ function nestingFault(nodes: readonly AdfNode[]): ConvertFault | undefined {
|
||||
while (pending.length > 0) {
|
||||
const node = pending.pop()
|
||||
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
|
||||
pending.push(...(node.content ?? []))
|
||||
pending.push(...nodeContent(node))
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function marksFault(marks: readonly AdfMark[] | undefined): ConvertFault | undefined {
|
||||
for (const mark of marks ?? []) {
|
||||
const fault = attributesFault(mark.attrs, mark.type, markAttributeNesting)
|
||||
function marksFault(marks: readonly AdfMark[]): ConvertFault | undefined {
|
||||
for (const mark of marks) {
|
||||
const fault = attributesFault(nodeAttrs(mark), mark.type, markAttributeNesting)
|
||||
if (fault !== undefined) return fault
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function attributesFault(attrs: AdfAttributes | undefined, type: string, levels: number = largestNesting): ConvertFault | undefined {
|
||||
for (const [key, value] of Object.entries(attrs ?? {})) {
|
||||
function attributesFault(attrs: AdfAttributes, type: string, levels: number = largestNesting): ConvertFault | undefined {
|
||||
for (const [key, value] of Object.entries(attrs)) {
|
||||
if (overNested(value, levels)) return { code: 'unsupported-nesting-depth', message: attributeNestingMessage(key, type, levels) }
|
||||
}
|
||||
return undefined
|
||||
|
||||
@@ -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'])
|
||||
})
|
||||
@@ -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'
|
||||
|
||||
type JsonContainer = JsonValue[] | { [key: string]: JsonValue }
|
||||
|
||||
type NodeHolder = { content?: AdfNode[] }
|
||||
|
||||
export function sameMark(candidate: AdfMark, mark: AdfMark): boolean {
|
||||
return markKey(candidate) === markKey(mark)
|
||||
}
|
||||
|
||||
// AGENTS.md §2: adjacent text nodes carrying identical marks are one node.
|
||||
export function mergeAdjacentText(nodes: readonly AdfNode[]): AdfNode[] {
|
||||
const merged: AdfNode[] = []
|
||||
for (const node of nodes) {
|
||||
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 ?? ''}` }
|
||||
continue
|
||||
}
|
||||
@@ -19,8 +24,61 @@ export function mergeAdjacentText(nodes: readonly AdfNode[]): AdfNode[] {
|
||||
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 {
|
||||
return marksKey(previous.marks ?? []) === marksKey(node.marks ?? [])
|
||||
return marksKey(nodeMarks(previous)) === marksKey(nodeMarks(node))
|
||||
}
|
||||
|
||||
function marksKey(marks: readonly AdfMark[]): string {
|
||||
@@ -28,5 +86,5 @@ function marksKey(marks: readonly AdfMark[]): string {
|
||||
}
|
||||
|
||||
function markKey(mark: AdfMark): string {
|
||||
return `${mark.type} ${serializeCanonicalJson(mark.attrs ?? {}, 'compact')}`
|
||||
return `${mark.type} ${serializeCanonicalJson(nodeAttrs(mark), 'compact')}`
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import type { JsonValue } from './json-value.ts'
|
||||
import { serializeCanonicalJson } from './canonical-json.ts'
|
||||
|
||||
test('sorts object keys recursively', () => {
|
||||
@@ -39,3 +40,15 @@ test('leaves non-ASCII raw', () => {
|
||||
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"]')
|
||||
})
|
||||
|
||||
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)}`)
|
||||
})
|
||||
|
||||
+33
-19
@@ -2,28 +2,42 @@ import type { JsonValue } from './json-value.ts'
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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 entries = keys.map((key) => `${JSON.stringify(key)}${separator}${serialize(value[key] ?? null, indent, depth + 1)}`)
|
||||
return `{${join(entries, indent, depth)}}`
|
||||
}
|
||||
return JSON.stringify(value)
|
||||
function objectMembers(value: { [key: string]: JsonValue }, indent: string): Member[] {
|
||||
const separator = indent === '' ? ':' : ': '
|
||||
return Object.keys(value)
|
||||
.sort()
|
||||
.map((key) => ({ label: `${JSON.stringify(key)}${separator}`, value: value[key] ?? null }))
|
||||
}
|
||||
|
||||
function join(parts: readonly string[], indent: string, depth: number): string {
|
||||
if (indent === '') return parts.join(',')
|
||||
const inner = `\n${indent.repeat(depth + 1)}`
|
||||
return `${inner}${parts.join(`,${inner}`)}\n${indent.repeat(depth)}`
|
||||
function schedule(pending: Pending[], open: string, members: readonly Member[], close: string, indent: string, depth: number): void {
|
||||
if (members.length === 0) {
|
||||
pending.push(`${open}${close}`)
|
||||
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)
|
||||
}
|
||||
|
||||
+22
-16
@@ -9,10 +9,12 @@ import { isAdfDocument } from './adf/document.ts'
|
||||
import { isJsonValue } from './json-value.ts'
|
||||
import { markdownToAdf } from './markdown/parse/markdown-to-adf.ts'
|
||||
import { serializeCanonicalJson } from './canonical-json.ts'
|
||||
import { toEditorNormal } from './adf/editor-normal.ts'
|
||||
|
||||
const corpusRoot = join(dirname(fileURLToPath(import.meta.url)), '..', 'corpus')
|
||||
const errorsRoot = join(corpusRoot, 'errors')
|
||||
const normalizationRoot = join(corpusRoot, 'normalization')
|
||||
const realPayloadsRoot = join(corpusRoot, 'real-payloads')
|
||||
const roundTripRoot = join(corpusRoot, 'round-trip')
|
||||
|
||||
const roundTripDirectories = ['block-nodes', 'combinations', 'commonmark-subset', 'inline-nodes', 'opaque-carry']
|
||||
@@ -59,7 +61,7 @@ function corpusJsonPaths(): string[] {
|
||||
}
|
||||
|
||||
test('every corpus directory is a kind the runner reads', () => {
|
||||
assert.deepEqual(directoryNames(corpusRoot), ['commonmark-spec', 'errors', 'normalization', 'round-trip'])
|
||||
assert.deepEqual(directoryNames(corpusRoot), ['commonmark-spec', 'errors', 'normalization', 'real-payloads', 'round-trip'])
|
||||
})
|
||||
|
||||
test('every round-trip directory is a kind the runner reads', () => {
|
||||
@@ -95,7 +97,7 @@ for (const directory of roundTripDirectories) {
|
||||
assert.ok(isAdfDocument(expected), `${name}.json is not an ADF document`)
|
||||
const result = markdownToAdf(readFileSync(join(roundTripRoot, directory, `${name}.md`), 'utf8'))
|
||||
assert.ok(result.ok, result.ok ? '' : `${result.error.code}: ${result.error.message}`)
|
||||
assert.deepEqual(result.value, expected)
|
||||
assert.deepEqual(toEditorNormal(result.value), expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -106,18 +108,6 @@ function roundTripFixtures(): { name: string; path: string }[] {
|
||||
)
|
||||
}
|
||||
|
||||
test('no two round-trip documents share one markdown spelling', () => {
|
||||
const spellings = new Map<string, string>()
|
||||
for (const fixture of roundTripFixtures()) {
|
||||
const parsed: unknown = JSON.parse(readFileSync(fixture.path, 'utf8'))
|
||||
assert.ok(isAdfDocument(parsed), `${fixture.name} is not an ADF document`)
|
||||
const result = adfToMarkdown(parsed)
|
||||
assert.ok(result.ok, result.ok ? '' : `${result.error.code}: ${result.error.message}`)
|
||||
assert.equal(spellings.get(result.value), undefined, `${fixture.name} and ${spellings.get(result.value)} share one markdown spelling`)
|
||||
spellings.set(result.value, fixture.name)
|
||||
}
|
||||
})
|
||||
|
||||
test('no round-trip fixture repeats the document another holds', () => {
|
||||
const documents = new Map<string, string>()
|
||||
for (const fixture of roundTripFixtures()) {
|
||||
@@ -184,12 +174,28 @@ for (const name of pairedNames(normalizationRoot, '.md', '.json')) {
|
||||
assert.ok(isAdfDocument(expected), `${name}.json is not an ADF document`)
|
||||
const result = markdownToAdf(readFileSync(join(normalizationRoot, `${name}.md`), 'utf8'))
|
||||
assert.ok(result.ok, result.ok ? '' : `${result.error.code}: ${result.error.message}`)
|
||||
assert.deepEqual(result.value, expected)
|
||||
assert.deepEqual(toEditorNormal(result.value), expected)
|
||||
const emitted = adfToMarkdown(result.value)
|
||||
assert.ok(emitted.ok, emitted.ok ? '' : `${emitted.error.code}: ${emitted.error.message}`)
|
||||
const again = markdownToAdf(emitted.value)
|
||||
assert.ok(again.ok, again.ok ? '' : `${again.error.code}: ${again.error.message}`)
|
||||
assert.deepEqual(again.value, expected)
|
||||
assert.deepEqual(toEditorNormal(again.value), expected)
|
||||
})
|
||||
}
|
||||
|
||||
test('real-payloads holds payloads', () => {
|
||||
assert.ok(names(realPayloadsRoot, '.json').length > 0)
|
||||
})
|
||||
|
||||
for (const name of names(realPayloadsRoot, '.json')) {
|
||||
test(`real-payloads/${name} emits markdown that reads back to it`, () => {
|
||||
const payload: unknown = JSON.parse(readFileSync(join(realPayloadsRoot, `${name}.json`), 'utf8'))
|
||||
assert.ok(isAdfDocument(payload), `${name}.json is not an ADF document`)
|
||||
const emitted = adfToMarkdown(payload)
|
||||
assert.ok(emitted.ok, emitted.ok ? '' : `${emitted.error.code}: ${emitted.error.message}`)
|
||||
const parsed = markdownToAdf(emitted.value)
|
||||
assert.ok(parsed.ok, parsed.ok ? '' : `${parsed.error.code}: ${parsed.error.message}`)
|
||||
assert.deepEqual(toEditorNormal(parsed.value), payload)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
import fc from 'fast-check'
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import type { AdfDocument } from './adf/document.ts'
|
||||
import type { Arbitrary, DepthIdentifier } from 'fast-check'
|
||||
import type { AttributeVocabulary } from './adf/attribute-vocabulary.ts'
|
||||
import type { JsonValue } from './json-value.ts'
|
||||
import type { Result } from './result.ts'
|
||||
import { adfDocument, attributes, jsonKey, jsonValue, markdownPieces, propertyRuns, propertyTimeout, textOf } from './property-harness.ts'
|
||||
import { adfToMarkdown } from './markdown/emit/adf-to-markdown.ts'
|
||||
import { blockArgument } from './markdown/block-directive-arguments.ts'
|
||||
import { blockDirectives } from './adf/block-directives.ts'
|
||||
import { carryFence, carryName } from './markdown/opaque-carry.ts'
|
||||
import { directivePrefix, spellAttributes, spellInlineDirectiveOpener, spellJsonAttribute, spellLeafDirective, spellStringAttribute, spellVocabulary } from './markdown/directive-syntax.ts'
|
||||
import { fencedCodeBlock } from './markdown/backtick-runs.ts'
|
||||
import { inlineDirectives } from './adf/inline-directives.ts'
|
||||
import { listBreakName } from './markdown/list-break.ts'
|
||||
import { markAttributes } from './adf/mark-attributes.ts'
|
||||
import { markSpelling } from './markdown/mark-spellings.ts'
|
||||
import { markdownToAdf } from './markdown/parse/markdown-to-adf.ts'
|
||||
import { marksAttribute } from './markdown/block-directive-marks.ts'
|
||||
import { nodeContent, nodeMarks } from './adf/document.ts'
|
||||
import { serializeCanonicalJson } from './canonical-json.ts'
|
||||
import { textDirectiveName } from './markdown/text-directive.ts'
|
||||
import { toEditorNormal } from './adf/editor-normal.ts'
|
||||
import { vocabularyPairs } from './adf/attribute-vocabulary.ts'
|
||||
|
||||
type Choice = { arbitrary: Arbitrary<string>; hostile?: true; weight: number }
|
||||
|
||||
type Edit = [at: number, removed: number, inserted: string]
|
||||
|
||||
type InlineMarkdown = { destination: Arbitrary<string>; inlines: Arbitrary<string>; label: Arbitrary<string>; oneLine: Arbitrary<string>; text: Arbitrary<string>; word: Arbitrary<string> }
|
||||
|
||||
type LeafMarkdown = { fencedCode: Arbitrary<string>; leafBlock: Arbitrary<string> }
|
||||
|
||||
const commonMarkTypes = new Set(['blockquote', 'bulletList', 'codeBlock', 'hardBreak', 'heading', 'listItem', 'orderedList', 'paragraph', 'rule', 'text'])
|
||||
const directiveShapedFloor = 330
|
||||
const fixpointFloor = 600
|
||||
const gateRuns = 1000
|
||||
const markdownMarkTypes = new Set(Object.keys(markAttributes).filter((type) => markSpelling(type)?.kind !== 'directive'))
|
||||
|
||||
const vocabularies = [...Object.values(blockDirectives).map((directive) => directive.attributes), ...Object.values(inlineDirectives).map((directive) => directive.attributes), ...Object.values(markAttributes)]
|
||||
const attributeKeys = [
|
||||
...new Set([...vocabularies.flatMap((vocabulary) => Object.keys(vocabulary)), ...Object.keys(blockDirectives).flatMap((type) => blockArgument(type) ?? []), marksAttribute, 'json', textDirectiveName]),
|
||||
]
|
||||
const directiveNames = [...Object.keys(blockDirectives), ...Object.keys(inlineDirectives), ...Object.keys(markAttributes), carryName, listBreakName, textDirectiveName]
|
||||
|
||||
// Hostile generation reaches refusals; clean generation holds none a single piece would trip, so a whole document reaches the emitter.
|
||||
function choose(hostile: boolean, choices: readonly Choice[], depth?: { depthIdentifier: DepthIdentifier; maxDepth: number }): Arbitrary<string> {
|
||||
const held = choices.filter((choice) => hostile || choice.hostile !== true).map(({ arbitrary, weight }) => ({ arbitrary, weight }))
|
||||
return depth === undefined ? fc.oneof(...held) : fc.oneof({ ...depth, depthSize: 'small' }, ...held)
|
||||
}
|
||||
|
||||
const bareToken = fc.stringMatching(/^[A-Za-z0-9_-]{1,8}$/)
|
||||
const prose = fc.stringMatching(/^[A-Za-z][a-z]{0,6}(?: [a-z]{1,6}){0,3}$/)
|
||||
const cleanText = fc.string({ maxLength: 12, unit: fc.constantFrom(...'aZ09 \t!"#$%&\'()*+,-./;=?@[\\]^_`{}~é\xa0🎉') })
|
||||
const syntaxTokens = fc.constantFrom(
|
||||
...[...String.fromCodePoint(0x0, 0xb, 0xc, 0x85, 0xa0, 0x200b, 0x2028, 0x3000, 0xfeff)],
|
||||
'\n',
|
||||
'\r\n',
|
||||
'\r',
|
||||
'\t',
|
||||
' ',
|
||||
' \n',
|
||||
'\\\n',
|
||||
'**',
|
||||
'__',
|
||||
'~~',
|
||||
'***',
|
||||
'```',
|
||||
'~~~',
|
||||
' ',
|
||||
'# ',
|
||||
'---',
|
||||
'===',
|
||||
'| ',
|
||||
' |',
|
||||
'<!--',
|
||||
'-->',
|
||||
'&',
|
||||
'&#',
|
||||
)
|
||||
const piece = fc.oneof(markdownPieces, syntaxTokens)
|
||||
|
||||
const namedEntity = fc.constantFrom('&', '<', '"', '©', ' ', 'ö', '&bogus;', '&', '&#;', '&', '≧̸')
|
||||
const numericEntity = fc
|
||||
.tuple(fc.oneof(fc.integer({ max: 0x7f, min: 0 }), fc.integer({ max: 0xffff, min: 0 }), fc.integer({ max: 0x110000, min: 0 })), fc.boolean())
|
||||
.map(([code, hex]) => (hex ? `&#x${code.toString(16)};` : `&#${code};`))
|
||||
const escape = fc.constantFrom(...'!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~a \n').map((escaped) => `\\${escaped}`)
|
||||
const backticks = fc.integer({ max: 3, min: 1 }).map((count) => '`'.repeat(count))
|
||||
const codeSpan = fc
|
||||
.tuple(backticks, fc.oneof(prose, textOf(0)), fc.oneof({ arbitrary: fc.constant(undefined), weight: 4 }, { arbitrary: backticks, weight: 1 }))
|
||||
.map(([opener, body, closer]) => `${opener}${body}${closer ?? opener}`)
|
||||
const autolink = fc.oneof(
|
||||
fc.tuple(fc.constantFrom('http://', 'https://', 'mailto:', 'ab:', 'x+y.z-:'), prose).map(([scheme, rest]) => `<${scheme}${rest.replaceAll(' ', '/')}>`),
|
||||
fc.stringMatching(/^<[a-z.+]{1,6}@[a-z-]{1,6}(?:\.[a-z]{1,4})?>$/),
|
||||
)
|
||||
const hostileAutolink = fc.tuple(fc.constantFrom('http://', 'ab:', 'a:'), textOf(0)).map(([scheme, rest]) => `<${scheme}${rest}>`)
|
||||
const inlineHtml = fc.constantFrom('<span>', '</span>', '<a href="x">', "<b class='y'/>", '<!-- c -->', '<!---->', '<?x?>', '<![CDATA[x]]>', '<!X y>', '<br/>', '<b', '<3', '< a>')
|
||||
const hardBreak = fc.constantFrom('\\\n', ' \n', '\n', spellLeafDirective('hardBreak', ''))
|
||||
const spellTextDirective = (held: string) => spellLeafDirective(textDirectiveName, `{${textDirectiveName}=${spellStringAttribute(held)}}`)
|
||||
const textDirective = fc.constantFrom(' ', ' ', '\t', '\n', '\n\n').map(spellTextDirective)
|
||||
const hostileTextDirective = fc.constantFrom(' \n', 'a', '').map(spellTextDirective)
|
||||
|
||||
const url = fc.stringMatching(/^https?:\/\/[a-z]{1,6}\.[a-z]{2,3}(?:\/[a-z0-9()]{0,5})?$/)
|
||||
const title = (text: Arbitrary<string>) => fc.oneof(fc.constant(''), text.map((held) => ` "${held}"`), text.map((held) => ` '${held}'`), text.map((held) => ` (${held})`))
|
||||
|
||||
const attributeValue = fc.oneof(
|
||||
{ arbitrary: bareToken, weight: 3 },
|
||||
{ arbitrary: fc.constantFrom('true', 'false', '0', '1', '3', '-1', '1.5', '1e2', '"1"', '01', 'null', '"[]"', '"{}"', '"#deebff"'), weight: 2 },
|
||||
{ arbitrary: textOf(0).map(spellStringAttribute), weight: 3 },
|
||||
{ arbitrary: jsonValue.map(spellJsonAttribute), weight: 2 },
|
||||
{ arbitrary: textOf(0).map((held) => JSON.stringify(held)), weight: 1 },
|
||||
{ arbitrary: textOf(0).map((held) => `"${held}`), weight: 1 },
|
||||
)
|
||||
const attributePairs = fc.uniqueArray(fc.tuple(fc.oneof({ arbitrary: fc.constantFrom(...attributeKeys), weight: 4 }, { arbitrary: bareToken, weight: 1 }), attributeValue), {
|
||||
maxLength: 3,
|
||||
minLength: 1,
|
||||
selector: ([key]) => key,
|
||||
})
|
||||
const hostileAttributes = fc.oneof(
|
||||
{ arbitrary: fc.constant(''), weight: 3 },
|
||||
{
|
||||
arbitrary: fc.tuple(attributePairs, fc.boolean()).map(([pairs, sorted]) => {
|
||||
const ordered = sorted ? pairs.toSorted(([left], [right]) => (left < right ? -1 : 1)) : pairs
|
||||
return `{${ordered.map(([key, value]) => `${key}=${value}`).join(' ')}}`
|
||||
}),
|
||||
weight: 6,
|
||||
},
|
||||
{ arbitrary: fc.constantFrom('{}', '{ }', '{a}', '{a=}', '{=b}', '{a=b', '{a=b c=d}', '{a="}"}'), weight: 1 },
|
||||
)
|
||||
|
||||
function tableAttributes(vocabulary: AttributeVocabulary, slot?: string): Arbitrary<string> {
|
||||
return attributes(vocabulary).map((attrs) => spellAttributes(spellVocabulary(vocabularyPairs(attrs, vocabulary, slot === undefined ? [] : [slot]) ?? [])))
|
||||
}
|
||||
|
||||
const directiveName = fc.oneof({ arbitrary: fc.constantFrom(...directiveNames), weight: 8 }, { arbitrary: fc.stringMatching(/^[A-Za-z0-9-]{1,6}$/), weight: 1 })
|
||||
const hostileArgument = fc.oneof(
|
||||
{ arbitrary: fc.constant(''), weight: 3 },
|
||||
{ arbitrary: fc.constantFrom(' info', ' warning', ' custom', ' DONE', ' TODO'), weight: 2 },
|
||||
{ arbitrary: fc.oneof(bareToken.map((held) => ` ${held}`), fc.constantFrom(' info', ' a b', ' "a"')), weight: 1 },
|
||||
)
|
||||
|
||||
function directiveHeader(colons: number, name: string, argument: string, attrs: string): string {
|
||||
return `${':'.repeat(colons)}${name}${argument}${attrs === '' ? '' : ` ${attrs}`}`
|
||||
}
|
||||
|
||||
function container(header: (colons: number) => string, body: string, closer: number | null): string {
|
||||
const colons = Math.max(2, ...[...body.matchAll(/:{2,}/g)].map(([run]) => run.length)) + 1
|
||||
return [header(colons), ...(body === '' ? [] : [body]), ':'.repeat(closer ?? colons)].join('\n')
|
||||
}
|
||||
|
||||
const carriedNode = fc.oneof(
|
||||
fc.tuple(fc.constantFrom('mention', 'paragraph', 'status', 'widget'), fc.dictionary(jsonKey, jsonValue, { maxKeys: 2, noNullPrototype: true })).map(([type, attrs]): JsonValue => ({ attrs, type })),
|
||||
textOf(1).map((held): JsonValue => ({ text: held, type: 'text' })),
|
||||
)
|
||||
const spellCarry = (json: string) => spellLeafDirective(carryName, `{json=${spellStringAttribute(json)}}`)
|
||||
const inlineCarry = carriedNode.map((node) => spellCarry(serializeCanonicalJson(node, 'compact')))
|
||||
const hostileInlineCarry = fc.oneof(carriedNode, jsonValue).map((node) => spellCarry(JSON.stringify(node, null, 1)))
|
||||
const blockCarry = carriedNode.map((node) => fencedCodeBlock(carryFence, serializeCanonicalJson(node, 'two-space')))
|
||||
const hostileBlockCarry = fc.oneof(carriedNode, jsonValue).map((node) => fencedCodeBlock(carryFence, JSON.stringify(node)))
|
||||
|
||||
function prefixLines(body: string, first: string, rest: (index: number) => string): string {
|
||||
return body
|
||||
.split('\n')
|
||||
.map((line, index) => (index === 0 ? `${first}${line}` : line === '' ? rest(index).trimEnd() : `${rest(index)}${line}`))
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
const separator = fc.oneof({ arbitrary: fc.constant('\n\n'), weight: 4 }, { arbitrary: fc.constant('\n'), weight: 3 }, { arbitrary: fc.constantFrom('\n\n\n', '\n \n', '\n\t\n'), weight: 1 })
|
||||
const quotePrefix = fc.oneof({ arbitrary: fc.constant('> '), weight: 6 }, { arbitrary: fc.constantFrom('>', ' > ', '> ', '>\t', ''), weight: 1 })
|
||||
const listMarker = fc.oneof(
|
||||
{ arbitrary: fc.constantFrom('-', '*', '+'), weight: 3 },
|
||||
{
|
||||
arbitrary: fc
|
||||
.tuple(fc.oneof({ arbitrary: fc.integer({ max: 3, min: 0 }), weight: 4 }, { arbitrary: fc.integer({ max: 1000000000, min: 0 }), weight: 1 }), fc.constantFrom('.', ')'))
|
||||
.map(([start, delimiter]) => `${start}${delimiter}`),
|
||||
weight: 2,
|
||||
},
|
||||
)
|
||||
const indentDrift = fc.oneof({ arbitrary: fc.constant(0), weight: 6 }, { arbitrary: fc.integer({ max: 2, min: -2 }), weight: 1 })
|
||||
const closerDrift = fc.option(fc.integer({ max: 5, min: 2 }), { freq: 6 })
|
||||
|
||||
function markdownOf(hostile: boolean): Arbitrary<string> {
|
||||
const inline = inlineMarkdown(hostile)
|
||||
return blockMarkdown(hostile, inline, leafBlocks(hostile, inline))
|
||||
}
|
||||
|
||||
function inlineMarkdown(hostile: boolean): InlineMarkdown {
|
||||
const inlineDepth = fc.createDepthIdentifier()
|
||||
const text = hostile ? fc.oneof(cleanText, textOf(0)) : cleanText
|
||||
const word = fc.oneof({ arbitrary: prose, weight: 3 }, { arbitrary: text.filter((held) => held !== ''), weight: 2 })
|
||||
const destination = fc.oneof(url, text, fc.stringMatching(/^<[0-9.#][a-z0-9 ]{0,6}>$/), fc.constant(''), ...(hostile ? [text.map((held) => `<${held}>`)] : []))
|
||||
const label = fc.oneof(prose, word)
|
||||
|
||||
const { inlines } = fc.letrec<{ inline: string; inlines: string }>((tie) => ({
|
||||
inline: choose(
|
||||
hostile,
|
||||
[
|
||||
{ arbitrary: word, weight: 12 },
|
||||
{ arbitrary: fc.oneof(codeSpan, autolink, namedEntity, numericEntity, escape, hardBreak, textDirective, inlineCarry), weight: 8 },
|
||||
{ arbitrary: fc.oneof(hostileAutolink, hostileInlineCarry, hostileTextDirective, inlineHtml, piece), hostile: true, weight: 4 },
|
||||
{
|
||||
arbitrary: fc
|
||||
.tuple(fc.constantFrom('*', '_', '**', '__', '***', '~~', '~'), fc.constantFrom('', '', ' '), tie('inlines'), fc.constantFrom('', '', ' '), fc.option(fc.constantFrom('*', '_', '**', '~~'), { freq: 4 }))
|
||||
.map(([opener, inside, body, closing, closer]) => `${opener}${inside}${body}${closing}${closer ?? opener}`),
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
arbitrary: fc
|
||||
.tuple(fc.constantFrom('', '', hostile ? '!' : ''), tie('inlines'), fc.oneof(fc.tuple(destination, title(text)).map(([target, titled]) => `(${target}${titled})`), label.map((held) => `[${held}]`), fc.constantFrom('', '[]')))
|
||||
.map(([image, content, target]) => `${image}[${content}]${target}`),
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
arbitrary: fc.oneof(
|
||||
...Object.entries(inlineDirectives).map(([name, directive]) =>
|
||||
fc
|
||||
.tuple(directive.textAttribute === undefined ? fc.constant(null) : fc.option(hostile ? word : prose), tableAttributes(directive.attributes, directive.textAttribute))
|
||||
.map(([slot, attrs]) => (slot === null ? spellLeafDirective(name, attrs) : `${spellInlineDirectiveOpener(name)}${slot}]${attrs}`)),
|
||||
),
|
||||
...Object.entries(markAttributes)
|
||||
.filter(([name]) => hostile || markSpelling(name)?.kind === 'directive')
|
||||
.map(([name, vocabulary]) => fc.tuple(tie('inlines'), tableAttributes(vocabulary)).map(([content, attrs]) => `${spellInlineDirectiveOpener(name)}${content}]${attrs}`)),
|
||||
),
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
arbitrary: fc
|
||||
.tuple(directiveName, fc.option(tie('inlines'), { freq: 3 }), hostileAttributes)
|
||||
.map(([name, content, attrs]) => `${directivePrefix}${name}${content === null ? '' : `[${content}]`}${attrs}`),
|
||||
hostile: true,
|
||||
weight: 2,
|
||||
},
|
||||
],
|
||||
{ depthIdentifier: inlineDepth, maxDepth: 3 },
|
||||
),
|
||||
inlines: fc.array(tie('inline'), { depthIdentifier: inlineDepth, maxLength: 4, minLength: 1 }).map((parts) => parts.join('')),
|
||||
}))
|
||||
return { destination, inlines, label, oneLine: inlines.map((held) => held.replace(/[\n\r]/g, ' ')), text, word }
|
||||
}
|
||||
|
||||
function leafBlocks(hostile: boolean, { destination, inlines, label, oneLine, text, word }: InlineMarkdown): LeafMarkdown {
|
||||
const fencedCode = fc
|
||||
.tuple(
|
||||
fc.constantFrom('```', '```', '~~~', '````', '``'),
|
||||
fc.oneof(fc.constant(''), bareToken, text),
|
||||
fc.array(fc.oneof(prose, text, fc.constantFrom('```', '~~~', ':::', ' x')), { maxLength: 3 }),
|
||||
fc.constantFrom('', '', '`', '~', 'none'),
|
||||
)
|
||||
.map(([fence, info, lines, closer]) => [`${fence}${info}`, ...lines, ...(closer === 'none' ? [] : [`${fence}${closer}`])].join('\n'))
|
||||
const pipeTable = fc
|
||||
.record({
|
||||
body: fc.array(fc.array(oneLine, { maxLength: 3 }), { maxLength: 2 }),
|
||||
delimiter: fc.array(fc.constantFrom('---', '-', ':--', '--:', ':-:', '', '==='), { maxLength: 3, minLength: 1 }),
|
||||
header: fc.array(oneLine, { maxLength: 3, minLength: 1 }),
|
||||
leading: fc.boolean(),
|
||||
regular: hostile ? fc.boolean() : fc.constant(true),
|
||||
trailing: fc.boolean(),
|
||||
})
|
||||
.map(({ body, delimiter, header, leading, regular, trailing }) => {
|
||||
const row = (cells: readonly string[]) => (regular || leading ? `| ${cells.join(' | ')}` : cells.join(' | ')) + (regular || trailing ? ' |' : '')
|
||||
const width = (cells: readonly string[]) => (regular ? header.map((_, index) => cells[index] ?? '') : cells)
|
||||
return [row(header), row(regular ? header.map(() => '---') : delimiter), ...body.map((cells) => row(width(cells)))].join('\n')
|
||||
})
|
||||
|
||||
const leafBlock = choose(hostile, [
|
||||
{
|
||||
arbitrary: fc
|
||||
.tuple(fc.integer({ max: 7, min: 1 }), fc.constantFrom(' ', ' ', '', '\t'), oneLine, fc.constantFrom('', '', ' #', '#', ' ## '))
|
||||
.map(([level, gap, content, closer]) => `${'#'.repeat(level)}${gap}${content}${closer}`),
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
arbitrary: fc.tuple(inlines, fc.constantFrom('=', '-'), fc.integer({ max: 4, min: 1 }), fc.constantFrom('', ' ')).map(([content, underline, length, trailing]) => `${content}\n${underline.repeat(length)}${trailing}`),
|
||||
weight: 2,
|
||||
},
|
||||
{ arbitrary: fc.constantFrom('---', '***', '___', '- - -', ' * * *', '_____', '--', '*-*'), weight: 1 },
|
||||
{ arbitrary: fencedCode, weight: 2 },
|
||||
{ arbitrary: blockCarry, weight: 1 },
|
||||
{
|
||||
arbitrary: fc.tuple(fc.constantFrom(' ', '\t', ' '), fc.array(fc.oneof(prose, text), { maxLength: 3, minLength: 1 })).map(([indent, lines]) => lines.map((line) => `${indent}${line}`).join('\n')),
|
||||
weight: 1,
|
||||
},
|
||||
{ arbitrary: pipeTable, weight: 2 },
|
||||
{ arbitrary: fc.tuple(label, destination, title(text)).map(([name, target, titled]) => `[${name}]: ${target}${titled}`), weight: 1 },
|
||||
{ arbitrary: fc.tuple(word, fc.oneof(url, fc.constant(''))).map(([alt, target]) => ``), weight: 1 },
|
||||
{ arbitrary: hostileBlockCarry, hostile: true, weight: 1 },
|
||||
{
|
||||
arbitrary: fc.constantFrom('<div>\ntext\n</div>', '<!-- comment -->', '<pre>\nx\n</pre>', '<?php echo 1; ?>', '<!DOCTYPE html>', '<table>', '<custom-tag attr="1">', '</div>', '<![CDATA[\nx\n]]>'),
|
||||
hostile: true,
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
arbitrary: fc.tuple(fc.constantFrom(2, 2, 3, 1, 4), directiveName, hostileArgument, hostileAttributes).map(([colons, name, argument, attrs]) => directiveHeader(colons, name, argument, attrs)),
|
||||
hostile: true,
|
||||
weight: 1,
|
||||
},
|
||||
{ arbitrary: fc.constantFrom(':::', '::', '::::', ':::panel', '::: panel', ':::panel info extra'), hostile: true, weight: 1 },
|
||||
])
|
||||
return { fencedCode, leafBlock }
|
||||
}
|
||||
|
||||
function blockMarkdown(hostile: boolean, { inlines, oneLine }: InlineMarkdown, { fencedCode, leafBlock }: LeafMarkdown): Arbitrary<string> {
|
||||
const blockDepth = fc.createDepthIdentifier()
|
||||
const { blocks } = fc.letrec<{ block: string; blocks: string }>((tie) => {
|
||||
const bodyByModel = { block: fc.oneof(tie('blocks'), fc.constant('')), code: fencedCode, inline: fc.oneof(oneLine, fc.constant('')) }
|
||||
const tableDirectives = Object.entries(blockDirectives).map(([name, directive]) => {
|
||||
const argument =
|
||||
blockArgument(name) === undefined
|
||||
? fc.constant('')
|
||||
: fc.oneof({ arbitrary: fc.constantFrom(' DONE', ' TODO', ' custom', ' info', ' warning'), weight: 3 }, { arbitrary: bareToken.map((held) => ` ${held}`), weight: 1 })
|
||||
const attrs = hostile ? fc.oneof({ arbitrary: tableAttributes(directive.attributes), weight: 4 }, { arbitrary: hostileAttributes, weight: 1 }) : tableAttributes(directive.attributes)
|
||||
if (directive.contentModel === 'none') return fc.tuple(argument, attrs).map(([held, spelled]) => directiveHeader(2, name, held, spelled))
|
||||
return fc
|
||||
.tuple(argument, attrs, bodyByModel[directive.contentModel], hostile ? closerDrift : fc.constant(null))
|
||||
.map(([held, spelled, body, closer]) => container((colons) => directiveHeader(colons, name, held, spelled), body, closer))
|
||||
})
|
||||
return {
|
||||
block: choose(
|
||||
hostile,
|
||||
[
|
||||
{ arbitrary: fc.array(inlines, { maxLength: 3, minLength: 1 }).map((lines) => lines.join('\n')), weight: 10 },
|
||||
{ arbitrary: leafBlock, weight: 10 },
|
||||
{
|
||||
arbitrary: fc.tuple(tie('blocks'), fc.array(quotePrefix, { maxLength: 3, minLength: 1 })).map(([body, prefixes]) => prefixLines(body, '> ', (index) => prefixes[index % prefixes.length] ?? '')),
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
arbitrary: fc
|
||||
.tuple(listMarker, fc.array(fc.tuple(fc.option(listMarker, { freq: 4 }), fc.constantFrom(' ', ' ', ' ', ' ', '\t', '', ' '), tie('blocks'), indentDrift), { maxLength: 3, minLength: 1 }), separator)
|
||||
.map(([listed, items, gap]) =>
|
||||
items
|
||||
.map(([own, space, body, drift]) => {
|
||||
const marker = own ?? listed
|
||||
return prefixLines(body, `${marker}${space}`, () => ' '.repeat(Math.max(0, marker.length + space.length + drift)))
|
||||
})
|
||||
.join(gap === '\n\n' ? '\n\n' : '\n'),
|
||||
),
|
||||
weight: 4,
|
||||
},
|
||||
{ arbitrary: fc.oneof(...tableDirectives), weight: 3 },
|
||||
{
|
||||
arbitrary: fc
|
||||
.tuple(directiveName, hostileArgument, hostileAttributes, tie('blocks'), fc.option(fc.integer({ max: 5, min: 3 }), { freq: 2 }), closerDrift)
|
||||
.map(([name, argument, attrs, body, opener, closer]) => container((colons) => directiveHeader(opener ?? colons, name, argument, attrs), body, closer)),
|
||||
hostile: true,
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
{ depthIdentifier: blockDepth, maxDepth: 3 },
|
||||
),
|
||||
blocks: fc
|
||||
.array(fc.tuple(separator, tie('block')), { depthIdentifier: blockDepth, maxLength: 3, minLength: 1 })
|
||||
.map((entries) => entries.map(([gap, block], index) => (index === 0 ? block : `${gap}${block}`)).join('')),
|
||||
}
|
||||
})
|
||||
return blocks
|
||||
}
|
||||
|
||||
const cleanMarkdown = markdownOf(false)
|
||||
const hostileMarkdown = markdownOf(true)
|
||||
|
||||
const canonical = adfDocument
|
||||
.map((document) => adfToMarkdown(document))
|
||||
.filter((emitted): emitted is Extract<Result<string>, { ok: true }> => emitted.ok)
|
||||
.map((emitted) => emitted.value)
|
||||
|
||||
function edited(markdown: string, edits: readonly Edit[]): string {
|
||||
let text = markdown
|
||||
for (const [at, removed, inserted] of edits) {
|
||||
const index = at % (text.length + 1)
|
||||
text = `${text.slice(0, index)}${inserted}${text.slice(index + removed)}`
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
const edit: Arbitrary<Edit> = fc.tuple(fc.nat(), fc.nat({ max: 3 }), fc.oneof(piece, fc.constant('')))
|
||||
|
||||
const markdown = fc.oneof(
|
||||
{ arbitrary: cleanMarkdown, weight: 4 },
|
||||
{ arbitrary: hostileMarkdown, weight: 2 },
|
||||
{ arbitrary: canonical, weight: 2 },
|
||||
{ arbitrary: fc.tuple(fc.oneof(cleanMarkdown, canonical), fc.array(edit, { maxLength: 3, minLength: 1 })).map(([held, edits]) => edited(held, edits)), weight: 3 },
|
||||
{ arbitrary: fc.string({ maxLength: 40, unit: piece }), weight: 1 },
|
||||
)
|
||||
|
||||
const document = fc
|
||||
.tuple(markdown, fc.oneof({ arbitrary: fc.constant('\n'), weight: 8 }, { arbitrary: fc.constantFrom('\r\n', '\r'), weight: 1 }), fc.constantFrom('', '', '\n', ' \n'))
|
||||
.map(([held, ending, trailing]) => `${held}${trailing}`.replaceAll('\n', ending))
|
||||
|
||||
function holdsDirectiveShape(document: AdfDocument): boolean {
|
||||
const pending = [...nodeContent(document)]
|
||||
for (let node = pending.pop(); node !== undefined; node = pending.pop()) {
|
||||
if (!commonMarkTypes.has(node.type) || nodeMarks(node).some((mark) => !markdownMarkTypes.has(mark.type))) return true
|
||||
pending.push(...nodeContent(node))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
test('generated markdown refuses, or what it parses to refuses to emit, or its spelling reads back and spells itself', { timeout: propertyTimeout }, () => {
|
||||
const parameters = propertyRuns(gateRuns)
|
||||
let directiveShaped = 0
|
||||
let fixpoints = 0
|
||||
fc.assert(
|
||||
fc.property(document, (input) => {
|
||||
const parsed = markdownToAdf(input)
|
||||
if (!parsed.ok) return
|
||||
const emitted = adfToMarkdown(parsed.value)
|
||||
if (!emitted.ok) return
|
||||
fixpoints += 1
|
||||
if (holdsDirectiveShape(parsed.value)) directiveShaped += 1
|
||||
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), toEditorNormal(parsed.value), `reading ${JSON.stringify(emitted.value)}`)
|
||||
const respelled = adfToMarkdown(read.value)
|
||||
assert.ok(respelled.ok, respelled.ok ? '' : `${respelled.error.code}: ${respelled.error.message} — spelling ${JSON.stringify(emitted.value)} again`)
|
||||
assert.equal(respelled.value, emitted.value)
|
||||
}),
|
||||
parameters,
|
||||
)
|
||||
if (!parameters.gate) return
|
||||
assert.ok(fixpoints >= fixpointFloor, `${fixpoints} of ${gateRuns} runs reached the fixpoint, under the floor of ${fixpointFloor}`)
|
||||
assert.ok(directiveShaped >= directiveShapedFloor, `${directiveShaped} runs reaching the fixpoint held a node or mark outside CommonMark's own types, under the floor of ${directiveShapedFloor}`)
|
||||
})
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { AdfMark } from '../adf/document.ts'
|
||||
import type { JsonValue } from '../json-value.ts'
|
||||
import { isAdfMark } from '../adf/document.ts'
|
||||
import { isAdfMark, nodeAttrs } from '../adf/document.ts'
|
||||
import { serializeCanonicalJson } from '../canonical-json.ts'
|
||||
|
||||
export const marksAttribute = 'marks'
|
||||
|
||||
export function markValues(marks: readonly AdfMark[]): JsonValue {
|
||||
return marks.map((mark) => {
|
||||
const attrs = mark.attrs ?? {}
|
||||
const attrs = nodeAttrs(mark)
|
||||
return Object.keys(attrs).length === 0 ? { type: mark.type } : { attrs, type: mark.type }
|
||||
})
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user