rapier-markdown-kit 1.2.31 → 1.2.33

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/README.md CHANGED
@@ -57,7 +57,7 @@ import {parseAssets} from 'rapier-markdown-kit/assets'; // the picture appendi
57
57
  ## Choose a Rapier workflow
58
58
 
59
59
  Rapier is a phone-first Markdown editor for notes, diagrams, drawing and watercolor painting. It runs offline
60
- as one HTML file on Android, Web and Windows.
60
+ as one HTML file on Android, Web, Windows and Linux.
61
61
 
62
62
  1. **Work together live.** Connect to `https://mcp.rapier.website/door/mcp`, then call `open` to share one document with the person.
63
63
  2. **Deliver an offline editor.** Run `npx rapier-html 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)).
@@ -164,7 +164,7 @@ family name share the host's font namespace. Print retains its existing factory
164
164
 
165
165
  | Import | What it is |
166
166
  | --- | --- |
167
- | `rapier-markdown-kit/markdown-it` | `md.use(rapierMarkdown)`: the reference style's markup in markdown-it, in one line. |
167
+ | `rapier-markdown-kit/markdown-it` | `md.use(rapierMarkdown)`: the reference style's markup in markdown-it, in one line, with rung 1 of `md-layout:v1` (align, indents, list markers, picture width, lines and opacity, paragraph typography). |
168
168
  | `rapier-markdown-kit/parsed` | A frozen, serializable view of completed Markdown tokens, bound to source, revision and grammar. |
169
169
  | `rapier-markdown-kit/layout` | The `md-layout:v1` comment a picture carries: parse it, validate it, write it back. |
170
170
  | `rapier-markdown-kit/marks` | Unified typography values, span writer and parser installer; preserved color/font comments, ink and page breaks. |
@@ -50,15 +50,27 @@ const OPEN = '\n\n<!-- rapier-carried/1\n', CLOSE = '\n-->\n';
50
50
  export function documentTrailer(text, options) {
51
51
  const parts = validateParts(text, options);
52
52
  if (!parts.ledger && !parts.authorship) return '';
53
- const json = JSON.stringify(parts).replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
53
+ const json = JSON.stringify(parts).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/-/g, '\\u002d');
54
54
  return OPEN + json + CLOSE;
55
55
  }
56
56
  export function writeDocument(text, options) {
57
57
  return text + documentTrailer(text, options);
58
58
  }
59
+ function unfinishedTrailer(source) {
60
+ const at = source.lastIndexOf(OPEN);
61
+ if (at < 0) return false;
62
+ let fence = null;
63
+ for (const line of source.slice(0, at).split('\n')) {
64
+ const found = /^ {0,3}(`{3,}|~{3,})/.exec(line);
65
+ if (!found) continue;
66
+ if (!fence) fence = found[1];
67
+ else if (found[1][0] === fence[0] && found[1].length >= fence.length && !line.slice(found[0].length).trim()) fence = null;
68
+ }
69
+ return !fence;
70
+ }
59
71
  export function readDocument(source) {
60
72
  if (!source.endsWith(CLOSE)) {
61
- if (source.includes(OPEN)) throw fault('unfinished document metadata');
73
+ if (unfinishedTrailer(source)) throw fault('unfinished document metadata');
62
74
  return {text: source, ledger: null, authorship: null};
63
75
  }
64
76
  const at = source.lastIndexOf(OPEN);
@@ -272,13 +272,167 @@ function textDeclarations(units) {
272
272
  return [...definitions, ...continuations, ...(text.match(/<!--[\s\S]*?(?:-->|$)/g) || [])].join('\0');
273
273
  }
274
274
 
275
+ function* textGroups(units, a, b) {
276
+ const changes = [...a.map(e => ({...e, side: 0})), ...b.map(e => ({...e, side: 1}))].sort((x, y) => x.start - y.start || x.end - y.end || x.side - y.side);
277
+ for (let i = 0; i < changes.length;) {
278
+ const group = [changes[i++]], start = group[0].start; let end = group[0].end;
279
+ while (i < changes.length && (changes[i].start < end || group.some(e => {
280
+ const next = changes[i];
281
+ const afterBlock = (edit, insert) => insert.start === insert.end && insert.start === edit.end && edit.start < edit.end && units.slice(edit.start, edit.end).every(unit => textEnvelopeAt(unit));
282
+ if (afterBlock(e, next) || afterBlock(next, e)) return false;
283
+ if (e.start === e.end && next.start <= e.start && e.start <= next.end) return true;
284
+ if (next.start === next.end && e.start <= next.start && next.start <= e.end) return true;
285
+ return next.start >= e.end && units.slice(e.start, e.end).every(textSpace) &&
286
+ units.slice(e.end, next.start).every(textSpace) && units.slice(next.start, next.end).every(textSpace);
287
+ }))) {
288
+ group.push(changes[i]); end = Math.max(end, changes[i++].end);
289
+ }
290
+ const before = units.slice(start, end).join('');
291
+ const version = side => {
292
+ let value = '', at = start;
293
+ for (const e of group.filter(e => e.side === side)) { value += units.slice(at, e.start).join('') + e.text; at = e.end; }
294
+ return value + units.slice(at, end).join('');
295
+ };
296
+ yield {start, end, before, av: version(0), bv: version(1)};
297
+ }
298
+ }
299
+
300
+ function textMoves(groups) {
301
+ const blank = value => !value.trim();
302
+ const side = (g, which) => which === 0 ? g.av : g.bv;
303
+ for (const g of groups) {
304
+ if (g.resolved !== undefined || blank(g.before)) continue;
305
+ for (const gone of [0, 1]) {
306
+ const removed = side(g, gone), other = side(g, 1 - gone);
307
+ if (!blank(removed) || other === g.before) continue;
308
+ if (other.split(g.before).length === 2) { g.resolved = other.replace(g.before, ''); break; }
309
+ const homes = groups.filter(h => h !== g && h.resolved === undefined && blank(h.before) && blank(side(h, 1 - gone)) && side(h, gone).trim() === g.before.trim());
310
+ if (homes.length === 1) {
311
+ const trailing = side(homes[0], gone).match(/\s*$/)[0], edited = other.replace(/\s*$/, '');
312
+ g.resolved = ''; homes[0].resolved = side(homes[0], gone).match(/^\s*/)[0] + edited + trailing; break;
313
+ }
314
+ }
315
+ }
316
+ return groups;
317
+ }
318
+
319
+ const textLines = text => text.match(/[^\r\n]*(?:\r\n|\r|\n)|[^\r\n]+$/g) || [];
320
+ const textWords = text => text.match(/\s+|\S+/g) || [];
321
+ const TEXT_ITEM = /^( {0,3})(?:[-+*]|\d{1,9}[.)])[ \t]+\S/;
322
+ const TEXT_TAGS = /^(tags|aliases):[ \t]*\[([^\[\]\r\n]*)\][ \t]*(\r\n|\r|\n|$)/;
323
+ function textCounts(text) {
324
+ const counts = new Map();
325
+ for (const word of text.match(/[\p{L}\p{N}_]+/gu) || []) counts.set(word, (counts.get(word) || 0) + 1);
326
+ return counts;
327
+ }
328
+ function textConserved(before, av, bv, merged) {
329
+ const [b, a, t, m] = [before, av, bv, merged].map(textCounts);
330
+ for (const word of new Set([...b.keys(), ...a.keys(), ...t.keys(), ...m.keys()])) {
331
+ const base = b.get(word) || 0, da = (a.get(word) || 0) - base, dt = (t.get(word) || 0) - base, got = (m.get(word) || 0) - base;
332
+ const both = [da + dt, da, dt];
333
+ if (da * dt > 0 ? got < Math.min(...both) || got > Math.max(...both) : got !== da + dt) return false;
334
+ }
335
+ return true;
336
+ }
337
+ function textRefine(before, av, bv, options, level = 'line') {
338
+ const merged = textRefineLevel(before, av, bv, options, level);
339
+ return merged !== null && textConserved(before, av, bv, merged) ? merged : null;
340
+ }
341
+ function textRefineLevel(before, av, bv, options, level) {
342
+ const split = level === 'line' ? textLines : textWords;
343
+ const units = split(before), oursUnits = split(av), theirsUnits = split(bv);
344
+ const reordered = textPermuted(units, oursUnits, theirsUnits);
345
+ if (reordered !== null) return reordered;
346
+ const a = textEdits(units, oursUnits), b = textEdits(units, theirsUnits);
347
+ if (!a || !b) return null;
348
+ let out = '', pos = 0;
349
+ for (const g of textGroups(units, a, b)) {
350
+ out += units.slice(pos, g.start).join('');
351
+ if (g.av === g.bv) out += g.av;
352
+ else if (g.av === g.before) out += g.bv;
353
+ else if (g.bv === g.before) out += g.av;
354
+ else {
355
+ const fine = level === 'line' ? textRefineGroup(g, options) : null;
356
+ if (fine === null) return null;
357
+ out += fine;
358
+ }
359
+ pos = g.end;
360
+ }
361
+ return out + units.slice(pos).join('');
362
+ }
363
+ function textPermuted(units, ours, theirs) {
364
+ const solid = list => list.filter(unit => !textSpace(unit));
365
+ const baseSolid = solid(units), known = new Map(baseSolid.map((unit, k) => [unit, k]));
366
+ if (known.size !== baseSolid.length) return null;
367
+ const reorders = side => { const x = solid(side); return x.length === baseSolid.length && x.every(unit => known.has(unit)) && new Set(x).size === x.length && side.join('') !== units.join(''); };
368
+ const [first, second] = [reorders(ours), reorders(theirs)];
369
+ if (first === second) return null;
370
+ const [moved, changed] = first ? [ours, theirs] : [theirs, ours];
371
+ const edits = textEdits(units, changed);
372
+ if (!edits) return null;
373
+ const replace = new Map(), after = new Map();
374
+ for (const e of edits) {
375
+ const inner = []; for (let i = e.start; i < e.end; i++) if (!textSpace(units[i])) inner.push(i);
376
+ if (!inner.length) {
377
+ if (!e.text.trim()) continue;
378
+ let k = -1; for (let i = e.start - 1; i >= 0; i--) if (!textSpace(units[i])) { k = known.get(units[i]); break; }
379
+ const gap = e.start > 0 && textSpace(units[e.start - 1]);
380
+ after.set(k, [...(after.get(k) || []), {text: e.text, gap}]);
381
+ } else if (inner.length === 1) {
382
+ if (!e.text.trim()) replace.set(known.get(units[inner[0]]), units.slice(e.start, e.end).some(textSpace) ? ' ' : '');
383
+ else if (units.slice(e.start, e.end).some(textSpace)) return null;
384
+ else replace.set(known.get(units[inner[0]]), e.text);
385
+ } else return null;
386
+ }
387
+ const out = [], place = (k, from) => {
388
+ const rows = after.get(k) || [];
389
+ return rows.filter(row => row.gap === from).map(row => row.text);
390
+ };
391
+ out.push(...place(-1, false));
392
+ for (let i = 0; i < moved.length; i++) {
393
+ const unit = moved[i];
394
+ if (textSpace(unit)) { out.push(unit); continue; }
395
+ const k = known.get(unit);
396
+ if (replace.get(k) === '' || replace.get(k) === ' ') {
397
+ if (replace.get(k) === ' ') { if (i + 1 < moved.length && textSpace(moved[i + 1])) i++; else if (out.length && textSpace(out[out.length - 1])) out.pop(); }
398
+ } else out.push(replace.has(k) ? replace.get(k) : unit);
399
+ out.push(...place(k, false));
400
+ if (after.get(k)?.some(row => row.gap)) { if (i + 1 < moved.length && textSpace(moved[i + 1])) out.push(moved[++i]); out.push(...place(k, true)); }
401
+ }
402
+ return out.join('');
403
+ }
404
+
405
+ function textRefineGroup({before, av, bv}, options) {
406
+ const [first, second] = textDeviceOptions(options), ids = first < second ? [0, 1] : [1, 0];
407
+ const added = (side, base) => side.startsWith(base) ? side.slice(base.length) : null;
408
+ const oursAdded = added(av, before), theirsAdded = added(bv, before);
409
+ if (oursAdded !== null && theirsAdded !== null && textLines(oursAdded + theirsAdded).every(line => TEXT_ITEM.test(line))) {
410
+ const tail = value => value && !/[\r\n]$/.test(value) ? value + (before.includes('\r\n') ? '\r\n' : '\n') : value;
411
+ const parts = [tail(oursAdded), tail(theirsAdded)];
412
+ const last = tail(before) === before ? '' : (before.includes('\r\n') ? '\r\n' : '\n');
413
+ return before + last + parts[ids[0]] + parts[ids[1]];
414
+ }
415
+ const lines = textLines(before);
416
+ if (lines.length === 1) {
417
+ const tags = TEXT_TAGS.exec(before), x = TEXT_TAGS.exec(av), y = TEXT_TAGS.exec(bv);
418
+ if (tags && x && y && x[1] === tags[1] && y[1] === tags[1]) {
419
+ const list = row => row[2].split(',').map(item => item.trim()).filter(Boolean);
420
+ const base = list(tags), ours = list(x), theirs = list(y);
421
+ const kept = base.filter(item => ours.includes(item) && theirs.includes(item));
422
+ const added = [ours, theirs].map(list => list.filter(item => !base.includes(item)));
423
+ const joined = [...new Set([...kept, ...added[ids[0]], ...added[ids[1]]])];
424
+ return tags[1] + ': [' + joined.join(', ') + ']' + tags[3];
425
+ }
426
+ }
427
+ return textRefineLevel(before, av, bv, options, 'word');
428
+ }
275
429
  export function mergeText(base, ours, theirs, options = {}) {
276
430
  canonicalNote(ours); canonicalNote(theirs); if (base !== null) canonicalNote(base);
277
431
  const unchanged = value => ({text: value, clean: !value.includes(TEXT_CONFLICT), conflicts: value.includes(TEXT_CONFLICT) ? [{kind: 'existing-conflict', advisory: true}] : []});
278
432
  if (ours === theirs) return unchanged(ours);
279
433
  if (base === ours) return unchanged(theirs);
280
434
  if (base === theirs) return unchanged(ours);
281
- const conflicts = []; let text = '';
435
+ const conflicts = []; let text = '', a, b;
282
436
  const conflict = (before, a, b, kind) => {
283
437
  const ab = inspectTextConflicts(a), bb = inspectTextConflicts(b);
284
438
  const au = textUnits(a), bu = textUnits(b); let first = 0, last = 0;
@@ -316,38 +470,21 @@ export function mergeText(base, ours, theirs, options = {}) {
316
470
  if (base === null) conflict(null, ours, theirs, 'no-ancestor');
317
471
  else if (textDeclarations(oursUnits) !== declared || textDeclarations(theirsUnits) !== declared) conflict(base, ours, theirs, 'document-bindings');
318
472
  else {
319
- const a = textEdits(units, oursUnits), b = textEdits(units, theirsUnits);
320
- if (!a || !b) conflict(base, ours, theirs, 'ambiguous-alignment-or-budget');
473
+ const reordered = textPermuted(units, oursUnits, theirsUnits);
474
+ if (reordered !== null && textConserved(base, ours, theirs, reordered)) text += reordered;
475
+ else if (!(a = textEdits(units, oursUnits)) || !(b = textEdits(units, theirsUnits))) conflict(base, ours, theirs, 'ambiguous-alignment-or-budget');
321
476
  else {
322
- const changes = [...a.map(e => ({...e, side: 0})), ...b.map(e => ({...e, side: 1}))].sort((x, y) => x.start - y.start || x.end - y.end || x.side - y.side);
323
477
  let pos = 0;
324
- for (let i = 0; i < changes.length;) {
325
- const group = [changes[i++]], start = group[0].start; let end = group[0].end;
326
- while (i < changes.length && (changes[i].start < end || group.some(e => {
327
- const next = changes[i];
328
- const afterBlock = (edit, insert) => insert.start === insert.end && insert.start === edit.end && edit.start < edit.end && units.slice(edit.start, edit.end).every(unit => textEnvelopeAt(unit));
329
- if (afterBlock(e, next) || afterBlock(next, e)) return false;
330
- if (e.start === e.end && next.start <= e.start && e.start <= next.end) return true;
331
- if (next.start === next.end && e.start <= next.start && next.start <= e.end) return true;
332
- return next.start >= e.end && units.slice(e.start, e.end).every(textSpace) &&
333
- units.slice(e.end, next.start).every(textSpace) && units.slice(next.start, next.end).every(textSpace);
334
- }))) {
335
- group.push(changes[i]); end = Math.max(end, changes[i++].end);
336
- }
478
+ for (const {start, end, before, av, bv, resolved} of textMoves([...textGroups(units, a, b)])) {
337
479
  text += units.slice(pos, start).join('');
338
- const before = units.slice(start, end).join('');
339
- const version = side => {
340
- let value = '', at = start;
341
- for (const e of group.filter(e => e.side === side)) { value += units.slice(at, e.start).join('') + e.text; at = e.end; }
342
- return value + units.slice(at, end).join('');
343
- };
344
- const av = version(0), bv = version(1);
345
- if (av === bv) text += av;
480
+ if (resolved !== undefined) text += resolved;
481
+ else if (av === bv) text += av;
346
482
  else if (av === before) text += bv;
347
483
  else if (bv === before) text += av;
348
484
  else {
349
485
  const tick = end === start + 1 ? textTick(before, av, bv) : null;
350
- if (tick !== null) text += tick; else conflict(before, av, bv, 'text');
486
+ const fine = tick === null ? textRefine(before, av, bv, options) : null;
487
+ if (tick !== null) text += tick; else if (fine !== null) text += fine; else conflict(before, av, bv, 'text');
351
488
  }
352
489
  pos = end;
353
490
  }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {parseLayout, imageStyle} from './layout.mjs';
2
+ import {parseLayout, imageStyle, textStyle} from './layout.mjs';
3
3
  import {typography, isPageBreakBlock} from './marks.mjs';
4
4
  import {installMarkdownImages} from './assets.mjs';
5
5
  import {tuneMarkdownEngine} from '../spec/md-engine.mjs';
@@ -26,11 +26,25 @@ export function rapierMarkdown(md) {
26
26
  const layout = parseLayout(marker.content);
27
27
  if (!layout) continue;
28
28
  children.pop();
29
- if (layout.align) owner.attrSet('data-md-align', layout.align);
29
+ if (layout.align && (layout.align !== 'justify' || owner.type === 'paragraph_open')) owner.attrSet('data-md-align', layout.align);
30
30
  const visible = children.filter(token => token.type !== 'text' || token.content.trim());
31
- if (visible.length === 1 && visible[0].type === 'image' && layout.width) {
32
- visible[0].attrSet('style', imageStyle(layout));
33
- visible[0].attrSet('data-md-image-width', String(layout.width));
31
+ const picture = visible.length === 1 && visible[0].type === 'image' ? visible[0]
32
+ : visible.length === 3 && visible[0].type === 'link_open' && visible[1].type === 'image' && visible[2].type === 'link_close' ? visible[1] : null;
33
+ if (picture) {
34
+ if (layout.width != null || layout.lines != null || layout.opacity != null) {
35
+ const style = imageStyle(layout);
36
+ if (style) picture.attrSet('style', style);
37
+ if (layout.width != null) picture.attrSet('data-md-image-width', String(layout.width));
38
+ }
39
+ continue;
40
+ }
41
+ for (const key of ['first', 'indent', 'before', 'after', 'lines', 'hyphens', 'caps', 'hanging']) if (layout[key]) owner.attrSet('data-md-' + key, String(layout[key]));
42
+ const style = [textStyle(layout), layout.lines != null ? '--md-drop-lines:' + layout.lines : ''].filter(Boolean).join(';');
43
+ if (style) owner.attrSet('style', style);
44
+ const item = state.tokens[i - 2];
45
+ if (layout.marker && item?.type === 'list_item_open') {
46
+ item.attrSet('data-md-marker', String(layout.marker));
47
+ item.attrSet('style', (item.attrGet('style') ? item.attrGet('style') + ';' : '') + textStyle({marker: layout.marker}));
34
48
  }
35
49
  }
36
50
  });
@@ -337,8 +337,8 @@ export async function collectPdfGlyphs(main, {width, check, faces = [], prepareF
337
337
  probe.style.setProperty('visibility', 'hidden', 'important'); probe.textContent = String(value); item.prepend(probe);
338
338
  const parts = await measure(probe.firstChild, style, font); probe.remove();
339
339
  if (!parts.length || parts.some(part => !part.font)) continue;
340
- parts.at(-1).text += ' ';
341
- markers.set(item, {text: String(value) + ' ', parts, ...Object.fromEntries(['x','y','width','height','page'].map(key => [key, parts[0][key]]))});
340
+ parts.at(-1).text += '. ';
341
+ markers.set(item, {text: String(value) + '. ', parts, ...Object.fromEntries(['x','y','width','height','page'].map(key => [key, parts[0][key]]))});
342
342
  const id = 'm' + markers.size, old = item.getAttribute('data-rapier-pdf-marker'); item.setAttribute('data-rapier-pdf-marker', id);
343
343
  restoreSteps.push(() => old == null ? item.removeAttribute('data-rapier-pdf-marker') : item.setAttribute('data-rapier-pdf-marker', old));
344
344
  markerRules.push('[data-rapier-pdf-marker="' + id + '"]::before{-webkit-text-fill-color:transparent!important}');
@@ -92,7 +92,7 @@ function geometry(node) {
92
92
  unsupported();
93
93
  }
94
94
 
95
- export async function collectPdfVectors(main, {width, height, count, capture, signal, current, check, maxBytes, sanitizeSvg, collectText}) {
95
+ export async function collectPdfVectors(main, {width, height, count, capture, signal, current, check, maxBytes, sanitizeSvg, collectText, pictures = false, underlay = false}) {
96
96
  const document = main.ownerDocument, window = document.defaultView, origin = main.getBoundingClientRect();
97
97
  const pages = Array.from({length: count}, () => []), hidden = [], temporary = [], textRoots = [], textRestores = [];
98
98
  const colorCanvas = document.createElement('canvas'); colorCanvas.width = colorCanvas.height = 1;
@@ -117,20 +117,33 @@ export async function collectPdfVectors(main, {width, height, count, capture, si
117
117
  try {
118
118
  for (const original of roots) {
119
119
  check();
120
- let root = original, mount = null;
120
+ let root = original, mount = null, turned = false, groupOpacity = 1, asImage = false, under = false;
121
121
  const originalBox = original.getBoundingClientRect();
122
122
  if (!(originalBox.width > 0 && originalBox.height > 0)) continue;
123
123
  if (original.localName === 'img') {
124
124
  if (typeof sanitizeSvg !== 'function') continue;
125
125
  const address = original.getAttribute('src') || '', match = /^data:image\/svg\+xml(?:;charset=[^;,]+)?(;base64)?,([\s\S]*)$/i.exec(address);
126
- if (!match) continue;
126
+ if (!match && !(pictures && /^data:image\//i.test(address))) continue;
127
+ asImage = !match;
128
+ }
129
+ if (original.localName === 'img' && !asImage) {
130
+ const address = original.getAttribute('src') || '', match = /^data:image\/svg\+xml(?:;charset=[^;,]+)?(;base64)?,([\s\S]*)$/i.exec(address);
127
131
  let text;
128
132
  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;}
129
133
  const parsed = new window.DOMParser().parseFromString(sanitizeSvg(text), 'image/svg+xml');
130
134
  if (parsed.querySelector('parsererror') || parsed.documentElement.namespaceURI !== SVG) continue;
135
+ const shown = window.getComputedStyle(original);
136
+ under = Number(shown.zIndex) < 0;
137
+ if (under && !underlay) continue;
138
+ turned = shown.transform !== 'none'; groupOpacity = Number(shown.opacity);
139
+ const unturnedWidth = turned ? Number.parseFloat(shown.width) : originalBox.width, unturnedHeight = turned ? Number.parseFloat(shown.height) : originalBox.height;
140
+ if (!(unturnedWidth > 0 && unturnedHeight > 0)) continue;
141
+ const [originX, originY] = shown.transformOrigin.split(' ').map(Number.parseFloat);
142
+ if (turned && !(Math.abs(originX - unturnedWidth / 2) < .5 && Math.abs(originY - unturnedHeight / 2) < .5)) continue;
143
+ const left = originalBox.left + originalBox.width / 2 - unturnedWidth / 2, top = originalBox.top + originalBox.height / 2 - unturnedHeight / 2;
131
144
  root = document.importNode(parsed.documentElement, true);
132
- root.style.setProperty('width', originalBox.width + 'px', 'important'); root.style.setProperty('height', originalBox.height + 'px', 'important');
133
- 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`;
145
+ root.style.setProperty('width', unturnedWidth + 'px', 'important'); root.style.setProperty('height', unturnedHeight + 'px', 'important');
146
+ mount = document.createElement('div'); mount.style.cssText = `position:fixed;left:${left}px;top:${top}px;width:${unturnedWidth}px;height:${unturnedHeight}px;pointer-events:none` + (turned ? `;transform:${shown.transform};transform-origin:50% 50%` : '');
134
147
  mount.attachShadow({mode: 'open'}).append(root); document.body.append(mount); temporary.push(mount); await document.fonts.ready;
135
148
  const glyphs = await collectText?.(root); if (glyphs) textRestores.push(glyphs.restore);
136
149
  textRoots.push({root, original, text: glyphs});
@@ -181,7 +194,7 @@ export async function collectPdfVectors(main, {width, height, count, capture, si
181
194
  } catch (_) { sequence.push({raster: node}); return; }
182
195
  if (!path) return;
183
196
  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;}
184
- const opacity = Number(style.opacity), fillAlpha = fill ? fill[3] * Number(style.fillOpacity) * opacity : 1, strokeAlpha = stroke ? stroke[3] * Number(style.strokeOpacity) * opacity : 1;
197
+ const opacity = Number(style.opacity) * groupOpacity, fillAlpha = fill ? fill[3] * Number(style.fillOpacity) * opacity : 1, strokeAlpha = stroke ? stroke[3] * Number(style.strokeOpacity) * opacity : 1;
185
198
  let commands = nums([matrix.a, matrix.b, matrix.c, matrix.d, matrix.e - origin.left, matrix.f - origin.top]) + ' cm\n';
186
199
  if (fill) commands += nums(fill.slice(0, 3)) + ' rg\n';
187
200
  if (stroke) {
@@ -193,12 +206,19 @@ export async function collectPdfVectors(main, {width, height, count, capture, si
193
206
  commands += path + (fill && stroke ? style.fillRule === 'evenodd' ? 'B*' : 'B' : fill ? style.fillRule === 'evenodd' ? 'f*' : 'f' : stroke ? 'S' : 'n') + '\n';
194
207
  sequence.push({commands, fillAlpha, strokeAlpha}); geometries++;
195
208
  };
196
- await walk(root);
197
- if (!geometries && !textRoots.find(entry => entry.root === root)?.text?.nodes.size) continue;
209
+ if (asImage) sequence.push({raster: root}); else await walk(root);
210
+ if ((turned || groupOpacity !== 1) && (sequence.some(piece => piece.raster) || textRoots.find(entry => entry.root === root)?.text?.nodes.size)) {
211
+ const entry = textRoots.find(row => row.root === root);
212
+ if (entry?.text) textRestores.pop()?.();
213
+ if (entry) textRoots.splice(textRoots.indexOf(entry), 1);
214
+ mount?.remove(); temporary.splice(temporary.indexOf(mount), 1);
215
+ continue;
216
+ }
217
+ if (!geometries && !asImage && !textRoots.find(entry => entry.root === root)?.text?.nodes.size) continue;
198
218
  const painted = [];
199
219
  for (const piece of sequence) {const held = piece.raster ? await raster(piece.raster) : piece; if (held) painted.push(held);}
200
220
  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));
201
- 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}});
221
+ 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}, ...(under && {under: true})});
202
222
  hidden.push({node: original, style: original.getAttribute('style')}); original.style.setProperty('visibility', 'hidden', 'important');
203
223
  }
204
224
  return {pages, textRoots, restore};
@@ -1243,7 +1243,7 @@ function* _rapierProjectPortableNodesSteps(semanticRoot, options) {
1243
1243
  ? (element.style.backgroundColor || RAPIER_PORTABLE_HIGHLIGHT_HEX[String(element.getAttribute('data-rapier-highlight') || '').toLowerCase()] || RAPIER_PORTABLE_HIGHLIGHT_HEX.yellow)
1244
1244
  : null;
1245
1245
 
1246
- const isTableCaption = element.tagName === 'P' && element.classList.contains('rapier-table-caption');
1246
+ const isTableCaption = element.tagName === 'P' && (element.classList.contains('rapier-table-caption') || element.classList.contains('rapier-figure-caption'));
1247
1247
 
1248
1248
  const isPageBreak = element.hasAttribute('data-md-break');
1249
1249
  for (const attribute of Array.from(element.attributes)) { yield;
@@ -1271,6 +1271,13 @@ function* _rapierProjectPortableNodesSteps(semanticRoot, options) {
1271
1271
  if (paragraphLayout.before != null) element.style.marginTop = paragraphLayout.before + 'pt';
1272
1272
  if (paragraphLayout.after != null) element.style.marginBottom = paragraphLayout.after + 'pt';
1273
1273
  }
1274
+ if (!keepLayoutData && paragraphLayout && /^(?:P|H[1-6])$/.test(element.tagName) && !element.closest('table,pre') && (paragraphLayout.first != null || paragraphLayout.indent != null)) {
1275
+ const indents = {...(paragraphLayout.first != null && {first: paragraphLayout.first}), ...(paragraphLayout.indent != null && {indent: paragraphLayout.indent})};
1276
+ for (const declaration of globalThis.RapierMarkdownLayout.textStyle(indents, {portable: true}).split(';').filter(Boolean)) {
1277
+ const at = declaration.indexOf(':');
1278
+ element.style.setProperty(declaration.slice(0, at), declaration.slice(at + 1));
1279
+ }
1280
+ }
1274
1281
  if (!keepLayoutData && element.tagName === 'P' && !element.closest('table,pre')) {
1275
1282
  if (paragraphLayout?.hyphens === 'auto') { element.style.hyphens = 'auto'; element.style.textWrap = 'pretty'; }
1276
1283
  if (paragraphLayout?.hanging === 'on') element.style.setProperty('hanging-punctuation', 'first allow-end');