rapier-html 1.2.52 → 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.52",
3
+ "version": "1.2.55",
4
4
  "path": "dist/rapier.html",
5
- "bytes": 6445804,
6
- "sha256": "3c4983d36a53d73ff6e72cfd3b03bd819b7ffbc560c6ebdd2865e8013422a104",
7
- "builtAt": "2026-10-31T05:43:35.807Z",
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,11 +2,6 @@
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
5
  Try it: `npx rapier-html@1.2.52 notes.md`, then open `notes.rapier.html` in any browser.
11
6
  Install: `npm install rapier-html@1.2.52`.
12
7
 
@@ -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};