mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
178 lines
14 KiB
Markdown
178 lines
14 KiB
Markdown
# Migrating
|
|
|
|
## From `0.3.1` to `0.4.0`
|
|
|
|
`isAdfDocument` is gone. `readAdfDocument` takes its place and returns a `Result<AdfDocument>`; the
|
|
`message` of its error names the branch that refused.
|
|
|
|
| `0.3.1` | `0.4.0` |
|
|
| --- | --- |
|
|
| `if (isAdfDocument(value)) convert(value)` | `const read = readAdfDocument(value); if (read.ok) convert(read.value)` |
|
|
| `const valid = isAdfDocument(value)` | `const valid = readAdfDocument(value).ok` |
|
|
|
|
Convert `read.value`: it is a copy that shares no object with `value`. The copy keeps own
|
|
enumerable keys, as `JSON.stringify` does, so an inherited or non-enumerable key drops. A document
|
|
nesting past 500 levels still reads; the converters refuse it as `unsupported-nesting-depth`.
|
|
|
|
`readAdfDocument`, `adfToHtml`, `adfToLosslessMarkdown` and `adfToPortableMarkdown` read a document
|
|
once into that copy. A document holding an accessor property, or a Proxy that throws, is
|
|
`not-an-adf-document` in `0.4.0`, where `0.3.1` ran the getter or threw. A document holding more
|
|
than 10,000,000 JSON values is `unsupported-node-shape`.
|
|
|
|
## From `0.2.0` to `0.3.0`
|
|
|
|
Every reader skips code text when it spells a mark other than a link, and `losslessMarkdownToAdf`
|
|
also skips every other node Atlassian's schema withholds the mark from:
|
|
|
|
| Input | `0.2.0` | `0.3.0` |
|
|
| --- | --- | --- |
|
|
| a `date`, `emoji`, `inlineCard`, `mention`, `status` or `mediaInline` inside a mark's spelling (`**!adf:mention[@A]{id=x}**`) | the node holds the mark | the node holds only marks its type takes; text beside it keeps the mark |
|
|
| a link around only nodes that cannot take it, hard breaks aside, or a directive mark around only such nodes or code spans (`[!adf:status[Done]{color=green}](/u)`) | the nodes hold the mark | `unsupported-node-shape`; write the mark around text beside them |
|
|
| a code span inside any mark's spelling but a link's (``**`npm ci`**``) | the code text holds the mark | the code text holds only `code`, and a link where one wraps it |
|
|
| text inside `!adf:border[…]` (`!adf:border[x]{color="#091e42" size=2}`) | the text holds `border` | the text holds none, and where nothing the spelling wraps takes `border`, `unsupported-node-shape`: delete the spelling and keep its text. Only a `mediaInline` takes `border` |
|
|
|
|
`0.3.0`'s `adfToLosslessMarkdown` writes a node holding a withheld mark as `!adf:carry{json="…"}`
|
|
(an `inlineCard`'s link excepted, below), where `0.2.0` wrote these spellings. `0.3.0` reads
|
|
markdown stored from such a document without the mark, and refuses it where the mark wrapped only those nodes. Keep the mark only in markdown
|
|
written from ADF that held it: Atlassian's schema withholds it, so a site may refuse the document.
|
|
To keep it, read the markdown with `0.2.0` and write it with `0.3.0`, installed side by side:
|
|
|
|
```sh
|
|
npm install @larvit/adf-codec@0.3.0 adf-codec-0.2@npm:@larvit/adf-codec@0.2.0
|
|
```
|
|
|
|
That recipe cannot keep a link on an `inlineCard`: `0.3.0` refuses it as `unsupported-node-shape`,
|
|
the error's `path` naming the card, since Atlassian's schema gives a card no link mark. Remove the
|
|
`link` mark from that card before you write it with `0.3.0`; to keep the link's target, put the
|
|
link on a `text` node beside the card.
|
|
|
|
`0.3.0` refuses, as `unsupported-node-shape`, a known block holding `text`, a known non-text inline
|
|
node holding `text` or non-empty `content`, and a known block that takes no content, such as `rule`,
|
|
holding non-empty `content`. Atlassian's schema allows none of these. `adfToPortableMarkdown` wrote
|
|
such a node in `0.2.0`, and `adfToLosslessMarkdown` wrote one inside a carried node. Both now refuse
|
|
it, and `losslessMarkdownToAdf` refuses stored markdown whose carry holds one.
|
|
|
|
Read such stored markdown with `0.2.0`, then write the document with `0.3.0`. Each refusal names one
|
|
wrong key, `text` or `content`, on the node the error's `path` names, and a call reports only the
|
|
first: delete that key and write again until the call succeeds.
|
|
|
|
## From `0.1.0` to `0.2.0`
|
|
|
|
Each call names the markdown it reads or writes:
|
|
|
|
| `0.1.0` | `0.2.0` |
|
|
| --- | --- |
|
|
| `adfToMarkdown` | `adfToLosslessMarkdown` |
|
|
| `markdownToAdf`, for markdown `adfToMarkdown` wrote, edited since or not | `losslessMarkdownToAdf` |
|
|
| `markdownToAdf`, for markdown no `adfToMarkdown` output went into | `portableMarkdownToAdf`, which reads GFM's pipe tables and `~~`, and reads alerts, callouts, task markers and `==` as nodes ([Portable markdown](README.md#portable-markdown)). Or `commonMarkToAdf`, which reads every one of those, and a directive and an `adf:` fence, as CommonMark does: as text and code |
|
|
|
|
Directives moved under the `!adf:` prefix. `losslessMarkdownToAdf` 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 { adfToLosslessMarkdown, type AdfNode } from '@larvit/adf-codec'
|
|
import { markdownToAdf as markdownToAdf010 } from 'adf-codec-0.1'
|
|
|
|
// 0.1.0 gave a hard break the marks spelled around it, which 0.2.0 writes as !adf:carry{…}. Drop them
|
|
// only where text beside the break holds them too, so a break that was a link's whole text keeps its target.
|
|
function unmarkHardBreaks(nodes: AdfNode[]): AdfNode[] {
|
|
return nodes.map((node, index) => {
|
|
if (node.type !== 'hardBreak') return node.content === undefined ? node : { ...node, content: unmarkHardBreaks(node.content) }
|
|
const holds = (side: AdfNode | undefined) =>
|
|
side?.type === 'text' && (node.marks ?? []).every((mark) => side.marks?.some((other) => JSON.stringify(other) === JSON.stringify(mark)))
|
|
const { marks, ...unmarked } = node
|
|
return holds(nodes[index - 1]) || holds(nodes[index + 1]) ? unmarked : node
|
|
})
|
|
}
|
|
|
|
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 ? adfToLosslessMarkdown({ ...parsed.value, content: unmarkHardBreaks(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 the `content`
|
|
the recipe passes to `adfToLosslessMarkdown`, 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}` |
|
|
| `:::mediaSingle {layout=center}` … `:::` around `::media {alt=a marks="[{\"attrs\":{\"href\":\"/v\"},\"type\":\"link\"}]" type=external url=u}` | `[](/v)` |
|
|
| `::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}` |
|
|
| `::listBreak` between two lists of a kind | no separator: adjacent lists of a kind alternate `- ` and `* `, or `1.` and `1)` |
|
|
| `colwidth="[340,420]"` | `colwidth="340,420"`; a `colwidth` holding no number, or a value that is not a number, rides the `adf:tableCell` or `adf:tableHeader` carry |
|
|
| `marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"` on a block | `marks="breakout{mode=wide}"`; `marks` stays a JSON array where the flavour does not list a mark's type or one of its attributes, or a value holds another type than the flavour gives it |
|
|
|
|
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 list whose bullet character or ordered delimiter changes (`- a` then `* b`, `1. a` then `1) b`) | one list | two lists, as CommonMark reads them |
|
|
| a link reference definition between two list items, at the list's own indentation (`- a`, a blank line, `[r]: /u`, `- b`) | one list | two lists, as CommonMark reads them; indent the definition under the item, or move it below the list, to keep one list |
|
|
| 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 (`""`) | `{ type: 'doc', version: 1 }` | `{ content: [], type: 'doc', version: 1 }`; `!adf:doc {content=none}` reads as the former |
|
|
| a hard break inside a mark's spelling (`*a\` then `b*` on the next line) | the hard break holds the mark | the hard break holds no marks, as Atlassian's schema requires, and the recipe above drops the marks `0.1.0` gave it wherever text beside it holds them. A link whose text is only hard breaks reads as its destination linked, followed by those breaks |
|
|
| a link whose text is empty (`[](/url)`, `[]()`) | literal text | `/url` linked to `/url`, and nothing where the destination is empty too |
|
|
| a code fence whose info string opens `adf:` (```` ```adf:x ````) | a `codeBlock` with that language | in `losslessMarkdownToAdf`, 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
|
|
|
|
`unmappable-image`, `unspellable-character`, `unspellable-line-start`, `unspellable-link` and
|
|
`unspellable-whitespace` leave `ConvertErrorCode`, and `engine-limit` and `not-a-string` join it. A
|
|
`switch` naming one of the five stops compiling, and one naming every code needs the two new cases.
|
|
A document `adfToMarkdown` refused with one of the four `unspellable-` codes now converts in
|
|
`adfToLosslessMarkdown`; markdown the parser refused with one of the five now reads, or refuses with
|
|
the code the table gives.
|
|
|
|
| Input | `0.1.0` | `0.2.0` |
|
|
| --- | --- | --- |
|
|
| an image not alone in its paragraph (`See .`, `# `), on parse | `unmappable-image` | its alt text linked to its URL, or, inside a link, plain text the link marks; give the image a paragraph of its own, a blank line between it and any other line, to keep it an image |
|
|
| a titled image alone in its paragraph (``), on parse | `unmappable-image` | the image, captioned with its title |
|
|
| a link alone in its paragraph whose whole text is an image (`[](/v)`), on parse | `unmappable-image` | the image, its `media` marked with the link |
|
|
| a link whose `href` or `title` no CommonMark escape spells, on emit | `unspellable-link` | spells `!adf:link[text]{attrs}` |
|
|
| a carriage return in text, on emit | `unspellable-character` | spells ` ` |
|
|
| a null character in text, on emit | `unspellable-character` | spells `!adf:text{text="\u0000"}` |
|
|
| a carriage return or null character in a code block, or a null character in a code span, on emit | `unspellable-character` | rides the carry |
|
|
| a paragraph line opening with a code span of three or more backticks, on emit | `unspellable-line-start` | spells the code span |
|
|
| a block directive whose CommonMark spelling holds a carriage return, a null character or a code span opening a line, on parse | `unspellable-character`, `unspellable-line-start` | `unsupported-node-shape`; write the CommonMark spelling |
|
|
| an `emoji`, `mention` or `status` whose `text` holds a line ending or a null character, on emit | `unspellable-whitespace`, `unspellable-character` | rides the inline carry |
|
|
| an `emoji`, `mention` or `status` directive whose content slot spells a line ending (` `, ` `, `!adf:text{text="\n"}`), on parse | `unspellable-whitespace` | `unsupported-node-shape`; replace it with a space, or write the node as `!adf:carry` |
|
|
| a text node holding no text, or holding `content`, on emit | `unsupported-node-shape` | rides the carry |
|
|
| a `codeBlock` holding other than plain text nodes, on emit | `unsupported-node-shape` | rides the block carry |
|
|
| a leaf node given a body (`media`) | `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 |
|
|
| `null`, `undefined` or a non-empty array, on parse | throws a `TypeError` | `not-a-string` |
|
|
| a `String` object, on parse | the document its text spells | `not-a-string`; pass `String(value)` |
|
|
| any other value that is not a string, such as a number or `{}`, on parse | an empty document | `not-a-string` |
|
|
| a conversion past the JavaScript engine's longest array, `Map` or `Set`, or meeting an out-of-memory throw the call can catch, and on JavaScriptCore one past its longest string | throws | `engine-limit`; retry it once |
|