mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
21 KiB
21 KiB
Changelog
Unreleased
portableMarkdownToAdfreads GFM's pipe table further: a body row holding fewer cells than the header is padded with empty cells, a line holding no|after the table is a row of one cell, anda |over--- |is a one-column table. A row holding more cells stays an error result.MIGRATION.mdshows the change.adfToPortableMarkdownkeeps a link around the note standing in place of a node it marks, such as a stored image:[_(image not included)_](https://example.com/a).- Breaking:
readAdfDocumentreplacesisAdfDocument: it returns aResult<AdfDocument>whose error names the branch that refused, andMIGRATION.mdshows the change. readAdfDocument,adfToHtml,adfToLosslessMarkdownandadfToPortableMarkdownread the document once into a plain copy. A document holding an accessor property, or a Proxy that throws, returnsnot-an-adf-document, where0.3.1ran the getter, looped or threw. The copy keeps own enumerable keys alone, so an inherited or non-enumerable key drops. A document of more than 10,000,000 JSON values returnsunsupported-node-shape.
0.3.1
- The README opens with a one-line description, a list of the calls and the package's perks, and
package.json's description, which npmjs shows, is now that same line, naming Jira and Confluence.
0.3.0
- Breaking:
adfToPortableMarkdownrefuses, asunsupported-node-shape, a known block holdingtext, a known non-text inline node holdingtextor non-emptycontent, and a known block that takes no content, such asrule, holding non-emptycontent.adfToLosslessMarkdownnow refuses them inside a carried node too, andlosslessMarkdownToAdfrefuses a carry holding one.MIGRATION.mdsays how to convert stored markdown. - Breaking:
losslessMarkdownToAdfgives adate,emoji,inlineCard,mention,statusormediaInlineonly 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 isunsupported-node-shape, even in markdownadfToLosslessMarkdownwrote. A link around hard breaks alone still reads as before.adfToLosslessMarkdownwrites a node holding a mark its type cannot take as!adf:carry{json="…"}, except an inline card a link marks, which it refuses.MIGRATION.mdsays how to convert stored markdown. - Breaking:
adfToLosslessMarkdownandadfToPortableMarkdownrefuse an inline card a link marks asunsupported-node-shapeat the card's path, andlosslessMarkdownToAdfrefuses a carry holding one, since Atlassian's schema gives a card no link mark.MIGRATION.mdsays what to do. - Breaking: every reader gives code text no mark but
linkandannotation, as Atlassian's schema requires:**`npm ci`**now reads as plain code, and a directive mark other than a link around only code spans isunsupported-node-shape.adfToLosslessMarkdownwrites code text holding another mark as!adf:carry{json="…"}, andadfToPortableMarkdowndrops that mark. - Breaking:
losslessMarkdownToAdfgives text nobordermark, as Atlassian's schema requires:!adf:border[x]{color="#091e42" size=2}isunsupported-node-shape, and only amediaInlinetakesborder.adfToLosslessMarkdownwrites text holdingborderas!adf:carry{json="…"}.MIGRATION.mdsays how to convert stored markdown. docs/feature-table.mdsets example documents beside whatadfToLosslessMarkdown,adfToPortableMarkdownandadfToHtmlwrite for them, and says whether each output reads back to the same document.- 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.
adfToHtmlwrites ADF as HTML: paragraphs, headings, lists, quotes, tables, code blocks, rules, hard breaks, a centered, uncaptioned external image holding no attribute but its URL and alt, andem,strong,codeand links as their elements. An unmarked emoji, mention, status, date and card holding a URL, ordataholding one, not both, are<span class="adf-emoji">,<span class="adf-mention">,<span class="adf-status">,<time class="adf-date">and<a class="adf-inline-card">. A paragraph, heading, quote, list, list item, table, row, cell, code block, rule, emoji, mention, status, date or card writes its other attributes asdata-*attributes,localIdasdata-local-id. Every other node is an element of classadf-carryholding the node's JSON indata-adfand 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. It refuses, carried or not and asunsupported-node-shape, a known block holdingtext, a known non-text inline node holdingtextor non-emptycontent, and a known block that takes no content, such asrule, holding non-emptycontent.htmlToAdfreadsadfToHtml's HTML back to the document it came from, a carry element from itsdata-adf. It reads that HTML as a browser'sinnerHTMLwrites it too (a raw"in text, , a raw<or>in an attribute value) and as a form post sends it (\r\nfor each line feed). An edit to an emoji's, mention's or status's text becomes itstext; an edit to a date's or a card's text alone is refused. It refuses any other HTML, including an edit to what a carry element shows, with the codes README §The calls lists. It also refuses a carry element holding a shapeadfToHtmlrefuses.adfToLosslessMarkdownandadfToPortableMarkdownwrite anem,strike,strongorlinkmark once across the hard breaks between two texts it marks:_One\thenTwo_, where they wrote_One_\then_Two_. They write anem,strikeorstrongmark once around a code span:**Run `npm ci` first**.adfToLosslessMarkdownalso writes those three once across mentions, emoji, dates, statuses, inline cards and inline media, as**Ping !adf:mention[@A]{id=x} now**, and a link once across mentions, emoji, dates, statuses and inline cards. The markdown reads back to the same ADF as before, except thatadfToPortableMarkdowndrops whitespace beside a hard break a mark crosses, where CommonMark strips it. Where the joined line would not read back, the writers write the line as before.- 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  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. adfToPortableMarkdownkeeps 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.adfToPortableMarkdownkeeps 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"), andportableMarkdownToAdfreads the title back.losslessMarkdownToAdfnames the CommonMark spelling to write when it refuses a directive whose node CommonMark spells, such as[](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. losslessMarkdownToAdftakes 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](brepeated: 16000 of them took 25 seconds, now 80 ms. adfToLosslessMarkdownandadfToPortableMarkdownwrite faster a line of many marked texts or inline nodes that need a carry or lose their mark, as in textunbeside strong-r, repeated: 1000 of them took 10 seconds, now 150 ms. They also write faster a line mixing em, strike and strong: textunbeside-rmarked em and strong, repeated 1000 times, took up to 9 seconds, now under 170 ms, and(marked em, strike and strong beside a mention, repeated 1000 times, took 18 seconds inadfToPortableMarkdown, now 150 ms. A line where em shares a delimiter run with strong takes time linear in its length:~~between spaces marked em and strong,)marked em, strike and strong,a, then.ununmarked strike and strong, repeated 1000 times, took 56 seconds inadfToLosslessMarkdownand 130 inadfToPortableMarkdown, now 2.6 and 1.5 seconds. A line still read back wrong after 32 tries is written with every node holding em, strike or strong as!adf:carry{json="…"}inadfToLosslessMarkdown, and without those marks inadfToPortableMarkdown.adfToLosslessMarkdownandadfToPortableMarkdowntake time linear in the markdown when lists and quotes nest deeply, and so doeslosslessMarkdownToAdf: a list nested 480 levels around 64 KB of paragraphs took 2 seconds to write, now 100 ms. A!adf:blockquoteholding 2 MB of lines in a list nested 480 levels is still refused asunsupported-node-shape, now after 0.8 seconds where it took 2.6.- Every reader leaves
[text],[text][],[text][label]and a definition[label]: urlliteral 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 tob(c. Markdown the writers wrote reads as before, since they escape such a parenthesis. adfToLosslessMarkdownandadfToPortableMarkdownescape 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](cis 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.
adfToMarkdownandmarkdownToAdfare nowadfToLosslessMarkdownandlosslessMarkdownToAdf. A bot's or a model's markdown reads inportableMarkdownToAdf, and markdown known to be strict CommonMark in the newcommonMarkToAdf, where a directive, a pipe table,~~and anadf:fence read as text and code.MIGRATION.mdsays which call to take. - Breaking: the markdown readers refuse a value that is not a string with the new code
not-a-string;MIGRATION.mdlists what each value did before. - Breaking: every call returns the new code
engine-limitwhere a conversion passes the JavaScript engine's longest array,MaporSet, 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, makesisAdfDocumentreturnfalseand the converters refuse it asnot-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:::/::/:nameforms: text holding an unescaped!adf:is claimed. Convert stored markdown perMIGRATION.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 withouttype, and a code fence whose info string opensadf:is claimed, andadfis an ordinary code block language. Convert stored markdown perMIGRATION.md. - Breaking: the markdown readers read markdown holding no block as a document whose
contentis empty, as Atlassian's schema requires;!adf:doc {content=none}spells a document holding nocontentkey. - Breaking:
unspellable-character,unspellable-line-startandunspellable-whitespaceleaveConvertErrorCode. A line ending in anemoji,mentionorstatusdirective's content slot is refused asunsupported-node-shape. A carriage return in text is spelled 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 holdingcontent, and anemoji,mentionorstatuswhosetextholds a line ending or a null character ride the carry. SeeMIGRATION.md. - Breaking:
unspellable-linkleavesConvertErrorCode; a link whosehrefortitleno CommonMark escape spells is written as!adf:link[text]{attrs}. - Breaking: some directive refusals carry
malformed-directivewhere they carriedunsupported-node-shape, and an empty node's leaf and closed spellings swap which one parses;MIGRATION.mdlists each. - Breaking: a link whose text holds another link keeps the inner link and leaves the outer
brackets literal text, where
0.1.0split the outer link around it; seeMIGRATION.md. - Breaking:
unmappable-imageleavesConvertErrorCode, 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. SeeMIGRATION.md. - Breaking: an empty link text reads as its destination,
[](/url)as/urllinked, and[]()as nothing, where both spellings stayed literal text. SeeMIGRATION.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
losslessMarkdownToAdfrefuses a directive mark wrapping only hard breaks asunsupported-node-shape.adfToLosslessMarkdownwrites a hard break holding marks as!adf:carry{json="…"}. SeeMIGRATION.md. - Breaking: a list whose bullet character or ordered delimiter changes reads as two lists, as
CommonMark reads it:
- athen* b,1. athen1) b.adfToLosslessMarkdowngives a list right after one of its kind the other marker,*or1), in place of the::listBreakseparator. Convert stored markdown perMIGRATION.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.0read one list. SeeMIGRATION.md. - Breaking:
adfToLosslessMarkdownwrites a centered, uncaptioned external image whose one mark is a link holding onlyhrefandtitleas[](href "title"), where it wrote themediaSingledirective. Convert stored markdown perMIGRATION.md. - Breaking:
adfToLosslessMarkdownwritescolwidthas its widths joined by commas,colwidth="340,420", and a block'smarksas each mark's type and attributes,marks="breakout{mode=wide}", where it wrote both as JSON arrays, andlosslessMarkdownToAdfrefuses an array the new spelling holds.marksstays a JSON array where the flavour does not list a mark. Convert stored markdown perMIGRATION.md. - Fix the time
adfToLosslessMarkdownandadfToPortableMarkdowntake 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-equalsdocasJSON.parsebuilds it. Two adjacent text nodes CommonMark would read back as one are parted by!adf:textBreak{}, an emptyattrs,contentormarksis spelled{attrs=empty},{content=empty}or{marks=empty},-0is spelled-0, and acodeBlockof several text nodes is a fence per node. AcodeBlockholding other than plain text nodes rides the block carry, where it was refused. - Fix a link
titleholding 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:losslessMarkdownToAdfreads it as the key it spells, where on V8 13.6 and later (Node 24, Deno, Chrome)markdownToAdfcould refuse markdownadfToMarkdownwrote, asunsupported-node-shape, once the process had read a key holding a backslash (V8 bug). - Fix a cyclic document, in which an object holds itself:
adfToLosslessMarkdownandadfToPortableMarkdownrefuse it asnot-an-adf-documentandisAdfDocumentreturnsfalse, where all three hung. - Fix a document whose markdown would be longer than 536870888 characters, V8's longest string:
adfToLosslessMarkdownandadfToPortableMarkdownrefuse it asunsupported-node-shapeon 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:
losslessMarkdownToAdfrefuses it asunsupported-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:
adfToLosslessMarkdownandadfToPortableMarkdownneed 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*threwRangeError.losslessMarkdownToAdfno longer throwsRangeErroron such a paragraph inside a directive. - Fix deeply nested compact JSON in an
adf:fence:losslessMarkdownToAdfrefuses it asunsupported-node-shape, as it refuses any compact JSON there, where about a megabyte of it threw. - Add
adfToPortableMarkdownandportableMarkdownToAdf, 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.portableMarkdownToAdfreads GFM's pipe table with or without leading pipes, and with alignment colons, dropping the alignment. - Spell
rule'scolor,styleandweight,layoutSection'scolumnRuleStyleand a link'scollection,idandoccurrenceKeydirectly where they rode the opaque carry. - The README opens with what the package does and for whom, and
package.jsoncarries search keywords. - The source moves to 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,markdownToAdfandisAdfDocument. Nothing throws, and every error carries acodefrom a closed list.