3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-12 11:48:29 +01:00

Answer architecture round 1: closeLeaf records the block definitions end beside the blocks it pushes, openedList's comment names the exception, the README drops its counts, the changelog puts other changes last, and item 98 files the blank-line re-scan

This commit is contained in:
2026-10-10 11:11:06 +02:00
parent caf64604fc
commit 6182ba6b63
4 changed files with 35 additions and 25 deletions
+6 -6
View File
@@ -6,8 +6,6 @@
CommonMark reads it: `- a` then `* b`, `1. a` then `1) b`. `adfToLosslessMarkdown` gives a list
right after one of its kind the other marker, `* ` or `1)`, in place of the `::listBreak`
separator. Convert stored markdown per `MIGRATION.md`.
- **Breaking:** two lists a link reference definition parts read as two lists, as CommonMark
reads them, where they read as one; see `MIGRATION.md`.
- **Breaking:** each call names the markdown it reads or writes. `adfToMarkdown` and `markdownToAdf`
are now `adfToLosslessMarkdown` and `losslessMarkdownToAdf`. A bot's or a model's markdown reads
in `portableMarkdownToAdf`, and markdown known to be strict CommonMark in the new
@@ -58,6 +56,8 @@
`losslessMarkdownToAdf` refuses a directive mark wrapping only hard breaks as
`unsupported-node-shape`. `adfToLosslessMarkdown` writes a hard break holding marks as
`!adf:carry{json="…"}`. See `MIGRATION.md`.
- **Breaking:** two lists a link reference definition parts read as two lists, as CommonMark
reads them, where `0.1.0` read one; see `MIGRATION.md`.
- `adfToLosslessMarkdown` and `adfToPortableMarkdown` write text holding a long run of `*`, `_`, `~`
or backticks, or a heading ending in a long run of `#`, in time linear in the run: a run of 20000
`*` took seconds before, and now takes milliseconds.
@@ -79,16 +79,16 @@
- Fix a cyclic document, in which an object holds itself: `adfToLosslessMarkdown` and
`adfToPortableMarkdown` refuse it as `not-an-adf-document` and `isAdfDocument` returns `false`,
where all three hung.
- The README opens with what the package does and for whom, and `package.json` carries search
keywords.
- The source moves to [github.com/larvit/adf-codec](https://github.com/larvit/adf-codec), where
issues and pull requests go.
- Add `adfToPortableMarkdown` and `portableMarkdownToAdf`, a lossy pair converting ADF to and from
markdown GitHub, GitLab and Obsidian render: alerts, callouts, task lists, `==highlights==` and
pipe tables. A task item keeps its paragraphs, then its nested task lists; the rest of the item,
such as an image or a code block, stands after the task list.
- Spell `rule`'s `color`, `style` and `weight`, `layoutSection`'s `columnRuleStyle` and a link's
`collection`, `id` and `occurrenceKey` directly where they rode the opaque carry.
- The README opens with what the package does and for whom, and `package.json` carries search
keywords.
- The source moves to [github.com/larvit/adf-codec](https://github.com/larvit/adf-codec), where
issues and pull requests go.
## 0.1.0
+8 -7
View File
@@ -239,8 +239,8 @@ Serves Goals 1, 4 and 5.
without a delimiter row, is an error result; each can be kept literal (`spec/flavour.md`).
- A document any reader built, written back with `adfToLosslessMarkdown`, takes the library's
canonical spelling, which round-trips byte-identically.
- Six CommonMark spellings parse without an error and build a document the reference
implementation renders differently. Four because ADF holds no node for what it renders:
- These CommonMark spellings parse without an error and build a document the reference
implementation renders differently, where ADF holds no node for what it renders:
- An empty link text reads as its destination, `[](/url)` as `/url` linked to `/url`, and `[]()`
as nothing. A link text holding only hard breaks reads the same, followed by its hard breaks.
- An image reads as an image only alone in its paragraph. Anywhere else — beside other text, in
@@ -253,11 +253,12 @@ Serves Goals 1, 4 and 5.
- An image's title becomes its caption when the image stays an image, and its link's title when
it becomes linked alt text. Inside another link, the title is dropped.
And two more: a shortcut reference matching its definition only under Unicode case folding
stays unresolved, and a link whose text holds an autolink keeps the inner link and leaves the
outer brackets literal text, which the spec requires and the reference itself breaks, nesting one
`<a>` in the other. `corpus/commonmark-spec/exceptions.json` pins the first four `node-model`
where the suite reaches them and the fifth `pending`; the suite holds no example of the last.
And these: a shortcut reference matching its definition only under Unicode case folding stays
unresolved, and a link whose text holds an autolink keeps the inner link and leaves the outer
brackets literal text, which the spec requires and the reference itself breaks, nesting one `<a>`
in the other. `corpus/commonmark-spec/exceptions.json` pins the spellings ADF holds no node for
`node-model` where the suite reaches them, and the case-folded reference `pending`; the suite
holds no example of a link holding an autolink.
- Not every document converts back: `adfToLosslessMarkdown` 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
+8 -10
View File
@@ -75,8 +75,8 @@ type ContainerStack = {
type Walk = ParsedBlocks & {
claims: Claims
// The block a paragraph of link reference definitions alone last closed after, which ends a list as any block does.
definedAfter: Block | undefined
// The last block before a paragraph of link reference definitions alone, which ends a list as any block does.
endedByDefinitions: Block | undefined
leaf: OpenLeaf | undefined
leafOpeners: Map<Block[], Map<string, LeafOpener>>
position: SourcePosition
@@ -91,8 +91,8 @@ export function parseBlocks(markdown: string, claims: Claims): ParsedBlocks {
const walk: Walk = {
blocks: [],
claims,
definedAfter: undefined,
definitions: new Map(),
endedByDefinitions: undefined,
leaf: undefined,
leafOpeners: new Map(),
position: { line: 1, offset: 0 },
@@ -249,12 +249,12 @@ function openContainer(walk: Walk, start: ContainerStart): void {
walk.stack.push({ blocks, indentation: start.indentation, kind: 'item', list })
}
// An item opening after its list's last item closed, as an empty item a blank line ends does, continues that list.
// An item opening after its list's last item closed, as an empty item a blank line ends does, continues that list, unless definitions ended it.
function openedList(walk: Walk, start: Extract<ContainerStart, { kind: 'item' }>): ListBlock {
if (!start.fresh) return start.list
const blocks = currentBlocks(walk)
const previous = blocks.at(-1)
if ((previous?.kind === 'bulletList' || previous?.kind === 'orderedList') && previous !== walk.definedAfter && continuesList(previous, start.list)) return previous
if ((previous?.kind === 'bulletList' || previous?.kind === 'orderedList') && previous !== walk.endedByDefinitions && continuesList(previous, start.list)) return previous
blocks.push(start.list)
return start.list
}
@@ -466,7 +466,8 @@ function closeLeaf(walk: Walk): void {
if (leaf === undefined) return
if (leaf.kind === 'paragraph') {
const paragraph = takeParagraph(walk)
if (paragraph !== undefined) currentBlocks(walk).push(bareTableFault(paragraph, walk.claims) ?? paragraph)
if (paragraph === undefined) walk.endedByDefinitions = currentBlocks(walk).at(-1)
else currentBlocks(walk).push(bareTableFault(paragraph, walk.claims) ?? paragraph)
return
}
walk.leaf = undefined
@@ -517,10 +518,7 @@ function takeParagraph(walk: Walk): Extract<Block, { kind: 'paragraph' }> | unde
if (leaf?.kind !== 'paragraph') return undefined
walk.leaf = undefined
const text = readLinkDefinitions(walk.definitions, leaf.lines.join('\n'))
if (text === '') {
walk.definedAfter = currentBlocks(walk).at(-1)
return undefined
}
if (text === '') return undefined
const kept = leaf.positions[leaf.lines.length - text.split('\n').length]
return { kind: 'paragraph', position: kept ?? leaf.position, text }
}
+13 -2
View File
@@ -6,7 +6,7 @@
`Bar = 9`
`Next ID = 98`
`Next ID = 99`
| Goal | W |
|---|---|
@@ -35,6 +35,7 @@
| 96 | 0.2.1 | defect | **Read and write a text holding many unclosed backtick runs, or many `<!--`, `<?`, `<![CDATA[` or `<!` openers without a terminator, in time linear in the text.** | 2 | 3 | 5 | 9 | 9 | 9.2 |
| 92 | 0.2.1 | | **Lower the reading cost of the files a chunk reads most before its first commit.** | 2 | 4 | 2 | 4 | 1 | 9.0 |
| 33 | 0.2.1 | defect | **Emit a line in time linear in its mark runs, in `adfToLosslessMarkdown` and `adfToPortableMarkdown`.** | 4 | 5 | 6 | 9 | 9 | 8.7 |
| 98 | 0.2.1 | defect | **Read blank lines inside deeply nested list items in time linear in the markdown.** | 3 | 3 | 5 | 9 | 9 | 8.2 |
| 7 | 0.3.0 | decision | **Ship HTML: `adfToHtml`, `htmlToAdf`, and markdown to and from HTML composed through ADF, each call named by its flavour.** | 6 | 9 | 9 | 9 | 2, 4 | 25.8 |
| 45 | 0.3.0 | | **Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.** | 2 | 3 | 6 | 8 | 1 | 25.2 |
| 6 | 0.3.0 | decision | **Specify the HTML dialect.** | 2 | 6 | 7 | 8 | 2, 4 | 24.7 |
@@ -143,6 +144,15 @@ the portable reduction's `spellableLine` drops one mark per re-emit the same way
whose attributes no spelling writes re-emits the line the same way before riding the carry. Make all
three linear.
### 98. Read blank lines inside deeply nested list items in time linear in the markdown.
`matchContainers` in `src/markdown/parse/blocks.ts` continues every open item holding a block on
each blank line, and `parseBlocks` holds no depth guard: `readBlocks` refuses past 500 levels only
after the walk. `'- '.repeat(n) + 'a\n' + '\n'.repeat(n)` took 675 ms at n = 5000, 2.7 s at 10000
and 10.7 s at 20000 (60 kB) before refusing as `unsupported-nesting-depth`. `docs/decisions.md`
§Cost fixes are measured, never timed calls the re-scan bounded by that guard; correct it with the
fix.
### 7. Ship HTML: `adfToHtml`, `htmlToAdf`, and markdown to and from HTML composed through ADF, each call named by its flavour.
Lands after items 6, 59, 60 and 62. Which flavours get a composed call is this item's to settle;
@@ -344,7 +354,8 @@ quadratic in a run of blanks inside one leaf: a paragraph of `a`, 80 000 spaces,
### 56. Give each piece of `blocks.ts`'s block-walk state and `inline-content.ts`'s `Scan` one owner that returns what it changes.
Technical principle "One owner per value": the `Walk` record passes through twelve functions that
mutate it and return `void`; `walk.leaf` alone is written in seven places. `ContainerStack` in the
mutate it and return `void`; `walk.leaf` alone is written in seven places, and `openedList` reads
the `endedByDefinitions` that `closeLeaf` leaves. `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. The same shape recurs in