Compare commits
4 Commits
main
..
33a5e01511
| Author | SHA1 | Date | |
|---|---|---|---|
| 33a5e01511 | |||
| 016d5eba26 | |||
| ba50364ae7 | |||
| e41f9719da |
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"categories": { "correctness": "off" },
|
||||
"ignorePatterns": ["src/**/*.test.ts", "src/conformance/property-harness.ts"],
|
||||
"rules": {
|
||||
"eslint/max-lines-per-function": ["error", { "IIFEs": true, "max": 52, "skipBlankLines": false, "skipComments": false }]
|
||||
}
|
||||
}
|
||||
@@ -1,204 +1,350 @@
|
||||
# Working in this repo
|
||||
|
||||
The rules for every collaborator, human or agent, and an index of the decisions a reader would
|
||||
otherwise relitigate. Using the library: `README.md`. What is still to build: `todo.md`.
|
||||
Decisions a reader would otherwise relitigate, and the rules for every collaborator, human or
|
||||
agent. Using the library: `README.md`. What is still to build: `todo.md`.
|
||||
|
||||
## Decisions
|
||||
## 1. Three formats, ADF is the hub
|
||||
|
||||
In `docs/decisions.md`:
|
||||
ADF, one markdown flavour, one HTML dialect. Six directions exposed, but markdown↔HTML compose
|
||||
through ADF: four conversions exist to keep correct — never write a fifth. No fourth format, ever;
|
||||
each one doubles the directions.
|
||||
|
||||
- Plain markdown is a flavour of the grammar
|
||||
- The round-trip is the product
|
||||
- Markdown in is a canonical fixpoint
|
||||
- Equality is deep
|
||||
- `!adf:textBreak{}` parts text CommonMark would join
|
||||
- An empty key spells `empty`
|
||||
- `-0` is spelled `-0`
|
||||
- Empty markdown is a document of no blocks
|
||||
- Unknown nodes ride the carry
|
||||
- The carry fence names the node type
|
||||
- A code block is a fence per text node
|
||||
- Foreign HTML sorts three ways
|
||||
- Names stay text
|
||||
- Directives under `!adf:`
|
||||
- CommonMark is a subset
|
||||
- Tables
|
||||
- Links
|
||||
- Ids stay site-local
|
||||
- Plain task ids come from position
|
||||
- A callout title keeps its link targets
|
||||
- The plain flavour's spellings
|
||||
- The HTML dialect
|
||||
- No runtime dependencies
|
||||
- Standards ship as data
|
||||
- fast-check
|
||||
- Any ES2022 engine
|
||||
- ESM only
|
||||
- One built entrypoint
|
||||
- Public on npm
|
||||
- The formats are API
|
||||
- The code list
|
||||
- Which code a cause takes
|
||||
- `message` and `path`
|
||||
- Publish on a version bump
|
||||
- Docs describe the release being built
|
||||
- No schema validation
|
||||
- The gate runs on Deno and Bun
|
||||
- The gate installs the tarball
|
||||
- Firefox reads the build
|
||||
- The coverage floors
|
||||
- The size ratchet
|
||||
- The project ships under the comprehension floor until items 59, 60, 61 and 62 land
|
||||
- Properties on a fixed seed
|
||||
- The CommonMark suite checks three ways
|
||||
- The flavour spec is read as a source
|
||||
- The node tables answer to Atlassian's schema
|
||||
- Nothing recurses unbounded
|
||||
- Nothing spreads an unbounded array
|
||||
- A retry loop checks its own termination
|
||||
- Readers scan by index
|
||||
- The spelling memo
|
||||
- Cost fixes are measured, never timed
|
||||
- Only the hard break holds a raw newline
|
||||
- Emphasis follows CommonMark's matching
|
||||
- Readable spellings take the `try` prefix
|
||||
- The attribute vocabulary is ADF's
|
||||
- The source parts by ADF and format
|
||||
## 2. The round-trip is the product
|
||||
|
||||
## 1. Nothing about any consumer
|
||||
`markdownToAdf(adfToMarkdown(doc))` and `htmlToAdf(adfToHtml(doc))` must equal `doc` — anything
|
||||
less silently destroys content an editor could not represent, in a document it did not author.
|
||||
When losslessness and readability conflict, losslessness wins.
|
||||
|
||||
The other direction is a canonical fixpoint, not byte-identity: human markdown normalizes, the way
|
||||
back yields the library's canonical spelling, and that spelling round-trips byte-identically —
|
||||
where there is a way back. CommonMark spells some things the flavour has no escape for — a link
|
||||
destination or title holding a backslash or newline, a paragraph opening with a code span whose
|
||||
backticks read back as a fence — so a parse succeeding does not imply a spellable document;
|
||||
`corpus/commonmark-spec/exceptions.json` names those.
|
||||
|
||||
"Equals" is structural equality over editor-normal ADF — adjacent text nodes with identical marks
|
||||
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.
|
||||
|
||||
## 3. Unknown input policy
|
||||
|
||||
- Unknown ADF node: carried opaquely — raw JSON rides a dedicated syntax in both formats and
|
||||
restores to a deep-equal node. The round-trip holds for documents newer than the library. So
|
||||
does a known node no section spells where it stands: a markdown serializer spells a node by type
|
||||
without checking its position, and refusing loses a document ADF itself keeps in an
|
||||
`unsupportedBlock`. Where a container's own spelling cannot hold the child it has — a
|
||||
`bulletList` outside `listItem`, a `codeBlock` outside text — the error result names that
|
||||
instead.
|
||||
- Unmappable foreign HTML element: error result naming the element — never a silent drop.
|
||||
- Bare `@name` / `:smile:` in typed text: stays a text node. Only directives produce
|
||||
mention/emoji/media nodes; resolving names to ids needs I/O, which is the consumer's job.
|
||||
|
||||
## 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.
|
||||
- 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.
|
||||
- Identity-bearing nodes carry their ids in attributes; a document is only portable within its
|
||||
site — accepted.
|
||||
- The HTML dialect mirrors this: semantic elements, stable `adf-*` classes, `data-*` for what HTML
|
||||
cannot express, text always escaped. No stylesheet ships.
|
||||
|
||||
## 5. Dependencies
|
||||
|
||||
`dependencies` is empty. A runtime dependency enters only through a decision entry here stating
|
||||
why ~20 lines of own code cannot do the job, who maintains it, and what auditing it costs. So the
|
||||
CommonMark and HTML parsers are written in this repo. A table a standard fixes is data rather than
|
||||
a dependency: HTML5's 2125 semicolon-terminated character references ship packed in their own
|
||||
module, so entity decoding is complete without one. The CommonMark spec suite is the same shape of
|
||||
data and ships vendored at `corpus/commonmark-spec/` rather than as the `commonmark-spec` dev
|
||||
dependency — that package is CommonJS-only, and Renovate auto-bumping a spec version would silently
|
||||
point the vendored exception list's example numbers at a renumbered suite. A spec bump is a
|
||||
deliberate re-pin, exceptions re-derived by hand beside it. Atlassian's ADF JSON Schemas ship
|
||||
vendored the same way, at `spec/adf-schema/`, rather than as the `@atlaskit/adf-schema` dev
|
||||
dependency — CommonJS-only, some fifty packages with React among them, and a release most days for
|
||||
Renovate to automerge — re-pinned by hand when a payload or a report shows the need.
|
||||
`devDependencies`: few, each earning its keep; they never reach a consumer.
|
||||
|
||||
## 6. The package contract
|
||||
|
||||
- Runs on any ES2022 engine, not only Node — a browser as readily as a server. The shipped source
|
||||
is ECMAScript and nothing else: no host import, no host global, no DOM. `tsconfig.build.json` is
|
||||
that gate, typechecking and emitting the shipped files alone, so `node:fs`, `process` and an
|
||||
ES2024 method are compile errors here rather than a consumer's crash there. The standard is the line, never an
|
||||
engine list: one implementing it in part — Hermes is the live doubt, on §10's property escapes
|
||||
and on lookbehind — is out of scope rather than a bug. Node's test runner, the corpus reads and
|
||||
the build are the repo's own,
|
||||
never the library's, and `engines.node` states the floor the shipped JavaScript needs — `>=18` —
|
||||
never the higher one those repo-only tools want.
|
||||
- ESM only — no CommonJS build, no dual-package hazard.
|
||||
- One entrypoint: built JavaScript, `.d.ts` beside it. Do not add a TypeScript-source entrypoint —
|
||||
Node refuses to type-strip under `node_modules` (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`),
|
||||
so it cannot serve an npm consumer.
|
||||
- Published to public npmjs as `@larvit/adf-codec`. Public source: the Gitea repo
|
||||
goes public, LICENSE in place, before the first publish.
|
||||
- Exact versions: `save-exact=true` in `.npmrc`.
|
||||
|
||||
## 7. Nothing about any consumer
|
||||
|
||||
No Jira client, no HTTP, no REST shapes, no issue keys, no actual consumer named anywhere. Design
|
||||
against the README's personas.
|
||||
|
||||
## 2. Release automation
|
||||
## 8. Semver: the formats are API
|
||||
|
||||
- The bump commit renames `CHANGELOG.md`'s `## Unreleased` to the version.
|
||||
- Exact versions: `save-exact=true` in `.npmrc`.
|
||||
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.
|
||||
|
||||
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.
|
||||
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
|
||||
directions, one code covers them all and
|
||||
`path` and `message` say which — `unsupported-nesting-depth` is the 500-level guard whichever
|
||||
direction hits it, `unspellable-link` the destination and the title alike. Where two codes stay
|
||||
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` —
|
||||
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
|
||||
for. A cause the carry answers gets no code: a mark no
|
||||
spelling writes rides the carry with its node. A directive whose name reads back to no node is
|
||||
`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
|
||||
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
|
||||
kind or its canonical spelling, an argument or a body its content model does not take — is
|
||||
`unsupported-node-shape`, the emitter's code for the same mismatch read the other way — one code
|
||||
across both directions for good, since the call site knows which direction it called and parting
|
||||
them after `0.1.0` is MAJOR. `unmappable-html` names the version rather than the element: this one
|
||||
converts no raw HTML, so at `0.3.0` the mapped elements stop erroring and the code stays for what
|
||||
no ADF node carries. A refusal found before its path is known — the block walk's, a directive
|
||||
reader's — is a `ConvertFault`, the code and message alone; the node walk attaches the path as it
|
||||
descends, so a document reports its first error in document order. `not-an-adf-document` carries
|
||||
the document's own path throughout: eight of the guard's nine branches read the document's own
|
||||
shape, and threading a path to the ninth — a malformed node anywhere in the tree — wants the
|
||||
manual stack §11's no-recursion rule forces, whose empty half no input reaches. The message names
|
||||
the violation instead. Depth is not one of the nine: `adfDocumentFault` returns the code with the
|
||||
message, so an attribute value past 500 levels is `unsupported-nesting-depth` from the emitter as
|
||||
it already is from the parser, both directions refusing the same value. A node's attribute is
|
||||
counted from the value itself, never from the `attrs` object holding it; a mark's is counted three
|
||||
levels in, because the block directive spells the whole mark set as one JSON attribute and the
|
||||
parser reads the value at the bottom of array, mark and `attrs`. `isAdfDocument` is true for a depth fault:
|
||||
a deep document is a document, as the 2000-level blocks and the 600-deep marks the guard already
|
||||
waves through are, and depth is the walks' answer rather than the shape's. A non-finite number
|
||||
stays parted where depth is joined: the parse says `unsupported-node-shape` because the markdown is
|
||||
at fault, the emit `not-an-adf-document` because the input is, and unlike depth nothing round-trips
|
||||
inconsistently between them.
|
||||
|
||||
`position` is the parse side's alone: an emitter reads no source, so an emit error carries `path`
|
||||
and nothing more. It is `{ line, offset }` at the start of the line the block holding the refusal
|
||||
begins on — the offset indexing the string the caller passed, the line counted from 1 — minted by
|
||||
the block walk and attached as results return, so the innermost block wins, the emitter's own
|
||||
refusals the parser re-enters for the CommonMark spelling included.
|
||||
|
||||
A parse names a position for every refusal it returns, so the type says so rather than the prose:
|
||||
`Result<T, E extends ConvertError = ConvertError>`, and a direction reading a source returns
|
||||
`Result<T, ParseError>` — `ConvertError` with `position` required. An optional field a direction
|
||||
always fills is a branch a consumer cannot take, and the `!` §11 bans is how they take it anyway.
|
||||
`htmlToAdf` inherits this at `0.3.0`; the composed `markdownToHtml` and `htmlToMarkdown` keep the
|
||||
wide `Result<T>`, since half their refusals come from an emit stage that read no source.
|
||||
|
||||
## 9. Release automation
|
||||
|
||||
- `package.json` version on `main` is the source of truth. CI on `main`: tests green and version
|
||||
differs from npm → publish and tag `vX.Y.Z`. No bump, no deploy; the bump is each shipping PR's
|
||||
deliberate semver judgment. `publish.sh` is that job, and `private: true` stops it before it
|
||||
reads the token, so the pipeline is live and silent until the maintainer's first bump drops the
|
||||
field.
|
||||
- The publish and the tag each observe their own end state — the version on npm, the tag on the
|
||||
remote — and neither gates the other, so a run that dies between them converges on the next push
|
||||
to `main` rather than leaving npm ahead of the tags. An unanswered registry reads the same as an
|
||||
unpublished version, which npm's own duplicate rejection is what catches. The job rebuilds rather
|
||||
than taking the gate's `dist`: the lockfile is committed, the image is patch-pinned and `tsc` is
|
||||
deterministic, so the two builds agree, and promoting an artifact would make the release path
|
||||
depend on a store that the gate would then have to keep.
|
||||
- Renovate watches devDependencies, Docker pins and action tags; automerges everything on green CI.
|
||||
- Docker images pin the full patch version (`node:24.19.0-alpine3.24`, never `node:24`), as
|
||||
specific as the publisher tags: `oven/bun:1.4.0-alpine` pins Bun's patch and leaves the base
|
||||
floating because Bun publishes nothing narrower. Actions pin semver tags.
|
||||
|
||||
## 3. Tests first, in Docker
|
||||
## 10. Tests first, in Docker
|
||||
|
||||
Test for the behaviour wanted first, then implement until green. `node --test`, beside the code.
|
||||
Node, tsc and npm never run on the host — only via the pinned images (§2). Tests are independent,
|
||||
containers are torn down after a run. A test reaches only for what Node's, Deno's and Bun's `node:`
|
||||
shims all carry.
|
||||
Node, tsc and npm never run on the host — only via the pinned images (§9). Tests are independent,
|
||||
coverage does not decline, containers are torn down after a run.
|
||||
|
||||
Every leg announces its name and, where a container is in play, the image, before it runs and its
|
||||
elapsed time after, `publish.sh` alongside `ci.sh`, so a long run reads as progress rather than as
|
||||
a hang. A leg added later owes the same marker, and a function a leg reaches chains its statements
|
||||
with `&&`, because the `||` that captures the leg's status suspends `set -e` for everything it
|
||||
calls. A leg whose output is both streamed and grepped keeps the copy in a `mktemp`
|
||||
file: `tee /dev/stderr` reopens fd 2, and under `./ci.sh > log 2>&1` the two offsets punch NUL
|
||||
holes through each other's lines.
|
||||
The gate runs that same suite under Deno and Bun as well as Node, the three images pinned alike,
|
||||
and neither extra leg is Node's proof twice. Deno refuses an extensionless or directory specifier,
|
||||
so it holds the module graph to the fully-spelled form a browser can load; Bun runs
|
||||
JavaScriptCore, the one engine of the three that is not V8, where the Unicode property escapes
|
||||
emphasis matching leans on can disagree. Both refuse a run matching no test, so Node's is the only
|
||||
vacuous-green guard, and a test may reach only for what all three `node:` shims carry — the price
|
||||
of proving those engines over the corpus rather than over a smoke import.
|
||||
|
||||
`PROPERTY_RUNS=<runs>` raises the property runs and randomizes the seed for local digging. The
|
||||
generators and run parameters properties share live in `src/conformance/property-harness.ts`,
|
||||
outside the build and coverage.
|
||||
The gate then packs the build and installs the tarball under `package-tests/`, so `files`,
|
||||
`exports` and `types` are proved on the artifact that ships rather than on the source tree a
|
||||
self-reference would resolve against. `consumer.ts` typechecks the emitted `.d.ts` from outside
|
||||
`tsconfig.build.json` — declaration emit leaves the `.ts` specifiers
|
||||
`rewriteRelativeImportExtensions` rewrites in the JavaScript, and this is what says a consumer's
|
||||
resolver maps them, under `NodeNext` alone; a `.d.ts` reader that is not `tsc` stays unproven.
|
||||
`node-floor.js` round-trips the installed package under a Node pinned to `engines.node`'s floor.
|
||||
|
||||
Each `- ` bullet in `spec/flavour.md`'s `## Block nodes`, `## Inline nodes` and `## Marks` declares
|
||||
the nodes named before its first em dash, with the attributes following `Attributes: ` — a
|
||||
parenthesized value set reading `string`; fenced examples are skipped. Keep prose out of a bullet.
|
||||
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.
|
||||
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.
|
||||
The leg re-checks the conversions and nothing else — each fixture's emitted markdown, its parsed
|
||||
document, its error code — leaving the corpus's pairing, uniqueness, source positions and
|
||||
byte-level equality to the Node suite that owns them.
|
||||
|
||||
## 4. Code rules
|
||||
The floors live in the `test` script, so `npm test` and the gate are one path: 100% of lines and
|
||||
functions, and a branch floor that only ever moves upward. It sits below 100 because the guards
|
||||
`noUncheckedIndexedAccess` and ADF's optional keys force — `?? []`, `?? {}`, `?.`, an index
|
||||
compared against `undefined` — have a half no valid document reaches.
|
||||
|
||||
- Two-space indent, English everywhere. Alphabetical order wherever order carries no meaning,
|
||||
keyed on the name a line introduces: an import sorts on its first binding, type imports ahead of
|
||||
value imports, so moving or renaming a module reorders nothing.
|
||||
The corpus, all checked in: hand-built fixtures per node and combination; real sanitized ADF from
|
||||
live Atlassian APIs; the CommonMark spec suite against `markdownToAdf` and `markdownToHtml`.
|
||||
|
||||
`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
|
||||
named before its first em dash, with the attributes following `Attributes: ` — a parenthesized
|
||||
value set reading `string` — and must equal the tables in `adf/`. Keep prose in those sections out
|
||||
of a bullet; fenced examples are skipped. It guards the attributes alone: nodes that differ in
|
||||
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
|
||||
carries no meaning.
|
||||
- Failures are values: everything returns
|
||||
`Result<T>` — `{ ok: true; value } | { ok: false; error: ConvertError }` — nothing throws.
|
||||
`try/catch` only wrapped tightly around a call that genuinely throws, converted to a result on
|
||||
the spot. A reader with no path to name returns `Read<T>` instead, the same two arms over a
|
||||
`ConvertFault`, and `faulted` attaches the path where the walk knows it.
|
||||
- Only the hard break's inline segment holds a raw newline — every other spelling escapes one or
|
||||
refuses it — which is how the whitespace carry finds a line edge.
|
||||
- Emphasis is spelled against CommonMark's matching, never flanking alone: a delimiter run in text
|
||||
escapes wherever CommonMark could open or close with it, leaving the emitter's own delimiters the
|
||||
only ones in play, and a pair that matching hands to another delimiter rides the carry instead.
|
||||
`matchEmphasis` transcribes the reference `process_emphasis` line for line, and its closer walk and
|
||||
opener search stay whole: broken into named steps they drift from the algorithm being faithful is
|
||||
the whole point of.
|
||||
- A readable spelling tried ahead of a general one — a CommonMark block, the image, the pipe
|
||||
table, a pipe cell — gives way with `undefined` for every shape it cannot spell, and fails only
|
||||
where the general form fails on the same node. Refusing there refuses a document the general
|
||||
form spells, so a refusal the general form does not share belongs in the general form or
|
||||
nowhere — save the nested list a tight spelling would swallow, whose refusal the
|
||||
tight-versus-blank answer owns (`todo.md` 2b).
|
||||
- Nothing recurses unbounded: the guards walk iteratively, and blocks, marks and JSON values — an
|
||||
attribute's and a carried node's alike — are all held to 500 levels, so a deep document is a
|
||||
`Result` rather than the stack overflow that waits near 2000.
|
||||
- A reader takes the text and an index — a sticky regex whose `lastIndex` the caller sets on the
|
||||
line before it reads, `indexOf` — never a fresh slice per character, and a per-character walk
|
||||
hoists the scan that does not vary with the character. The pipeline persona feeds documents
|
||||
nobody typed, and a megabyte through a quadratic walk is a minute rather than a millisecond.
|
||||
- No casts: `as`, `as unknown as`, non-null `!`. A boundary owes a type guard validating the
|
||||
fields it claims (`isAdfDocument`); past it everything is typed. Make invalid states
|
||||
unrepresentable.
|
||||
- Failures are values: everything returns `Result<T>`, nothing throws. `try/catch` only wrapped
|
||||
tightly around a call that genuinely throws, converted to a result on the spot. A reader with no
|
||||
path to name returns `Read<T>`, and the walk attaches the path where it knows it.
|
||||
- `src/adf/` holds ADF's own knowledge and imports no format. Each format directory (`markdown/`,
|
||||
`html/`) parts into `emit/` (ADF→format) and `parse/` (format→ADF), its root holding what both
|
||||
directions read. A construct's reader lives in that root beside the regex the emitter escapes
|
||||
against, so the two cannot drift; a reader with no emit counterpart goes in `parse/`, unless it is
|
||||
part of a construct the root already holds — a grammar stays in one file rather than splitting
|
||||
across the seam. A rule both
|
||||
directions must answer alike — whether a list marker interrupts a paragraph — is one function
|
||||
there too, never a copy per direction, however conservative the copy would be. Where the rule is
|
||||
the emitter's own choice, input consults it rather than restating it: the parser asks
|
||||
`commonMarkSpelling` which form the emitter picks, so no fixture the emitter writes can be
|
||||
refused, and a spelling the emitter refuses gives its own error rather than a second name for it.
|
||||
- The attribute vocabulary is ADF's: `adf/` walks it and narrows each value to its kind, and a
|
||||
format spells the narrowed value. A spelling that re-checks the type is the check's second copy.
|
||||
Reading a spelling back is the format's own: the reader sits beside the spelling it inverts, so
|
||||
decode-respell-compare cannot drift, and each format writes its own — canonical JSON for a
|
||||
number is the markdown flavour's choice, not ADF's.
|
||||
- Explicit over implicit; descriptive names; no catch-all files (`utils`, `helpers`, `misc`); a
|
||||
file does not repeat its directory in its name — `adf/document.ts`, never
|
||||
`adf/adf-document.ts`.
|
||||
- Reuse before adding; the smallest sufficient diff is the benchmark; no speculative generality —
|
||||
a second consumer, or it goes.
|
||||
- Explicit over implicit; descriptive names; no catch-all files (`utils`, `helpers`, `misc`); a file
|
||||
does not repeat its directory in its name — `adf/document.ts`, never `adf/adf-document.ts`. A name
|
||||
is the noun `spec/flavour.md` or ADF's schema uses for the thing.
|
||||
|
||||
## 5. Prose to a minimum
|
||||
## 12. Prose to a minimum
|
||||
|
||||
Applies everywhere: comments, every markdown file in this repo (this one included), PR text.
|
||||
|
||||
- Default is no comment. One earns its single line only by naming an invariant, footgun or
|
||||
external constraint the code cannot show — never restatement, history, absence or arrangement.
|
||||
A second line belongs in the commit message or a `docs/decisions.md` entry.
|
||||
A second line belongs in the commit message or a decision entry here.
|
||||
- Every prose comment in a diff is a review question; the default answer is delete.
|
||||
- A doc paragraph says what the repo cannot say for itself, or it goes. The fix for a redundant
|
||||
one is deletion, not trimming. A false claim in any doc is a bug, fixed where found.
|
||||
- Published text — npm README, error messages, API docs — never references internal systems,
|
||||
tickets or repos.
|
||||
|
||||
## 6. Commits and PRs
|
||||
## 13. Commits and PRs
|
||||
|
||||
One-line commit messages and PR titles; short PR summaries. No AI-attribution markers, ever.
|
||||
|
||||
## 7. The working loop
|
||||
## 14. Non-goals
|
||||
|
||||
A session works one chunk, starting from the first item in `todo.md` not waiting on an unmet "Lands
|
||||
after", and stops when that chunk merges, whatever it was asked to finish: a release is a chain of
|
||||
sessions, so an instruction to work until a release is done names the chain, not the session. An
|
||||
open PR is a chunk already in flight, and finishing it is the session.
|
||||
Per chunk:
|
||||
No wiki markup (§1), no network or filesystem I/O, no name→id resolution (§3), no ADF schema
|
||||
validation or exported validator — a refusal that keeps the round-trip is not schema validation,
|
||||
so the one a spelled node carrying the same mark type twice earns stays, and input nesting a
|
||||
spelling inside its own kind (`*(*a*)*`) names that mark once, no shipped CSS (§4), no
|
||||
streaming APIs, no performance budget past §11's scanning rule — nothing here is tuned, and no
|
||||
figure is promised. A CLI is a later goal (`todo.md`), not a non-goal.
|
||||
|
||||
1. Fresh worktree off updated `origin/main`; implement tests-first (§3).
|
||||
## 15. The working loop
|
||||
|
||||
One unchecked `todo.md` item per session, in the smallest PR-able chunk — split a big milestone
|
||||
into sub-items in `todo.md` before starting it. A chunk running a little over or under that is not
|
||||
worth deliberating; what matters is that nothing is left undone in the end. Per chunk:
|
||||
|
||||
1. Fresh worktree off updated `origin/main`; implement tests-first (§10).
|
||||
2. Run the larv-review flow until it passes and CI is green. A reviewer launch states the latest
|
||||
gate result (commit and outcome); a reviewer does not re-run `ci.sh` or the tests when a
|
||||
result exists for the commit under review, or when the diff since that result cannot affect
|
||||
it (docs-only) — re-run only what its own findings or fixes invalidate.
|
||||
3. Merge the PR (standing authorization, this repo only, granted by the maintainer through the
|
||||
`0.2.0` release), report, stop.
|
||||
3. Merge the PR (standing authorization, this repo only, granted through the `0.2.0` release —
|
||||
the maintainer, 2026-09-13), check the box in `todo.md` and move the item's text to
|
||||
`todo-history.md`, leaving its title behind, report, stop. The next chunk gets a fresh session.
|
||||
|
||||
Reserved for the maintainer whatever any rule here says: changing `version` in `package.json` (a
|
||||
bump on `main` publishes, `docs/decisions.md` §Publish on a version bump — every release is the
|
||||
maintainer's) and the `NPM_TOKEN` secret.
|
||||
Ask, don't guess: any choice where what the maintainer would pick is not near-certain gets asked,
|
||||
and the answer lands as a decision in this file. The confidence bar is very high — asking too
|
||||
often is the accepted cost, guessing wrong is not.
|
||||
|
||||
### Ask, don't guess
|
||||
Reserved for the maintainer, never the agent: changing `version` in `package.json` (a bump on
|
||||
`main` publishes, §9 — every release is the maintainer's) and the `NPM_TOKEN` secret.
|
||||
|
||||
Any choice where what the maintainer would pick is not near-certain gets asked. The confidence bar
|
||||
is very high — asking too often is the accepted cost, guessing wrong is not.
|
||||
|
||||
An ask is a gap in `docs/decisions.md`, and its answer is the entry that closes it, landing there —
|
||||
never the instance alone; an answer that is a goal lands in the README, one that is a working rule
|
||||
here. Before asking, name the class the question belongs to and the entries of that class; where
|
||||
one already decides it, apply it without asking, and where it reads two ways on this input, that
|
||||
reading is the ask. Never ask "A or B?": state the gap, the earlier entries of its class, the
|
||||
nearest text, a candidate entry in that file's voice, and the instance it yields. An entry that
|
||||
keeps collecting instances is wrong: rewrite it.
|
||||
|
||||
Which output the audience expects — README goal 5 — is settled by a reader panel rather than
|
||||
asked: three fresh-context readers, one per README persona the question serves, each given only
|
||||
`## Audience` and the input, writing what they expect before picking among outputs the goals
|
||||
allow, rendered, shuffled, with no rationale and nothing saying what is implemented. Three agreeing
|
||||
settle it; otherwise four more read, five of seven settle it, and less is a missing goal, asked.
|
||||
The verdict lands in `docs/decisions.md`.
|
||||
|
||||
A writer panel settles every new or changed markdown or HTML spelling: a reader panel whose readers
|
||||
are the people who read and write that format (README `## Audience`). A panel of the developer
|
||||
personas settles a question about what an app relies on.
|
||||
|
||||
### Stated numbers
|
||||
|
||||
A stated number — 500 levels, the branch floor — is kept; a chunk that cannot keep it asks, naming
|
||||
the number it can reach. A number the code needs and no entry states is a gap.
|
||||
|
||||
### The continuous loop
|
||||
|
||||
A `/loop` session counts as a chain of sessions, each iteration starting by re-reading `AGENTS.md`
|
||||
and `todo.md` and trusting them over anything remembered from earlier iterations. The loop session
|
||||
is a thin driver: each chunk's work runs in a fresh-context subagent holding this file as its
|
||||
charter, and the driver only relays maintainer questions, runs the review flow, merges, and cleans
|
||||
up. The loop stops when only maintainer-reserved acts remain.
|
||||
|
||||
## 8. Scoring run
|
||||
|
||||
The comprehension panel's fill-ins:
|
||||
|
||||
- Language: TypeScript.
|
||||
- Kind: a pure-function document converter with hand-written parsers and emitters.
|
||||
- Domain: Atlassian Document Format and CommonMark parsing.
|
||||
- Domain docs: the CommonMark spec, ADF's JSON schema and `spec/flavour.md`.
|
||||
- 3am question: a viewer/editor app reports that a document it saved comes back with two text
|
||||
nodes merged and a mark gone after `markdownToAdf(adfToMarkdown(doc))`.
|
||||
A continuous loop session (`/loop`) counts as a chain of sessions: one chunk per iteration, each
|
||||
iteration starting by re-reading `AGENTS.md` and `todo.md` and trusting them over anything
|
||||
remembered from earlier iterations. The loop session is a thin driver: each chunk's work runs in
|
||||
a fresh-context subagent holding this file as its charter, and the driver only relays maintainer
|
||||
questions, runs the review flow, merges, and cleans up. The loop stops when only
|
||||
maintainer-reserved acts remain.
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
- **Breaking:** directives, the inline opaque carry among them (now `!adf:carry{json="…"}`), are
|
||||
spelled under an `!adf:` prefix (`!adf:name … !adf:/name`, `!adf:name[content]{attrs}`,
|
||||
`!adf:name arg {attrs}`) in place of the `:::`/`::`/`:name` forms: text holding an unescaped
|
||||
`!adf:` is claimed. Convert stored markdown per `MIGRATION.md`.
|
||||
- **Breaking:** the block carry is a code fence whose info string `adf:<type>` names the node's
|
||||
type, its body the node's JSON without `type`, and a code fence whose info string opens `adf:` is
|
||||
claimed, and `adf` is an ordinary code block language. Convert stored markdown per `MIGRATION.md`.
|
||||
- **Breaking:** `markdownToAdf` and `plainMarkdownToAdf` read markdown holding no block as a
|
||||
document whose `content` is empty, as Atlassian's schema requires; `!adf:doc {content=none}`
|
||||
spells a document holding no `content` key.
|
||||
- `markdownToAdf(adfToMarkdown(doc))` deep-equals `doc` as `JSON.parse` builds it: two adjacent text
|
||||
nodes CommonMark would read back as one are parted by `!adf:textBreak{}`, an empty `attrs`,
|
||||
`content` or `marks` is spelled `{attrs=empty}`, `{content=empty}` or `{marks=empty}`, `-0` is
|
||||
spelled `-0`, and a `codeBlock` of several text nodes is a fence per node. A `codeBlock` holding
|
||||
other than plain text nodes rides the block carry, where it was refused.
|
||||
- **Breaking:** `unspellable-link` leaves `ConvertErrorCode`; a link whose `href` or `title` no
|
||||
CommonMark escape spells is written as `!adf:link[text]{attrs}`.
|
||||
- **Breaking:** some directive refusals carry `malformed-directive` where they carried
|
||||
`unsupported-node-shape`, and an empty node's leaf and closed spellings swap which one parses;
|
||||
`MIGRATION.md` lists each.
|
||||
- **Breaking:** a link whose text holds another link keeps the inner link and leaves the outer
|
||||
brackets literal text, where `0.1.0` split the outer link around it; see `MIGRATION.md`.
|
||||
- Add `adfToPlainMarkdown` and `plainMarkdownToAdf`, a lossy pair converting ADF to and from
|
||||
markdown GitHub, GitLab and Obsidian render: alerts, callouts, task lists, `==highlights==` and
|
||||
pipe tables.
|
||||
- Spell `rule`'s `color`, `style` and `weight`, `layoutSection`'s `columnRuleStyle` and a link's
|
||||
`collection`, `id` and `occurrenceKey` directly where they rode the opaque carry.
|
||||
- Fix an image inside another image's description: it flattens into the alt text, where it was
|
||||
refused.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- First release: lossless conversion between ADF and an extended markdown flavour —
|
||||
`adfToMarkdown`, `markdownToAdf` and `isAdfDocument`. Nothing throws, and every error carries a `code` from a
|
||||
closed list.
|
||||
@@ -1,75 +0,0 @@
|
||||
# Migrating
|
||||
|
||||
## From `0.1.0` to `0.2.0`
|
||||
|
||||
Directives moved under the `!adf:` prefix. `0.2.0` reads `0.1.0`'s spelling without an error,
|
||||
turning each directive into text and each carried node into an `adf` code block. Before `0.2.0`
|
||||
reads any `0.1.0` markdown, convert what is stored or in flight (an open editor, a queue) with the
|
||||
recipe below, and rewrite markdown your code writes or matches (templates, prompts, patterns) by the
|
||||
tables below. Stored ADF needs one change: give a document holding no `content` key `content: []`.
|
||||
`0.1.0` built that shape from empty markdown, meaning the empty document.
|
||||
|
||||
### Convert markdown
|
||||
|
||||
Read it with `0.1.0` and write it with `0.2.0`, installed side by side:
|
||||
|
||||
```sh
|
||||
npm install @larvit/adf-codec@0.2.0 adf-codec-0.1@npm:@larvit/adf-codec@0.1.0
|
||||
```
|
||||
|
||||
```ts
|
||||
import { adfToMarkdown } from '@larvit/adf-codec'
|
||||
import { markdownToAdf as markdownToAdf010 } from 'adf-codec-0.1'
|
||||
|
||||
function migrateMarkdown(stored: string) {
|
||||
const parsed = markdownToAdf010(stored)
|
||||
// 0.1.0 dropped an empty content array, so a document with no content key meant an empty one.
|
||||
return parsed.ok ? adfToMarkdown({ ...parsed.value, content: parsed.value.content ?? [] }) : parsed
|
||||
}
|
||||
```
|
||||
|
||||
- Convert each document once: a second pass can return ok while turning the directives into text.
|
||||
Stop `0.1.0` writing first, and record which documents are converted.
|
||||
- A refusal carrying `position` is `0.1.0`'s parse, which refused that markdown before too. One
|
||||
without is `0.2.0`'s emit: store the document `markdownToAdf010` read as ADF, with
|
||||
`content: parsed.value.content ?? []` as the recipe gives it, rather than keeping the unconverted
|
||||
markdown.
|
||||
|
||||
### Spellings
|
||||
|
||||
| `0.1.0` | `0.2.0` |
|
||||
| --- | --- |
|
||||
| `:::panel info` … `:::`, the fence longer per nesting level | `!adf:panel info` … `!adf:/panel` at any depth |
|
||||
| `::media {id=a type=file}` | `!adf:media {id=a type=file}` |
|
||||
| `::taskItem TODO {localId=i}`: an empty `caption`, `decisionItem`, `paragraph` or `taskItem`, or an empty `heading` carrying `localId` | `!adf:taskItem TODO {localId=i}` then `!adf:/taskItem` |
|
||||
| `:mention[@Mikael]{id=5b10a2}` | `!adf:mention[@Mikael]{id=5b10a2}` |
|
||||
| the `adf` code fence and `:adf{json="…"}` | the `adf:<type>` code fence, its JSON without `type`, and `!adf:carry{json="…"}` |
|
||||
| `\:` keeps a directive literal | `\!adf:` keeps a directive literal |
|
||||
| `:adf{json="…"}` carrying a link for its `collection`, `id` or `occurrenceKey` | `!adf:link[text]{attrs}` |
|
||||
|
||||
A colon run and `:name[` are plain text now, and `adf` an ordinary code block language; text
|
||||
holding an unescaped `!adf:` and a code fence whose info string opens `adf:` are claimed instead.
|
||||
|
||||
### Readings
|
||||
|
||||
Markdown the spelling table leaves alone, which `0.2.0` reads as a different document.
|
||||
|
||||
| Input | `0.1.0` | `0.2.0` |
|
||||
| --- | --- | --- |
|
||||
| a link whose text already holds one (`[a<https://example.com/>b](/v)`) | marks every node the inner link does not, splitting the outer link around it | leaves the outer brackets literal text; write the pieces as separate links to keep them |
|
||||
| markdown holding no block (`markdownToAdf("")`) | `{ type: 'doc', version: 1 }` | `{ content: [], type: 'doc', version: 1 }`; `!adf:doc {content=none}` reads as the former |
|
||||
| a code fence whose info string opens `adf:` (```` ```adf:x ````) | a `codeBlock` with that language | the block carry, refusing a body that is not one node's canonical JSON; write `!adf:codeBlock {language="adf:x"}` around a bare fence to keep the code block |
|
||||
|
||||
### Error codes
|
||||
|
||||
`unspellable-link` leaves `ConvertErrorCode`: a `switch` naming it stops compiling, and the link
|
||||
it named converts.
|
||||
|
||||
| Input | `0.1.0` | `0.2.0` |
|
||||
| --- | --- | --- |
|
||||
| a link whose `href` or `title` no CommonMark escape spells, on emit | `unspellable-link` | spells `!adf:link[text]{attrs}` |
|
||||
| a `codeBlock` holding other than plain text nodes, on emit | `unsupported-node-shape` | rides the block carry |
|
||||
| a leaf node given a body (`media`, `listBreak`) | `unsupported-node-shape` | `malformed-directive` |
|
||||
| a node with a block body written as a leaf (`panel`) | `unsupported-node-shape` | `malformed-directive` |
|
||||
| an empty node the `::taskItem` spelling row names, written as a leaf | parses | `malformed-directive` |
|
||||
| an empty node the `::taskItem` spelling row names, written with a closer | `unsupported-node-shape` | parses |
|
||||
@@ -4,13 +4,9 @@ Lossless conversion between **Atlassian Document Format** (ADF), an extended mar
|
||||
an HTML dialect.
|
||||
|
||||
**Status: published — the markdown round-trip (`adfToMarkdown`, `markdownToAdf`); HTML at
|
||||
`0.2.0`.**
|
||||
Plan:
|
||||
[`todo.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/todo.md). Decisions:
|
||||
[`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md). Changes:
|
||||
[`CHANGELOG.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/CHANGELOG.md). The lossless flavour's grammar:
|
||||
[`spec/flavour.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/spec/flavour.md).
|
||||
Upgrading from `0.1.0`: [convert your markdown first](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/MIGRATION.md).
|
||||
`0.3.0`.**
|
||||
Plan: `todo.md`. Decisions: `AGENTS.md`. The flavour's grammar:
|
||||
[`spec/flavour.md`](spec/flavour.md).
|
||||
|
||||
## What it is for
|
||||
|
||||
@@ -22,40 +18,6 @@ one-directional and lossy. A consumer that shows a document and lets someone edi
|
||||
directions lossless — otherwise saving destroys the panels, mentions and attachments it could not
|
||||
represent.
|
||||
|
||||
## Goals
|
||||
|
||||
The most useful ADF conversion library available, judged by these goals, in priority order:
|
||||
|
||||
1. **Lossless, and every call returns a result, never a throw.**
|
||||
2. **ADF is the hub.**
|
||||
3. **Each format reads and writes as its standard says.**
|
||||
4. **Our markdown is CommonMark, extended only where CommonMark has no spelling.**
|
||||
5. **No surprises: output reads and edits the way its audience expects.**
|
||||
6. **Lossy conversion drops form, never content.**
|
||||
7. **Runs in any JavaScript engine, with no runtime dependencies and nothing to configure or connect.**
|
||||
8. **Fast, and linear in the document's size.**
|
||||
9. **Easy to find, and clear at a glance what it does.**
|
||||
|
||||
## Audience
|
||||
|
||||
Application developers embedding the library, in four personas. All four rely on the guarantees
|
||||
below and on an error's `code` being a closed list; none may rely on an error message's wording,
|
||||
which is free text.
|
||||
|
||||
- **Viewer/editor app** — shows a document, lets a human edit, posts it back. Relies on the
|
||||
round-trip holding for whatever the site's editor wrote, unknown node types included, and on a
|
||||
refusal arriving before the save rather than after.
|
||||
- **Bot posting content** — turns generated markdown into ADF. Relies on plain CommonMark being
|
||||
valid input, so nothing upstream has to learn a flavour.
|
||||
- **Export/indexing tool** — converts ADF to markdown or HTML in bulk. Relies on readable output
|
||||
and on every refusal being deterministic, so a document that fails fails the same way next run.
|
||||
- **LLM/agent pipeline** — hands documents to a model as markdown and writes the edits back.
|
||||
Relies on the round-trip and on markdown a reader half-knowing the lossless flavour can still edit.
|
||||
|
||||
Behind those apps, the people who read and write the markdown, and later the HTML: product
|
||||
managers, engineers and support agents working in Atlassian products through a plugin or another
|
||||
UI. They know markdown and not ADF, and rely on every spelling saying what it means to them.
|
||||
|
||||
## The shape
|
||||
|
||||
```sh
|
||||
@@ -65,7 +27,7 @@ npm install @larvit/adf-codec
|
||||
```ts
|
||||
import { markdownToAdf } from '@larvit/adf-codec'
|
||||
|
||||
const result = markdownToAdf('# Release notes\n\n!adf:panel info\nShipped on Tuesday.\n!adf:/panel\n')
|
||||
const result = markdownToAdf('# Release notes\n\n:::panel info\nShipped on Tuesday.\n:::\n')
|
||||
if (result.ok) {
|
||||
send(result.value)
|
||||
} else {
|
||||
@@ -74,84 +36,25 @@ if (result.ok) {
|
||||
}
|
||||
```
|
||||
|
||||
Serves Goals 1, 2 and 7. Pure functions, each taking a whole document and returning a whole
|
||||
result; no I/O, no configuration. `markdownToHtml` and `htmlToMarkdown` convert through ADF:
|
||||
they keep only what ADF holds, and refuse what `markdownToAdf` or `htmlToAdf` refuses.
|
||||
Pure functions, no I/O, no configuration. ADF is the hub: markdown↔HTML compose through it.
|
||||
|
||||
```ts
|
||||
adfToMarkdown(doc: AdfDocument): Result<string>
|
||||
markdownToAdf(markdown: string): Result<AdfDocument, ParseError>
|
||||
isAdfDocument(v: unknown): v is AdfDocument
|
||||
|
||||
adfToPlainMarkdown(doc: AdfDocument): Result<string>
|
||||
plainMarkdownToAdf(markdown: string): Result<AdfDocument, ParseError>
|
||||
|
||||
adfToHtml(doc: AdfDocument): Result<string> // 0.2.0
|
||||
htmlToAdf(html: string): Result<AdfDocument, ParseError> // 0.2.0
|
||||
markdownToHtml(markdown: string): Result<string> // 0.2.0
|
||||
htmlToMarkdown(html: string): Result<string> // 0.2.0
|
||||
adfToHtml(doc: AdfDocument): Result<string> // 0.3.0
|
||||
htmlToAdf(html: string): Result<AdfDocument, ParseError> // 0.3.0
|
||||
markdownToHtml(markdown: string): Result<string> // 0.3.0, via ADF
|
||||
htmlToMarkdown(html: string): Result<string> // 0.3.0, via ADF
|
||||
```
|
||||
|
||||
`Result<T>` is `{ ok: true; value: T } | { ok: false; error: ConvertError }` — nothing throws.
|
||||
|
||||
## Plain markdown
|
||||
|
||||
Serves Goal 6. Plain markdown is a second flavour of the same grammar. `adfToPlainMarkdown` writes
|
||||
markdown other tools render — GitHub, GitLab, Obsidian and the like — keeping the content and
|
||||
dropping the rest: attributes, colours, layout, identity. Content is what a reader of the rendered
|
||||
document sees or follows: its text, images and link targets. It refuses only
|
||||
`not-an-adf-document`, `unsupported-document-version` and `unsupported-nesting-depth`, and writes
|
||||
no directive.
|
||||
|
||||
`plainMarkdownToAdf` reads what `markdownToAdf` reads and refuses what it refuses, and reads the
|
||||
conventions below as nodes, taking other tools' spellings too; a backslash keeps a marker as text:
|
||||
`\==x==`, `> \[!NOTE]`, `- \[x]`. Markdown `adfToPlainMarkdown` wrote reads back and writes again
|
||||
byte for byte; the document it came from does not come back.
|
||||
|
||||
To edit a document and save it back, use `adfToMarkdown` and `markdownToAdf`: saving what this pair
|
||||
read replaces mentions, attachments and macros with text.
|
||||
|
||||
| ADF | Written | Read back |
|
||||
| --- | --- | --- |
|
||||
| `panel` | a GitHub alert, `> [!WARNING]`: info `NOTE`, note `IMPORTANT`, tip and success `TIP`, warning `WARNING`, error `CAUTION`, custom `NOTE` | `NOTE` info, `IMPORTANT` note, `TIP` tip, `WARNING` warning, `CAUTION` error, and Obsidian's: hint tip; success, check, done success; attention warning; danger, failure, fail, missing, bug, error error; any other word info — in any case; the rest of the marker's line is the first paragraph |
|
||||
| `expand`, `nestedExpand` | Obsidian's folded callout, `> [!NOTE]- Title` | `-` or `+` after any word, the rest of the marker's line the title; a link reads `text (target)`, or its text alone where the text is the target with or without `mailto:`; an expand inside an expand is a `nestedExpand` |
|
||||
| `taskList` | `- [x] Done`, `- [ ] Todo` | a bullet list whose every item is so marked, `[X]` too |
|
||||
| `backgroundColor` | `==text==` | `==text==` on one line, the text touching both delimiters, bounded outside by whitespace, punctuation or a line edge, or touching a Han, Hangul, Hiragana, Katakana, Thai, Lao, Khmer or Myanmar character on either side, in the editor's default highlight `#f8e6a0` |
|
||||
| `table` | a pipe table: the first row its header, a cell's blocks on one line, a span kept under its header by empty cells | — |
|
||||
| `decisionList` | a bullet list | — |
|
||||
| `mention`, `status`, `emoji`, `date` | their text: `@` kept, a mention with none `@` and its id, an emoji its `shortName` without, a date `2026-09-13` in UTC | — |
|
||||
| `inlineCard`, `blockCard`, `embedCard` | a link to the card's URL, else its name | — |
|
||||
| external `media` | `` in a block, `[alt](url)` inline | — |
|
||||
| stored `media`, `mediaInline`, `extension`, `inlineExtension` | their `alt` or `text` | — |
|
||||
| `layoutSection`, `bodiedExtension`, `bodiedSyncBlock`, `multiBodiedExtension`, `extensionFrame`, `caption`, a node this version does not know | its blocks or its text | — |
|
||||
| `placeholder` | nothing | — |
|
||||
|
||||
- Content the document only references, with no text of its own to keep, leaves an italic note
|
||||
naming it where it stood: `_(image not included)_` for stored media with no `alt`,
|
||||
`_(link card not included)_` for a card with neither URL nor name, an extension with no `text`
|
||||
its key, `_(jira-issues-table not included)_`, or `_(extension not included)_` without one, and
|
||||
`_(synced block not included)_` for a `syncBlock`.
|
||||
- `backgroundColor`, `code`, `em`, `link`, `strike` and `strong` stay; every other mark drops,
|
||||
keeping its text, and so does a mark the flavour cannot spell where it stands.
|
||||
- A newline in text is a hard break and in an expand's title a space, edge whitespace outside a
|
||||
link or code span is trimmed, carriage returns and null characters are removed, and an empty
|
||||
paragraph drops.
|
||||
- An ordered list numbered past `999999999`, or adjacent ordered lists whose numbering does not
|
||||
continue, is one bullet list keeping its numbers as text.
|
||||
- A task list beside a bullet or decision list, or holding a block other than a task, joins one
|
||||
bullet list keeping its states as text: `- \[x] Done`.
|
||||
- Text that would read as a marker takes a backslash: `==` wherever it could open or close a
|
||||
highlight, `[!…]` opening a quote, and `[x]` or `[ ]` opening any list item, since GitHub reads
|
||||
that marker per item.
|
||||
- A node read back carries no `localId` except a `taskList`, `taskItem` or `blockTaskItem`, which
|
||||
Atlassian's schema requires one on: each gets a UUID v4 hashed from the whole markdown and its
|
||||
position, the same on every read. Join markdown bound for one document and read it once: the same
|
||||
markdown read twice into one document repeats its ids.
|
||||
|
||||
## The errors
|
||||
|
||||
Serves Goal 1. An ADF node type this version does not know is not an error: the lossless pair
|
||||
carries it opaquely and restores it unchanged ([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#unknown-nodes-ride-the-carry)).
|
||||
An ADF node type this version does not know is not an error: it is carried opaquely and restores
|
||||
unchanged (AGENTS.md §3).
|
||||
|
||||
`ConvertError` is `{ code, message, path, position? }`. `code` is the exported `ConvertErrorCode`,
|
||||
stable across minors and safe to `switch` on exhaustively with no `default`; `message` is free text
|
||||
@@ -171,17 +74,17 @@ UTF-16 code unit, a JavaScript string index rather than a codepoint or a byte of
|
||||
or before the refusal — currently the start of the line the enclosing block begins on; a later
|
||||
minor may narrow that, never widen it.
|
||||
|
||||
Parsing — `markdownToAdf` and `plainMarkdownToAdf`, and `htmlToAdf` at `0.2.0`:
|
||||
Parsing — `markdownToAdf`, and `htmlToAdf` at `0.3.0`:
|
||||
|
||||
| Code | Fires when | What you can do |
|
||||
| --- | --- | --- |
|
||||
| `malformed-directive` | an `!adf:` the grammar cannot read — a prefix completing no directive, an unclosed container, `[content]` or `{attrs}`, a closer with no container of its name open, a leaf given a body, `{attrs}` out of order or duplicated, invalid JSON in an opaque carry | write the spelling the message names, or keep it literal: escape the prefix — `\!adf:`, block and inline alike — or, for a code fence whose info string opens `adf:`, drop the info string and wrap the fence in `!adf:codeBlock {language="adf:…"}` |
|
||||
| `malformed-directive` | a `:::` block or `:name[…]` inline directive the grammar cannot read — an unclosed fence or `[content]`, `{attrs}` out of order or duplicated, invalid JSON in an `adf` carry | write the spelling the message names, or escape the line — `\:::` for a block, `\:` for an inline one — to keep it literal text |
|
||||
| `malformed-pipe-table` | a pipe row that is no pipe table — a missing or ragged `---` delimiter row, an alignment colon in it, or a row not opening with a pipe | open every row with a pipe and give the delimiter row the header's cell count; to keep the lines literal text instead, escape the leading pipe of every one — escaping a single row leaves the next to open a fresh table and fail the same way |
|
||||
| `unknown-directive-name` | a directive whose name is no node or mark this version spells | check the name in `spec/flavour.md`, or escape the prefix as `\!adf:`; the spelling itself is well formed, so a later minor may give the name meaning |
|
||||
| `unmappable-html` | the input holds an HTML construct the documented element set does not map, a comment and a processing instruction among them — at this version that is every raw HTML construct in markdown, the element set landing at `0.2.0` | remove the construct, or write what it holds in the lossless flavour |
|
||||
| `unmappable-image` | an image sits inside other content that is not another image's description, or carries a title | give the image a paragraph of its own and drop the title |
|
||||
| `unknown-directive-name` | a directive whose name is no node or mark this version spells | check the name in `spec/flavour.md`, or escape the colon; the spelling itself is well formed, so a later minor may give the name meaning |
|
||||
| `unmappable-html` | the markdown holds a raw HTML tag, comment or processing instruction | remove it or write it in the flavour — ADF holds no raw-HTML node, and the element mapping lands at `0.3.0` |
|
||||
| `unmappable-image` | an image sits inside other content, or carries a title | give the image a paragraph of its own and drop the title |
|
||||
|
||||
Emitting — `adfToMarkdown`, and `adfToHtml` at `0.2.0`:
|
||||
Emitting — `adfToMarkdown`, and `adfToHtml` at `0.3.0`:
|
||||
|
||||
| Code | Fires when | What you can do |
|
||||
| --- | --- | --- |
|
||||
@@ -196,61 +99,59 @@ emit refuses:
|
||||
| --- | --- | --- |
|
||||
| `unspellable-character` | text or a code block holds a carriage return or a null character, which CommonMark rewrites wherever it sits | strip or replace the character; no escape carries it through the round-trip |
|
||||
| `unspellable-line-start` | a paragraph line begins with a code span whose backticks would read back as a code fence | put any text before the code span |
|
||||
| `unspellable-link` | a link `href` or `title` holds what no canonical escape spells — a backslash, a newline, a control character, an entity reference, an angle bracket beside a space | percent-encode the destination (`%5C` for the backslash, `%26` for the `&` that opens the entity), or drop the title |
|
||||
| `unspellable-whitespace` | an `emoji`, `mention` or `status` holds a newline in the text its inline directive spells in the content slot | replace it with a space — an inline directive never spans lines |
|
||||
| `unsupported-nesting-depth` | blocks, marks, an attribute's JSON or a carried node's JSON nest past 500 levels | keep the ADF and pass the document over, or show it read-only; flatten the input where you are the one who wrote it |
|
||||
| `unsupported-node-shape` | a node carries an attribute, value, argument or body its type does not take, or lacks one it needs — or markdown writes as a directive a node or mark the lossless flavour spells as CommonMark, or a reserved directive stands out of place: `!adf:textBreak{}` or `!adf:listBreak` parting nothing, `!adf:doc` anywhere but as the whole document | fix what the message names; `spec/flavour.md` lists every type's attributes and body |
|
||||
| `unsupported-node-shape` | a node carries an attribute, value, argument or body its type does not take — or markdown writes as a directive a node the flavour spells as CommonMark | write the shape the message names; `spec/flavour.md` lists every type's attributes and body |
|
||||
|
||||
## The guarantees
|
||||
|
||||
Serves Goals 1, 3 and 4.
|
||||
|
||||
- `markdownToAdf(adfToMarkdown(doc))` deep-equals `doc` as JSON, for a document of plain objects
|
||||
as `JSON.parse` builds them — every key and value as `doc` holds it, adjacent text nodes, an
|
||||
empty `attrs`, `content` or `marks` and `-0` included, and unknown node types carried opaquely
|
||||
([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#unknown-nodes-ride-the-carry)).
|
||||
- Markdown this library reads, and markdown it writes, means what the CommonMark spec says; from
|
||||
`0.2.0`, well-formed HTML means what the HTML standard says, read or written. The bullets below
|
||||
name every exception.
|
||||
- Plain CommonMark is valid input to `markdownToAdf` apart from the raw HTML `unmappable-html`
|
||||
names, with four carve-outs — literal text matching directive, pipe-table or strikethrough syntax,
|
||||
and a code fence whose info string opens `adf:`, are claimed (each can be kept literal —
|
||||
`spec/flavour.md`) — and one gap: a CommonMark image fits only as its own title-less paragraph;
|
||||
mid-text and titled images are error results, save an image inside another's description, which
|
||||
flattens into the alt text. Converting back yields the library's canonical spelling, which
|
||||
round-trips byte-identically — where it converts back at all: a parse succeeding is no promise of
|
||||
that, so keep the source until the way back succeeds. ``` ` `` ` ``` reads cleanly and then
|
||||
refuses.
|
||||
- Four CommonMark spellings parse without an error and build a document the reference
|
||||
implementation renders differently: `[](/url)` and `[]()` stay literal text against CommonMark's
|
||||
empty link, a list continuing past a marker change stays one list against CommonMark's two, a
|
||||
shortcut reference matching its definition only under Unicode case folding stays unresolved, and
|
||||
a link whose text holds an autolink keeps the inner link and leaves the outer brackets literal
|
||||
text, which the spec requires and the reference itself breaks, nesting one `<a>` in the other.
|
||||
The first three are pinned `pending` in `corpus/commonmark-spec/exceptions.json`; the suite
|
||||
holds no example of the fourth.
|
||||
- `markdownToAdf(adfToMarkdown(doc))` equals `doc` — unknown node types included, carried opaquely
|
||||
(AGENTS.md §3).
|
||||
- Plain CommonMark is valid input to `markdownToAdf` apart from the raw HTML below, with three
|
||||
carve-outs — literal text matching directive, pipe-table or strikethrough syntax is claimed
|
||||
(escapable — `spec/flavour.md`) — and one gap: a CommonMark image fits only as its own
|
||||
title-less paragraph; mid-text and titled images are error results. Converting back yields the
|
||||
library's canonical spelling, which round-trips byte-identically — where it converts back at
|
||||
all: a parse succeeding is no promise of that, so keep the source until the way back succeeds.
|
||||
`[a](/a\b)`, `<http://x?a=1&b=2>` and `[a](/x y)` read cleanly and then refuse.
|
||||
- Three CommonMark spellings parse without an error and build a document the reference renders
|
||||
differently: `[](/url)` and `[]()` stay literal text against CommonMark's empty link, a list
|
||||
continuing past a marker change stays one list against CommonMark's two, and a shortcut
|
||||
reference matching its definition only under Unicode case folding stays unresolved. Each is
|
||||
pinned `pending` in `corpus/commonmark-spec/exceptions.json`.
|
||||
- Raw HTML in markdown input is an error result, never a silent drop — a tag, a comment and a
|
||||
processing instruction alike. ADF holds no raw-HTML node; the element mapping ships at `0.3.0`.
|
||||
- Not every document converts back: `adfToMarkdown` is partial on valid ADF — a text node holding
|
||||
a carriage return, or a paragraph line beginning with a code span whose backticks read back as a
|
||||
fence. Show the refusal and keep the document read-only; saving markdown you could not produce
|
||||
is the loss the round-trip exists to stop.
|
||||
a carriage return, a link destination or title no canonical escape spells, a paragraph line
|
||||
beginning with a code span whose backticks read back as a fence. Show the refusal and keep the
|
||||
document read-only; saving markdown you could not produce is the loss the round-trip exists to
|
||||
stop.
|
||||
- The pipe table narrows GFM's twice: every row opens with a pipe, so GFM's bare form is an error
|
||||
result rather than the prose it reads as, and an alignment colon in the delimiter row is an
|
||||
error too — ADF holds no column alignment. The trailing pipe is canonical output, optional in
|
||||
input.
|
||||
- Past that and `~~`, no GFM: an autolink literal and a `- [ ]` marker stay text, and a checklist
|
||||
is the `taskList` directive — `plainMarkdownToAdf` turns the marker into a `taskList`.
|
||||
- A document nested deeper than 500 levels is an error result, not a stack overflow, and no input
|
||||
makes a call loop forever.
|
||||
- The emitted formats are semver surface
|
||||
([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#the-formats-are-api)).
|
||||
- **`0.2.0`** — `htmlToAdf(adfToHtml(doc))` deep-equals `doc`; fidelity HTML cannot express rides
|
||||
`data-*` attributes. Foreign HTML maps a documented element set, which markdown's raw HTML reads
|
||||
through as well, and a construct outside it is an error; well-formed HTML only — no tag-soup
|
||||
recovery.
|
||||
is the `taskList` directive.
|
||||
- A document nested deeper than 500 levels is an error result, not a stack overflow.
|
||||
- The emitted formats are semver surface (AGENTS.md §8).
|
||||
- **`0.3.0`** — `htmlToAdf(adfToHtml(doc))` equals `doc`; fidelity HTML cannot express rides
|
||||
`data-*` attributes. Foreign HTML maps a documented element set, an unmappable element is an
|
||||
error, and well-formed HTML only — no tag-soup recovery.
|
||||
|
||||
## Who it is for
|
||||
|
||||
Personas, never named consumers (AGENTS.md §7):
|
||||
|
||||
- **Viewer/editor app** — shows a document, lets a human edit, posts back. Losslessness above all.
|
||||
- **Bot posting content** — converts generated markdown to ADF; needs the CommonMark promise.
|
||||
- **Export/indexing tool** — bulk ADF→markdown/HTML; needs readable output.
|
||||
- **LLM/agent pipeline** — documents to a model as markdown, edits back; needs the round-trip and
|
||||
markdown legible to a reader that half-knows the flavour.
|
||||
|
||||
## The package
|
||||
|
||||
Serves Goal 7. ESM only, no runtime dependencies, public npm. Built JavaScript with `.d.ts`
|
||||
beside it. Pure ECMAScript at an ES2022 baseline, reaching for no host API; the test suite runs
|
||||
under Node, Deno and Bun, and a headless Firefox converts the corpus through the built entrypoint.
|
||||
Contract: [`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#any-es2022-engine), §Any
|
||||
ES2022 engine to §Public on npm.
|
||||
ESM only, no runtime dependencies, public npmjs. Built JavaScript with `.d.ts` beside it.
|
||||
Pure ECMAScript at an ES2022 baseline, reaching for no host API; the test suite runs under Node,
|
||||
Deno and Bun, and a headless Firefox converts the corpus through the built entrypoint.
|
||||
Contract: `AGENTS.md` §5–6.
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
try {
|
||||
const { adfToMarkdown, isAdfDocument, markdownToAdf } = await import('/dist/index.js')
|
||||
// WebDriver's JSON reads -0 back as 0, so a document crosses as JSON text with -0 tagged; run.js revives it.
|
||||
const spelled = (result) => (result.ok ? { ok: true, value: JSON.stringify(result.value, (_, value) => (Object.is(value, -0) ? '\u0000-0' : value)) } : result)
|
||||
window.convertCorpus = (corpus) => ({
|
||||
errors: corpus.errors.map(({ markdown, name }) => ({ name, parsed: markdownToAdf(markdown) })),
|
||||
normalization: corpus.normalization.map(({ markdown, name }) => ({ name, parsed: spelled(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 ? spelled(markdownToAdf(emitted.value)) : undefined }
|
||||
}),
|
||||
normalization: corpus.normalization.map(({ markdown, name }) => ({ name, parsed: markdownToAdf(markdown) })),
|
||||
roundTrip: corpus.roundTrip.map(({ json, markdown, name }) => {
|
||||
const adf = JSON.parse(json)
|
||||
return { emitted: adfToMarkdown(adf), isDocument: isAdfDocument(adf), name, parsed: spelled(markdownToAdf(markdown)) }
|
||||
return { emitted: adfToMarkdown(adf), isDocument: isAdfDocument(adf), name, parsed: markdownToAdf(markdown) }
|
||||
}),
|
||||
})
|
||||
} catch (cause) {
|
||||
|
||||
+5
-21
@@ -1,7 +1,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 { readFileSync, readdirSync } from 'node:fs'
|
||||
|
||||
const contentTypes = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript' }
|
||||
const driver = 'http://127.0.0.1:4444'
|
||||
@@ -49,11 +49,6 @@ function fixtureNames(kind, extension) {
|
||||
.sort()
|
||||
}
|
||||
|
||||
// convert-corpus.js tags -0 so it survives WebDriver's JSON.
|
||||
function revived(text) {
|
||||
return JSON.parse(text, (_, value) => (value === '\u0000-0' ? -0 : value))
|
||||
}
|
||||
|
||||
function refusal(result) {
|
||||
return result.ok ? '' : `${result.error.code}: ${result.error.message}`
|
||||
}
|
||||
@@ -71,7 +66,6 @@ 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(
|
||||
@@ -80,7 +74,7 @@ assert.deepEqual(
|
||||
.map((entry) => entry.name)
|
||||
.filter((name) => name !== 'commonmark-spec')
|
||||
.sort(),
|
||||
['errors', 'normalization', 'real-payloads', 'round-trip'],
|
||||
['errors', 'normalization', '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`)
|
||||
@@ -111,7 +105,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(revived(result.parsed.value), JSON.parse(json))
|
||||
assert.deepEqual(result.parsed.value, JSON.parse(json))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -119,17 +113,7 @@ 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(revived(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(revived(result.parsed.value), JSON.parse(json))
|
||||
assert.deepEqual(result.parsed.value, JSON.parse(fixture(name, '.json')))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -142,5 +126,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 and ${corpus.realPayloads.length} real payloads`,
|
||||
`Firefox ${session.capabilities.browserVersion} converted ${corpus.roundTrip.length} round-trip, ${corpus.normalization.length} normalization and ${corpus.errors.length} error fixtures`,
|
||||
)
|
||||
|
||||
@@ -3,31 +3,28 @@ set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
source ./docker-runner.sh
|
||||
|
||||
leg "install ($node_image)" in_image "$node_image" npm ci
|
||||
leg "typecheck ($node_image)" in_image "$node_image" npm run typecheck
|
||||
leg "size ratchet ($node_image)" in_image "$node_image" npm run size-ratchet
|
||||
in_image "$node_image" npm ci
|
||||
in_image "$node_image" npm run typecheck
|
||||
|
||||
test_log=$(mktemp)
|
||||
node_tests() {
|
||||
in_image "$node_image" npm test 2>&1 | tee "$test_log"
|
||||
}
|
||||
|
||||
leg "tests ($node_image)" node_tests
|
||||
if grep -q 'ℹ tests 0' "$test_log"; then
|
||||
if ! test_output=$(in_image "$node_image" npm test 2>&1); then
|
||||
printf '%s\n' "$test_output"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$test_output"
|
||||
if printf '%s' "$test_output" | grep -q 'ℹ tests 0'; then
|
||||
echo 'the gate ran zero tests — failing instead of a vacuous green'
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$test_log"
|
||||
|
||||
leg "tests ($deno_image)" in_image "$deno_image" deno test --allow-env=PROPERTY_RUNS --allow-read --no-check src/
|
||||
leg "tests ($bun_image)" in_image "$bun_image" bun test src/
|
||||
in_image "$deno_image" deno test --allow-read --no-check src/
|
||||
in_image "$bun_image" bun test src/
|
||||
|
||||
leg "build ($node_image)" in_image "$node_image" npm run build
|
||||
leg "pack and install the tarball ($node_image)" in_image "$node_image" sh -c 'set -e
|
||||
in_image "$node_image" npm run build
|
||||
in_image "$node_image" sh -c 'set -e
|
||||
rm -rf package-tests/node_modules
|
||||
npm pack --pack-destination /tmp
|
||||
npm install --no-audit --no-fund --no-package-lock --no-save --offline --prefix package-tests /tmp/*.tgz'
|
||||
leg "typecheck the consumer ($node_image)" in_image "$node_image" npx tsc -p package-tests
|
||||
leg "round-trip on the engines floor ($floor_image)" in_image "$floor_image" node package-tests/node-floor.js
|
||||
npm pack --pack-destination /tmp >/dev/null
|
||||
npm install --no-audit --no-fund --no-package-lock --no-save --offline --prefix package-tests /tmp/*.tgz >/dev/null'
|
||||
in_image "$node_image" npx tsc -p package-tests
|
||||
in_image "$floor_image" node package-tests/node-floor.js
|
||||
|
||||
leg "browser ($firefox_image)" with_firefox in_image "$node_image" node browser-tests/run.js
|
||||
with_firefox in_image "$node_image" node browser-tests/run.js
|
||||
|
||||
+12
-12
@@ -1,26 +1,26 @@
|
||||
# The corpus
|
||||
|
||||
One directory per contract kind:
|
||||
One directory per contract kind, each landing with its milestone:
|
||||
|
||||
- `round-trip/` — `<name>.json` + `<name>.md`: the markdown `adfToMarkdown` must emit for that
|
||||
document, byte for byte, and that `markdownToAdf` must read back to it (`docs/decisions.md` §The
|
||||
round-trip is the product). Grouped by what the fixture exercises.
|
||||
document, byte for byte, and that `markdownToAdf` must read back to it (AGENTS.md §2). Grouped
|
||||
by what the fixture exercises.
|
||||
- `normalization/` — `<name>.md` + `<name>.json`: markdown input, and the document
|
||||
`markdownToAdf` must build from it, which must in turn emit and read back to itself. The
|
||||
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`: ADF Atlassian's editor wrote, round-tripped ADF→markdown→ADF.
|
||||
No expected markdown.
|
||||
- `real-payloads/` — `<name>.json`: sanitized live ADF, 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
|
||||
`reason`. `kind` is `mark-model` (the permanent count divergence from ADF's mark-per-text-node
|
||||
model), `unspellable` (parses but the flavour has no spelling) or `pending` (a parser gap).
|
||||
model), `unspellable` (parses but the flavour has no spelling) or `pending` (a parser gap a later
|
||||
milestone may close).
|
||||
|
||||
JSON is two-space indent, keys sorted, and a document read back must deep-equal the fixture's
|
||||
(`docs/decisions.md` §Equality is deep). `spec.json` is the vendored, upstream machine-readable
|
||||
suite, byte-exact from [spec.commonmark.org](https://spec.commonmark.org/0.31.2/spec.json)
|
||||
(CommonMark 0.31.2, © John MacFarlane,
|
||||
[CC-BY-SA-4.0](https://creativecommons.org/licenses/by-sa/4.0/)), and is not re-serialized by the
|
||||
corpus gate.
|
||||
JSON is editor-normal (AGENTS.md §2), two-space indent, keys sorted. `spec.json` is the vendored,
|
||||
upstream machine-readable suite, byte-exact from
|
||||
[spec.commonmark.org](https://spec.commonmark.org/0.31.2/spec.json) (CommonMark 0.31.2, © John
|
||||
MacFarlane, [CC-BY-SA-4.0](https://creativecommons.org/licenses/by-sa/4.0/)), and is not
|
||||
re-serialized by the corpus gate.
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
[
|
||||
{
|
||||
"check": "fixpoint",
|
||||
"divergence": "unspellable-link",
|
||||
"example": 196,
|
||||
"kind": "unspellable",
|
||||
"reason": "The link title holds literal newlines no escape spells."
|
||||
},
|
||||
{
|
||||
"check": "fixpoint",
|
||||
"divergence": "unspellable-link",
|
||||
"example": 202,
|
||||
"kind": "unspellable",
|
||||
"reason": "The link destination holds a backslash the flavour cannot escape."
|
||||
},
|
||||
{
|
||||
"check": "count",
|
||||
"divergence": "ul 2/1",
|
||||
@@ -335,6 +349,13 @@
|
||||
"kind": "pending",
|
||||
"reason": "An empty link text stays literal text; CommonMark renders an empty link."
|
||||
},
|
||||
{
|
||||
"check": "fixpoint",
|
||||
"divergence": "unspellable-link",
|
||||
"example": 502,
|
||||
"kind": "unspellable",
|
||||
"reason": "The link destination holds a backslash the flavour cannot escape."
|
||||
},
|
||||
{
|
||||
"check": "count",
|
||||
"divergence": "a 1/5 em 1/4",
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
!adf:tableCell {colwidth="[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[1]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"}
|
||||
!adf:/tableCell
|
||||
:::tableCell {colwidth="[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[1]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"}
|
||||
:::
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1,5 +0,0 @@
|
||||
```adf:
|
||||
{
|
||||
"type": "blockCard"
|
||||
}
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1,3 +0,0 @@
|
||||
```adf:\\
|
||||
{}
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1,5 +0,0 @@
|
||||
```adf:blockCard
|
||||
{
|
||||
"type": "blockCard"
|
||||
}
|
||||
```
|
||||
@@ -1 +1 @@
|
||||
_a !adf:carry{json="{\"type\":\"blockCard\"}"} b_
|
||||
_a :adf{json="{\"type\":\"blockCard\"}"} b_
|
||||
|
||||
@@ -1 +1 @@
|
||||
!adf:carry{json="{"}
|
||||
:adf{json="{"}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
```adf:blockCard
|
||||
{"attrs":
|
||||
```adf
|
||||
{"type":
|
||||
```
|
||||
|
||||
@@ -1 +1 @@
|
||||
!adf:carry{json="[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"}
|
||||
:adf{json="[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
> Part.
|
||||
!adf:/panel
|
||||
:::
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
!adf:codeBlock {wrap=true}
|
||||
:::codeBlock {wrap=true}
|
||||
fn main() {}
|
||||
!adf:/codeBlock
|
||||
:::
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1,8 +0,0 @@
|
||||
!adf:codeBlock {attrs=empty}
|
||||
```js
|
||||
a
|
||||
```
|
||||
```js
|
||||
b
|
||||
```
|
||||
!adf:/codeBlock
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1,7 +0,0 @@
|
||||
!adf:codeBlock
|
||||
```
|
||||
a
|
||||
```
|
||||
```
|
||||
```
|
||||
!adf:/codeBlock
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1,8 +0,0 @@
|
||||
!adf:codeBlock
|
||||
```js
|
||||
a
|
||||
```
|
||||
```ts
|
||||
b
|
||||
```
|
||||
!adf:/codeBlock
|
||||
@@ -1,5 +1,5 @@
|
||||
!adf:codeBlock {language=rust wrap=true}
|
||||
:::codeBlock {language=rust wrap=true}
|
||||
```
|
||||
fn main() {}
|
||||
```
|
||||
!adf:/codeBlock
|
||||
:::
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
:::
|
||||
@@ -0,0 +1,3 @@
|
||||
:::adf
|
||||
{"type":"rule"}
|
||||
:::
|
||||
@@ -1 +1 @@
|
||||
!adf:media {type=file id=a-1}
|
||||
::media {type=file id=a-1}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
!adf:panel info {bogus=1}
|
||||
:::panel info {bogus=1}
|
||||
Part.
|
||||
!adf:/panel
|
||||
:::
|
||||
|
||||
@@ -1 +1 @@
|
||||
malformed-directive
|
||||
unsupported-node-shape
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
!adf:media {id=01a0aa6b-4221-7518-a640-2b97d6217ce2 type=file}
|
||||
:::rule
|
||||
Part.
|
||||
!adf:/media
|
||||
:::
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
!adf:carry
|
||||
{"type":"rule"}
|
||||
!adf:/carry
|
||||
@@ -1,4 +0,0 @@
|
||||
!adf:panel info
|
||||
!adf:expand
|
||||
Part.
|
||||
!adf:/panel
|
||||
@@ -1,2 +0,0 @@
|
||||
!adf:panel info
|
||||
Part.
|
||||
@@ -1 +1 @@
|
||||
Part !adf:mention[**@Mikael**]{id=b1c2}.
|
||||
Part :mention[**@Mikael**]{id=b1c2}.
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
:::panel info
|
||||
Part.
|
||||
@@ -1 +1 @@
|
||||
!adf:rule
|
||||
::rule
|
||||
|
||||
@@ -1 +1 @@
|
||||
Part !adf:mention[@A and more.
|
||||
Part :mention[@A and more.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1 +0,0 @@
|
||||
See !adf:link[the docs]{href="https://example.com/docs"} here.
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1 +0,0 @@
|
||||
See !adf:link[the docs]{title="Setup guide"} here.
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1 +0,0 @@
|
||||
See !adf:link[<https://example.com/docs>]{collection=contentId-98237 href="/docs"} here.
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1 +0,0 @@
|
||||
!adf:link[the docs]{href="https://example.com/docs"}
|
||||
@@ -1 +1 @@
|
||||
Part !adf:em[emphasised] here.
|
||||
Part :em[emphasised] here.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
!adf:widget info
|
||||
:::widget info
|
||||
Part.
|
||||
!adf:/widget
|
||||
:::
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
malformed-directive
|
||||
@@ -1 +0,0 @@
|
||||
Part !adf: here.
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1 +0,0 @@
|
||||
!adf:status[!adf:carry{json="{\"attrs\":{\"localId\":\"x\"},\"text\":\"ok\",\"type\":\"text\"}"}]{color=red}
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1 +0,0 @@
|
||||
!adf:status[!adf:carry{json="{\"marks\":[],\"text\":\"x\",\"type\":\"text\"}"}]
|
||||
@@ -1 +1 @@
|
||||
Part !adf:text{text=plain} here.
|
||||
Part :text{text=plain} here.
|
||||
|
||||
@@ -1 +1 @@
|
||||
!adf:rule {localId="a-1"}
|
||||
::rule {localId="a-1"}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1 +0,0 @@
|
||||
!adf:doc
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1,3 +0,0 @@
|
||||
!adf:doc {content=none}
|
||||
|
||||
Text.
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1,3 +0,0 @@
|
||||
!adf:panel info {attrs=empty}
|
||||
Text.
|
||||
!adf:/panel
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1,3 +0,0 @@
|
||||
!adf:paragraph {content=empty}
|
||||
Text.
|
||||
!adf:/paragraph
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1,3 +0,0 @@
|
||||
!adf:paragraph {attrs=none}
|
||||
Text.
|
||||
!adf:/paragraph
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1 +0,0 @@
|
||||
**!adf:date{marks=empty}**
|
||||
@@ -1 +0,0 @@
|
||||
malformed-directive
|
||||
@@ -1,7 +0,0 @@
|
||||
- Bolt M8
|
||||
|
||||
!adf:listBreak
|
||||
Packed.
|
||||
!adf:/listBreak
|
||||
|
||||
- Nut M8
|
||||
@@ -1,5 +1,5 @@
|
||||
- Bolt M8
|
||||
|
||||
!adf:listBreak
|
||||
::listBreak
|
||||
|
||||
Packed.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1 +0,0 @@
|
||||
a!adf:textBreak{x=y}b
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1 +0,0 @@
|
||||
a!adf:textBreak[x]b
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1 +0,0 @@
|
||||
**a**!adf:textBreak{}b
|
||||
@@ -1 +0,0 @@
|
||||
unsupported-node-shape
|
||||
@@ -1 +0,0 @@
|
||||
!adf:textBreak{}Hello
|
||||
@@ -1,107 +0,0 @@
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "[",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"marks": [
|
||||
{
|
||||
"attrs": {
|
||||
"href": "https://example.com/"
|
||||
},
|
||||
"type": "link"
|
||||
}
|
||||
],
|
||||
"text": "https://example.com/",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"text": "](/v)",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"type": "paragraph"
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "[a",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"marks": [
|
||||
{
|
||||
"attrs": {
|
||||
"href": "https://example.com/b"
|
||||
},
|
||||
"type": "link"
|
||||
}
|
||||
],
|
||||
"text": "https://example.com/b",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"text": "c](/v)",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"type": "paragraph"
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "[",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"marks": [
|
||||
{
|
||||
"attrs": {
|
||||
"collection": "contentId-98237",
|
||||
"href": "/docs"
|
||||
},
|
||||
"type": "link"
|
||||
}
|
||||
],
|
||||
"text": "the docs",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"text": "](/v)",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"type": "paragraph"
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "[",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"marks": [
|
||||
{
|
||||
"attrs": {
|
||||
"href": "/u"
|
||||
},
|
||||
"type": "link"
|
||||
}
|
||||
],
|
||||
"text": "a",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"text": "](/v)",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"type": "paragraph"
|
||||
}
|
||||
],
|
||||
"type": "doc",
|
||||
"version": 1
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
[<https://example.com/>](/v)
|
||||
|
||||
[a<https://example.com/b>c](/v)
|
||||
|
||||
[!adf:link[the docs]{collection=contentId-98237 href="/docs"}](/v)
|
||||
|
||||
[[a](/u)](/v)
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"marks": [
|
||||
{
|
||||
"attrs": {
|
||||
"href": "/a\\b"
|
||||
},
|
||||
"type": "link"
|
||||
}
|
||||
],
|
||||
"text": "a",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"type": "paragraph"
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"marks": [
|
||||
{
|
||||
"attrs": {
|
||||
"href": "http://x?a=1&b=2"
|
||||
},
|
||||
"type": "link"
|
||||
}
|
||||
],
|
||||
"text": "http://x?a=1&b=2",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"type": "paragraph"
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"marks": [
|
||||
{
|
||||
"attrs": {
|
||||
"href": "/x\ny"
|
||||
},
|
||||
"type": "link"
|
||||
}
|
||||
],
|
||||
"text": "a",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"type": "paragraph"
|
||||
}
|
||||
],
|
||||
"type": "doc",
|
||||
"version": 1
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
[a](/a\b)
|
||||
|
||||
<http://x?a=1&b=2>
|
||||
|
||||
[a](/x y)
|
||||
@@ -1,173 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -1,527 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -1,630 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user