rapier-html 1.1.101 → 1.1.104

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 CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "package": "rapier-html",
3
- "version": "1.1.101",
3
+ "version": "1.1.104",
4
4
  "path": "dist/rapier.html",
5
- "bytes": 3629386,
6
- "sha256": "a29d2c44a7aff82ae00f83f4ea37a9ab021399ba0698877ee7fa44ab8a904629",
7
- "builtAt": "2026-10-11T20:18:09.798Z",
5
+ "bytes": 3565876,
6
+ "sha256": "11ef86ddc14ffd60276d7266331ae27bd47c3a4a395aedf8c8601a3a395ae5d9",
7
+ "builtAt": "2026-10-12T17:02:11.693Z",
8
8
  "node": "v22.23.3",
9
9
  "canonical": true,
10
10
  "packing": "zopfli",
@@ -14,15 +14,15 @@
14
14
  "delivery": "built-in",
15
15
  "kind": "script",
16
16
  "element": "rapier-plugin-math",
17
- "bytes": 803451,
18
- "packedBytes": 544485,
17
+ "bytes": 714051,
18
+ "packedBytes": 495012,
19
19
  "files": [
20
20
  {
21
21
  "id": "rapier-math",
22
- "name": "rapier-math-1.0.1.js",
23
- "version": "1.0.1",
24
- "bytes": 803451,
25
- "sri": "1kdn6mohx3a5MAiwqRWY+6oviUHUheHfY1VMsxjDsUrn7woTaa7FSKo4juYtuNaF"
22
+ "name": "rapier-math-1.1.0.js",
23
+ "version": "1.1.0",
24
+ "bytes": 714051,
25
+ "sri": "dYIPrqCf4rrTcAKw1Zzzo7vujbNtk2C7Zik/tIYOaLVT0U66WOXcGNE6waFbKych"
26
26
  }
27
27
  ]
28
28
  },
@@ -31,15 +31,15 @@
31
31
  "delivery": "built-in",
32
32
  "kind": "script",
33
33
  "element": "rapier-plugin-mermaid",
34
- "bytes": 294857,
35
- "packedBytes": 103232,
34
+ "bytes": 292169,
35
+ "packedBytes": 102242,
36
36
  "files": [
37
37
  {
38
38
  "id": "rapier-mermaid",
39
- "name": "rapier-diagrams-1.1.101.js",
40
- "version": "1.1.101",
41
- "bytes": 294857,
42
- "sri": "fmQHeVOJnX1gwaqVihnI/FFSP8hm+Bh/dLQWQ17yvUAJoF9tFd2d6mY5tZ249t8u"
39
+ "name": "rapier-diagrams-1.1.104.js",
40
+ "version": "1.1.104",
41
+ "bytes": 292169,
42
+ "sri": "b+ByE92VMwroMyB4gJIAdisjv+ONeQERVFCCP9nBfchHTUxACYXg1YNbdm2bp7FT"
43
43
  }
44
44
  ]
45
45
  },
@@ -91,8 +91,8 @@
91
91
  "files": [
92
92
  {
93
93
  "id": "rapier-font-subset",
94
- "name": "rapier-font-subset-1.js",
95
- "version": "1",
94
+ "name": "rapier-font-subset-2.js",
95
+ "version": "2",
96
96
  "bytes": 1214356,
97
97
  "sri": "q4mHA3+u3N3Yt8z1wisCEXQqWNo5UhHZ0nUESid8BGNZz6BtV8dSUlVOvyndc0Tj"
98
98
  }
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.1.101 notes.md`.
5
+ changed and receive the person's edited copy through the door. Start with `npx rapier-html@1.1.104 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.1.101 notes.md # the editor, notes.md inside it, written beside the file
22
- npx -- rapier-html@1.1.101 notes.md --view draw # opens on Draw: sketch and paint, the document behind it
23
- npx -- rapier-html@1.1.101 notes.md --view notes # opens the Notes library
24
- npx -- rapier-html@1.1.101 notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
25
- npx -- rapier-html@1.1.101 revised.md --compare original.md # opens showing what changed
26
- npx -- rapier-html@1.1.101 notes.md --return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT" # Send back
27
- npx -- rapier-html@1.1.101 notes.md out.html # a named output
28
- npm install rapier-html@1.1.101 # as a library: wrap, unwrap
21
+ npx -- rapier-html@1.1.104 notes.md # the editor, notes.md inside it, written beside the file
22
+ npx -- rapier-html@1.1.104 notes.md --view draw # opens on Draw: sketch and paint, the document behind it
23
+ npx -- rapier-html@1.1.104 notes.md --view notes # opens the Notes library
24
+ npx -- rapier-html@1.1.104 notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
25
+ npx -- rapier-html@1.1.104 revised.md --compare original.md # opens showing what changed
26
+ npx -- rapier-html@1.1.104 notes.md --return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT" # Send back
27
+ npx -- rapier-html@1.1.104 notes.md out.html # a named output
28
+ npm install rapier-html@1.1.104 # 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.
@@ -47,7 +47,7 @@ editor and lightweight reader (about 190 kB gzipped). The agent edits live; the
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
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.1.101 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)).
50
+ 2. **Deliver an offline editor.** Run `npx rapier-html@1.1.104 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.
@@ -21,8 +21,8 @@ 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`), the layout model and line planner
25
- (`layout/model.mjs`, `layout/line-plan.mjs`, `layout/typography.mjs`),
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`), the layout model and line planner
25
+ (`layout/model.mjs`, `layout/line-plan.mjs`, `layout/typography.mjs`, `layout/preparation.mjs`),
26
26
  the portable HTML and document metadata helpers (`spec/html-blocks.mjs`, `spec/frontmatter.mjs`,
27
27
  `spec/document-settings.mjs`, `spec/document-fonts.mjs`), the Will grammar (`agent/will.mjs`), the picture appendix reader and writer
28
28
  (`spec/md-assets.mjs`), the two shared-page reference readers (`tools/read-shared-page.mjs`,
@@ -71,18 +71,20 @@ 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' || typeof value.text !== 'string' ||
75
- typeof value.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(value.sha256) || sha256(value.text) !== value.sha256)
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} = 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 value.revision !== 'string' || !value.revision || value.revision.length > 256 || /[\u0000-\u001f\u007f]/.test(value.revision))
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 value.name !== 'string' || !value.name || [...value.name].length > 256 || /[\\/\u0000-\u001f\u007f]/.test(value.name))
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 value.by !== 'string' || !value.by.trim() || value.by.length > 96 || /[\u0000-\u001f\u007f]/.test(value.by))
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 value.at !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(value.at) || !Number.isFinite(Date.parse(value.at)))
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
- return {text: value.text, sha256: value.sha256, revision: value.revision, name: value.name, by: value.by, at: value.at};
87
+ return {text, sha256: digest, revision, name, by, at};
86
88
  }
87
89
  const escapeAttribute = value => value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
88
90
  const readAttribute = value => value.replace(/&(quot|lt|gt|amp);/g, (_, name) => ({quot: '"', lt: '<', gt: '>', amp: '&'})[name]);
@@ -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
- export function canonicalJSON(value, depth = 0, active = new Set()) {
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
- const encode = item => canonicalJSON(item, depth + 1, active);
20
- const out = array ? '[' + Array.from({length: value.length}, (_, i) => encode(descriptors[i].value)).join(',') + ']'
21
- : '{' + keys.sort().map(key => JSON.stringify(key) + ':' + encode(descriptors[key].value)).join(',') + '}';
22
- active.delete(value); return out;
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 plainData(value) { return JSON.parse(canonicalJSON(typeof value === 'string' ? JSON.parse(value) : value)); }
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
- export function documentText(text) {
29
- if (typeof text !== 'string' || /[\uD800-\uDFFF]/u.test(text)) throw fault('document is not whole-character text');
30
- if (new TextEncoder().encode(text).length > 64 * 1024 * 1024) throw fault('document exceeds the editor text limit');
31
- return text;
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);