rapier-markdown-kit 1.1.99 → 1.1.101

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.
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // Paired source placement, shared by live editing and canonical-history proof.
3
3
  import {_rapierTransformSplices, _rapierValidAuthored, _rapierHistoryEffects} from './journal-records.mjs';
4
+ import {projectInverseSource} from './inverse-source.mjs';
4
5
  import {sha256} from './hash.mjs';
5
6
  import {canonicalJSON} from './data.mjs';
6
7
 
@@ -169,7 +170,38 @@ function authoredReplay(base, records, append = null) {
169
170
  const origins = new Map(), byId = new Map(), originalOrder = [], cache = new Map(), frontier = new Set();
170
171
  const effects = _rapierHistoryEffects(append ? [...records, append] : records);
171
172
  if (!effects.ok) throw new RangeError('canonical_origin_invalid');
172
- let source = base;
173
+ const referenced = new Set();
174
+ let sharedInverses = false;
175
+ for (const refs of effects.references.values()) for (const id of refs) {
176
+ if (referenced.has(id)) sharedInverses = true;
177
+ referenced.add(id);
178
+ }
179
+ const projectInverse = (text, placed, id) => {
180
+ const refs = effects.references.get(id), before = _rapierHistoryEffects(placed);
181
+ const after = _rapierHistoryEffects([...placed, {transaction: {id, sourceTransactionIds: refs}}]);
182
+ if (!before.ok || !after.ok) throw new RangeError('canonical_origin_invalid');
183
+ const result = projectInverseSource({source: text, initialSource: base,
184
+ rows: placed.map(record => ({record, splices: record.splices})), effects: before}, new Set(refs), after);
185
+ if (!result.ok) throw new RangeError('canonical_origin_invalid');
186
+ return result;
187
+ };
188
+ // Only immutable text digests are reused, within this one replay. Every
189
+ // authored-source comparison and physical placement proof still runs.
190
+ const textHashes = new Map();
191
+ const hashText = text => {
192
+ if (typeof text !== 'string') return sha256(text);
193
+ let digest = textHashes.get(text);
194
+ if (digest === undefined) { digest = sha256(text); textHashes.set(text, digest); }
195
+ return digest;
196
+ };
197
+ const same = (a, b) => canonicalJSON(a) === canonicalJSON(b);
198
+ // Only owner-created ordered views of already admitted string IDs use this.
199
+ const sameIds = (a, b) => {
200
+ if (a.length !== b.length) return false;
201
+ for (let index = 0; index < a.length; index++) if (a[index] !== b[index]) return false;
202
+ return true;
203
+ };
204
+ let source = base, linearPrefix = 0;
173
205
  for (const record of append ? [...records, append] : records) {
174
206
  const id = record.transaction.id;
175
207
  if (byId.has(id)) throw new RangeError('canonical_origin_invalid');
@@ -189,6 +221,11 @@ function authoredReplay(base, records, append = null) {
189
221
  })()))) throw new RangeError('canonical_origin_invalid');
190
222
  const refs = effects.references.get(id) || [];
191
223
  if (refs.some(id => !seen.has(id))) throw new RangeError('canonical_origin_invalid');
224
+ // An explicit origin can prove the same linear placement as an implicit one.
225
+ // Preserve the origin, but do not reconstruct every growing prefix to prove it again.
226
+ if (linearPrefix === originalOrder.length && same(origin.basis, [...frontier].sort()) &&
227
+ same(origin.splices, record.splices) && (!Object.hasOwn(record, 'authored') || origin.source === sha256(source)))
228
+ linearPrefix++;
192
229
  for (const prior of seen) frontier.delete(prior); frontier.add(id);
193
230
  origins.set(id, origin); byId.set(id, record); originalOrder.push(id);
194
231
  if (record !== append) {
@@ -196,7 +233,6 @@ function authoredReplay(base, records, append = null) {
196
233
  if (source === null) throw new RangeError('canonical_origin_invalid');
197
234
  }
198
235
  }
199
- const same = (a, b) => canonicalJSON(a) === canonicalJSON(b);
200
236
  const ordered = (ids, selected = null) => {
201
237
  const order = [], known = new Set(ids), waiting = new Map(), dependents = new Map(), ready = [];
202
238
  const rank = id => selected ? Number(selected.has(id)) : Number(origins.get(id).splices.some(row => row.removed.length));
@@ -229,17 +265,60 @@ function authoredReplay(base, records, append = null) {
229
265
  closure.add(prior); pending.push(...origins.get(prior).basis); }
230
266
  return closure;
231
267
  };
232
- const canonicalOrder = ordered(originalOrder), firstExplicit = (append ? [...records, append] : records).findIndex(record => Object.hasOwn(record, 'authored'));
233
- const implicitPrefix = firstExplicit < 0 ? records.length : firstExplicit;
234
- const materialize = order => {
235
- const key = canonicalJSON(order);
268
+ const canonicalOrder = ordered(originalOrder);
269
+ // Only the three immediate fresh-spread call sites use this entry. Nothing
270
+ // retains allocation trust after an array has crossed an observable method.
271
+ const literalKey = order => {
272
+ // An input iterator can supply arbitrary values. Preserve the whole-array
273
+ // depth/refusal path before any per-string encoding in that case.
274
+ for (let index = 0; index < order.length; index++)
275
+ if (typeof order[index] !== 'string') return canonicalJSON(order);
276
+ // Keep each canonical primitive encoder call and its serializer behavior.
277
+ return '[' + Array.from({length: order.length}, (_, index) => canonicalJSON(order[index])).join(',') + ']';
278
+ };
279
+ const materialize = (order, rawSubmitted = false) => materializeKey(order, canonicalJSON(order), rawSubmitted);
280
+ const materializeLiteral = order => materializeKey(order, literalKey(order), false);
281
+ const materializeKey = (order, key, rawSubmitted) => {
236
282
  if (cache.has(key)) return cache.get(key);
237
- const placed = [], ids = new Set(), sourceIndices = new Map(); let text = base;
283
+ let prefixEnds = null;
284
+ const prefixKey = length => {
285
+ // The original submitted array keeps its existing slice/species/data gate.
286
+ // Only this module's own order allocations use the validated key string.
287
+ if (rawSubmitted) return canonicalJSON(order.slice(0, length));
288
+ if (!prefixEnds) {
289
+ prefixEnds = [1];
290
+ let quoted = false, depth = 0;
291
+ // Scan canonical JSON only: an escaped code unit cannot end a string,
292
+ // and commas inside strings or nested values cannot split this array.
293
+ for (let at = 1; at < key.length - 1; at++) {
294
+ const code = key.charCodeAt(at);
295
+ if (quoted) {
296
+ if (code === 92) at++;
297
+ else if (code === 34) quoted = false;
298
+ } else if (code === 34) quoted = true;
299
+ else if (code === 91 || code === 123) depth++;
300
+ else if (code === 93 || code === 125) depth--;
301
+ else if (code === 44 && depth === 0) prefixEnds.push(at);
302
+ }
303
+ if (key.length > 2) prefixEnds.push(key.length - 1);
304
+ }
305
+ return length === prefixEnds.length - 1 ? key : key.slice(0, prefixEnds[length]) + ']';
306
+ };
307
+ let placed = [], ids = new Set(), sourceIndices = new Map(), text = base;
238
308
  if (!order.length) { const empty = {text, records: [], sourceIndices}; cache.set(key, empty); return empty; }
239
- // An implicit linear prefix already is its authored view. Replay it once;
309
+ // A proved linear prefix already is its authored view. Replay it once;
240
310
  // reconstructing every growing prefix adds no evidence.
241
311
  let prefix = 0;
242
- while (prefix < order.length && prefix < implicitPrefix && order[prefix] === originalOrder[prefix]) {
312
+ // Resume only an exact ordered prefix already proved in this replay. Copy its
313
+ // mutable containers before extending it; transported rows retain their placement.
314
+ for (let length = order.length - 1; length > 0; length--) {
315
+ const prior = cache.get(prefixKey(length));
316
+ if (!prior) continue;
317
+ placed = prior.records.slice(); ids = new Set(order.slice(0, length));
318
+ sourceIndices = new Map(prior.sourceIndices); text = prior.text; prefix = length;
319
+ break;
320
+ }
321
+ while (prefix < order.length && prefix < linearPrefix && order[prefix] === originalOrder[prefix]) {
243
322
  const record = byId.get(order[prefix]); text = _rapierTransformSplices(text, record.splices);
244
323
  if (text === null) throw new RangeError('canonical_origin_invalid');
245
324
  placed.push(record); ids.add(order[prefix++]);
@@ -249,28 +328,53 @@ function authoredReplay(base, records, append = null) {
249
328
  const original = byId.get(id), origin = origins.get(id);
250
329
  if (!original || ids.has(id) || origin.basis.some(prior => !ids.has(prior)))
251
330
  throw new RangeError('canonical_origin_invalid');
252
- const closure = closureOf(origin.basis), basisOrder = canonicalOrder.filter(prior => closure.has(prior)), basis = materialize(basisOrder);
253
- if (sha256(basis.text) !== origin.source) throw new RangeError('canonical_origin_invalid');
331
+ const closure = closureOf(origin.basis);
332
+ if (closure.size === ids.size && [...closure].every(prior => ids.has(prior))) {
333
+ // The current view already is the complete authored context. There are no
334
+ // independent rows to cross, even when earlier acts were concurrent.
335
+ if (hashText(text) !== origin.source) throw new RangeError('canonical_origin_invalid');
336
+ const next = _rapierTransformSplices(text, origin.splices);
337
+ if (next === null) throw new RangeError('canonical_origin_invalid');
338
+ placed.push({...original, splices: origin.splices}); text = next; ids.add(id);
339
+ if (original.derivedCommentIndex != null) sourceIndices.set(id, origin.splices.map((_, index) => index));
340
+ cache.set(prefixKey(placed.length), {text, records: placed.slice(), sourceIndices: new Map(sourceIndices)});
341
+ continue;
342
+ }
343
+ const basisOrder = canonicalOrder.filter(prior => closure.has(prior)), basis = materialize(basisOrder);
344
+ if (hashText(basis.text) !== origin.source) throw new RangeError('canonical_origin_invalid');
254
345
  // Reconstruct causal context before crossing it. Physical gaps may already
255
346
  // have collapsed under a deletion; they cannot choose concurrent ordering.
256
- const contextOrder = canonicalOrder.filter(prior => ids.has(prior)), context = same(contextOrder, [...ids]) ? {text, records: placed} : materialize(contextOrder);
347
+ const contextOrder = canonicalOrder.filter(prior => ids.has(prior)), context = sameIds(contextOrder, [...ids]) ? {text, records: placed} : materialize(contextOrder);
257
348
  if (context.text !== text) throw new RangeError('canonical_origin_invalid');
258
349
  const alignedOrder = [...basisOrder, ...contextOrder.filter(prior => !closure.has(prior))];
259
- const aligned = same(alignedOrder, [...ids]) ? {text, records: placed} : materialize(alignedOrder);
350
+ const aligned = sameIds(alignedOrder, [...ids]) ? {text, records: placed} : materializeLiteral(alignedOrder);
260
351
  if (aligned.text !== text) throw new RangeError('canonical_origin_invalid');
261
352
  const crossed = aligned.records.slice(basisOrder.length);
262
- const moved = transposeRecord(basis.text, origin.splices, id, crossed);
353
+ let moved;
354
+ if (sharedInverses && effects.references.get(id)?.length) {
355
+ // Each inverse retains its own targets. First prove the exact intention
356
+ // in its causal source, then place that choice in the accepted effect
357
+ // graph. Another inverse may already suppress the same original bytes.
358
+ const authored = projectInverse(basis.text, basis.records, id);
359
+ if (!same(origin.splices, authored.splices) ||
360
+ _rapierTransformSplices(basis.text, origin.splices) !== authored.after)
361
+ throw new RangeError('canonical_origin_invalid');
362
+ const accepted = projectInverse(text, aligned.records, id);
363
+ moved = {text: accepted.after, splices: accepted.splices};
364
+ } else moved = transposeRecord(basis.text, origin.splices, id, crossed);
263
365
  if (_rapierTransformSplices(text, moved.splices) !== moved.text) throw new RangeError('canonical_origin_invalid');
264
366
  placed.push({...original, splices: moved.splices}); text = moved.text; ids.add(id);
265
367
  if (original.derivedCommentIndex != null) sourceIndices.set(id, moved.sourceIndices);
266
- cache.set(canonicalJSON(order.slice(0, placed.length)), {text, records: placed.slice(), sourceIndices: new Map(sourceIndices)});
368
+ cache.set(prefixKey(placed.length), {text, records: placed.slice(), sourceIndices: new Map(sourceIndices)});
267
369
  }
268
370
  const value = {text, records: placed, sourceIndices}; cache.set(key, value); return value;
269
371
  };
270
372
  const physicalOrder = append ? originalOrder.slice(0, -1) : originalOrder, physical = materialize(physicalOrder);
271
373
  if (physical.text !== source || physical.records.some((row, index) => !same(row.splices, records[index].splices)))
272
374
  throw new RangeError('canonical_origin_invalid');
273
- return {source, origins, originalOrder, materialize, ordered};
375
+ // Raw submission is selected here by a private call site, never an input flag.
376
+ return {source, origins, originalOrder, materialize, materializeLiteral,
377
+ materializeSubmitted: order => materialize(order, true), ordered};
274
378
  }
275
379
 
276
380
  export function authoredHistory(base, records, selected = null) {
@@ -306,13 +410,13 @@ export function authoredSourceReader(base, records) {
306
410
  export function transposeAuthored(base, records, incoming, submitted) {
307
411
  if (!incoming.authored || !Array.isArray(submitted)) throw new RangeError('canonical_origin_invalid');
308
412
  const replay = authoredReplay(base, records, incoming), id = incoming.transaction.id;
309
- const authored = replay.materialize([...submitted, id]);
413
+ const authored = replay.materializeLiteral([...submitted, id]);
310
414
  if (canonicalJSON(authored.records.at(-1).splices) !== canonicalJSON(incoming.splices))
311
415
  throw new RangeError('canonical_origin_invalid');
312
416
  const accepted = replay.materialize(replay.originalOrder), known = new Set([...submitted, id]);
313
- const reciprocal = replay.materialize([...submitted, id, ...replay.ordered(replay.originalOrder).filter(prior => !known.has(prior))]);
417
+ const reciprocal = replay.materializeLiteral([...submitted, id, ...replay.ordered(replay.originalOrder).filter(prior => !known.has(prior))]);
314
418
  if (reciprocal.text !== accepted.text) throw new RangeError('canonical_origin_invalid');
315
419
  return {text: accepted.text, splices: accepted.records.at(-1).splices,
316
- submittedBefore: replay.materialize(submitted).text, submitted: authored.text,
420
+ submittedBefore: replay.materializeSubmitted(submitted).text, submitted: authored.text,
317
421
  against: reciprocal.records.slice(submitted.length + 1)};
318
422
  }
@@ -0,0 +1,159 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // The licensed static faces retain the shipped variable font's exact glyphs and metrics.
3
+ // Browser ranges own placement; unsupported shaping stays in the original page paint.
4
+
5
+ const fontCache = new Map();
6
+ const familyName = value => value.trim().replace(/^['"]|['"]$/g, '');
7
+
8
+ function readFont(asset) {
9
+ if (fontCache.has(asset)) return fontCache.get(asset);
10
+ const bytes = Uint8Array.from(atob(asset.data), character => character.charCodeAt(0));
11
+ const view = new DataView(bytes.buffer), u16 = at => view.getUint16(at), i16 = at => view.getInt16(at), u32 = at => view.getUint32(at);
12
+ const tag = at => String.fromCharCode(...bytes.subarray(at, at + 4)), tables = new Map();
13
+ for (let at = 12, end = 12 + u16(4) * 16; at < end; at += 16) tables.set(tag(at), {at: u32(at + 8), length: u32(at + 12)});
14
+ const head = tables.get('head').at, hhea = tables.get('hhea').at, hmtx = tables.get('hmtx').at, cmap = tables.get('cmap').at;
15
+ const units = u16(head + 18), metricCount = u16(hhea + 34), glyphCount = u16(tables.get('maxp').at + 4);
16
+ let mapping = null;
17
+ for (let i = 0; i < u16(cmap + 2); i++) {
18
+ const at = cmap + 4 + i * 8, platform = u16(at), encoding = u16(at + 2), table = cmap + u32(at + 4), format = u16(table);
19
+ if ((platform === 0 || (platform === 3 && [1, 10].includes(encoding))) && [4, 12].includes(format))
20
+ if (!mapping || format > mapping.format) mapping = {at: table, format};
21
+ }
22
+ if (!mapping) throw new Error('The embedded PDF font has no Unicode character map.');
23
+ const glyph = code => {
24
+ const at = mapping.at;
25
+ if (mapping.format === 12) {
26
+ let low = 0, high = u32(at + 12) - 1;
27
+ while (low <= high) {const middle = (low + high) >> 1, row = at + 16 + middle * 12;
28
+ if (code < u32(row)) high = middle - 1; else if (code > u32(row + 4)) low = middle + 1; else return u32(row + 8) + code - u32(row);}
29
+ return 0;
30
+ }
31
+ if (code > 0xffff) return 0;
32
+ const count = u16(at + 6) / 2, ends = at + 14, starts = ends + count * 2 + 2, deltas = starts + count * 2, offsets = deltas + count * 2;
33
+ let low = 0, high = count - 1;
34
+ while (low < high) {const middle = (low + high) >> 1; if (u16(ends + middle * 2) < code) low = middle + 1; else high = middle;}
35
+ if (code < u16(starts + low * 2) || code > u16(ends + low * 2)) return 0;
36
+ const offset = u16(offsets + low * 2), delta = i16(deltas + low * 2);
37
+ if (!offset) return (code + delta) & 0xffff;
38
+ const id = u16(offsets + low * 2 + offset + (code - u16(starts + low * 2)) * 2);
39
+ return id ? (id + delta) & 0xffff : 0;
40
+ };
41
+ const coverage = at => {
42
+ const rows = [];
43
+ if (u16(at) === 1) for (let i = 0; i < u16(at + 2); i++) rows.push(u16(at + 4 + i * 2));
44
+ else if (u16(at) === 2) for (let i = 0; i < u16(at + 2); i++) {const row = at + 4 + i * 6; for (let id = u16(row); id <= u16(row + 2); id++) rows[u16(row + 4) + id - u16(row)] = id;}
45
+ else throw new Error('Unsupported embedded font coverage.');
46
+ return rows;
47
+ };
48
+ const ligatures = [], gsub = tables.get('GSUB')?.at;
49
+ if (gsub) {
50
+ const features = gsub + u16(gsub + 6), lookups = gsub + u16(gsub + 8);
51
+ for (let i = 0; i < u16(features); i++) {
52
+ const record = features + 2 + i * 6; if (tag(record) !== 'liga') continue;
53
+ const feature = features + u16(record + 4);
54
+ for (let j = 0; j < u16(feature + 2); j++) {
55
+ const lookup = lookups + u16(lookups + 2 + u16(feature + 4 + j * 2) * 2), rows = new Map();
56
+ if (u16(lookup) !== 4 || (u16(lookup + 2) & ~8)) throw new Error('Unsupported embedded font ligature lookup.');
57
+ for (let k = 0; k < u16(lookup + 4); k++) {
58
+ const sub = lookup + u16(lookup + 6 + k * 2); if (u16(sub) !== 1) throw new Error('Unsupported embedded font ligature table.');
59
+ const covered = coverage(sub + u16(sub + 2));
60
+ for (let n = 0; n < u16(sub + 4); n++) {
61
+ const set = sub + u16(sub + 6 + n * 2), values = [];
62
+ for (let l = 0; l < u16(set); l++) {const item = set + u16(set + 2 + l * 2); values.push({glyph: u16(item), components: Array.from({length: u16(item + 2) - 1}, (_, m) => u16(item + 4 + m * 2))});}
63
+ rows.set(covered[n], values);
64
+ }
65
+ }
66
+ ligatures.push(rows);
67
+ }
68
+ }
69
+ }
70
+ const os2 = tables.get('OS/2');
71
+ const font = {asset, bytes, units, glyph, ligatures, glyphCount, advance: id => u16(hmtx + Math.min(id, metricCount - 1) * 4),
72
+ bbox: [36, 38, 40, 42].map(offset => i16(head + offset)), ascent: i16(hhea + 4), descent: i16(hhea + 6),
73
+ capHeight: os2 && os2.length >= 90 && u16(os2.at) >= 2 ? i16(os2.at + 88) : i16(hhea + 4)};
74
+ fontCache.set(asset, font); return font;
75
+ }
76
+
77
+ function shape(value, font, preserve, ligatures) {
78
+ const result = [];
79
+ for (let at = 0; at < value.length;) {
80
+ const code = value.codePointAt(at), character = String.fromCodePoint(code), start = at;
81
+ if (/[\p{Mark}\p{Format}]/u.test(character) || (/[\p{Letter}]/u.test(character) && !/\p{Script=Latin}/u.test(character)) || code === 0xfb01 || code === 0xfb02) return null;
82
+ at += character.length;
83
+ if (!preserve && /[ \t\r\n\f]/.test(character)) {while (at < value.length && /[ \t\r\n\f]/.test(value[at])) at++;}
84
+ const text = preserve ? character.replace(/[\r\n]/g, '') : /[ \t\r\n\f]/.test(character) ? ' ' : character;
85
+ const glyph = font.glyph(/[\t\r\n\f]/.test(character) ? 32 : code);
86
+ if (!glyph || glyph >= font.glyphCount) return null;
87
+ result.push({start, end: at, text, glyph});
88
+ }
89
+ if (ligatures) for (const lookup of font.ligatures) for (let i = 0; i < result.length; i++) {
90
+ const found = lookup.get(result[i].glyph)?.find(row => row.components.every((id, offset) => result[i + offset + 1]?.glyph === id));
91
+ if (found) {const held = result.splice(i, found.components.length + 1); result.splice(i, 0, {start: held[0].start, end: held.at(-1).end, text: held.map(row => row.text).join(''), glyph: found.glyph});}
92
+ }
93
+ return result;
94
+ }
95
+
96
+ export async function collectPdfGlyphs(main, {width, check, faces = []}) {
97
+ const document = main.ownerDocument, window = document.defaultView, origin = main.getBoundingClientRect();
98
+ const nodes = new Map(), hidden = [], canvas = document.createElement('canvas'); canvas.width = canvas.height = 1;
99
+ const context = canvas.getContext('2d', {willReadFrequently: true});
100
+ const restore = () => {for (const row of hidden) {if (row.value) row.node.style.setProperty('-webkit-text-fill-color', row.value, row.priority); else row.node.style.removeProperty('-webkit-text-fill-color');} canvas.width = canvas.height = 0;};
101
+ if (!context) return {nodes, restore};
102
+ const colorCache = new Map(), styles = new Map(), styleOf = node => {if (!styles.has(node)) styles.set(node, window.getComputedStyle(node)); return styles.get(node);};
103
+ const color = value => {if (!colorCache.has(value)) {context.clearRect(0, 0, 1, 1); context.fillStyle = value; context.fillRect(0, 0, 1, 1); colorCache.set(value, Array.from(context.getImageData(0, 0, 1, 1).data, item => item / 255));} return colorCache.get(value);};
104
+ const safe = node => {
105
+ for (let parent = node; parent; parent = parent.parentElement) {
106
+ const style = styleOf(parent);
107
+ if (style.visibility !== 'visible' || style.display === 'none' || style.transform !== 'none' || style.filter !== 'none' || style.mixBlendMode !== 'normal'
108
+ || ['absolute', 'fixed', 'sticky'].includes(style.position) || style.zIndex !== 'auto'
109
+ || Number(style.opacity) !== 1 || style.clip !== 'auto' || style.clipPath !== 'none' || style.maskImage !== 'none'
110
+ || (parent !== main && (style.overflowX !== 'visible' || style.overflowY !== 'visible'))) return false;
111
+ if (parent === main) break;
112
+ }
113
+ return true;
114
+ };
115
+ try {
116
+ let visited = 0;
117
+ for (const element of main.querySelectorAll('*')) {
118
+ if (++visited % 128 === 0) {await new Promise(resolve => setTimeout(resolve, 0)); check();}
119
+ // Leaving the existing element in place preserves selectors, counters, line breaking and inline marks.
120
+ // Keep preformatted blocks, their inline struts and syntax paint together.
121
+ if (element.namespaceURI !== 'http://www.w3.org/1999/xhtml' || element.children.length || element.closest('svg,pre,.rapier-will-carrier') || element.localName === 'li') continue;
122
+ const textNodes = Array.from(element.childNodes).filter(node => node.nodeType === 3);
123
+ if (textNodes.length !== 1 || !textNodes[0].data.trim() || !safe(element)) continue;
124
+ const language = (element.closest('[lang]')?.getAttribute('lang') || document.documentElement.lang || '').toLowerCase();
125
+ if (language && !/^en(?:-|$)/.test(language)) continue;
126
+ const style = styleOf(element), family = familyName(style.fontFamily.split(',')[0]), weight = Number(style.fontWeight), size = Number.parseFloat(style.fontSize);
127
+ if (!['Geist', 'Geist Mono'].includes(family) || style.fontStyle !== 'normal' || style.direction !== 'ltr' || style.writingMode !== 'horizontal-tb'
128
+ || style.textTransform !== 'none' || style.textShadow !== 'none' || style.hyphens === 'auto' || Number.parseFloat(style.webkitTextStrokeWidth) > 0
129
+ || ['fontFeatureSettings', 'fontVariationSettings', 'fontVariantAlternates', 'fontVariantCaps', 'fontVariantNumeric', 'fontVariantEastAsian', 'fontVariantPosition'].some(name => style[name] && style[name] !== 'normal')
130
+ || (style.fontSizeAdjust && style.fontSizeAdjust !== 'none') || !['normal', '100%'].includes(style.fontStretch) || style.textRendering === 'optimizeSpeed'
131
+ || !['normal', 'none', 'no-common-ligatures'].includes(style.fontVariantLigatures) || !(size > 0)) continue;
132
+ const generated = ['::before', '::after'].some(pseudo => !['none', 'normal', '""', "''"].includes(window.getComputedStyle(element, pseudo).content));
133
+ const altered = ['::first-letter', '::first-line'].some(pseudo => {const held = window.getComputedStyle(element, pseudo); return ['fontFamily', 'fontWeight', 'fontSize', 'fontStyle', 'fontVariantCaps', 'color'].some(name => held[name] !== style[name]);});
134
+ if (generated || altered || !Array.from(document.fonts).some(face => familyName(face.family) === family && face.status === 'loaded')) continue;
135
+ const asset = faces.find(face => face.family === family && face.weight === weight);
136
+ if (!asset) continue;
137
+ const font = readFont(asset), preserve = /pre|break-spaces/.test(style.whiteSpace), node = textNodes[0];
138
+ const shaped = shape(node.data, font, preserve, style.fontVariantLigatures === 'normal' && !(Number.parseFloat(style.letterSpacing) || 0));
139
+ if (!shaped) continue;
140
+ context.font = [style.fontStyle, style.fontWeight, style.fontSize, style.fontFamily].join(' ');
141
+ const metrics = context.measureText('Hg'), ascent = metrics.fontBoundingBoxAscent;
142
+ if (!Number.isFinite(ascent)) continue;
143
+ const rgba = color(style.webkitTextFillColor || style.color), range = document.createRange();
144
+ let valid = true;
145
+ for (const item of shaped) {
146
+ range.setStart(node, item.start); range.setEnd(node, item.end);
147
+ const rects = Array.from(range.getClientRects()).filter(rect => rect.height > 0), first = rects[0];
148
+ if (!first || rects.some(rect => Math.abs(rect.top - first.top) > .25)) {valid = false; break;}
149
+ const page = Math.max(0, Math.floor((first.left - origin.left + .01) / width));
150
+ Object.assign(item, {font, size, rgba, x: first.left - origin.left - page * width, baseline: first.top - origin.top + ascent});
151
+ }
152
+ if (!valid) continue;
153
+ nodes.set(node, shaped);
154
+ hidden.push({node: element, value: element.style.getPropertyValue('-webkit-text-fill-color'), priority: element.style.getPropertyPriority('-webkit-text-fill-color')});
155
+ element.style.setProperty('-webkit-text-fill-color', 'transparent', 'important');
156
+ }
157
+ check(); return {nodes, restore};
158
+ } catch (error) {restore(); throw error;}
159
+ }
@@ -0,0 +1,204 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SVG keeps its geometry in PDF; only paint operations that need browser compositing are rasterized.
3
+ const SVG = 'http://www.w3.org/2000/svg';
4
+ const number = value => Number(Number(value).toFixed(6));
5
+ const nums = values => values.map(number).join(' ');
6
+ const unsupported = () => { throw new Error('SVG operation needs browser compositing'); };
7
+
8
+ function pathCommands(value) {
9
+ const tokens = String(value).match(/[a-zA-Z]|[+-]?(?:\d*\.\d+|\d+\.?\d*)(?:[eE][+-]?\d+)?/g) || [];
10
+ if (String(value).replace(/[a-zA-Z]|[+-]?(?:\d*\.\d+|\d+\.?\d*)(?:[eE][+-]?\d+)?|[\s,]+/g, '')) unsupported();
11
+ const counts = {M: 2, L: 2, H: 1, V: 1, C: 6, S: 4, Q: 4, T: 2, A: 7};
12
+ let at = 0, command = '', previous = '', x = 0, y = 0, startX = 0, startY = 0, controlX = 0, controlY = 0, out = '';
13
+ const emit = (kind, values) => { if (!values.every(Number.isFinite)) unsupported(); out += nums(values) + ' ' + kind + '\n'; };
14
+ while (at < tokens.length) {
15
+ if (/^[A-Za-z]$/.test(tokens[at])) command = tokens[at++];
16
+ if (!command) unsupported();
17
+ const kind = command.toUpperCase(), relative = command !== kind;
18
+ if (kind === 'Z') { out += 'h\n'; x = startX; y = startY; previous = 'Z'; command = ''; continue; }
19
+ const size = counts[kind]; if (!size || at + size > tokens.length) unsupported();
20
+ const p = tokens.slice(at, at + size).map(Number); if (!p.every(Number.isFinite)) unsupported(); at += size;
21
+ const px = offset => p[offset] + (relative ? x : 0), py = offset => p[offset] + (relative ? y : 0);
22
+ let nextX, nextY;
23
+ if (kind === 'M' || kind === 'L') {
24
+ nextX = px(0); nextY = py(1); emit(kind === 'M' ? 'm' : 'l', [nextX, nextY]);
25
+ if (kind === 'M') { startX = nextX; startY = nextY; command = relative ? 'l' : 'L'; }
26
+ } else if (kind === 'H') { nextX = px(0); nextY = y; emit('l', [nextX, nextY]); }
27
+ else if (kind === 'V') { nextX = x; nextY = py(0); emit('l', [nextX, nextY]); }
28
+ else if (kind === 'C') {
29
+ nextX = px(4); nextY = py(5); controlX = px(2); controlY = py(3);
30
+ emit('c', [px(0), py(1), controlX, controlY, nextX, nextY]);
31
+ } else if (kind === 'S') {
32
+ nextX = px(2); nextY = py(3);
33
+ const firstX = /[CS]/.test(previous) ? 2 * x - controlX : x, firstY = /[CS]/.test(previous) ? 2 * y - controlY : y;
34
+ controlX = px(0); controlY = py(1); emit('c', [firstX, firstY, controlX, controlY, nextX, nextY]);
35
+ } else if (kind === 'Q' || kind === 'T') {
36
+ const qx = kind === 'Q' ? px(0) : /[QT]/.test(previous) ? 2 * x - controlX : x;
37
+ const qy = kind === 'Q' ? py(1) : /[QT]/.test(previous) ? 2 * y - controlY : y;
38
+ nextX = px(kind === 'Q' ? 2 : 0); nextY = py(kind === 'Q' ? 3 : 1);
39
+ emit('c', [x + (qx - x) * 2 / 3, y + (qy - y) * 2 / 3, nextX + (qx - nextX) * 2 / 3, nextY + (qy - nextY) * 2 / 3, nextX, nextY]);
40
+ controlX = qx; controlY = qy;
41
+ } else {
42
+ nextX = px(5); nextY = py(6);
43
+ let rx = Math.abs(p[0]), ry = Math.abs(p[1]);
44
+ if (![0, 1].includes(p[3]) || ![0, 1].includes(p[4])) unsupported();
45
+ if (!rx || !ry) emit('l', [nextX, nextY]);
46
+ else if (nextX !== x || nextY !== y) {
47
+ const angle = p[2] * Math.PI / 180, c = Math.cos(angle), s = Math.sin(angle);
48
+ const dx = (x - nextX) / 2, dy = (y - nextY) / 2, u = c * dx + s * dy, v = -s * dx + c * dy;
49
+ const fit = u * u / (rx * rx) + v * v / (ry * ry);
50
+ if (fit > 1) { const scale = Math.sqrt(fit); rx *= scale; ry *= scale; }
51
+ const denominator = rx * rx * v * v + ry * ry * u * u;
52
+ const scale = (p[3] === p[4] ? -1 : 1) * Math.sqrt(Math.max(0, (rx * rx * ry * ry - denominator) / denominator));
53
+ const cu = scale * rx * v / ry, cv = -scale * ry * u / rx;
54
+ const cx = c * cu - s * cv + (x + nextX) / 2, cy = s * cu + c * cv + (y + nextY) / 2;
55
+ let start = Math.atan2((v - cv) / ry, (u - cu) / rx), end = Math.atan2((-v - cv) / ry, (-u - cu) / rx), sweep = end - start;
56
+ if (p[4] && sweep < 0) sweep += 2 * Math.PI; else if (!p[4] && sweep > 0) sweep -= 2 * Math.PI;
57
+ const steps = Math.ceil(Math.abs(sweep) / (Math.PI / 2)), step = sweep / steps;
58
+ const point = t => [cx + c * rx * Math.cos(t) - s * ry * Math.sin(t), cy + s * rx * Math.cos(t) + c * ry * Math.sin(t)];
59
+ const tangent = t => [-c * rx * Math.sin(t) - s * ry * Math.cos(t), -s * rx * Math.sin(t) + c * ry * Math.cos(t)];
60
+ for (let i = 0; i < steps; i++) {
61
+ const finish = start + step, alpha = 4 / 3 * Math.tan(step / 4), first = point(start), last = point(finish), a = tangent(start), b = tangent(finish);
62
+ emit('c', [first[0] + alpha * a[0], first[1] + alpha * a[1], last[0] - alpha * b[0], last[1] - alpha * b[1], ...last]); start = finish;
63
+ }
64
+ }
65
+ }
66
+ x = nextX; y = nextY; previous = kind;
67
+ }
68
+ return out;
69
+ }
70
+
71
+ function geometry(node) {
72
+ const value = name => { const property = node[name]?.baseVal; return property && typeof property.value === 'number' ? property.value : Number(node.getAttribute(name) || 0); };
73
+ const kind = node.localName;
74
+ if (kind === 'path') return pathCommands(node.getAttribute('d') || '');
75
+ if (kind === 'line') return nums([value('x1'), value('y1')]) + ' m\n' + nums([value('x2'), value('y2')]) + ' l\n';
76
+ if (kind === 'polyline' || kind === 'polygon') {
77
+ const points = Array.from({length: node.points.numberOfItems}, (_, i) => node.points.getItem(i));
78
+ return points.map((point, i) => nums([point.x, point.y]) + (i ? ' l\n' : ' m\n')).join('') + (kind === 'polygon' ? 'h\n' : '');
79
+ }
80
+ if (kind === 'rect') {
81
+ const x = value('x'), y = value('y'), width = value('width'), height = value('height');
82
+ if (!(width > 0 && height > 0)) return '';
83
+ let rx = value('rx'), ry = value('ry'); if (!node.hasAttribute('rx')) rx = ry; if (!node.hasAttribute('ry')) ry = rx;
84
+ rx = Math.min(rx, width / 2); ry = Math.min(ry, height / 2);
85
+ if (!(rx > 0 && ry > 0)) return nums([x, y, width, height]) + ' re\n';
86
+ return pathCommands(`M${x + rx} ${y}H${x + width - rx}A${rx} ${ry} 0 0 1 ${x + width} ${y + ry}V${y + height - ry}A${rx} ${ry} 0 0 1 ${x + width - rx} ${y + height}H${x + rx}A${rx} ${ry} 0 0 1 ${x} ${y + height - ry}V${y + ry}A${rx} ${ry} 0 0 1 ${x + rx} ${y}Z`);
87
+ }
88
+ if (kind === 'circle' || kind === 'ellipse') {
89
+ const cx = value('cx'), cy = value('cy'), rx = value(kind === 'circle' ? 'r' : 'rx'), ry = value(kind === 'circle' ? 'r' : 'ry');
90
+ if (!(rx > 0 && ry > 0)) return '';
91
+ return pathCommands(`M${cx - rx} ${cy}A${rx} ${ry} 0 1 0 ${cx + rx} ${cy}A${rx} ${ry} 0 1 0 ${cx - rx} ${cy}Z`);
92
+ }
93
+ unsupported();
94
+ }
95
+
96
+ export async function collectPdfVectors(main, {width, height, count, capture, signal, current, check, maxBytes, sanitizeSvg}) {
97
+ const document = main.ownerDocument, window = document.defaultView, origin = main.getBoundingClientRect();
98
+ const pages = Array.from({length: count}, () => []), hidden = [], temporary = [], textRoots = [];
99
+ const colorCanvas = document.createElement('canvas'); colorCanvas.width = colorCanvas.height = 1;
100
+ const colorContext = colorCanvas.getContext('2d', {willReadFrequently: true});
101
+ if (!colorContext) return {pages, textRoots, restore() {}};
102
+ const colors = new Map();
103
+ let rasterBytes = 0, visited = 0;
104
+ const compress = async bytes => {
105
+ const result = new Uint8Array(await new Response(new Blob([bytes]).stream().pipeThrough(new CompressionStream('deflate'))).arrayBuffer());
106
+ check(); rasterBytes += result.length;
107
+ if (rasterBytes > maxBytes) throw Object.assign(new Error('The exported file exceeds the file limit.'), {code: 'export_too_large'});
108
+ return result;
109
+ };
110
+ const color = text => {
111
+ if (text === 'none') return null;
112
+ if (text.includes('url(')) unsupported();
113
+ if (!colors.has(text)) { colorContext.clearRect(0, 0, 1, 1); colorContext.fillStyle = text; colorContext.fillRect(0, 0, 1, 1); colors.set(text, Array.from(colorContext.getImageData(0, 0, 1, 1).data, n => n / 255)); }
114
+ return colors.get(text);
115
+ };
116
+ const restore = () => { for (const {node, value, priority} of hidden) {if (value) node.style.setProperty('visibility', value, priority); else node.style.removeProperty('visibility');} for (const node of temporary) node.remove(); colorCanvas.width = colorCanvas.height = 0; };
117
+ const roots = Array.from(main.querySelectorAll('svg,img')).filter(node => node.localName === 'img' || !node.ownerSVGElement);
118
+ try {
119
+ for (const original of roots) {
120
+ check();
121
+ let root = original, mount = null;
122
+ const originalBox = original.getBoundingClientRect();
123
+ if (!(originalBox.width > 0 && originalBox.height > 0)) continue;
124
+ if (original.localName === 'img') {
125
+ if (typeof sanitizeSvg !== 'function') continue;
126
+ const address = original.getAttribute('src') || '', match = /^data:image\/svg\+xml(?:;charset=[^;,]+)?(;base64)?,([\s\S]*)$/i.exec(address);
127
+ if (!match) continue;
128
+ let text;
129
+ try {text = match[1] ? new TextDecoder('utf-8', {fatal: true}).decode(Uint8Array.from(atob(match[2]), ch => ch.charCodeAt(0))) : decodeURIComponent(match[2]);} catch (_) {continue;}
130
+ const parsed = new window.DOMParser().parseFromString(sanitizeSvg(text), 'image/svg+xml');
131
+ if (parsed.querySelector('parsererror') || parsed.documentElement.namespaceURI !== SVG) continue;
132
+ root = document.importNode(parsed.documentElement, true);
133
+ root.style.setProperty('width', originalBox.width + 'px', 'important'); root.style.setProperty('height', originalBox.height + 'px', 'important');
134
+ mount = document.createElement('div'); mount.style.cssText = `position:fixed;left:${originalBox.left}px;top:${originalBox.top}px;width:${originalBox.width}px;height:${originalBox.height}px;pointer-events:none`;
135
+ mount.append(root); document.body.append(mount); temporary.push(mount); await document.fonts.ready;
136
+ textRoots.push(root);
137
+ }
138
+ const box = root.getBoundingClientRect(), nodes = [root, ...root.querySelectorAll('*')], sequence = [];
139
+ const raster = async node => {
140
+ const clone = root.cloneNode(true), copies = [clone, ...clone.querySelectorAll('*')];
141
+ const selected = copies[nodes.indexOf(node)];
142
+ for (let held = selected; held && held !== clone; held = held.parentElement)
143
+ for (const sibling of Array.from(held.parentElement.children)) if (sibling !== held && !['defs', 'style', 'metadata', 'title', 'desc'].includes(sibling.localName)) sibling.remove();
144
+ const wrap = document.createElement('div');
145
+ wrap.style.cssText = `position:fixed;left:0;top:0;width:${box.width}px;height:${box.height}px;pointer-events:none;background:transparent`;
146
+ clone.style.setProperty('width', box.width + 'px', 'important'); clone.style.setProperty('height', box.height + 'px', 'important');
147
+ wrap.append(clone); document.body.append(wrap);
148
+ try {
149
+ const plainText = node.localName === 'text' && window.getComputedStyle(node).filter === 'none' && window.getComputedStyle(node).textShadow === 'none';
150
+ const bounds = plainText ? node.getBoundingClientRect() : box;
151
+ const left = Math.max(0, bounds.left - box.left - 2), top = Math.max(0, bounds.top - box.top - 2);
152
+ const right = Math.min(box.width, bounds.right - box.left + 2), bottom = Math.min(box.height, bounds.bottom - box.top + 2);
153
+ if (!(right > left && bottom > top)) return;
154
+ const result = await capture({root: wrap, clip: {x: left, y: top, width: right - left, height: bottom - top}, transparent: true, scale: 2, signal, current}); check();
155
+ const image = new window.Image(); image.src = 'data:' + result.mimeType + ';base64,' + result.data; await image.decode(); check();
156
+ const canvas = document.createElement('canvas'); canvas.width = image.naturalWidth; canvas.height = image.naturalHeight;
157
+ try {
158
+ const context = canvas.getContext('2d', {willReadFrequently: true}); if (!context) throw new Error('PDF image conversion is unavailable.'); context.drawImage(image, 0, 0);
159
+ const rgba = context.getImageData(0, 0, canvas.width, canvas.height).data, rgb = new Uint8Array(canvas.width * canvas.height * 3), alpha = new Uint8Array(canvas.width * canvas.height);
160
+ for (let i = 0, p = 0; i < rgba.length; i += 4, p++) {rgb.set(rgba.subarray(i, i + 3), p * 3); alpha[p] = rgba[i + 3];}
161
+ return {image: {rgb: await compress(rgb), alpha: await compress(alpha), width: canvas.width, height: canvas.height}, x: box.left - origin.left + left, y: box.top - origin.top + top, width: right - left, height: bottom - top};
162
+ } finally {canvas.width = canvas.height = 0; image.src = '';}
163
+ } finally {wrap.remove();}
164
+ };
165
+ let geometries = 0;
166
+ const walk = async node => {
167
+ if (++visited % 128 === 0) {await new Promise(resolve => setTimeout(resolve, 0)); check();}
168
+ const style = window.getComputedStyle(node), kind = node.localName;
169
+ if (['defs', 'style', 'metadata', 'title', 'desc'].includes(kind) || style.display === 'none' || style.visibility === 'hidden') return;
170
+ const group = ['svg', 'g', 'a'].includes(kind);
171
+ if (style.filter !== 'none' || style.mask !== 'none' || style.clipPath !== 'none' || style.mixBlendMode !== 'normal' || (group && Number(style.opacity) !== 1)) { sequence.push({raster: node}); return; }
172
+ if (group) {for (const child of node.children) await walk(child); return;}
173
+ let path, fill, stroke;
174
+ try {
175
+ path = geometry(node); fill = color(style.fill);
176
+ // SVG zero-width strokes are absent; PDF's zero width instead paints a hairline.
177
+ stroke = Number.parseFloat(style.strokeWidth) === 0 ? null : color(style.stroke);
178
+ if (style.vectorEffect !== 'none' || style.markerStart !== 'none' || style.markerMid !== 'none' || style.markerEnd !== 'none' || style.paintOrder !== 'normal') unsupported();
179
+ } catch (_) { sequence.push({raster: node}); return; }
180
+ if (!path) return;
181
+ const matrix = node.getScreenCTM(); if (!matrix || ![matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f].every(Number.isFinite)) {sequence.push({raster: node}); return;}
182
+ const opacity = Number(style.opacity), fillAlpha = fill ? fill[3] * Number(style.fillOpacity) * opacity : 1, strokeAlpha = stroke ? stroke[3] * Number(style.strokeOpacity) * opacity : 1;
183
+ let commands = nums([matrix.a, matrix.b, matrix.c, matrix.d, matrix.e - origin.left, matrix.f - origin.top]) + ' cm\n';
184
+ if (fill) commands += nums(fill.slice(0, 3)) + ' rg\n';
185
+ if (stroke) {
186
+ const dash = style.strokeDasharray === 'none' ? [] : style.strokeDasharray.split(/[ ,]+/).map(Number.parseFloat);
187
+ let factor = 1; const authoredLength = Number(node.getAttribute('pathLength'));
188
+ if (authoredLength > 0 && typeof node.getTotalLength === 'function') factor = node.getTotalLength() / authoredLength;
189
+ commands += nums(stroke.slice(0, 3)) + ' RG\n' + number(Number.parseFloat(style.strokeWidth)) + ' w\n' + ({butt: 0, round: 1, square: 2}[style.strokeLinecap] ?? 0) + ' J\n' + ({miter: 0, round: 1, bevel: 2}[style.strokeLinejoin] ?? 0) + ' j\n' + number(Number(style.strokeMiterlimit)) + ' M\n[' + nums(dash.map(n => n * factor)) + '] ' + number(Number.parseFloat(style.strokeDashoffset) * factor) + ' d\n';
190
+ }
191
+ commands += path + (fill && stroke ? style.fillRule === 'evenodd' ? 'B*' : 'B' : fill ? style.fillRule === 'evenodd' ? 'f*' : 'f' : stroke ? 'S' : 'n') + '\n';
192
+ sequence.push({commands, fillAlpha, strokeAlpha}); geometries++;
193
+ };
194
+ await walk(root);
195
+ if (!geometries) continue;
196
+ const painted = [];
197
+ for (const piece of sequence) {const held = piece.raster ? await raster(piece.raster) : piece; if (held) painted.push(held);}
198
+ const first = Math.max(0, Math.floor((box.left - origin.left + .01) / width)), last = Math.min(count - 1, Math.floor((box.right - origin.left - .01) / width));
199
+ for (let page = first; page <= last; page++) pages[page].push({sequence: painted, x: -page * width, clip: {x: box.left - origin.left, y: box.top - origin.top, width: box.width, height: box.height}});
200
+ hidden.push({node: original, value: original.style.getPropertyValue('visibility'), priority: original.style.getPropertyPriority('visibility')}); original.style.setProperty('visibility', 'hidden', 'important');
201
+ }
202
+ return {pages, textRoots, restore};
203
+ } catch (error) {restore(); throw error;}
204
+ }