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
@@ -10,6 +10,56 @@ export default function plugin(md: any, pluginOptions?: {}): void;
10
10
  * on its own page instead of resolving against the document canvas and
11
11
  * painting on the last page of the book.
12
12
  *
13
+ * `min-height: var(--gp-content-h)` is the other half of that contract: a
14
+ * page root that only shrink-wraps its text is a containing block whose
15
+ * `bottom` edge is the end of the PROSE, so `.gp-pin .gp-bottom` (and every
16
+ * hand-written `position: absolute; bottom: 0`) lands under the last
17
+ * paragraph instead of at the page foot. Paged.js used to supply this height
18
+ * for free — `.pagedjs_pagebox > .pagedjs_area > .pagedjs_page_content > div
19
+ * { height: inherit; }` stretched the page root to the page area — and
20
+ * deleting the polyfill (native-only migration, Phase 6) silently took
21
+ * page-boundary pinning with it. Both renderers now publish the page
22
+ * CONTENT height as `--gp-content-h` for the page context the element is in
23
+ * (the viewer on each `.gp-strip`; the compiler on `:root` plus every
24
+ * `page:` assignment selector), and custom properties inherit, so this one
25
+ * rule reaches a page root at any wrapper depth. Undefined var (plain
26
+ * markdown-it use, no engine) falls back to `1px`, which the cushion below
27
+ * takes back to zero — the rule is inert outside the pipeline, leaving the
28
+ * page root shrink-wrapped exactly as it is today.
29
+ *
30
+ * It is `min-height`, not `height`: a `.page` whose content runs past one
31
+ * sheet must still fragment normally. The whole rule is `:where()`, so an
32
+ * author who wants any of it back sets their own value at any specificity.
33
+ *
34
+ * `box-sizing` and `display` are what make a page-sized min-height safe, and
35
+ * both were found by MEASURING a real book (docs/fixtures/css-authoring-spike)
36
+ * print one extra sheet without them:
37
+ *
38
+ * - `border-box`, because a page root with padding (that fixture's cover
39
+ * has `padding-top: 2.5in`) would otherwise be one content box PLUS its
40
+ * padding tall, which is one sheet plus 2.5in — the overflow lands on a
41
+ * spurious blank sheet.
42
+ * - `flow-root`, because a child margin collapsing THROUGH the root's edge
43
+ * survives the forced break and eats into the new sheet: that fixture's
44
+ * last page opens with an `<h2>` whose 22.4px margin-top pushed a
45
+ * 719px-tall root down 22.4px on a 720px sheet, spilling it. A page root
46
+ * is a page; containing its children's margins is what a page does. The
47
+ * viewer already forces `flow-root` on displaced page roots for exactly
48
+ * this reason (`stabilizeFullHeightPageRoots`, fragment.ts) — declaring
49
+ * it once in core means both renderers start from the same box. Zero
50
+ * specificity, so an authored `display: flex`/`grid` page root still
51
+ * wins, which is the case that function's comment warns about.
52
+ *
53
+ * The `- 1px` is the fragmentation cushion, and it is load-bearing. A box
54
+ * whose bottom edge lands EXACTLY on the fragmentainer's edge costs an empty
55
+ * fragment: MEASURED (Chromium 148, 384x480px sheet, 24px margins, one
56
+ * short page root) `min-height: 432px` prints 2 sheets with the second
57
+ * blank while `431px` prints 1 — and the viewer's multicol strip does the
58
+ * same, which is how it first showed up (an 8pp preview of a 7pp book on
59
+ * the gp-image-positioning fixture). One px at 96dpi is 0.75pt: it moves a
60
+ * pinned foot up by less than a point and absorbs sub-pixel rounding in the
61
+ * published value. Do not "clean it up" — the sheet it costs is silent.
62
+ *
13
63
  * The break/orphan rules below (`break-after` on headings, image sizing,
14
64
  * first-child glue) are all `:where()` so they carry zero specificity —
15
65
  * author CSS at any specificity wins outright, reusing this same
@@ -29,4 +79,4 @@ export default function plugin(md: any, pluginOptions?: {}): void;
29
79
  * `text-align: center` still centers it — `display: block` would not).
30
80
  *
31
81
  */
32
- export const MARKER_CSS: "\n/* The UA default of 8px body margin is a screen affordance with no meaning\n in paged media, and engines disagree about it: a polyfill that treats the\n page div as the page box drops it, native print keeps it. Left in place it\n insets EVERY page's content by 8px per side, and -- measured, 300dpi,\n 6x4in sheet -- it is what stops a full-width block from reaching the\n paper: it lands at 0.080..5.917in of a 6in sheet instead of\n 0.000..6.000in, because width:100% resolves against the BODY content box,\n not the page's. Zeroing it here (first in the cascade) makes the two\n agree. Authors who want a body margin still just declare one. */\nbody { margin: 0; }\n\n.gp-page-break { break-before: page; }\n.page { break-before: page; }\n.spread { break-before: page; }\n:where(.page, .spread) { position: relative; }\n.gp-column-break { break-after: column; height: 0; font-size: 0; line-height: 0; visibility: hidden; }\n\n:where(h1,h2,h3,h4,h5,h6) { break-after: avoid; }\n:where(img, svg, video) { max-width: 100%; }\n:where(p > img:only-child, figure > img) { width: fit-content; max-width: 100%; height: auto; vertical-align: bottom; }\n:where(.section, figure) > :where(:first-child) { break-before: avoid; }\n\n";
82
+ export const MARKER_CSS: "\n/* The UA default of 8px body margin is a screen affordance with no meaning\n in paged media, and engines disagree about it: a polyfill that treats the\n page div as the page box drops it, native print keeps it. Left in place it\n insets EVERY page's content by 8px per side, and -- measured, 300dpi,\n 6x4in sheet -- it is what stops a full-width block from reaching the\n paper: it lands at 0.080..5.917in of a 6in sheet instead of\n 0.000..6.000in, because width:100% resolves against the BODY content box,\n not the page's. Zeroing it here (first in the cascade) makes the two\n agree. Authors who want a body margin still just declare one. */\nbody { margin: 0; }\n\n.gp-page-break { break-before: page; }\n.page { break-before: page; }\n.spread { break-before: page; }\n:where(.page, .spread) { position: relative; display: flow-root; box-sizing: border-box; min-height: calc(var(--gp-content-h, 1px) - 1px); }\n.gp-column-break { break-after: column; height: 0; font-size: 0; line-height: 0; visibility: hidden; }\n\n:where(h1,h2,h3,h4,h5,h6) { break-after: avoid; }\n:where(img, svg, video) { max-width: 100%; }\n:where(p > img:only-child, figure > img) { width: fit-content; max-width: 100%; height: auto; vertical-align: bottom; }\n:where(.section, figure) > :where(:first-child) { break-before: avoid; }\n\n";
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  MANIFEST_FILENAMES,
3
3
  runLint
4
- } from "./cli-n25qycwz.js";
4
+ } from "./cli-eq5naw4m.js";
5
5
  import {
6
6
  log
7
- } from "./cli-ra0ed2xt.js";
7
+ } from "./cli-cqtggsng.js";
8
8
  import {
9
9
  UsageError,
10
10
  rejectExtraPositionals,
11
11
  rejectUnknownFlags
12
- } from "./cli-0r0tq16s.js";
13
- import"./cli-hp9r2pzt.js";
12
+ } from "./cli-149edp6b.js";
13
+ import"./cli-zfcryxg8.js";
14
14
  import"./cli-c41yr7he.js";
15
15
  import {
16
16
  EXIT_CODES
@@ -5,16 +5,16 @@ import {
5
5
  TARGETS,
6
6
  TARGET_IDS,
7
7
  scaffoldProject
8
- } from "./cli-n25qycwz.js";
8
+ } from "./cli-eq5naw4m.js";
9
9
  import {
10
10
  resolveGhostscript
11
- } from "./cli-ra0ed2xt.js";
11
+ } from "./cli-cqtggsng.js";
12
12
  import {
13
13
  UsageError,
14
14
  rejectExtraPositionals,
15
15
  rejectUnknownFlags
16
- } from "./cli-0r0tq16s.js";
17
- import"./cli-hp9r2pzt.js";
16
+ } from "./cli-149edp6b.js";
17
+ import"./cli-zfcryxg8.js";
18
18
  import {
19
19
  isToolAvailable
20
20
  } from "./cli-c41yr7he.js";
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  addNpmPlugin,
3
3
  parseNpmPluginSpec
4
- } from "./cli-n25qycwz.js";
5
- import"./cli-ra0ed2xt.js";
4
+ } from "./cli-eq5naw4m.js";
5
+ import"./cli-cqtggsng.js";
6
6
  import {
7
7
  UsageError,
8
8
  rejectExtraPositionals,
9
9
  rejectUnknownFlags
10
- } from "./cli-0r0tq16s.js";
11
- import"./cli-hp9r2pzt.js";
10
+ } from "./cli-149edp6b.js";
11
+ import"./cli-zfcryxg8.js";
12
12
  import"./cli-c41yr7he.js";
13
13
  import {
14
14
  EXIT_CODES
@@ -2,16 +2,16 @@ import {
2
2
  executeValidation,
3
3
  publishTargetFor,
4
4
  reportMissingTools
5
- } from "./cli-n25qycwz.js";
5
+ } from "./cli-eq5naw4m.js";
6
6
  import {
7
7
  log
8
- } from "./cli-ra0ed2xt.js";
8
+ } from "./cli-cqtggsng.js";
9
9
  import {
10
10
  UsageError,
11
11
  rejectExtraPositionals,
12
12
  rejectUnknownFlags
13
- } from "./cli-0r0tq16s.js";
14
- import"./cli-hp9r2pzt.js";
13
+ } from "./cli-149edp6b.js";
14
+ import"./cli-zfcryxg8.js";
15
15
  import"./cli-c41yr7he.js";
16
16
  import {
17
17
  EXIT_CODES
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  previewArgs
3
- } from "./cli-revgt4pr.js";
3
+ } from "./cli-yzf38679.js";
4
4
  import {
5
5
  openPath,
6
6
  runBuild,
7
7
  splitOutPath,
8
8
  startPreviewServer
9
- } from "./cli-n25qycwz.js";
9
+ } from "./cli-eq5naw4m.js";
10
10
  import {
11
11
  log
12
- } from "./cli-ra0ed2xt.js";
12
+ } from "./cli-cqtggsng.js";
13
13
  import {
14
14
  UsageError,
15
15
  parseEngine,
@@ -18,8 +18,8 @@ import {
18
18
  rejectExtraPositionals,
19
19
  rejectUnknownFlags,
20
20
  resolvePort
21
- } from "./cli-0r0tq16s.js";
22
- import"./cli-hp9r2pzt.js";
21
+ } from "./cli-149edp6b.js";
22
+ import"./cli-zfcryxg8.js";
23
23
  import"./cli-c41yr7he.js";
24
24
  import {
25
25
  BuildError
@@ -82,6 +82,7 @@ var preview_default = defineCommand({
82
82
  skipLint: !!args["skip-lint"],
83
83
  skipPreValidate: !!args["skip-pre-validate"],
84
84
  skipPostValidate: !!args["skip-post-validate"],
85
+ allowShrink: !!args["allow-shrink"],
85
86
  engine,
86
87
  rawArgs: args
87
88
  });
@@ -331,6 +331,125 @@
331
331
  return null;
332
332
  }
333
333
 
334
+ // Full hit stack at a point, top-most first. Degrades to the single
335
+ // elementFromPoint() hit on hosts without elementsFromPoint().
336
+ function elementStackAtPoint(x, y) {
337
+ try {
338
+ if (typeof document.elementsFromPoint === 'function') {
339
+ var els = document.elementsFromPoint(x, y);
340
+ if (els && els.length) return Array.prototype.slice.call(els);
341
+ }
342
+ } catch (_e) { /* fall through to the single-element path */ }
343
+ var el = elementAtPoint(x, y);
344
+ return el ? [el] : [];
345
+ }
346
+
347
+ // Computed z-index as a number; NaN for 'auto'/unset/unsupported hosts
348
+ // (NaN < 0 is false, so those never qualify as behind-layered below).
349
+ function zIndexOf(el) {
350
+ try {
351
+ if (typeof window.getComputedStyle === 'function') {
352
+ return parseFloat(window.getComputedStyle(el).zIndex);
353
+ }
354
+ } catch (_e) { /* unsupported host */ }
355
+ return NaN;
356
+ }
357
+
358
+ // Why elementsFromPoint (plural) exists here: a `.gp-behind` image — the
359
+ // image-properties "Layer" facet sets `z-index: -1` to paint a pinned
360
+ // plate UNDER the page's own text (gutterpress-css.ts's depth ladder) —
361
+ // hit-tests beneath the covering paragraph boxes and the annotated
362
+ // `.page`/`.section` containers too, so document.elementFromPoint() can
363
+ // NEVER return it. Every right-click used to resolve the covering element
364
+ // instead, leaving the image's context menu unreachable at EVERY point.
365
+ // Probe the full hit stack for the first (= top-most in paint order, so
366
+ // the upper of two overlapping plates wins) image layered at negative z.
367
+ // ONLY negative-z images qualify: a normally-layered image that merely
368
+ // overlaps other content is already reachable at its uncovered points,
369
+ // and stealing the covering content's right-clicks would invert the bug.
370
+ // Computed style, not a `gp-behind` class test, mirrors the build-time
371
+ // engine.layer.trapped audit — book CSS can layer an image behind with a
372
+ // bare `z-index: -1` of its own.
373
+ function behindImageInStack(stack) {
374
+ for (var i = 0; i < stack.length; i++) {
375
+ var el = stack[i];
376
+ if (el.tagName && el.tagName.toLowerCase() === 'img' && zIndexOf(el) < 0) return el;
377
+ }
378
+ return null;
379
+ }
380
+
381
+ // The .gp-sheet whose box contains a viewport point, or null. The sheet
382
+ // chrome lives in the hit-transparent .gp-layer (viewer.css), so it can
383
+ // never appear in the hit stack — this is a geometric scan of the cached
384
+ // page list (refreshPages), not a DOM hit test.
385
+ function sheetAtPoint(x, y) {
386
+ if (pages.length === 0) refreshPages();
387
+ for (var i = 0; i < pages.length; i++) {
388
+ var r = pages[i].getBoundingClientRect();
389
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return pages[i];
390
+ }
391
+ return null;
392
+ }
393
+
394
+ // The annotated .page/.spread wrapper that OWNS a sheet: greatest
395
+ // client-rect intersection with the sheet's box — the same rect-vs-sheet
396
+ // resolution nativeRectsFor() uses. Null when the page has no author
397
+ // @page/@spread wrapper (plain flowed content) — callers then keep the
398
+ // plain hit result.
399
+ function pageMarkerElForSheet(sheet) {
400
+ var sheetRect = sheet.getBoundingClientRect();
401
+ var candidates = document.querySelectorAll('.page[data-source-range], .spread[data-source-range]');
402
+ var best = null;
403
+ var bestArea = 0;
404
+ for (var i = 0; i < candidates.length; i++) {
405
+ var rects = rectsOf(candidates[i]);
406
+ for (var j = 0; j < rects.length; j++) {
407
+ var r = rects[j];
408
+ var width = Math.min(r.right, sheetRect.right) - Math.max(r.left, sheetRect.left);
409
+ var height = Math.min(r.bottom, sheetRect.bottom) - Math.max(r.top, sheetRect.top);
410
+ var area = width > 0 && height > 0 ? width * height : 0;
411
+ if (area > bestArea) {
412
+ bestArea = area;
413
+ best = candidates[i];
414
+ }
415
+ }
416
+ }
417
+ return best;
418
+ }
419
+
420
+ // The element getContextTargetAt() resolves the payload from. `topmostOnly`
421
+ // (the keyboard path) skips the behind-image probe: its anchor is a
422
+ // SYNTHETIC block-center point (keyboardAnchorPoint()), not a user-aimed
423
+ // pointer position — probing beneath it would hijack every keyboard menu
424
+ // on a page with a background plate.
425
+ function contextPointEl(x, y, topmostOnly) {
426
+ var stack = elementStackAtPoint(x, y);
427
+ var topEl = stack.length ? stack[0] : null;
428
+ if (topmostOnly || !topEl || !topEl.closest) return topEl;
429
+ // The user aimed at visible interactive content — a directly-hit image
430
+ // (an <img> only ever matches closest('img') as itself: images have no
431
+ // descendants) or a link's own text — never probe beneath those.
432
+ if (topEl.closest('img') || topEl.closest('a')) return topEl;
433
+ // Margin-box furniture (running headers, page numbers) keeps its native
434
+ // context menu even when a full-bleed plate runs beneath it — the
435
+ // contextmenu listener's kind==='none' contract below.
436
+ if (topEl.closest('.gp-marginbox')) return topEl;
437
+ var behind = behindImageInStack(stack);
438
+ if (behind) return behind;
439
+ // Margin band (protocol v7): the point sits in a page's margin — outside
440
+ // every author box, so nothing annotated is under it (the sheet chrome
441
+ // and the run/strip boxes are hit-transparent viewer chrome). When the
442
+ // point still lies inside a .gp-sheet, resolve to the annotated
443
+ // .page/.spread that owns that sheet, so right-clicking anywhere on the
444
+ // paper reaches the @page marker, not just the content box.
445
+ if (!resolveAnnotatedBlock(topEl)) {
446
+ var sheet = sheetAtPoint(x, y);
447
+ var owner = sheet ? pageMarkerElForSheet(sheet) : null;
448
+ if (owner) return owner;
449
+ }
450
+ return topEl;
451
+ }
452
+
334
453
  function elementOf(node) {
335
454
  if (!node) return null;
336
455
  return node.nodeType === 1 ? node : (node.parentElement || null);
@@ -454,6 +573,21 @@
454
573
 
455
574
  var block = resolveAnnotatedBlock(pointEl);
456
575
 
576
+ // Secondary page-marker target (protocol v7): resolveAnnotatedBlock()
577
+ // returns the INNERMOST annotated block, so inside a @section the
578
+ // enclosing @page marker never wins the primary slot. Surface the
579
+ // enclosing .page/.spread/.chapter wrapper's own marker line as a
580
+ // secondary field — the same always-populated pattern image/link/
581
+ // selection use — so the menu can offer "Edit page marker…" when it
582
+ // differs from the primary target.
583
+ var pageMarkerEl = pointEl && pointEl.closest ? pointEl.closest('.page, .spread, .chapter') : null;
584
+ var pageMarkerRange = pageMarkerEl ? sourceRangeOf(pageMarkerEl) : null;
585
+ var pageMarker = pageMarkerRange ? {
586
+ chapter: chapterOf(pageMarkerEl),
587
+ range: pageMarkerRange,
588
+ blockTag: pageMarkerEl.tagName ? pageMarkerEl.tagName.toLowerCase() : null
589
+ } : null;
590
+
457
591
  var kind;
458
592
  if (selection) kind = 'selection';
459
593
  else if (image) kind = 'image';
@@ -471,7 +605,8 @@
471
605
  rect: plainRect(block || pointEl),
472
606
  image: image,
473
607
  link: link,
474
- selection: selection
608
+ selection: selection,
609
+ pageMarker: pageMarker
475
610
  };
476
611
  }
477
612
 
@@ -609,8 +744,10 @@
609
744
  // `setSpread()` does the real relayout: `column-wrap: wrap` wraps each
610
745
  // chapter's columns into a 2-column grid so content, not just sheet
611
746
  // chrome, actually moves into pairs — see fragment.ts's
612
- // `applySpreadMode`. No-ops to single-row on a browser without the
613
- // capability (Firefox/Safari as of this writing).
747
+ // `applySpreadMode`, which no-ops to single-row where CSS
748
+ // `column-wrap`/`column-height` are unavailable (Chromium < 145).
749
+ // The guard below is a LIB-version check, not a browser one: a
750
+ // hot-updated shell may be paired with an older bundled viewer.
614
751
  if (window.Gutterpress && typeof window.Gutterpress.setSpread === 'function') {
615
752
  window.Gutterpress.setSpread(currentViewMode !== 'single');
616
753
  }
@@ -660,13 +797,21 @@
660
797
  // v6 (WORK PACKAGE B item 2): getRectsFor()/setEditMask() dropped the
661
798
  // {ref} form entirely — {chapter, range} is the only target shape now
662
799
  // (the native viewer never mints a ref — it never clones).
663
- getProtocolVersion: function () { return 6; },
800
+ // v7: getContextTargetAt() gained the `pageMarker` secondary field (the
801
+ // enclosing .page/.spread/.chapter marker) and the margin-band fallback
802
+ // (a point inside a .gp-sheet but outside every author box resolves to
803
+ // the sheet's owning .page/.spread). An older SPA simply ignores the
804
+ // extra field; a newer SPA feature-detects by the field's presence.
805
+ getProtocolVersion: function () { return 7; },
664
806
 
665
807
  // Resolve the annotated element/selection at a viewport point (protocol
666
808
  // v4). Pure read; see buildContextTarget() above for the full contract.
809
+ // Point resolution prefers a behind-layered (negative z-index) image the
810
+ // top-most hit covers — see contextPointEl(); `spec.topmostOnly` opts a
811
+ // synthetic-anchor caller (the keyboard listener) out of that probe.
667
812
  getContextTargetAt: function (spec) {
668
813
  spec = spec || {};
669
- return buildContextTarget(elementAtPoint(spec.x, spec.y));
814
+ return buildContextTarget(contextPointEl(spec.x, spec.y, spec.topmostOnly));
670
815
  },
671
816
 
672
817
  // All fragment rects for one logical block (protocol v6, §5.3), targeted
@@ -976,7 +1121,9 @@
976
1121
  var isMenuKey = e.key === 'ContextMenu';
977
1122
  if (!isShiftF10 && !isMenuKey) return;
978
1123
  var anchor = keyboardAnchorPoint();
979
- var detail = api.getContextTargetAt({ x: anchor.x, y: anchor.y });
1124
+ // topmostOnly: the anchor is a synthetic block-center point, so the
1125
+ // behind-image probe must not run — see contextPointEl().
1126
+ var detail = api.getContextTargetAt({ x: anchor.x, y: anchor.y, topmostOnly: true });
980
1127
  detail.x = anchor.x;
981
1128
  detail.y = anchor.y;
982
1129
  detail.via = 'keyboard';
@@ -7,17 +7,17 @@ import {
7
7
  publishCredentialKey,
8
8
  publishProviderFor,
9
9
  runPublish
10
- } from "./cli-n25qycwz.js";
10
+ } from "./cli-eq5naw4m.js";
11
11
  import {
12
12
  FileTokenStore,
13
13
  log
14
- } from "./cli-ra0ed2xt.js";
14
+ } from "./cli-cqtggsng.js";
15
15
  import {
16
16
  UsageError,
17
17
  rejectExtraPositionals,
18
18
  rejectUnknownFlags
19
- } from "./cli-0r0tq16s.js";
20
- import"./cli-hp9r2pzt.js";
19
+ } from "./cli-149edp6b.js";
20
+ import"./cli-zfcryxg8.js";
21
21
  import"./cli-c41yr7he.js";
22
22
  import {
23
23
  EXIT_CODES
package/dist/render.js CHANGED
@@ -172,6 +172,9 @@ function warn(env, line, type, message, marker) {
172
172
  env.layoutWarnings = [];
173
173
  env.layoutWarnings.push({ line, type, message, marker });
174
174
  }
175
+ function proseEscapeHint(kind) {
176
+ return ` If this line is prose that happens to begin with "@${kind}" — a wrapped sentence, for` + ` instance — escape it as \\@${kind} or write it as \`@${kind}\` in backticks, and it stays text.`;
177
+ }
175
178
  function addClasses(token, baseClass, extraClass) {
176
179
  const cls = [];
177
180
  if (baseClass)
@@ -233,14 +236,14 @@ function plugin(md, pluginOptions = {}) {
233
236
  const unknown = parsed.__unknownTokens;
234
237
  delete parsed.__unknownTokens;
235
238
  for (const t of unknown) {
236
- warn(state.env, startLine + 1, "unrecognized_marker_token", `@${parsed.kind}: "${t}" is not something a marker understands, so it was kept verbatim as a class name. Write a class as .my-class (or {.my-class}), an id as #my-id, and anything else as key=value.`, parsed);
239
+ warn(state.env, startLine + 1, "unrecognized_marker_token", `@${parsed.kind}: "${t}" is not something a marker understands, so it was kept verbatim as a class name. Write a class as .my-class (or {.my-class}), an id as #my-id, and anything else as key=value.` + proseEscapeHint(parsed.kind), parsed);
237
240
  }
238
241
  }
239
242
  if (parsed.__extraBareTokens) {
240
243
  const { tokens, named } = parsed.__extraBareTokens;
241
244
  delete parsed.__extraBareTokens;
242
245
  const list = tokens.map((t) => `"${t}"`).join(", ");
243
- warn(state.env, startLine + 1, "extra_bare_marker_token", named ? `@${parsed.kind}: only the first plain word (${list.split(", ")[0]}) is used as the name; the rest (${tokens.slice(1).map((t) => `"${t}"`).join(", ")}) became class names instead. Quote a name that contains spaces ("${tokens.join(" ")}"), or write classes as .${tokens.slice(1).join(" .")}.` : `@${parsed.kind}: ${list} are several plain words, but a marker has only one name slot — so NONE of them was used as the name and they all became class names instead. Quote a name that contains spaces ("${tokens.join(" ")}"), or write classes as .${tokens.join(" .")}.`, parsed);
246
+ warn(state.env, startLine + 1, "extra_bare_marker_token", named ? `@${parsed.kind}: only the first plain word (${list.split(", ")[0]}) is used as the name; the rest (${tokens.slice(1).map((t) => `"${t}"`).join(", ")}) became class names instead. Quote a name that contains spaces ("${tokens.join(" ")}"), or write classes as .${tokens.slice(1).join(" .")}.` : `@${parsed.kind}: ${list} are several plain words, but a marker has only one name slot — so NONE of them was used as the name and they all became class names instead. Quote a name that contains spaces ("${tokens.join(" ")}"), or write classes as .${tokens.join(" .")}.` + proseEscapeHint(parsed.kind), parsed);
244
247
  }
245
248
  state.line = startLine + 1;
246
249
  return true;
@@ -332,7 +335,12 @@ function plugin(md, pluginOptions = {}) {
332
335
  t.meta = { line: meta.__line };
333
336
  addClasses(t, "spread", meta.attrs && meta.attrs.class ? meta.attrs.class : "");
334
337
  attachDataAttrs(t, "spread", meta.name, meta.attrs || {});
335
- stack.open({ kind: "spread", noPagesYet: true, sawAnyPage: false }, t);
338
+ stack.open({
339
+ kind: "spread",
340
+ classes: meta.attrs && meta.attrs.class || "",
341
+ noPagesYet: true,
342
+ sawAnyPage: false
343
+ }, t);
336
344
  }
337
345
  function openPage(meta) {
338
346
  const chapter = stack.get("chapter");
@@ -347,7 +355,7 @@ function plugin(md, pluginOptions = {}) {
347
355
  const label = chapter ? chapter.label : "";
348
356
  if (label)
349
357
  t.attrSet("data-chapter-label", label);
350
- stack.open({ kind: "page" }, t);
358
+ stack.open({ kind: "page", classes: explicit }, t);
351
359
  if (label && !chapter.openerEmitted) {
352
360
  const opener = new state.Token("html_block", "", 0);
353
361
  opener.content = `<div class="chapter-opener" data-chapter-label="${escapeAttr(label)}">${escapeHtml(label)}</div>
@@ -370,13 +378,51 @@ function plugin(md, pluginOptions = {}) {
370
378
  t.meta = { hasColumnBreak: false, line: meta.__line };
371
379
  stack.open({
372
380
  kind: "section",
381
+ classes: meta.attrs && meta.attrs.class || "",
382
+ sawContent: false,
373
383
  meta: { name: meta.name || null, attrs: { ...meta.attrs || {} } },
374
384
  openToken: t
375
385
  }, t);
376
386
  }
387
+ function breakHost() {
388
+ return stack.get("section") || stack.get("page") || stack.get("spread");
389
+ }
390
+ function gridClassOf(frame) {
391
+ if (!frame || !frame.classes)
392
+ return null;
393
+ return frame.classes.split(/\s+/).find((c) => c.startsWith("gp-grid-")) || null;
394
+ }
395
+ function warnBreakInsideGrid(kind, line, meta) {
396
+ const host = breakHost();
397
+ const gridClass = gridClassOf(host);
398
+ if (!gridClass)
399
+ return;
400
+ warn(state.env, line, "break_inside_grid", `@${kind} inside a grid container: the enclosing @${host.kind} carries .${gridClass}, so this break's <div> becomes a grid ITEM — it takes a cell of its own, corrupts the grid's placement, and print and the live preview then disagree about which page the content after it lands on. Move the break outside the grid @${host.kind}, or remove it — grid rows already flow and fragment across pages on their own.`, meta);
401
+ }
402
+ function warnIfEmptyDecoratedSection(closingKind, closingLine) {
403
+ const sec = stack.get("section");
404
+ if (!sec || sec.sawContent)
405
+ return;
406
+ const attrs = sec.meta && sec.meta.attrs || {};
407
+ const decorations = [];
408
+ for (const c of (attrs.class || "").split(/\s+/).filter(Boolean))
409
+ decorations.push(`.${c}`);
410
+ for (const [k, v] of Object.entries(attrs)) {
411
+ if (k === "class")
412
+ continue;
413
+ decorations.push(k === "id" ? `#${v}` : `${k}=${v}`);
414
+ }
415
+ if (!decorations.length)
416
+ return;
417
+ const openLine = sec.openToken && sec.openToken.meta && Number.isFinite(sec.openToken.meta.line) ? sec.openToken.meta.line : closingLine;
418
+ warn(state.env, openLine, "empty_section", `This @section (${decorations.join(" ")}) was closed by the @${closingKind} on line ${closingLine} with no content between the two markers, so its styling applies to an empty element and nothing prints the layout it asked for. Delete one of the two markers, or move the content that belongs inside the section between them.`, null);
419
+ }
377
420
  for (let i = 0;i < state.tokens.length; i++) {
378
421
  const tok = state.tokens[i];
379
422
  if (tok.type !== "layout_marker") {
423
+ const openSectionFrame = stack.get("section");
424
+ if (openSectionFrame)
425
+ openSectionFrame.sawContent = true;
380
426
  out.push(tok);
381
427
  continue;
382
428
  }
@@ -402,6 +448,7 @@ function plugin(md, pluginOptions = {}) {
402
448
  continue;
403
449
  }
404
450
  if (kind === "section") {
451
+ warnIfEmptyDecoratedSection("section", line);
405
452
  stack.close("section");
406
453
  const spread = stack.get("spread");
407
454
  if (spread && spread.noPagesYet) {
@@ -430,6 +477,7 @@ function plugin(md, pluginOptions = {}) {
430
477
  continue;
431
478
  }
432
479
  if (kind === "page-break") {
480
+ warnBreakInsideGrid("page-break", line, meta);
433
481
  const t = new state.Token("layout_page_break", "div", 0);
434
482
  t.meta = { line };
435
483
  t.attrSet("class", "gp-page-break");
@@ -438,6 +486,7 @@ function plugin(md, pluginOptions = {}) {
438
486
  continue;
439
487
  }
440
488
  if (kind === "column-break") {
489
+ warnBreakInsideGrid("column-break", line, meta);
441
490
  const openSectionFrame = stack.get("section");
442
491
  if (openSectionFrame && openSectionFrame.openToken && !openSectionFrame.openToken.meta.hasColumnBreak) {
443
492
  const sectionCls = openSectionFrame.openToken.attrGet("class") || "";
@@ -453,6 +502,7 @@ function plugin(md, pluginOptions = {}) {
453
502
  continue;
454
503
  }
455
504
  if (kind === "end-section") {
505
+ warnIfEmptyDecoratedSection("end-section", line);
456
506
  stack.close("section");
457
507
  continue;
458
508
  }
@@ -540,7 +590,7 @@ body { margin: 0; }
540
590
  .gp-page-break { break-before: page; }
541
591
  .page { break-before: page; }
542
592
  .spread { break-before: page; }
543
- :where(.page, .spread) { position: relative; }
593
+ :where(.page, .spread) { position: relative; display: flow-root; box-sizing: border-box; min-height: calc(var(--gp-content-h, 1px) - 1px); }
544
594
  .gp-column-break { break-after: column; height: 0; font-size: 0; line-height: 0; visibility: hidden; }
545
595
 
546
596
  :where(h1,h2,h3,h4,h5,h6) { break-after: avoid; }
@@ -624,6 +674,26 @@ var GUTTERPRESS_CSS = `
624
674
  .gp-columns-2 { columns: 2; column-gap: var(--gp-column-gap, 1.5em); }
625
675
  .gp-columns-3 { columns: 3; column-gap: var(--gp-column-gap, 1.5em); }
626
676
 
677
+ /* grid runs — the SLOTTED counterpart to the column runs above. Grid places
678
+ each child into the next cell, across then down (deterministic slots: card
679
+ layouts, stat blocks, image-plus-caption pairs); columns FLOW one text run
680
+ down then across. Same neutral-primitive rationale as .gp-columns-*, and
681
+ permanent vocabulary for the same reason: standard CSS Grid verbatim, no
682
+ spec gap to remove later. MEASURED (Chromium 151, gp-grid evidence pack):
683
+ grid rows fragment across sheets with EXACT print/viewer parity — 2- and
684
+ 3-col, unequal item heights, mid-row cuts, multi-sheet overflow,
685
+ break-inside:avoid, gap geometry — so a grid taller than the page is safe,
686
+ no fit-one-page constraint. Two things to know, not fix:
687
+ - on a min-height page root (MARKER_CSS), default align-content
688
+ stretches rows apart to fill the page — identically in both engines.
689
+ Authors wanting packed rows set align-content: start.
690
+ - a @page-break / @column-break marker DIRECTLY inside a grid container
691
+ becomes a grid item and corrupts placement (the one measured parity
692
+ break); markers.js diagnoses it (break_inside_grid).
693
+ --gp-grid-gap is author-settable. */
694
+ .gp-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gp-grid-gap, 1.5em); }
695
+ .gp-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gp-grid-gap, 1.5em); }
696
+
627
697
  /* shape wrap — text follows the image's alpha silhouette instead of its
628
698
  rectangular box. shape-outside only applies to floats, so this is inert
629
699
  without .gp-left/.gp-right (and under .gp-pin, which un-floats). The
@@ -686,8 +756,14 @@ img.gp-shape {
686
756
  opacity, filter, transform on it traps the negative layer inside).
687
757
  Core keeps .page/.spread at 'position: relative; z-index: auto'
688
758
  precisely so they are not stacking contexts.
689
- - a clipping ancestor (overflow other than visible), the same mechanism
690
- that clips a .gp-bleed plate back to the wrapper's width.
759
+ - a clipping ancestor (overflow other than visible) but only where
760
+ the art actually overhangs that ancestor's clip box on a clipped
761
+ axis: the overhang is cut off, the same mechanism that clips a
762
+ .gp-bleed plate back to the wrapper's width. Clipping never reorders
763
+ layers — within-bounds art under a clipping .page prints whole and
764
+ still behind (measured; see the build audit's comment in
765
+ engine/compiler/build.ts), and a static wrapper's overflow never
766
+ binds an abspos .gp-pin at all.
691
767
  The build-time engine.layer.trapped audit reports both against the live
692
768
  ancestor chain. printsafe/page-containment is only an early source hint for
693
769
  declarations written directly on .page/.spread. */
@@ -5,17 +5,17 @@ import {
5
5
  isUnbornRepo,
6
6
  repairRepo,
7
7
  verifyRepoReadable
8
- } from "./cli-n25qycwz.js";
8
+ } from "./cli-eq5naw4m.js";
9
9
  import {
10
10
  FileTokenStore,
11
11
  defaultConfigDir
12
- } from "./cli-ra0ed2xt.js";
12
+ } from "./cli-cqtggsng.js";
13
13
  import {
14
14
  UsageError,
15
15
  rejectExtraPositionals,
16
16
  rejectUnknownFlags
17
- } from "./cli-0r0tq16s.js";
18
- import"./cli-hp9r2pzt.js";
17
+ } from "./cli-149edp6b.js";
18
+ import"./cli-zfcryxg8.js";
19
19
  import"./cli-c41yr7he.js";
20
20
  import"./cli-46ycxe6r.js";
21
21
  import"./cli-k1065rkg.js";
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  executeAndReport
3
- } from "./cli-n25qycwz.js";
3
+ } from "./cli-eq5naw4m.js";
4
4
  import {
5
5
  log
6
- } from "./cli-ra0ed2xt.js";
6
+ } from "./cli-cqtggsng.js";
7
7
  import {
8
8
  UsageError,
9
9
  rejectExtraPositionals,
10
10
  rejectUnknownFlags
11
- } from "./cli-0r0tq16s.js";
12
- import"./cli-hp9r2pzt.js";
11
+ } from "./cli-149edp6b.js";
12
+ import"./cli-zfcryxg8.js";
13
13
  import"./cli-c41yr7he.js";
14
14
  import {
15
15
  EXIT_CODES
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gutterpress",
3
- "version": "0.10.0-alpha.4",
3
+ "version": "0.10.0",
4
4
  "description": "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
5
5
  "author": "itlackey",
6
6
  "license": "MPL-2.0",