rapier-html 1.2.51 → 1.2.55

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.2.51",
3
+ "version": "1.2.55",
4
4
  "path": "dist/rapier.html",
5
- "bytes": 5977150,
6
- "sha256": "b0725f815f0ad0c9e6dbe53570710f48865f09646ed8ad23b3fdf67f50f1a655",
7
- "builtAt": "2026-10-28T13:48:17.410Z",
5
+ "bytes": 6613729,
6
+ "sha256": "66dd1e87abc3cd368fdd5481ce4ad8816ea5235fb08f1788fbfaa67ed111581f",
7
+ "builtAt": "2026-11-03T23:25:11.656Z",
8
8
  "node": "v22.23.4",
9
9
  "canonical": true,
10
10
  "packing": "zopfli",
package/README.md CHANGED
@@ -2,13 +2,8 @@
2
2
 
3
3
  Turn Markdown into one offline HTML file that contains the full Rapier editor.
4
4
 
5
- <picture>
6
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jackskip22/rapier-plugins/main/assets/write-390-dark.png">
7
- <img src="https://raw.githubusercontent.com/jackskip22/rapier-plugins/main/assets/write-390-light.png" alt="The Rapier editor on a phone: a launch plan with an agent's change marked" width="320">
8
- </picture>
9
-
10
- Try it: `npx rapier-html@1.2.51 notes.md`, then open `notes.rapier.html` in any browser.
11
- Install: `npm install rapier-html@1.2.51`.
5
+ Try it: `npx rapier-html@1.2.52 notes.md`, then open `notes.rapier.html` in any browser.
6
+ Install: `npm install rapier-html@1.2.52`.
12
7
 
13
8
  It opens on a phone or computer, with no account. People read, edit, draw and paint; agents hand over documents, show
14
9
  what changed and get the edited copy back.
@@ -27,14 +22,14 @@ The file needs no hosted workspace.
27
22
  Needs Node 22 or newer.
28
23
 
29
24
  ```sh
30
- npx -- rapier-html@1.2.51 notes.md # the editor, notes.md inside it, written beside the file
31
- npx -- rapier-html@1.2.51 notes.md --view draw # opens on Draw: sketch and paint, the document behind it
32
- npx -- rapier-html@1.2.51 notes.md --include draw,math # carries Draw and Math (default: what the document needs)
33
- npx -- rapier-html@1.2.51 notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
34
- npx -- rapier-html@1.2.51 revised.md --compare original.md # opens showing what changed
35
- npx -- rapier-html@1.2.51 notes.md --return <return_url> --return-expires-at <return_expires_at> # Send back
36
- npx -- rapier-html@1.2.51 notes.md out.html # a named output
37
- npm install rapier-html@1.2.51 # as a library: writePage, unwrap
25
+ npx -- rapier-html@1.2.52 notes.md # the editor, notes.md inside it, written beside the file
26
+ npx -- rapier-html@1.2.52 notes.md --view draw # opens on Draw: sketch and paint, the document behind it
27
+ npx -- rapier-html@1.2.52 notes.md --include draw,math # carries Draw and Math (default: what the document needs)
28
+ npx -- rapier-html@1.2.52 notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
29
+ npx -- rapier-html@1.2.52 revised.md --compare original.md # opens showing what changed
30
+ npx -- rapier-html@1.2.52 notes.md --return <return_url> --return-expires-at <return_expires_at> # Send back
31
+ npx -- rapier-html@1.2.52 notes.md out.html # a named output
32
+ npm install rapier-html@1.2.52 # as a library: writePage, unwrap
38
33
  ```
39
34
 
40
35
  The first `--` is for npm, so `--help` reaches the command. It never overwrites a file.
@@ -1,32 +1,71 @@
1
1
  // SPDX-License-Identifier: MIT
2
+ function countLines(source, from, to) {
3
+ let lines = 0;
4
+ for (let i = from; i < to; i++) {
5
+ const code = source.charCodeAt(i);
6
+ if (code === 0x0a || code === 0x0d) lines++;
7
+ }
8
+ return lines;
9
+ }
10
+
2
11
  export function createMathMatcher(source, {singleDollar = false, brackets = true} = {}) {
3
12
  if (typeof source !== 'string') throw new TypeError('Math boundaries require source text.');
4
- const markers = new Map(), next = Object.create(null);
5
- let slashes = 0, line = 0;
6
- for (let at = 0; at < source.length;) {
7
- const char = source[at];
8
- if (char === '\\') { slashes++; at++; continue; }
9
- if (char === '\n' || char === '\r') line++;
10
- if (char === '$') {
13
+ const len = source.length;
14
+ if (len === 0 || (source.indexOf('$') < 0 && (!brackets || source.indexOf('\\') < 0))) {
15
+ return () => null;
16
+ }
17
+ const markers = new Map(), list = [], next = Object.create(null);
18
+ if (!brackets) {
19
+ let line = 0, lastAt = 0;
20
+ for (let at = source.indexOf('$'); at >= 0; at = source.indexOf('$', at)) {
21
+ line += countLines(source, lastAt, at);
22
+ let slashes = 0;
23
+ for (let s = at - 1; s >= 0 && source.charCodeAt(s) === 0x5c; s--) slashes++;
11
24
  let end = at + 1;
12
- while (source[end] === '$') end++;
25
+ while (end < len && source.charCodeAt(end) === 0x24) end++;
13
26
  const count = end - at;
14
- if (!(slashes & 1) && (count === 2 || count === 1 && singleDollar))
15
- markers.set(at, {at, marker: count === 2 ? '$$' : '$', line});
27
+ if (!(slashes & 1) && (count === 2 || count === 1 && singleDollar)) {
28
+ const entry = {at, marker: count === 2 ? '$$' : '$', line, close: undefined};
29
+ markers.set(at, entry);
30
+ list.push(entry);
31
+ }
32
+ lastAt = end;
16
33
  at = end;
17
- } else {
18
- if (brackets && slashes & 1 && '()[]'.includes(char))
19
- markers.set(at - 1, {at: at - 1, marker: '\\' + char, line});
20
- at++;
21
34
  }
22
- slashes = 0;
35
+ } else {
36
+ let slashes = 0, line = 0;
37
+ for (let at = 0; at < len;) {
38
+ const code = source.charCodeAt(at);
39
+ if (code === 0x5c) { slashes++; at++; continue; }
40
+ if (code === 0x0a || code === 0x0d) line++;
41
+ if (code === 0x24) {
42
+ let end = at + 1;
43
+ while (end < len && source.charCodeAt(end) === 0x24) end++;
44
+ const count = end - at;
45
+ if (!(slashes & 1) && (count === 2 || count === 1 && singleDollar)) {
46
+ const entry = {at, marker: count === 2 ? '$$' : '$', line, close: undefined};
47
+ markers.set(at, entry);
48
+ list.push(entry);
49
+ }
50
+ at = end;
51
+ } else {
52
+ if (slashes & 1 && (code === 0x28 || code === 0x29 || code === 0x5b || code === 0x5d)) {
53
+ const entry = {at: at - 1, marker: '\\' + source[at], line, close: undefined};
54
+ markers.set(at - 1, entry);
55
+ list.push(entry);
56
+ }
57
+ at++;
58
+ }
59
+ slashes = 0;
60
+ }
23
61
  }
24
- for (const entry of [...markers.values()].reverse()) {
62
+ for (let i = list.length - 1; i >= 0; i--) {
63
+ const entry = list[i];
25
64
  const closing = entry.marker === '\\(' ? '\\)' : entry.marker === '\\[' ? '\\]' : entry.marker;
26
65
  entry.close = next[closing];
27
66
  next[entry.marker] = entry;
28
67
  }
29
- return function match(start, limit = source.length) {
68
+ return function match(start, limit = len) {
30
69
  const open = markers.get(start), close = open?.close;
31
70
  if (!close || open.marker === '\\)' || open.marker === '\\]' || close.at + close.marker.length > limit) return null;
32
71
  const bodyStart = start + open.marker.length, bodyEnd = close.at;
@@ -1,6 +1,19 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  import {createMathMatcher} from "./delimiters.mjs";
3
3
 
4
+ function isMathFenceInfo(info) {
5
+ if (!info || info.length < 4) return false;
6
+ let start = 0;
7
+ const len = info.length;
8
+ while (start < len && info.charCodeAt(start) <= 0x20) start++;
9
+ if (start + 4 > len) return false;
10
+ if ((info.charCodeAt(start) | 32) !== 0x6d ||
11
+ (info.charCodeAt(start + 1) | 32) !== 0x61 ||
12
+ (info.charCodeAt(start + 2) | 32) !== 0x74 ||
13
+ (info.charCodeAt(start + 3) | 32) !== 0x68) return false;
14
+ return start + 4 === len || info.charCodeAt(start + 4) <= 0x20;
15
+ }
16
+
4
17
  export function installMathRules(md, {singleDollar = false, brackets = true, ruleName = 'rapier_math', inLink = false} = {}) {
5
18
  const authored = new WeakMap();
6
19
  md.core.ruler.before('normalize', ruleName + '_source', state => {
@@ -9,13 +22,13 @@ export function installMathRules(md, {singleDollar = false, brackets = true, rul
9
22
  md.core.ruler.after('block', ruleName + '_source_release', state => authored.delete(state.tokens));
10
23
  const tokenize = md.block.tokenize;
11
24
  md.block.tokenize = function(state, startLine, endLine) {
12
- const first = state.tokens.length;
25
+ const first = Math.max(state.tokens.length, state._mathScanned || 0);
13
26
  try { return tokenize.call(this, state, startLine, endLine); }
14
27
  finally {
15
- for (let index = first; index < state.tokens.length; index++) {
28
+ const len = state.tokens.length;
29
+ for (let index = first; index < len; index++) {
16
30
  const token = state.tokens[index];
17
- if (token.type !== 'fence' || !token.map || token.meta?.mathSource !== undefined ||
18
- String(token.info || '').trim().split(/\s+/)[0].toLowerCase() !== 'math') continue;
31
+ if (token.type !== 'fence' || !token.map || token.meta?.mathSource !== undefined || !isMathFenceInfo(token.info)) continue;
19
32
  const held = authored.get(state.tokens), source = held?.source ?? state.src;
20
33
  let value;
21
34
  if (!state.rapierSourceSlice && source === state.src) value = state.getLines(token.map[0], token.map[1], state.blkIndent, false);
@@ -42,6 +55,7 @@ export function installMathRules(md, {singleDollar = false, brackets = true, rul
42
55
  }
43
56
  token.meta = {...token.meta, mathSource: value};
44
57
  }
58
+ state._mathScanned = len;
45
59
  }
46
60
  };
47
61
  const renderText = md.renderer.renderInlineAsText;
@@ -62,7 +76,8 @@ export function installMathRules(md, {singleDollar = false, brackets = true, rul
62
76
  }
63
77
  md.inline.ruler.before('escape', ruleName, (state, silent) => {
64
78
  if (!inLink && state.linkLevel > 0) return false;
65
- if (state.src[state.pos] !== '$' && state.src[state.pos] !== '\\') return false;
79
+ const ch = state.src.charCodeAt(state.pos);
80
+ if (ch !== 0x24 && (!brackets || ch !== 0x5c)) return false;
66
81
  const span = match(state, state.pos, state.posMax);
67
82
  if (!span) return false;
68
83
  if (!silent) {
@@ -77,7 +92,8 @@ export function installMathRules(md, {singleDollar = false, brackets = true, rul
77
92
  md.block.ruler.before('fence', ruleName, (state, startLine, endLine, silent) => {
78
93
  if (state.sCount[startLine] - state.blkIndent >= 4) return false;
79
94
  const start = state.bMarks[startLine] + state.tShift[startLine];
80
- if (state.src.slice(start, start + 2) !== '$$' && (!brackets || state.src.slice(start, start + 2) !== '\\[')) return false;
95
+ const c0 = state.src.charCodeAt(start), c1 = state.src.charCodeAt(start + 1);
96
+ if (!(c0 === 0x24 && c1 === 0x24) && (!brackets || !(c0 === 0x5c && c1 === 0x5b))) return false;
81
97
  const span = match(state, start, state.eMarks[endLine - 1]);
82
98
  if (!span?.displayMode) return false;
83
99
  let last = startLine;
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // Pure text and markdown-it tokens: no DOM, no byte decoding, no globalThis.Rapier* (MIT kit; decoders stay in images/assets.mjs).
3
3
  import {isWordSidecarBlock} from "./word-sidecar.mjs";
4
+ import {tuneMarkdownEngine} from "./md-engine.mjs";
4
5
  import {sourceFacts, sourceView, sourceEdit} from "./source.mjs";
5
6
 
6
7
  export const IMAGE_LIMITS = Object.freeze({bytes: 16 * 1024 * 1024, sourceChars: 64 * 1024 * 1024, dimension: 16384, pixels: 24000000, assets: 1024});
@@ -66,27 +67,22 @@ function parserFor(factory = configured || globalThis.markdownit) {
66
67
  if (configured && factory === globalThis.markdownit) return configured;
67
68
  if (typeof factory !== 'function') return fail('markdown_parser_unavailable');
68
69
  let parser = parsers.get(factory);
69
- if (!parser) { parser = installMarkdownImages(factory({html: true, maxNesting: 20})); parsers.set(factory, parser); }
70
+ if (!parser) { parser = installMarkdownImages(tuneMarkdownEngine(factory({html: true, maxNesting: 20}))); parsers.set(factory, parser); }
70
71
  return parser;
71
72
  }
72
73
  export {parserFor as markdownParser};
73
74
  export const markdownBodyOffset = source => splitSource ? splitSource(source).bodyOffset : 0;
74
75
 
75
- const BLOCK_WINDOW = 1024, TABLE_STEP = 128, WINDOW_END = Symbol('block window end');
76
- const LINE_TABLES = ['bMarks', 'eMarks', 'tShift', 'sCount', 'bsCount'];
77
- const lineIndex = key => typeof key === 'string' && /^(?:0|[1-9]\d*)$/.test(key);
78
- let unheldLine = null;
79
- const unheldLines = new Proxy(Array.prototype, {
80
- get: (target, key, table) => unheldLine && lineIndex(key) ? unheldLine(table, Number(key)) : Reflect.get(target, key, table),
81
- set: (target, key, value, table) => unheldLine && lineIndex(key) ? unheldLine(table, Number(key), true, value) : Reflect.set(target, key, value, table),
82
- });
76
+ const BLOCK_WINDOW = 1024, WINDOW_END = Symbol('block window end');
83
77
 
84
78
  export function eachBlockWindow(source, bodyOffset, parser, env, visit, keepNul = false) {
85
79
  for (const window of blockWindows(source, bodyOffset, parser, env, keepNul)) visit(window);
86
80
  }
87
81
 
88
82
  export function* blockWindows(source, bodyOffset, parser, env, keepNul = false) {
89
- const lineEnds = source.slice(bodyOffset).replace(/\r\n?/g, '\n'), body = keepNul ? lineEnds : lineEnds.replace(/\0/g, '\ufffd');
83
+ const rawSlice = bodyOffset ? source.slice(bodyOffset) : source;
84
+ const lineEnds = rawSlice.indexOf('\r') >= 0 ? rawSlice.replace(/\r\n?/g, '\n') : rawSlice;
85
+ const body = keepNul || lineEnds.indexOf('\0') < 0 ? lineEnds : lineEnds.replace(/\0/g, '\ufffd');
90
86
  let pair = source.indexOf('\r\n', bodyOffset), shift = bodyOffset;
91
87
  const sourceOffset = at => {
92
88
  while (pair >= 0 && at + shift > pair) { shift++; pair = source.indexOf('\r\n', pair + 2); }
@@ -94,89 +90,52 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
94
90
  return at + shift;
95
91
  };
96
92
  const tokens = [];
97
- let origin = 0, first = 0, state;
93
+ const state = new parser.block.State(body, parser, env, tokens);
94
+ state.rapierSourceSlice = (from, to) => source.slice(sourceOffset(from), sourceOffset(to));
95
+ const totalLines = state.lineMax;
96
+ let origin = 0, first = 0, windowStartLine = 0;
98
97
  const report = () => ({tokens, firstLine: first, lineStart: line => {
99
98
  if (!line) return sourceOffset(origin);
100
- const end = Object.hasOwn(state.eMarks, line - 1) ? state.eMarks[line - 1] : body.length;
99
+ const absLine = first + line - 1;
100
+ const end = absLine >= 0 && absLine < totalLines ? state.eMarks[absLine] : body.length;
101
101
  return end < body.length ? sourceOffset(end + 1) : undefined;
102
102
  }});
103
- let lines = 0;
104
- for (let at = body.indexOf('\n'); at !== -1; at = body.indexOf('\n', at + 1)) lines++;
105
- if (lines <= BLOCK_WINDOW) {
106
- state = new parser.block.State(body, parser, env, tokens);
107
- state.rapierSourceSlice = (from, to) => source.slice(sourceOffset(from), sourceOffset(to));
108
- parser.block.tokenize(state, state.line, state.lineMax);
103
+ if (totalLines <= BLOCK_WINDOW) {
104
+ parser.block.tokenize(state, state.line, totalLines);
109
105
  yield report(); return;
110
106
  }
111
- state = new parser.block.State(body.slice(body.lastIndexOf('\n') + 1), parser, env, tokens);
112
- state.rapierSourceSlice = (from, to) => source.slice(sourceOffset(from), sourceOffset(to));
113
- lines += state.lineMax;
114
- state.src = body;
115
- const tables = new WeakMap();
116
- for (const name of LINE_TABLES) { state[name].length = 0; tables.set(Object.setPrototypeOf(state[name], unheldLines), name); }
117
- let frontier = 0, ended = false, betweenBlocks = false;
118
- const fill = line => {
119
- while (!ended && line >= state.bMarks.length) {
120
- const held = state.bMarks.length;
121
- let until = frontier;
122
- for (let more = held ? Math.max(TABLE_STEP, held - BLOCK_WINDOW) : BLOCK_WINDOW; more && until < body.length; more--) {
123
- const end = body.indexOf('\n', until);
124
- until = end < 0 ? body.length : end + 1;
125
- }
126
- const scanned = new parser.block.State(body.slice(frontier, until), parser, env, []);
127
- ended = until === body.length;
128
- const added = ended ? scanned.lineMax + 1 : scanned.lineMax;
129
- for (const name of LINE_TABLES) {
130
- const old = state[name], next = scanned[name], offset = name === 'bMarks' || name === 'eMarks' ? frontier : 0, table = [];
131
- for (let at = 0; at < held; at++) table.push(old[at]);
132
- for (let at = 0; at < added; at++) table.push(next[at] + offset);
133
- old.length = 0;
134
- tables.set(table, name);
135
- state[name] = Object.setPrototypeOf(table, unheldLines);
136
- }
137
- frontier = until;
138
- }
139
- };
140
- const answer = (table, line, write, value) => {
141
- const name = tables.get(table);
142
- if (line >= state[name].length) {
143
- if (betweenBlocks) throw WINDOW_END;
144
- fill(line);
145
- }
146
- const current = state[name], held = Object.hasOwn(current, line);
147
- if (!write) return held ? current[line] : undefined;
148
- if (held) current[line] = value;
149
- else Object.defineProperty(current, line, {value, writable: true, enumerable: true, configurable: true});
150
- return true;
151
- };
152
107
  const skipEmptyLines = state.skipEmptyLines;
153
108
  state.skipEmptyLines = function (line) {
154
- if (this.level || line !== this.line || !line) return skipEmptyLines.call(this, line);
155
- if (line >= BLOCK_WINDOW) throw WINDOW_END;
156
- betweenBlocks = true;
157
- line = skipEmptyLines.call(this, line);
158
- betweenBlocks = false;
159
- return line;
160
- };
161
- for (;;) {
162
- for (let at = origin; at < body.length; at++) {
163
- const code = body.charCodeAt(at);
164
- if (code === 10) { origin = at + 1; first++; }
165
- else if (!parser.utils.isSpace(code)) break;
109
+ if (this.level || line !== this.line || line === windowStartLine) return skipEmptyLines.call(this, line);
110
+ if (line - windowStartLine >= BLOCK_WINDOW) throw WINDOW_END;
111
+ const next = skipEmptyLines.call(this, line);
112
+ if (next - windowStartLine >= BLOCK_WINDOW) {
113
+ this.line = windowStartLine + BLOCK_WINDOW;
114
+ throw WINDOW_END;
166
115
  }
167
- frontier = origin; ended = false; betweenBlocks = false;
168
- state.line = 0; state.lineMax = lines - first; state.rapierFirstLine = first;
169
- const outer = unheldLine;
170
- unheldLine = answer;
116
+ return next;
117
+ };
118
+ for (let startLine = 0; startLine < totalLines;) {
119
+ startLine = skipEmptyLines.call(state, startLine);
120
+ if (startLine >= totalLines) break;
121
+ windowStartLine = startLine;
122
+ first = startLine;
123
+ origin = startLine === 0 ? 0 : state.eMarks[startLine - 1] + 1;
124
+ state.line = startLine;
125
+ state.rapierFirstLine = first;
171
126
  let finished = true;
172
- try { parser.block.tokenize(state, 0, state.lineMax); }
127
+ try { parser.block.tokenize(state, startLine, totalLines); }
173
128
  catch (error) { if (error !== WINDOW_END) throw error; finished = false; }
174
- finally { unheldLine = outer; }
129
+ if (first !== 0) {
130
+ for (let i = 0; i < tokens.length; i++) {
131
+ const m = tokens[i].map;
132
+ if (m) { m[0] -= first; m[1] -= first; }
133
+ }
134
+ }
175
135
  yield report();
176
136
  if (finished) return;
177
137
  tokens.length = 0;
178
- origin = state.eMarks[state.line - 1] + 1; first += state.line;
179
- for (const name of LINE_TABLES) state[name].length = 0;
138
+ startLine = state.line;
180
139
  }
181
140
  }
182
141
 
@@ -184,7 +143,7 @@ const INVALID_PERCENT_PAYLOAD = /[^A-Za-z0-9._~:\/?@+$,;=%-]/;
184
143
  const isHexUnit = code => code >= 48 && code <= 57 || code >= 65 && code <= 70 || code >= 97 && code <= 102;
185
144
  let lastScan = null;
186
145
  export function dataImage(url) {
187
- if (typeof url !== 'string' || url.length > IMAGE_LIMITS.sourceChars) return null;
146
+ if (typeof url !== 'string' || url.length < 15 || (url.charCodeAt(0) | 32) !== 0x64 || url.charCodeAt(4) !== 0x3a || url.length > IMAGE_LIMITS.sourceChars) return null;
188
147
  if (url.length > 4096) {
189
148
  if (lastScan?.url !== url) lastScan = {url, image: scanDataImage(url)};
190
149
  return lastScan.image && {...lastScan.image};