rapier-html 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/BUILD.json +25 -18
- package/README.md +11 -11
- package/dist/ledger/LICENSE +5 -2
- package/dist/ledger/carried.mjs +17 -10
- package/dist/ledger/data.mjs +73 -13
- package/dist/ledger/diff.mjs +359 -224
- package/dist/ledger/format.mjs +176 -54
- package/dist/ledger/hash.mjs +91 -18
- package/dist/ledger/history.mjs +156 -116
- package/dist/ledger/inverse-source.mjs +158 -19
- package/dist/ledger/journal-records.mjs +292 -67
- package/dist/rapier.html +78 -359
- package/package.json +1 -1
package/BUILD.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "rapier-html",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"path": "dist/rapier.html",
|
|
5
|
-
"bytes":
|
|
6
|
-
"sha256": "
|
|
7
|
-
"builtAt": "2026-10-
|
|
5
|
+
"bytes": 5086203,
|
|
6
|
+
"sha256": "864fe7a94b5c3064653e2a7423449f439c0ab864062ec1644a88dee90c042af6",
|
|
7
|
+
"builtAt": "2026-10-14T16:53:28.260Z",
|
|
8
8
|
"node": "v22.23.3",
|
|
9
9
|
"canonical": true,
|
|
10
10
|
"packing": "zopfli",
|
|
@@ -14,32 +14,39 @@
|
|
|
14
14
|
"delivery": "built-in",
|
|
15
15
|
"kind": "script",
|
|
16
16
|
"element": "rapier-plugin-math",
|
|
17
|
-
"bytes":
|
|
18
|
-
"packedBytes":
|
|
17
|
+
"bytes": 749536,
|
|
18
|
+
"packedBytes": 504166,
|
|
19
19
|
"files": [
|
|
20
20
|
{
|
|
21
21
|
"id": "rapier-math",
|
|
22
|
-
"name": "rapier-math-
|
|
23
|
-
"version": "
|
|
24
|
-
"bytes":
|
|
25
|
-
"sri": "
|
|
22
|
+
"name": "rapier-math-2.2.0.js",
|
|
23
|
+
"version": "2.2.0",
|
|
24
|
+
"bytes": 749536,
|
|
25
|
+
"sri": "pzYte7azbAoS8XSwIKjFg7mxGOxAxBKo9/IUy07H4j0WoyguAS/zwkR1O6kDXq9S"
|
|
26
26
|
}
|
|
27
27
|
]
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"id": "mermaid",
|
|
31
31
|
"delivery": "built-in",
|
|
32
|
-
"kind": "
|
|
32
|
+
"kind": "files",
|
|
33
33
|
"element": "rapier-plugin-mermaid",
|
|
34
|
-
"bytes":
|
|
35
|
-
"packedBytes":
|
|
34
|
+
"bytes": 7384142,
|
|
35
|
+
"packedBytes": 1563922,
|
|
36
36
|
"files": [
|
|
37
37
|
{
|
|
38
|
-
"id": "rapier-mermaid",
|
|
39
|
-
"name": "
|
|
40
|
-
"version": "
|
|
41
|
-
"bytes":
|
|
42
|
-
"sri": "
|
|
38
|
+
"id": "rapier-mermaid-core",
|
|
39
|
+
"name": "mermaid.min.js",
|
|
40
|
+
"version": "12.1.0",
|
|
41
|
+
"bytes": 5493176,
|
|
42
|
+
"sri": "EbBpjO7rlR6eqZEcG7GaPpyk9H9WrMyPWX4d3KvPYltgt8Z8l0z6R56B1qP40pR4"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "rapier-mermaid-zenuml",
|
|
46
|
+
"name": "rapier-zenuml-4.5.0-1.js",
|
|
47
|
+
"version": "4.5.0-1",
|
|
48
|
+
"bytes": 1890966,
|
|
49
|
+
"sri": "MVn/Or1nIHTDvy84lSSJAH2Fx1Y7AGoaMfd7hW7i3LJe7M/dau8VDFnF8Q+X0qOl"
|
|
43
50
|
}
|
|
44
51
|
]
|
|
45
52
|
},
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Turn Markdown into one offline HTML file with the full Rapier editor. It opens in any browser on a phone or
|
|
4
4
|
computer, with no account. People read, edit, draw and paint watercolor. Agents deliver documents, show what
|
|
5
|
-
changed and receive the person's edited copy through the door. Start with `npx rapier-html@1.
|
|
5
|
+
changed and receive the person's edited copy through the door. Start with `npx rapier-html@1.2.3 notes.md`.
|
|
6
6
|
|
|
7
7
|
Use it for a plan to rearrange, an illustrated guide to annotate, a draft to continue writing, or a revision
|
|
8
8
|
to review. Text and drawings stay editable, and the file remains usable independently of a hosted workspace.
|
|
@@ -18,14 +18,14 @@ to review. Text and drawings stay editable, and the file remains usable independ
|
|
|
18
18
|
Node 22 or newer.
|
|
19
19
|
|
|
20
20
|
```sh
|
|
21
|
-
npx -- rapier-html@1.
|
|
22
|
-
npx -- rapier-html@1.
|
|
23
|
-
npx -- rapier-html@1.
|
|
24
|
-
npx -- rapier-html@1.
|
|
25
|
-
npx -- rapier-html@1.
|
|
26
|
-
npx -- rapier-html@1.
|
|
27
|
-
npx -- rapier-html@1.
|
|
28
|
-
npm install rapier-html@1.
|
|
21
|
+
npx -- rapier-html@1.2.3 notes.md # the editor, notes.md inside it, written beside the file
|
|
22
|
+
npx -- rapier-html@1.2.3 notes.md --view draw # opens on Draw: sketch and paint, the document behind it
|
|
23
|
+
npx -- rapier-html@1.2.3 notes.md --view notes # opens the Notes library
|
|
24
|
+
npx -- rapier-html@1.2.3 notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
|
|
25
|
+
npx -- rapier-html@1.2.3 revised.md --compare original.md # opens showing what changed
|
|
26
|
+
npx -- rapier-html@1.2.3 notes.md --return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT" # Send back
|
|
27
|
+
npx -- rapier-html@1.2.3 notes.md out.html # a named output
|
|
28
|
+
npm install rapier-html@1.2.3 # as a library: wrap, unwrap
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
The first `--` is for npm, so `--help` reaches the command. It never overwrites a file.
|
|
@@ -46,8 +46,8 @@ Collaborate over MCP and WebMCP, use optional encrypted sync to your own Cloudfl
|
|
|
46
46
|
editor and lightweight reader (about 190 kB gzipped). The agent edits live; the person sees its presence and
|
|
47
47
|
changes, taps a change to see the before and undoes anything. Use `show {target:"comparison"}` to show a diff deliberately.
|
|
48
48
|
|
|
49
|
-
1. **Work together live.** Connect to `https://mcp.rapier.website/mcp`, then call `open` to share one document with the person.
|
|
50
|
-
2. **Deliver an offline editor.** Run `npx rapier-html@1.
|
|
49
|
+
1. **Work together live.** Connect to `https://mcp.rapier.website/door/mcp`, then call `open` to share one document with the person.
|
|
50
|
+
2. **Deliver an offline editor.** Run `npx rapier-html@1.2.3 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)).
|
|
51
51
|
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.
|
|
52
52
|
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.
|
|
53
53
|
5. **Encode JPEG XL.** Install [rapier-jxl](https://github.com/jackskip22/rapier-jxl) to encode pixels, photographs and existing JPEGs in JavaScript.
|
package/dist/ledger/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/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`,
|
package/dist/ledger/carried.mjs
CHANGED
|
@@ -71,18 +71,21 @@ export function readDocument(source) {
|
|
|
71
71
|
|
|
72
72
|
// One portable baseline. A digest is integrity evidence, not authentication of the named author.
|
|
73
73
|
export function readBase(value) {
|
|
74
|
-
if (!value || typeof value !== 'object'
|
|
75
|
-
|
|
74
|
+
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
|
+
const {text, sha256: digest, revision, name, by, at, fonts} = value;
|
|
77
|
+
if (typeof text !== 'string' || typeof digest !== 'string' || !/^[a-f0-9]{64}$/.test(digest) || sha256(text) !== digest)
|
|
76
78
|
throw fault('comparison base SHA-256 differs from its text');
|
|
77
|
-
if (typeof
|
|
79
|
+
if (typeof revision !== 'string' || !revision || revision.length > 256 || /[\u0000-\u001f\u007f]/.test(revision))
|
|
78
80
|
throw fault('invalid comparison base revision');
|
|
79
|
-
if (typeof
|
|
81
|
+
if (typeof name !== 'string' || !name || [...name].length > 256 || /[\\/\u0000-\u001f\u007f]/.test(name))
|
|
80
82
|
throw fault('invalid comparison base name');
|
|
81
|
-
if (typeof
|
|
83
|
+
if (typeof by !== 'string' || !by.trim() || by.length > 96 || /[\u0000-\u001f\u007f]/.test(by))
|
|
82
84
|
throw fault('a comparison needs its author name');
|
|
83
|
-
if (typeof
|
|
85
|
+
if (typeof at !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(at) || !Number.isFinite(Date.parse(at)))
|
|
84
86
|
throw fault('the time of a comparison is an ISO 8601 UTC timestamp');
|
|
85
|
-
|
|
87
|
+
if (fonts != null && typeof fonts !== 'string') throw fault('invalid comparison font resources');
|
|
88
|
+
return {text, sha256: digest, revision, name, by, at, ...(fonts ? {fonts} : {})};
|
|
86
89
|
}
|
|
87
90
|
const escapeAttribute = value => value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
88
91
|
const readAttribute = value => value.replace(/&(quot|lt|gt|amp);/g, (_, name) => ({quot: '"', lt: '<', gt: '>', amp: '&'})[name]);
|
|
@@ -91,7 +94,8 @@ export function writeBase(text, value) {
|
|
|
91
94
|
const base = readBase(value);
|
|
92
95
|
if (base.text === text) return '';
|
|
93
96
|
return '<script type="text/markdown" id="rapier-base"' + ['name', 'sha256', 'revision', 'by', 'at']
|
|
94
|
-
.map(key => ' data-' + key + '="' + escapeAttribute(base[key]) + '"').join('') +
|
|
97
|
+
.map(key => ' data-' + key + '="' + escapeAttribute(base[key]) + '"').join('') +
|
|
98
|
+
(base.fonts ? ' data-fonts="' + escapeAttribute(JSON.stringify(base.fonts)) + '"' : '') + '>' + encodeCarried(base.text) + '</script>';
|
|
95
99
|
}
|
|
96
100
|
export function readBaseElements(elements) {
|
|
97
101
|
const rows = [...elements].filter(element => element.id === 'rapier-base');
|
|
@@ -100,7 +104,10 @@ export function readBaseElements(elements) {
|
|
|
100
104
|
if (rows.length !== 1 || row.type !== 'text/markdown' || (row.tagName && row.tagName.toLowerCase() !== 'script'))
|
|
101
105
|
throw fault('duplicate or executable comparison base');
|
|
102
106
|
const get = name => row.getAttribute ? row.getAttribute('data-' + name) : row[name];
|
|
103
|
-
|
|
107
|
+
let fonts;
|
|
108
|
+
try { const value = get('fonts'); if (value != null) fonts = JSON.parse(value); }
|
|
109
|
+
catch (_) { throw fault('invalid comparison font resources'); }
|
|
110
|
+
return readBase({text: decodeCarried(row.textContent), fonts, ...Object.fromEntries(['name', 'sha256', 'revision', 'by', 'at'].map(key => [key, get(key)]))});
|
|
104
111
|
}
|
|
105
112
|
export function takeBase(html) {
|
|
106
113
|
const elements = [];
|
|
@@ -113,7 +120,7 @@ export function takeBase(html) {
|
|
|
113
120
|
seen.add(key); values[key] = value;
|
|
114
121
|
}
|
|
115
122
|
elements.push({id: values.id, type: values.type, textContent: content,
|
|
116
|
-
...Object.fromEntries(['name', 'sha256', 'revision', 'by', 'at'].map(key => [key, values['data-' + key]]))});
|
|
123
|
+
...Object.fromEntries(['name', 'sha256', 'revision', 'by', 'at', 'fonts'].map(key => [key, values['data-' + key]]))});
|
|
117
124
|
return '';
|
|
118
125
|
});
|
|
119
126
|
return {html: stripped, base: readBaseElements(elements)};
|
package/dist/ledger/data.mjs
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// Only plain JSON data enters the public format. Accessors and toJSON are never executed.
|
|
3
3
|
export function fault(message) { return Object.assign(new Error('Ledger: ' + message), {code: 'ledger_invalid'}); }
|
|
4
|
-
|
|
5
|
-
if (value === null || typeof value === 'boolean') return JSON.stringify(value);
|
|
6
|
-
if (typeof value === 'string') { if (/[\uD800-\uDFFF]/u.test(value)) throw fault('unpaired surrogate'); return JSON.stringify(value); }
|
|
7
|
-
if (typeof value === 'number' && Number.isFinite(value) && !Object.is(value, -0)) return JSON.stringify(value);
|
|
4
|
+
function fields(value, depth, active, sorted = true) {
|
|
8
5
|
if (!value || typeof value !== 'object' || depth > 64 || active.has(value)) throw fault('not plain JSON data');
|
|
9
6
|
const array = Array.isArray(value), proto = Object.getPrototypeOf(value);
|
|
10
7
|
if (!array && proto !== null && (Object.getPrototypeOf(proto) !== null || Object.getOwnPropertyDescriptor(proto, 'constructor')?.value?.name !== 'Object')) throw fault('not a plain object');
|
|
@@ -15,19 +12,82 @@ export function canonicalJSON(value, depth = 0, active = new Set()) {
|
|
|
15
12
|
if (typeof key !== 'string' || ['__proto__', 'constructor', 'prototype'].includes(key) || !Object.hasOwn(field, 'value') || !field.enumerable) throw fault('non-data property');
|
|
16
13
|
}
|
|
17
14
|
if (array && (keys.length !== value.length + 1 || keys.some(k => k !== 'length' && (!/^(0|[1-9]\d*)$/.test(k) || +k >= value.length)))) throw fault('sparse array');
|
|
15
|
+
return {array, descriptors, keys: array ? keys.filter(key => key !== 'length') : sorted ? keys.sort() : keys};
|
|
16
|
+
}
|
|
17
|
+
function scalar(value) {
|
|
18
|
+
if (value === null || typeof value === 'boolean') return true;
|
|
19
|
+
if (typeof value === 'string') { if (/[\uD800-\uDFFF]/u.test(value)) throw fault('unpaired surrogate'); return true; }
|
|
20
|
+
return typeof value === 'number' && Number.isFinite(value) && !Object.is(value, -0);
|
|
21
|
+
}
|
|
22
|
+
// A canonical copy does not need a serialized document and a second parsed document.
|
|
23
|
+
// Sorted insertion preserves the previous plainData result, including numeric-key ordering.
|
|
24
|
+
export function plainData(value) {
|
|
25
|
+
if (typeof value === 'string') value = JSON.parse(value);
|
|
26
|
+
const active = new Set();
|
|
27
|
+
const copy = (value, depth) => {
|
|
28
|
+
if (scalar(value)) return value;
|
|
29
|
+
const {array, descriptors, keys} = fields(value, depth, active);
|
|
30
|
+
const out = array ? [] : {};
|
|
31
|
+
active.add(value);
|
|
32
|
+
for (const key of keys) out[key] = copy(descriptors[key].value, depth + 1);
|
|
33
|
+
active.delete(value); return out;
|
|
34
|
+
};
|
|
35
|
+
return copy(value, 0);
|
|
36
|
+
}
|
|
37
|
+
// Each yielded string is bounded, even when one value contains the whole document.
|
|
38
|
+
// JSON.stringify still owns escaping; scalar boundaries keep its spelling exact.
|
|
39
|
+
function* quoted(text, whole = true) {
|
|
40
|
+
yield '"';
|
|
41
|
+
for (let at = 0; at < text.length;) {
|
|
42
|
+
let end = Math.min(text.length, at + 16384);
|
|
43
|
+
if (end < text.length && (text.charCodeAt(end - 1) & 0xfc00) === 0xd800 && (text.charCodeAt(end) & 0xfc00) === 0xdc00) end--;
|
|
44
|
+
const part = text.slice(at, end);
|
|
45
|
+
if (whole && /[\uD800-\uDFFF]/u.test(part)) throw fault('unpaired surrogate');
|
|
46
|
+
yield JSON.stringify(part).slice(1, -1); at = end;
|
|
47
|
+
}
|
|
48
|
+
yield '"';
|
|
49
|
+
}
|
|
50
|
+
function* dataParts(value, depth, active, sorted) {
|
|
51
|
+
if (typeof value === 'string') { yield* quoted(value); return; }
|
|
52
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'number' && Number.isFinite(value) && !Object.is(value, -0)) {
|
|
53
|
+
yield JSON.stringify(value); return;
|
|
54
|
+
}
|
|
55
|
+
const {array, descriptors, keys} = fields(value, depth, active, sorted);
|
|
18
56
|
active.add(value);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
57
|
+
yield array ? '[' : '{';
|
|
58
|
+
for (let i = 0; i < keys.length; i++) {
|
|
59
|
+
if (i) yield ',';
|
|
60
|
+
const key = keys[i];
|
|
61
|
+
if (!array) { yield* quoted(key, false); yield ':'; }
|
|
62
|
+
yield* dataParts(descriptors[key].value, depth + 1, active, sorted);
|
|
63
|
+
}
|
|
64
|
+
yield array ? ']' : '}';
|
|
65
|
+
active.delete(value);
|
|
23
66
|
}
|
|
24
|
-
export function
|
|
67
|
+
export function canonicalParts(value, depth = 0, active = new Set()) { return dataParts(value, depth, active, true); }
|
|
68
|
+
export function jsonParts(value) { return dataParts(value, 0, new Set(), false); }
|
|
69
|
+
export function canonicalJSON(value, depth = 0, active = new Set()) { return Array.from(canonicalParts(value, depth, active)).join(''); }
|
|
25
70
|
export function exactKeys(value, keys) {
|
|
26
71
|
return value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
27
72
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
73
|
+
// TextEncoder's exact byte count, without allocating an encoded copy. Whole-character
|
|
74
|
+
// admission is performed in this same pass when the document owner requests it.
|
|
75
|
+
export function utf8Length(text, whole = false) {
|
|
76
|
+
let bytes = 0;
|
|
77
|
+
for (let at = 0; at < text.length; at++) {
|
|
78
|
+
const code = text.charCodeAt(at);
|
|
79
|
+
if (code < 0x80) bytes++;
|
|
80
|
+
else if (code < 0x800) bytes += 2;
|
|
81
|
+
else if ((code & 0xfc00) === 0xd800 && (text.charCodeAt(at + 1) & 0xfc00) === 0xdc00) { bytes += 4; at++; }
|
|
82
|
+
else { if (whole && code >= 0xd800 && code <= 0xdfff) throw fault('document is not whole-character text'); bytes += 3; }
|
|
83
|
+
}
|
|
84
|
+
return bytes;
|
|
85
|
+
}
|
|
86
|
+
export function documentBytes(text) {
|
|
87
|
+
if (typeof text !== 'string') throw fault('document is not whole-character text');
|
|
88
|
+
const bytes = utf8Length(text, true);
|
|
89
|
+
if (bytes > 64 * 1024 * 1024) throw fault('document exceeds the editor text limit');
|
|
90
|
+
return bytes;
|
|
32
91
|
}
|
|
92
|
+
export function documentText(text) { documentBytes(text); return text; }
|
|
33
93
|
export const nonnegative = value => Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0);
|