rapier-markdown-kit 1.1.102 → 1.2.3
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 +5 -2
- package/README.md +245 -54
- package/dist/agent/vendor/hyphen-en-us/LICENSE +14 -0
- package/dist/agent/vendor/hyphen-en-us/SOURCE.json +33 -0
- package/dist/agent/vendor/hyphen-en-us/hyph-en-us.hyp.txt +14 -0
- package/dist/agent/vendor/hyphen-en-us/hyph-en-us.pat.txt +4938 -0
- package/dist/agent/vendor/hyphen-en-us/patterns.js +25 -0
- package/dist/agent/vendor/pretext/LICENSE +208 -0
- package/dist/agent/vendor/pretext/NOTICE +26 -13
- package/dist/agent/vendor/pretext/SOURCE.json +213 -45
- package/dist/agent/vendor/pretext/analysis.js +675 -1224
- package/dist/agent/vendor/pretext/entry-geometry.js +183 -0
- package/dist/agent/vendor/pretext/gecko-line-breaks.js +688 -0
- package/dist/agent/vendor/pretext/generated/engine-break-data.js +57 -0
- package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +80 -0
- package/dist/agent/vendor/pretext/graphemes.js +98 -0
- package/dist/agent/vendor/pretext/han-kerning.js +253 -0
- package/dist/agent/vendor/pretext/layout.js +352 -290
- package/dist/agent/vendor/pretext/line-break.js +1154 -588
- package/dist/agent/vendor/pretext/line-breaks.js +892 -0
- package/dist/agent/vendor/pretext/line-text.js +81 -39
- package/dist/agent/vendor/pretext/measurement.js +1105 -176
- package/dist/agent/vendor/pretext/prepare.js +807 -0
- package/dist/agent/vendor/pretext/rich-inline.js +1242 -273
- package/dist/agent/will.mjs +2 -0
- package/dist/images/gif.mjs +47 -0
- package/dist/images/playback.mjs +101 -0
- package/dist/kit/assets.mjs +5 -1
- 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 +13 -11
- package/dist/kit/export-fonts.mjs +26 -11
- package/dist/kit/index.mjs +1 -0
- package/dist/kit/ledger/carried.mjs +17 -10
- package/dist/kit/ledger/data.mjs +73 -13
- package/dist/kit/ledger/format.mjs +176 -54
- package/dist/kit/ledger/hash.mjs +91 -18
- package/dist/kit/ledger/history.mjs +156 -116
- package/dist/kit/ledger/inverse-source.mjs +158 -19
- package/dist/kit/ledger/journal-records.mjs +292 -67
- package/dist/kit/ledger/merge.mjs +4 -3
- package/dist/kit/model.mjs +1 -1
- package/dist/kit/pdf-glyphs.mjs +4 -4
- package/dist/kit/pdf-vectors.mjs +4 -1
- package/dist/kit/rapier-ledger.mjs +2 -2
- package/dist/kit/render-ink.mjs +1 -1
- package/dist/kit/render-markdown.mjs +400 -148
- package/dist/kit/render-print.mjs +131 -55
- package/dist/kit/render-sanitize.mjs +30 -7
- package/dist/kit/render-work.mjs +99 -20
- package/dist/kit/render.mjs +133 -140
- package/dist/kit/typography.mjs +2 -0
- package/dist/layout/hyphenation.mjs +127 -0
- package/dist/layout/line-plan.mjs +88 -96
- package/dist/layout/model.mjs +71 -88
- package/dist/layout/paragraph-fit.mjs +191 -0
- package/dist/layout/preparation.mjs +373 -0
- package/dist/layout/prepared-paragraph.mjs +223 -0
- package/dist/layout/typography.mjs +345 -111
- package/dist/packages/rapier-diff/index.mjs +359 -224
- package/dist/spec/document-fonts.mjs +14 -5
- package/dist/spec/document-settings.mjs +11 -164
- package/dist/spec/html-blocks.mjs +618 -79
- package/dist/spec/md-assets.mjs +178 -36
- package/dist/spec/md-layout.mjs +13 -2
- package/dist/spec/md-marks.mjs +220 -127
- package/dist/spec/md-source.mjs +157 -0
- package/package.json +2 -1
- package/style.css +15 -50
- package/dist/agent/vendor/pretext/bidi.js +0 -151
- package/dist/agent/vendor/pretext/generated/bidi-data.js +0 -3831
package/dist/agent/will.mjs
CHANGED
|
@@ -59,6 +59,8 @@ function _rapierWillParse(text) {
|
|
|
59
59
|
const will = {
|
|
60
60
|
present: false, fault: '', faults: [], blocks: [], markers: [], regions: [], text: source,
|
|
61
61
|
};
|
|
62
|
+
// A marker, well formed or near, opens its line with one of the four spellings hasWillMarkers looks for.
|
|
63
|
+
if (!hasWillMarkers(source)) return will;
|
|
62
64
|
let open = null;
|
|
63
65
|
for (let index = 0, start = 0; start <= source.length; index++) {
|
|
64
66
|
let contentEnd = start;
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
duration += control ? Math.max(20, (control[4] + control[5] * 256) * 10) : 100;
|
|
43
|
+
if (frames > 240 || width * height * frames > 96000000 || duration > 120000) fail('GIF animation exceeds the frame, work or duration budget');
|
|
44
|
+
first ||= {control, image: bytes.subarray(start, at)}; control = null;
|
|
45
|
+
}
|
|
46
|
+
fail('GIF trailer is missing');
|
|
47
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
let inflight = null;
|
|
35
|
+
const closeDecoder = () => {ticket++; win.clearTimeout(timer); timer = 0; const old = decoder; decoder = null; if (old) inflight ? inflight.then(() => old.close()) : old.close();};
|
|
36
|
+
const settle = async promise => {
|
|
37
|
+
let timeout;
|
|
38
|
+
try {return await Promise.race([promise, new Promise((_, reject) => {timeout = win.setTimeout(() => reject(new Error('Animation decode timeout')), 5000);})]);}
|
|
39
|
+
finally {win.clearTimeout(timeout);}
|
|
40
|
+
};
|
|
41
|
+
async function step() {
|
|
42
|
+
if (closed || paused) return;
|
|
43
|
+
const stamp = ticket;
|
|
44
|
+
try {await admitted;} catch {return;}
|
|
45
|
+
try {
|
|
46
|
+
if (!win.ImageDecoder || !await win.ImageDecoder.isTypeSupported(type)) {
|
|
47
|
+
state.status = 'playback-unavailable'; paused = true; changed(state); return;
|
|
48
|
+
}
|
|
49
|
+
if (closed || paused || stamp !== ticket) return;
|
|
50
|
+
if (!decoder) {
|
|
51
|
+
if ([...active].reduce((sum, [other, pixels]) => sum + (other === canvas ? 0 : pixels), 0) + size.width * size.height > 24000000 || !active.has(canvas) && active.size >= 8) {
|
|
52
|
+
state.status = 'playback-budget'; paused = true; changed(state); return;
|
|
53
|
+
}
|
|
54
|
+
active.set(canvas, size.width * size.height);
|
|
55
|
+
decoder = new win.ImageDecoder({data, type, preferAnimation: true});
|
|
56
|
+
const current = decoder; await settle(current.tracks.ready);
|
|
57
|
+
if (closed || paused || stamp !== ticket || decoder !== current) return;
|
|
58
|
+
const track = current.tracks.selectedTrack;
|
|
59
|
+
if (!track || track.frameCount < 1 || track.frameCount > 240 || info && track.frameCount !== info.frames) throw new Error('Animation frame budget or container mismatch');
|
|
60
|
+
state.frames = track.frameCount; state.animated = track.animated;
|
|
61
|
+
}
|
|
62
|
+
const current = decoder, decoding = current.decode({frameIndex: next, completeFramesOnly: true});
|
|
63
|
+
const settled = inflight = decoding.then(() => {}, () => {}).then(() => {if (inflight === settled) inflight = null;});
|
|
64
|
+
decoding.then(result => {if (closed || stamp !== ticket) result.image.close();}, () => {});
|
|
65
|
+
const result = await settle(decoding);
|
|
66
|
+
const frame = result.image;
|
|
67
|
+
try {
|
|
68
|
+
if (closed || paused || stamp !== ticket || decoder !== current) return;
|
|
69
|
+
const w = frame.displayWidth, h = frame.displayHeight;
|
|
70
|
+
if (!result.complete || w !== size.width || h !== size.height || w * h * state.frames > 96000000) throw new Error('Decoded animation exceeds the image budget');
|
|
71
|
+
if (canvas.width !== w) canvas.width = w;
|
|
72
|
+
if (canvas.height !== h) canvas.height = h;
|
|
73
|
+
const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, w, h); ctx.drawImage(frame, 0, 0);
|
|
74
|
+
if (!state.first && next === 0) state.first = canvas.toDataURL('image/png');
|
|
75
|
+
const delay = Math.max(20, (frame.duration || 100000) / 1000);
|
|
76
|
+
duration += delay; if (duration > 120000) throw new Error('Animation exceeds the duration budget');
|
|
77
|
+
state.status = 'ready'; state.frame = next; changed(state);
|
|
78
|
+
if (single) {paused = true; closeDecoder(); return;}
|
|
79
|
+
next++;
|
|
80
|
+
if (next === state.frames) {
|
|
81
|
+
next = 0; duration = 0; loops++;
|
|
82
|
+
if (!state.animated || Number.isFinite(current.tracks.selectedTrack.repetitionCount) && loops > current.tracks.selectedTrack.repetitionCount) {paused = true; closeDecoder(); return;}
|
|
83
|
+
}
|
|
84
|
+
timer = win.setTimeout(step, delay);
|
|
85
|
+
} finally {frame.close();}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (closed || stamp !== ticket) return;
|
|
88
|
+
state.status = 'decode-failed'; state.error = String(error.message).slice(0, 160); paused = true; closeDecoder(); changed(state);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {state, play() {if (!closed && paused && !['decode-failed', 'admission-failed'].includes(state.status)) {paused = false; void step();}},
|
|
92
|
+
// A suspension that arrives during the one-frame decode (an offscreen host's first visibility report) waits for it.
|
|
93
|
+
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;},
|
|
94
|
+
// Pause keeps the decoder and its frame position; only suspension and removal release it.
|
|
95
|
+
pause() {paused = true; ticket++; win.clearTimeout(timer); timer = 0;},
|
|
96
|
+
suspend() {if (single) {held = true; return;} paused = true; closeDecoder(); canvas.width = canvas.height = 0; active.delete(canvas); state.status = 'suspended'; changed(state);},
|
|
97
|
+
close() {closed = true; paused = true; closeDecoder(); canvas.width = canvas.height = 0; active.delete(canvas);},
|
|
98
|
+
get paused() {return paused;}};
|
|
99
|
+
}
|
|
100
|
+
return {raster, inspectGIF, encode, source: '(' + createMediaPlayback.toString() + ')(window, ' + inspectGIF.toString() + ')'};
|
|
101
|
+
}
|
package/dist/kit/assets.mjs
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// Re-export from ../spec/md-assets.mjs only: ../images/assets.mjs pulls AGPL decoders into the MIT graph (tools/check-kit.mjs).
|
|
3
|
-
|
|
3
|
+
// The kit's names are listed: that module also exports what only Rapier's own parser consumers share.
|
|
4
|
+
export {IMAGE_LIMITS, configureParser, normalizeLabel, assetTitle, installMarkdownImages, markdownParser, markdownBodyOffset,
|
|
5
|
+
dataImage, parseAssets, documentAssets, packAssetIndex, restoreAssetIndex, projectImageDefinitions, blockwiseAssets,
|
|
6
|
+
imageEnvironment, referenceOccurs, mayRetireImageDefinitions, retireDeletedImageDefinitions, assetOmissions, isAssetBlock,
|
|
7
|
+
escapeImageAlt, serializeAsset, appendAssetText, appendAsset} from '../spec/md-assets.mjs';
|
|
@@ -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
|
|
|
@@ -19,7 +19,7 @@ function parseArgs(argv) {
|
|
|
19
19
|
const args = parseArgs(process.argv.slice(2));
|
|
20
20
|
const impl = await import(args.impl.startsWith('file://') ? args.impl : 'file://' + args.impl);
|
|
21
21
|
|
|
22
|
-
const need = ['parseLayout', 'formatLayout', '
|
|
22
|
+
const need = ['parseLayout', 'formatLayout', 'createPreparationScope', 'flowLines', 'pictureSlices', 'bandsProfile', 'polygonProfile', 'rasterTiltProfile', 'parseProfile', 'wrapColumnFloor'];
|
|
23
23
|
const missing = need.filter(name => typeof impl[name] !== 'function');
|
|
24
24
|
if (missing.length) { console.error('run.mjs: implementation at ' + args.impl + ' is missing: ' + missing.join(', ')); process.exit(2); }
|
|
25
25
|
|
|
@@ -34,6 +34,7 @@ function buildProfile(hint) {
|
|
|
34
34
|
const documents = loadDocuments(here);
|
|
35
35
|
// Install once over merged data: Pretext memoizes its context (measurer.mjs).
|
|
36
36
|
const measurer = installMeasurer(documents);
|
|
37
|
+
const preparation = impl.createPreparationScope();
|
|
37
38
|
|
|
38
39
|
// Preservation and presentation are separate claims; preservation is exact or a bug.
|
|
39
40
|
const preservationRows = documents.map(expected => {
|
|
@@ -62,9 +63,8 @@ const rows = [];
|
|
|
62
63
|
for (const expected of documents) {
|
|
63
64
|
const font = expected.measurer.font.replace(/\/[^ ]+ /, ' '); // canvas fonts take no line-height
|
|
64
65
|
const before = {hits: measurer.stats.hits, misses: measurer.stats.misses};
|
|
65
|
-
const
|
|
66
|
-
if (!
|
|
67
|
-
const flow = impl.prepareRichInline([{text: run.raw, font, letterSpacing: expected.measurer.letterSpacing || 0}]);
|
|
66
|
+
const flow = preparation.prepare(expected, [{text: expected.anchorText, font, letterSpacing: expected.measurer.letterSpacing || 0}]);
|
|
67
|
+
if (!flow) { rows.push({document: expected.document, verdict: 'ERROR', detail: 'rich preparation refused this font/text/letterSpacing'}); continue; }
|
|
68
68
|
let obstacles = [];
|
|
69
69
|
if (expected.picture?.rect) {
|
|
70
70
|
const profile = buildProfile(expected.profileHint);
|
|
@@ -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
118
|
console.log(`-- their picture pushes no flow obstacle, so layout/interchange.js's own 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})`);
|
|
@@ -157,13 +157,17 @@ function unicodeIncludes(face, character) {
|
|
|
157
157
|
});
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
function
|
|
160
|
+
function fontSource(bytes) {
|
|
161
161
|
const signature = String.fromCharCode(...bytes.subarray(0, 4));
|
|
162
162
|
const format = signature === 'wOFF' ? 'woff' : signature === 'wOF2' ? 'woff2' : signature === 'OTTO' ? 'opentype' : 'truetype';
|
|
163
163
|
const mime = {woff: 'woff', woff2: 'woff2', opentype: 'otf', truetype: 'ttf'}[format];
|
|
164
164
|
let binary = '';
|
|
165
165
|
for (let at = 0; at < bytes.length; at += 16384) binary += String.fromCharCode(...bytes.subarray(at, at + 16384));
|
|
166
|
-
|
|
166
|
+
return 'url("data:font/' + mime + ';base64,' + btoa(binary) + '") format("' + format + '")';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function writeFace(face, source) {
|
|
170
|
+
const properties = {...face.properties, src: source};
|
|
167
171
|
delete properties['unicode-range'];
|
|
168
172
|
return '@font-face{' + Object.entries(properties).map(([name, value]) => name + ':' + value).join(';') + '}';
|
|
169
173
|
}
|
|
@@ -291,20 +295,31 @@ export async function exportFontCss(root, {settings, css = '', fontCss = '', sub
|
|
|
291
295
|
const custom = new Set(fontFaces(customFontCss).map(face => face.family));
|
|
292
296
|
candidates = candidates.filter(face => face.family === 'geist' && !custom.has(face.family));
|
|
293
297
|
}
|
|
294
|
-
const faces = fontRuns(root, {settings, css, faces: candidates}), output = [];
|
|
298
|
+
const faces = fontRuns(root, {settings, css, faces: candidates}), output = [], prepared = new Map();
|
|
295
299
|
let payloadBytes = 0;
|
|
296
300
|
for (const face of faces) {
|
|
297
|
-
const original = Uint8Array.from(atob(face.source.slice(face.source.indexOf(',') + 1)), character => character.charCodeAt(0));
|
|
298
301
|
// Adjacent inline nodes retain shaping sequences; independent runs cannot compose together.
|
|
299
|
-
const
|
|
302
|
+
const text = face.runs.join('\n');
|
|
303
|
+
if (!prepared.has(face.source)) prepared.set(face.source, new Map());
|
|
304
|
+
const variants = prepared.get(face.source);
|
|
305
|
+
if (!variants.has(text)) {
|
|
306
|
+
const original = Uint8Array.from(atob(face.source.slice(face.source.indexOf(',') + 1)), character => character.charCodeAt(0));
|
|
307
|
+
const bytes = await subset(original, text);
|
|
308
|
+
if (portableFactory) {
|
|
309
|
+
if (!(bytes instanceof Uint8Array) || !bytes.length || bytes.length >= original.length || bytes.length > FACTORY_FONT_BYTES) return '';
|
|
310
|
+
if (typeof validateFont !== 'function' || await validateFont(Uint8Array.from(bytes), {family: 'Geist'}) !== true) return '';
|
|
311
|
+
} else if (bytes === null) { variants.set(text, null); continue; }
|
|
312
|
+
if (!(bytes instanceof Uint8Array) || !bytes.length) throw new Error('The page font subset is empty.');
|
|
313
|
+
// Retain the encoded boundary value, not additional original or subset byte buffers.
|
|
314
|
+
variants.set(text, {source: fontSource(bytes), length: bytes.length});
|
|
315
|
+
}
|
|
316
|
+
const saved = variants.get(text);
|
|
317
|
+
if (saved === null) continue;
|
|
300
318
|
if (portableFactory) {
|
|
301
|
-
|
|
302
|
-
payloadBytes += bytes.length;
|
|
319
|
+
payloadBytes += saved.length;
|
|
303
320
|
if (payloadBytes > FACTORY_FONT_BYTES) return '';
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (!(bytes instanceof Uint8Array) || !bytes.length) throw new Error('The page font subset is empty.');
|
|
307
|
-
output.push(writeFace(face, bytes));
|
|
321
|
+
}
|
|
322
|
+
output.push(writeFace(face, saved.source));
|
|
308
323
|
}
|
|
309
324
|
const result = output.join('\n');
|
|
310
325
|
return portableFactory && new TextEncoder().encode(result).length > FACTORY_FONT_CSS_BYTES ? '' : result;
|