Compare commits
18 Commits
03f18ee17b
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5798a3cde1 | |||
| 55c6f01517 | |||
| 8a27cb52d8 | |||
| 730bf88da7 | |||
| db4599837c | |||
| b36f6fbeef | |||
| be54d24dc0 | |||
| e844943d46 | |||
| 0fd98de1fd | |||
| 7dd3ef0b24 | |||
| 03dbc46763 | |||
| 04931ab35a | |||
| d88f4a4351 | |||
| bb83888d8f | |||
| a0e4247cfd | |||
| b01dfd4b0b | |||
| 63e0e1dd52 | |||
| 7b57c1edc8 |
@@ -169,7 +169,7 @@ reading is the ask. Never ask "A or B?": state the gap, the earlier entries of i
|
||||
nearest text, a candidate entry in that file's voice, and the instance it yields. An entry that
|
||||
keeps collecting instances is wrong: rewrite it.
|
||||
|
||||
Which output the audience expects — README goal 5 — is settled by a reader panel rather than
|
||||
Which output the audience expects — README goal 6 — is settled by a reader panel rather than
|
||||
asked: three fresh-context readers, one per README persona the question serves, each given only
|
||||
`## Audience` and the input, writing what they expect before picking among outputs the goals
|
||||
allow, rendered, shuffled, with no rationale and nothing saying what is implemented. Three agreeing
|
||||
|
||||
+6
-5
@@ -18,11 +18,12 @@
|
||||
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.
|
||||
- **Breaking:** `unspellable-character` and `unspellable-line-start` leave `ConvertErrorCode`, and
|
||||
`adfToMarkdown` no longer refuses `unspellable-whitespace`. A carriage return in text is spelled
|
||||
` ` and a null character `!adf:text{text="\u0000"}`; a code block holding either, a text node
|
||||
holding no text or holding `content`, a paragraph line opening with a code span whose backticks
|
||||
read back as a fence, and an `emoji`, `mention` or `status` whose `text` holds a line ending or a
|
||||
null character ride the carry. See `MIGRATION.md`.
|
||||
`adfToMarkdown` no longer returns `unspellable-whitespace`. 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 holding
|
||||
`content`, and an `emoji`, `mention` or `status` whose `text` holds a line ending or a null
|
||||
character ride the carry. See `MIGRATION.md`.
|
||||
- Fix a link `title` holding a null character: it is written as `!adf:link[text]{attrs}`, where it
|
||||
read back as U+FFFD.
|
||||
- **Breaking:** `unspellable-link` leaves `ConvertErrorCode`; a link whose `href` or `title` no
|
||||
|
||||
+5
-3
@@ -63,15 +63,17 @@ Markdown the spelling table leaves alone, which `0.2.0` reads as a different doc
|
||||
### Error codes
|
||||
|
||||
`unspellable-character`, `unspellable-line-start` and `unspellable-link` leave `ConvertErrorCode`:
|
||||
a `switch` naming one stops compiling, and the document it named converts.
|
||||
a `switch` naming one stops compiling. A document `adfToMarkdown` refused with one of them now
|
||||
converts; markdown the parser refused with one of them now refuses with the code the table gives.
|
||||
|
||||
| Input | `0.1.0` | `0.2.0` |
|
||||
| --- | --- | --- |
|
||||
| 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 code span, on emit | `unspellable-character` | rides the carry |
|
||||
| a paragraph line opening with a code span whose backticks read back as a fence, on emit | `unspellable-line-start` | rides the inline carry |
|
||||
| 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 |
|
||||
|
||||
@@ -28,13 +28,13 @@ The most useful ADF conversion library available, judged by these goals, in prio
|
||||
|
||||
1. **Lossless, and every call returns a result, never a throw.**
|
||||
2. **ADF is the hub.**
|
||||
3. **Each format reads and writes as its standard says.**
|
||||
4. **Our markdown is CommonMark, extended only where CommonMark has no spelling.**
|
||||
5. **No surprises: output reads and edits the way its audience expects.**
|
||||
6. **Lossy conversion drops form, never content.**
|
||||
7. **Runs in any JavaScript engine, with no runtime dependencies and nothing to configure or connect.**
|
||||
8. **Fast, and linear in the document's size as JSON.**
|
||||
9. **Easy to find, and clear at a glance what it does.**
|
||||
3. **Easy to find, and clear at a glance what it does.**
|
||||
4. **Each format reads and writes as its standard says.**
|
||||
5. **Our markdown is CommonMark, extended only where CommonMark has no spelling.**
|
||||
6. **No surprises: output reads and edits the way its audience expects.**
|
||||
7. **Lossy conversion drops form, never content.**
|
||||
8. **Runs in any JavaScript engine, with no runtime dependencies and nothing to configure or connect.**
|
||||
9. **Fast, and linear in the document's size as JSON.**
|
||||
|
||||
## Audience
|
||||
|
||||
@@ -74,7 +74,7 @@ if (result.ok) {
|
||||
}
|
||||
```
|
||||
|
||||
Serves Goals 1, 2 and 7. Pure functions, each taking a whole document and returning a whole
|
||||
Serves Goals 1, 2 and 8. Pure functions, each taking a whole document and returning a whole
|
||||
result; no I/O, no configuration. `markdownToHtml` and `htmlToMarkdown` convert through ADF:
|
||||
they keep only what ADF holds, and refuse what `markdownToAdf` or `htmlToAdf` refuses.
|
||||
|
||||
@@ -96,7 +96,7 @@ htmlToMarkdown(html: string): Result<string> // 0.2.0
|
||||
|
||||
## Plain markdown
|
||||
|
||||
Serves Goal 6. Plain markdown is a second flavour of the same grammar. `adfToPlainMarkdown` writes
|
||||
Serves Goal 7. Plain markdown is a second flavour of the same grammar. `adfToPlainMarkdown` writes
|
||||
markdown other tools render — GitHub, GitLab, Obsidian and the like — keeping the content and
|
||||
dropping the rest: attributes, colours, layout, identity. Content is what a reader of the rendered
|
||||
document sees or follows: its text, images and link targets. It refuses only
|
||||
@@ -136,6 +136,7 @@ read replaces mentions, attachments and macros with text.
|
||||
- A newline in text is a hard break and in an expand's title a space, edge whitespace outside a
|
||||
link or code span is trimmed, carriage returns and null characters are removed, and an empty
|
||||
paragraph drops.
|
||||
- A text node's `content` drops: Atlassian's schema forbids it, so no rendered document shows it.
|
||||
- An ordered list numbered past `999999999`, or adjacent ordered lists whose numbering does not
|
||||
continue, is one bullet list keeping its numbers as text.
|
||||
- A task list beside a bullet or decision list, or holding a block other than a task, joins one
|
||||
@@ -196,11 +197,11 @@ emit refuses:
|
||||
| Code | Fires when | What you can do |
|
||||
| --- | --- | --- |
|
||||
| `unsupported-nesting-depth` | blocks, marks, an attribute's JSON or a carried node's JSON nest past 500 levels | keep the ADF and pass the document over, or show it read-only; flatten the input where you are the one who wrote it |
|
||||
| `unsupported-node-shape` | a node carries an attribute, value, argument or body its type does not take, or lacks one it needs — or markdown writes as a directive a node or mark the lossless flavour spells as CommonMark, or a reserved directive stands out of place: `!adf:textBreak{}` or `!adf:listBreak` parting nothing, `!adf:doc` anywhere but as the whole document | fix what the message names; `spec/flavour.md` lists every type's attributes and body |
|
||||
| `unsupported-node-shape` | parsing: markdown spells a node with an attribute, value, argument or body its type does not take, or without one it needs, writes as a directive a node or mark the lossless flavour spells as CommonMark, or puts a reserved directive out of place: `!adf:textBreak{}` or `!adf:listBreak` parting nothing, `!adf:doc` anywhere but as the whole document. Emitting: a node carrying one mark type twice, a block or a non-text inline node holding `text`, or a leaf block or a non-text inline node holding `content` | fix what the message names; `spec/flavour.md` lists every type's attributes and body |
|
||||
|
||||
## The guarantees
|
||||
|
||||
Serves Goals 1, 3 and 4.
|
||||
Serves Goals 1, 4 and 5.
|
||||
|
||||
- `markdownToAdf(adfToMarkdown(doc))` deep-equals `doc` as JSON, for a document of plain objects
|
||||
as `JSON.parse` builds them — every key and value as `doc` holds it, adjacent text nodes, an
|
||||
@@ -224,8 +225,8 @@ Serves Goals 1, 3 and 4.
|
||||
text, which the spec requires and the reference itself breaks, nesting one `<a>` in the other.
|
||||
The first three are pinned `pending` in `corpus/commonmark-spec/exceptions.json`; the suite
|
||||
holds no example of the fourth.
|
||||
- Not every document converts back: `adfToMarkdown` refuses with `unsupported-nesting-depth` or
|
||||
`unsupported-node-shape` as the tables above say. Show the refusal and keep the document
|
||||
- Not every document converts back: `adfToMarkdown` refuses a document nesting past 500 levels, and
|
||||
the node shapes the `unsupported-node-shape` row lists. Show the refusal and keep the document
|
||||
read-only; saving markdown you could not produce is the loss the round-trip exists to stop.
|
||||
- The pipe table narrows GFM's twice: every row opens with a pipe, so GFM's bare form is an error
|
||||
result rather than the prose it reads as, and an alignment colon in the delimiter row is an
|
||||
@@ -244,7 +245,7 @@ Serves Goals 1, 3 and 4.
|
||||
|
||||
## The package
|
||||
|
||||
Serves Goal 7. ESM only, no runtime dependencies, public npm. Built JavaScript with `.d.ts`
|
||||
Serves Goal 8. ESM only, no runtime dependencies, public npm. Built JavaScript with `.d.ts`
|
||||
beside it. Pure ECMAScript at an ES2022 baseline, reaching for no host API; the test suite runs
|
||||
under Node, Deno and Bun, and a headless Firefox converts the corpus through the built entrypoint.
|
||||
Contract: [`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#any-es2022-engine), §Any
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ One directory per contract kind:
|
||||
is the suite; `refusals.json` pins each refusing example to its error `code`; `exceptions.json`
|
||||
pins each known divergence by `check`, `example`, `kind` and the exact `divergence`, with a
|
||||
`reason`. `kind` is `mark-model` (the permanent count divergence from ADF's mark-per-text-node
|
||||
model), `unspellable` (parses but the flavour has no spelling) or `pending` (a parser gap).
|
||||
model) or `pending` (a parser gap).
|
||||
|
||||
JSON is two-space indent, keys sorted, and a document read back must deep-equal the fixture's
|
||||
(`docs/decisions.md` §Equality is deep). `spec.json` is the vendored, upstream machine-readable
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "a\r\nb",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"type": "codeBlock"
|
||||
}
|
||||
],
|
||||
"type": "doc",
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
```adf:codeBlock
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "a\r\nb",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"marks": [
|
||||
{
|
||||
"type": "code"
|
||||
}
|
||||
],
|
||||
"text": "``",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"type": "paragraph"
|
||||
}
|
||||
],
|
||||
"type": "doc",
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
``` `` ```
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "a\r\nb\u0000c",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"marks": [
|
||||
{
|
||||
"attrs": {
|
||||
"href": "https://example.com/",
|
||||
"title": "t\u0000"
|
||||
},
|
||||
"type": "link"
|
||||
}
|
||||
],
|
||||
"text": "link",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"type": "paragraph"
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"attrs": {
|
||||
"color": "neutral",
|
||||
"text": "a\nb"
|
||||
},
|
||||
"type": "status"
|
||||
},
|
||||
{
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"type": "paragraph"
|
||||
}
|
||||
],
|
||||
"type": "doc",
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
a !adf:text{text="\n"}b!adf:text{text="\u0000"}c!adf:link[link]{href="https://example.com/" title="t\u0000"}
|
||||
|
||||
!adf:carry{json="{\"attrs\":{\"color\":\"neutral\",\"text\":\"a\\nb\"},\"type\":\"status\"}"}!adf:carry{json="{\"type\":\"text\"}"}
|
||||
+46
-45
@@ -23,8 +23,8 @@ sanitized and a mention keeps the test user's real account id.
|
||||
|
||||
## Markdown in is a canonical fixpoint
|
||||
|
||||
2026-08-23, the maintainer; the way back made total 2026-10-04. Goals 1 and 4. Valid while
|
||||
markdown input may be written by hand.
|
||||
2026-08-23, the maintainer; every parsed document has converted back since 2026-10-04. Goals 1 and
|
||||
5. Valid while markdown input may be written by hand.
|
||||
|
||||
The other direction is a canonical fixpoint, not byte-identity: human markdown normalizes, the way
|
||||
back yields the library's canonical spelling, and that spelling round-trips byte-identically.
|
||||
@@ -54,7 +54,7 @@ around the leaf. The grammar: `spec/flavour.md` §Inline nodes, **Adjacent text
|
||||
|
||||
## An empty key spells `empty`
|
||||
|
||||
2026-10-03, a writer panel and the maintainer. Goals 1 and 5. Valid while no attribute value
|
||||
2026-10-03, a writer panel and the maintainer. Goals 1 and 6. Valid while no attribute value
|
||||
spells an empty object or array.
|
||||
|
||||
An `attrs`, `content` or `marks` key holding an empty object or array is the reserved key with the
|
||||
@@ -71,7 +71,7 @@ reads it back as `-0`. The grammar: `spec/flavour.md` §Block nodes and §The Co
|
||||
|
||||
## Empty markdown is a document of no blocks
|
||||
|
||||
2026-10-03, a writer panel and the maintainer. Goals 1 and 5. Valid while ADF's schema requires
|
||||
2026-10-03, a writer panel and the maintainer. Goals 1 and 6. Valid while ADF's schema requires
|
||||
`content` on `doc`.
|
||||
|
||||
Markdown holding no block reads as `{ content: [], type: 'doc', version: 1 }`, the document
|
||||
@@ -94,7 +94,7 @@ checking its position, and refusing loses a document ADF itself keeps in an `uns
|
||||
|
||||
## The carry fence names the node type
|
||||
|
||||
2026-10-03, the maintainer. Goals 1 and 5. Valid while a code fence's info string reads back
|
||||
2026-10-03, the maintainer. Goals 1 and 6. Valid while a code fence's info string reads back
|
||||
verbatim.
|
||||
|
||||
The block carry is a code fence whose info string `adf:<type>` names the node's type, its body the
|
||||
@@ -118,7 +118,7 @@ language, and none is empty beside another, since a text node holds text. The gr
|
||||
|
||||
## Foreign HTML sorts three ways
|
||||
|
||||
2026-08-23, the sort 2026-09-20, the maintainer. Goals 1 and 4. Valid while ADF holds no node
|
||||
2026-08-23, the sort 2026-09-20, the maintainer. Goals 1 and 5. Valid while ADF holds no node
|
||||
for a bare container, a comment or a script. Lands with `todo.md` item 6.
|
||||
|
||||
Every foreign element `htmlToAdf` and `markdownToAdf` read sorts one of three ways, never a silent
|
||||
@@ -139,14 +139,14 @@ than they buy.
|
||||
|
||||
## Names stay text
|
||||
|
||||
2026-08-23, the maintainer. Goal 7. Valid while resolving a name to an id needs I/O.
|
||||
2026-08-23, the maintainer. Goal 8. Valid while resolving a name to an id needs I/O.
|
||||
|
||||
A bare `@name` or `:smile:` in typed text stays a text node. Only directives produce
|
||||
mention/emoji/media nodes; resolving names to ids is the consumer's job.
|
||||
|
||||
## Directives under `!adf:`
|
||||
|
||||
2026-08-23, prefixed `!adf:` 2026-09-16, the maintainer. Goals 4 and 5. Valid while prose does not
|
||||
2026-08-23, prefixed `!adf:` 2026-09-16, the maintainer. Goals 5 and 6. Valid while prose does not
|
||||
write `!adf:`.
|
||||
|
||||
Directives are one grammar for everything markdown lacks, namespaced under `!adf:`:
|
||||
@@ -157,7 +157,7 @@ opener nests by itself and leaf versus container falls out of the node's content
|
||||
|
||||
## CommonMark is a subset
|
||||
|
||||
2026-08-23, the maintainer. Goal 4. Valid while prose rarely writes the shapes the carve-outs claim.
|
||||
2026-08-23, the maintainer. Goal 5. Valid while prose rarely writes the shapes the carve-outs claim.
|
||||
|
||||
Plain CommonMark is a subset, with carve-outs (`spec/flavour.md`): literal text shaped like a
|
||||
directive, a pipe table or a `~~` pair is claimed, and so is a code fence whose info string opens
|
||||
@@ -166,14 +166,14 @@ claims, giving CommonMark its own reader, and `todo.md` item 65 ends the image g
|
||||
|
||||
## Tables
|
||||
|
||||
2026-08-23, the maintainer. Goal 5. Valid while a pipe table holds only one header row and inline
|
||||
2026-08-23, the maintainer. Goal 6. Valid while a pipe table holds only one header row and inline
|
||||
cells.
|
||||
|
||||
One header row plus plain inline cells → pipe table; anything richer → directive form.
|
||||
|
||||
## Links
|
||||
|
||||
2026-09-13, nesting 2026-09-17, the maintainer. Goals 1 and 5. Valid while CommonMark's link
|
||||
2026-09-13, nesting 2026-09-17, the maintainer. Goals 1 and 6. Valid while CommonMark's link
|
||||
syntax is what readers edit.
|
||||
|
||||
`[text](url "title")`, or `<url>` for a bare autolink-shaped text, wherever CommonMark spells the
|
||||
@@ -192,7 +192,7 @@ accepted.
|
||||
|
||||
## Plain task ids come from position
|
||||
|
||||
2026-09-26, spelling 2026-09-29, the maintainer. Goals 6 and 7. Valid while a site rejects a task
|
||||
2026-09-26, spelling 2026-09-29, the maintainer. Goals 7 and 8. Valid while a site rejects a task
|
||||
node with no `localId`.
|
||||
|
||||
`plainMarkdownToAdf` gives each `taskList`, `taskItem` and `blockTaskItem` lacking one a `localId`
|
||||
@@ -204,7 +204,7 @@ ids are only skipped.
|
||||
|
||||
## A callout title keeps its link targets
|
||||
|
||||
2026-09-29, the maintainer. Goals 5 and 6. Valid while an expand's `title` is a string.
|
||||
2026-09-29, the maintainer. Goals 6 and 7. Valid while an expand's `title` is a string.
|
||||
|
||||
`plainMarkdownToAdf` writes a link in a folded callout's title as its text and its target in
|
||||
parentheses: `> [!faq]- See [x](http://y)` reads to the title `See x (http://y)`. A link whose text
|
||||
@@ -213,7 +213,7 @@ is its target, with or without `mailto:`, keeps its text alone: `<http://y>` tit
|
||||
|
||||
## The plain flavour's spellings
|
||||
|
||||
2026-09-14, panels 2026-09-25 and 2026-09-29, the maintainer. Goals 5 and 6. Valid while GitHub's
|
||||
2026-09-14, panels 2026-09-25 and 2026-09-29, the maintainer. Goals 6 and 7. Valid while GitHub's
|
||||
renderer is the one the audience's markdown is read in.
|
||||
|
||||
README §Plain markdown's rows come from a survey of GitHub, GitLab, Gitea, Obsidian, Pandoc,
|
||||
@@ -232,7 +232,7 @@ past `[x]`/`[ ]`, and lifting bare URLs, `@name`, `:shortcode:` or ISO dates int
|
||||
|
||||
## The HTML dialect
|
||||
|
||||
2026-08-23, the maintainer. Goals 5 and 7. Valid while HTML output is read by consumers styling it
|
||||
2026-08-23, the maintainer. Goals 6 and 8. Valid while HTML output is read by consumers styling it
|
||||
themselves.
|
||||
|
||||
The HTML dialect mirrors the markdown flavour: semantic elements, stable `adf-*` classes, `data-*`
|
||||
@@ -240,7 +240,7 @@ for what HTML cannot express, text always escaped. No stylesheet ships.
|
||||
|
||||
## No runtime dependencies
|
||||
|
||||
2026-08-23, the maintainer. Goal 7. Valid while ~20 lines of own code, or a vendored table, do each
|
||||
2026-08-23, the maintainer. Goal 8. Valid while ~20 lines of own code, or a vendored table, do each
|
||||
job a dependency would.
|
||||
|
||||
`dependencies` is empty. A runtime dependency enters only through an entry here stating why ~20
|
||||
@@ -250,7 +250,7 @@ and HTML parsers are written in this repo.
|
||||
## Standards ship as data
|
||||
|
||||
2026-08-30, the CommonMark suite 2026-09-05 and ADF's schemas 2026-09-13, the maintainer. Goals 1,
|
||||
4 and 7. Valid while each table is fixed data a dependency would only wrap.
|
||||
5 and 8. Valid while each table is fixed data a dependency would only wrap.
|
||||
|
||||
A table a standard fixes is data rather than a dependency: HTML5's 2125 semicolon-terminated
|
||||
character references ship packed in their own module, so entity decoding is complete without one.
|
||||
@@ -273,7 +273,7 @@ nodes that break it.
|
||||
|
||||
## Any ES2022 engine
|
||||
|
||||
2026-09-01, the maintainer. Goal 7. Valid while ES2022 is the floor browsers and servers share.
|
||||
2026-09-01, the maintainer. Goal 8. Valid while ES2022 is the floor browsers and servers share.
|
||||
|
||||
The library runs on any ES2022 engine, not only Node — a browser as readily as a server. The
|
||||
shipped source is ECMAScript and nothing else: no host import, no host global, no DOM.
|
||||
@@ -287,13 +287,13 @@ never the higher one those repo-only tools want.
|
||||
|
||||
## ESM only
|
||||
|
||||
2026-08-23, the maintainer. Goal 7. Valid while the audience's toolchains all import ES modules.
|
||||
2026-08-23, the maintainer. Goal 8. Valid while the audience's toolchains all import ES modules.
|
||||
|
||||
No CommonJS build, no dual-package hazard.
|
||||
|
||||
## One built entrypoint
|
||||
|
||||
2026-08-23, the maintainer. Goal 7. Valid while Node refuses to type-strip under `node_modules`.
|
||||
2026-08-23, the maintainer. Goal 8. Valid while Node refuses to type-strip under `node_modules`.
|
||||
|
||||
Built JavaScript, `.d.ts` beside it. Do not add a TypeScript-source entrypoint — Node refuses to
|
||||
type-strip under `node_modules` (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`), so it cannot serve
|
||||
@@ -301,7 +301,7 @@ an npm consumer.
|
||||
|
||||
## Public on npm
|
||||
|
||||
2026-08-23, the name 2026-09-01, the maintainer. Goals 2 and 7. Valid while the package's source
|
||||
2026-08-23, the name 2026-09-01, the maintainer. Goals 2 and 8. Valid while the package's source
|
||||
stays public beside it.
|
||||
|
||||
Published to public npm as `@larvit/adf-codec`. Public source: the Gitea repo goes public,
|
||||
@@ -347,10 +347,10 @@ handles one cause alike whichever node, attribute or direction raised it.
|
||||
|
||||
- A code names the cause; where one cause recurs across node types, across one mark's attributes
|
||||
or across directions, one code covers them all and `path` and `message` say which —
|
||||
`unsupported-nesting-depth` is the 500-level guard whichever direction hits it. Where two codes
|
||||
stay apart, the line between them is what they name: `unspellable-whitespace` is the newline an
|
||||
inline directive's content slot spells in input, where the emitter carries the node
|
||||
(2026-10-04).
|
||||
`unsupported-nesting-depth` is the 500-level guard whichever direction hits it.
|
||||
`unspellable-whitespace` is the newline an inline directive's content slot spells in input; the
|
||||
emitter carries such a node. A null character in that slot is `unsupported-node-shape`, since
|
||||
`unspellable-whitespace` does not read true of it (2026-10-04).
|
||||
- A claim code names the spelling claimed, never the node that spelling would have built: a
|
||||
malformed `!adf:table` is a `malformed-directive`, and an alignment colon a
|
||||
`malformed-pipe-table` — the flavour's own delimiter row is `-` runs, so the grammar refuses the
|
||||
@@ -375,7 +375,7 @@ handles one cause alike whichever node, attribute or direction raised it.
|
||||
|
||||
## `message` and `path`
|
||||
|
||||
2026-09-03, the path 2026-09-23, the maintainer. Goals 1 and 4. Valid while a person fixing the
|
||||
2026-09-03, the path 2026-09-23, the maintainer. Goals 1 and 5. Valid while a person fixing the
|
||||
input reads `message`.
|
||||
|
||||
- A message names the violation, not the rule alone — a rule by itself states a truth the reader
|
||||
@@ -389,7 +389,7 @@ input reads `message`.
|
||||
|
||||
## Publish on a version bump
|
||||
|
||||
2026-08-23, converging 2026-09-03, the maintainer. Goal 7. Valid while CI on `main` holds the npm
|
||||
2026-08-23, converging 2026-09-03, the maintainer. Goal 8. Valid while CI on `main` holds the npm
|
||||
token.
|
||||
|
||||
`package.json` version on `main` is the source of truth. CI on `main`: tests green and the version
|
||||
@@ -405,7 +405,7 @@ depend on a store that the gate would then have to keep.
|
||||
|
||||
## Docs describe the release being built
|
||||
|
||||
2026-09-16, the maintainer. Goal 7. Valid while a bump on `main` publishes.
|
||||
2026-09-16, the maintainer. Goal 8. Valid while a bump on `main` publishes.
|
||||
|
||||
Docs on `main` describe the release being built rather than the version npm holds, so they match it
|
||||
the moment the bump publishes; add no interim note marking the gap.
|
||||
@@ -421,7 +421,7 @@ nesting a spelling inside its own kind (`*(*a*)*`) names that mark once.
|
||||
|
||||
## The gate runs on Deno and Bun
|
||||
|
||||
2026-09-01, Deno's reason 2026-09-28, the maintainer. Goals 3 and 7. Valid while the library claims
|
||||
2026-09-01, Deno's reason 2026-09-28, the maintainer. Goals 4 and 8. Valid while the library claims
|
||||
any ES2022 engine.
|
||||
|
||||
The gate runs the suite under Deno and Bun as well as Node. Bun runs JavaScriptCore, the one engine
|
||||
@@ -433,7 +433,7 @@ over the corpus rather than over a smoke import.
|
||||
|
||||
## The gate installs the tarball
|
||||
|
||||
2026-09-03, the maintainer. Goal 7. Valid while consumers install the packed package.
|
||||
2026-09-03, the maintainer. Goal 8. Valid while consumers install the packed package.
|
||||
|
||||
The gate packs the build and installs the tarball under `package-tests/`, so `files`, `exports`
|
||||
and `types` are proved on the artifact that ships rather than on the source tree a self-reference
|
||||
@@ -445,7 +445,7 @@ resolver maps them, under `NodeNext` alone; a `.d.ts` reader that is not `tsc` s
|
||||
|
||||
## Firefox reads the build
|
||||
|
||||
2026-09-04, the maintainer. Goal 7. Valid while the library claims a browser and no other leg runs
|
||||
2026-09-04, the maintainer. Goal 8. Valid while the library claims a browser and no other leg runs
|
||||
SpiderMonkey.
|
||||
|
||||
A headless Firefox loads `dist/index.js` over HTTP and converts the round-trip, normalization and
|
||||
@@ -493,16 +493,17 @@ exits 0 on.
|
||||
2026-10-03, the maintainer. KISS, a technical principle, and its comprehension floor of 7. Valid
|
||||
while `todo.md` items 59, 60, 61 and 62 are open.
|
||||
|
||||
A four-seat comprehension panel scored the project under the floor of 7. Its round-5 scores
|
||||
(2026-10-03) are the baseline. Since 2026-10-04, a chunk whose panel scores any dimension lower
|
||||
A four-seat comprehension panel scored the project under the floor of 7. Its scoring run of
|
||||
2026-10-04 is the baseline. Since 2026-10-04, a chunk whose panel scores any dimension lower
|
||||
improves what the seats named and scores again, and after three such rounds asks the maintainer.
|
||||
|
||||
| Seat | Navigation | Locality | Shape | Self-sufficiency | Overall |
|
||||
|---|---|---|---|---|---|
|
||||
| Junior | 6 | 5 | 6 | 5 | 5.5 |
|
||||
| Mid | 6.5 | 5 | 6 | 4.5 | 5.5 |
|
||||
| Senior | 6.5 | 5.5 | 6.5 | 5.5 | 6 |
|
||||
| Architect | 6.5 | 5.5 | 6 | 6 | 6 |
|
||||
| Junior | 7 | 5.5 | 6.5 | 6 | 6 |
|
||||
| Mid | 7 | 5.5 | 6.5 | 6 | 6 |
|
||||
| Senior | 7 | 5.5 | 6.5 | 5.5 | 6 |
|
||||
| Architect | 7 | 5.5 | 6 | 6 | 6 |
|
||||
| Mean | 7.00 | 5.50 | 6.38 | 5.88 | 6.00 |
|
||||
|
||||
Three panels on near-identical code scored overall means of 5.88, 5.75 and 5.75, and a seat moves
|
||||
±0.5 between runs.
|
||||
@@ -516,7 +517,7 @@ markdown, on a fixed seed in the gate; a counterexample found becomes a round-tr
|
||||
|
||||
## The CommonMark suite checks three ways
|
||||
|
||||
2026-08-27, the maintainer. Goals 1 and 3. Valid while the suite's answers are HTML ADF cannot be
|
||||
2026-08-27, the maintainer. Goals 1 and 4. Valid while the suite's answers are HTML ADF cannot be
|
||||
compared against.
|
||||
|
||||
Each example is a named error or markdown that parses and emits to itself byte for byte; its
|
||||
@@ -563,12 +564,12 @@ once doubled the parser's frames per level.
|
||||
|
||||
## No value `JSON.parse` or `structuredClone` builds loops a call
|
||||
|
||||
2026-10-04, the maintainer. Goals 1 and 8. Valid while every persona hands in plain data, as
|
||||
2026-10-04, the maintainer. Goals 1 and 9. Valid while every persona hands in plain data, as
|
||||
`JSON.parse` or `structuredClone` builds it.
|
||||
|
||||
The guards walk a value by `Object.values` and refuse a cycle in it. An inherited or non-enumerable
|
||||
key, a getter, a Proxy or a custom iterator is the caller's to rule out: no persona builds one.
|
||||
Goal 8's size counts ADF as JSON, so a shared object costs once per place it is held.
|
||||
Goal 9's size counts ADF as JSON, so a shared object costs once per place it is held.
|
||||
|
||||
## Nothing spreads an unbounded array
|
||||
|
||||
@@ -588,7 +589,7 @@ termination is the loop's own check.
|
||||
|
||||
## Readers scan by index
|
||||
|
||||
2026-08-30, the kept scan 2026-09-18, the maintainer. Goal 8. Valid while the pipeline persona
|
||||
2026-08-30, the kept scan 2026-09-18, the maintainer. Goal 9. Valid while the pipeline persona
|
||||
feeds documents nobody typed.
|
||||
|
||||
A reader takes the text and an index — a sticky regex whose `lastIndex` the caller sets on the line
|
||||
@@ -600,7 +601,7 @@ two cannot disagree — which is what makes the kept value a memo rather than a
|
||||
|
||||
## The spelling memo
|
||||
|
||||
2026-09-19, the maintainer. Goal 8. Valid while the `commonMarkSpelling` ask spells a node once
|
||||
2026-09-19, the maintainer. Goal 9. Valid while the `commonMarkSpelling` ask spells a node once
|
||||
per level above it otherwise.
|
||||
|
||||
The parse and the plain reduction keep each node's readable spelling in a memo, so the
|
||||
@@ -612,7 +613,7 @@ one. Only what succeeded is kept, so no path minted at another position is ever
|
||||
|
||||
## Cost fixes are measured, never timed
|
||||
|
||||
2026-09-18, the maintainer and the stability-reviewer. Goal 8. Valid while Goal 8 promises growth
|
||||
2026-09-18, the maintainer and the stability-reviewer. Goal 9. Valid while Goal 9 promises growth
|
||||
rather than a figure.
|
||||
|
||||
A cost fix that changes no behaviour lands on the suite staying green with no fixture output
|
||||
@@ -631,7 +632,7 @@ refuses it — which is how the whitespace carry finds a line edge.
|
||||
|
||||
## Emphasis follows CommonMark's matching
|
||||
|
||||
2026-08-27, the maintainer. Goals 1 and 3. Valid while CommonMark's emphasis rules are the
|
||||
2026-08-27, the maintainer. Goals 1 and 4. Valid while CommonMark's emphasis rules are the
|
||||
reader's.
|
||||
|
||||
Emphasis is spelled against CommonMark's matching, never flanking alone: a delimiter run in text
|
||||
@@ -640,7 +641,7 @@ only ones in play, and a pair that matching hands to another delimiter rides the
|
||||
|
||||
## Readable spellings take the `try` prefix
|
||||
|
||||
2026-09-21, the maintainer. Goals 1 and 5. Valid while a readable spelling's refusal would cost a
|
||||
2026-09-21, the maintainer. Goals 1 and 6. Valid while a readable spelling's refusal would cost a
|
||||
document the general form spells.
|
||||
|
||||
A readable spelling tried ahead of a general one takes the `try` prefix and fails only where the
|
||||
|
||||
+18
-17
@@ -450,7 +450,8 @@ the nodes below, `emoji`, `mention` and `status` spell their `text` attribute in
|
||||
plain text: `[]` is the empty string, absent content is the absent attribute, non-empty content
|
||||
parsing to anything but one text node carrying neither marks, attributes nor content is a named
|
||||
error, and so is a `text` key in `{attrs}`. An enclosing mark spelling does not reach into the slot.
|
||||
A node whose `text` holds a line ending or a null character rides the inline carry.
|
||||
An `emoji`, `mention` or `status` whose `text` attribute holds a line ending or a null character
|
||||
rides the inline carry; a content slot spelling one is a named error.
|
||||
The rest take no content, `!adf:text` included; content on a node that takes none is a named error.
|
||||
|
||||
- `date` — Attributes: `localId` (string), `timestamp` (string, epoch milliseconds).
|
||||
@@ -473,15 +474,15 @@ The rest take no content, `!adf:text` included; content on a node that takes non
|
||||
Shipped !adf:emoji[🎉]{shortName=":tada:"} on !adf:date{timestamp=1756080000000}.
|
||||
```
|
||||
|
||||
**Characters CommonMark cannot hold.** A carriage return in text is ` `, which CommonMark reads
|
||||
back as the character. A newline inside a text node, a null character, which CommonMark reads as
|
||||
U+FFFD, and a space or tab where CommonMark strips or refuses one — a block's inline content edges,
|
||||
either side of a line break, an em, strong or strike spelling's inner edges, a pipe cell's edges —
|
||||
is spelled `!adf:text{text="…"}`, the reserved key carrying the node's text, escaped by the
|
||||
attribute grammar and never literal: pipe cells trim and pad. The emitter wraps the run alone and
|
||||
leaves the rest plain text, which the spelled run joins on reading. Input reads that spelling
|
||||
alone: the value is one run of spaces and tabs, one run of newlines, or one run of null characters,
|
||||
and anything else — a mixed run, or text CommonMark carries plainly — is a named error.
|
||||
**Text CommonMark cannot hold plainly.** A carriage return in text is ` `, which CommonMark
|
||||
reads back as the character. Three things are spelled `!adf:text{text="…"}`: a newline inside a text
|
||||
node; a null character, which CommonMark reads as U+FFFD; and a space or tab where CommonMark strips
|
||||
or refuses one — a block's inline content edges, either side of a line break, an em, strong or
|
||||
strike spelling's inner edges, a pipe cell's edges. The reserved key carries the node's text,
|
||||
escaped by the attribute grammar and never literal: pipe cells trim and pad. The emitter wraps the
|
||||
run alone and leaves the rest plain text, which the spelled run joins on reading. Input reads that
|
||||
spelling alone: the value is one run of spaces and tabs, one run of newlines, or one run of null
|
||||
characters, and anything else — a mixed run, or text CommonMark carries plainly — is a named error.
|
||||
|
||||
```
|
||||
!adf:text{text=" "}Two leading spaces held, and one text node split!adf:text{text="\n"}over two lines.
|
||||
@@ -534,13 +535,13 @@ breaks at every node the emitter carries, so no emitted carry sits inside a mark
|
||||
An inline node whose marks no nesting spells — a mark type not listed here, an attrs key its
|
||||
spelling does not list, a value that is not the spelling's type, an attribute the spelling needs and
|
||||
the mark lacks, an empty `attrs` on a mark CommonMark spells, an order putting a code span outside
|
||||
another mark, `code` over anything but a text node holding text, over text holding a line ending or
|
||||
a null character, or opening a paragraph line its backticks would read back as a fence, or a spelling
|
||||
CommonMark's flanking rules cannot open or close where the run sits (`un**-real**istic`), or one
|
||||
CommonMark's matching pairs elsewhere — the intra-word `*` runs together with a neighbouring `**`,
|
||||
and the multiple-of-3 rule can leave the merged run's pairing to another delimiter — rides the
|
||||
inline carry whole. An opaque carry inside a mark spelling is a named error in input: the carry
|
||||
restores its node exactly, marks included (`docs/decisions.md` §Unknown nodes ride the carry).
|
||||
another mark, `code` over anything but a text node holding text, or over text holding a line ending
|
||||
or a null character, or a spelling CommonMark's flanking rules cannot open or close where the run
|
||||
sits (`un**-real**istic`), or one CommonMark's matching pairs elsewhere — the intra-word `*` runs
|
||||
together with a neighbouring `**`, and the multiple-of-3 rule can leave the merged run's pairing to
|
||||
another delimiter — rides the inline carry whole. An opaque carry inside a mark spelling is a named
|
||||
error in input: the carry restores its node exactly, marks included (`docs/decisions.md` §Unknown
|
||||
nodes ride the carry).
|
||||
|
||||
```
|
||||
!adf:textColor[**Overdue**]{color="#ae2e24"}, H!adf:subsup[2]{type=sub}O, !adf:underline[signed].
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ export function emptyKeys(held: { attrs?: AdfAttributes; content?: AdfNode[]; ma
|
||||
return keys
|
||||
}
|
||||
|
||||
// A format spells this node as text; anything more rides a carry, save content, which is refused.
|
||||
// No attrs, content or empty marks; a format may spell this node as text, and anything more rides a carry.
|
||||
export function isBareText(node: AdfNode): boolean {
|
||||
return node.type === 'text' && node.attrs === undefined && node.content === undefined && node.marks?.length !== 0
|
||||
}
|
||||
|
||||
@@ -34,11 +34,11 @@ function shownText(nodes: readonly AdfNode[]): string[] {
|
||||
return shown.filter((text) => text !== '')
|
||||
}
|
||||
|
||||
test('a generated document refuses to emit, or its markdown reads back to it', { timeout: propertyTimeout }, () => {
|
||||
test('a generated document emits markdown that reads back to it', { timeout: propertyTimeout }, () => {
|
||||
fc.assert(
|
||||
fc.property(adfDocument, (document) => {
|
||||
const emitted = adfToMarkdown(document)
|
||||
if (!emitted.ok) return
|
||||
assert.ok(emitted.ok, emitted.ok ? '' : `${emitted.error.code}: ${emitted.error.message}`)
|
||||
const read = markdownToAdf(emitted.value)
|
||||
assert.ok(read.ok, read.ok ? '' : `${read.error.code}: ${read.error.message} — reading ${JSON.stringify(emitted.value)}`)
|
||||
assert.deepEqual(read.value, document, `reading ${JSON.stringify(emitted.value)}`)
|
||||
|
||||
@@ -83,10 +83,6 @@ export function backslashEscape(text: string, index: number): string | undefined
|
||||
return isAsciiPunctuation(escaped) ? escaped : undefined
|
||||
}
|
||||
|
||||
export function claimsLine(line: string, position: LinePosition): boolean {
|
||||
return escapesLineClaim(line, 0, position) || orderedListOpener.test(line)
|
||||
}
|
||||
|
||||
export function claimsPipeLine(line: string): boolean {
|
||||
return pipeClaim.test(line)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { backslashEscape, decodeTextEscapes, holdsControlCharacter } from './grammar.ts'
|
||||
import { backslashEscape, decodeTextEscapes, holdsControlCharacter, holdsNullCharacter } from './grammar.ts'
|
||||
import { holdsEntityReference } from './entity-references.ts'
|
||||
|
||||
export type LinkDefinition = { destination: string; title?: string }
|
||||
@@ -117,5 +117,5 @@ export function escapeUnbalanced(spelling: string, opener: string, closer: strin
|
||||
}
|
||||
|
||||
function spellTitle(title: string): string | undefined {
|
||||
return /[\n\r\0\\]/.test(title) || holdsEntityReference(title) ? undefined : ` "${title.replaceAll('"', '\\"')}"`
|
||||
return /[\n\r\\]/.test(title) || holdsNullCharacter(title) || holdsEntityReference(title) ? undefined : ` "${title.replaceAll('"', '\\"')}"`
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AttributeKind, VocabularyPair, VocabularyValue } from '../adf/attribute-vocabulary.ts'
|
||||
import type { ConvertFault } from '../result.ts'
|
||||
import type { JsonValue } from '../json-value.ts'
|
||||
import { backslashEscape } from './commonmark/grammar.ts'
|
||||
import { backslashEscape, holdsNullCharacter } from './commonmark/grammar.ts'
|
||||
import { backtickRun, closingBacktickRun } from './commonmark/backtick-runs.ts'
|
||||
import { isJsonValue, overNested } from '../json-value.ts'
|
||||
import { largestNesting } from '../nesting.ts'
|
||||
@@ -100,10 +100,10 @@ export function readSoleStringAttribute(span: DirectiveSpan, key: string): Read<
|
||||
return { value: spelled.decoded }
|
||||
}
|
||||
|
||||
// An inline directive never spans lines, so no content slot holds a line ending.
|
||||
export function slotLineEndingFault(type: string, text: string): ConvertFault | undefined {
|
||||
if (!/[\n\r]/.test(text)) return undefined
|
||||
return { code: 'unspellable-whitespace', message: `the ${type} content slot holds a newline no inline directive spans` }
|
||||
// An inline directive never spans lines, and no content slot holds a null character; the emitter carries such a node.
|
||||
export function slotFault(type: string, text: string): ConvertFault | undefined {
|
||||
if (/[\n\r]/.test(text)) return { code: 'unspellable-whitespace', message: `the ${type} content slot holds a newline no inline directive spans` }
|
||||
return holdsNullCharacter(text) ? unsupportedNodeShape(`the ${type} content slot holds a null character: write the node as !adf:carry`) : undefined
|
||||
}
|
||||
|
||||
export function spellAttributes(pairs: readonly (readonly [string, string])[]): string {
|
||||
|
||||
@@ -153,11 +153,10 @@ function readsBack(doc: AdfDocument): string {
|
||||
return spelled
|
||||
}
|
||||
|
||||
test('carries a code span whose backticks would open its line as a fence', () => {
|
||||
const fence: AdfNode = { marks: [{ type: 'code' }], text: '```', type: 'text' }
|
||||
const carried = '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"code\\"}],\\"text\\":\\"\\u0060\\u0060\\u0060\\",\\"type\\":\\"text\\"}"}'
|
||||
assert.equal(readsBack(document(paragraph(fence))), `${carried}\n`)
|
||||
assert.equal(readsBack(document(paragraph({ text: 'a', type: 'text' }, { type: 'hardBreak' }, fence, fence))), `a\\\n${carried}\`\`\`\` \`\`\` \`\`\`\`\n`)
|
||||
test('spells a code span opening a line, whose backticks open no fence', () => {
|
||||
const fence: AdfNode = { marks: [{ type: 'code' }], text: '``', type: 'text' }
|
||||
assert.equal(readsBack(document(paragraph(fence))), '``` `` ```\n')
|
||||
assert.equal(readsBack(document(paragraph({ text: 'a', type: 'text' }, { type: 'hardBreak' }, fence))), 'a\\\n``` `` ```\n')
|
||||
})
|
||||
|
||||
test('escapes the delimiter row a hard break leaves opening a pipe table with no leading pipe', () => {
|
||||
|
||||
@@ -272,7 +272,7 @@ function emitCodeDirective(node: AdfNode, model: BlockNodeModel, path: ConvertEr
|
||||
return success(directivePair(node, opener.value, texts.map((text) => fencedCodeBlock(info, text)).join('\n')))
|
||||
}
|
||||
|
||||
// The text each fence holds, or `undefined` where a child is no text a fence keeps, which the carry holds instead.
|
||||
// The text of each fence, or `undefined` where a child holds what no fence keeps, so the block carry takes the node.
|
||||
function fencedTexts(node: AdfNode): string[] | undefined {
|
||||
if (node.content === undefined) return ['']
|
||||
const texts: string[] = []
|
||||
|
||||
@@ -4,17 +4,17 @@ import type { InlineNodeModel } from '../../adf/inline-nodes.ts'
|
||||
import type { LineContainer } from '../line-container.ts'
|
||||
import { assembleInlineLine, isSyntax, type InlineEscaping, type InlineSegment, type MarkRun, type NodeRange } from './line-escaping.ts'
|
||||
import { carriedInline } from '../opaque-carry.ts'
|
||||
import { claimsLine, holdsNullCharacter, trimTrailingSpace } from '../commonmark/grammar.ts'
|
||||
import { holdsNullCharacter, trimTrailingSpace } from '../commonmark/grammar.ts'
|
||||
import { commonMarkLink, linkHref, markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
|
||||
import { identicalMark, isBareText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||
import { escapeUnbalanced, spellDestination } from '../commonmark/link-syntax.ts'
|
||||
import { identicalMark, isBareText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { highlightDelimiter } from '../plain/conventions.ts'
|
||||
import { inlineNodeModel } from '../../adf/inline-nodes.ts'
|
||||
import { joinsWhenRead, textBreakSpelling } from '../adjacent-text.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
import { longestBacktickRun } from '../commonmark/backtick-runs.ts'
|
||||
import { slotLineEndingFault, spellInlineDirectiveOpener, spellInlineLeafDirective } from '../directive-syntax.ts'
|
||||
import { slotFault, spellInlineDirectiveOpener, spellInlineLeafDirective } from '../directive-syntax.ts'
|
||||
import { spellInlineNodeAttributes } from './inline-directive-spelling.ts'
|
||||
import { spellTextDirective } from '../text-directive.ts'
|
||||
|
||||
@@ -38,7 +38,7 @@ type LineAttempt = { fallback: NodeRange | 'opening-link'; line?: undefined } |
|
||||
|
||||
type LineFallbacks = { carried: Set<number>; flavour: Flavour; openingLinkAsDirective: boolean }
|
||||
|
||||
export type PlainLineFallback = { kind: 'claimed-line'; line: number; text: string } | { kind: 'opening-link' } | { kind: 'unspellable-run'; runs: [MarkRun, ...MarkRun[]] }
|
||||
export type PlainLineFallback = { kind: 'opening-link' } | { kind: 'unspellable-run'; runs: [MarkRun, ...MarkRun[]] }
|
||||
|
||||
export function emitInlineLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, flavour: Flavour): Result<string> {
|
||||
const emitted = emitLine(nodes, container, path, flavour)
|
||||
@@ -119,19 +119,7 @@ function attemptLine(segments: readonly InlineSegment[], container: LineContaine
|
||||
const verdict = lineVerdict(segments, container, flavour)
|
||||
if (verdict.kind === 'opening-link') return { fallback: 'opening-link' }
|
||||
if (verdict.kind === 'unspellable-run') return { fallback: verdict.runs[0] }
|
||||
if (verdict.kind === 'line') return { line: verdict.text }
|
||||
return { fallback: lineOpener(segments, verdict.line) }
|
||||
}
|
||||
|
||||
// A code span's backticks are what block parsing claims, and its segment names its node; a segment naming none leaves takeFallback to refuse the line.
|
||||
function lineOpener(segments: readonly InlineSegment[], line: number): NodeRange {
|
||||
let breaks = 0
|
||||
const opener = segments.find((segment) => {
|
||||
const opens = breaks === line && segment.text !== ''
|
||||
breaks += segment.text.split('\n').length - 1
|
||||
return opens
|
||||
})
|
||||
return opener?.nodes ?? { first: 0, last: -1 }
|
||||
return { line: verdict.text }
|
||||
}
|
||||
|
||||
// The fallbacks in the order a line takes them, or the line where it takes none.
|
||||
@@ -139,10 +127,7 @@ function lineVerdict(segments: readonly InlineSegment[], container: LineContaine
|
||||
const assembled = assembleInlineLine(segments, container, flavour)
|
||||
if (assembled.openingLinkAsDirective) return { kind: 'opening-link' }
|
||||
const [run, ...others] = assembled.unspellableRuns
|
||||
if (run !== undefined) return { kind: 'unspellable-run', runs: [run, ...others] }
|
||||
const lines = assembled.line.split('\n')
|
||||
const claimed = container === 'paragraph' ? lines.findIndex((single, index) => claimsLine(single, index === 0 ? 'first' : 'later')) : -1
|
||||
return claimed === -1 ? { kind: 'line', text: assembled.line } : { kind: 'claimed-line', line: claimed, text: lines[claimed] ?? '' }
|
||||
return run === undefined ? { kind: 'line', text: assembled.line } : { kind: 'unspellable-run', runs: [run, ...others] }
|
||||
}
|
||||
|
||||
// spec/flavour.md, Inline nodes.
|
||||
@@ -232,9 +217,14 @@ function nodePath(context: InlineContext, index: number): ConvertErrorPath {
|
||||
return [...context.path, 'content', index]
|
||||
}
|
||||
|
||||
// Decides the text-node and slot-fault carries up front, so a line holding many of them is emitted once.
|
||||
function carries(node: AdfNode, carried: ReadonlySet<number>, index: number): boolean {
|
||||
if (carried.has(index)) return true
|
||||
return node.type !== 'text' && inlineNodeModel(node.type) === undefined
|
||||
if (node.type === 'text') return !isBareText(node) || typeof node.text !== 'string' || node.text === ''
|
||||
const model = inlineNodeModel(node.type)
|
||||
if (model === undefined) return true
|
||||
const slot = model.textAttribute === undefined ? undefined : nodeAttrs(node)[model.textAttribute]
|
||||
return typeof slot === 'string' && slotFault(node.type, slot) !== undefined && nodeContent(node).length === 0 && node.text === undefined
|
||||
}
|
||||
|
||||
function emitLeaf(node: AdfNode, context: InlineContext, index: number): Result<Emission> {
|
||||
@@ -268,13 +258,12 @@ function emitInlineDirective(node: AdfNode, model: InlineNodeModel, index: numbe
|
||||
if (attributes === undefined) return success({ carry: { first: index, last: index } })
|
||||
const slot = model.textAttribute === undefined ? undefined : nodeAttrs(node)[model.textAttribute]
|
||||
if (slot === undefined) return success({ segments: [syntax(spellInlineLeafDirective(node.type, attributes))] })
|
||||
if (typeof slot !== 'string') return success({ carry: { first: index, last: index } })
|
||||
if (slotLineEndingFault(node.type, slot) !== undefined || holdsNullCharacter(slot)) return success({ carry: { first: index, last: index } })
|
||||
if (typeof slot !== 'string' || slotFault(node.type, slot) !== undefined) return success({ carry: { first: index, last: index } })
|
||||
const content: InlineSegment[] = slot === '' ? [] : [{ escaping: 'bracketed', text: slot }]
|
||||
return success({ segments: [syntax(spellInlineDirectiveOpener(node.type)), ...content, syntax(`]${attributes}`)] })
|
||||
}
|
||||
|
||||
// spec/flavour.md, Inline nodes: CommonMark reads a carriage return as a line ending, and a null character as U+FFFD.
|
||||
// spec/flavour.md, Text CommonMark cannot hold plainly.
|
||||
function emitText(node: AdfNode, context: InlineContext, index: number): Emission {
|
||||
if (!isBareText(node) || typeof node.text !== 'string' || node.text === '') return { carry: { first: index, last: index } }
|
||||
const escaping: InlineEscaping = context.bracketed ? 'bracketed' : 'backslash'
|
||||
@@ -324,16 +313,14 @@ function emitHighlight(nodes: readonly AdfNode[], depth: number, range: NodeRang
|
||||
|
||||
// Each node its own span: CommonMark reads two adjacent text nodes in one span back as one.
|
||||
function emitCodeSpan(nodes: readonly AdfNode[], depth: number, range: NodeRange): Emission {
|
||||
const segments: InlineSegment[] = []
|
||||
for (const [offset, node] of nodes.entries()) {
|
||||
const spans: string[] = []
|
||||
for (const node of nodes) {
|
||||
const { text } = node
|
||||
if (!isBareText(node) || nodeMarks(node).length !== depth + 1 || typeof text !== 'string' || text === '' || /[\n\r]/.test(text) || holdsNullCharacter(text)) return { carry: range }
|
||||
if (offset > 0) segments.push(syntax(textBreakSpelling))
|
||||
const fence = '`'.repeat(longestBacktickRun(text) + 1)
|
||||
const index = range.first + offset
|
||||
segments.push({ escaping: 'none', nodes: { first: index, last: index }, text: `${fence}${needsPadding(text) ? ` ${text} ` : text}${fence}` })
|
||||
spans.push(`${fence}${needsPadding(text) ? ` ${text} ` : text}${fence}`)
|
||||
}
|
||||
return { segments }
|
||||
return { segments: [syntax(spans.join(textBreakSpelling))] }
|
||||
}
|
||||
|
||||
function needsPadding(text: string): boolean {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { inlineMarkSpellingFault } from './directive-marks.ts'
|
||||
import { inlineNodeModel } from '../../adf/inline-nodes.ts'
|
||||
import { readEmptyKeys, spellsEmpty } from '../empty-keys.ts'
|
||||
import { readVocabulary } from './directive-attributes.ts'
|
||||
import { slotLineEndingFault } from '../directive-syntax.ts'
|
||||
import { slotFault } from '../directive-syntax.ts'
|
||||
import { textBreakName } from '../adjacent-text.ts'
|
||||
import { textDirectiveName } from '../text-directive.ts'
|
||||
|
||||
@@ -68,8 +68,8 @@ export function readInlineDirectiveNode(
|
||||
if (text === undefined) {
|
||||
return failure('unsupported-node-shape', `the ${name} content slot holds one text node carrying neither marks, attributes nor content: this one holds something else`, path)
|
||||
}
|
||||
const spans = slotLineEndingFault(name, text)
|
||||
if (spans !== undefined) return faulted(spans, path)
|
||||
const fault = slotFault(name, text)
|
||||
if (fault !== undefined) return faulted(fault, path)
|
||||
attrs.value[slot] = text
|
||||
}
|
||||
return success(namedNode(name, attrs.value, undefined, empty.value.empty))
|
||||
|
||||
@@ -304,7 +304,7 @@ test('names the directive form a node CommonMark spells refuses', () => {
|
||||
assert.deepEqual(content(markdownToAdf('!adf:bulletList\n!adf:listItem\n---\n!adf:/listItem\n!adf:/bulletList\n')), [bulletList(item({ type: 'rule' }))])
|
||||
})
|
||||
|
||||
test('refuses the directive form of a blockquote opening with a code span the carry spells', () => {
|
||||
test('refuses the directive form of a blockquote opening with a code span', () => {
|
||||
assert.equal(content(markdownToAdf('!adf:blockquote\n` `` `\n!adf:/blockquote\n')), 'unsupported-node-shape: blockquote takes the CommonMark spelling, not the directive form')
|
||||
})
|
||||
|
||||
@@ -1005,6 +1005,7 @@ test('names the content slot no lone plain text node reads back from', () => {
|
||||
assert.equal(content(markdownToAdf('!adf:status[!adf:text{text="\\n"}]{color=yellow}\n')), spans)
|
||||
assert.equal(content(markdownToAdf('!adf:status[a b]{color=yellow}\n')), spans)
|
||||
assert.equal(content(markdownToAdf('!adf:status[a b]{color=yellow}\n')), spans)
|
||||
assert.equal(content(markdownToAdf('!adf:status[!adf:text{text="\\u0000"}]{color=yellow}\n')), 'unsupported-node-shape: the status content slot holds a null character: write the node as !adf:carry')
|
||||
assert.equal(content(markdownToAdf('Part !adf:mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: mention spells its text attribute in the content slot, never in {attrs}')
|
||||
})
|
||||
|
||||
|
||||
@@ -313,12 +313,13 @@ test('breaks a line at a newline and trims whitespace at every edge CommonMark s
|
||||
assert.equal(plain(paragraph(text(' x ', code))), '` x `\n')
|
||||
assert.equal(plain(node('heading', { level: 1 }, text(' h\ni '))), '# h i\n')
|
||||
assert.equal(plain(paragraph(text('a\r\u0000b'))), 'ab\n')
|
||||
assert.equal(plain(paragraph(text('x', link('/u', 't\r\u0000')))), '[x](/u "t")\n')
|
||||
assert.equal(plain(paragraph({ content: [text('dropped')], text: 'a', type: 'text' }, text('b'))), 'ab\n')
|
||||
})
|
||||
|
||||
test('drops the code mark of a span opening a line with backticks that read as a fence', () => {
|
||||
assert.equal(plain(paragraph(text('``` x', code))), '\\`\\`\\` x\n')
|
||||
assert.equal(plain(paragraph(text('a\n'), text('``` x', code))), 'a\\\n\\`\\`\\` x\n')
|
||||
assert.equal(plain(paragraph(text('a '), text('``` x', code))), 'a ```` ``` x ````\n')
|
||||
test('keeps a code span opening a line, whose backticks open no fence', () => {
|
||||
assert.equal(plain(paragraph(text('``` x', code))), '```` ``` x ````\n')
|
||||
assert.equal(plain(paragraph(text('a\n'), text('``` x', code))), 'a\\\n```` ``` x ````\n')
|
||||
})
|
||||
|
||||
test('drops an empty paragraph and merges adjacent lists of one type', () => {
|
||||
|
||||
@@ -77,9 +77,9 @@ test('normalizes blocks and mark attributes nesting far past the levels a recurs
|
||||
assert.deepEqual(merged.content?.[0]?.content?.map((text) => text.text), ['ab'])
|
||||
})
|
||||
|
||||
test('joins a text node holding content to no neighbour, so its content is kept', () => {
|
||||
const holding: AdfNode = { content: [{ text: 'kept', type: 'text' }], text: 'a', type: 'text' }
|
||||
for (const content of [[holding, { text: 'b', type: 'text' }], [{ text: 'b', type: 'text' }, holding]]) {
|
||||
assert.deepEqual(toEditorNormal({ content: [{ content, type: 'paragraph' }], type: 'doc', version: 1 }).content, [{ content, type: 'paragraph' }])
|
||||
}
|
||||
test('drops the content a text node holds, and joins the node as any other', () => {
|
||||
const holding: AdfNode = { content: [{ text: 'dropped', type: 'text' }], text: 'a', type: 'text' }
|
||||
const normal = (content: AdfNode[]): AdfNode[] | undefined => toEditorNormal({ content: [{ content, type: 'paragraph' }], type: 'doc', version: 1 }).content
|
||||
assert.deepEqual(normal([holding, { text: 'b', type: 'text' }]), [{ content: [{ text: 'ab', type: 'text' }], type: 'paragraph' }])
|
||||
assert.deepEqual(normal([{ text: 'b', type: 'text' }, holding]), [{ content: [{ text: 'ba', type: 'text' }], type: 'paragraph' }])
|
||||
})
|
||||
|
||||
@@ -7,9 +7,9 @@ type JsonContainer = JsonValue[] | { [key: string]: JsonValue }
|
||||
|
||||
type NodeHolder = { content?: AdfNode[] }
|
||||
|
||||
// The editor's rule is the reader's over the pair's editor-normal forms; a text node holding content never joins, so its content is kept.
|
||||
// The editor joins two text nodes where the reader would join their editor-normal forms.
|
||||
export function joinsWhenEditorNormal(previous: AdfNode, node: AdfNode): boolean {
|
||||
if (previous.type !== 'text' || node.type !== 'text' || nodeContent(previous).length > 0 || nodeContent(node).length > 0) return false
|
||||
if (previous.type !== 'text' || node.type !== 'text') return false
|
||||
return joinsWhenRead(normalNode(previous), normalNode(node))
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ export function toEditorNormal(document: AdfDocument): AdfDocument {
|
||||
if (content.length === 0) continue
|
||||
entry.holder.content = content.map((source) => {
|
||||
const holder = normalNode(source)
|
||||
pending.push({ holder, source })
|
||||
// Atlassian's schema forbids content on a text node, so the editor drops it.
|
||||
if (source.type !== 'text') pending.push({ holder, source })
|
||||
return holder
|
||||
})
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ function plainMarks(marks: readonly AdfMark[], container: LineContainer, text: s
|
||||
function plainLink(mark: AdfMark, container: LineContainer): AdfMark | undefined {
|
||||
const attrs = nodeAttrs(mark)
|
||||
const held = attrs['href']
|
||||
const title = typeof attrs['title'] === 'string' ? attrs['title'].replace(/\r/g, '').replace(/\n/g, ' ') : undefined
|
||||
const title = typeof attrs['title'] === 'string' ? attrs['title'].replace(/[\r\0]/g, '').replace(/\n/g, ' ') : undefined
|
||||
if (typeof held !== 'string') return undefined
|
||||
const href = writableHref(container === 'table-cell' ? held.replaceAll('|', '%7C') : held)
|
||||
if (title === undefined || spellLinkTarget(href, title) === undefined || (container === 'table-cell' && title.includes('|'))) return { attrs: { href }, type: 'link' }
|
||||
@@ -267,21 +267,13 @@ function spellableLine(leaves: AdfNode[], container: LineContainer, path: Conver
|
||||
|
||||
function withoutFallback(leaves: readonly AdfNode[], fallback: PlainLineFallback): AdfNode[] | undefined {
|
||||
if (fallback.kind === 'unspellable-run') return withoutMarks(leaves, fallback.runs)
|
||||
const first = fallback.kind === 'opening-link' ? 0 : lineStart(leaves, fallback.line)
|
||||
const mark = nodeMarks(leaves[first] ?? {})[0]
|
||||
if (mark === undefined || mark.type !== (fallback.kind === 'opening-link' ? 'link' : 'code')) return undefined
|
||||
let last = first
|
||||
const mark = nodeMarks(leaves[0] ?? {})[0]
|
||||
if (mark === undefined || mark.type !== 'link') return undefined
|
||||
let last = 0
|
||||
while (sameMarkAt(nodeMarks(leaves[last + 1] ?? {}), [mark], 0)) last += 1
|
||||
// A code span is what binds the `]` a link definition reads, and dropping it keeps the link target.
|
||||
const spans = leaves.slice(first, last + 1).some((leaf) => nodeMarks(leaf).length > 1 && nodeMarks(leaf).at(-1)?.type === 'code')
|
||||
if (mark.type === 'link' && spans) return leaves.map((leaf, index) => (index < first || index > last ? leaf : withMarks(leaf, nodeMarks(leaf).filter((held) => held.type !== 'code'))))
|
||||
return withoutMarks(leaves, [{ depth: 0, first, last }])
|
||||
}
|
||||
|
||||
function lineStart(leaves: readonly AdfNode[], line: number): number {
|
||||
let index = 0
|
||||
for (let breaks = 0; breaks < line && index < leaves.length; index += 1) if (leaves[index]?.type === 'hardBreak') breaks += 1
|
||||
return index
|
||||
const spans = leaves.slice(0, last + 1).some((leaf) => nodeMarks(leaf).length > 1 && nodeMarks(leaf).at(-1)?.type === 'code')
|
||||
return spans ? leaves.map((leaf, index) => (index > last ? leaf : withMarks(leaf, nodeMarks(leaf).filter((held) => held.type !== 'code')))) : undefined
|
||||
}
|
||||
|
||||
// The runs cover disjoint leaves, so one pass drops them all.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
`Bar = 9`
|
||||
|
||||
`Next ID = 67`
|
||||
`Next ID = 74`
|
||||
|
||||
| Goal | W |
|
||||
|---|---|
|
||||
@@ -24,43 +24,56 @@
|
||||
|
||||
| ID | Release | Exempt | Item | R | S | A | G | Goals | Score |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| 7 | 0.2.0 | | **Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF.** | 6 | 9 | 9 | 9 | 2, 3 | 25.8 |
|
||||
| 47 | 0.2.0 | | **Open the README with what the package is, what it does and for whom.** | 1 | 4 | 7 | 9 | 3 | 26.0 |
|
||||
| 7 | 0.2.0 | | **Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF.** | 6 | 9 | 9 | 9 | 2, 4 | 25.8 |
|
||||
| 45 | 0.2.0 | | **Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.** | 2 | 3 | 6 | 8 | 1 | 25.2 |
|
||||
| 6 | 0.2.0 | decision | **Specify the HTML dialect.** | 2 | 6 | 7 | 8 | 2, 3 | 24.7 |
|
||||
| 64 | 0.2.0 | defect | **Read back unchanged on V8 every JSON key the emitter writes, with a fixture whose key holds `\`, `"` or a control character.** | 4 | 4 | 6 | 8 | 1, 7 | 23.0 |
|
||||
| 67 | 0.2.0 | defect | **Push an expand title's leaves one at a time, so a title carrying many text nodes returns a result.** | 1 | 1 | 4 | 9 | 1 | 24.8 |
|
||||
| 6 | 0.2.0 | decision | **Specify the HTML dialect.** | 2 | 6 | 7 | 8 | 2, 4 | 24.7 |
|
||||
| 64 | 0.2.0 | defect | **Read back unchanged on V8 every JSON key the emitter writes, with a fixture whose key holds `\`, `"` or a control character.** | 4 | 4 | 6 | 8 | 1, 8 | 23.0 |
|
||||
| 66 | 0.2.0 | defect | **Refuse an attribute value that is not a plain object, array or JSON primitive.** | 3 | 2 | 5 | 8 | 1 | 22.5 |
|
||||
| 43 | 0.2.0 | decision | **Give each markdown input its own reader, strict to its own standard.** | 6 | 7 | 8 | 9 | 3, 4 | 22.3 |
|
||||
| 65 | 0.2.0 | defect | **Read an image not alone in its paragraph as its alt text linked to its URL, and a titled image alone in its paragraph as the image captioned with its title.** | 5 | 5 | 7 | 7 | 3, 4, 6 | 18.7 |
|
||||
| 49 | 0.2.0 | | **Read a list whose bullet or ordered delimiter changes as two lists, in both readers, and retire `!adf:listBreak`.** | 4 | 5 | 5 | 8 | 3, 4 | 17.2 |
|
||||
| 71 | 0.2.0 | | **Add a README table comparing the package with the other ADF converters, each cell checked against that package's own docs or behaviour.** | 1 | 3 | 6 | 7 | 3 | 21.2 |
|
||||
| 43 | 0.2.0 | decision | **Give each markdown input its own reader, strict to its own standard.** | 6 | 7 | 8 | 9 | 4, 5 | 20.3 |
|
||||
| 68 | 0.2.0 | | **Carry a non-text inline node holding `content` or `text`, a block holding `text`, and a leaf block holding `content`.** | 3 | 3 | 4 | 7 | 1 | 18.2 |
|
||||
| 65 | 0.2.0 | defect | **Read an image not alone in its paragraph as its alt text linked to its URL, and a titled image alone in its paragraph as the image captioned with its title.** | 5 | 5 | 7 | 7 | 4, 5, 7 | 17.1 |
|
||||
| 70 | 0.2.0 | | **Give `package.json` the `keywords` someone searching npm for an ADF converter types.** | 1 | 1 | 4 | 6 | 3 | 16.1 |
|
||||
| 72 | 0.2.0 | | **Make GitHub the canonical repository, with `gitea.larvit.se/larvit/adf-codec` a read-only pull mirror of it.** | 5 | 6 | 5 | 8 | 3 | 16.0 |
|
||||
| 49 | 0.2.0 | | **Read a list whose bullet or ordered delimiter changes as two lists, in both readers, and retire `!adf:listBreak`.** | 4 | 5 | 5 | 8 | 4, 5 | 15.5 |
|
||||
| 46 | 0.2.0 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 4 | 5 | 5 | 3, 8 | 14.8 |
|
||||
| 61 | 0.2.0 | decision | **Have the emitter ask the inline reader how a line reads back, in place of `line-escaping.ts` predicting it.** | 6 | 8 | 3 | 8 | 1 | 14.0 |
|
||||
| 52 | 0.2.0 | | **Spell `colwidth` as a comma list, `colwidth="340,420"`.** | 3 | 3 | 5 | 6 | 5 | 13.0 |
|
||||
| 51 | 0.2.0 | | **Match a reference label to its definition under Unicode case folding.** | 2 | 2 | 2 | 7 | 3, 4 | 12.4 |
|
||||
| 53 | 0.2.0 | | **Put a block's `marks` spelling to the writer panel and adopt its pick.** | 4 | 5 | 5 | 6 | 5 | 11.5 |
|
||||
| 52 | 0.2.0 | | **Spell `colwidth` as a comma list, `colwidth="340,420"`.** | 3 | 3 | 5 | 6 | 6 | 11.5 |
|
||||
| 51 | 0.2.0 | | **Match a reference label to its definition under Unicode case folding.** | 2 | 2 | 2 | 7 | 4, 5 | 10.9 |
|
||||
| 60 | 0.2.0 | decision | **Collect the questions `parse/` asks `emit/` into one named module.** | 3 | 4 | 2 | 6 | 2 | 10.7 |
|
||||
| 59 | 0.2.0 | decision | **Group the directive grammar into `src/markdown/directive/`, move `Read<T>` to `result.ts`, and move `Flavour` to `markdown/flavour.ts`.** | 3 | 5 | 2 | 6 | 2 | 10.4 |
|
||||
| 50 | 0.2.0 | | **Read `[](/url)` and `[]()` as CommonMark's empty link.** | 4 | 4 | 3 | 6 | 3, 4, 6 | 10.4 |
|
||||
| 53 | 0.2.0 | | **Put a block's `marks` spelling to the writer panel and adopt its pick.** | 4 | 5 | 5 | 6 | 6 | 10.0 |
|
||||
| 62 | 0.2.0 | decision | **Move the plain flavour's reading out of `parse/` and its writing out of `emit/` into `markdown/plain/`, so `emit/` no longer imports `plain/`.** | 4 | 5 | 2 | 6 | 2 | 9.4 |
|
||||
| 50 | 0.2.0 | | **Read `[](/url)` and `[]()` as CommonMark's empty link.** | 4 | 4 | 3 | 6 | 4, 5, 7 | 9.0 |
|
||||
| 73 | 0.3.0 | | **Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions.** | 1 | 2 | 6 | 8 | 3 | 23.0 |
|
||||
| 9 | 0.3.0 | | **Ship an online sandbox: a web page with two textboxes converting between ADF and markdown on the library's browser build.** | 2 | 6 | 6 | 8 | 3 | 21.0 |
|
||||
| 38 | 0.3.0 | | **Spell a lone surrogate in a text node so it survives a UTF-8 encode.** | 2 | 2 | 4 | 7 | 1 | 19.5 |
|
||||
| 47 | 0.3.0 | | **Open the README with what the package is, what it does and for whom.** | 1 | 4 | 7 | 9 | 9 | 14.0 |
|
||||
| 34 | 0.3.0 | | **Read emphasis flanking by the whole character beside an astral symbol.** | 2 | 3 | 3 | 6 | 3, 4 | 12.6 |
|
||||
| 42 | 0.3.0 | | **Trim a text leaf's trailing blanks in linear time.** | 1 | 2 | 5 | 9 | 8 | 12.5 |
|
||||
| 48 | 0.3.0 | | **Keep the release path publishing past npm's bypass-2FA token retirement.** | 4 | 4 | 8 | 3 | 9 | 11.7 |
|
||||
| 34 | 0.3.0 | | **Read emphasis flanking by the whole character beside an astral symbol.** | 2 | 3 | 3 | 6 | 4, 5 | 11.3 |
|
||||
| 31 | 0.3.0 | | **Make the branch-coverage figure repeat across runs of an unchanged tree.** | 2 | 3 | 3 | 4 | 1 | 11.2 |
|
||||
| 33 | 0.3.0 | | **Emit a line in time linear in its mark runs, in `adfToMarkdown` and `adfToPlainMarkdown`.** | 4 | 5 | 6 | 9 | 8 | 10.7 |
|
||||
| 46 | 0.3.0 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 4 | 5 | 5 | 7, 9 | 10.3 |
|
||||
| 9 | 0.3.0 | | **Ship an online sandbox: a web page with two textboxes converting between ADF and markdown on the library's browser build.** | 2 | 6 | 6 | 8 | 9 | 10.3 |
|
||||
| 42 | 0.3.0 | defect | **Trim a text leaf's trailing blanks in linear time.** | 1 | 2 | 5 | 9 | 9 | 10.5 |
|
||||
| 33 | 0.3.0 | defect | **Emit a line in time linear in its mark runs, in `adfToMarkdown` and `adfToPlainMarkdown`.** | 4 | 5 | 6 | 9 | 9 | 8.7 |
|
||||
| 56 | 0.3.0 | principle | **Give each piece of `blocks.ts`'s block-walk state and `inline-content.ts`'s `Scan` one owner that returns what it changes.** | 4 | 5 | 2 | 4 | 1 | 6.8 |
|
||||
| 57 | 0.3.0 | principle | **Make each `ci.sh` leg build what it reads, so one leg run alone tests the current tree.** | 2 | 3 | 1 | 3 | 7 | 1.2 |
|
||||
| 58 | 0.3.0 | principle | **Port `ci.sh`, `publish.sh` and `docker-runner.sh` to standalone Python scripts.** | 4 | 6 | 1 | 2 | 7 | -2.2 |
|
||||
| 8 | 0.4.0 | | **Ship a CLI.** | 3 | 7 | 7 | 6 | 9 | 10.6 |
|
||||
| 57 | 0.3.0 | principle | **Make each `ci.sh` leg build what it reads, so one leg run alone tests the current tree.** | 2 | 3 | 1 | 3 | 8 | 0.6 |
|
||||
| 58 | 0.3.0 | principle | **Port `ci.sh`, `publish.sh` and `docker-runner.sh` to standalone Python scripts.** | 4 | 6 | 1 | 2 | 8 | -2.6 |
|
||||
| 8 | 0.4.0 | | **Ship a CLI.** | 3 | 7 | 7 | 6 | 3 | 18.6 |
|
||||
|
||||
## Details
|
||||
|
||||
### 47. Open the README with what the package is, what it does and for whom.
|
||||
|
||||
Lands after item 72: the badges are npm's version and the GitHub Actions status. Today the README
|
||||
opens with the pre-launch rationale — Atlassian's REST APIs, `pf-editor-service/convert` being
|
||||
decommissioned, a link to JRACLOUD-77436. The background goes entirely, no endpoint, ticket or "why"
|
||||
note left. The README names the lossy pair, `adfToPlainMarkdown` and `plainMarkdownToAdf`, and the
|
||||
flavours it writes and reads — GitHub Flavored Markdown's alerts and task lists, Obsidian Flavored
|
||||
Markdown's callouts — so a search for any of these names finds the package.
|
||||
|
||||
### 7. Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF.
|
||||
|
||||
Lands after items 6, 59, 60, 61 and 62. The CommonMark spec suite also runs against
|
||||
`markdownToHtml`. The README documents HTML as it documents markdown, and its tagline and
|
||||
`package.json`'s `description` regain HTML.
|
||||
`markdownToHtml`. The README documents HTML as it documents markdown.
|
||||
|
||||
### 45. Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.
|
||||
|
||||
@@ -68,6 +81,13 @@ Goal 1 has every call return a result; the boolean guard is the one export that
|
||||
cannot say which branch refused, where `not-an-adf-document`'s message already does. Breaking:
|
||||
`MIGRATION.md` shows the guard's replacement.
|
||||
|
||||
### 67. Push an expand title's leaves one at a time, so a title carrying many text nodes returns a result.
|
||||
|
||||
`quoteNode`'s title loop (`parse/markdown-to-adf.ts`) spreads `inlineLeaves`'s result into
|
||||
`push`, against `docs/decisions.md` §Nothing spreads an unbounded array: an inline carry in a
|
||||
folded callout's title holding an unknown node with ~125k text children throws a `RangeError` from
|
||||
`plainMarkdownToAdf`. Found by the README-goals audit, 2026-10-04.
|
||||
|
||||
### 6. Specify the HTML dialect.
|
||||
|
||||
Element-by-element mapping, the `data-*` fidelity scheme, the opaque-carry form, and the documented
|
||||
@@ -90,6 +110,15 @@ typed array in `attrs`, as `structuredClone` keeps it, passes the guard and is w
|
||||
key per element: `ok: true` with content lost. Accept an object whose prototype is
|
||||
`Object.prototype` or `null`, and an array. Found by the stability review of item 63, 2026-10-04.
|
||||
|
||||
### 71. Add a README table comparing the package with the other ADF converters, each cell checked against that package's own docs or behaviour.
|
||||
|
||||
Lands after items 7, 46 and 47: the table names HTML, compares item 46's size figure, and sits in
|
||||
the README item 47 reshapes. Its columns: which directions each converts, whether the round-trip
|
||||
holds, what an unknown node does, the formats, runtime dependencies and size. Candidates are the
|
||||
packages an npm search for ADF and markdown returns, marklassian among them. Every cell cites that
|
||||
package's README or a run against its published version, and the table names each version and the
|
||||
date it was checked. Found while planning discoverability, 2026-10-04.
|
||||
|
||||
### 43. Give each markdown input its own reader, strict to its own standard.
|
||||
|
||||
Today `markdownToAdf` reads CommonMark and the lossless flavour as one input: text shaped like a
|
||||
@@ -98,6 +127,14 @@ a code fence whose info string opens `adf:` becomes the block carry where Common
|
||||
caller names the markdown it hands in: CommonMark, read as its spec says, or the lossless flavour,
|
||||
read as `spec/flavour.md` says. Breaking: `MIGRATION.md` says which call a caller takes.
|
||||
|
||||
### 68. Carry a non-text inline node holding `content` or `text`, a block holding `text`, and a leaf block holding `content`.
|
||||
|
||||
`refuseContentAndText` (`emit/inline-line.ts`) and `emitDirectiveBlock` (`emit/adf-to-markdown.ts`)
|
||||
refuse these with `unsupported-node-shape`, though `isAdfDocument` accepts them and the carry
|
||||
round-trips them; `docs/decisions.md` §The code list gives a cause the carry answers no code. Drop
|
||||
them from the emitting half of the README's `unsupported-node-shape` row. Found by the README-goals
|
||||
audit, 2026-10-04.
|
||||
|
||||
### 65. Read an image not alone in its paragraph as its alt text linked to its URL, and a titled image alone in its paragraph as the image captioned with its title.
|
||||
|
||||
Both are refused today with `unmappable-image`: 14 examples in
|
||||
@@ -105,16 +142,39 @@ Both are refused today with `unmappable-image`: 14 examples in
|
||||
takes only Atlassian media ids, never a URL. An image not alone in its paragraph reads as its alt
|
||||
text, linked to its URL. Its title becomes the link's `title`. An empty alt text reads as the URL,
|
||||
as item 50 reads an empty link. A titled image alone in its paragraph reads as the image, with its
|
||||
title as a `caption` (the maintainer, 2026-10-04). By Goal 4, the lossless flavour reads both as
|
||||
title as a `caption` (the maintainer, 2026-10-04). By Goal 5, the lossless flavour reads both as
|
||||
CommonMark does. An image inside a link: `[](/uri)` (spec example 517). Alone in
|
||||
its paragraph, it reads as the image with a `link` mark to `/uri`, which ADF's `media` takes. Not
|
||||
alone in its paragraph, the image reads as its alt text linked to `/uri`, and its URL drops. A
|
||||
writer panel chose this reading 3 of 3 (2026-10-04), by Goal 5, which outranks Goal 6.
|
||||
writer panel chose this reading 3 of 3 (2026-10-04), by Goal 6, which outranks Goal 7.
|
||||
|
||||
### 70. Give `package.json` the `keywords` someone searching npm for an ADF converter types.
|
||||
|
||||
`package.json` has none, and npm's search ranks on them. Take the terms the other ADF converters on
|
||||
npm list and the names the README uses — ADF, Atlassian Document Format, markdown, CommonMark, HTML
|
||||
— so a search for any of them can reach the package. Found while planning discoverability,
|
||||
2026-10-04.
|
||||
|
||||
### 72. Make GitHub the canonical repository, with `gitea.larvit.se/larvit/adf-codec` a read-only pull mirror of it.
|
||||
|
||||
The maintainer's call, 2026-10-04: GitHub is where an ADF converter is searched for, and the Gitea
|
||||
copy keeps the move back open. The workflows move to `.github/workflows/` with `ci.sh` unchanged.
|
||||
Publishing moves to npm's trusted publishing over OIDC, which GitHub-hosted Actions support, and
|
||||
which outlives the bypass-2FA token npm retires around January 2027: `publish.sh` drops `NPM_TOKEN`
|
||||
for the Actions OIDC variables, and AGENTS.md §7 reserves the trusted-publisher setup in place of
|
||||
the secret. `package.json`'s `repository` moves and it gains `bugs` and `homepage`. The links in
|
||||
README, CHANGELOG and MIGRATION follow, as do AGENTS.md's forge commands and merge style.
|
||||
`docs/decisions.md` gains the entry "GitHub is canonical, and Gitea a pull mirror of it" (Goal 3),
|
||||
and §Public on npm and §Publish on a version bump are revised: the source is public on GitHub, and
|
||||
CI publishes through npm's trusted publisher. Gitea's open issues and its releases move to GitHub
|
||||
first, since a mirror carries only git refs; the mirror disables issues and its description points
|
||||
at GitHub. Creating the GitHub repository, configuring npm's trusted publisher, and re-creating the
|
||||
Gitea copy as a mirror (Gitea cannot convert an existing repository) are the maintainer's.
|
||||
|
||||
### 49. Read a list whose bullet or ordered delimiter changes as two lists, in both readers, and retire `!adf:listBreak`.
|
||||
|
||||
Lands after item 43. Today `- a` then `+ b`, or `1.` then `1)`, reads as one list; CommonMark reads
|
||||
two (spec examples 301 and 302). Goal 4 settles it for the lossless flavour too: CommonMark spells
|
||||
two (spec examples 301 and 302). Goal 5 settles it for the lossless flavour too: CommonMark spells
|
||||
adjacent lists by changing the marker, so `adfToMarkdown` and `adfToPlainMarkdown` alternate `-` and
|
||||
`*` between adjacent bullet lists and `.` and `)` between adjacent ordered lists, and
|
||||
`!adf:listBreak` retires. `*` is the maintainer's pick (2026-10-04). Breaking, so it ships beside
|
||||
@@ -122,11 +182,23 @@ item 43: `MIGRATION.md`'s Readings and Spellings tables gain their rows. Example
|
||||
their `pending` exceptions, and the spelling leaves the README's "Four CommonMark spellings" bullet,
|
||||
which counts one fewer.
|
||||
|
||||
### 46. Publish the bundle size in the README, failing the release pipeline when it drifts.
|
||||
|
||||
Lands after item 7, which changes it. The quantity is what a consumer downloads and loads: the
|
||||
tarball `npm pack` produces, its unpacked `dist`, and the built JavaScript minified + gzipped — the
|
||||
figure the competitors advertise (marklassian's "12kb") and the only apples-to-apples one, since
|
||||
ours ships tsc's unminified output and no minifier yet (decide here whether to minify for the build
|
||||
or report the unminified gzip). The figure lands in README §The package beside the "no runtime
|
||||
dependencies" claim. Measured today, unminified: tarball 60.4 kB, unpacked 221.5 kB, JS gzipped 45.6
|
||||
kB.
|
||||
|
||||
### 61. Have the emitter ask the inline reader how a line reads back, in place of `line-escaping.ts` predicting it.
|
||||
|
||||
The comprehension panel's worst place: `escapeClaims` and `escapeClosedRuns` re-implement the
|
||||
reader's view — flanking, code-span closers, link-definition openings, highlight flanking — and
|
||||
only the property tests catch drift. It caps the panel's Locality score.
|
||||
only the property tests catch drift. It caps the panel's Locality score. The replacement runs in
|
||||
time linear in the line: today `mergesWithSyntax`, `touchesSyntax` and `closesHeading` rescan a run
|
||||
of one character from each of its characters.
|
||||
|
||||
### 52. Spell `colwidth` as a comma list, `colwidth="340,420"`.
|
||||
|
||||
@@ -140,12 +212,6 @@ example 540); lowercasing and then uppercasing folds it. Breaking, so it ships b
|
||||
`MIGRATION.md`'s Readings table gains its row. Its `pending` exceptions go, and its spelling leaves
|
||||
the README's "Four CommonMark spellings" bullet, which counts one fewer.
|
||||
|
||||
### 53. Put a block's `marks` spelling to the writer panel and adopt its pick.
|
||||
|
||||
Today `marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"`, the marks array as
|
||||
escaped JSON. Breaking where the panel picks another spelling: `MIGRATION.md`'s Spellings table
|
||||
gains its row.
|
||||
|
||||
### 60. Collect the questions `parse/` asks `emit/` into one named module.
|
||||
|
||||
`parse/` asks `emit/` through `commonMarkSpelling` and `openingLinkTakesDirective`, each imported
|
||||
@@ -158,14 +224,11 @@ Eight directive files sit across three directories, and the `markdown/` root hol
|
||||
needs `Read<T>` and `Flavour` out of the plain flavour and `markdown/`; it needs the carry and the
|
||||
mark spellings too, which item 7 moves where it learns what HTML shares.
|
||||
|
||||
### 50. Read `[](/url)` and `[]()` as CommonMark's empty link.
|
||||
### 53. Put a block's `marks` spelling to the writer panel and adopt its pick.
|
||||
|
||||
Both stay literal text today (spec examples 484 and 487). ADF holds no empty text node to carry a
|
||||
link mark, so a link whose text is empty takes its URL as its text (the maintainer, 2026-10-04):
|
||||
`[](/url)` reads as `/url` linked to `/url`. `[]()` has no URL to show; the chunk settles it by Goals
|
||||
3 and 6. Breaking, so it ships beside item 43: `MIGRATION.md`'s Readings table gains its row. Its
|
||||
`pending` exceptions go, and its spelling leaves the README's "Four CommonMark spellings" bullet,
|
||||
which counts one fewer.
|
||||
Today `marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"`, the marks array as
|
||||
escaped JSON. Breaking where the panel picks another spelling: `MIGRATION.md`'s Spellings table
|
||||
gains its row.
|
||||
|
||||
### 62. Move the plain flavour's reading out of `parse/` and its writing out of `emit/` into `markdown/plain/`, so `emit/` no longer imports `plain/`.
|
||||
|
||||
@@ -175,46 +238,32 @@ the `inlineLeaves` use. Writing: `spellPlainBlock`, `quotedUnder`, `tryTaskList`
|
||||
`plain/conventions.ts`, which `emit/` imports them from; item 59 moves `Flavour`. Every
|
||||
comprehension reader on 2026-10-03 named the plain flavour's spread across three directories.
|
||||
|
||||
### 50. Read `[](/url)` and `[]()` as CommonMark's empty link.
|
||||
|
||||
Both stay literal text today (spec examples 484 and 487). ADF holds no empty text node to carry a
|
||||
link mark, so a link whose text is empty takes its URL as its text (the maintainer, 2026-10-04):
|
||||
`[](/url)` reads as `/url` linked to `/url`. `[]()` has no URL to show; the chunk settles it by Goals
|
||||
4 and 7. Breaking, so it ships beside item 43: `MIGRATION.md`'s Readings table gains its row. Its
|
||||
`pending` exceptions go, and its spelling leaves the README's "Four CommonMark spellings" bullet,
|
||||
which counts one fewer.
|
||||
|
||||
### 73. Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions.
|
||||
|
||||
Lands after `0.2.0` is published with items 47, 71 and 72, so each post links the GitHub repository,
|
||||
the comparison and a README that opens with what the package is. We draft each post for the place it
|
||||
goes; the maintainer posts them. Found while planning discoverability, 2026-10-04.
|
||||
|
||||
### 38. Spell a lone surrogate in a text node so it survives a UTF-8 encode.
|
||||
|
||||
`adfToMarkdown` emits it verbatim, so markdown stored as UTF-8 reads back U+FFFD; attribute values
|
||||
already escape it.
|
||||
|
||||
### 47. Open the README with what the package is, what it does and for whom.
|
||||
|
||||
It opens with the pre-launch rationale — Atlassian's REST APIs, `pf-editor-service/convert` being
|
||||
decommissioned, a link to JRACLOUD-77436. The background goes entirely, no endpoint, ticket or "why"
|
||||
note left. The badges are npm's version and the Gitea Actions status. The README names the lossy
|
||||
pair, `adfToPlainMarkdown` and `plainMarkdownToAdf`, and the flavours it writes and reads — GitHub
|
||||
Flavored Markdown's alerts and task lists, Obsidian Flavored Markdown's callouts — so a search for
|
||||
any of these names finds the package.
|
||||
|
||||
### 34. Read emphasis flanking by the whole character beside an astral symbol.
|
||||
|
||||
Check whether `line-escaping.ts`'s `charAt` and the parser's flanking read one UTF-16 unit beside an
|
||||
astral symbol — a lone surrogate is neither punctuation nor symbol, where CommonMark reads `😀` as
|
||||
punctuation — and, where they do, read the code point, with a fixture per direction.
|
||||
|
||||
### 42. Trim a text leaf's trailing blanks in linear time.
|
||||
|
||||
`plain/inline-reduction.ts`'s `leafEdges` finds the trail with an unanchored `/[ \t]*$/`, quadratic
|
||||
in a run of blanks inside one leaf: a paragraph of `a`, 80 000 spaces, `b` takes 6.5 s in
|
||||
`adfToPlainMarkdown`. Scan backward, as the expand title's trim does.
|
||||
|
||||
### 48. Keep the release path publishing past npm's bypass-2FA token retirement.
|
||||
|
||||
Lands after 2027-01-01, or after a release run fails on the token, whichever comes first: the
|
||||
maintainer chose on 2026-10-02 to wait and see whether the retirement bites. It holds back no
|
||||
release: when the rest of its release is done, it moves to the next. `0.1.0` published only once the
|
||||
npm token carried **Bypass 2FA**: the account requiring no 2FA on writes was not enough, and npm
|
||||
answered `EOTP` until the token itself bypassed. npm retires bypass-2FA tokens for direct publishing
|
||||
around January 2027, leaving them `npm stage publish`, which a maintainer approves with 2FA; its
|
||||
replacement — trusted publishing over OIDC — supports GitHub-hosted Actions, GitLab.com's shared
|
||||
runners and CircleCI's cloud, self-hosted runners planned without a date. Revisit: whether npm has
|
||||
added Gitea or self-hosted OIDC, and otherwise whether the release moves to the staged publish —
|
||||
which fits badly with publish-on-merge, and is the maintainer's trade to weigh. The Goals and G
|
||||
cells are provisional: no README goal covers the release path.
|
||||
|
||||
### 31. Make the branch-coverage figure repeat across runs of an unchanged tree.
|
||||
|
||||
Three Node test legs over one unchanged tree reported `emit/inline-line.ts` at 95.83%, 96.23% and
|
||||
@@ -226,21 +275,19 @@ moves upward, so the first raise to the measured figure reddens a run that chang
|
||||
measurement repeatable, or state the number the floor may be raised to and why it is not the
|
||||
measured one.
|
||||
|
||||
### 42. Trim a text leaf's trailing blanks in linear time.
|
||||
|
||||
`plain/inline-reduction.ts`'s `leafEdges` finds the trail with an unanchored `/[ \t]*$/`, quadratic
|
||||
in a run of blanks inside one leaf: a paragraph of `a`, 80 000 spaces, `b` takes 6.5 s in
|
||||
`adfToPlainMarkdown`. Scan backward, as the expand title's trim does.
|
||||
|
||||
### 33. Emit a line in time linear in its mark runs, in `adfToMarkdown` and `adfToPlainMarkdown`.
|
||||
|
||||
`adfToMarkdown` spends 23 s on one paragraph of 2000 × `un` plus `**-r**`: each run its flanking
|
||||
cannot spell re-emits the whole line before riding the carry, quadratic in the runs, and the plain
|
||||
reduction's `spellableLine` drops one mark per re-emit the same way. Make both linear.
|
||||
|
||||
### 46. Publish the bundle size in the README, failing the release pipeline when it drifts.
|
||||
|
||||
Lands after item 7, which changes it. The quantity is what a consumer downloads and loads: the
|
||||
tarball `npm pack` produces, its unpacked `dist`, and the built JavaScript minified + gzipped — the
|
||||
figure the competitors advertise (marklassian's "12kb") and the only apples-to-apples one, since
|
||||
ours ships tsc's unminified output and no minifier yet (decide here whether to minify for the build
|
||||
or report the unminified gzip). The figure lands in README §The package beside the "no runtime
|
||||
dependencies" claim. Measured today, unminified: tarball 60.4 kB, unpacked 221.5 kB, JS gzipped 45.6
|
||||
kB.
|
||||
reduction's `spellableLine` drops one mark per re-emit the same way. An inline node whose
|
||||
attributes no spelling writes re-emits the line the same way before riding the carry. Make all
|
||||
three linear.
|
||||
|
||||
### 56. Give each piece of `blocks.ts`'s block-walk state and `inline-content.ts`'s `Scan` one owner that returns what it changes.
|
||||
|
||||
@@ -248,7 +295,11 @@ Technical principle "One owner per value": the `Walk` record passes through twel
|
||||
mutate it and return `void`; `walk.leaf` alone is written in seven places. `ContainerStack` in the
|
||||
same file shows the shape to follow. `inline-content.ts`'s `Scan` has the same shape: about fifteen
|
||||
functions write `pending`, `pieces`, `deactivatedBefore` and `openingSpellableLink` and return
|
||||
`void`, and `parseInlineContent` reads a flag `scanInline` leaves on it.
|
||||
`void`, and `parseInlineContent` reads a flag `scanInline` leaves on it. The same shape recurs in
|
||||
`mintTaskIds` (`plain/task-ids.ts`), which writes `attrs` on the document `readDocument` built;
|
||||
`takeFallback` (`emit/inline-line.ts`), which writes the record the next `lineSegments` pass reads;
|
||||
`writeUnpaired`, `markPairings` and `markHighlights` (`inline-content.ts`), whose order changes the
|
||||
output; and `nestIn` (`plain/adf-to-plain-markdown.ts`), which pushes into its caller's array.
|
||||
|
||||
### 57. Make each `ci.sh` leg build what it reads, so one leg run alone tests the current tree.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user