diff --git a/CHANGELOG.md b/CHANGELOG.md index c5659ce..9f3b4bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,9 +58,10 @@ as in `[a](b` repeated: 16000 of them took 25 seconds, now 80 ms. - `adfToLosslessMarkdown` and `adfToPortableMarkdown` write faster a line of many marked texts or inline nodes that need a carry or lose their mark, as in text `un` beside strong `-r`, repeated: - 1000 of them took 10 seconds, now 150 ms. So does a line mixing em, strike and strong: 1000 - `un` beside `-r` marked em and strong took 9 seconds, now 140 ms, and 1000 `(` marked em, strike - and strong beside a mention took 18 seconds in `adfToPortableMarkdown`, now 110 ms. + 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. - `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 diff --git a/src/markdown/emit/inline-line.test.ts b/src/markdown/emit/inline-line.test.ts index 16b8893..c5c8710 100644 --- a/src/markdown/emit/inline-line.test.ts +++ b/src/markdown/emit/inline-line.test.ts @@ -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 seed's runs rarely reach: carrying one run lets a neighbour read as written, or one pass carries a repeated run's every copy. + // 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. { ...propertyRuns(3000), examples: [ diff --git a/src/markdown/emit/line-escaping.ts b/src/markdown/emit/line-escaping.ts index 3a8fa03..0f08d5e 100644 --- a/src/markdown/emit/line-escaping.ts +++ b/src/markdown/emit/line-escaping.ts @@ -28,7 +28,7 @@ export type InlineSegment = export type AssembledLine = { line: string; openingLinkAsDirective?: true; unspellableRuns: MarkRun[] } -// How the caller takes a run back: a carry swaps its nodes for one blob, splitting the runs around it; a drop removes its mark, moving no other delimiter where `alone` holds. +// How the caller takes a run back: a carry swaps its nodes for one carry directive, splitting every run holding it; a drop removes its mark, moving no other delimiter where `alone` holds. export type RunFallback = { kind: 'carry' } | { alone: (run: MarkRun) => boolean; kind: 'drop' } type ScanLine = { position: LinePosition; start: number; text: string } @@ -38,12 +38,12 @@ type EmphasisPair = { closer: number; delimiter: string; nodes: MarkRun; opener: type Escapings = (index: number) => InlineEscaping | undefined -// Which emphasis pairs fall with the first misread one, each marked in `fallen` once settled. +// Which emphasis pairs fall: go back in this pass with the first misread one. `fallen` flags each pair settled to fall. type PairSweep = { readonly fallback: RunFallback readonly fallen: Uint8Array readonly first: number - // The leaf boundaries where dropping a pair that may fall first can merge the runs meeting there. + // The ends of every run whose drop is not alone: dropping it can merge the runs meeting there. readonly merging: ReadonlySet readonly output: string // The pair each placed segment delimits, -1 where it delimits none. @@ -55,15 +55,15 @@ type PairSweep = { // Where a segment's first character landed in the output. type PlacedSegment = { segment: InlineSegment; start: number } -// How a pair stands while another falls: `either` where it may fall first. -type Standing = 'either' | 'gone' | 'there' - // The escaped line once reading it back asks for no further escape. type ReadBack = { output: string; placed: PlacedSegment[]; reading: InlineReading } // Which of a run's characters escape: every escapable one, or every one before `end`. type RunEscapes = { end: number; escapable: false } | { escapable: true } +// How a pair stands while another falls: `either` where it may fall before the judged one. +type Standing = 'either' | 'gone' | 'there' + // Which text characters escape wherever they stand: one of them opened a construct when the line was read back. type TextEscapes = { backticks: boolean; brackets: boolean } @@ -290,7 +290,7 @@ function claimsCharacter({ container, escapings, headingCloser, inline: { text: } } -// The runs the line does not read back as written: the first emphasis run to close and every run that falls with it, else every misflanked highlight run. One misread emphasis pair can re-pair the delimiters after it. +// The runs this pass takes back: the first emphasis run the line misreads and every run that falls with it, else every misflanked highlight run. One misread emphasis pair can re-pair the delimiters after it. function unspellableRuns(placed: readonly PlacedSegment[], reading: InlineReading, output: string, fallback: RunFallback): MarkRun[] { const { pairAt, pairs } = emphasisPairs(placed) const first = pairs.findIndex((pair) => !readAsWritten(reading, placed, pair)) @@ -351,7 +351,7 @@ function holdsAlike({ fallen, pairs }: PairSweep, judged: number, character: str return false } -// A carry splits every pair holding it, so a carried pair holds no pair that may fall first; a dropped pair keeps its leaves 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 closing before it, 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 @@ -376,10 +376,10 @@ function mayFlank(sweep: PairSweep, judged: number, character: string, end: 'clo ) } -// The characters that may stand past the run of `character` holding the delimiter at `from`, walking by `step`; `undefined` where a pair beside may move. +// The characters that may stand past the run of `character` holding the delimiter at `from`; `undefined` where a pair beside may move. function runEdge(sweep: PairSweep, judged: number, from: number, step: -1 | 1, character: string): string[] | undefined { const found: string[] = [] - // A backslash escaping the next text character stands past the last delimiter walked, so the walk tracks where that delimiter ends. + // Past the run stands the character after its last delimiter: a backslash where the text beside is escaped, which the next segment's start skips. let edge = delimiterEdge(sweep.placed, from, step) for (let at = from + step; ; at += step) { const segment = sweep.placed[at]?.segment @@ -403,7 +403,7 @@ function delimiterEdge(placed: readonly PlacedSegment[], at: number, step: -1 | return step === 1 ? delimiter.start + delimiter.segment.text.length : delimiter.start } -// How the other pair stands when the judged one falls. Past the first, every pair closing after the judged one still stands, since that one cannot read as written; `undefined` where a carry or a merge may move the other's delimiters. +// How the other pair stands when the judged one falls. Where the judged pair closes after the first, every pair closing after it still stands, since the judged pair is then the first that cannot read as written; `undefined` where a carry or a merge may move the other's delimiters. function pairStanding({ fallback, fallen, first, merging, pairs }: PairSweep, judged: number, other: number): Standing | undefined { const pair = pairs[other] if (other === judged) return 'there' @@ -413,7 +413,7 @@ function pairStanding({ fallback, fallen, first, merging, pairs }: PairSweep, ju return fallen[other] === 1 ? 'gone' : 'either' } -// The characters the other pair may be written with when the judged one falls: emphasis takes `_`, turning `*` beside a word character. +// The characters the other pair may be written with when the judged one falls: em takes `_`, turning `*` beside a word character. function pairSpellings(sweep: PairSweep, judged: number, other: number): string[] | undefined { const pair = sweep.pairs[other] if (pair === undefined) return undefined diff --git a/src/markdown/portable/inline-reduction.test.ts b/src/markdown/portable/inline-reduction.test.ts index d6253f4..b4bbe70 100644 --- a/src/markdown/portable/inline-reduction.test.ts +++ b/src/markdown/portable/inline-reduction.test.ts @@ -10,7 +10,7 @@ test('reduces a line alike dropping every run a pass may or the first alone', () fc.property(markedLine, lineContainer, (nodes, container) => { assert.deepEqual(reduceInline(nodes, container, [], 0), reduceInline(nodes, container, [], 0, 'first')) }), - // Lines the seed's runs rarely reach: a later run must not drop with the first, or one pass drops a repeated run's every copy. + // Lines the fixed seed rarely generates: a later run must not drop with the first, or one pass drops every copy of a repeated mark run. { ...propertyRuns(3000), examples: [ diff --git a/src/markdown/portable/inline-reduction.ts b/src/markdown/portable/inline-reduction.ts index 4c3bda4..d428caf 100644 --- a/src/markdown/portable/inline-reduction.ts +++ b/src/markdown/portable/inline-reduction.ts @@ -261,7 +261,7 @@ function droppedRuns(leaves: readonly AdfNode[], runs: readonly [MarkRun, ...Mar return perPass === 'every' ? runs : runs.filter((run, index) => index === 0 || nodeMarks(leaves[run.first] ?? {})[run.depth]?.type === highlight) } -// Dropping the run merges no runs and moves no edge, whatever else drops: at each end, the leaf beside holds no mark type the edge leaf holds inside the run's mark. +// Dropping the run merges no runs and moves no run's end, whatever else drops: at each end, the leaf outside holds no mark type the end leaf holds inside the run's mark. function dropsAlone(leaves: readonly AdfNode[], run: MarkRun): boolean { return endsAlone(leaves[run.first], leaves[run.first - 1], run.depth) && endsAlone(leaves[run.last], leaves[run.last + 1], run.depth) } diff --git a/todo.md b/todo.md index 81cd41a..c5d4658 100644 --- a/todo.md +++ b/todo.md @@ -12,7 +12,7 @@ | 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, with no switch only tests use in production signatures.** | 3 | 1 | 2 | 4 | 5 | code health by 5 | 13.1 | +| 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 | | 150 | 0.3.0 | | decision | **Read `adfToHtml`'s HTML in `htmlToAdf` as a browser serializes it too — a raw `"` in text, ` `, `\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,14 +69,17 @@ ## Details -### 160. Make the block-text tree and the batched mark fallbacks read as easily as the code before items 33 and 109, with no switch only tests use in production signatures. +### 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 maintainer chose to merge it and file this. Make each place they named easier to read: `writtenText` and `Shape` in `block-text.ts` (a stack walk and eight hand-kept measures), `FallbacksPerPass` threaded through `emitInlineLine` and `reduceInline` only for two properties, -`holdsOnlyRuleCharacters` holding BlockText's rule in the grammar, and `markerOpening` repeating -the marker regexes' `[`. No output may change, and the timings of items 33, 109 and 158 must hold. +`unspellableRuns`'s pair sweep in `line-escaping.ts`, which needs CommonMark's flanking rules open +beside it, `takenThisPass` and `checkedCarry` in `inline-line.ts`, `tryListItemLines` reading +`block-text.ts`'s measures, `holdsOnlyRuleCharacters` holding BlockText's rule in the grammar, and +`markerOpening` repeating the marker regexes' `[`. No output may change, and the timings the cost +entries under `CHANGELOG.md` → `## Unreleased` give for both writers must hold. ### 150. Read `adfToHtml`'s HTML in `htmlToAdf` as a browser serializes it too — a raw `"` in text, ` `, `\r\n` line endings — so a page's `innerHTML` or a form post reads back.