gutterpress 0.10.5-beta.1 → 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.
- package/dist/api/index.js +1 -1
- package/dist/{audit-9pf96y1w.js → audit-j57qptj6.js} +4 -4
- package/dist/{build-jw9kynts.js → build-1dkfbqrn.js} +4 -4
- package/dist/{cli-5w9y6r2f.js → cli-55rdak96.js} +341 -192
- package/dist/{cli-nb902265.js → cli-622k3vky.js} +1 -1
- package/dist/{cli-xewqry0j.js → cli-969n6fcc.js} +1 -1
- package/dist/{cli-0t4zfevc.js → cli-bwbs3qm4.js} +1 -1
- package/dist/cli.js +14 -14
- package/dist/{doctor-6vmxh7mz.js → doctor-k7p66yss.js} +2 -2
- package/dist/{engine-mtyjc4v6.js → engine-4ctgbwj5.js} +1 -1
- package/dist/{engine-fwe8djyn.js → engine-bhhcne06.js} +1 -1
- package/dist/{index-3xv7vwv2.js → index-aqxnmfzx.js} +160 -11
- package/dist/{index-ja0p4w5f.js → index-eyzv51dw.js} +1 -1
- package/dist/index.js +2 -2
- package/dist/lib/markdown/assemble.d.ts +3 -1
- package/dist/lib/markdown/gutterpress-css.d.ts +31 -5
- package/dist/lib/markdown/markers.d.ts +79 -3
- package/dist/lib/pdf-inspect.d.ts +33 -5
- package/dist/lib/render-parity.d.ts +108 -0
- package/dist/{lint-kzvbrn3d.js → lint-pb625yxd.js} +4 -4
- package/dist/{new-cyna8tyn.js → new-e84j6fe3.js} +4 -4
- package/dist/{plugin-mpc5v8sr.js → plugin-kv51tq0m.js} +4 -4
- package/dist/{preflight-fc178as2.js → preflight-w8yrpa3r.js} +4 -4
- package/dist/{preview-qnbbtyqb.js → preview-v5wpgdwj.js} +4 -4
- package/dist/{publish-gmgpyh4n.js → publish-5b77kasn.js} +4 -4
- package/dist/render.js +146 -6
- package/dist/{validate-0t3vjpmk.js → validate-vrhnbazt.js} +4 -4
- package/package.json +1 -1
package/dist/api/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
executeAndReport
|
|
3
|
-
} from "./cli-
|
|
3
|
+
} from "./cli-55rdak96.js";
|
|
4
4
|
import {
|
|
5
5
|
log
|
|
6
|
-
} from "./cli-
|
|
6
|
+
} from "./cli-622k3vky.js";
|
|
7
7
|
import {
|
|
8
8
|
UsageError,
|
|
9
9
|
rejectExtraPositionals,
|
|
10
10
|
rejectUnknownFlags
|
|
11
|
-
} from "./cli-
|
|
12
|
-
import"./cli-
|
|
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-
|
|
4
|
+
} from "./cli-55rdak96.js";
|
|
5
5
|
import {
|
|
6
6
|
log
|
|
7
|
-
} from "./cli-
|
|
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-
|
|
16
|
-
import"./cli-
|
|
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-
|
|
16
|
+
} from "./cli-622k3vky.js";
|
|
17
17
|
import {
|
|
18
18
|
DEBOUNCE,
|
|
19
19
|
UsageError,
|
|
20
20
|
resolvePort
|
|
21
|
-
} from "./cli-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
6438
|
+
@layer gp.marker {
|
|
6439
|
+
${MARKER_CSS.trim()}
|
|
6440
|
+
}`,
|
|
6294
6441
|
`/* gutterpress */
|
|
6295
|
-
|
|
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-
|
|
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,
|