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

Answer architecture round 1: the claims type pins what every writer spells, the HTML calls take flavour names, model-written GFM reads in portableMarkdownToAdf

This commit is contained in:
2026-10-09 02:42:55 +02:00
committed by Lillem4n
parent 9805275bf7
commit 4d5f83459f
8 changed files with 52 additions and 19 deletions
+2 -1
View File
@@ -62,7 +62,8 @@
once the process had read a key holding a backslash
([V8 bug](https://issues.chromium.org/issues/521080746)).
- Fix a cyclic document, in which an object holds itself: `adfToLosslessMarkdown` and
`adfToPortableMarkdown` refuse it as `not-an-adf-document` and `isAdfDocument` returns `false`, where all three hung.
`adfToPortableMarkdown` refuse it as `not-an-adf-document` and `isAdfDocument` returns `false`,
where all three hung.
## 0.1.0
+4 -4
View File
@@ -8,7 +8,7 @@ Each call names the markdown it reads or writes:
| --- | --- |
| `adfToMarkdown` | `adfToLosslessMarkdown` |
| `markdownToAdf`, for markdown `adfToMarkdown` wrote | `losslessMarkdownToAdf` |
| `markdownToAdf`, for markdown a person or a model wrote | `commonMarkToAdf`, which reads a directive, a pipe table, `~~` and an `adf:` fence as CommonMark does, as text and code |
| `markdownToAdf`, for markdown a person or a model wrote | `portableMarkdownToAdf`, which keeps GFM's pipe tables and `~~`; or `commonMarkToAdf`, which reads them, a directive and an `adf:` fence as CommonMark does, as text and code |
Directives moved under the `!adf:` prefix. `losslessMarkdownToAdf` reads `0.1.0`'s spelling without
an error, turning each directive into text and each carried node into an `adf` code block. Before
@@ -72,9 +72,9 @@ Markdown the spelling table leaves alone, which `0.2.0` reads as a different doc
`unspellable-character`, `unspellable-line-start` and `unspellable-link` leave `ConvertErrorCode`
and `not-a-string` joins it: a `switch` naming one of the three stops compiling, and one naming
every code needs the new case. A document `adfToMarkdown` refused with one of the three
`adfToLosslessMarkdown` now converts; markdown the parser refused with one of the three now refuses with the code the table
gives.
every code needs the new case. A document `adfToMarkdown` refused with one of the three now
converts in `adfToLosslessMarkdown`; markdown the parser refused with one of the three now refuses
with the code the table gives.
| Input | `0.1.0` | `0.2.0` |
| --- | --- | --- |
+7 -7
View File
@@ -11,7 +11,8 @@ products store as ADF.
- **Lossless:** `adfToLosslessMarkdown` and `losslessMarkdownToAdf` give back the document they
started from — panels, mentions, statuses and node types this version does not know included.
- **CommonMark in:** `commonMarkToAdf` reads a bot's or a model's markdown as the CommonMark spec
says, apart from raw HTML and the exceptions [The guarantees](#the-guarantees) lists.
says, apart from raw HTML and the exceptions [The guarantees](#the-guarantees) lists; markdown
holding GFM's pipe tables or `~~` reads in `portableMarkdownToAdf`.
- **Portable markdown, lossy:** `adfToPortableMarkdown` and `portableMarkdownToAdf` convert to and
from the markdown that GitHub, GitLab and Obsidian render — GitHub Flavored Markdown's alerts,
task lists and pipe tables, Obsidian Flavored Markdown's callouts and highlights.
@@ -19,7 +20,8 @@ products store as ADF.
save edits back, use the lossless pair.
- **No throws:** every call returns a result; a refusal carries a code from a closed list.
- **Runs anywhere:** pure functions with no runtime dependencies, in Node, Deno, Bun and browsers.
- **HTML at `0.3.0`:** `adfToHtml`, `htmlToAdf`, `markdownToHtml`, `htmlToMarkdown`.
- **HTML at `0.3.0`:** `adfToHtml`, `htmlToAdf`, and markdown to and from HTML through ADF, each
call named by its markdown flavour.
[The shape](#the-shape) · [Goals](#goals) · [Audience](#audience) ·
[Portable markdown](#portable-markdown) · [The errors](#the-errors) ·
@@ -49,8 +51,8 @@ if (result.ok) {
```
Serves [Goals](#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 `losslessMarkdownToAdf` or `htmlToAdf` refuses.
whole result; no I/O, no configuration. The `0.3.0` calls between markdown and HTML convert through
ADF: they keep only what ADF holds, and refuse what their markdown reader or `htmlToAdf` refuses.
```ts
adfToLosslessMarkdown(doc: AdfDocument): Result<string>
@@ -64,8 +66,6 @@ portableMarkdownToAdf(markdown: string): Result<AdfDocument, ParseError>
adfToHtml(doc: AdfDocument): Result<string> // 0.3.0
htmlToAdf(html: string): Result<AdfDocument, ParseError> // 0.3.0
markdownToHtml(markdown: string): Result<string> // 0.3.0
htmlToMarkdown(html: string): Result<string> // 0.3.0
```
`Result<T>` is `{ ok: true; value: T } | { ok: false; error: ConvertError }` — nothing throws.
@@ -170,7 +170,7 @@ stable across minors and safe to `switch` on exhaustively with no `default`; `me
and may change in any release. A parse always names a position, so the three markdown readers
return `ParseError`, whose `position` reads without a guard; an emit reads no
source and carries `path` alone; one handler typed on `ConvertError` takes both, which is what the
composed `markdownToHtml` and `htmlToMarkdown` hand back. `path` is the node's place from the
calls between markdown and HTML hand back. `path` is the node's place from the
document root, alternating `'content'` and an index, so `path.map((step) => '/' + step).join('')`
is a JSON Pointer at the node — the empty path being the document itself.
+26
View File
@@ -0,0 +1,26 @@
# Comprehension history
## 2026-10-09T00:41:03Z, PR #19 at 859c952, against 9519606
Ruling: better
| Seat | Vote |
|---|---|
| Mid A | better |
| Inherited architect | better |
Mid A, decided by:
- `src/markdown/portable/task-ids.ts:7` `mintTaskIds`: one walk, no carried set filled by the parser as a side effect
- `src/markdown/parse/markdown-to-adf.ts:26` `Reading`: plain data, `codeBlockNode` no longer mutates `carried`
- `src/markdown/portable/conventions.ts:8` `flavourClaims`: one table states what each reader claims; branches name the claim they test
- `src/markdown/parse/markdown-to-adf.ts:161` `quoteNode`: the title is read in the reader's own flavour, no hop through `inlineLeaves`
- `src/index.ts:5` exports: each call named by its flavour; "plain" no longer means two things
Inherited architect, decided by:
- `src/markdown/parse/markdown-to-adf.ts:26` `Reading`: the hidden `carried` set and the retrying `mintTaskIds` are gone
- `src/markdown/parse/markdown-to-adf.ts:161` `quoteNode`: parser and emitter no longer correct only together
- `src/markdown/parse/markdown-to-adf.ts:113` `readBlock`: branches name the claim (`claims.alerts`, `claims.taskMarkers`)
- `src/index.ts:5` exports: flavour names say which file to open
- `src/markdown/portable/conventions.ts:3` `flavourClaims`: cost, all three flavours' rows live under `portable/`
+2 -1
View File
@@ -13,7 +13,8 @@ A function's name says which markdown it reads or writes, and a reader reads tha
| `commonMarkToAdf` | CommonMark as its spec says: a directive, a pipe table, `~~`, `==` and an `adf:` fence read as CommonMark reads them |
| `adfToPortableMarkdown`, `portableMarkdownToAdf` | CommonMark, GFM's pipe tables, `~~`, alerts and task markers, and Obsidian's callouts and `==`; no directive and no `adf:` fence |
No `adfToCommonMark`: the portable writer serves every persona a CommonMark writer would. The panel
The calls between markdown and HTML that `todo.md` item 7 ships take their flavour's name too. No
`adfToCommonMark`: the portable writer serves every persona a CommonMark writer would. The panel
picked `portable` 4 of 7 over `plain` 3, `gfm` and `rendered` none: the export readers read it as
"renders anywhere, lossy", and three readers read `plain` as holding no tables or alerts.
@@ -444,7 +444,6 @@ test('generated markdown refuses, or its spelling reads back and spells itself',
assert.ok(directiveShaped >= directiveShapedFloor, `${directiveShaped} runs reaching the fixpoint held a node or mark outside CommonMark's own types, under the floor of ${directiveShapedFloor}`)
})
test('generated markdown read as CommonMark refuses, or its document reads back through the lossless pair', { timeout: propertyTimeout }, () => {
fc.assert(
fc.property(document, (input) => {
+5 -1
View File
@@ -15,7 +15,11 @@ export type Claims = {
taskMarkers: boolean
}
export const flavourClaims: Readonly<Record<Flavour, Claims>> = {
// The writer spells pipe tables and `~~` in every flavour it writes.
type WrittenClaims = Claims & { pipeTables: true; strikethrough: true }
// No flavour claims directives beside alerts or highlights: an expand directive leaves `inExpand` unset and a directive's slot reads `==`.
export const flavourClaims: Readonly<{ commonmark: Claims } & Record<WrittenFlavour, WrittenClaims>> = {
commonmark: { alerts: false, carryFence: false, directives: false, highlights: false, pipeTables: false, strikethrough: false, taskMarkers: false },
lossless: { alerts: false, carryFence: true, directives: true, highlights: false, pipeTables: true, strikethrough: true, taskMarkers: false },
portable: { alerts: true, carryFence: false, directives: false, highlights: true, pipeTables: true, strikethrough: true, taskMarkers: true },
+6 -4
View File
@@ -32,7 +32,7 @@
| 59 | 0.2.0 | decision | **Group the directive grammar into `src/markdown/directive/`, move `Read<T>` to `result.ts`, and move `Flavour` and `flavourClaims` to `markdown/flavour.ts`.** | 3 | 5 | 2 | 6 | 2 | 10.4 |
| 62 | 0.2.0 | decision | **Move the portable flavour's reading out of `parse/` and its writing out of `emit/` into `markdown/portable/`, so `emit/` no longer imports `portable/`.** | 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 |
| 7 | 0.3.0 | | **Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF.** | 6 | 9 | 9 | 9 | 2, 4 | 25.8 |
| 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 |
| 77 | 0.3.0 | decision | **Read the input once into a plain copy, so an accessor property or a throwing Proxy yields `not-an-adf-document` and every call returns a result.** | 4 | 4 | 5 | 9 | 1 | 23.0 |
@@ -106,6 +106,8 @@ parts by ADF and format true as they grow.
Eight directive files sit across three directories, and the `markdown/` root holds 14 entries. HTML
needs `Read<T>` and `Flavour` out of the portable flavour and `markdown/`; it needs the carry and
the mark spellings too, which item 7 moves where it learns what HTML shares.
Once moved, a reader takes a `Flavour` and looks up its claims, as a writer does, so no reader is
handed a set of claims no flavour holds.
### 62. Move the portable flavour's reading out of `parse/` and its writing out of `emit/` into `markdown/portable/`, so `emit/` no longer imports `portable/`.
@@ -124,10 +126,10 @@ link mark, so a link whose text is empty takes its URL as its text (the maintain
Goals 4 and 7. Breaking: `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.
### 7. Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF.
### 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, 61 and 62. The CommonMark spec suite also runs against
`markdownToHtml`. The README documents HTML as it documents markdown. `htmlToAdf` refuses a value
Lands after items 6, 59, 60, 61 and 62. Which flavours get a composed call is this item's to
settle; the CommonMark spec suite also runs against the one reading CommonMark. The README documents HTML as it documents markdown. `htmlToAdf` refuses a value
that is not a string as `not-a-string` (`docs/decisions.md` §Which code a cause takes).
### 45. Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.