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

Answer stability round 2: 1 🔴, 1 🟡 (0 misread); carry pairs spanning the same nodes together, file item 161 for em sharing a run with strong

This commit is contained in:
2026-10-30 11:45:10 +01:00
parent 443bf5a255
commit 08f79fae3f
4 changed files with 38 additions and 9 deletions
+3 -1
View File
@@ -61,7 +61,9 @@
1000 of them took 10 seconds, now 150 ms. They also write faster a line mixing em, strike and
strong: text `un` beside `-r` marked em and strong, repeated 1000 times, took up to 9 seconds,
now under 170 ms, and `(` marked em, strike and strong beside a mention, repeated 1000 times,
took 18 seconds in `adfToPortableMarkdown`, now 150 ms.
took 18 seconds in `adfToPortableMarkdown`, now 150 ms. A line where em shares a delimiter run
with strong takes time quadratic in its length, as in `(` marked em and strong beside `(` marked
em, repeated.
- `adfToLosslessMarkdown` and `adfToPortableMarkdown` take time linear in the markdown when lists
and quotes nest deeply, and so does `losslessMarkdownToAdf`: a list nested 480 levels around
64 KB of paragraphs took 2 seconds to write, now 100 ms. A `!adf:blockquote` holding 2 MB of lines
+3 -1
View File
@@ -10,7 +10,7 @@ test('spells a line alike taking every fallback a pass finds or the first alone'
fc.property(markedLine, lineContainer, (nodes, container) => {
assert.deepEqual(emitInlineLine(nodes, container, [], 'lossless'), emitInlineLine(nodes, container, [], 'lossless', 'first'))
}),
// Lines the fixed seed rarely generates: carrying one run lets a neighbour read as written, or one pass carries every copy of a repeated mark run.
// Lines the fixed seed rarely generates: carrying one run lets a neighbour or its holder read as written, or one pass carries every copy of a repeated mark run.
{
...propertyRuns(3000),
examples: [
@@ -21,6 +21,8 @@ test('spells a line alike taking every fallback a pass finds or the first alone'
[[{ text: 'un', type: 'text' }, { marks: [{ type: 'em' }, { type: 'strong' }], text: '-r', type: 'text' }, { marks: [{ type: 'strike' }], text: 'un', type: 'text' }, { marks: [{ type: 'strike' }, { type: 'em' }, { type: 'strong' }], text: '-r', type: 'text' }, { text: 'z', type: 'text' }], 'paragraph'],
[[{ marks: [{ type: 'strike' }], text: ' b', type: 'text' }, { marks: [{ type: 'em' }], text: '!-', type: 'text' }], 'paragraph'],
[[{ marks: [{ type: 'strike' }], text: 'r', type: 'text' }, { marks: [{ type: 'strike' }, { type: 'strong' }, { type: 'em' }], text: '[ ', type: 'text' }, ...[1, 2, 3, 4, 5].map(() => ({ marks: [{ type: 'strike' }], text: ' ', type: 'text' }))], 'heading'],
[[{ text: 'a', type: 'text' }, { marks: [{ type: 'em' }, { type: 'strike' }], text: '(', type: 'text' }, { marks: [{ type: 'em' }], text: 'a', type: 'text' }], 'paragraph'],
[[{ text: 'b', type: 'text' }, { marks: [{ type: 'strong' }], text: '(a', type: 'text' }, { marks: [{ type: 'em' }], text: '(b', type: 'text' }, { text: 'c', type: 'text' }], 'paragraph'],
],
},
)
+12 -5
View File
@@ -351,7 +351,7 @@ function holdsAlike({ fallen, pairs }: PairSweep, judged: number, character: str
return false
}
// A carry splits every pair holding it, so a pair falls cleanly under a carry only where it holds no pair closing before it, and under a drop only where no run merges at its ends.
// A carry splits every pair holding it, so a pair falls cleanly under a carry only where it holds no pair spanning fewer nodes, and under a drop only where no run merges at its ends.
function fallsCleanly({ fallback, merging, pairs }: PairSweep, judged: number): boolean {
const pair = pairs[judged]
if (pair === undefined) return false
@@ -409,7 +409,8 @@ function pairStanding({ fallback, fallen, first, merging, pairs }: PairSweep, ju
if (other === judged) return 'there'
if (pair === undefined || mergesAt(merging, pair)) return undefined
if (other > judged && judged > first) return 'there'
if (fallback.kind === 'carry') return undefined
// Under a carry, pairs spanning the same nodes stand and fall together.
if (fallback.kind === 'carry') return spansAlike(pair, pairs[judged]) ? 'there' : undefined
return fallen[other] === 1 ? 'gone' : 'either'
}
@@ -448,10 +449,16 @@ function wordBeside(sweep: PairSweep, judged: number, from: number, step: -1 | 1
}
}
// Pairs close in order, so a pair holding any pair that closes before the judged one holds the one just before it.
// Whether the pair holds one a carry may take before the judged one. Pairs close in order, so the judged pair, or one holding it, holds such a pair only where it holds the last of them to close.
function holdsEarlier(pairs: readonly EmphasisPair[], judged: number, pair: EmphasisPair): boolean {
const earlier = pairs[judged - 1]
return earlier !== undefined && pair.nodes.first <= earlier.nodes.first && earlier.nodes.last <= pair.nodes.last
let earlier = judged - 1
while (spansAlike(pairs[earlier], pairs[judged])) earlier -= 1
const held = pairs[earlier]
return held !== undefined && pair.nodes.first <= held.nodes.first && held.nodes.last <= pair.nodes.last
}
function spansAlike(pair: EmphasisPair | undefined, other: EmphasisPair | undefined): boolean {
return pair !== undefined && other !== undefined && pair.nodes.first === other.nodes.first && pair.nodes.last === other.nodes.last
}
function highlightFlanks(output: string, start: number, role: DelimiterRole): boolean {
+20 -2
View File
@@ -6,13 +6,14 @@
`Bar = 0.1`
`Next ID = 161`
`Next ID = 162`
## Items
| ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 160 | 0.3.0 | | | **Make the block-text tree and the batched mark fallbacks read as easily as the code before items 33 and 109, and keep every switch only tests use out of production signatures.** | 3 | 1 | 2 | 4 | 5 | code health by 5 | 13.1 |
| 161 | 0.3.0 | | defect | **Write a line where em shares a delimiter run with strong in time linear in its mark runs, in `adfToLosslessMarkdown` and `adfToPortableMarkdown`.** | 4 | 1 | 2 | 5 | 5 | 9 by 3 | 7.8 |
| 160 | 0.3.0 | 161 | | **Make the block-text tree and the batched mark fallbacks read as easily as the code before items 33 and 109, and keep every switch only tests use out of production signatures.** | 3 | 1 | 2 | 4 | 5 | code health by 5 | 13.1 |
| 150 | 0.3.0 | | decision | **Read `adfToHtml`'s HTML in `htmlToAdf` as a browser serializes it too — a raw `"` in text, `&nbsp;`, `\r\n` line endings — so a page's `innerHTML` or a form post reads back.** | 2 | 2 | 2 | 4 | 4 | 6 by 5 | 5.0 |
| 115 | 0.3.0 | | defect | **Build text holding no `border` mark in `losslessMarkdownToAdf`, and have `adfToLosslessMarkdown` carry text holding one.** | 2 | 1 | 2 | 3 | 3 | 4 by 3 | 4.6 |
| 91 | 0.3.0 | | | **Spell an emphasis once across the code spans, an emphasis or link once across the hard breaks, dates, emoji, inline cards, mentions and statuses between two nodes it marks, and an emphasis across inline media, in both writers.** | 3 | 2 | 2 | 5 | 5 | 6 by 6 | 3.6 |
@@ -69,6 +70,23 @@
## Details
### 161. Write a line where em shares a delimiter run with strong in time linear in its mark runs, in `adfToLosslessMarkdown` and `adfToPortableMarkdown`.
The stability review of item 158 found it, on main before item 158 as well. Em's `*` and strong's
`**` form one delimiter run, so once that strong closes, the next strong opener that can close
captures the `*` left over. One run per pass then takes back one strong per pass, and every pass
must write the same. At 1000 repeats:
- `(` marked em and strong beside `(` marked em, then `a`: 7.4 s in `adfToPortableMarkdown`;
- `~** ` marked em and strong beside `- _` marked em, then `r`: 8.6 s in `adfToPortableMarkdown`;
- ` ~~ ` marked em and strong beside `)` marked em, strike and strong, then `a` and `.unun` marked
strike and strong: 33 s in `adfToLosslessMarkdown`, 39 s in `adfToPortableMarkdown`.
v0.2.0 wrote each in under 100 ms at 400 repeats: lossless carried the whole line, and portable
kept strong on every `(`. `unspellableRuns` judges pair by pair, so it cannot predict these captures
without CommonMark's delimiter stack across passes. Dropping a doomed em before the strongs it holds
may be linear and keep more marks, but it changes what the portable writer keeps: ask first.
### 160. Make the block-text tree and the batched mark fallbacks read as easily as the code before items 33 and 109, and keep every switch only tests use out of production signatures.
PR #106's paired ruling read worse than c7bbb04: both seats ruled Locality and Shape worse, the