3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-13 20:28:28 +01:00
Files
adf-codec/CHANGELOG.md
T

207 lines
16 KiB
Markdown

# Changelog
## Unreleased
- The package's description and keywords say it needs no React, Atlaskit or runtime dependency and
runs in Node, Forge, Deno, Bun and browsers, and the README opens with the call to take for each
need. The README's first comparison table lists the npm packages running in Node that convert ADF
to or from markdown, wrappers and forks aside; its second table lists other ecosystems'
libraries and Atlassian's own paths.
- `adfToHtml` writes ADF as HTML: paragraphs, headings, lists, quotes, code blocks, rules, hard
breaks, a centered, uncaptioned external image holding no attribute but its URL and alt, and `em`,
`strong`, `code` and links as their elements. A paragraph, heading, quote, list, list item, code block or
rule writes its other attributes as `data-*` attributes, `localId` as `data-local-id`. Every other
node is an element of class `adf-carry` holding the node's JSON in `data-adf` and showing its
content, links included. Every URL is written as the ADF holds it,
`javascript:` included: sanitize HTML from an author you do not trust before showing it.
- `htmlToAdf` reads `adfToHtml`'s HTML back to the document it came from, a carry element from its
`data-adf`. It refuses any other HTML, including an edit to what a carry element shows, with the
codes README §The calls lists.
- **Breaking:** `losslessMarkdownToAdf` gives a `date`, `emoji`, `inlineCard`, `mention`, `status`
or `mediaInline` only the marks Atlassian's schema lets it hold: `**!adf:mention[@A]{id=…}**` now
reads as a plain mention. A link or directive mark around only nodes that cannot take it is
`unsupported-node-shape`, even in markdown `adfToLosslessMarkdown` wrote. A link around hard
breaks alone still reads as before. `adfToLosslessMarkdown` writes a node holding a mark its type
cannot take as `!adf:carry{json="…"}`. `MIGRATION.md` says how to convert stored markdown.
- **Breaking:** every reader gives code text no mark but `link` and `annotation`, as Atlassian's
schema requires: ``**`npm ci`**`` now reads as plain code, and a directive mark other than a link
around only code spans is `unsupported-node-shape`. `adfToLosslessMarkdown` writes code text
holding another mark as `!adf:carry{json="…"}`, and `adfToPortableMarkdown` drops that mark.
- Every reader keeps the URL and title of an image inside a link's text, where it dropped them:
after the link comes `(image)`, which links to the image, so
`[look at ![the moon](moon.png "Full") tonight](/moon)` reads as
`[look at the moon tonight](/moon) ([image](moon.png "Full"))`. A link to the image's own URL
takes no `(image)`, unless the image's title is one the link lacks.
- `adfToPortableMarkdown` keeps both targets of a linked external image standing inline, as in a
table cell: `[alt](href) ([image](url))`, where it wrote `[alt](url)` and dropped the link's target.
- `adfToPortableMarkdown` keeps a link title holding a backslash or an entity reference such as
`&`, and in a table cell one holding a `|`, where it dropped the title. It escapes the
character where CommonMark would read it otherwise, as in `[docs](https://example.com "AT\&T")`,
and `portableMarkdownToAdf` reads the title back.
- `losslessMarkdownToAdf` names the CommonMark spelling to write when it refuses a directive whose
node CommonMark spells, such as `[![alt](url)](href)` for a linked `!adf:mediaSingle`.
- Every reader and writer takes time linear in the text when it holds many `<!--`, `<?`,
`<![CDATA[` or `<!` openers without a terminator, or many unclosed backtick runs outside a
directive's `[content]`. Such a text took time quadratic in its length: 160000 characters of `<?`
took seven seconds each way, now 50 ms.
- Every reader takes time linear in the markdown when it holds blank lines under list items nested
deeply. Such a list is still refused as `unsupported-nesting-depth`, now after 40 ms where 20000
nested items and as many blank lines took 10.7 seconds.
- Every reader takes time linear in the markdown when tab-indented lines sit under list items
nested deeply. Such a list is still refused as `unsupported-nesting-depth`, now after 107 ms where
20 lines of 40000 tabs under 80000 nested items took 1988 ms.
- `losslessMarkdownToAdf` takes time linear in the markdown when an inline directive's `[content]`
holds many unclosed backtick runs: 640000 characters of them took one second, now 100 ms.
- Every reader takes time linear in the inline markdown when emphasis spans many links, or many
closing brackets follow a link: 8000 links inside 8000 `*` took two seconds, now about 100 ms.
- Every reader takes time linear in the inline markdown when it holds many `](` that open no link,
as in `[a](b` repeated: 16000 of them took 25 seconds, now 80 ms.
- `adfToLosslessMarkdown` and `adfToPortableMarkdown` take time linear in a line's marks when many
of its marked texts or inline nodes need a carry or lose their mark, as in text `un` beside strong
`-r`, repeated: 1000 of them took 10 seconds, now 150 ms. Such a line whose texts each hold two
or more of em, strong and strike still takes time quadratic in its length.
- `adfToLosslessMarkdown` and `adfToPortableMarkdown` take time linear in the markdown when lists
and quotes nest deeply, and so does `losslessMarkdownToAdf`: a list nested 480 levels around
64 KB of paragraphs took 2 seconds to write, now 100 ms. A `!adf:blockquote` holding 2 MB of lines
in a list nested 480 levels is still refused as `unsupported-node-shape`, now after 0.8 seconds
where it took 2.6.
- Every reader leaves `[text]`, `[text][]`, `[text][label]` and a definition `[label]: url` literal
when the label runs over 999 characters, where it read them as a link or a definition. CommonMark
caps a label at 999 characters, an escape counting as two.
- Every reader leaves a link or a definition literal when its unbracketed destination holds a `(`
that is neither escaped nor closed, as CommonMark requires: `[a](b(c "t")` is now text, where it
read as a link to `b(c`. Markdown the writers wrote reads as before, since they escape such a
parenthesis.
- `adfToLosslessMarkdown` and `adfToPortableMarkdown` escape every text `[` in a line where one
would open a link, and every text backtick where one would open a code span: the text
`[a](b) [c]` is now written `\[a](b) \[c]`, where it was `\[a](b) [c]`. A text `[` that opens
nothing stays bare: `[a] b](c` is now written as is, where it was `\[a] b](c`. Markdown written
by 0.2.0 reads the same in 0.3.0.
- The README compares the package with seven other ADF converters, each checked against its
published version.
## 0.2.0
- **Breaking:** each call names the markdown it reads or writes. `adfToMarkdown` and `markdownToAdf`
are now `adfToLosslessMarkdown` and `losslessMarkdownToAdf`. A bot's or a model's markdown reads
in `portableMarkdownToAdf`, and markdown known to be strict CommonMark in the new
`commonMarkToAdf`, where a directive, a pipe table, `~~` and an `adf:` fence read as text and
code. `MIGRATION.md` says which call to take.
- **Breaking:** the markdown readers refuse a value that is not a string with the new code
`not-a-string`; `MIGRATION.md` lists what each value did before.
- **Breaking:** every call returns the new code `engine-limit` where a conversion passes the
JavaScript engine's longest array, `Map` or `Set`, or meets an out-of-memory throw the call can
catch, and, on JavaScriptCore, its longest string. A retry may succeed. Every other refusal stays deterministic.
- **Breaking:** an attribute value that is not a plain object, array or JSON primitive, such as a
`Date`, `Map`, typed array or class instance, makes `isAdfDocument` return `false` and the
converters refuse it as `not-an-adf-document`. Before, the converters wrote it as `{}` or its own
keys. Build it as JSON holds it: `date.toISOString()`, `Object.fromEntries(map)`,
`Array.from(bytes)`, `{ ...instance }`.
- **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:** the markdown readers 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.
- **Breaking:** `unspellable-character`, `unspellable-line-start` and `unspellable-whitespace`
leave `ConvertErrorCode`. A line ending in an `emoji`, `mention` or `status` directive's content
slot is refused as `unsupported-node-shape`. A carriage return in text is spelled `&#13;` and a
null character `!adf:text{text="\u0000"}`; a paragraph line opening with a code span is spelled
as that code span, its backticks opening no fence; a code block holding either character, a code
span holding a null character, a text node holding no text or holding `content`, and an `emoji`,
`mention` or `status` whose `text` holds a line ending or a null character ride the carry. See
`MIGRATION.md`.
- **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`.
- **Breaking:** `unmappable-image` leaves `ConvertErrorCode`, and every image reads. An image reads
as an image only alone in its paragraph, and its title becomes the caption. Anywhere else, after
a task marker too, it reads as its alt text linked to its URL; inside a link, as plain text the
link marks. A link alone in its paragraph whose whole text is one image reads as that image, with
the link marking it. To keep an image an image, give it a paragraph of its own. See
`MIGRATION.md`.
- **Breaking:** an empty link text reads as its destination, `[](/url)` as `/url` linked, and
`[]()` as nothing, where both spellings stayed literal text. See `MIGRATION.md`.
- **Breaking:** a hard break inside emphasis, strike, a link or a directive mark reads holding no
marks, as Atlassian's schema requires, where every reader gave it the marks around it. A link
whose text is only hard breaks reads as its destination linked, followed by those breaks, and
`losslessMarkdownToAdf` refuses a directive mark wrapping only hard breaks as
`unsupported-node-shape`. `adfToLosslessMarkdown` writes a hard break holding marks as
`!adf:carry{json="…"}`. See `MIGRATION.md`.
- **Breaking:** a list whose bullet character or ordered delimiter changes reads as two lists, as
CommonMark reads it: `- a` then `* b`, `1. a` then `1) b`. `adfToLosslessMarkdown` gives a list
right after one of its kind the other marker, `* ` or `1)`, in place of the `::listBreak`
separator. Convert stored markdown per `MIGRATION.md`.
- **Breaking:** a link reference definition between two list items, after a blank line and at the
list's own indentation, ends the list, as CommonMark reads it; `0.1.0` read one list. See
`MIGRATION.md`.
- **Breaking:** `adfToLosslessMarkdown` writes a centered, uncaptioned external image whose one
mark is a link holding only `href` and `title` as `[![alt](url)](href "title")`, where it wrote the
`mediaSingle` directive. Convert stored markdown per `MIGRATION.md`.
- **Breaking:** `adfToLosslessMarkdown` writes `colwidth` as its widths joined by commas,
`colwidth="340,420"`, and a block's `marks` as each mark's type and attributes,
`marks="breakout{mode=wide}"`, where it wrote both as JSON arrays, and `losslessMarkdownToAdf`
refuses an array the new spelling holds. `marks` stays a JSON array where the flavour does not list
a mark. Convert stored markdown per `MIGRATION.md`.
- Fix the time `adfToLosslessMarkdown` and `adfToPortableMarkdown` take on text holding a long run of
`*`, `_`, `~` or backticks, or a heading ending in a long run of `#`: the time is linear in the
run, and a run of 20000 `*` takes milliseconds where it took seconds.
- Fix the round-trip, so `losslessMarkdownToAdf(adfToLosslessMarkdown(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.
- Fix a link `title` holding a null character: it is written as `!adf:link[text]{attrs}`, where it
read back as U+FFFD.
- Fix an image inside another image's description: it flattens into the alt text, where it was
refused.
- Fix a JSON key holding an escape, in a carried node or a JSON attribute such as `parameters`:
`losslessMarkdownToAdf` reads it as the key it spells, where on V8 13.6 and later (Node 24, Deno,
Chrome) `markdownToAdf` could refuse markdown `adfToMarkdown` wrote, as `unsupported-node-shape`,
once the process had read a key holding a backslash
([V8 bug](https://issues.chromium.org/issues/521080746)).
- Fix a cyclic document, in which an object holds itself: `adfToLosslessMarkdown` and
`adfToPortableMarkdown` refuse it as `not-an-adf-document` and `isAdfDocument` returns `false`,
where all three hung.
- Fix a document whose markdown would be longer than 536870888 characters, V8's longest string:
`adfToLosslessMarkdown` and `adfToPortableMarkdown` refuse it as `unsupported-node-shape` on every
engine, where they threw or ran out of memory.
- Fix a directive that, written out as plain CommonMark, would be longer than 536870888 characters:
`losslessMarkdownToAdf` refuses it as `unsupported-node-shape`, where a few megabytes of nested
list lines ran the call out of memory.
- Fix the markdown readers past V8's or SpiderMonkey's longest string: they return
`unsupported-node-shape`, where they threw.
- Fix a paragraph of tens of millions of characters: `adfToLosslessMarkdown` and
`adfToPortableMarkdown` need a few bytes a character on top of the document and its markdown,
where they needed about 50. Before, 80 million characters ran Node's default heap out of memory
and 20 million `*` threw `RangeError`. `losslessMarkdownToAdf` no longer throws `RangeError` on
such a paragraph inside a directive.
- Fix deeply nested compact JSON in an `adf:` fence: `losslessMarkdownToAdf` refuses it as
`unsupported-node-shape`, as it refuses any compact JSON there, where about a megabyte of it threw.
- Add `adfToPortableMarkdown` and `portableMarkdownToAdf`, a lossy pair converting ADF to and from
markdown GitHub, GitLab and Obsidian render: alerts, callouts, task lists, `==highlights==` and
pipe tables. A task item keeps its paragraphs, then its nested task lists; the rest of the item,
such as an image or a code block, stands after the task list. `portableMarkdownToAdf` reads GFM's
pipe table with or without leading pipes, and with alignment colons, dropping the alignment.
- 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.
- The README opens with what the package does and for whom, and `package.json` carries search
keywords.
- The source moves to [github.com/larvit/adf-codec](https://github.com/larvit/adf-codec), where
issues and pull requests go.
## 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.