# Migrating ## From `0.3.1` to `0.4.0` `isAdfDocument` is gone. `readAdfDocument` takes its place and returns a `Result`; 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` | | `docs.filter(isAdfDocument)` | `docs.filter((value) => 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, looped or threw. A document of a class that keeps its fields behind getters (Vue 2, MobX) is refused too: hand in a snapshot, `JSON.parse(JSON.stringify(document))`. 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}` | `[![a](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:` 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 (`[ab](/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 ![a](u).`, `# ![a](u)`), 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 (`![a](u "t")`), on parse | `unmappable-image` | the image, captioned with its title | | a link alone in its paragraph whose whole text is an image (`[![a](u)](/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 |