mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-13 12:18:28 +01:00
Answer prose round 1: the README says which refusal differs between engines, items 106 and 107 read one idea a sentence, and two test titles and a comment read on one pass
This commit is contained in:
+1
-1
@@ -88,7 +88,7 @@
|
||||
`adfToLosslessMarkdown` and `adfToPortableMarkdown` refuse it as `unsupported-node-shape`, where
|
||||
they threw.
|
||||
- Fix deeply nested compact JSON in an `adf:` fence: `losslessMarkdownToAdf` refuses it as
|
||||
`unsupported-node-shape`, like any compact JSON there, where a megabyte of it threw.
|
||||
`unsupported-node-shape`, as it refuses any compact JSON there, where about a megabyte of it threw.
|
||||
- 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,
|
||||
|
||||
@@ -408,8 +408,9 @@ A parse always names a position, so the three markdown readers return `ParseErro
|
||||
typed on `ConvertError` takes both.
|
||||
|
||||
A call reports the first refusal in document order and stops, so a document with several surfaces
|
||||
them one per call. Every refusal is deterministic — there is no I/O anywhere — so a retry returns
|
||||
the identical error: fix the input, or set the document aside.
|
||||
them one per call. Every refusal is deterministic on one engine — there is no I/O anywhere — so a
|
||||
retry returns the identical error: fix the input, or set the document aside. Only the engine-limit
|
||||
refusal under `unsupported-node-shape` differs between engines.
|
||||
|
||||
### Parsing
|
||||
|
||||
@@ -442,7 +443,7 @@ The emitter raises these, and so does a parse, which asks the emitter how to spe
|
||||
| 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` | parsing, in `losslessMarkdownToAdf`: markdown spells a node with an attribute, value, argument or body its type does not take, or without one it needs, wraps a directive mark around nothing or only hard breaks, writes as a directive a node or mark the lossless flavour spells as CommonMark, or puts a reserved directive out of place: `!adf:textBreak{}` parting nothing, `!adf:doc` anywhere but as the whole document. Emitting, in `adfToLosslessMarkdown`: a node not riding the carry that carries 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`. Emitting, in `adfToPortableMarkdown`: a line or task list the portable writer cannot spell, a bug in this library. Emitting, in both writers: a document whose markdown would be longer than the longest string or array the JavaScript engine builds — on V8, about 537 million characters of markdown, or about 134 million in one text node | fix what the message names; [`spec/flavour.md`](https://github.com/larvit/adf-codec/blob/main/spec/flavour.md) lists every type's attributes and body. For a portable line or task list, report it. Past the engine's longest string or array, keep the ADF and pass the document over, or show it read-only |
|
||||
| `unsupported-node-shape` | parsing, in `losslessMarkdownToAdf`: markdown spells a node with an attribute, value, argument or body its type does not take, or without one it needs, wraps a directive mark around nothing or only hard breaks, writes as a directive a node or mark the lossless flavour spells as CommonMark, or puts a reserved directive out of place: `!adf:textBreak{}` parting nothing, `!adf:doc` anywhere but as the whole document. Emitting, in `adfToLosslessMarkdown`: a node not riding the carry that carries 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`. Emitting, in `adfToPortableMarkdown`: a line or task list the portable writer cannot spell, a bug in this library. Emitting, in both writers: a document whose markdown would be longer than the longest string or array the JavaScript engine builds — on V8, about 537 million characters of markdown, or about 134 million in one text node | fix what the message names; [`spec/flavour.md`](https://github.com/larvit/adf-codec/blob/main/spec/flavour.md) lists every type's attributes and body. For a portable line or task list, report it as a bug. Past the engine's longest string or array, keep the ADF and pass the document over, or show it read-only |
|
||||
|
||||
## The guarantees
|
||||
|
||||
|
||||
+3
-3
@@ -463,9 +463,9 @@ handles one cause alike whichever node, attribute or direction raised it.
|
||||
emitting — no document holds one, so no round-trip crosses them (2026-09-23).
|
||||
- A document whose markdown would be longer than the longest string or array the JavaScript engine
|
||||
builds is `unsupported-node-shape` at the document's path, in either writer: the document does
|
||||
not convert, and the bound is the engine's own. A carried node reaches it on about 1 MB of
|
||||
deeply nested JSON, since the block carry indents two spaces a level; `todo.md` item 107 spells
|
||||
it in a length linear in its JSON (2026-10-11).
|
||||
not convert, and no spelling in the input causes the bound (§The code list). A carried node
|
||||
passes it on about 1 MB of deeply nested JSON, since the block carry indents two spaces a level;
|
||||
`todo.md` item 107 respells the carry in a length linear in its JSON (2026-10-11).
|
||||
- The markdown readers and `htmlToAdf` refuse a value that is not a string as `not-a-string`, at
|
||||
position `{ line: 1, offset: 0 }` with an empty path: no existing code reads true of it, and
|
||||
`not-an-adf-document` names the emitters' input (2026-10-05).
|
||||
|
||||
@@ -15,7 +15,7 @@ export function serializeCanonicalJson(value: JsonValue, spelling: JsonSpelling)
|
||||
return text.join('')
|
||||
}
|
||||
|
||||
// Stops at the first piece `text` does not hold: two-space indentation can spell a short compact text past the engine's longest string.
|
||||
// Stops at the first piece that does not match `text`: re-spelled with two-space indentation, a short compact text can pass the engine's longest string.
|
||||
export function spellsCanonicalJson(value: JsonValue, spelling: JsonSpelling, text: string): boolean {
|
||||
let offset = 0
|
||||
const whole = writeCanonicalJson(value, spelling, (piece) => {
|
||||
@@ -26,7 +26,6 @@ export function spellsCanonicalJson(value: JsonValue, spelling: JsonSpelling, te
|
||||
return whole && offset === text.length
|
||||
}
|
||||
|
||||
// `write` returns false to stop the walk, and the walk then returns false.
|
||||
function writeCanonicalJson(value: JsonValue, spelling: JsonSpelling, write: (piece: string) => boolean): boolean {
|
||||
const indent = spelling === 'compact' ? '' : ' '
|
||||
const pending: Pending[] = [{ depth: 0, value }]
|
||||
|
||||
@@ -448,7 +448,7 @@ test('names the canonical spelling a carried JSON reads alone', () => {
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:carry{json="{\\"type\\":\\"blockCard\\",\\"attrs\\":{}}"}\n')), `${canonically}compact, keys sorted${literal}`)
|
||||
})
|
||||
|
||||
test('refuses compact JSON in the carry fence without spelling it two-space past the engine\'s longest string', () => {
|
||||
test('refuses compact JSON in the carry fence without building its two-space spelling', () => {
|
||||
const json = `{"attrs":{"value":${'['.repeat(495)}${new Array(2_200_000).fill(0).join(',')}${']'.repeat(495)}}}`
|
||||
assert.equal(
|
||||
content(losslessMarkdownToAdf(`\`\`\`adf:unknownBlock\n${json}\n\`\`\`\n`)),
|
||||
|
||||
@@ -70,7 +70,7 @@ test('refuses what the document guard refuses, and nothing else', () => {
|
||||
assert.match(portableDocument(document(paragraph(deepInline))), /^unsupported-nesting-depth at /)
|
||||
})
|
||||
|
||||
test('refuses a document whose text merges past the engine\'s longest string', () => {
|
||||
test('refuses a paragraph whose merged text passes the engine\'s longest string', () => {
|
||||
const long = 'a'.repeat(2 ** 28)
|
||||
const merged = paragraph(...Array.from({ length: 9 }, () => text(long)))
|
||||
assert.equal(portableDocument(document(merged)), 'unsupported-node-shape at /')
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
| ID | Release | Exempt | Item | R | S | A | G | Goals | Score |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| 106 | 0.2.0 | defect | **Refuse a document or markdown whose spelling would pass a stated length before building it, so no call exhausts the heap.** | 4 | 6 | 5 | 8 | 1, 9 | 20.5 |
|
||||
| 106 | 0.2.0 | defect | **Refuse a document or markdown whose output would pass a stated length before building it, so no call exhausts the heap.** | 4 | 6 | 5 | 8 | 1, 9 | 20.5 |
|
||||
| 103 | 0.2.0 | | **Fold `unspellable-whitespace` into `unsupported-node-shape`, so a line ending and a null character in a content slot take one code.** | 2 | 2 | 5 | 6 | 1 | 19.5 |
|
||||
| 84 | 0.2.0 | | **Read GFM's pipe table without leading pipes, and with alignment colons, in `portableMarkdownToAdf`, dropping the alignment.** | 3 | 4 | 7 | 7 | 4, 7 | 19.4 |
|
||||
| 52 | 0.2.0 | | **Spell `colwidth` as a comma list, `colwidth="340,420"`.** | 3 | 3 | 5 | 6 | 6 | 11.5 |
|
||||
@@ -73,19 +73,19 @@
|
||||
|
||||
## Details
|
||||
|
||||
### 106. Refuse a document or markdown whose spelling would pass a stated length before building it, so no call exhausts the heap.
|
||||
### 106. Refuse a document or markdown whose output would pass a stated length before building it, so no call exhausts the heap.
|
||||
|
||||
The writers return `unsupported-node-shape` where the engine throws past its longest string, but
|
||||
some documents exhaust the heap first, which no catch survives. `losslessMarkdownToAdf` on 2.1 MB
|
||||
of markdown, a `!adf:blockquote` holding a list nested 490 levels whose paragraph runs on in 700,000
|
||||
some documents exhaust the heap first, which no catch survives. `losslessMarkdownToAdf` on 2.1 MB of
|
||||
markdown, a `!adf:blockquote` holding a list nested 490 levels whose paragraph runs on in 700,000
|
||||
lazy lines `b\`, crashed Node 24 out of heap: `directiveNode` re-spells the subtree, indenting each
|
||||
line to its depth. `adfToLosslessMarkdown` on nine text nodes sharing one 2^28-character string
|
||||
got Bun OOM-killed under 8 GB: `escape` in `line-escaping.ts` pushes one array element per
|
||||
character. Where the heap holds out, the readers' re-spelling can still throw a `RangeError`, and
|
||||
JavaScriptCore's `Out of memory`, which the writers refuse, may also mean a failed allocation a
|
||||
retry survives. The length to refuse past is a number no entry states yet (AGENTS.md §Stated numbers):
|
||||
V8's 2^29 − 24 on every engine, so the same document converts alike everywhere, is the candidate.
|
||||
Found by the principles audit of item 83, 2026-10-11.
|
||||
line to its depth. `adfToLosslessMarkdown` on nine text nodes sharing one 2^28-character string got
|
||||
Bun OOM-killed under 8 GB: `escape` in `line-escaping.ts` pushes one array element per character.
|
||||
Where the heap holds out, the readers' re-spelling can still throw a `RangeError`. JavaScriptCore's
|
||||
`Out of memory`, which the writers refuse, may also mean a failed allocation that a retry survives.
|
||||
The candidate length to refuse past is V8's 2^29 − 24 on every engine, so the same document converts
|
||||
alike everywhere; no entry states it yet (AGENTS.md §Stated numbers). Found by the principles audit
|
||||
of item 83, 2026-10-11.
|
||||
|
||||
### 103. Fold `unspellable-whitespace` into `unsupported-node-shape`, so a line ending and a null character in a content slot take one code.
|
||||
|
||||
@@ -313,11 +313,11 @@ interim sentence.
|
||||
|
||||
### 107. Spell the block carry's JSON so its length stays linear in the node's JSON.
|
||||
|
||||
The block carry indents two spaces a level, so a carried node's JSON about 1 MB deep in nested
|
||||
arrays spells past V8's longest string, which the writers refuse as `unsupported-node-shape`; the
|
||||
inline carry's compact JSON would fit. §The code list calls a refusal no spelling recovers from a
|
||||
gap in the flavour. A format change: §The formats are API. Found by the README-goals audit of item
|
||||
83, 2026-10-11.
|
||||
The block carry indents two spaces a level, so about 1 MB of deeply nested arrays in a carried
|
||||
node's JSON spells past V8's longest string, which the writers refuse as `unsupported-node-shape`;
|
||||
the inline carry's compact JSON would fit. docs/decisions.md §The code list calls a refusal no
|
||||
spelling recovers from a gap in the flavour. Respelling the carry changes the format (§The formats
|
||||
are API). Found by the README-goals audit of item 83, 2026-10-11.
|
||||
|
||||
### 51. Match a reference label to its definition under Unicode case folding.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user