rapier-markdown-kit 1.2.7 → 1.2.9
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/README.md +32 -30
- package/dist/agent/will.mjs +1 -6
- package/dist/images/gif.mjs +0 -3
- package/dist/images/playback.mjs +1 -10
- package/dist/kit/assets.mjs +0 -1
- package/dist/kit/conformance/measurer.mjs +0 -0
- package/dist/kit/conformance/run.mjs +1 -8
- package/dist/kit/export-fonts.mjs +0 -14
- package/dist/kit/index.mjs +1 -1
- package/dist/kit/ledger/authored-origin.mjs +0 -5
- package/dist/kit/ledger/authorship.mjs +0 -2
- package/dist/kit/ledger/carried.d.mts +20 -0
- package/dist/kit/ledger/carried.mjs +4 -7
- package/dist/kit/ledger/data.mjs +0 -8
- package/dist/kit/ledger/format.mjs +0 -25
- package/dist/kit/ledger/hash.mjs +11 -7
- package/dist/kit/ledger/history.mjs +10 -29
- package/dist/kit/ledger/inverse-source.mjs +0 -46
- package/dist/kit/ledger/journal-records.mjs +24 -56
- package/dist/kit/ledger/merge.mjs +0 -27
- package/dist/kit/ledger/text-merge.mjs +0 -20
- package/dist/kit/ledger/transport.mjs +0 -47
- package/dist/kit/ledger.d.mts +88 -0
- package/dist/kit/ledger.mjs +14 -0
- package/dist/kit/markdown-it.d.mts +4 -0
- package/dist/kit/markdown-it.mjs +36 -0
- package/dist/kit/pdf-glyphs.mjs +39 -45
- package/dist/kit/pdf-source.d.mts +5 -0
- package/dist/kit/pdf-source.mjs +125 -0
- package/dist/kit/pdf-vectors.mjs +0 -3
- package/dist/kit/print-capitals.mjs +0 -4
- package/dist/kit/render-entry.mjs +0 -1
- package/dist/kit/render-ink.mjs +0 -5
- package/dist/kit/render-markdown.mjs +15 -119
- package/dist/kit/render-print.mjs +4 -49
- package/dist/kit/render-sanitize.mjs +22 -58
- package/dist/kit/render-styles.mjs +0 -7
- package/dist/kit/render-work.mjs +0 -12
- package/dist/kit/render.mjs +18 -173
- package/dist/kit/will.d.mts +42 -0
- package/dist/layout/hyphenation.mjs +0 -23
- package/dist/layout/line-plan.mjs +0 -20
- package/dist/layout/model.mjs +0 -55
- package/dist/layout/paragraph-fit.mjs +50 -60
- package/dist/layout/preparation.mjs +23 -56
- package/dist/layout/prepared-paragraph.mjs +0 -25
- package/dist/layout/typography.mjs +103 -41
- package/dist/packages/rapier-diff/concerns.mjs +0 -16
- package/dist/packages/rapier-diff/diff.mjs +0 -7
- package/dist/packages/rapier-diff/index.mjs +0 -1
- package/dist/packages/rapier-diff/matching.mjs +0 -19
- package/dist/packages/rapier-diff/merge.mjs +0 -9
- package/dist/packages/rapier-diff/moves.mjs +0 -7
- package/dist/packages/rapier-diff/structure.mjs +0 -2
- package/dist/packages/rapier-diff/tokens.mjs +0 -6
- package/dist/packages/rapier-diff/walk.mjs +0 -12
- package/dist/spec/document-fonts.mjs +0 -9
- package/dist/spec/document-settings.mjs +6 -25
- package/dist/spec/frontmatter.mjs +0 -1
- package/dist/spec/html-blocks.mjs +3 -37
- package/dist/spec/md-assets.mjs +2 -61
- package/dist/spec/md-layout.mjs +0 -34
- package/dist/spec/md-marks.mjs +0 -51
- package/dist/spec/md-source.mjs +0 -16
- package/dist/spec/word-sidecar.mjs +0 -2
- package/package.json +21 -4
- package/style.css +38 -21
package/README.md
CHANGED
|
@@ -24,6 +24,28 @@ deliberately show a diff. The kit supplies the portable source and history those
|
|
|
24
24
|
npm install rapier-markdown-kit
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
Render a document with [markdown-it](https://github.com/markdown-it/markdown-it), then style it with
|
|
28
|
+
`rapier-markdown-kit/style.css` inside `<main class="md-render">`:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
import MarkdownIt from 'markdown-it';
|
|
32
|
+
import {rapierMarkdown} from 'rapier-markdown-kit/markdown-it';
|
|
33
|
+
|
|
34
|
+
const html = new MarkdownIt({html: true}).use(rapierMarkdown).render('# Plan\n\nShips **Friday**.\n');
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Keep a document's history with authorship, and merge two offline copies:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import {openLedger, replaceLedgerText, merge, authorship} from 'rapier-markdown-kit/rapier-ledger';
|
|
41
|
+
|
|
42
|
+
const base = openLedger('Draft\n');
|
|
43
|
+
const edited = replaceLedgerText(base, 'Draft, revised\n', {actor: {kind: 'human', id: 'ana'}});
|
|
44
|
+
console.log(merge(edited, base).text, authorship(edited).runs);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
TypeScript declarations ship for `markdown-it`, `will`, `rapier-ledger`, `ledger/carried` and `pdf`.
|
|
48
|
+
|
|
27
49
|
```js
|
|
28
50
|
import {parseLayout} from 'rapier-markdown-kit/layout'; // the md-layout comment a picture carries
|
|
29
51
|
import {typography, formatPageBreak} from 'rapier-markdown-kit/marks'; // inline typography and page breaks
|
|
@@ -46,6 +68,9 @@ as one HTML file on Android, Web and Windows.
|
|
|
46
68
|
|
|
47
69
|
## The shared renderer
|
|
48
70
|
|
|
71
|
+
To render Markdown in your own app, use `rapier-markdown-kit/markdown-it` (above). The render factories below are the
|
|
72
|
+
editor's own seam: each needs a `host` that supplies its DOM, Markdown, picture, style and font ports.
|
|
73
|
+
|
|
49
74
|
`rapier-markdown-kit/render` exports `createRenderer`, `createMarkdownRenderer`,
|
|
50
75
|
`createRenderStyles`, `createRenderSanitizer` and `createPrintRenderer`, plus the explicit font helpers
|
|
51
76
|
`exportFontCss`, `requireDocumentFonts`, `fontRequests`, `waitForFonts` and `waitForFontRequests`. These are the
|
|
@@ -139,6 +164,7 @@ family name share the host's font namespace. Print retains its existing factory
|
|
|
139
164
|
|
|
140
165
|
| Import | What it is |
|
|
141
166
|
| --- | --- |
|
|
167
|
+
| `rapier-markdown-kit/markdown-it` | `md.use(rapierMarkdown)`: the reference style's markup in markdown-it, in one line. |
|
|
142
168
|
| `rapier-markdown-kit/layout` | The `md-layout:v1` comment a picture carries: parse it, validate it, write it back. |
|
|
143
169
|
| `rapier-markdown-kit/marks` | Unified typography values, span writer and parser installer; preserved color/font comments, ink and page breaks. |
|
|
144
170
|
| `rapier-markdown-kit/model` | Occupancy profiles and the line planner: the wrapping itself. |
|
|
@@ -146,7 +172,8 @@ family name share the host's font namespace. Print retains its existing factory
|
|
|
146
172
|
| `rapier-markdown-kit/will` | The Will/1 grammar (a separate, optional standard): the markers around an agent's intent regions. |
|
|
147
173
|
| `rapier-markdown-kit/assets` | The picture appendix: the reference definitions a document's pictures live in. It never decodes a picture. |
|
|
148
174
|
| `rapier-markdown-kit/style.css` | The reference style: type, lists, tables, tasks, pictures and the document's other markup, scoped to `.md-render`. |
|
|
149
|
-
| `rapier-markdown-kit/rapier-ledger` | The MIT edit
|
|
175
|
+
| `rapier-markdown-kit/rapier-ledger` | The MIT edit ledger: `openLedger`, replay, authorship, selective Undo, offline `merge`, three-way `mergeText`, and the carried parts. |
|
|
176
|
+
| `rapier-markdown-kit/pdf` | `attachPdfSource`: attach the exact source to a PDF without rewriting its pages; `readPdfPageCount`. |
|
|
150
177
|
| `rapier-markdown-kit/ledger/carried` | Checked optional parts for pages and saved Markdown. |
|
|
151
178
|
| `rapier-markdown-kit` | Named exports from the layout, marks, model, typography, Will, assets and render modules, plus the `ledger` namespace. |
|
|
152
179
|
|
|
@@ -290,38 +317,13 @@ contract is [Reference style](https://github.com/jackskip22/rapier/blob/main/doc
|
|
|
290
317
|
<!-- reference-style-example -->
|
|
291
318
|
```js
|
|
292
319
|
import MarkdownIt from 'markdown-it';
|
|
293
|
-
import {
|
|
294
|
-
import {
|
|
295
|
-
import {
|
|
320
|
+
import {rapierMarkdown} from 'rapier-markdown-kit/markdown-it';
|
|
321
|
+
import {formatLayout} from 'rapier-markdown-kit/layout';
|
|
322
|
+
import {TEXT_COLOR_NAMES, typography, formatColorRun, formatPageBreak} from 'rapier-markdown-kit/marks';
|
|
296
323
|
import {readFile, writeFile} from 'node:fs/promises';
|
|
297
324
|
import {pathToFileURL} from 'node:url';
|
|
298
325
|
|
|
299
|
-
const md =
|
|
300
|
-
md.renderer.rules.table_open = (tokens, index, options, env, renderer) =>
|
|
301
|
-
'<div class="table-scroll-wrap">\n' + renderer.renderToken(tokens, index, options);
|
|
302
|
-
md.renderer.rules.table_close = (tokens, index, options, env, renderer) =>
|
|
303
|
-
renderer.renderToken(tokens, index, options) + '</div>\n';
|
|
304
|
-
md.core.ruler.after('inline', 'reference-layout', state => {
|
|
305
|
-
for (let i = 1; i < state.tokens.length; i++) {
|
|
306
|
-
const inline = state.tokens[i], owner = state.tokens[i - 1];
|
|
307
|
-
if (inline.type !== 'inline' || !['paragraph_open', 'heading_open'].includes(owner.type)) continue;
|
|
308
|
-
const children = inline.children, marker = children.at(-1);
|
|
309
|
-
const layout = marker?.type === 'html_inline' ? parseLayout(marker.content) : null;
|
|
310
|
-
if (!layout) continue;
|
|
311
|
-
children.pop();
|
|
312
|
-
if (layout.align) owner.attrSet('data-md-align', layout.align);
|
|
313
|
-
const visible = children.filter(token => token.type !== 'text' || token.content.trim());
|
|
314
|
-
if (visible.length === 1 && visible[0].type === 'image' && layout.width) {
|
|
315
|
-
visible[0].attrSet('style', imageStyle(layout));
|
|
316
|
-
visible[0].attrSet('data-md-image-width', String(layout.width));
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
const htmlBlock = md.renderer.rules.html_block;
|
|
321
|
-
md.renderer.rules.html_block = (tokens, index, options, env, renderer) =>
|
|
322
|
-
isPageBreakBlock(tokens[index].content)
|
|
323
|
-
? '<div class="rapier-page-break" data-md-break="page" role="separator" aria-label="Page break"></div>\n'
|
|
324
|
-
: htmlBlock(tokens, index, options, env, renderer);
|
|
326
|
+
const md = new MarkdownIt({html: true, linkify: true}).use(rapierMarkdown);
|
|
325
327
|
|
|
326
328
|
export function renderReferenceDocument(source) {
|
|
327
329
|
return md.render(source);
|
package/dist/agent/will.mjs
CHANGED
|
@@ -10,7 +10,7 @@ const _RAPIER_WILL_UNSPACED_OPENER = '<!--will/';
|
|
|
10
10
|
const _RAPIER_WILL_UNSPACED_CLOSER = '<!--/will';
|
|
11
11
|
|
|
12
12
|
function hasWillMarkers(text) {
|
|
13
|
-
return
|
|
13
|
+
return /(?:^|[\r\n])<!-- ?(?:will\/|\/will)/.test(String(text == null ? '' : text));
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function _rapierWillMarkerOf(content) {
|
|
@@ -48,18 +48,14 @@ function _rapierWillMarkerOf(content) {
|
|
|
48
48
|
if (!intent.length || intent.indexOf('--') >= 0) return near('malformed_marker');
|
|
49
49
|
if ([...intent].length > _RAPIER_WILL_INTENT_LIMIT) return near('intent_over_bound');
|
|
50
50
|
}
|
|
51
|
-
// The words after the colon are document data, never permission; willGovern alone decides.
|
|
52
51
|
return { kind: 'open', law: word, intent, content: line };
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
// Scan CR, CRLF and LF lines directly into Will's reducer, including a trailing empty line.
|
|
56
|
-
// Offsets remain authored UTF-16 coordinates; no editor helper or whole-document line tables.
|
|
57
54
|
function _rapierWillParse(text) {
|
|
58
55
|
const source = String(text == null ? '' : text);
|
|
59
56
|
const will = {
|
|
60
57
|
present: false, fault: '', faults: [], blocks: [], markers: [], regions: [], text: source,
|
|
61
58
|
};
|
|
62
|
-
// A marker, well formed or near, opens its line with one of the four spellings hasWillMarkers looks for.
|
|
63
59
|
if (!hasWillMarkers(source)) return will;
|
|
64
60
|
let open = null;
|
|
65
61
|
for (let index = 0, start = 0; start <= source.length; index++) {
|
|
@@ -150,7 +146,6 @@ function _rapierWillIntentOf(will, start, end) {
|
|
|
150
146
|
const found = _rapierWillRegionsIn(will, start, end);
|
|
151
147
|
if (found.length !== 1) return '';
|
|
152
148
|
const region = found[0];
|
|
153
|
-
// Disclosed with the region's law, never in place of it: under keep and append the words widen nothing.
|
|
154
149
|
return region.intent && start >= region.start && end <= region.end ? region.intent : '';
|
|
155
150
|
}
|
|
156
151
|
|
package/dist/images/gif.mjs
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
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
2
|
export function inspectGIF(bytes) {
|
|
5
3
|
const fail = message => {throw Object.assign(new Error(message), {code: 'GIF_ADMISSION'});};
|
|
6
4
|
if (!(bytes instanceof Uint8Array) || bytes.length < 13 || bytes.length > 16 * 1024 * 1024 ||
|
|
@@ -39,7 +37,6 @@ export function inspectGIF(bytes) {
|
|
|
39
37
|
at += 9; if (packed & 128) at += 3 * (2 << (packed & 7));
|
|
40
38
|
need(1); if (bytes[at] < 2 || bytes[at] > 8) fail('Invalid GIF code size');
|
|
41
39
|
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
40
|
const delay = control ? control[4] + control[5] * 256 : 0;
|
|
44
41
|
duration += delay > 1 ? delay * 10 : 100;
|
|
45
42
|
if (frames > 240 || width * height * frames > 96000000 || duration > 120000) fail('GIF animation exceeds the frame, work or duration budget');
|
package/dist/images/playback.mjs
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
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
2
|
export function createMediaPlayback(win, inspectGIF) {
|
|
5
3
|
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
4
|
const bytes = async url => {
|
|
8
5
|
const text = url.slice(url.indexOf(',') + 1), slice = 1 << 20;
|
|
9
6
|
const out = new Uint8Array(text.length / 4 * 3 - (text.endsWith('==') ? 2 : text.endsWith('=') ? 1 : 0));
|
|
@@ -15,7 +12,6 @@ export function createMediaPlayback(win, inspectGIF) {
|
|
|
15
12
|
return out;
|
|
16
13
|
};
|
|
17
14
|
function raster(url, canvas, changed = () => {}, dimensions = null) {
|
|
18
|
-
// setFromBase64 refuses any character outside base64 while decoding.
|
|
19
15
|
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
16
|
const type = url.slice(5, url.indexOf(';')).toLowerCase();
|
|
21
17
|
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;
|
|
@@ -30,8 +26,6 @@ export function createMediaPlayback(win, inspectGIF) {
|
|
|
30
26
|
if (!closed) changed(state);
|
|
31
27
|
})();
|
|
32
28
|
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
29
|
let inflight = null;
|
|
36
30
|
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
31
|
const settle = async promise => {
|
|
@@ -73,8 +67,7 @@ export function createMediaPlayback(win, inspectGIF) {
|
|
|
73
67
|
if (canvas.height !== h) canvas.height = h;
|
|
74
68
|
const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, w, h); ctx.drawImage(frame, 0, 0);
|
|
75
69
|
if (!state.first && next === 0) state.first = canvas.toDataURL('image/png');
|
|
76
|
-
|
|
77
|
-
const delay = frame.duration > 10000 ? Math.max(20, frame.duration / 1000) : 100;
|
|
70
|
+
const delay = type !== 'image/gif' ? Math.max(8, frame.duration / 1000) : frame.duration > 10000 ? Math.max(20, frame.duration / 1000) : 100;
|
|
78
71
|
duration += delay; if (duration > 120000) throw new Error('Animation exceeds the duration budget');
|
|
79
72
|
state.status = 'ready'; state.frame = next; changed(state);
|
|
80
73
|
if (single) {paused = true; closeDecoder(); return;}
|
|
@@ -91,9 +84,7 @@ export function createMediaPlayback(win, inspectGIF) {
|
|
|
91
84
|
}
|
|
92
85
|
}
|
|
93
86
|
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
87
|
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
88
|
pause() {paused = true; ticket++; win.clearTimeout(timer); timer = 0;},
|
|
98
89
|
suspend() {if (single) {held = true; return;} paused = true; closeDecoder(); canvas.width = canvas.height = 0; state.status = 'suspended'; changed(state);},
|
|
99
90
|
close() {closed = true; paused = true; closeDecoder(); canvas.width = canvas.height = 0;},
|
package/dist/kit/assets.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
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
|
-
// The kit's names are listed: that module also exports what only Rapier's own parser consumers share.
|
|
4
3
|
export {IMAGE_LIMITS, configureParser, normalizeLabel, assetTitle, installMarkdownImages, markdownParser, markdownBodyOffset,
|
|
5
4
|
dataImage, parseAssets, documentAssets, packAssetIndex, restoreAssetIndex, projectImageDefinitions, blockwiseAssets,
|
|
6
5
|
imageEnvironment, referenceOccurs, mayRetireImageDefinitions, retireDeletedImageDefinitions, assetOmissions, isAssetBlock,
|
|
Binary file
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
// SPDX-License-Identifier: MIT
|
|
3
|
-
// Usage: node run.mjs [--impl path/to/module.mjs] [--tolerance 2]
|
|
4
2
|
import {resolve} from 'node:path';
|
|
5
3
|
import {dirname} from 'node:path';
|
|
6
4
|
import {fileURLToPath} from 'node:url';
|
|
@@ -32,11 +30,9 @@ function buildProfile(hint) {
|
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
const documents = loadDocuments(here);
|
|
35
|
-
// Install once over merged data: Pretext memoizes its context (measurer.mjs).
|
|
36
33
|
const measurer = installMeasurer(documents);
|
|
37
34
|
const preparation = impl.createPreparationScope();
|
|
38
35
|
|
|
39
|
-
// Preservation and presentation are separate claims; preservation is exact or a bug.
|
|
40
36
|
const preservationRows = documents.map(expected => {
|
|
41
37
|
const comment = expected.picture?.layout;
|
|
42
38
|
if (!comment) return {document: expected.document, verdict: 'n/a', detail: "this fixture's picture carries no md-layout comment"};
|
|
@@ -61,7 +57,7 @@ const preservationRows = documents.map(expected => {
|
|
|
61
57
|
console.log('Presentation (line boxes; hits/misses show how much of each row rests on the real trace vs. the character-sum fallback)\n');
|
|
62
58
|
const rows = [];
|
|
63
59
|
for (const expected of documents) {
|
|
64
|
-
const font = expected.measurer.font.replace(/\/[^ ]+ /, ' ');
|
|
60
|
+
const font = expected.measurer.font.replace(/\/[^ ]+ /, ' ');
|
|
65
61
|
const before = {hits: measurer.stats.hits, misses: measurer.stats.misses};
|
|
66
62
|
const flow = preparation.prepare(expected, [{text: expected.anchorText, font, letterSpacing: expected.measurer.letterSpacing || 0}]);
|
|
67
63
|
if (!flow) { rows.push({document: expected.document, verdict: 'ERROR', detail: 'rich preparation refused this font/text/letterSpacing'}); continue; }
|
|
@@ -80,14 +76,11 @@ for (const expected of documents) {
|
|
|
80
76
|
let maxLeftDelta = 0, maxRightDelta = 0, maxTopDelta = 0;
|
|
81
77
|
for (let i = 0; i < Math.min(plan.lines.length, expected.lines.length); i++) {
|
|
82
78
|
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
79
|
if (expected.anchorFlowed) maxTopDelta = Math.max(maxTopDelta, Math.abs(got.y - want.top));
|
|
85
|
-
// Flowed lines compare slot geometry (x+width); unflowed lines compare the measured tight extent.
|
|
86
80
|
const right = expected.anchorFlowed ? got.x + got.width : got.x + measurer.measure(font, got.fragments.map(f => f.text).join(''));
|
|
87
81
|
maxLeftDelta = Math.max(maxLeftDelta, Math.abs(got.x - want.left));
|
|
88
82
|
maxRightDelta = Math.max(maxRightDelta, Math.abs(right - want.right));
|
|
89
83
|
}
|
|
90
|
-
// A line that keeps its edges but lands on another row is a different layout.
|
|
91
84
|
const rowsAgree = maxTopDelta <= args.tolerance;
|
|
92
85
|
const withinTolerance = lineCountAgrees && rowsAgree && maxLeftDelta <= args.tolerance && maxRightDelta <= args.tolerance;
|
|
93
86
|
rows.push({
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
import {documentFontUsage, CSS_GENERIC_FONT_FAMILIES} from '../spec/document-fonts.mjs';
|
|
3
3
|
import {normalizeFontFamily} from '../spec/md-marks.mjs';
|
|
4
|
-
// Collect document font data without a layout engine. Authored drawing resources keep their identities.
|
|
5
4
|
const FONT_FIELDS = new Set(['font', 'font-family', 'font-weight', 'font-style', 'font-stretch', 'text-transform']);
|
|
6
5
|
const FAMILY = "'Geist',system-ui,sans-serif";
|
|
7
6
|
const MONO = "'Geist Mono',monospace";
|
|
8
7
|
|
|
9
|
-
// Optional HTML enhancement: the font payload and its encoded stylesheet have separate limits.
|
|
10
8
|
const FACTORY_FONT_BYTES = 8 * 1024, FACTORY_FONT_CSS_BYTES = 12 * 1024;
|
|
11
9
|
const decodeCss = value => value.replace(/\\(?:([\da-f]{1,6})(?:\r\n|[ \t\n\r\f])?|([^\n\r\f]))/gi,
|
|
12
10
|
(_, hex, character) => hex ? String.fromCodePoint(Math.max(1, Math.min(0x10ffff, parseInt(hex, 16)))) : character);
|
|
@@ -68,7 +66,6 @@ function rules(text, visit) {
|
|
|
68
66
|
}
|
|
69
67
|
|
|
70
68
|
function specificity(selector) {
|
|
71
|
-
// The reference sheet uses :where() for zero-specificity defaults.
|
|
72
69
|
let value = selector.replace(/:where\((?:[^()]|\([^()]*\))*\)/g, '');
|
|
73
70
|
const ids = (value.match(/#[\w-]+/g) || []).length;
|
|
74
71
|
const classes = (value.match(/\.[\w-]+|\[[^\]]*\]|:(?!:)[\w-]+/g) || []).length;
|
|
@@ -291,14 +288,12 @@ export async function exportFontCss(root, {settings, css = '', fontCss = '', sub
|
|
|
291
288
|
if (typeof subset !== 'function') throw new TypeError('The page font writer needs a subsetter.');
|
|
292
289
|
let candidates = fontFaces(fontCss);
|
|
293
290
|
if (portableFactory) {
|
|
294
|
-
// These candidates come from the factory resource host. Carried data owns a same-name family.
|
|
295
291
|
const custom = new Set(fontFaces(customFontCss).map(face => face.family));
|
|
296
292
|
candidates = candidates.filter(face => face.family === 'geist' && !custom.has(face.family));
|
|
297
293
|
}
|
|
298
294
|
const faces = fontRuns(root, {settings, css, faces: candidates}), output = [], prepared = new Map();
|
|
299
295
|
let payloadBytes = 0;
|
|
300
296
|
for (const face of faces) {
|
|
301
|
-
// Adjacent inline nodes retain shaping sequences; independent runs cannot compose together.
|
|
302
297
|
const text = face.runs.join('\n');
|
|
303
298
|
if (!prepared.has(face.source)) prepared.set(face.source, new Map());
|
|
304
299
|
const variants = prepared.get(face.source);
|
|
@@ -310,7 +305,6 @@ export async function exportFontCss(root, {settings, css = '', fontCss = '', sub
|
|
|
310
305
|
if (typeof validateFont !== 'function' || await validateFont(Uint8Array.from(bytes), {family: 'Geist'}) !== true) return '';
|
|
311
306
|
} else if (bytes === null) { variants.set(text, null); continue; }
|
|
312
307
|
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
308
|
variants.set(text, {source: fontSource(bytes), length: bytes.length});
|
|
315
309
|
}
|
|
316
310
|
const saved = variants.get(text);
|
|
@@ -329,10 +323,8 @@ export async function exportFontCss(root, {settings, css = '', fontCss = '', sub
|
|
|
329
323
|
}
|
|
330
324
|
}
|
|
331
325
|
|
|
332
|
-
// Validate the captured custom resources without rewriting their source or borrowing a live catalog.
|
|
333
326
|
export async function requireDocumentFonts(source, {parser, index, root, settings, css = '', fontCss = '', validateFont}) {
|
|
334
327
|
const candidates = fontFaces(fontCss), known = new Set(candidates.map(face => face.family));
|
|
335
|
-
// Missing named choices participate in the same cascade, but inert and generic runs need no data.
|
|
336
328
|
for (const family of ['Geist', 'Geist Mono', ...documentFontUsage(source, parser).keys()]) {
|
|
337
329
|
const name = family.toLowerCase();
|
|
338
330
|
if (!known.has(name)) {
|
|
@@ -363,8 +355,6 @@ export async function requireDocumentFonts(source, {parser, index, root, setting
|
|
|
363
355
|
throw Object.assign(new Error('Embedded font validation is unavailable: ' + asset.family), {code: 'export_font_unavailable'});
|
|
364
356
|
if (await validateFont(bytes, {family: asset.family, format: asset.format}) !== true)
|
|
365
357
|
throw Object.assign(new Error('The embedded font is invalid: ' + asset.family), {code: 'export_font_invalid'});
|
|
366
|
-
// The host validates the font; the viewing engine must also accept this exact container.
|
|
367
|
-
// Loading an uninstalled face checks bytes without changing this document's font set.
|
|
368
358
|
if (typeof Font === 'function') try { await new Font('rapier-export-validation', bytes).load(); }
|
|
369
359
|
catch (cause) { throw Object.assign(new Error('The embedded font could not be loaded: ' + asset.family, {cause}), {code: 'export_font_invalid'}); }
|
|
370
360
|
checked.add(asset.url);
|
|
@@ -378,7 +368,6 @@ export function fontRequests(root, options = {}) {
|
|
|
378
368
|
stretch: record.properties['font-stretch'] || '100%'}));
|
|
379
369
|
}
|
|
380
370
|
|
|
381
|
-
// This self-contained readiness step also runs in the isolated server print document.
|
|
382
371
|
export async function waitForFontRequests(doc, requests) {
|
|
383
372
|
if (!requests?.length) return;
|
|
384
373
|
const family = value => String(value).trim().replace(/^(['"])([\s\S]*)\1$/, '$2').replace(/\\(?:([\da-f]{1,6})\s?|([^\r\n\f]))/gi,
|
|
@@ -395,9 +384,6 @@ export async function waitForFontRequests(doc, requests) {
|
|
|
395
384
|
}
|
|
396
385
|
}
|
|
397
386
|
|
|
398
|
-
// CSS font loading can finish successfully while a required face is in the error state.
|
|
399
|
-
// Load the exact embedded bytes before print takes its immutable page, and leave unrelated
|
|
400
|
-
// local fallbacks and unused family styles outside this document's readiness decision.
|
|
401
387
|
export async function waitForFonts(root, {settings, css = '', fontCss = '', install = true} = {}) {
|
|
402
388
|
const required = fontRuns(root, {settings, css, fontCss});
|
|
403
389
|
if (!required.length) return () => {};
|
package/dist/kit/index.mjs
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Immutable authored origins shared by causal transport and inverse projection.
|
|
3
2
|
import {_rapierTransformSplices, _rapierValidAuthored} from './journal-records.mjs';
|
|
4
3
|
import {sha256} from './hash.mjs';
|
|
5
4
|
|
|
6
|
-
// The frontier names only causal maxima. Linear history needs one predecessor,
|
|
7
|
-
// and concurrent predecessors remain separate until an act observes both.
|
|
8
5
|
function authoredFrontier(records, visit = null) {
|
|
9
6
|
const dependencies = new Map(), frontier = new Set();
|
|
10
7
|
for (const record of records) {
|
|
@@ -38,8 +35,6 @@ export function authoredPlacements(base, records) {
|
|
|
38
35
|
return origins;
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
// Immutable authored placement stays on its own act. Basis IDs refer only to this
|
|
42
|
-
// canonical history; no source snapshot or duplicate history is stored.
|
|
43
38
|
export function authoredPlacement(source, splices, basis) {
|
|
44
39
|
const origin = {basis: basis.slice().sort(), source: sha256(source), splices: splices.map(row => ({...row}))};
|
|
45
40
|
if (!_rapierValidAuthored(origin) || _rapierTransformSplices(source, splices) === null)
|
|
@@ -17,7 +17,6 @@ export function authorship(input) {
|
|
|
17
17
|
const {ledger, text} = readLedger(input), references = _rapierHistoryEffects(ledger.records).references, removed = new Map();
|
|
18
18
|
let runs = ledger.start.text ? [[0, ledger.start.text.length, 'system', 'unattributed', 0]] : [];
|
|
19
19
|
for (const record of ledger.records) {
|
|
20
|
-
// An Undo or Redo puts back text the act it references removed: that text keeps the runs it had.
|
|
21
20
|
const tx = record.transaction, gone = [], kept = (references?.get(tx.id) || []).flatMap(id => removed.get(id) || []);
|
|
22
21
|
for (const row of record.splices) {
|
|
23
22
|
const cut = row.pos + row.removed.length, delta = row.inserted.length - row.removed.length, before = [], after = [], inside = [];
|
|
@@ -49,7 +48,6 @@ export function readAuthorship(input, text) {
|
|
|
49
48
|
if (cursor !== text.length) throw fault('authorship does not cover the text');
|
|
50
49
|
return value;
|
|
51
50
|
}
|
|
52
|
-
// Authorship-only imports become an incomplete insertion ledger. There is no mutable blame database.
|
|
53
51
|
export function ledgerFromAuthorship(input, text, documentAuthority, metadata) {
|
|
54
52
|
const value = readAuthorship(input, text), records = [];
|
|
55
53
|
let current = '', root = textRoot('');
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// The ledger and authorship carried inside the document: an optional HTML comment at the end of the file.
|
|
3
|
+
import type {LedgerEnvelope, AuthorshipEnvelope, Metadata} from '../ledger.mjs';
|
|
4
|
+
export interface DocumentParts { ledger?: LedgerEnvelope | null; authorship?: AuthorshipEnvelope | null; metadata?: Metadata; }
|
|
5
|
+
export interface ReadParts { text: string; ledger: LedgerEnvelope | null; authorship: AuthorshipEnvelope | null; }
|
|
6
|
+
/** The text with its parts appended as one EOF comment; the text alone when there are none. */
|
|
7
|
+
export function writeDocument(text: string, parts?: DocumentParts): string;
|
|
8
|
+
/** Splits a saved file into its exact text and verified parts; throws on a damaged or mismatched comment. */
|
|
9
|
+
export function readDocument(source: string): ReadParts;
|
|
10
|
+
export function writeParts(text: string, parts?: DocumentParts): string;
|
|
11
|
+
export function readParts(text: string, elements: unknown): {ledger: LedgerEnvelope | null; authorship: AuthorshipEnvelope | null};
|
|
12
|
+
export function validateParts(text: string, parts?: DocumentParts): {ledger: LedgerEnvelope | null; authorship: AuthorshipEnvelope | null};
|
|
13
|
+
export function takeParts(html: string, text?: string): {html: string; ledger: LedgerEnvelope | null; authorship: AuthorshipEnvelope | null};
|
|
14
|
+
export function encodeCarried(text: string): string;
|
|
15
|
+
export function decodeCarried(text: string): string;
|
|
16
|
+
export interface ComparisonBase { text: string; sha256: string; revision: string; name: string; by: string; at: string; }
|
|
17
|
+
export function readBase(value: Partial<ComparisonBase>): ComparisonBase;
|
|
18
|
+
export function writeBase(text: string, value: Partial<ComparisonBase>): string;
|
|
19
|
+
export function readBaseElements(elements: unknown): ComparisonBase | null;
|
|
20
|
+
export function takeBase(html: string): {html: string; base: ComparisonBase | null};
|
|
@@ -10,7 +10,6 @@ export function decodeCarried(text) {
|
|
|
10
10
|
return String(text).replace(/\\([\\/!r0])/g, (_, c) => c === '\\' ? '\\' : c === 'r' ? '\r' : c === '0' ? '\0' : c === '/' ? '</' : '<!');
|
|
11
11
|
}
|
|
12
12
|
export function validateParts(text, {ledger = null, authorship = null, metadata} = {}) {
|
|
13
|
-
// A file's leading BOM is metadata, as in the editor's source intake.
|
|
14
13
|
const canonical = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
15
14
|
const history = ledger === null ? null : readLedger(ledger, canonical, metadata).ledger;
|
|
16
15
|
const runs = authorship === null ? null : readAuthorship(authorship, canonical);
|
|
@@ -22,7 +21,6 @@ export function writeParts(text, options) {
|
|
|
22
21
|
return Object.entries(parts).filter(([, value]) => value !== null).map(([name, value]) =>
|
|
23
22
|
'<script type="application/json" id="rapier-' + name + '">' + encodeCarried(JSON.stringify(value)) + '</script>\n').join('');
|
|
24
23
|
}
|
|
25
|
-
// Pure adapter: the browser supplies inert script elements, Node supplies parsed script records.
|
|
26
24
|
export function readParts(text, elements) {
|
|
27
25
|
const parts = {ledger: null, authorship: null};
|
|
28
26
|
for (const element of elements) {
|
|
@@ -35,7 +33,6 @@ export function readParts(text, elements) {
|
|
|
35
33
|
}
|
|
36
34
|
return validateParts(text, parts);
|
|
37
35
|
}
|
|
38
|
-
// The wrapper only removes whole script elements. Strings inside other scripts are not carriers.
|
|
39
36
|
export function takeParts(html, text) {
|
|
40
37
|
const elements = [];
|
|
41
38
|
const stripped = html.replace(/<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi, (whole, attrs, content) => {
|
|
@@ -50,7 +47,6 @@ export function takeParts(html, text) {
|
|
|
50
47
|
return {html: stripped, ...readParts(text ?? '', elements)};
|
|
51
48
|
}
|
|
52
49
|
const OPEN = '\n\n<!-- rapier-carried/1\n', CLOSE = '\n-->\n';
|
|
53
|
-
// One optional HTML comment at EOF; every other Markdown reader ignores it. Source is never rewritten.
|
|
54
50
|
export function writeDocument(text, options) {
|
|
55
51
|
const parts = validateParts(text, options);
|
|
56
52
|
if (!parts.ledger && !parts.authorship) return text;
|
|
@@ -58,9 +54,12 @@ export function writeDocument(text, options) {
|
|
|
58
54
|
return text + OPEN + json + CLOSE;
|
|
59
55
|
}
|
|
60
56
|
export function readDocument(source) {
|
|
57
|
+
if (!source.endsWith(CLOSE)) {
|
|
58
|
+
if (source.includes(OPEN)) throw fault('unfinished document metadata');
|
|
59
|
+
return {text: source, ledger: null, authorship: null};
|
|
60
|
+
}
|
|
61
61
|
const at = source.lastIndexOf(OPEN);
|
|
62
62
|
if (at < 0) return {text: source, ledger: null, authorship: null};
|
|
63
|
-
if (!source.endsWith(CLOSE)) throw fault('unfinished document metadata');
|
|
64
63
|
let parts;
|
|
65
64
|
try { parts = JSON.parse(source.slice(at + OPEN.length, -CLOSE.length)); }
|
|
66
65
|
catch (_) { throw fault('invalid document metadata'); }
|
|
@@ -69,10 +68,8 @@ export function readDocument(source) {
|
|
|
69
68
|
return {text, ...validateParts(text, parts)};
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
// One portable baseline. A digest is integrity evidence, not authentication of the named author.
|
|
73
71
|
export function readBase(value) {
|
|
74
72
|
if (!value || typeof value !== 'object') throw fault('comparison base SHA-256 differs from its text');
|
|
75
|
-
// Capture each caller-owned field once; admission and serialization use these same scalars.
|
|
76
73
|
const {text, sha256: digest, revision, name, by, at, fonts} = value;
|
|
77
74
|
if (typeof text !== 'string' || typeof digest !== 'string' || !/^[a-f0-9]{64}$/.test(digest) || sha256(text) !== digest)
|
|
78
75
|
throw fault('comparison base SHA-256 differs from its text');
|
package/dist/kit/ledger/data.mjs
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Only plain JSON data enters the public format. Accessors and toJSON are never executed.
|
|
3
2
|
export function fault(message) { return Object.assign(new Error('Ledger: ' + message), {code: 'ledger_invalid'}); }
|
|
4
|
-
// A plain object's prototype is null or Object's own. Both lookups read descriptors, so no getter runs.
|
|
5
3
|
export const plainPrototype = proto => proto === null || Object.getPrototypeOf(proto) === null &&
|
|
6
4
|
Object.getOwnPropertyDescriptor(Object.getOwnPropertyDescriptor(proto, 'constructor')?.value || {}, 'name')?.value === 'Object';
|
|
7
5
|
export function fields(value, depth, active, sorted = true) {
|
|
@@ -22,8 +20,6 @@ export function scalar(value) {
|
|
|
22
20
|
if (typeof value === 'string') { if (/[\uD800-\uDFFF]/u.test(value)) throw fault('unpaired surrogate'); return true; }
|
|
23
21
|
return typeof value === 'number' && Number.isFinite(value) && !Object.is(value, -0);
|
|
24
22
|
}
|
|
25
|
-
// A canonical copy does not need a serialized document and a second parsed document.
|
|
26
|
-
// Sorted insertion preserves the previous plainData result, including numeric-key ordering.
|
|
27
23
|
export function plainData(value) {
|
|
28
24
|
if (typeof value === 'string') value = JSON.parse(value);
|
|
29
25
|
const active = new Set();
|
|
@@ -37,8 +33,6 @@ export function plainData(value) {
|
|
|
37
33
|
};
|
|
38
34
|
return copy(value, 0);
|
|
39
35
|
}
|
|
40
|
-
// Each yielded string is bounded, even when one value contains the whole document.
|
|
41
|
-
// JSON.stringify still owns escaping; scalar boundaries keep its spelling exact.
|
|
42
36
|
function* quoted(text, whole = true) {
|
|
43
37
|
yield '"';
|
|
44
38
|
for (let at = 0; at < text.length;) {
|
|
@@ -73,8 +67,6 @@ export function canonicalJSON(value, depth = 0, active = new Set()) { return Arr
|
|
|
73
67
|
export function exactKeys(value, keys) {
|
|
74
68
|
return value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
75
69
|
}
|
|
76
|
-
// TextEncoder's exact byte count, without allocating an encoded copy. Whole-character
|
|
77
|
-
// admission is performed in this same pass when the document owner requests it.
|
|
78
70
|
export function utf8Length(text, whole = false) {
|
|
79
71
|
let bytes = 0;
|
|
80
72
|
for (let at = 0; at < text.length; at++) {
|
|
@@ -8,8 +8,6 @@ import {canonicalJSON, canonicalParts, plainData, documentText, utf8Length, exac
|
|
|
8
8
|
export {textRoot, rootAfter};
|
|
9
9
|
const digest = value => sha256Parts(canonicalParts(value));
|
|
10
10
|
const rootShape = value => typeof value === 'string' && /^\d+:[a-z0-9]+:[a-z0-9]+$/.test(value);
|
|
11
|
-
// Structural/history admission is shared by the reader and the producer. The producer
|
|
12
|
-
// proves source replay by reversing exact admitted splices; it need not replay its output.
|
|
13
11
|
function* checkedRecords(ledger, prepared) {
|
|
14
12
|
if (!prepared.ok || ledger.records.length !== prepared.records.length) throw fault('record chain differs');
|
|
15
13
|
let root = ledger.start.root, revision = ledger.start.revision;
|
|
@@ -27,16 +25,9 @@ function* checkedRecords(ledger, prepared) {
|
|
|
27
25
|
}
|
|
28
26
|
if (ledger.head.revision !== revision || ledger.head.root !== root) throw fault('history does not describe this document');
|
|
29
27
|
}
|
|
30
|
-
// A ledger object read again is proved once: a boot reads one stored history through several records. The proof holds only
|
|
31
|
-
// while that object still carries the exact proved data (its start text compared whole, the rest as canonical JSON), so a
|
|
32
|
-
// changed byte is proved again, and refused. The proof lives as long as the object does.
|
|
33
28
|
const proofs = new WeakMap();
|
|
34
29
|
function readLedgerState(input, expectedText, expectedMetadata) {
|
|
35
30
|
const ledger = plainData(input);
|
|
36
|
-
// The identifier rapier-ledger/2 is unchanged and is not backward compatible. This reader
|
|
37
|
-
// admits an empty physical splice whose authored rows name the same replacement. A reader
|
|
38
|
-
// from before that rule rejects the chain. Mixed-version sync throws notes_history_unavailable
|
|
39
|
-
// and leaves the older store unchanged; that refusal is not a held or caughtUp result.
|
|
40
31
|
if (!exactKeys(ledger, ['format','documentAuthority','start','records','head','complete','sha256']) || ledger.format !== 'rapier-ledger/2' ||
|
|
41
32
|
typeof ledger.documentAuthority !== 'string' || !ledger.documentAuthority || ledger.documentAuthority.length > 256 ||
|
|
42
33
|
typeof ledger.complete !== 'boolean' || !Array.isArray(ledger.records) ||
|
|
@@ -81,7 +72,6 @@ function proveLedger(ledger) {
|
|
|
81
72
|
if (ledger.head.revision !== revision || ledger.head.root !== root || ledger.head.sha256 !== sha256(text)) throw fault('history does not describe this document');
|
|
82
73
|
return {text, metadata, root, revision, prepared, source};
|
|
83
74
|
}
|
|
84
|
-
// A stored copy may hold the head text alone. Its start is that text with every record reversed; readLedger proves it.
|
|
85
75
|
export function ledgerWithStart(input, text) {
|
|
86
76
|
let source = new HistorySource(documentText(text));
|
|
87
77
|
for (let i = input.records.length - 1; i >= 0; i--) {
|
|
@@ -129,11 +119,8 @@ function* sourceFacts(text) {
|
|
|
129
119
|
}
|
|
130
120
|
function* produceLedger({text, metadata, records, documentAuthority, revision, root, complete = false, earlier = records} = {}) {
|
|
131
121
|
if (!_rapierValidMetadata(metadata)) throw fault('document metadata required');
|
|
132
|
-
// Metadata has two admitted scalar fields. Keep its insertion order in the
|
|
133
|
-
// emitted envelope while capturing it before the first cooperative boundary.
|
|
134
122
|
metadata = {...metadata};
|
|
135
123
|
if (!Array.isArray(records) || !Array.isArray(earlier)) throw fault('records required');
|
|
136
|
-
// Inspect before touching properties: exporting must not call accessors on an alleged record.
|
|
137
124
|
const prepared = _rapierPrepareHistoryRecords(records, earlier);
|
|
138
125
|
if (!prepared.ok) throw fault('navigation target was not retained');
|
|
139
126
|
const headFacts = yield* sourceFacts(text);
|
|
@@ -153,8 +140,6 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
|
|
|
153
140
|
startSource = startSource.apply(expanded[i].splices, true);
|
|
154
141
|
startMetadata = _rapierTransformMetadata(startMetadata, expanded[i].metadata, true);
|
|
155
142
|
if (startSource === null || !startMetadata) throw fault('history does not reverse from this document');
|
|
156
|
-
// Whole scalars and exact splice boundaries were just checked. Count the changed
|
|
157
|
-
// bytes, rather than re-encoding every reconstructed whole-document revision.
|
|
158
143
|
for (const row of expanded[i].splices) bytes += utf8Length(row.removed) - utf8Length(row.inserted);
|
|
159
144
|
if (bytes > 64 * 1024 * 1024) throw fault('document exceeds the editor text limit');
|
|
160
145
|
yield;
|
|
@@ -174,8 +159,6 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
|
|
|
174
159
|
const payload = {format: 'rapier-ledger/2', documentAuthority,
|
|
175
160
|
start: {text: startText, revision: startRevision, root: startRoot, sha256: startSHA, metadata: startMetadata},
|
|
176
161
|
records: expanded, head: {revision, root, sha256: headSHA, metadata: {...metadata}}, complete: complete === true && startRevision === 0 && expanded.every((row, i) => !i || row.transaction.baseRevision === expanded[i - 1].transaction.revision)};
|
|
177
|
-
// Expanded rows are the exact canonical projection of this owned preparation.
|
|
178
|
-
// Keep its navigation/effect proof instead of admitting another copied graph.
|
|
179
162
|
for (const _record of checkedRecords(payload, prepared)) yield;
|
|
180
163
|
if (expanded.some(row => Object.hasOwn(row, 'authored'))) {
|
|
181
164
|
try { authoredHistory(startText, expanded); } catch (_) { throw fault('authored origin differs'); }
|
|
@@ -191,7 +174,6 @@ function finish(producer) {
|
|
|
191
174
|
return step.value;
|
|
192
175
|
}
|
|
193
176
|
export function exportLedger(input) { return finish(produceLedger(input)).ledger; }
|
|
194
|
-
// Its returned facts were computed while admitting/hashing the exact head, never supplied.
|
|
195
177
|
export async function prepareLedgerAsync(input, {yield: yieldWork, signal} = {}) {
|
|
196
178
|
const producer = produceLedger(input);
|
|
197
179
|
let step;
|
|
@@ -199,7 +181,6 @@ export async function prepareLedgerAsync(input, {yield: yieldWork, signal} = {})
|
|
|
199
181
|
signal?.throwIfAborted();
|
|
200
182
|
step = producer.next();
|
|
201
183
|
if (!step.done && yieldWork) {
|
|
202
|
-
// The caller owns scheduling and its clock; ledger admission reads neither.
|
|
203
184
|
const pending = yieldWork();
|
|
204
185
|
if (pending) await pending;
|
|
205
186
|
}
|
|
@@ -207,7 +188,6 @@ export async function prepareLedgerAsync(input, {yield: yieldWork, signal} = {})
|
|
|
207
188
|
signal?.throwIfAborted();
|
|
208
189
|
return step.value;
|
|
209
190
|
}
|
|
210
|
-
// Derived navigation, never a second history. A target older than start is intentionally unavailable.
|
|
211
191
|
export function historyEnvelope(input, segmentIdentity = [], expectedText, expectedMetadata) {
|
|
212
192
|
const {value: {ledger, text, metadata}, prepared, source} = readLedgerState(input, expectedText, expectedMetadata);
|
|
213
193
|
const {byId, effects: {pictures}} = prepared;
|
|
@@ -235,8 +215,6 @@ export function historyEnvelope(input, segmentIdentity = [], expectedText, expec
|
|
|
235
215
|
}
|
|
236
216
|
if (ledger.complete) {
|
|
237
217
|
if (ledger.records.length && !branch.length) throw fault('complete history lost its branch');
|
|
238
|
-
// Literal admitted splices are reversible. Proving the actual head back to
|
|
239
|
-
// the exact start also proves its forward active branch, without replaying it.
|
|
240
218
|
if (traversed.length !== ledger.start.text.length || !traversed.matches(0, ledger.start.text) ||
|
|
241
219
|
canonicalJSON(traversedMetadata) !== canonicalJSON(ledger.start.metadata)) throw fault('complete branch differs');
|
|
242
220
|
}
|
|
@@ -246,8 +224,6 @@ export function historyEnvelope(input, segmentIdentity = [], expectedText, expec
|
|
|
246
224
|
traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata);
|
|
247
225
|
if (traversed === null || !traversedMetadata) throw fault('redo branch does not replay');
|
|
248
226
|
}
|
|
249
|
-
// Picture folding may relocate finished definitions while navigating. Keep its
|
|
250
|
-
// distinct forward proof; the literal-only case above needs no second traversal.
|
|
251
227
|
if (ledger.complete && pictures.size) {
|
|
252
228
|
let active = new HistorySource(ledger.start.text), activeMetadata = {...ledger.start.metadata};
|
|
253
229
|
for (let i = 0; i < branch.length; i++) {
|
|
@@ -270,7 +246,6 @@ export function agentActorId(door, agent) {
|
|
|
270
246
|
return door + '/' + agent.name;
|
|
271
247
|
}
|
|
272
248
|
|
|
273
|
-
// A writer changing representation records that change, rather than attaching an unrelated head.
|
|
274
249
|
export function replaceLedgerText(input, text, {actor = {kind: 'system', id: 'rapier'}, operation = 'document.replace', at} = {}) {
|
|
275
250
|
const current = readLedger(input); documentText(text);
|
|
276
251
|
if (current.text === text) return current.ledger;
|