gutterpress 0.10.0-alpha.4 → 0.10.0

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.
Files changed (38) hide show
  1. package/README.md +2 -0
  2. package/dist/api/index.js +2 -2
  3. package/dist/{audit-k1vnfwvc.js → audit-cvarpa72.js} +4 -4
  4. package/dist/{build-hqmwgvdw.js → build-6r502chw.js} +6 -4
  5. package/dist/{cli-0r0tq16s.js → cli-149edp6b.js} +1 -1
  6. package/dist/{cli-ra0ed2xt.js → cli-cqtggsng.js} +1 -1
  7. package/dist/{cli-n25qycwz.js → cli-eq5naw4m.js} +222 -141
  8. package/dist/{cli-revgt4pr.js → cli-yzf38679.js} +1 -0
  9. package/dist/{cli-hp9r2pzt.js → cli-zfcryxg8.js} +327 -24
  10. package/dist/cli.js +16 -16
  11. package/dist/{doctor-dxms7ehm.js → doctor-akvxbtjb.js} +2 -2
  12. package/dist/engine/compiler/build.d.ts +1 -1
  13. package/dist/engine/shared/flush.d.ts +59 -0
  14. package/dist/engine/shared/margin-box-support.d.ts +24 -0
  15. package/dist/{engine-z4p9sr4h.js → engine-b159tbns.js} +2 -2
  16. package/dist/{engine-wa7y9av9.js → engine-ft4cr3ep.js} +1 -1
  17. package/dist/{gutterpress-agent-1ctgfz92.js → gutterpress-agent-cazqstr1.js} +49 -1
  18. package/dist/{gutterpress-viewer-cem7dmr5.js → gutterpress-viewer-te8g5grx.js} +191 -71
  19. package/dist/{index-05y3dnxq.js → index-ge7q9xj3.js} +330 -249
  20. package/dist/{index-xxg4zfrg.js → index-wq3r5pj7.js} +287 -23
  21. package/dist/{index-9tyq9kks.js → index-ycpvr0am.js} +47 -4
  22. package/dist/index.js +3 -3
  23. package/dist/lib/build-runner.d.ts +7 -0
  24. package/dist/lib/build-staging.d.ts +51 -0
  25. package/dist/lib/engine.d.ts +9 -0
  26. package/dist/lib/markdown/gutterpress-css.d.ts +9 -1
  27. package/dist/lib/markdown/markers.d.ts +51 -1
  28. package/dist/{lint-xjwm5ep8.js → lint-p2sw53d9.js} +4 -4
  29. package/dist/{new-kwdwpf0j.js → new-hvq0x91q.js} +4 -4
  30. package/dist/{plugin-rg4tnn96.js → plugin-pssmk0dx.js} +4 -4
  31. package/dist/{preflight-3127y25z.js → preflight-4j00yd0g.js} +4 -4
  32. package/dist/{preview-ncgfhqmw.js → preview-d4s6gk6p.js} +6 -5
  33. package/dist/{preview-interface-435cczt5.js → preview-interface-0ssk8bmm.js} +153 -6
  34. package/dist/{publish-pr0rwh6p.js → publish-tkc26en7.js} +4 -4
  35. package/dist/render.js +83 -7
  36. package/dist/{repair-8270smfw.js → repair-2va4w12t.js} +4 -4
  37. package/dist/{validate-54e17rae.js → validate-w9vfefrw.js} +4 -4
  38. package/package.json +1 -1
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  build
3
- } from "./index-xxg4zfrg.js";
3
+ } from "./index-wq3r5pj7.js";
4
4
  import {
5
5
  BuildError,
6
6
  RENDER_TIMEOUT_MS,
7
7
  assertMilestone,
8
8
  connectChromium,
9
9
  getBrowser
10
- } from "./index-9tyq9kks.js";
10
+ } from "./index-ycpvr0am.js";
11
11
  import"./index-37x76zdn.js";
12
12
 
13
13
  // src/lib/engine.ts
@@ -4,7 +4,7 @@ import {
4
4
  build,
5
5
  connectChromium,
6
6
  getBrowser
7
- } from "./cli-hp9r2pzt.js";
7
+ } from "./cli-zfcryxg8.js";
8
8
  import"./cli-c41yr7he.js";
9
9
  import {
10
10
  BuildError
@@ -44,8 +44,10 @@
44
44
  targetTexts: () => targetTexts,
45
45
  stringSources: () => stringSources,
46
46
  setGenerated: () => setGenerated,
47
+ setFlushFurniture: () => setFlushFurniture,
47
48
  instrument: () => instrument,
48
49
  forcedBreakSites: () => forcedBreakSites,
50
+ flushRoots: () => flushRoots,
49
51
  fillLeaders: () => fillLeaders,
50
52
  counterResetSites: () => counterResetSites,
51
53
  collectCss: () => collectCss,
@@ -557,6 +559,50 @@
557
559
  addCss("gp-generated-content", css);
558
560
  return entries.length;
559
561
  }
562
+ function flushRoots() {
563
+ const out = [];
564
+ for (const root of Array.from(document.querySelectorAll(".page, .spread"))) {
565
+ const edges = ["top", "right", "bottom", "left"].filter((edge) => root.querySelector(`.gp-pin.gp-flush.gp-${edge}`));
566
+ if (!edges.length)
567
+ continue;
568
+ const page = getComputedStyle(root).page;
569
+ const key = `${page === "auto" ? "" : page}|${edges.map((e) => e[0]).join("")}`;
570
+ root.dataset.gpFlush = key;
571
+ out.push({ id: ensureAnchor(root), page, edges, key });
572
+ }
573
+ return out;
574
+ }
575
+ function setFlushFurniture(items) {
576
+ let painted = 0;
577
+ for (const item of items) {
578
+ const host = anchorHost(item.id);
579
+ if (!host)
580
+ continue;
581
+ let layer = host.querySelector(":scope > .gp-flush-furniture");
582
+ if (!layer) {
583
+ layer = document.createElement("div");
584
+ layer.className = "gp-flush-furniture";
585
+ layer.setAttribute("aria-hidden", "true");
586
+ layer.setAttribute("style", "position:absolute;inset:0;pointer-events:none;z-index:10;");
587
+ host.appendChild(layer);
588
+ }
589
+ layer.textContent = "";
590
+ for (const b of item.boxes) {
591
+ const slot = document.createElement("div");
592
+ slot.dataset.box = b.box;
593
+ const justify = b.align === "center" ? "center" : b.align === "end" ? "flex-end" : "flex-start";
594
+ slot.setAttribute("style", `position:absolute;left:${b.x}px;top:${b.y}px;width:${b.w}px;height:${b.h}px;` + `display:flex;align-items:center;justify-content:${justify};overflow:hidden;white-space:pre;`);
595
+ const content = document.createElement("span");
596
+ content.textContent = b.text;
597
+ for (const [prop, value] of Object.entries(b.decls))
598
+ content.style.setProperty(prop, value);
599
+ slot.appendChild(content);
600
+ layer.appendChild(slot);
601
+ painted++;
602
+ }
603
+ }
604
+ return painted;
605
+ }
560
606
  var api = {
561
607
  auditContent,
562
608
  collectCss,
@@ -569,7 +615,9 @@
569
615
  fillLeaders,
570
616
  instrument,
571
617
  addCss,
572
- setGenerated
618
+ setGenerated,
619
+ flushRoots,
620
+ setFlushFurniture
573
621
  };
574
622
  if (typeof window !== "undefined")
575
623
  window.__gp = api;
@@ -24,6 +24,7 @@
24
24
  strideOf: () => strideOf,
25
25
  stabilizeFullHeightPageRoots: () => stabilizeFullHeightPageRoots,
26
26
  spreadModeSupported: () => spreadModeSupported,
27
+ runPageBox: () => runPageBox,
27
28
  rowStrideOf: () => rowStrideOf,
28
29
  pageRangeOf: () => pageRangeOf,
29
30
  pageOf: () => pageOf,
@@ -33,9 +34,11 @@
33
34
  injectBreakMapping: () => injectBreakMapping,
34
35
  fragmentDocument: () => fragmentDocument,
35
36
  forcedColumnBreaksSupported: () => forcedColumnBreaksSupported,
37
+ contentEdgeRect: () => contentEdgeRect,
36
38
  compensateTrailingMarginsBeforeAvoids: () => compensateTrailingMarginsBeforeAvoids,
37
39
  compensateRepeatedHeaders: () => compensateRepeatedHeaders,
38
40
  compensateRectoBreaks: () => compensateRectoBreaks,
41
+ columnReserve: () => columnReserve,
39
42
  collectCssText: () => collectCssText,
40
43
  buildStrips: () => buildStrips,
41
44
  blankPageIndices: () => blankPageIndices,
@@ -94,11 +97,23 @@
94
97
  transform: translate(var(--gp-margin-left), var(--gp-margin-top));
95
98
  }
96
99
 
97
- /* Sheets/margin boxes are painted behind/around the strip by the decoration
98
- layer; the strip itself must stay transparent so they show through. */
99
- .gp-strip > * {
100
- break-inside: auto;
101
- }
100
+ /* NOTE: there is deliberately no \`.gp-strip > * { break-inside: auto }\` here.
101
+ The original spike shipped one, under a comment about sheet transparency that
102
+ described no rule of its own, and it was never justified anywhere. Because
103
+ viewer chrome is injected after the author's stylesheet, it beat every
104
+ single-class author rule at equal specificity: a top-level block the author
105
+ declared atomic (\`.card\`, \`.section\`, a stat block) split on screen while the
106
+ PDF moved it whole and left a short page. Measured on
107
+ \`docs/fixtures/atomic-blocks\`: print 6pp, viewer 5pp with the rule, 6pp and
108
+ 6pp without it; a longer probe of the same shape ran 25pp against 21pp.
109
+ Chromium honours \`break-inside: avoid\` in multicol as it does in paged media,
110
+ including relaxing it for a block too tall to fit an empty fragmentainer, so
111
+ nothing here needs to override the author.
112
+
113
+ Scope: \`>\` means this only ever reached the flow root's OWN children, so a
114
+ book whose atomic blocks sit inside \`@page\`/\`@section\` wrappers was never
115
+ affected — which is why removing it does not move the dc-op-manual field
116
+ guide's remaining 4-page divergence. That one is still open. */
102
117
 
103
118
  /* View mode (\`applySpreadMode\` in fragment.ts): wraps this run's multicol
104
119
  columns into \`--gp-wrap-cols\` ROWS instead of one long row — 2 for
@@ -148,6 +163,23 @@
148
163
  .gp-run {
149
164
  position: relative;
150
165
  margin: 0 0 var(--gp-sheet-gap);
166
+ /* Hit-transparent, like \`.gp-layer\` below: in wrap mode a recto-starting
167
+ run is pulled up over the previous run's last row (decorate.ts's
168
+ cross-run negative margin-top — the spread-composition overlap), and
169
+ its leading \`.gp-wrap-spacer\` slot is visually empty. The covered page
170
+ paints through, but the run's full-width box used to win
171
+ elementFromPoint there, so clicks, right-clicks, link hits, and text
172
+ selection were all dead on every covered page (measured: 60/60 dead
173
+ probe points in two-column view). Runs are viewer chrome — only author
174
+ content may capture hits. */
175
+ pointer-events: none;
176
+ }
177
+
178
+ /* …and the author's content re-enables hit testing. Equal specificity with
179
+ \`.gp-run { pointer-events: none }\` above, so ORDER is load-bearing:
180
+ \`auto\` must come after \`none\`. */
181
+ .gp-strip > * {
182
+ pointer-events: auto;
151
183
  }
152
184
 
153
185
  .gp-layer {
@@ -957,6 +989,20 @@
957
989
  `);
958
990
  }
959
991
 
992
+ // src/engine/shared/flush.ts
993
+ var FLUSH_EDGES = ["top", "right", "bottom", "left"];
994
+ function flushEdgesIn(root) {
995
+ return FLUSH_EDGES.filter((edge) => root.querySelector(`.gp-pin.gp-flush.gp-${edge}`));
996
+ }
997
+ function flushMargins(margin, edges) {
998
+ return {
999
+ top: edges.includes("top") ? 0 : margin.top,
1000
+ right: edges.includes("right") ? 0 : margin.right,
1001
+ bottom: edges.includes("bottom") ? 0 : margin.bottom,
1002
+ left: edges.includes("left") ? 0 : margin.left
1003
+ };
1004
+ }
1005
+
960
1006
  // src/engine/viewer/fragment.ts
961
1007
  var PX_PER_PT = 96 / 72;
962
1008
  function injectViewerCss(doc = document) {
@@ -1005,6 +1051,21 @@
1005
1051
  function forcedColumnBreaksSupported() {
1006
1052
  return typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("break-before", "column") && CSS.supports("break-after", "column");
1007
1053
  }
1054
+ var FRAGMENT_EPSILON_PX = 1;
1055
+ function contentEdgeRect(site, atEnd) {
1056
+ const rects = Array.from(site.getClientRects());
1057
+ const solid = rects.filter((r) => r.height >= FRAGMENT_EPSILON_PX);
1058
+ const pool = solid.length ? solid : rects;
1059
+ return atEnd ? pool.at(-1) : pool[0];
1060
+ }
1061
+ function columnReserve(offset, columnHeight) {
1062
+ const remaining = columnHeight - offset;
1063
+ if (remaining < FRAGMENT_EPSILON_PX)
1064
+ return null;
1065
+ if (remaining > columnHeight - FRAGMENT_EPSILON_PX)
1066
+ return null;
1067
+ return Math.ceil(remaining);
1068
+ }
1008
1069
  function synthesizeColumnBreaks(model) {
1009
1070
  const sites = [];
1010
1071
  const seen = new WeakMap;
@@ -1041,23 +1102,22 @@
1041
1102
  const strip = el.closest(".gp-strip");
1042
1103
  if (!strip)
1043
1104
  continue;
1044
- if (prop === "break-before" && !el.previousElementSibling)
1045
- continue;
1105
+ let site = el;
1106
+ if (prop === "break-before") {
1107
+ const target = propagatedBreakTarget(el, strip);
1108
+ if (!target)
1109
+ continue;
1110
+ site = target;
1111
+ }
1046
1112
  if (prop === "break-after" && !el.nextElementSibling)
1047
1113
  continue;
1048
- const rects = Array.from(el.getClientRects());
1049
- const rect = prop === "break-after" ? rects.at(-1) : rects[0];
1114
+ const rect = contentEdgeRect(site, prop === "break-after");
1050
1115
  if (!rect)
1051
1116
  continue;
1052
1117
  const stripTop = strip.getBoundingClientRect().top;
1053
1118
  const edge = prop === "break-after" ? rect.bottom : rect.top;
1054
- const offset = edge - stripTop;
1055
- if (prop === "break-before" && offset < 0.5)
1056
- continue;
1057
- if (prop === "break-after" && strip.clientHeight - offset < 0.5)
1058
- continue;
1059
- const reserve = Math.ceil(strip.clientHeight - offset);
1060
- if (reserve <= 0)
1119
+ const reserve = columnReserve((edge - stripTop) / cssZoomOf(strip), strip.clientHeight);
1120
+ if (reserve === null)
1061
1121
  continue;
1062
1122
  const spacer = document.createElement("div");
1063
1123
  spacer.className = "gp-column-break-spacer";
@@ -1066,8 +1126,28 @@
1066
1126
  if (prop === "break-after")
1067
1127
  el.after(spacer);
1068
1128
  else
1069
- el.before(spacer);
1129
+ site.before(spacer);
1130
+ }
1131
+ }
1132
+ function propagatedBreakTarget(el, strip) {
1133
+ let site = el;
1134
+ while (!site.previousElementSibling) {
1135
+ const parent = site.parentElement;
1136
+ if (!parent || parent === strip)
1137
+ return null;
1138
+ const cs = getComputedStyle(site);
1139
+ if (cs.position === "absolute" || cs.position === "fixed" || cs.float !== "none")
1140
+ return null;
1141
+ for (let node = site.previousSibling;node; node = node.previousSibling) {
1142
+ if (node.nodeType === 3 && (node.textContent ?? "").trim() !== "")
1143
+ return site;
1144
+ }
1145
+ const pcs = getComputedStyle(parent);
1146
+ if (!/^(block|flow-root)$/.test(pcs.display) || pcs.columnCount !== "auto")
1147
+ return null;
1148
+ site = parent;
1070
1149
  }
1150
+ return site;
1071
1151
  }
1072
1152
  function directPageName(el, model) {
1073
1153
  for (const a of model.pageAssignments) {
@@ -1087,12 +1167,13 @@
1087
1167
  }
1088
1168
  return false;
1089
1169
  }
1090
- function pushRun(runs, page, nodes) {
1170
+ function pushRun(runs, page, nodes, flushEdges) {
1171
+ const key = (r) => `${r.page ?? ""}\x00${(r.flushEdges ?? []).join(",")}`;
1091
1172
  const last = runs[runs.length - 1];
1092
- if (last && last.page === page)
1173
+ if (last && key(last) === key({ page, flushEdges }))
1093
1174
  last.nodes.push(...nodes);
1094
1175
  else
1095
- runs.push({ page, nodes });
1176
+ runs.push({ page, nodes, flushEdges: flushEdges?.length ? flushEdges : undefined });
1096
1177
  }
1097
1178
  function explodeChildren(container, model) {
1098
1179
  const runs = [];
@@ -1104,7 +1185,7 @@
1104
1185
  pending = [];
1105
1186
  const last = runs[runs.length - 1];
1106
1187
  if (last) {
1107
- pushRun(runs, last.page, held);
1188
+ pushRun(runs, last.page, held, last.flushEdges);
1108
1189
  return [];
1109
1190
  }
1110
1191
  if (held.some((n) => (n.textContent ?? "").trim() !== "")) {
@@ -1121,13 +1202,14 @@
1121
1202
  const kid = node;
1122
1203
  if (kid.classList.contains("gp-layer"))
1123
1204
  continue;
1205
+ const flush = flushEdgesIn(kid);
1124
1206
  const own = directPageName(kid, model);
1125
1207
  if (own !== undefined) {
1126
- pushRun(runs, own, [...carry(), kid]);
1208
+ pushRun(runs, own, [...carry(), kid], flush);
1127
1209
  continue;
1128
1210
  }
1129
1211
  if (!hasDescendantPageAssignment(kid, model)) {
1130
- pushRun(runs, undefined, [...carry(), kid]);
1212
+ pushRun(runs, undefined, [...carry(), kid], flush);
1131
1213
  continue;
1132
1214
  }
1133
1215
  const inner = explodeChildren(kid, model);
@@ -1141,7 +1223,7 @@
1141
1223
  for (const n of r.nodes)
1142
1224
  shell.appendChild(n);
1143
1225
  kid.before(shell);
1144
- pushRun(runs, r.page, [...lead, shell]);
1226
+ pushRun(runs, r.page, [...lead, shell], flushEdgesIn(shell));
1145
1227
  lead = [];
1146
1228
  }
1147
1229
  kid.remove();
@@ -1232,19 +1314,21 @@
1232
1314
  const prevRect = prevRects.at(-1);
1233
1315
  const stripRect = stripEl.getBoundingClientRect();
1234
1316
  const { stride } = stripMetrics(stripEl);
1235
- const colOf = (r) => Math.floor((r.left - stripRect.left + 1) / stride);
1317
+ const zoom = cssZoomOf(stripEl);
1318
+ const colOf = (r) => Math.floor(((r.left - stripRect.left) / zoom + 1) / stride);
1236
1319
  const currentCol = colOf(rect);
1237
1320
  if (currentCol !== colOf(prevRect) + 1)
1238
1321
  continue;
1239
- if (Math.abs(rect.top - stripRect.top) > 0.5)
1322
+ if (Math.abs(rect.top - stripRect.top) / zoom > 0.5)
1240
1323
  continue;
1241
1324
  const marginEnd = parseFloat(getComputedStyle(prev).marginBlockEnd) || 0;
1242
1325
  if (marginEnd <= 0.5)
1243
1326
  continue;
1244
- const remaining = stripEl.clientHeight - (prevRect.bottom - stripRect.top);
1245
- if (rect.height > remaining + 0.5)
1327
+ const remaining = stripEl.clientHeight - (prevRect.bottom - stripRect.top) / zoom;
1328
+ const height = rect.height / zoom;
1329
+ if (height > remaining + 0.5)
1246
1330
  continue;
1247
- if (rect.height + marginEnd <= remaining + 0.5)
1331
+ if (height + marginEnd <= remaining + 0.5)
1248
1332
  continue;
1249
1333
  prev.dataset.gpTrailingMargin = "compensated";
1250
1334
  prev.dataset.gpTrailingMarginValue = prev.style.getPropertyValue("margin-block-end");
@@ -1277,6 +1361,20 @@
1277
1361
  }
1278
1362
  }
1279
1363
  }
1364
+ function runPageBox(model, name, warnings = []) {
1365
+ const right = resolvePage(model, { name, pseudos: ["right"] }).geometry;
1366
+ const left = resolvePage(model, { name, pseudos: ["left"] }).geometry;
1367
+ const box = (g) => ({
1368
+ w: g.width - g.margin.left - g.margin.right,
1369
+ h: g.height - g.margin.top - g.margin.bottom
1370
+ });
1371
+ const r = box(right);
1372
+ const l = box(left);
1373
+ if (Math.abs(r.w - l.w) < 0.01 && Math.abs(r.h - l.h) < 0.01)
1374
+ return right;
1375
+ warnings.push(`@page :left and @page :right give ${name ? `the "${name}" page` : "this book"} ` + `different content areas (${l.w.toFixed(1)}×${l.h.toFixed(1)}pt vs ` + `${r.w.toFixed(1)}×${r.h.toFixed(1)}pt). Print alternates between them; the preview can ` + `only show one, so page breaks here may not match the PDF. Mirror the margins instead: ` + `swap which edge is wide and keep the total the same.`);
1376
+ return resolvePage(model, { name }).geometry;
1377
+ }
1280
1378
  function buildStrips(model, opts = {}, warnings = []) {
1281
1379
  const doc = document;
1282
1380
  const root = opts.root ?? doc.querySelector("main") ?? doc.body;
@@ -1284,26 +1382,34 @@
1284
1382
  const runs = explodeChildren(root, model);
1285
1383
  const strips = [];
1286
1384
  for (const run of runs) {
1287
- const { geometry } = resolvePage(model, { name: run.page });
1385
+ const geometry = runPageBox(model, run.page, warnings);
1288
1386
  const strip = doc.createElement("div");
1289
1387
  strip.className = "gp-strip";
1290
1388
  if (run.page)
1291
1389
  strip.dataset.page = run.page;
1292
- const w = pt(geometry.width - geometry.margin.left - geometry.margin.right);
1293
- const h = pt(geometry.height - geometry.margin.top - geometry.margin.bottom);
1390
+ const margin = run.flushEdges?.length ? flushMargins(geometry.margin, run.flushEdges) : geometry.margin;
1391
+ const w = pt(geometry.width - margin.left - margin.right);
1392
+ const h = pt(geometry.height - margin.top - margin.bottom);
1294
1393
  strip.style.setProperty("--gp-content-w", `${w}px`);
1295
1394
  strip.style.setProperty("--gp-content-h", `${h}px`);
1296
1395
  strip.style.setProperty("--gp-sheet-gap", `${gap}px`);
1297
1396
  strip.style.setProperty("--gp-page-w", `${pt(geometry.width)}px`);
1298
1397
  strip.style.setProperty("--gp-page-h", `${pt(geometry.height)}px`);
1299
- strip.style.setProperty("--gp-margin-top", `${pt(geometry.margin.top)}px`);
1300
- strip.style.setProperty("--gp-margin-right", `${pt(geometry.margin.right)}px`);
1301
- strip.style.setProperty("--gp-margin-bottom", `${pt(geometry.margin.bottom)}px`);
1302
- strip.style.setProperty("--gp-margin-left", `${pt(geometry.margin.left)}px`);
1398
+ strip.style.setProperty("--gp-margin-top", `${pt(margin.top)}px`);
1399
+ strip.style.setProperty("--gp-margin-right", `${pt(margin.right)}px`);
1400
+ strip.style.setProperty("--gp-margin-bottom", `${pt(margin.bottom)}px`);
1401
+ strip.style.setProperty("--gp-margin-left", `${pt(margin.left)}px`);
1303
1402
  run.nodes[0].before(strip);
1304
1403
  for (const n of run.nodes)
1305
1404
  strip.appendChild(n);
1306
- strips.push({ el: strip, page: run.page, geometry, pages: 0, offset: 0 });
1405
+ strips.push({
1406
+ el: strip,
1407
+ page: run.page,
1408
+ geometry,
1409
+ flushEdges: run.flushEdges,
1410
+ pages: 0,
1411
+ offset: 0
1412
+ });
1307
1413
  }
1308
1414
  for (const s of strips)
1309
1415
  clearLeadingForcedBreaks(s.el);
@@ -1329,14 +1435,15 @@
1329
1435
  passes = Math.max(passes, pass + 1);
1330
1436
  const stride = strideOf(strip.el);
1331
1437
  const stripLeft = strip.el.getBoundingClientRect().left - strip.el.scrollLeft;
1332
- const colOf = (r) => Math.floor((r.left - stripLeft + 1) / stride);
1438
+ const zoom = cssZoomOf(strip.el);
1439
+ const colOf = (r) => Math.floor(((r.left - stripLeft) / zoom + 1) / stride);
1333
1440
  const stripTop = strip.el.getBoundingClientRect().top;
1334
1441
  const colBottom = strip.el.clientHeight;
1335
1442
  const plans = [];
1336
1443
  for (const table of tables) {
1337
1444
  const head = table.tHead;
1338
1445
  const headRect = head?.getClientRects()[0];
1339
- const footHeight = table.tFoot?.getBoundingClientRect().height ?? 0;
1446
+ const footHeight = (table.tFoot?.getBoundingClientRect().height ?? 0) / zoom;
1340
1447
  const rows = Array.from(table.querySelectorAll("tbody > tr")).filter((r) => !r.classList.contains("gp-thead-shim") && !r.classList.contains("gp-tfoot-shim"));
1341
1448
  if (!rows.length)
1342
1449
  continue;
@@ -1351,10 +1458,10 @@
1351
1458
  const row = rows[i];
1352
1459
  if (cols[i] === lastCol || claims.has(row))
1353
1460
  continue;
1354
- const bottom = rects[i].bottom - stripTop;
1461
+ const bottom = (rects[i].bottom - stripTop) / zoom;
1355
1462
  if (bottom > colBottom - footHeight + 0.5) {
1356
1463
  newClaim = row;
1357
- claims.set(row, colBottom - (rects[i].top - stripTop));
1464
+ claims.set(row, colBottom - (rects[i].top - stripTop) / zoom);
1358
1465
  break;
1359
1466
  }
1360
1467
  }
@@ -1362,7 +1469,7 @@
1362
1469
  plans.push({
1363
1470
  table,
1364
1471
  push: headRect ? colOf(headRect) < cols[0] : false,
1365
- headHeight: headRect?.height ?? 0,
1472
+ headHeight: (headRect?.height ?? 0) / zoom,
1366
1473
  footHeight,
1367
1474
  breakRows: headRect ? rows.filter((_, i) => i > 0 && cols[i] > cols[i - 1]) : [],
1368
1475
  footRows: [...claims.entries()],
@@ -1486,6 +1593,9 @@
1486
1593
  function strideOf(strip) {
1487
1594
  return stripMetrics(strip).stride;
1488
1595
  }
1596
+ function cssZoomOf(el) {
1597
+ return el.currentCSSZoom ?? 1;
1598
+ }
1489
1599
  function stripMetrics(strip) {
1490
1600
  const cs = getComputedStyle(strip);
1491
1601
  const w = parseFloat(cs.getPropertyValue("--gp-content-w"));
@@ -1508,12 +1618,13 @@
1508
1618
  function indexInStrip(left, top, strip) {
1509
1619
  const { stride, rowStride } = stripMetrics(strip.el);
1510
1620
  const stripBox = strip.el.getBoundingClientRect();
1511
- const stripLeft = stripBox.left - strip.el.scrollLeft;
1512
- const stripTop = stripBox.top;
1621
+ const zoom = cssZoomOf(strip.el);
1622
+ const x = (left - stripBox.left) / zoom + strip.el.scrollLeft;
1623
+ const y = (top - stripBox.top) / zoom;
1513
1624
  const { perRow, shift } = wrapGeometry(strip);
1514
- const colVisual = Math.floor((left - stripLeft + 1) / stride);
1625
+ const colVisual = Math.floor((x + 1) / stride);
1515
1626
  const colClamped = Math.max(0, Math.min(perRow - 1, colVisual));
1516
- const row = Math.max(0, Math.floor((top - stripTop + 1) / rowStride));
1627
+ const row = Math.max(0, Math.floor((y + 1) / rowStride));
1517
1628
  const idx = row * perRow + colClamped - shift;
1518
1629
  return Math.max(0, Math.min(strip.pages - 1, idx));
1519
1630
  }
@@ -1864,6 +1975,37 @@
1864
1975
  function isIgnoredMarginBoxProperty(property) {
1865
1976
  return MARGIN_BOX_IGNORED_PROPERTIES.has(property.toLowerCase());
1866
1977
  }
1978
+ function marginBoxRectPt(name, g) {
1979
+ const { top, right, bottom, left } = g.margin;
1980
+ const cw = g.width - left - right;
1981
+ const ch = g.height - top - bottom;
1982
+ const third = (n) => n / 3;
1983
+ const T = {
1984
+ "top-left-corner": [0, 0, left, top],
1985
+ "top-left": [left, 0, third(cw), top],
1986
+ "top-center": [left + third(cw), 0, third(cw), top],
1987
+ "top-right": [left + 2 * third(cw), 0, third(cw), top],
1988
+ "top-right-corner": [g.width - right, 0, right, top],
1989
+ "bottom-left-corner": [0, g.height - bottom, left, bottom],
1990
+ "bottom-left": [left, g.height - bottom, third(cw), bottom],
1991
+ "bottom-center": [left + third(cw), g.height - bottom, third(cw), bottom],
1992
+ "bottom-right": [left + 2 * third(cw), g.height - bottom, third(cw), bottom],
1993
+ "bottom-right-corner": [g.width - right, g.height - bottom, right, bottom],
1994
+ "left-top": [0, top, left, third(ch)],
1995
+ "left-middle": [0, top + third(ch), left, third(ch)],
1996
+ "left-bottom": [0, top + 2 * third(ch), left, third(ch)],
1997
+ "right-top": [g.width - right, top, right, third(ch)],
1998
+ "right-middle": [g.width - right, top + third(ch), right, third(ch)],
1999
+ "right-bottom": [g.width - right, top + 2 * third(ch), right, third(ch)]
2000
+ };
2001
+ const [x, y, w, h] = T[name] ?? [0, 0, 0, 0];
2002
+ return { x, y, w, h };
2003
+ }
2004
+ function marginBoxAlign(name) {
2005
+ if (name.includes("center") || name.includes("middle"))
2006
+ return "center";
2007
+ return /right/.test(name) ? "end" : "start";
2008
+ }
1867
2009
 
1868
2010
  // src/engine/viewer/decorate.ts
1869
2011
  var px = (v) => `${v * PX_PER_PT}px`;
@@ -2120,7 +2262,7 @@
2120
2262
  box.className = "gp-marginbox";
2121
2263
  box.dataset.box = name;
2122
2264
  Object.assign(box.style, rectFor(name, g));
2123
- box.dataset.align = name.includes("center") || name.includes("middle") ? "center" : /right/.test(name) ? "end" : "start";
2265
+ box.dataset.align = marginBoxAlign(name);
2124
2266
  const content = document.createElement("span");
2125
2267
  content.className = "gp-marginbox-content";
2126
2268
  content.textContent = text;
@@ -2232,30 +2374,8 @@
2232
2374
  return run;
2233
2375
  }
2234
2376
  function rectFor(name, g) {
2235
- const { top, right, bottom, left } = g.margin;
2236
- const cw = g.width - left - right;
2237
- const ch = g.height - top - bottom;
2238
- const third = (n) => n / 3;
2239
- const T = {
2240
- "top-left-corner": [0, 0, left, top],
2241
- "top-left": [left, 0, third(cw), top],
2242
- "top-center": [left + third(cw), 0, third(cw), top],
2243
- "top-right": [left + 2 * third(cw), 0, third(cw), top],
2244
- "top-right-corner": [g.width - right, 0, right, top],
2245
- "bottom-left-corner": [0, g.height - bottom, left, bottom],
2246
- "bottom-left": [left, g.height - bottom, third(cw), bottom],
2247
- "bottom-center": [left + third(cw), g.height - bottom, third(cw), bottom],
2248
- "bottom-right": [left + 2 * third(cw), g.height - bottom, third(cw), bottom],
2249
- "bottom-right-corner": [g.width - right, g.height - bottom, right, bottom],
2250
- "left-top": [0, top, left, third(ch)],
2251
- "left-middle": [0, top + third(ch), left, third(ch)],
2252
- "left-bottom": [0, top + 2 * third(ch), left, third(ch)],
2253
- "right-top": [g.width - right, top, right, third(ch)],
2254
- "right-middle": [g.width - right, top + third(ch), right, third(ch)],
2255
- "right-bottom": [g.width - right, top + 2 * third(ch), right, third(ch)]
2256
- };
2257
- const [x, y, w, h] = T[name] ?? [0, 0, 0, 0];
2258
- return { left: px(x), top: px(y), width: px(w), height: px(h) };
2377
+ const r = marginBoxRectPt(name, g);
2378
+ return { left: px(r.x), top: px(r.y), width: px(r.w), height: px(r.h) };
2259
2379
  }
2260
2380
 
2261
2381
  // src/engine/viewer/index.ts