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:
+2
-1
@@ -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
@@ -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` |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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>`.
|
||||
|
||||
Reference in New Issue
Block a user