gutterpress 0.10.5 → 0.10.6-beta.1

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 (28) hide show
  1. package/dist/api/index.js +1 -1
  2. package/dist/{audit-ax5ps29k.js → audit-j57qptj6.js} +4 -4
  3. package/dist/{build-79dv44cn.js → build-1dkfbqrn.js} +4 -4
  4. package/dist/{cli-chkwtg9m.js → cli-55rdak96.js} +341 -192
  5. package/dist/{cli-5pq315qe.js → cli-622k3vky.js} +1 -1
  6. package/dist/{cli-v031r2h6.js → cli-969n6fcc.js} +1 -1
  7. package/dist/{cli-0t4zfevc.js → cli-bwbs3qm4.js} +1 -1
  8. package/dist/cli.js +14 -14
  9. package/dist/{doctor-qejmbv32.js → doctor-k7p66yss.js} +2 -2
  10. package/dist/{engine-mtyjc4v6.js → engine-4ctgbwj5.js} +1 -1
  11. package/dist/{engine-fwe8djyn.js → engine-bhhcne06.js} +1 -1
  12. package/dist/{index-ya1yw5me.js → index-aqxnmfzx.js} +160 -11
  13. package/dist/{index-ja0p4w5f.js → index-eyzv51dw.js} +1 -1
  14. package/dist/index.js +2 -2
  15. package/dist/lib/markdown/assemble.d.ts +3 -1
  16. package/dist/lib/markdown/gutterpress-css.d.ts +31 -5
  17. package/dist/lib/markdown/markers.d.ts +79 -3
  18. package/dist/lib/pdf-inspect.d.ts +33 -5
  19. package/dist/lib/render-parity.d.ts +108 -0
  20. package/dist/{lint-wweg6pf0.js → lint-pb625yxd.js} +4 -4
  21. package/dist/{new-yf8emyp3.js → new-e84j6fe3.js} +4 -4
  22. package/dist/{plugin-dj6crat4.js → plugin-kv51tq0m.js} +4 -4
  23. package/dist/{preflight-d34pzgr6.js → preflight-w8yrpa3r.js} +4 -4
  24. package/dist/{preview-g2f5982y.js → preview-v5wpgdwj.js} +4 -4
  25. package/dist/{publish-6vt6yqm4.js → publish-5b77kasn.js} +4 -4
  26. package/dist/render.js +146 -6
  27. package/dist/{validate-vjwgxswp.js → validate-vrhnbazt.js} +4 -4
  28. package/package.json +1 -1
package/dist/api/index.js CHANGED
@@ -106,7 +106,7 @@ import {
106
106
  syncProject,
107
107
  testRemoteAccess,
108
108
  validateProjectPlugins
109
- } from "../index-ya1yw5me.js";
109
+ } from "../index-aqxnmfzx.js";
110
110
  import {
111
111
  BuildError,
112
112
  DEFAULT_PRINT_OPTS,
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  executeAndReport
3
- } from "./cli-chkwtg9m.js";
3
+ } from "./cli-55rdak96.js";
4
4
  import {
5
5
  log
6
- } from "./cli-5pq315qe.js";
6
+ } from "./cli-622k3vky.js";
7
7
  import {
8
8
  UsageError,
9
9
  rejectExtraPositionals,
10
10
  rejectUnknownFlags
11
- } from "./cli-v031r2h6.js";
12
- import"./cli-0t4zfevc.js";
11
+ } from "./cli-969n6fcc.js";
12
+ import"./cli-bwbs3qm4.js";
13
13
  import"./cli-c41yr7he.js";
14
14
  import {
15
15
  EXIT_CODES
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  runBuild,
3
3
  splitOutPath
4
- } from "./cli-chkwtg9m.js";
4
+ } from "./cli-55rdak96.js";
5
5
  import {
6
6
  log
7
- } from "./cli-5pq315qe.js";
7
+ } from "./cli-622k3vky.js";
8
8
  import {
9
9
  UsageError,
10
10
  parseEngine,
@@ -12,8 +12,8 @@ import {
12
12
  parsePdfxFlavor,
13
13
  rejectExtraPositionals,
14
14
  rejectUnknownFlags
15
- } from "./cli-v031r2h6.js";
16
- import"./cli-0t4zfevc.js";
15
+ } from "./cli-969n6fcc.js";
16
+ import"./cli-bwbs3qm4.js";
17
17
  import"./cli-c41yr7he.js";
18
18
  import {
19
19
  BuildError
@@ -13,12 +13,12 @@ import {
13
13
  stampCreator,
14
14
  stripAnnotations,
15
15
  warn
16
- } from "./cli-5pq315qe.js";
16
+ } from "./cli-622k3vky.js";
17
17
  import {
18
18
  DEBOUNCE,
19
19
  UsageError,
20
20
  resolvePort
21
- } from "./cli-v031r2h6.js";
21
+ } from "./cli-969n6fcc.js";
22
22
  import {
23
23
  MARGIN_BOX_IGNORED_PROPERTIES,
24
24
  RENDER_TIMEOUT_MS,
@@ -27,7 +27,7 @@ import {
27
27
  getAssetPath,
28
28
  getBrowser,
29
29
  prewarmBrowser
30
- } from "./cli-0t4zfevc.js";
30
+ } from "./cli-bwbs3qm4.js";
31
31
  import {
32
32
  INSTALL_HINTS,
33
33
  execCapture,
@@ -1671,26 +1671,33 @@ function getTextPass(doc) {
1671
1671
  p = (async () => {
1672
1672
  const textByPage = [];
1673
1673
  const positions = [];
1674
+ const runsByPage = [];
1674
1675
  for (let i = 1;i <= doc.numPages; i++) {
1675
1676
  try {
1676
1677
  const page = await doc.getPage(i);
1677
1678
  const tc = await page.getTextContent();
1678
1679
  let pageText = "";
1680
+ const runs = [];
1679
1681
  for (const item of tc.items) {
1680
1682
  const it = item;
1681
1683
  if (typeof it.str !== "string")
1682
1684
  continue;
1683
1685
  pageText += it.str;
1684
1686
  if (it.transform && it.str.trim().length > 0) {
1685
- positions.push({ x: it.transform[4], y: it.transform[5] });
1687
+ const x = it.transform[4];
1688
+ const y = it.transform[5];
1689
+ positions.push({ x, y });
1690
+ runs.push({ s: it.str, x, y, w: it.width ?? 0, h: it.height ?? 0 });
1686
1691
  }
1687
1692
  }
1688
1693
  textByPage.push(pageText);
1694
+ runsByPage.push(runs);
1689
1695
  } catch {
1690
1696
  textByPage.push("");
1697
+ runsByPage.push([]);
1691
1698
  }
1692
1699
  }
1693
- return { textByPage, positions };
1700
+ return { textByPage, positions, runsByPage };
1694
1701
  })();
1695
1702
  textCache.set(doc, p);
1696
1703
  }
@@ -1754,6 +1761,8 @@ function getOpPass(doc) {
1754
1761
  name,
1755
1762
  placedW: Math.hypot(ctm[0], ctm[1]),
1756
1763
  placedH: Math.hypot(ctm[2], ctm[3]),
1764
+ x: ctm[4],
1765
+ y: ctm[5],
1757
1766
  page: i
1758
1767
  });
1759
1768
  } else if (fn === OPS.setFont) {
@@ -3707,22 +3716,329 @@ body { margin: 0; }
3707
3716
  :where(h1,h2,h3,h4,h5,h6) { break-after: avoid; }
3708
3717
  :where(img, svg, video) { max-width: 100%; }
3709
3718
  :where(p > img:only-child, figure > img) { width: fit-content; max-width: 100%; height: auto; vertical-align: bottom; }
3719
+ :where(p) > :where(img:not([class])) { max-height: calc(var(--gp-content-h) - 4px); object-fit: contain; }
3720
+ :where(figure) { break-inside: avoid; }
3710
3721
  :where(.section, figure) > :where(:first-child) { break-before: avoid; }
3711
3722
 
3712
3723
  `;
3713
3724
 
3725
+ // src/lib/markdown/gutterpress-css.ts
3726
+ var GUTTERPRESS_CSS = `
3727
+ /* gp-* author image/block vocabulary. One vocabulary, gp-* only — the
3728
+ pre-vocabulary utility names (.center/.float-left/.float-right/
3729
+ .full-width/.full-bleed) were REMOVED when gp-* shipped; books rename
3730
+ the classes in their markdown (see the migration note). Source ORDER is
3731
+ the contract — see the doctrine comment above. */
3732
+
3733
+ /* flow positions */
3734
+ .gp-left {
3735
+ float: left;
3736
+ margin: 0 var(--gp-gap, 1em) var(--gp-gap, 1em) 0;
3737
+ max-width: 50%;
3738
+ }
3739
+ .gp-right {
3740
+ float: right;
3741
+ margin: 0 0 var(--gp-gap, 1em) var(--gp-gap, 1em);
3742
+ max-width: 50%;
3743
+ }
3744
+ .gp-center {
3745
+ display: block;
3746
+ float: none;
3747
+ margin-left: auto;
3748
+ margin-right: auto;
3749
+ max-width: 100%;
3750
+ }
3751
+ .gp-full {
3752
+ display: block;
3753
+ float: none;
3754
+ width: 100%;
3755
+ max-width: 100%;
3756
+ }
3757
+ @page gp-full-bleed { margin-left: 0; margin-right: 0; }
3758
+ .gp-bleed {
3759
+ display: block;
3760
+ float: none;
3761
+ break-before: page;
3762
+ page: gp-full-bleed;
3763
+ max-width: none;
3764
+ width: 100%;
3765
+ margin-left: 0;
3766
+ margin-right: 0;
3767
+ }
3768
+
3769
+ /* sizes — AFTER the flow positions so max-width:100% lifts the floats' 50%
3770
+ cap at equal specificity */
3771
+ .gp-small { width: 25%; max-width: 100%; }
3772
+ .gp-medium { width: 50%; max-width: 100%; }
3773
+ .gp-large { width: 75%; max-width: 100%; }
3774
+
3775
+ /* float clearance presets — consumed by var(--gp-gap) in the float rules
3776
+ above and by .gp-shape's shape-margin below; --gp-gap itself is
3777
+ author-settable CSS */
3778
+ .gp-tight { --gp-gap: 0.5em; }
3779
+ .gp-loose { --gp-gap: 2em; }
3780
+
3781
+ /* column runs — plain CSS Multi-column, exposed as author vocabulary so
3782
+ "put this in two columns" does not require borrowing a styled container
3783
+ from the book's own component layer. That borrowing is what this exists
3784
+ to prevent: a book whose theme paints .section chrome by default gives
3785
+ every author who opens a section just to start a column run a panel they
3786
+ did not ask for, and the book then needs a reset rule to take it back.
3787
+ With a neutral primitive the author opts into columns and nothing else.
3788
+
3789
+ Permanent vocabulary, not a shim: Chromium implements multicol natively
3790
+ and these rules are the standard properties verbatim, so there is no
3791
+ spec gap here to remove later. Deliberately minimal — column-fill is
3792
+ NOT set, because the correct value depends on whether the run fragments
3793
+ across pages (auto packs each page's columns; the CSS initial balance is
3794
+ right for a run that fits on one page) and only the author knows which.
3795
+ --gp-column-gap is author-settable. */
3796
+ .gp-columns-2 { columns: 2; column-gap: var(--gp-column-gap, 1.5em); }
3797
+ .gp-columns-3 { columns: 3; column-gap: var(--gp-column-gap, 1.5em); }
3798
+
3799
+ /* the per-shape decisions the paragraph above deliberately leaves to the
3800
+ author, named instead of left as raw CSS every book was reinventing
3801
+ (2026-09-01 CSS architecture review, findings C1/C7 — CLAUDE.md §0:
3802
+ "behavior broadly useful to non-technical authors belongs in core").
3803
+ Permanent vocabulary, standard properties verbatim — same rationale as
3804
+ the column/grid runs above. One name each, no aliases:
3805
+ .gp-columns-all column-span: all a heading or block that
3806
+ spans every column in the
3807
+ run it sits inside.
3808
+ .gp-columns-flow column-fill: auto a run that FRAGMENTS across
3809
+ pages — every page's
3810
+ columns fill instead of
3811
+ only the last one
3812
+ balancing (the dead-column
3813
+ collapse the build's
3814
+ engine.multicol.dead-column
3815
+ warning names this fix
3816
+ for).
3817
+ .gp-columns-balanced column-fill: balance a run that fits on ONE
3818
+ page (the CSS initial
3819
+ value — ragged columns
3820
+ would be wrong here). */
3821
+ .gp-columns-all { column-span: all; }
3822
+ .gp-columns-flow { column-fill: auto; }
3823
+ .gp-columns-balanced { column-fill: balance; }
3824
+
3825
+ /* grid runs — the SLOTTED counterpart to the column runs above. Grid places
3826
+ each child into the next cell, across then down (deterministic slots: card
3827
+ layouts, stat blocks, image-plus-caption pairs); columns FLOW one text run
3828
+ down then across. Same neutral-primitive rationale as .gp-columns-*, and
3829
+ permanent vocabulary for the same reason: standard CSS Grid verbatim, no
3830
+ spec gap to remove later. MEASURED (Chromium 151, gp-grid evidence pack):
3831
+ grid rows fragment across sheets with EXACT print/viewer parity — 2- and
3832
+ 3-col, unequal item heights, mid-row cuts, multi-sheet overflow,
3833
+ break-inside:avoid, gap geometry — so a grid taller than the page is safe,
3834
+ no fit-one-page constraint. Two things to know, not fix:
3835
+ - on a min-height page root (MARKER_CSS), default align-content
3836
+ stretches rows apart to fill the page — identically in both engines.
3837
+ Authors wanting packed rows set align-content: start.
3838
+ - a @page-break / @column-break marker DIRECTLY inside a grid container
3839
+ becomes a grid item and corrupts placement (the one measured parity
3840
+ break); markers.js diagnoses it (break_inside_grid).
3841
+ --gp-grid-gap is author-settable. */
3842
+ .gp-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gp-grid-gap, 1.5em); }
3843
+ .gp-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gp-grid-gap, 1.5em); }
3844
+
3845
+ /* fragmentation controls — generic pagination utilities for any element,
3846
+ independent of the column/grid vocabulary above (a book reached for
3847
+ these just as often outside a multicol run: keeping a card whole,
3848
+ forcing a section to start a fresh page). Standard properties only —
3849
+ this project is Chromium-only (CLAUDE.md), so there are no legacy
3850
+ page-break-* twins to also emit. One name each. */
3851
+ .gp-no-break { break-inside: avoid; }
3852
+ .gp-break-before { break-before: page; }
3853
+
3854
+ /* shape wrap — text follows the image's alpha silhouette instead of its
3855
+ rectangular box. shape-outside only applies to floats, so this is inert
3856
+ without .gp-left/.gp-right (and under .gp-pin, which un-floats). The
3857
+ shape URL cannot be written in CSS (url() contexts can't read attr()),
3858
+ so the image renderer rule (images.ts) mirrors the src into an inline
3859
+ --gp-shape:url(...) custom property whenever it sees this class --
3860
+ authors only ever type the class. threshold 0.2 ignores near-transparent
3861
+ anti-aliasing halos; shape-margin shares the float-gap vocabulary. */
3862
+ img.gp-shape {
3863
+ shape-outside: var(--gp-shape);
3864
+ shape-image-threshold: 0.2;
3865
+ shape-margin: var(--gp-gap, 1em);
3866
+ }
3867
+
3868
+ /* pin — within the nearest positioned ancestor (.page/.spread, rule above).
3869
+ inset:0 and the explicit centers are load-bearing; see doctrine comment. */
3870
+ .gp-pin {
3871
+ position: absolute;
3872
+ inset: 0;
3873
+ align-self: center;
3874
+ justify-self: center;
3875
+ margin: 0;
3876
+ max-width: 100%;
3877
+ }
3878
+
3879
+ /* pin edge modifiers — AFTER .gp-pin to beat its center defaults;
3880
+ justify-self is inert on in-flow floats, so gp-left/gp-right safely do
3881
+ double duty as flow float + pin edge */
3882
+ .gp-top { align-self: start; }
3883
+ .gp-bottom { align-self: end; }
3884
+ .gp-left { justify-self: start; }
3885
+ .gp-right { justify-self: end; }
3886
+
3887
+ /* wrapper-margin neutralization (same pattern and rationale as the
3888
+ .gp-bleed paragraph-margin note in the doctrine comment; for pin, the
3889
+ emptied paragraph would otherwise leave a phantom margin gap in flow) */
3890
+ :where(p:has(> img.gp-bleed:only-child)) { margin: 0; }
3891
+ :where(p:has(> img.gp-pin:only-child)) { margin: 0; }
3892
+
3893
+ /* depth — a named ladder for z-index, so books stop hand-tuning bare
3894
+ integers. A real book measured 21 z-index declarations using only four
3895
+ distinct values (-1, 0, 1, 2), each written literally at its use site.
3896
+ The custom properties are the author-settable surface (a book needing a
3897
+ deeper stack raises them once); the classes are the shorthand.
3898
+
3899
+ NOT named "layer": CSS Paged Media 3 §3.1 already defines "page layers"
3900
+ (page background, canvas, borders, contents, margin boxes) and those are
3901
+ parts of the PAGE BOX, not a z-ladder for content. Reusing the word for a
3902
+ different concept would collide with the spec vocabulary this project
3903
+ tracks. The pin EDGE modifiers already own .gp-top/.gp-bottom, so the
3904
+ ladder avoids those words too.
3905
+
3906
+ .gp-behind is the one that earns its place: it puts a pinned image UNDER
3907
+ the page's text, which is otherwise impossible to express without a bare
3908
+ negative z-index. "Above" needs no class — an out-of-flow pin already
3909
+ paints above in-flow content.
3910
+
3911
+ Two things silently defeat .gp-behind, neither visible at the use site:
3912
+ - a stacking context on the .page/.spread ancestor (z-index, isolation,
3913
+ opacity, filter, transform on it traps the negative layer inside).
3914
+ Core keeps .page/.spread at 'position: relative; z-index: auto'
3915
+ precisely so they are not stacking contexts.
3916
+ - a clipping ancestor (overflow other than visible) — but only where
3917
+ the art actually overhangs that ancestor's clip box on a clipped
3918
+ axis: the overhang is cut off, the same mechanism that clips a
3919
+ .gp-bleed plate back to the wrapper's width. Clipping never reorders
3920
+ layers — within-bounds art under a clipping .page prints whole and
3921
+ still behind (measured; see the build audit's comment in
3922
+ engine/compiler/build.ts), and a static wrapper's overflow never
3923
+ binds an abspos .gp-pin at all.
3924
+ The build-time engine.layer.trapped audit reports both against the live
3925
+ ancestor chain. printsafe/page-containment is only an early source hint for
3926
+ declarations written directly on .page/.spread. */
3927
+ :root {
3928
+ --gp-z-behind: -1;
3929
+ --gp-z-base: 0;
3930
+ --gp-z-raised: 1;
3931
+ --gp-z-front: 2;
3932
+ }
3933
+ .gp-behind { z-index: var(--gp-z-behind); }
3934
+ .gp-base { z-index: var(--gp-z-base); }
3935
+ .gp-raised { z-index: var(--gp-z-raised); }
3936
+ .gp-front { z-index: var(--gp-z-front); }
3937
+ `;
3938
+ var GP_CLASSES = new Set([
3939
+ "gp-left",
3940
+ "gp-right",
3941
+ "gp-center",
3942
+ "gp-full",
3943
+ "gp-bleed",
3944
+ "gp-small",
3945
+ "gp-medium",
3946
+ "gp-large",
3947
+ "gp-tight",
3948
+ "gp-loose",
3949
+ "gp-columns-2",
3950
+ "gp-columns-3",
3951
+ "gp-columns-all",
3952
+ "gp-columns-flow",
3953
+ "gp-columns-balanced",
3954
+ "gp-no-break",
3955
+ "gp-break-before",
3956
+ "gp-grid-2",
3957
+ "gp-grid-3",
3958
+ "gp-shape",
3959
+ "gp-pin",
3960
+ "gp-top",
3961
+ "gp-bottom",
3962
+ "gp-behind",
3963
+ "gp-base",
3964
+ "gp-raised",
3965
+ "gp-front",
3966
+ "gp-page-break",
3967
+ "gp-column-break",
3968
+ "gp-continued",
3969
+ "gp-flush"
3970
+ ]);
3971
+
3714
3972
  // src/lib/markdown/gp-pin-scope.js
3715
3973
  function warn3(env, line, type, message, marker) {
3716
3974
  if (!env.layoutWarnings)
3717
3975
  env.layoutWarnings = [];
3718
3976
  env.layoutWarnings.push({ line, type, message, marker });
3719
3977
  }
3978
+ function editDistance2(a, b) {
3979
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
3980
+ for (let i = 1;i <= a.length; i++) {
3981
+ const cur = [i];
3982
+ for (let j = 1;j <= b.length; j++) {
3983
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
3984
+ }
3985
+ prev = cur;
3986
+ }
3987
+ return prev[b.length];
3988
+ }
3989
+ function nearestGpClass(cls) {
3990
+ const threshold = Math.max(2, Math.floor(cls.length / 3));
3991
+ let best = null;
3992
+ let bestDistance = Infinity;
3993
+ for (const known of GP_CLASSES) {
3994
+ const d = editDistance2(cls, known);
3995
+ if (d < bestDistance) {
3996
+ bestDistance = d;
3997
+ best = known;
3998
+ }
3999
+ }
4000
+ return best !== null && bestDistance > 0 && bestDistance <= threshold ? best : null;
4001
+ }
4002
+ function hasPinClass(token) {
4003
+ const cls = token.attrGet && token.attrGet("class");
4004
+ return typeof cls === "string" && cls.split(/\s+/).includes("gp-pin");
4005
+ }
4006
+ function elementNameFor(token) {
4007
+ switch (token.type) {
4008
+ case "layout_chapter_open":
4009
+ return "@chapter";
4010
+ case "layout_spread_open":
4011
+ return "@spread";
4012
+ case "layout_page_open":
4013
+ return "@page";
4014
+ case "layout_section_open":
4015
+ return "@section";
4016
+ case "image":
4017
+ return "an image";
4018
+ case "heading_open":
4019
+ return "a heading";
4020
+ case "paragraph_open":
4021
+ return "a paragraph";
4022
+ case "link_open":
4023
+ return "a link";
4024
+ default:
4025
+ return token.tag ? `a <${token.tag}>` : "an element";
4026
+ }
4027
+ }
4028
+ function checkUnknownGpClasses(token, env, line) {
4029
+ const cls = token.attrGet && token.attrGet("class");
4030
+ if (typeof cls !== "string" || !cls)
4031
+ return;
4032
+ for (const c of cls.split(/\s+/)) {
4033
+ if (!c.startsWith("gp-") || GP_CLASSES.has(c))
4034
+ continue;
4035
+ const suggestion = nearestGpClass(c);
4036
+ const base = `Unknown class "${c}" on ${elementNameFor(token)}.`;
4037
+ warn3(env, line, "unknown_gp_class", suggestion ? `${base} Did you mean "${suggestion}"?` : base);
4038
+ }
4039
+ }
3720
4040
  function gpPinScope(md) {
3721
4041
  const PIN_OUTSIDE_PAGE_MSG = "A .gp-pin element is not inside any @page or @spread, so it is pinned to the whole document instead of the page it sits on — it can print on a completely different sheet. Move it inside an @page or @spread block, or remove .gp-pin.";
3722
- function hasPinClass(token) {
3723
- const cls = token.attrGet && token.attrGet("class");
3724
- return typeof cls === "string" && cls.split(/\s+/).includes("gp-pin");
3725
- }
3726
4042
  md.core.ruler.push("gp_pin_scope_check", function(state) {
3727
4043
  let depth = 0;
3728
4044
  for (const token of state.tokens) {
@@ -3741,6 +4057,13 @@ function gpPinScope(md) {
3741
4057
  warn3(state.env, (token.map?.[0] ?? 0) + 1, "pin_outside_page", PIN_OUTSIDE_PAGE_MSG);
3742
4058
  }
3743
4059
  }
4060
+ const line = (token.map?.[0] ?? 0) + 1;
4061
+ checkUnknownGpClasses(token, state.env, line);
4062
+ if (token.type === "inline" && token.children) {
4063
+ for (const child of token.children) {
4064
+ checkUnknownGpClasses(child, state.env, line);
4065
+ }
4066
+ }
3744
4067
  }
3745
4068
  });
3746
4069
  }
@@ -5115,7 +5438,7 @@ import { readFile as readFile14 } from "node:fs/promises";
5115
5438
  var check22 = {
5116
5439
  id: "source.markdown.layout-markers",
5117
5440
  name: "Layout Markers",
5118
- description: "Reports @page/@section/@chapter marker arguments Gutterpress could not understand",
5441
+ description: "Reports @page/@section/@chapter marker arguments Gutterpress could not understand, plus any unknown gp-* class",
5119
5442
  category: "source",
5120
5443
  phase: "pre-build",
5121
5444
  async run(ctx) {
@@ -6063,185 +6386,6 @@ function canonicalChapterId(p) {
6063
6386
  return s;
6064
6387
  }
6065
6388
 
6066
- // src/lib/markdown/gutterpress-css.ts
6067
- var GUTTERPRESS_CSS = `
6068
- /* gp-* author image/block vocabulary. One vocabulary, gp-* only — the
6069
- pre-vocabulary utility names (.center/.float-left/.float-right/
6070
- .full-width/.full-bleed) were REMOVED when gp-* shipped; books rename
6071
- the classes in their markdown (see the migration note). Source ORDER is
6072
- the contract — see the doctrine comment above. */
6073
-
6074
- /* flow positions */
6075
- .gp-left {
6076
- float: left;
6077
- margin: 0 var(--gp-gap, 1em) var(--gp-gap, 1em) 0;
6078
- max-width: 50%;
6079
- }
6080
- .gp-right {
6081
- float: right;
6082
- margin: 0 0 var(--gp-gap, 1em) var(--gp-gap, 1em);
6083
- max-width: 50%;
6084
- }
6085
- .gp-center {
6086
- display: block;
6087
- float: none;
6088
- margin-left: auto;
6089
- margin-right: auto;
6090
- max-width: 100%;
6091
- }
6092
- .gp-full {
6093
- display: block;
6094
- float: none;
6095
- width: 100%;
6096
- max-width: 100%;
6097
- }
6098
- @page gp-full-bleed { margin-left: 0; margin-right: 0; }
6099
- .gp-bleed {
6100
- display: block;
6101
- float: none;
6102
- break-before: page;
6103
- page: gp-full-bleed;
6104
- max-width: none;
6105
- width: 100%;
6106
- margin-left: 0;
6107
- margin-right: 0;
6108
- }
6109
-
6110
- /* sizes — AFTER the flow positions so max-width:100% lifts the floats' 50%
6111
- cap at equal specificity */
6112
- .gp-small { width: 25%; max-width: 100%; }
6113
- .gp-medium { width: 50%; max-width: 100%; }
6114
- .gp-large { width: 75%; max-width: 100%; }
6115
-
6116
- /* float clearance presets — consumed by var(--gp-gap) in the float rules
6117
- above and by .gp-shape's shape-margin below; --gp-gap itself is
6118
- author-settable CSS */
6119
- .gp-tight { --gp-gap: 0.5em; }
6120
- .gp-loose { --gp-gap: 2em; }
6121
-
6122
- /* column runs — plain CSS Multi-column, exposed as author vocabulary so
6123
- "put this in two columns" does not require borrowing a styled container
6124
- from the book's own component layer. That borrowing is what this exists
6125
- to prevent: a book whose theme paints .section chrome by default gives
6126
- every author who opens a section just to start a column run a panel they
6127
- did not ask for, and the book then needs a reset rule to take it back.
6128
- With a neutral primitive the author opts into columns and nothing else.
6129
-
6130
- Permanent vocabulary, not a shim: Chromium implements multicol natively
6131
- and these rules are the standard properties verbatim, so there is no
6132
- spec gap here to remove later. Deliberately minimal — column-fill is
6133
- NOT set, because the correct value depends on whether the run fragments
6134
- across pages (auto packs each page's columns; the CSS initial balance is
6135
- right for a run that fits on one page) and only the author knows which.
6136
- --gp-column-gap is author-settable. */
6137
- .gp-columns-2 { columns: 2; column-gap: var(--gp-column-gap, 1.5em); }
6138
- .gp-columns-3 { columns: 3; column-gap: var(--gp-column-gap, 1.5em); }
6139
-
6140
- /* grid runs — the SLOTTED counterpart to the column runs above. Grid places
6141
- each child into the next cell, across then down (deterministic slots: card
6142
- layouts, stat blocks, image-plus-caption pairs); columns FLOW one text run
6143
- down then across. Same neutral-primitive rationale as .gp-columns-*, and
6144
- permanent vocabulary for the same reason: standard CSS Grid verbatim, no
6145
- spec gap to remove later. MEASURED (Chromium 151, gp-grid evidence pack):
6146
- grid rows fragment across sheets with EXACT print/viewer parity — 2- and
6147
- 3-col, unequal item heights, mid-row cuts, multi-sheet overflow,
6148
- break-inside:avoid, gap geometry — so a grid taller than the page is safe,
6149
- no fit-one-page constraint. Two things to know, not fix:
6150
- - on a min-height page root (MARKER_CSS), default align-content
6151
- stretches rows apart to fill the page — identically in both engines.
6152
- Authors wanting packed rows set align-content: start.
6153
- - a @page-break / @column-break marker DIRECTLY inside a grid container
6154
- becomes a grid item and corrupts placement (the one measured parity
6155
- break); markers.js diagnoses it (break_inside_grid).
6156
- --gp-grid-gap is author-settable. */
6157
- .gp-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gp-grid-gap, 1.5em); }
6158
- .gp-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gp-grid-gap, 1.5em); }
6159
-
6160
- /* shape wrap — text follows the image's alpha silhouette instead of its
6161
- rectangular box. shape-outside only applies to floats, so this is inert
6162
- without .gp-left/.gp-right (and under .gp-pin, which un-floats). The
6163
- shape URL cannot be written in CSS (url() contexts can't read attr()),
6164
- so the image renderer rule (images.ts) mirrors the src into an inline
6165
- --gp-shape:url(...) custom property whenever it sees this class --
6166
- authors only ever type the class. threshold 0.2 ignores near-transparent
6167
- anti-aliasing halos; shape-margin shares the float-gap vocabulary. */
6168
- img.gp-shape {
6169
- shape-outside: var(--gp-shape);
6170
- shape-image-threshold: 0.2;
6171
- shape-margin: var(--gp-gap, 1em);
6172
- }
6173
-
6174
- /* pin — within the nearest positioned ancestor (.page/.spread, rule above).
6175
- inset:0 and the explicit centers are load-bearing; see doctrine comment. */
6176
- .gp-pin {
6177
- position: absolute;
6178
- inset: 0;
6179
- align-self: center;
6180
- justify-self: center;
6181
- margin: 0;
6182
- max-width: 100%;
6183
- }
6184
-
6185
- /* pin edge modifiers — AFTER .gp-pin to beat its center defaults;
6186
- justify-self is inert on in-flow floats, so gp-left/gp-right safely do
6187
- double duty as flow float + pin edge */
6188
- .gp-top { align-self: start; }
6189
- .gp-bottom { align-self: end; }
6190
- .gp-left { justify-self: start; }
6191
- .gp-right { justify-self: end; }
6192
-
6193
- /* wrapper-margin neutralization (same pattern and rationale as the
6194
- .gp-bleed paragraph-margin note in the doctrine comment; for pin, the
6195
- emptied paragraph would otherwise leave a phantom margin gap in flow) */
6196
- :where(p:has(> img.gp-bleed:only-child)) { margin: 0; }
6197
- :where(p:has(> img.gp-pin:only-child)) { margin: 0; }
6198
-
6199
- /* depth — a named ladder for z-index, so books stop hand-tuning bare
6200
- integers. A real book measured 21 z-index declarations using only four
6201
- distinct values (-1, 0, 1, 2), each written literally at its use site.
6202
- The custom properties are the author-settable surface (a book needing a
6203
- deeper stack raises them once); the classes are the shorthand.
6204
-
6205
- NOT named "layer": CSS Paged Media 3 §3.1 already defines "page layers"
6206
- (page background, canvas, borders, contents, margin boxes) and those are
6207
- parts of the PAGE BOX, not a z-ladder for content. Reusing the word for a
6208
- different concept would collide with the spec vocabulary this project
6209
- tracks. The pin EDGE modifiers already own .gp-top/.gp-bottom, so the
6210
- ladder avoids those words too.
6211
-
6212
- .gp-behind is the one that earns its place: it puts a pinned image UNDER
6213
- the page's text, which is otherwise impossible to express without a bare
6214
- negative z-index. "Above" needs no class — an out-of-flow pin already
6215
- paints above in-flow content.
6216
-
6217
- Two things silently defeat .gp-behind, neither visible at the use site:
6218
- - a stacking context on the .page/.spread ancestor (z-index, isolation,
6219
- opacity, filter, transform on it traps the negative layer inside).
6220
- Core keeps .page/.spread at 'position: relative; z-index: auto'
6221
- precisely so they are not stacking contexts.
6222
- - a clipping ancestor (overflow other than visible) — but only where
6223
- the art actually overhangs that ancestor's clip box on a clipped
6224
- axis: the overhang is cut off, the same mechanism that clips a
6225
- .gp-bleed plate back to the wrapper's width. Clipping never reorders
6226
- layers — within-bounds art under a clipping .page prints whole and
6227
- still behind (measured; see the build audit's comment in
6228
- engine/compiler/build.ts), and a static wrapper's overflow never
6229
- binds an abspos .gp-pin at all.
6230
- The build-time engine.layer.trapped audit reports both against the live
6231
- ancestor chain. printsafe/page-containment is only an early source hint for
6232
- declarations written directly on .page/.spread. */
6233
- :root {
6234
- --gp-z-behind: -1;
6235
- --gp-z-base: 0;
6236
- --gp-z-raised: 1;
6237
- --gp-z-front: 2;
6238
- }
6239
- .gp-behind { z-index: var(--gp-z-behind); }
6240
- .gp-base { z-index: var(--gp-z-base); }
6241
- .gp-raised { z-index: var(--gp-z-raised); }
6242
- .gp-front { z-index: var(--gp-z-front); }
6243
- `;
6244
-
6245
6389
  // src/lib/markdown/assemble.ts
6246
6390
  async function assembleBookHtml(opts) {
6247
6391
  const title = opts.title ?? "Document";
@@ -6289,10 +6433,15 @@ ${rendered}
6289
6433
  opts.onImageRefs([...imageRefs]);
6290
6434
  }
6291
6435
  const inlineCss = [
6436
+ "@layer gp.marker, gp.vocab;",
6292
6437
  `/* gutterpress markers */
6293
- ${MARKER_CSS.trim()}`,
6438
+ @layer gp.marker {
6439
+ ${MARKER_CSS.trim()}
6440
+ }`,
6294
6441
  `/* gutterpress */
6295
- ${GUTTERPRESS_CSS.trim()}`,
6442
+ @layer gp.vocab {
6443
+ ${GUTTERPRESS_CSS.trim()}
6444
+ }`,
6296
6445
  pluginCss ? `/* user plugin css */
6297
6446
  ${pluginCss.trim()}` : null,
6298
6447
  projectCss ? `/* project css */
@@ -7537,7 +7686,7 @@ class PdfOutput {
7537
7686
  const rawPdf = pdfxMode ? path8.join(stage, "raw.pdf") : path8.resolve(pdfFile);
7538
7687
  await fsp2.mkdir(path8.dirname(path8.resolve(pdfFile)), { recursive: true });
7539
7688
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
7540
- const { buildNativePdf } = await import("./engine-fwe8djyn.js");
7689
+ const { buildNativePdf } = await import("./engine-bhhcne06.js");
7541
7690
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
7542
7691
  title: config.title,
7543
7692
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,