# Migrating ## 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)` | 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` and `unspellable-link` leave `ConvertErrorCode` and `not-a-string` joins it: a `switch` naming one of the four stops compiling, and one naming every code needs the new case. A document `adfToMarkdown` refused with one of the three `unspellable-` codes now converts in `adfToLosslessMarkdown`; markdown the parser refused with one of the four 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 | | 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` |