rapier-markdown-kit 1.1.104 → 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +8 -4
- package/README.md +50 -7
- package/dist/images/gif.mjs +49 -0
- package/dist/images/playback.mjs +103 -0
- package/dist/kit/conformance/03-wrap-around-silhouette.md +1 -1
- package/dist/kit/conformance/04-wrap-box.md +1 -1
- package/dist/kit/conformance/05-behind.md +1 -1
- package/dist/kit/conformance/06-front.md +1 -1
- package/dist/kit/conformance/07-rotate-raster.md +1 -1
- package/dist/kit/conformance/08-drawing-rotation.md +1 -1
- package/dist/kit/conformance/09-drawing-ring-interior.expected.json +18 -18
- package/dist/kit/conformance/09-drawing-ring-interior.md +1 -1
- package/dist/kit/conformance/10-both-sides.md +1 -1
- package/dist/kit/conformance/11-neighbour-skips-image.md +1 -1
- package/dist/kit/conformance/{12-heading-barrier.expected.json → 12-rule-barrier.expected.json} +1 -1
- package/dist/kit/conformance/{12-heading-barrier.md → 12-rule-barrier.md} +2 -2
- package/dist/kit/conformance/README.md +19 -28
- package/dist/kit/conformance/run.mjs +10 -8
- package/dist/kit/ledger/authored-origin.mjs +49 -0
- package/dist/kit/ledger/authorship.mjs +18 -9
- package/dist/kit/ledger/carried.mjs +10 -5
- package/dist/kit/ledger/data.mjs +7 -4
- package/dist/kit/ledger/format.mjs +37 -11
- package/dist/kit/ledger/hash.mjs +31 -15
- package/dist/kit/ledger/history.mjs +48 -10
- package/dist/kit/ledger/inverse-source.mjs +111 -49
- package/dist/kit/ledger/journal-records.mjs +24 -29
- package/dist/kit/ledger/merge.mjs +51 -15
- package/dist/kit/ledger/text-merge.mjs +75 -29
- package/dist/kit/ledger/transport.mjs +30 -45
- package/dist/kit/pdf-vectors.mjs +6 -2
- package/dist/kit/print-capitals.mjs +0 -1
- package/dist/kit/rapier-ledger.mjs +4 -2
- package/dist/kit/render-ink.mjs +1 -1
- package/dist/kit/render-markdown.mjs +155 -31
- package/dist/kit/render-print.mjs +173 -61
- package/dist/kit/render-sanitize.mjs +25 -15
- package/dist/kit/render-styles.mjs +3 -3
- package/dist/kit/render.mjs +37 -115
- package/dist/layout/model.mjs +10 -8
- package/dist/layout/paragraph-fit.mjs +1 -15
- package/dist/layout/typography.mjs +2 -1
- package/dist/packages/rapier-diff/concerns.mjs +163 -0
- package/dist/packages/rapier-diff/diff.mjs +68 -0
- package/dist/packages/rapier-diff/index.mjs +4 -701
- package/dist/packages/rapier-diff/matching.mjs +157 -0
- package/dist/packages/rapier-diff/merge.mjs +226 -0
- package/dist/packages/rapier-diff/moves.mjs +130 -0
- package/dist/packages/rapier-diff/structure.mjs +50 -0
- package/dist/packages/rapier-diff/tokens.mjs +72 -0
- package/dist/packages/rapier-diff/walk.mjs +219 -0
- package/dist/spec/document-fonts.mjs +1 -1
- package/dist/spec/document-settings.mjs +11 -164
- package/dist/spec/html-blocks.mjs +618 -79
- package/dist/spec/md-assets.mjs +29 -6
- package/dist/spec/md-layout.mjs +9 -1
- package/dist/spec/md-marks.mjs +25 -147
- package/dist/spec/md-source.mjs +69 -26
- package/package.json +1 -1
- package/style.css +7 -51
package/LICENSE
CHANGED
|
@@ -21,8 +21,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
21
21
|
SOFTWARE.
|
|
22
22
|
|
|
23
23
|
Files under this licence carry `SPDX-License-Identifier: MIT` in their first lines: the standard's
|
|
24
|
-
grammar, marks and reference style (`spec/md-layout.mjs`, `spec/md-marks.mjs`, `spec/md-source.mjs`, `spec/flowchart.mjs`, `spec/markdown-style.css`),
|
|
25
|
-
(`
|
|
24
|
+
grammar, marks and reference style (`spec/md-layout.mjs`, `spec/md-marks.mjs`, `spec/md-source.mjs`, `spec/flowchart.mjs`, `spec/markdown-style.css`), ink geometry
|
|
25
|
+
(`spec/ink.mjs`),
|
|
26
|
+
the layout model and line planner (`layout/model.mjs`, `layout/line-plan.mjs`, `layout/typography.mjs`, `layout/preparation.mjs`,
|
|
27
|
+
`layout/prepared-paragraph.mjs`, `layout/hyphenation.mjs`, `layout/paragraph-fit.mjs`), the animated-picture reader and playback
|
|
28
|
+
the HTML blocks use (`images/gif.mjs`, `images/playback.mjs`),
|
|
26
29
|
the portable HTML and document metadata helpers (`spec/html-blocks.mjs`, `spec/frontmatter.mjs`,
|
|
27
30
|
`spec/document-settings.mjs`, `spec/document-fonts.mjs`), the Will grammar (`agent/will.mjs`), the picture appendix reader and writer
|
|
28
31
|
(`spec/md-assets.mjs`), the two shared-page reference readers (`tools/read-shared-page.mjs`,
|
|
@@ -32,8 +35,9 @@ own MIT notices. The edit-ledger module (`kit/rapier-ledger.mjs`, `kit/ledger/*.
|
|
|
32
35
|
its editor compatibility import (`editor/journal-records.mjs`) are also covered. The
|
|
33
36
|
record validator/replayer and text-only conflict merger were moved, not copied,
|
|
34
37
|
into that module; the Notes index merger and the rest of the agent/editor remain
|
|
35
|
-
AGPL-3.0-only. The wrapper
|
|
36
|
-
|
|
38
|
+
AGPL-3.0-only. The page wrapper (`skills/rapier-html/wrap.mjs`, `skills/rapier-html/page.mjs`,
|
|
39
|
+
`skills/rapier-html/return-address.mjs`) and its carrier-staging helper
|
|
40
|
+
(`skills/rapier-html/package-files.mjs`) are MIT. The kit's MIT-marked facades,
|
|
37
41
|
`kit/render-entry.mjs`, `kit/render*.mjs`, `kit/export-fonts.mjs`, `kit/print-capitals.mjs`,
|
|
38
42
|
`kit/pdf-vectors.mjs`, `kit/pdf-glyphs.mjs`, `kit/stage.mjs` and MIT-marked conformance
|
|
39
43
|
modules are covered too. These names describe the Rapier-owned files carrying the
|
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ import {parseAssets} from 'rapier-markdown-kit/assets'; // the picture appendi
|
|
|
37
37
|
Rapier is a phone-first Markdown editor for notes, diagrams, drawing and watercolor painting. It runs offline
|
|
38
38
|
as one HTML file on Android, Web and Windows.
|
|
39
39
|
|
|
40
|
-
1. **Work together live.** Connect to `https://mcp.rapier.website/mcp`, then call `open` to share one document with the person.
|
|
40
|
+
1. **Work together live.** Connect to `https://mcp.rapier.website/door/mcp`, then call `open` to share one document with the person.
|
|
41
41
|
2. **Deliver an offline editor.** Run `npx rapier-html notes.md` to put the document and editor in one HTML file ([rapier-html](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-html)).
|
|
42
42
|
3. **Embed in your app.** Install [rapier-embed](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-embed); mount the editor or reader, with your app's agent over WebMCP in the editor.
|
|
43
43
|
4. **Keep portable Markdown.** Use [rapier-markdown-kit](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-markdown-kit) for pictures, editable SVG drawings and layout in one `.md` file. Respect [Will/1](https://github.com/jackskip22/will) `keep`, `append` and `edit` regions.
|
|
@@ -64,6 +64,27 @@ const {html, filename} = await writer.render(source, {filename: 'report.md'});
|
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
`createMarkdownRenderer(host).render(source, metadata)` returns the semantic HTML.
|
|
67
|
+
`createMarkdownRenderer(host).renderWithMathML(source, metadata)` returns
|
|
68
|
+
`{html, equations}` using the ready host math provider's `renderWithMathML` operation.
|
|
69
|
+
The provider must also support the existing `renderToString` operation.
|
|
70
|
+
Each equation is parsed once for its SVG and MathML. The HTML stays identical to
|
|
71
|
+
ordinary SVG rendering; `equations` follows render order and carries `source`
|
|
72
|
+
(the admitted token spelling, including its delimiters), `tex`, `displayMode`, `semantics`, and SVG
|
|
73
|
+
`errors`. Pass `{docKind: 'markdown'}` as metadata for Markdown documents.
|
|
74
|
+
The existing Markdown grammar may normalize display-block indentation; the original
|
|
75
|
+
document remains the source of untouched document bytes.
|
|
76
|
+
Inspect both `errors` and `semantics.status`: a rendered SVG can have refused or
|
|
77
|
+
partial semantic output. The method throws if the combined provider is unavailable
|
|
78
|
+
or fails, rather than returning an empty semantic success. It does not return an AST.
|
|
79
|
+
|
|
80
|
+
This explicit method makes structured output available to consuming applications;
|
|
81
|
+
Rapier's live editor and ordinary HTML/DOCX/print exports still use the current
|
|
82
|
+
engine's Latin Modern SVG path. They do not insert hidden MathML, synthesize speech,
|
|
83
|
+
or claim screen-reader qualification. A consumer that inserts MathML must preserve
|
|
84
|
+
its namespace and semantics/annotation elements, name enclosing links meaningfully,
|
|
85
|
+
and qualify its own accessibility presentation. The current ordinary document
|
|
86
|
+
sanitizer is not a MathML composition API.
|
|
87
|
+
|
|
67
88
|
The page writer's `render` returns `{html, filename}` asynchronously and preserves the
|
|
68
89
|
original source, including BOM and line endings, in the SHA-256-verified source carrier.
|
|
69
90
|
Every existing lower-level method is also returned for hosts that compose the stages.
|
|
@@ -235,8 +256,7 @@ are ignored; invalid/unclosed style strings admit no facts. Inner spans override
|
|
|
235
256
|
clearing a selection removes that property's overrides and preserves the surrounding words' styles.
|
|
236
257
|
|
|
237
258
|
Keep each opener with its first words and each closer with its last words. Soft/hard breaks may stay inside one paragraph; blank lines cannot. An opener alone on a line can become a raw HTML block.
|
|
238
|
-
|
|
239
|
-
with its whole block in a non-HTML conversion. With inline HTML enabled, the new form preserves words, emphasis,
|
|
259
|
+
With inline HTML enabled, the new form preserves words, emphasis,
|
|
240
260
|
links and heading rank; GitHub strips the styles, and HTML-disabled readers may show the tags. Rapier's **Copy
|
|
241
261
|
markdown without typography** removes recognized wrappers while retaining semantic Markdown and literal code.
|
|
242
262
|
Its separate Pandoc/Quarto copy option writes bracketed spans deliberately; Pandoc HTML carries the CSS, but
|
|
@@ -563,6 +583,8 @@ no accessors, sparse arrays, cycles, undefined, negative zero or unpaired surrog
|
|
|
563
583
|
Readers reject unknown envelope/start/head fields, bad checksums, invalid records,
|
|
564
584
|
invalid source boundaries, a replay/root mismatch, or a head different from the
|
|
565
585
|
actual document. `historyEnvelope` additionally proves the editor's Undo/Redo branch.
|
|
586
|
+
Its optional third and fourth arguments are the expected text and metadata, as in
|
|
587
|
+
`readLedger`; installing history can prove both source and navigation in one pass.
|
|
566
588
|
|
|
567
589
|
**Roots are path-dependent.** `textRoot(text)` mints the existing
|
|
568
590
|
`chars:fnv-base36:adler-base36` initial root. Each splice advances it with
|
|
@@ -601,7 +623,9 @@ the authorship projection recomputed from the ledger must agree exactly.
|
|
|
601
623
|
text. Runs are `[start, end, actorKind, actorId, at]`, in order, nonoverlapping and
|
|
602
624
|
covering every current character. A start without earlier authorship is attributed
|
|
603
625
|
to `system/unattributed`, time zero. Insertions take the inserting record's actor
|
|
604
|
-
and time
|
|
626
|
+
and time, except text an Undo or Redo puts back: a splice inserting exactly the text
|
|
627
|
+
that a splice of the referenced act removed keeps the runs that text had. Deleted
|
|
628
|
+
runs disappear. Thus the projection contains **no removed text**.
|
|
605
629
|
An authorship-only import becomes a clearly incomplete insertion ledger; future
|
|
606
630
|
attribution is again derived from that one ledger, never a mutable blame database.
|
|
607
631
|
|
|
@@ -628,6 +652,15 @@ The comparison page's `#rapier-base` is independent: `text/markdown`, with
|
|
|
628
652
|
display reference. It is not reconstructed from the optional ledger and never
|
|
629
653
|
replaces current source. `rapier-html --compare original.md revised.md` packages both texts.
|
|
630
654
|
|
|
655
|
+
A comparison may also carry `data-fonts`, an HTML-attribute-escaped JSON string of
|
|
656
|
+
canonical embedded `[font:]` reference definitions. The admitted base record exposes
|
|
657
|
+
this as `fonts`. These resources belong to the exact comparison, outside its text,
|
|
658
|
+
digest and revision; they never become a source edit or ledger record. Font preparation
|
|
659
|
+
validates their actual bytes and reads font usage only from the original comparison.
|
|
660
|
+
Existing source-owned assets remain authoritative. A missing personal-library font is
|
|
661
|
+
embedded here when available, otherwise export refuses. Reopening and resaving need
|
|
662
|
+
no library or network, and never substitute the current device's default font.
|
|
663
|
+
|
|
631
664
|
## Merging copies without a server
|
|
632
665
|
|
|
633
666
|
`merge(first, second)` locates a common checkpoint with the same root **and exact
|
|
@@ -638,9 +671,19 @@ editor's restore reader can replay the result. Both merge orders yield the same
|
|
|
638
671
|
text for independently edited nonoverlapping ranges; their ordered ledgers need
|
|
639
672
|
not be identical. Full and conflict receipts make repeated imports idempotent.
|
|
640
673
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
674
|
+
An identical replacement converges with both original acts retained and one physical
|
|
675
|
+
splice. The format identifier stays `rapier-ledger/2`. That unchanged identifier is not
|
|
676
|
+
backward compatibility with an older reader. There is no `rapier-ledger/3`, no
|
|
677
|
+
compatibility reader, and no migration. An older reader rejects the empty physical
|
|
678
|
+
witness with `ledger_invalid` and does not mutate the ledger. Mixed-version sync throws
|
|
679
|
+
`notes_history_unavailable` (`the note and its canonical history cannot be proved`).
|
|
680
|
+
The older participant's store snapshot and remote object count stay unchanged. That
|
|
681
|
+
throw is not a per-note `held` or `caughtUp` result. The same format string is not
|
|
682
|
+
interoperability, and the ledger is not rewritten into the older shape. Reactivating
|
|
683
|
+
any author of that replacement checks the shared text again; undoing that author
|
|
684
|
+
withdraws it once no author remains. A different overlap returns `text_overlap_unpreserved` and the caller's exact
|
|
685
|
+
ledger, including the text merge's alternatives for review, and does not mint a synthetic
|
|
686
|
+
act. Different Will `keep` regions require conflict review even when one copy was
|
|
644
687
|
unchanged. `review.required` names an unresolved result; a UI must not auto-accept
|
|
645
688
|
it. Rapier offers carried-copy imports from Compare through its existing review,
|
|
646
689
|
checks the live document again at commit, and installs the proven ledger before
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Container admission and a single-frame carrier, not a pixel/LZW decoder.
|
|
3
|
+
// Original bytes remain the asset. Native ImageDecoder owns frame composition.
|
|
4
|
+
export function inspectGIF(bytes) {
|
|
5
|
+
const fail = message => {throw Object.assign(new Error(message), {code: 'GIF_ADMISSION'});};
|
|
6
|
+
if (!(bytes instanceof Uint8Array) || bytes.length < 13 || bytes.length > 16 * 1024 * 1024 ||
|
|
7
|
+
!/^GIF8[79]a$/.test(String.fromCharCode(...bytes.subarray(0, 6)))) fail('Invalid GIF header or byte budget');
|
|
8
|
+
const u16 = at => bytes[at] + bytes[at + 1] * 256;
|
|
9
|
+
const width = u16(6), height = u16(8);
|
|
10
|
+
if (!width || !height || width > 16384 || height > 16384 || width * height > 24000000) fail('GIF dimensions exceed the image budget');
|
|
11
|
+
let at = 13, frames = 0, control = null, first = null, duration = 0;
|
|
12
|
+
const need = n => {if (at + n > bytes.length) fail('Truncated GIF');};
|
|
13
|
+
if (bytes[10] & 128) at += 3 * (2 << (bytes[10] & 7));
|
|
14
|
+
need(0); const prefix = at;
|
|
15
|
+
const blocks = () => {for (;;) {need(1); const n = bytes[at++]; need(n); at += n; if (!n) break;}};
|
|
16
|
+
while (at < bytes.length) {
|
|
17
|
+
const start = at, code = bytes[at++];
|
|
18
|
+
if (code === 0x3b) {
|
|
19
|
+
if (!first) fail('GIF contains no image');
|
|
20
|
+
const still = new Uint8Array(prefix + (first.control?.length || 0) + first.image.length + 1);
|
|
21
|
+
still.set(bytes.subarray(0, prefix)); let offset = prefix;
|
|
22
|
+
if (first.control) {still.set(first.control, offset); offset += first.control.length;}
|
|
23
|
+
still.set(first.image, offset); still[still.length - 1] = 0x3b;
|
|
24
|
+
return {type: 'image/gif', width, height, displayWidth: width, displayHeight: height, orientation: 1,
|
|
25
|
+
frames, animated: frames > 1, duration, first: still};
|
|
26
|
+
}
|
|
27
|
+
if (code === 0x21) {
|
|
28
|
+
need(1); const label = bytes[at++];
|
|
29
|
+
if (label === 0xf9) {
|
|
30
|
+
need(6); if (bytes[at] !== 4 || bytes[at + 5] !== 0 || (bytes[at + 1] >> 2 & 7) > 3) fail('Invalid GIF frame control');
|
|
31
|
+
control = bytes.subarray(start, at + 6); at += 6;
|
|
32
|
+
} else {blocks(); if (label === 1) fail('GIF plain-text rendering is unsupported');}
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (code !== 0x2c) fail('Invalid GIF record');
|
|
36
|
+
need(9);
|
|
37
|
+
const x = u16(at), y = u16(at + 2), w = u16(at + 4), h = u16(at + 6), packed = bytes[at + 8];
|
|
38
|
+
if (!w || !h || x + w > width || y + h > height) fail('Invalid GIF frame bounds');
|
|
39
|
+
at += 9; if (packed & 128) at += 3 * (2 << (packed & 7));
|
|
40
|
+
need(1); if (bytes[at] < 2 || bytes[at] > 8) fail('Invalid GIF code size');
|
|
41
|
+
at++; blocks(); frames++;
|
|
42
|
+
// A delay of 0 or 1 centisecond shows for 100 ms, as the browser draws it (images/playback.mjs times frames the same).
|
|
43
|
+
const delay = control ? control[4] + control[5] * 256 : 0;
|
|
44
|
+
duration += delay > 1 ? delay * 10 : 100;
|
|
45
|
+
if (frames > 240 || width * height * frames > 96000000 || duration > 120000) fail('GIF animation exceeds the frame, work or duration budget');
|
|
46
|
+
first ||= {control, image: bytes.subarray(start, at)}; control = null;
|
|
47
|
+
}
|
|
48
|
+
fail('GIF trailer is missing');
|
|
49
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Shared, self-contained playback for the host and its opaque HTML frames.
|
|
3
|
+
// No source/history authority; at most one decoder and one returned frame per object.
|
|
4
|
+
export function createMediaPlayback(win, inspectGIF) {
|
|
5
|
+
const active = new Map(), encode = bytes => bytes.toBase64();
|
|
6
|
+
// Base64 decodes in 1 MiB slices with a task yield between them, so a large animation never holds the main thread.
|
|
7
|
+
const bytes = async url => {
|
|
8
|
+
const text = url.slice(url.indexOf(',') + 1), slice = 1 << 20;
|
|
9
|
+
const out = new Uint8Array(text.length / 4 * 3 - (text.endsWith('==') ? 2 : text.endsWith('=') ? 1 : 0));
|
|
10
|
+
if (out.length > 16 * 1024 * 1024) throw new Error('Animation exceeds the byte budget');
|
|
11
|
+
for (let i = 0, at = 0; i < text.length; i += slice) {
|
|
12
|
+
if (i) await new Promise(resolve => win.setTimeout(resolve, 0));
|
|
13
|
+
at += out.subarray(at).setFromBase64(text.slice(i, i + slice)).written;
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
};
|
|
17
|
+
function raster(url, canvas, changed = () => {}, dimensions = null) {
|
|
18
|
+
// setFromBase64 refuses any character outside base64 while decoding.
|
|
19
|
+
if (url.length > 24 * 1024 * 1024 || !/^data:image\/(gif|jxl);base64,/i.test(url) || (url.length - url.indexOf(',') - 1) % 4) throw new Error('Unsupported animation source');
|
|
20
|
+
const type = url.slice(5, url.indexOf(';')).toLowerCase();
|
|
21
|
+
let data = null, info = null, size = null, decoder = null, timer = 0, closed = false, paused = true, ticket = 0, next = 0, loops = 0, duration = 0, single = false, held = false;
|
|
22
|
+
const state = {animated: true, status: 'loading', first: '', frames: 0};
|
|
23
|
+
const admitted = (async () => {
|
|
24
|
+
const buffer = await bytes(url); info = type === 'image/gif' ? inspectGIF(buffer) : null;
|
|
25
|
+
size = info || dimensions;
|
|
26
|
+
if (!size || !Number.isInteger(size.width) || !Number.isInteger(size.height) || size.width < 1 || size.height < 1 ||
|
|
27
|
+
size.width > 16384 || size.height > 16384 || size.width * size.height > 24000000) throw new Error('Animation requires bounded inspected dimensions');
|
|
28
|
+
data = buffer; state.animated = info?.animated ?? true; state.frames = info?.frames || 0;
|
|
29
|
+
if (info) state.first = 'data:image/gif;base64,' + encode(info.first);
|
|
30
|
+
if (!closed) changed(state);
|
|
31
|
+
})();
|
|
32
|
+
admitted.catch(error => {if (!closed) {state.status = 'admission-failed'; state.error = String(error.message).slice(0, 160); changed(state);}});
|
|
33
|
+
// A decoder closes only after its in-flight decode settles; Chrome 155's JPEG XL decoder fails when closed mid-decode.
|
|
34
|
+
// Its budget place goes with it: a poster or a finished loop holds no decoder.
|
|
35
|
+
let inflight = null;
|
|
36
|
+
const closeDecoder = () => {ticket++; win.clearTimeout(timer); timer = 0; active.delete(canvas); const old = decoder; decoder = null; if (old) inflight ? inflight.then(() => old.close()) : old.close();};
|
|
37
|
+
const settle = async promise => {
|
|
38
|
+
let timeout;
|
|
39
|
+
try {return await Promise.race([promise, new Promise((_, reject) => {timeout = win.setTimeout(() => reject(new Error('Animation decode timeout')), 5000);})]);}
|
|
40
|
+
finally {win.clearTimeout(timeout);}
|
|
41
|
+
};
|
|
42
|
+
async function step() {
|
|
43
|
+
if (closed || paused) return;
|
|
44
|
+
const stamp = ticket;
|
|
45
|
+
try {await admitted;} catch {return;}
|
|
46
|
+
try {
|
|
47
|
+
if (!win.ImageDecoder || !await win.ImageDecoder.isTypeSupported(type)) {
|
|
48
|
+
state.status = 'playback-unavailable'; paused = true; changed(state); return;
|
|
49
|
+
}
|
|
50
|
+
if (closed || paused || stamp !== ticket) return;
|
|
51
|
+
if (!decoder) {
|
|
52
|
+
if ([...active].reduce((sum, [other, pixels]) => sum + (other === canvas ? 0 : pixels), 0) + size.width * size.height > 24000000 || !active.has(canvas) && active.size >= 8) {
|
|
53
|
+
state.status = 'playback-budget'; paused = true; changed(state); return;
|
|
54
|
+
}
|
|
55
|
+
active.set(canvas, size.width * size.height);
|
|
56
|
+
decoder = new win.ImageDecoder({data, type, preferAnimation: true});
|
|
57
|
+
const current = decoder; await settle(current.tracks.ready);
|
|
58
|
+
if (closed || paused || stamp !== ticket || decoder !== current) return;
|
|
59
|
+
const track = current.tracks.selectedTrack;
|
|
60
|
+
if (!track || track.frameCount < 1 || track.frameCount > 240 || info && track.frameCount !== info.frames) throw new Error('Animation frame budget or container mismatch');
|
|
61
|
+
state.frames = track.frameCount; state.animated = track.animated;
|
|
62
|
+
}
|
|
63
|
+
const current = decoder, decoding = current.decode({frameIndex: next, completeFramesOnly: true});
|
|
64
|
+
const settled = inflight = decoding.then(() => {}, () => {}).then(() => {if (inflight === settled) inflight = null;});
|
|
65
|
+
decoding.then(result => {if (closed || stamp !== ticket) result.image.close();}, () => {});
|
|
66
|
+
const result = await settle(decoding);
|
|
67
|
+
const frame = result.image;
|
|
68
|
+
try {
|
|
69
|
+
if (closed || paused || stamp !== ticket || decoder !== current) return;
|
|
70
|
+
const w = frame.displayWidth, h = frame.displayHeight;
|
|
71
|
+
if (!result.complete || w !== size.width || h !== size.height || w * h * state.frames > 96000000) throw new Error('Decoded animation exceeds the image budget');
|
|
72
|
+
if (canvas.width !== w) canvas.width = w;
|
|
73
|
+
if (canvas.height !== h) canvas.height = h;
|
|
74
|
+
const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, w, h); ctx.drawImage(frame, 0, 0);
|
|
75
|
+
if (!state.first && next === 0) state.first = canvas.toDataURL('image/png');
|
|
76
|
+
// A frame of 10 ms or less shows for 100 ms, as the browser draws an <img> (images/gif.mjs counts the same).
|
|
77
|
+
const delay = frame.duration > 10000 ? Math.max(20, frame.duration / 1000) : 100;
|
|
78
|
+
duration += delay; if (duration > 120000) throw new Error('Animation exceeds the duration budget');
|
|
79
|
+
state.status = 'ready'; state.frame = next; changed(state);
|
|
80
|
+
if (single) {paused = true; closeDecoder(); return;}
|
|
81
|
+
next++;
|
|
82
|
+
if (next === state.frames) {
|
|
83
|
+
next = 0; duration = 0; loops++;
|
|
84
|
+
if (!state.animated || Number.isFinite(current.tracks.selectedTrack.repetitionCount) && loops > current.tracks.selectedTrack.repetitionCount) {paused = true; closeDecoder(); return;}
|
|
85
|
+
}
|
|
86
|
+
timer = win.setTimeout(step, delay);
|
|
87
|
+
} finally {frame.close();}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (closed || stamp !== ticket) return;
|
|
90
|
+
state.status = 'decode-failed'; state.error = String(error.message).slice(0, 160); paused = true; closeDecoder(); changed(state);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return {state, play() {if (!closed && paused && !['decode-failed', 'admission-failed'].includes(state.status)) {paused = false; void step();}},
|
|
94
|
+
// A suspension that arrives during the one-frame decode (an offscreen host's first visibility report) waits for it.
|
|
95
|
+
async first() {if (!state.first && !closed) {single = true; next = 0; paused = false; try {await step();} finally {single = false; paused = true; closeDecoder(); if (held) {held = false; this.suspend();}}} return state.first;},
|
|
96
|
+
// Pause keeps the decoder and its frame position; only suspension and removal release it.
|
|
97
|
+
pause() {paused = true; ticket++; win.clearTimeout(timer); timer = 0;},
|
|
98
|
+
suspend() {if (single) {held = true; return;} paused = true; closeDecoder(); canvas.width = canvas.height = 0; state.status = 'suspended'; changed(state);},
|
|
99
|
+
close() {closed = true; paused = true; closeDecoder(); canvas.width = canvas.height = 0;},
|
|
100
|
+
get paused() {return paused;}};
|
|
101
|
+
}
|
|
102
|
+
return {raster, inspectGIF, encode, source: '(' + createMediaPlayback.toString() + ')(window, ' + inspectGIF.toString() + ')'};
|
|
103
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
# Wrap around a silhouette
|
|
3
3
|
|
|
4
|
-
This paragraph comes before the picture and stays the anchor whenever the block right after the picture cannot be one -- another image paragraph, or a
|
|
4
|
+
This paragraph comes before the picture and stays the anchor whenever the block right after the picture cannot be one -- another image paragraph, or a table -- so the words below keep their own separate life.
|
|
5
5
|
|
|
6
6
|
![A silhouette picture][diamond] <!--md-layout:v1 width=35% wrap=around x=20%-->
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
# Wrap box
|
|
3
3
|
|
|
4
|
-
This paragraph comes before the picture and stays the anchor whenever the block right after the picture cannot be one -- another image paragraph, or a
|
|
4
|
+
This paragraph comes before the picture and stays the anchor whenever the block right after the picture cannot be one -- another image paragraph, or a table -- so the words below keep their own separate life.
|
|
5
5
|
|
|
6
6
|
![A tight-box picture][opaque] <!--md-layout:v1 width=35% wrap=box x=75%-->
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
# Behind
|
|
3
3
|
|
|
4
|
-
This paragraph comes before the picture and stays the anchor whenever the block right after the picture cannot be one -- another image paragraph, or a
|
|
4
|
+
This paragraph comes before the picture and stays the anchor whenever the block right after the picture cannot be one -- another image paragraph, or a table -- so the words below keep their own separate life.
|
|
5
5
|
|
|
6
6
|
![A behind picture][opaque] <!--md-layout:v1 width=35% wrap=behind x=50%-->
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
# Front
|
|
3
3
|
|
|
4
|
-
This paragraph comes before the picture and stays the anchor whenever the block right after the picture cannot be one -- another image paragraph, or a
|
|
4
|
+
This paragraph comes before the picture and stays the anchor whenever the block right after the picture cannot be one -- another image paragraph, or a table -- so the words below keep their own separate life.
|
|
5
5
|
|
|
6
6
|
![A front picture][opaque] <!--md-layout:v1 width=35% wrap=front x=50%-->
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
# Rotate a raster
|
|
3
3
|
|
|
4
|
-
This paragraph comes before the picture and stays the anchor whenever the block right after the picture cannot be one -- another image paragraph, or a
|
|
4
|
+
This paragraph comes before the picture and stays the anchor whenever the block right after the picture cannot be one -- another image paragraph, or a table -- so the words below keep their own separate life.
|
|
5
5
|
|
|
6
6
|
![A turned raster picture][opaque] <!--md-layout:v1 width=35% wrap=box x=75% rotate=25deg-->
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
# A drawing with its own rotation
|
|
3
3
|
|
|
4
|
-
This paragraph comes before the picture and stays the anchor whenever the block right after the picture cannot be one -- another image paragraph, or a
|
|
4
|
+
This paragraph comes before the picture and stays the anchor whenever the block right after the picture cannot be one -- another image paragraph, or a table -- so the words below keep their own separate life.
|
|
5
5
|
|
|
6
6
|
![A drawing already turned in its own bytes][rotated] <!--md-layout:v1 width=35% wrap=around x=75%-->
|
|
7
7
|
|
|
@@ -60,40 +60,34 @@
|
|
|
60
60
|
"bottom": 153.98
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
|
-
"left":
|
|
64
|
-
"top": 154,
|
|
65
|
-
"right": 61.91,
|
|
66
|
-
"bottom": 184.8
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
"left": 115.08,
|
|
63
|
+
"left": 103.98,
|
|
70
64
|
"top": 154,
|
|
71
|
-
"right":
|
|
65
|
+
"right": 354,
|
|
72
66
|
"bottom": 184.8
|
|
73
67
|
},
|
|
74
68
|
{
|
|
75
69
|
"left": 0,
|
|
76
70
|
"top": 184.8,
|
|
77
71
|
"right": 354,
|
|
78
|
-
"bottom": 215.
|
|
72
|
+
"bottom": 215.6
|
|
79
73
|
},
|
|
80
74
|
{
|
|
81
75
|
"left": 0,
|
|
82
|
-
"top": 215.
|
|
76
|
+
"top": 215.6,
|
|
83
77
|
"right": 354,
|
|
84
|
-
"bottom": 246.
|
|
78
|
+
"bottom": 246.4
|
|
85
79
|
},
|
|
86
80
|
{
|
|
87
81
|
"left": 0,
|
|
88
|
-
"top": 246.
|
|
82
|
+
"top": 246.4,
|
|
89
83
|
"right": 354,
|
|
90
|
-
"bottom": 277.
|
|
84
|
+
"bottom": 277.2
|
|
91
85
|
},
|
|
92
86
|
{
|
|
93
87
|
"left": 0,
|
|
94
|
-
"top": 277.
|
|
88
|
+
"top": 277.2,
|
|
95
89
|
"right": 354,
|
|
96
|
-
"bottom":
|
|
90
|
+
"bottom": 308
|
|
97
91
|
},
|
|
98
92
|
{
|
|
99
93
|
"left": 0,
|
|
@@ -105,13 +99,19 @@
|
|
|
105
99
|
"left": 0,
|
|
106
100
|
"top": 338.8,
|
|
107
101
|
"right": 354,
|
|
108
|
-
"bottom": 369.
|
|
102
|
+
"bottom": 369.6
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"left": 0,
|
|
106
|
+
"top": 369.6,
|
|
107
|
+
"right": 354,
|
|
108
|
+
"bottom": 400.4
|
|
109
109
|
},
|
|
110
110
|
{
|
|
111
111
|
"left": 0,
|
|
112
|
-
"top":
|
|
112
|
+
"top": 400.4,
|
|
113
113
|
"right": 354,
|
|
114
|
-
"bottom":
|
|
114
|
+
"bottom": 431.2
|
|
115
115
|
}
|
|
116
116
|
],
|
|
117
117
|
"glyphWidths": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
# An unfilled circle's interior
|
|
3
3
|
|
|
4
|
-
This paragraph comes before the picture and stays the anchor whenever the block right after the picture cannot be one -- another image paragraph, or a
|
|
4
|
+
This paragraph comes before the picture and stays the anchor whenever the block right after the picture cannot be one -- another image paragraph, or a table -- so the words below keep their own separate life.
|
|
5
5
|
|
|
6
6
|
![A ring drawing with an empty interior][ring] <!--md-layout:v1 width=45% wrap=around x=25%-->
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
# Both-sides wrapping
|
|
3
3
|
|
|
4
|
-
This paragraph comes before the picture and stays the anchor whenever the block right after the picture cannot be one -- another image paragraph, or a
|
|
4
|
+
This paragraph comes before the picture and stays the anchor whenever the block right after the picture cannot be one -- another image paragraph, or a table -- so the words below keep their own separate life.
|
|
5
5
|
|
|
6
6
|
![A centred picture][opaque] <!--md-layout:v1 width=22% wrap=around x=50%-->
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
# The neighbour search skips another image paragraph
|
|
3
3
|
|
|
4
|
-
This paragraph comes before the picture and stays the anchor whenever the block right after the picture cannot be one -- another image paragraph, or a
|
|
4
|
+
This paragraph comes before the picture and stays the anchor whenever the block right after the picture cannot be one -- another image paragraph, or a table -- so the words below keep their own separate life.
|
|
5
5
|
|
|
6
6
|
![The picture under test][diamond] <!--md-layout:v1 width=35% wrap=around x=20%-->
|
|
7
7
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
|
|
2
|
-
# The neighbour search stops at a
|
|
2
|
+
# The neighbour search stops at a rule
|
|
3
3
|
|
|
4
4
|
This paragraph comes before the picture and stays the anchor whenever the block right after the picture cannot be one -- another image paragraph, or a heading -- so the words below keep their own separate life.
|
|
5
5
|
|
|
6
6
|
![The picture under test][opaque] <!--md-layout:v1 width=35% wrap=around x=75%-->
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
***
|
|
9
9
|
|
|
10
10
|
This paragraph comes after the picture and is its ordinary anchor whenever nothing else stands in the way, wrapping beside it line by line the way the fixture is built to show plainly.
|
|
11
11
|
|
|
@@ -5,10 +5,12 @@ standard](https://rapier.website/markdown-standard): inline (no layout comment),
|
|
|
5
5
|
with a real alpha silhouette, `wrap=box`, `behind`, `front`, `rotate=` on a plain raster, a foreign
|
|
6
6
|
SVG "drawing" already turned in its own bytes, a ring drawing whose unfilled interior takes a line of
|
|
7
7
|
text, both-sides wrapping around a centred picture, the neighbour search skipping a second, unrelated
|
|
8
|
-
picture, a
|
|
8
|
+
picture, a rule after a picture (a barrier, so the paragraph before it anchors), and RTL (Arabic) text.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
The fixtures were captured at a 30.8 px line (`measurer.lineHeight`, 1.75 × the 17.6 px body); the reference
|
|
11
|
+
scale's line is now 28 px at M. The line height is an input to the comparison, so the recorded lines stay
|
|
12
|
+
self-consistent. In `01-inline` and `12-rule-barrier` the sentence "another image paragraph, or a heading" is
|
|
13
|
+
measured anchor text and stays as recorded; headings do anchor pictures.
|
|
12
14
|
|
|
13
15
|
## What each document's JSON carries
|
|
14
16
|
|
|
@@ -59,7 +61,10 @@ anchor; that historical measurement is not a normative neighbour-selection expec
|
|
|
59
61
|
|
|
60
62
|
## The expectations
|
|
61
63
|
|
|
62
|
-
`<name>.expected.json` is compared against the exported page's own reflow within 1.5 px; a missing one is a failure.
|
|
64
|
+
`<name>.expected.json` is compared against the exported page's own reflow within 1.5 px; a missing one is a failure.
|
|
65
|
+
Rows 5–13 of `09-drawing-ring-interior` come from the kit's own planner with the recorded measurer: the word-deferral
|
|
66
|
+
rule moves "ordinary" whole into the ring's wide slot, and the exported page wraps with a native `shape-outside` float,
|
|
67
|
+
which exposes no per-slot line boxes to record. `profileHint` is hand-computed from how each fixture's picture bytes were built ("Generation notes" below) and is restored by hand if a file is regenerated.
|
|
63
68
|
|
|
64
69
|
## Running the comparison
|
|
65
70
|
|
|
@@ -72,31 +77,17 @@ Prints two separate tables (preservation and presentation are different claims):
|
|
|
72
77
|
|
|
73
78
|
- **Preservation** — does each fixture's own layout comment round-trip exactly through
|
|
74
79
|
`parseLayout`/`formatLayout`. No measurer, no font: either exactly right, or a real bug. 12/12.
|
|
75
|
-
- **Presentation** — does the planned line count and each line's left/right
|
|
76
|
-
fact, within `--tolerance` pixels (default 2). This depends entirely on the measurer supplied;
|
|
80
|
+
- **Presentation** — does the planned line count, each flowed line's row (its top) and each line's left/right
|
|
81
|
+
agree with the recorded fact, within `--tolerance` pixels (default 2). A wrong row is DIFFERS. This depends entirely on the measurer supplied;
|
|
77
82
|
`run.mjs`'s own default (`measurer.mjs`) replays Rapier's real Pretext trace wherever one was
|
|
78
|
-
captured, falling back to the character-glyph sum only where none exists.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
left-edge delta (9.64px) that is a picture-obstacle *geometry* fact, not a text one: their
|
|
87
|
-
`profileHint` is a hand-computed, quantized alpha-band descriptor for a picture Rapier's own export
|
|
88
|
-
never serializes an occupancy descriptor for, and that descriptor's own precision — not the
|
|
89
|
-
measurer — is the residual gap. `07-rotate-raster` and `08-drawing-rotation` have right-edge geometry deltas of 4.48px and 5.31px, with no measurer calls. `09-drawing-ring-interior` plans the recorded 14 lines, but one row
|
|
90
|
-
differs by a whole slot (115.08px left, 292.09px right): its recorded trace, taken before a word
|
|
91
|
-
that must break inside itself was given the widest slot of its row, cuts "ordinary" into "ordinar"
|
|
92
|
-
and "y" in the ring's narrow left slot, where the planner now leaves that slot empty and sets the
|
|
93
|
-
word whole in the wide one (`layout/line-plan.mjs`). Its `expected.json` still holds the earlier cut;
|
|
94
|
-
it is regenerated from a browser export, not by hand. Before this
|
|
95
|
-
measurer existed (a per-character glyph-sum with no real trace at all), the same run reported 4
|
|
96
|
-
AGREE / 8 CLOSE / 1 DIFFERS — `13-rtl-text` disagreed by a whole line (7 expected, 8 planned): real
|
|
97
|
-
Arabic shaping (ligatures, diacritic combination) changes a run's total measured width in a way an
|
|
98
|
-
isolated-character sum cannot reproduce, and that was enough to move one word to the next line. The
|
|
99
|
-
real trace above fixes exactly that: `13-rtl-text` now plans the correct 7 lines.
|
|
83
|
+
captured, falling back to the character-glyph sum only where none exists. Result: **3/13
|
|
84
|
+
AGREE outright** (`04-wrap-box`, `10-both-sides`, `12-rule-barrier`, each a case where the compared edge is
|
|
85
|
+
pure obstacle-slot geometry), **10 CLOSE, 0 DIFFERS**. `01-inline`, `02-width-x-align`, `05-behind` and `06-front`
|
|
86
|
+
have no real trace (see `measurements` above) and rest on the character-sum fallback. `03-wrap-around-silhouette`,
|
|
87
|
+
`11-neighbour-skips-image` and `13-rtl-text` run Pretext for real yet carry a constant 9.64px left-edge delta, a
|
|
88
|
+
picture-obstacle geometry fact: their `profileHint` is a hand-computed, quantized alpha-band descriptor. `09` carries
|
|
89
|
+
7.8px of the same kind. `07-rotate-raster` and `08-drawing-rotation` have right-edge geometry deltas of 4.48px and
|
|
90
|
+
5.31px, with no measurer calls.
|
|
100
91
|
|
|
101
92
|
## Generation notes (how the pictures were built)
|
|
102
93
|
|
|
@@ -77,18 +77,22 @@ for (const expected of documents) {
|
|
|
77
77
|
if (!plan) { rows.push({document: expected.document, verdict: 'ERROR', detail: 'flowLines returned null (no plan fits)'}); continue; }
|
|
78
78
|
|
|
79
79
|
const lineCountAgrees = plan.lines.length === expected.lines.length;
|
|
80
|
-
let maxLeftDelta = 0, maxRightDelta = 0;
|
|
80
|
+
let maxLeftDelta = 0, maxRightDelta = 0, maxTopDelta = 0;
|
|
81
81
|
for (let i = 0; i < Math.min(plan.lines.length, expected.lines.length); i++) {
|
|
82
82
|
const got = plan.lines[i], want = expected.lines[i];
|
|
83
|
+
// Flowed lines are line boxes; an unflowed line's recorded top is its glyphs' own.
|
|
84
|
+
if (expected.anchorFlowed) maxTopDelta = Math.max(maxTopDelta, Math.abs(got.y - want.top));
|
|
83
85
|
// Flowed lines compare slot geometry (x+width); unflowed lines compare the measured tight extent.
|
|
84
86
|
const right = expected.anchorFlowed ? got.x + got.width : got.x + measurer.measure(font, got.fragments.map(f => f.text).join(''));
|
|
85
87
|
maxLeftDelta = Math.max(maxLeftDelta, Math.abs(got.x - want.left));
|
|
86
88
|
maxRightDelta = Math.max(maxRightDelta, Math.abs(right - want.right));
|
|
87
89
|
}
|
|
88
|
-
|
|
90
|
+
// A line that keeps its edges but lands on another row is a different layout.
|
|
91
|
+
const rowsAgree = maxTopDelta <= args.tolerance;
|
|
92
|
+
const withinTolerance = lineCountAgrees && rowsAgree && maxLeftDelta <= args.tolerance && maxRightDelta <= args.tolerance;
|
|
89
93
|
rows.push({
|
|
90
94
|
document: expected.document,
|
|
91
|
-
verdict: withinTolerance ? 'AGREE' : lineCountAgrees ? 'CLOSE' : 'DIFFERS',
|
|
95
|
+
verdict: withinTolerance ? 'AGREE' : lineCountAgrees && rowsAgree ? 'CLOSE' : 'DIFFERS',
|
|
92
96
|
expectedLines: expected.lines.length, gotLines: plan.lines.length,
|
|
93
97
|
maxLeftDelta: Math.round(maxLeftDelta * 100) / 100, maxRightDelta: Math.round(maxRightDelta * 100) / 100,
|
|
94
98
|
hits: measurer.stats.hits - before.hits, misses: measurer.stats.misses - before.misses,
|
|
@@ -109,16 +113,14 @@ console.log(`\n${agree}/${rows.length} documents AGREE within ${args.tolerance}p
|
|
|
109
113
|
console.log(`Measurer: ${measurer.stats.hits} queries answered from Rapier's own real browser trace, ${measurer.stats.misses} fell back to a character-glyph sum (no kerning/shaping).`);
|
|
110
114
|
console.log(`A row's own hits/misses can undercount its real coverage: several fixtures share one identical anchor sentence, and Pretext's own per-process`);
|
|
111
115
|
console.log(`segment cache (agent/vendor/pretext/measurement.js) serves a repeated (font, segment) query straight from that cache without calling`);
|
|
112
|
-
console.log(`measureText again at all -- real data, just not counted against the SECOND document to ask for it.
|
|
116
|
+
console.log(`measureText again at all -- real data, just not counted against the SECOND document to ask for it. Two named, separate reasons a row still`);
|
|
113
117
|
console.log(`reads CLOSE rather than AGREE, none of them "close enough": (a) no real trace exists at all for 01-inline/02-width-x-align/05-behind/06-front`);
|
|
114
|
-
console.log(`-- their picture pushes no flow obstacle, so
|
|
118
|
+
console.log(`-- their picture pushes no flow obstacle, so kit/render.mjs's exported projector's prepare() never calls Pretext for this paragraph during export (see`);
|
|
115
119
|
console.log(`measurer.mjs) -- the from-scratch character-sum approximation disagrees with real shaping by a bounded, named amount; (b) 03/11/13 DO run`);
|
|
116
120
|
console.log(`Pretext for real (hits > 0, 13-rtl-text at 100% -- every query answered from the real trace) yet still carry a small, constant left-edge delta`);
|
|
117
121
|
console.log(`(9.64px today, a constant from before this measurer existed) -- a picture-obstacle geometry fact, not a text one: these`);
|
|
118
122
|
console.log(`fixtures' profileHint is a hand-computed, quantized alpha band descriptor for a raster Rapier's own export never serialized an occupancy`);
|
|
119
|
-
console.log(`descriptor for (conformance/README.md, "profileHint"), and that descriptor's own precision -- not this lane's measurer -- is the residual gap;
|
|
120
|
-
console.log(`differs by a whole slot: its recorded trace cuts a word in the ring's narrow left slot, and the planner now gives a word that must break inside itself`);
|
|
121
|
-
console.log(`the widest slot of its row (layout/line-plan.mjs) -- the recorded lines are the earlier planner's.`);
|
|
123
|
+
console.log(`descriptor for (conformance/README.md, "profileHint"), and that descriptor's own precision -- not this lane's measurer -- is the residual gap; 09's 7.8px rows are the same kind of fact.`);
|
|
122
124
|
if (measurer.stats.missedQueries.length) {
|
|
123
125
|
console.log(`\nFirst ${Math.min(10, measurer.stats.missedQueries.length)} of ${measurer.stats.misses} missed (font, text) queries (fell back to the character sum):`);
|
|
124
126
|
for (const {font, text} of measurer.stats.missedQueries.slice(0, 10)) console.log(` ${JSON.stringify(text)} (${font})`);
|