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.
- package/dist/api/index.js +1 -1
- package/dist/{audit-ax5ps29k.js → audit-j57qptj6.js} +4 -4
- package/dist/{build-79dv44cn.js → build-1dkfbqrn.js} +4 -4
- package/dist/{cli-chkwtg9m.js → cli-55rdak96.js} +341 -192
- package/dist/{cli-5pq315qe.js → cli-622k3vky.js} +1 -1
- package/dist/{cli-v031r2h6.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-qejmbv32.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-ya1yw5me.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-wweg6pf0.js → lint-pb625yxd.js} +4 -4
- package/dist/{new-yf8emyp3.js → new-e84j6fe3.js} +4 -4
- package/dist/{plugin-dj6crat4.js → plugin-kv51tq0m.js} +4 -4
- package/dist/{preflight-d34pzgr6.js → preflight-w8yrpa3r.js} +4 -4
- package/dist/{preview-g2f5982y.js → preview-v5wpgdwj.js} +4 -4
- package/dist/{publish-6vt6yqm4.js → publish-5b77kasn.js} +4 -4
- package/dist/render.js +146 -6
- package/dist/{validate-vjwgxswp.js → validate-vrhnbazt.js} +4 -4
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
// package.json
|
|
5
5
|
var package_default = {
|
|
6
6
|
name: "gutterpress",
|
|
7
|
-
version: "0.10.
|
|
7
|
+
version: "0.10.6-beta.1",
|
|
8
8
|
description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
|
|
9
9
|
author: "itlackey",
|
|
10
10
|
license: "MPL-2.0",
|
|
@@ -2474,7 +2474,7 @@ ${mapCss}`;
|
|
|
2474
2474
|
if (pageBackgrounds.length)
|
|
2475
2475
|
log(`audit: ${pageBackgrounds.length} unreferenced @page background image(s)`);
|
|
2476
2476
|
for (const d of multicol)
|
|
2477
|
-
diagnose("engine.multicol.dead-column", `${d} runs over more than one page in columns, and only the last page's columns get balanced — earlier pages are left with an empty column. Add column-fill: auto
|
|
2477
|
+
diagnose("engine.multicol.dead-column", `${d} runs over more than one page in columns, and only the last page's columns get balanced — earlier pages are left with an empty column. Add .gp-columns-flow to ${d} (column-fill: auto) so every page's columns fill.`);
|
|
2478
2478
|
if (multicol.length)
|
|
2479
2479
|
log(`audit: ${multicol.length} fragmenting multicol warning(s)`);
|
|
2480
2480
|
}
|
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
UsageError,
|
|
7
7
|
package_default,
|
|
8
8
|
rejectUnknownFlags
|
|
9
|
-
} from "./cli-
|
|
9
|
+
} from "./cli-969n6fcc.js";
|
|
10
10
|
import {
|
|
11
11
|
EXIT_CODES
|
|
12
12
|
} from "./cli-46ycxe6r.js";
|
|
@@ -18,16 +18,16 @@ import {
|
|
|
18
18
|
import { defineCommand, parseArgs, runMain } from "citty";
|
|
19
19
|
import { statSync } from "node:fs";
|
|
20
20
|
var SUBCOMMANDS = {
|
|
21
|
-
new: () => import("./new-
|
|
22
|
-
preview: () => import("./preview-
|
|
23
|
-
build: () => import("./build-
|
|
24
|
-
publish: () => import("./publish-
|
|
25
|
-
lint: () => import("./lint-
|
|
26
|
-
validate: () => import("./validate-
|
|
27
|
-
audit: () => import("./audit-
|
|
28
|
-
preflight: () => import("./preflight-
|
|
29
|
-
doctor: () => import("./doctor-
|
|
30
|
-
plugin: () => import("./plugin-
|
|
21
|
+
new: () => import("./new-e84j6fe3.js").then((m) => m.default),
|
|
22
|
+
preview: () => import("./preview-v5wpgdwj.js").then((m) => m.default),
|
|
23
|
+
build: () => import("./build-1dkfbqrn.js").then((m) => m.default),
|
|
24
|
+
publish: () => import("./publish-5b77kasn.js").then((m) => m.default),
|
|
25
|
+
lint: () => import("./lint-pb625yxd.js").then((m) => m.default),
|
|
26
|
+
validate: () => import("./validate-vrhnbazt.js").then((m) => m.default),
|
|
27
|
+
audit: () => import("./audit-j57qptj6.js").then((m) => m.default),
|
|
28
|
+
preflight: () => import("./preflight-w8yrpa3r.js").then((m) => m.default),
|
|
29
|
+
doctor: () => import("./doctor-k7p66yss.js").then((m) => m.default),
|
|
30
|
+
plugin: () => import("./plugin-kv51tq0m.js").then((m) => m.default)
|
|
31
31
|
};
|
|
32
32
|
var VERSION = package_default.version;
|
|
33
33
|
var main = defineCommand({
|
|
@@ -52,7 +52,7 @@ async function preflightRequiredInvocations(rawArgs) {
|
|
|
52
52
|
const [command, ...commandArgs] = rawArgs;
|
|
53
53
|
try {
|
|
54
54
|
if (command === "new") {
|
|
55
|
-
const { newArgs } = await import("./new-
|
|
55
|
+
const { newArgs } = await import("./new-e84j6fe3.js");
|
|
56
56
|
rejectUnknownFlags(commandArgs, newArgs, "new");
|
|
57
57
|
const parsed2 = parseArgs(commandArgs, {
|
|
58
58
|
...newArgs,
|
|
@@ -64,7 +64,7 @@ async function preflightRequiredInvocations(rawArgs) {
|
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
66
|
if (command === "preflight") {
|
|
67
|
-
const { preflightArgs } = await import("./preflight-
|
|
67
|
+
const { preflightArgs } = await import("./preflight-w8yrpa3r.js");
|
|
68
68
|
rejectUnknownFlags(commandArgs, preflightArgs, "preflight");
|
|
69
69
|
const parsed2 = parseArgs(commandArgs, {
|
|
70
70
|
...preflightArgs,
|
|
@@ -88,7 +88,7 @@ async function preflightRequiredInvocations(rawArgs) {
|
|
|
88
88
|
if (subcommand !== "add") {
|
|
89
89
|
throw new UsageError(`gutterpress plugin: unknown command "${subcommand}"`);
|
|
90
90
|
}
|
|
91
|
-
const { pluginAddArgs } = await import("./plugin-
|
|
91
|
+
const { pluginAddArgs } = await import("./plugin-kv51tq0m.js");
|
|
92
92
|
rejectUnknownFlags(subcommandArgs, pluginAddArgs, "plugin add");
|
|
93
93
|
const parsed = parseArgs(subcommandArgs, {
|
|
94
94
|
...pluginAddArgs,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getSystemDiagnostics,
|
|
3
3
|
log
|
|
4
|
-
} from "./cli-
|
|
4
|
+
} from "./cli-622k3vky.js";
|
|
5
5
|
import {
|
|
6
6
|
UsageError,
|
|
7
7
|
rejectExtraPositionals,
|
|
8
8
|
rejectUnknownFlags
|
|
9
|
-
} from "./cli-
|
|
9
|
+
} from "./cli-969n6fcc.js";
|
|
10
10
|
import"./cli-c41yr7he.js";
|
|
11
11
|
import"./cli-46ycxe6r.js";
|
|
12
12
|
import"./cli-37x76zdn.js";
|
|
@@ -1186,6 +1186,8 @@ body { margin: 0; }
|
|
|
1186
1186
|
:where(h1,h2,h3,h4,h5,h6) { break-after: avoid; }
|
|
1187
1187
|
:where(img, svg, video) { max-width: 100%; }
|
|
1188
1188
|
:where(p > img:only-child, figure > img) { width: fit-content; max-width: 100%; height: auto; vertical-align: bottom; }
|
|
1189
|
+
:where(p) > :where(img:not([class])) { max-height: calc(var(--gp-content-h) - 4px); object-fit: contain; }
|
|
1190
|
+
:where(figure) { break-inside: avoid; }
|
|
1189
1191
|
:where(.section, figure) > :where(:first-child) { break-before: avoid; }
|
|
1190
1192
|
|
|
1191
1193
|
`;
|
|
@@ -1264,6 +1266,32 @@ var GUTTERPRESS_CSS = `
|
|
|
1264
1266
|
.gp-columns-2 { columns: 2; column-gap: var(--gp-column-gap, 1.5em); }
|
|
1265
1267
|
.gp-columns-3 { columns: 3; column-gap: var(--gp-column-gap, 1.5em); }
|
|
1266
1268
|
|
|
1269
|
+
/* the per-shape decisions the paragraph above deliberately leaves to the
|
|
1270
|
+
author, named instead of left as raw CSS every book was reinventing
|
|
1271
|
+
(2026-09-01 CSS architecture review, findings C1/C7 — CLAUDE.md §0:
|
|
1272
|
+
"behavior broadly useful to non-technical authors belongs in core").
|
|
1273
|
+
Permanent vocabulary, standard properties verbatim — same rationale as
|
|
1274
|
+
the column/grid runs above. One name each, no aliases:
|
|
1275
|
+
.gp-columns-all column-span: all a heading or block that
|
|
1276
|
+
spans every column in the
|
|
1277
|
+
run it sits inside.
|
|
1278
|
+
.gp-columns-flow column-fill: auto a run that FRAGMENTS across
|
|
1279
|
+
pages — every page's
|
|
1280
|
+
columns fill instead of
|
|
1281
|
+
only the last one
|
|
1282
|
+
balancing (the dead-column
|
|
1283
|
+
collapse the build's
|
|
1284
|
+
engine.multicol.dead-column
|
|
1285
|
+
warning names this fix
|
|
1286
|
+
for).
|
|
1287
|
+
.gp-columns-balanced column-fill: balance a run that fits on ONE
|
|
1288
|
+
page (the CSS initial
|
|
1289
|
+
value — ragged columns
|
|
1290
|
+
would be wrong here). */
|
|
1291
|
+
.gp-columns-all { column-span: all; }
|
|
1292
|
+
.gp-columns-flow { column-fill: auto; }
|
|
1293
|
+
.gp-columns-balanced { column-fill: balance; }
|
|
1294
|
+
|
|
1267
1295
|
/* grid runs — the SLOTTED counterpart to the column runs above. Grid places
|
|
1268
1296
|
each child into the next cell, across then down (deterministic slots: card
|
|
1269
1297
|
layouts, stat blocks, image-plus-caption pairs); columns FLOW one text run
|
|
@@ -1284,6 +1312,15 @@ var GUTTERPRESS_CSS = `
|
|
|
1284
1312
|
.gp-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gp-grid-gap, 1.5em); }
|
|
1285
1313
|
.gp-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gp-grid-gap, 1.5em); }
|
|
1286
1314
|
|
|
1315
|
+
/* fragmentation controls — generic pagination utilities for any element,
|
|
1316
|
+
independent of the column/grid vocabulary above (a book reached for
|
|
1317
|
+
these just as often outside a multicol run: keeping a card whole,
|
|
1318
|
+
forcing a section to start a fresh page). Standard properties only —
|
|
1319
|
+
this project is Chromium-only (CLAUDE.md), so there are no legacy
|
|
1320
|
+
page-break-* twins to also emit. One name each. */
|
|
1321
|
+
.gp-no-break { break-inside: avoid; }
|
|
1322
|
+
.gp-break-before { break-before: page; }
|
|
1323
|
+
|
|
1287
1324
|
/* shape wrap — text follows the image's alpha silhouette instead of its
|
|
1288
1325
|
rectangular box. shape-outside only applies to floats, so this is inert
|
|
1289
1326
|
without .gp-left/.gp-right (and under .gp-pin, which un-floats). The
|
|
@@ -1368,6 +1405,39 @@ img.gp-shape {
|
|
|
1368
1405
|
.gp-raised { z-index: var(--gp-z-raised); }
|
|
1369
1406
|
.gp-front { z-index: var(--gp-z-front); }
|
|
1370
1407
|
`;
|
|
1408
|
+
var GP_CLASSES = new Set([
|
|
1409
|
+
"gp-left",
|
|
1410
|
+
"gp-right",
|
|
1411
|
+
"gp-center",
|
|
1412
|
+
"gp-full",
|
|
1413
|
+
"gp-bleed",
|
|
1414
|
+
"gp-small",
|
|
1415
|
+
"gp-medium",
|
|
1416
|
+
"gp-large",
|
|
1417
|
+
"gp-tight",
|
|
1418
|
+
"gp-loose",
|
|
1419
|
+
"gp-columns-2",
|
|
1420
|
+
"gp-columns-3",
|
|
1421
|
+
"gp-columns-all",
|
|
1422
|
+
"gp-columns-flow",
|
|
1423
|
+
"gp-columns-balanced",
|
|
1424
|
+
"gp-no-break",
|
|
1425
|
+
"gp-break-before",
|
|
1426
|
+
"gp-grid-2",
|
|
1427
|
+
"gp-grid-3",
|
|
1428
|
+
"gp-shape",
|
|
1429
|
+
"gp-pin",
|
|
1430
|
+
"gp-top",
|
|
1431
|
+
"gp-bottom",
|
|
1432
|
+
"gp-behind",
|
|
1433
|
+
"gp-base",
|
|
1434
|
+
"gp-raised",
|
|
1435
|
+
"gp-front",
|
|
1436
|
+
"gp-page-break",
|
|
1437
|
+
"gp-column-break",
|
|
1438
|
+
"gp-continued",
|
|
1439
|
+
"gp-flush"
|
|
1440
|
+
]);
|
|
1371
1441
|
|
|
1372
1442
|
// src/lib/markdown/renderer.ts
|
|
1373
1443
|
import MarkdownIt from "markdown-it";
|
|
@@ -1444,12 +1514,70 @@ function warn3(env, line, type, message, marker) {
|
|
|
1444
1514
|
env.layoutWarnings = [];
|
|
1445
1515
|
env.layoutWarnings.push({ line, type, message, marker });
|
|
1446
1516
|
}
|
|
1517
|
+
function editDistance2(a, b) {
|
|
1518
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
1519
|
+
for (let i = 1;i <= a.length; i++) {
|
|
1520
|
+
const cur = [i];
|
|
1521
|
+
for (let j = 1;j <= b.length; j++) {
|
|
1522
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
1523
|
+
}
|
|
1524
|
+
prev = cur;
|
|
1525
|
+
}
|
|
1526
|
+
return prev[b.length];
|
|
1527
|
+
}
|
|
1528
|
+
function nearestGpClass(cls) {
|
|
1529
|
+
const threshold = Math.max(2, Math.floor(cls.length / 3));
|
|
1530
|
+
let best = null;
|
|
1531
|
+
let bestDistance = Infinity;
|
|
1532
|
+
for (const known of GP_CLASSES) {
|
|
1533
|
+
const d = editDistance2(cls, known);
|
|
1534
|
+
if (d < bestDistance) {
|
|
1535
|
+
bestDistance = d;
|
|
1536
|
+
best = known;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
return best !== null && bestDistance > 0 && bestDistance <= threshold ? best : null;
|
|
1540
|
+
}
|
|
1541
|
+
function hasPinClass(token) {
|
|
1542
|
+
const cls = token.attrGet && token.attrGet("class");
|
|
1543
|
+
return typeof cls === "string" && cls.split(/\s+/).includes("gp-pin");
|
|
1544
|
+
}
|
|
1545
|
+
function elementNameFor(token) {
|
|
1546
|
+
switch (token.type) {
|
|
1547
|
+
case "layout_chapter_open":
|
|
1548
|
+
return "@chapter";
|
|
1549
|
+
case "layout_spread_open":
|
|
1550
|
+
return "@spread";
|
|
1551
|
+
case "layout_page_open":
|
|
1552
|
+
return "@page";
|
|
1553
|
+
case "layout_section_open":
|
|
1554
|
+
return "@section";
|
|
1555
|
+
case "image":
|
|
1556
|
+
return "an image";
|
|
1557
|
+
case "heading_open":
|
|
1558
|
+
return "a heading";
|
|
1559
|
+
case "paragraph_open":
|
|
1560
|
+
return "a paragraph";
|
|
1561
|
+
case "link_open":
|
|
1562
|
+
return "a link";
|
|
1563
|
+
default:
|
|
1564
|
+
return token.tag ? `a <${token.tag}>` : "an element";
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
function checkUnknownGpClasses(token, env, line) {
|
|
1568
|
+
const cls = token.attrGet && token.attrGet("class");
|
|
1569
|
+
if (typeof cls !== "string" || !cls)
|
|
1570
|
+
return;
|
|
1571
|
+
for (const c of cls.split(/\s+/)) {
|
|
1572
|
+
if (!c.startsWith("gp-") || GP_CLASSES.has(c))
|
|
1573
|
+
continue;
|
|
1574
|
+
const suggestion = nearestGpClass(c);
|
|
1575
|
+
const base = `Unknown class "${c}" on ${elementNameFor(token)}.`;
|
|
1576
|
+
warn3(env, line, "unknown_gp_class", suggestion ? `${base} Did you mean "${suggestion}"?` : base);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1447
1579
|
function gpPinScope(md) {
|
|
1448
1580
|
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.";
|
|
1449
|
-
function hasPinClass(token) {
|
|
1450
|
-
const cls = token.attrGet && token.attrGet("class");
|
|
1451
|
-
return typeof cls === "string" && cls.split(/\s+/).includes("gp-pin");
|
|
1452
|
-
}
|
|
1453
1581
|
md.core.ruler.push("gp_pin_scope_check", function(state) {
|
|
1454
1582
|
let depth = 0;
|
|
1455
1583
|
for (const token of state.tokens) {
|
|
@@ -1468,6 +1596,13 @@ function gpPinScope(md) {
|
|
|
1468
1596
|
warn3(state.env, (token.map?.[0] ?? 0) + 1, "pin_outside_page", PIN_OUTSIDE_PAGE_MSG);
|
|
1469
1597
|
}
|
|
1470
1598
|
}
|
|
1599
|
+
const line = (token.map?.[0] ?? 0) + 1;
|
|
1600
|
+
checkUnknownGpClasses(token, state.env, line);
|
|
1601
|
+
if (token.type === "inline" && token.children) {
|
|
1602
|
+
for (const child of token.children) {
|
|
1603
|
+
checkUnknownGpClasses(child, state.env, line);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1471
1606
|
}
|
|
1472
1607
|
});
|
|
1473
1608
|
}
|
|
@@ -1991,10 +2126,15 @@ ${rendered}
|
|
|
1991
2126
|
opts.onImageRefs([...imageRefs]);
|
|
1992
2127
|
}
|
|
1993
2128
|
const inlineCss = [
|
|
2129
|
+
"@layer gp.marker, gp.vocab;",
|
|
1994
2130
|
`/* gutterpress markers */
|
|
1995
|
-
|
|
2131
|
+
@layer gp.marker {
|
|
2132
|
+
${MARKER_CSS.trim()}
|
|
2133
|
+
}`,
|
|
1996
2134
|
`/* gutterpress */
|
|
1997
|
-
|
|
2135
|
+
@layer gp.vocab {
|
|
2136
|
+
${GUTTERPRESS_CSS.trim()}
|
|
2137
|
+
}`,
|
|
1998
2138
|
pluginCss ? `/* user plugin css */
|
|
1999
2139
|
${pluginCss.trim()}` : null,
|
|
2000
2140
|
projectCss ? `/* project css */
|
|
@@ -3978,7 +4118,7 @@ import git from "isomorphic-git";
|
|
|
3978
4118
|
// package.json
|
|
3979
4119
|
var package_default = {
|
|
3980
4120
|
name: "gutterpress",
|
|
3981
|
-
version: "0.10.
|
|
4121
|
+
version: "0.10.6-beta.1",
|
|
3982
4122
|
description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
|
|
3983
4123
|
author: "itlackey",
|
|
3984
4124
|
license: "MPL-2.0",
|
|
@@ -4839,26 +4979,33 @@ function getTextPass(doc) {
|
|
|
4839
4979
|
p = (async () => {
|
|
4840
4980
|
const textByPage = [];
|
|
4841
4981
|
const positions = [];
|
|
4982
|
+
const runsByPage = [];
|
|
4842
4983
|
for (let i = 1;i <= doc.numPages; i++) {
|
|
4843
4984
|
try {
|
|
4844
4985
|
const page = await doc.getPage(i);
|
|
4845
4986
|
const tc = await page.getTextContent();
|
|
4846
4987
|
let pageText = "";
|
|
4988
|
+
const runs = [];
|
|
4847
4989
|
for (const item of tc.items) {
|
|
4848
4990
|
const it = item;
|
|
4849
4991
|
if (typeof it.str !== "string")
|
|
4850
4992
|
continue;
|
|
4851
4993
|
pageText += it.str;
|
|
4852
4994
|
if (it.transform && it.str.trim().length > 0) {
|
|
4853
|
-
|
|
4995
|
+
const x = it.transform[4];
|
|
4996
|
+
const y = it.transform[5];
|
|
4997
|
+
positions.push({ x, y });
|
|
4998
|
+
runs.push({ s: it.str, x, y, w: it.width ?? 0, h: it.height ?? 0 });
|
|
4854
4999
|
}
|
|
4855
5000
|
}
|
|
4856
5001
|
textByPage.push(pageText);
|
|
5002
|
+
runsByPage.push(runs);
|
|
4857
5003
|
} catch {
|
|
4858
5004
|
textByPage.push("");
|
|
5005
|
+
runsByPage.push([]);
|
|
4859
5006
|
}
|
|
4860
5007
|
}
|
|
4861
|
-
return { textByPage, positions };
|
|
5008
|
+
return { textByPage, positions, runsByPage };
|
|
4862
5009
|
})();
|
|
4863
5010
|
textCache.set(doc, p);
|
|
4864
5011
|
}
|
|
@@ -4922,6 +5069,8 @@ function getOpPass(doc) {
|
|
|
4922
5069
|
name,
|
|
4923
5070
|
placedW: Math.hypot(ctm[0], ctm[1]),
|
|
4924
5071
|
placedH: Math.hypot(ctm[2], ctm[3]),
|
|
5072
|
+
x: ctm[4],
|
|
5073
|
+
y: ctm[5],
|
|
4925
5074
|
page: i
|
|
4926
5075
|
});
|
|
4927
5076
|
} else if (fn === OPS.setFont) {
|
|
@@ -6195,7 +6344,7 @@ import { readFile as readFile17 } from "node:fs/promises";
|
|
|
6195
6344
|
var check22 = {
|
|
6196
6345
|
id: "source.markdown.layout-markers",
|
|
6197
6346
|
name: "Layout Markers",
|
|
6198
|
-
description: "Reports @page/@section/@chapter marker arguments Gutterpress could not understand",
|
|
6347
|
+
description: "Reports @page/@section/@chapter marker arguments Gutterpress could not understand, plus any unknown gp-* class",
|
|
6199
6348
|
category: "source",
|
|
6200
6349
|
phase: "pre-build",
|
|
6201
6350
|
async run(ctx) {
|
|
@@ -8032,7 +8181,7 @@ class PdfOutput {
|
|
|
8032
8181
|
const rawPdf = pdfxMode ? path8.join(stage, "raw.pdf") : path8.resolve(pdfFile);
|
|
8033
8182
|
await fsp2.mkdir(path8.dirname(path8.resolve(pdfFile)), { recursive: true });
|
|
8034
8183
|
log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
|
|
8035
|
-
const { buildNativePdf } = await import("./engine-
|
|
8184
|
+
const { buildNativePdf } = await import("./engine-4ctgbwj5.js");
|
|
8036
8185
|
const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
|
|
8037
8186
|
title: config.title,
|
|
8038
8187
|
author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
|
|
@@ -1989,7 +1989,7 @@ ${mapCss}`;
|
|
|
1989
1989
|
if (pageBackgrounds.length)
|
|
1990
1990
|
log(`audit: ${pageBackgrounds.length} unreferenced @page background image(s)`);
|
|
1991
1991
|
for (const d of multicol)
|
|
1992
|
-
diagnose("engine.multicol.dead-column", `${d} runs over more than one page in columns, and only the last page's columns get balanced — earlier pages are left with an empty column. Add column-fill: auto
|
|
1992
|
+
diagnose("engine.multicol.dead-column", `${d} runs over more than one page in columns, and only the last page's columns get balanced — earlier pages are left with an empty column. Add .gp-columns-flow to ${d} (column-fill: auto) so every page's columns fill.`);
|
|
1993
1993
|
if (multicol.length)
|
|
1994
1994
|
log(`audit: ${multicol.length} fragmenting multicol warning(s)`);
|
|
1995
1995
|
}
|
package/dist/index.js
CHANGED
|
@@ -124,10 +124,10 @@ import {
|
|
|
124
124
|
syncProject,
|
|
125
125
|
testRemoteAccess,
|
|
126
126
|
validateProjectPlugins
|
|
127
|
-
} from "./index-
|
|
127
|
+
} from "./index-aqxnmfzx.js";
|
|
128
128
|
import {
|
|
129
129
|
BUILD_DIAGNOSTIC_CODES
|
|
130
|
-
} from "./index-
|
|
130
|
+
} from "./index-eyzv51dw.js";
|
|
131
131
|
import {
|
|
132
132
|
BuildError,
|
|
133
133
|
DEFAULT_PRINT_OPTS,
|
|
@@ -8,7 +8,9 @@ export type ReadText = (relPath: string) => Promise<string>;
|
|
|
8
8
|
* `type`s (`ambiguous_marker_token`, `unrecognized_marker_token`,
|
|
9
9
|
* `extra_bare_marker_token`, `unknown_marker`, `nested_spread`,
|
|
10
10
|
* `continue_without_section`, `spread_without_pages`, `spread_eof_close`,
|
|
11
|
-
* `page_outside_spread`, `pin_outside_page`
|
|
11
|
+
* `page_outside_spread`, `pin_outside_page`, `unknown_gp_class` — the last
|
|
12
|
+
* emitted by `gp-pin-scope.js`'s `gp_pin_scope_check`, same as
|
|
13
|
+
* `pin_outside_page`, see #226).
|
|
12
14
|
*
|
|
13
15
|
* `section_without_page` and `implicit_page` were REMOVED 2026-08-12: a
|
|
14
16
|
* @section with no open @page is valid authoring (audited, 17/17 false
|
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
* classes authors apply to content. Keeping the two core blocks separate makes
|
|
8
8
|
* that ownership boundary explicit without implying an external plugin.
|
|
9
9
|
*
|
|
10
|
-
* Injected by assemble.ts
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Injected by assemble.ts inside `@layer gp.vocab` (#227), immediately after
|
|
11
|
+
* MARKER_CSS's own `@layer gp.marker`, and before user plugin CSS and the
|
|
12
|
+
* author's project stylesheets — both of which stay UNLAYERED. Per the CSS
|
|
13
|
+
* Cascade Layers spec, unlayered CSS always beats layered CSS regardless of
|
|
14
|
+
* selector specificity, so a plugin or project rule targeting a `gp-*` class
|
|
15
|
+
* now wins UNCONDITIONALLY, not merely at equal specificity — see
|
|
16
|
+
* assemble.ts's cascade-order comment for the full layer list.
|
|
14
17
|
*
|
|
15
18
|
* Also ships the author-facing `gp-*` image/block vocabulary (CLAUDE.md §0 —
|
|
16
19
|
* a behavior broadly useful to non-technical authors belongs in core, not a
|
|
@@ -133,4 +136,27 @@
|
|
|
133
136
|
* carrying gp-left could start shifting; that is standards-tracking per
|
|
134
137
|
* CLAUDE.md ("Chrome wins once it ships"), not a bug in the author's book.
|
|
135
138
|
*/
|
|
136
|
-
export declare const GUTTERPRESS_CSS = "\n/* gp-* author image/block vocabulary. One vocabulary, gp-* only \u2014 the\n pre-vocabulary utility names (.center/.float-left/.float-right/\n .full-width/.full-bleed) were REMOVED when gp-* shipped; books rename\n the classes in their markdown (see the migration note). Source ORDER is\n the contract \u2014 see the doctrine comment above. */\n\n/* flow positions */\n.gp-left {\n float: left;\n margin: 0 var(--gp-gap, 1em) var(--gp-gap, 1em) 0;\n max-width: 50%;\n}\n.gp-right {\n float: right;\n margin: 0 0 var(--gp-gap, 1em) var(--gp-gap, 1em);\n max-width: 50%;\n}\n.gp-center {\n display: block;\n float: none;\n margin-left: auto;\n margin-right: auto;\n max-width: 100%;\n}\n.gp-full {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n@page gp-full-bleed { margin-left: 0; margin-right: 0; }\n.gp-bleed {\n display: block;\n float: none;\n break-before: page;\n page: gp-full-bleed;\n max-width: none;\n width: 100%;\n margin-left: 0;\n margin-right: 0;\n}\n\n/* sizes \u2014 AFTER the flow positions so max-width:100% lifts the floats' 50%\n cap at equal specificity */\n.gp-small { width: 25%; max-width: 100%; }\n.gp-medium { width: 50%; max-width: 100%; }\n.gp-large { width: 75%; max-width: 100%; }\n\n/* float clearance presets \u2014 consumed by var(--gp-gap) in the float rules\n above and by .gp-shape's shape-margin below; --gp-gap itself is\n author-settable CSS */\n.gp-tight { --gp-gap: 0.5em; }\n.gp-loose { --gp-gap: 2em; }\n\n/* column runs \u2014 plain CSS Multi-column, exposed as author vocabulary so\n \"put this in two columns\" does not require borrowing a styled container\n from the book's own component layer. That borrowing is what this exists\n to prevent: a book whose theme paints .section chrome by default gives\n every author who opens a section just to start a column run a panel they\n did not ask for, and the book then needs a reset rule to take it back.\n With a neutral primitive the author opts into columns and nothing else.\n\n Permanent vocabulary, not a shim: Chromium implements multicol natively\n and these rules are the standard properties verbatim, so there is no\n spec gap here to remove later. Deliberately minimal \u2014 column-fill is\n NOT set, because the correct value depends on whether the run fragments\n across pages (auto packs each page's columns; the CSS initial balance is\n right for a run that fits on one page) and only the author knows which.\n --gp-column-gap is author-settable. */\n.gp-columns-2 { columns: 2; column-gap: var(--gp-column-gap, 1.5em); }\n.gp-columns-3 { columns: 3; column-gap: var(--gp-column-gap, 1.5em); }\n\n/* grid runs \u2014 the SLOTTED counterpart to the column runs above. Grid places\n each child into the next cell, across then down (deterministic slots: card\n layouts, stat blocks, image-plus-caption pairs); columns FLOW one text run\n down then across. Same neutral-primitive rationale as .gp-columns-*, and\n permanent vocabulary for the same reason: standard CSS Grid verbatim, no\n spec gap to remove later. MEASURED (Chromium 151, gp-grid evidence pack):\n grid rows fragment across sheets with EXACT print/viewer parity \u2014 2- and\n 3-col, unequal item heights, mid-row cuts, multi-sheet overflow,\n break-inside:avoid, gap geometry \u2014 so a grid taller than the page is safe,\n no fit-one-page constraint. Two things to know, not fix:\n - on a min-height page root (MARKER_CSS), default align-content\n stretches rows apart to fill the page \u2014 identically in both engines.\n Authors wanting packed rows set align-content: start.\n - a @page-break / @column-break marker DIRECTLY inside a grid container\n becomes a grid item and corrupts placement (the one measured parity\n break); markers.js diagnoses it (break_inside_grid).\n --gp-grid-gap is author-settable. */\n.gp-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gp-grid-gap, 1.5em); }\n.gp-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gp-grid-gap, 1.5em); }\n\n/* shape wrap \u2014 text follows the image's alpha silhouette instead of its\n rectangular box. shape-outside only applies to floats, so this is inert\n without .gp-left/.gp-right (and under .gp-pin, which un-floats). The\n shape URL cannot be written in CSS (url() contexts can't read attr()),\n so the image renderer rule (images.ts) mirrors the src into an inline\n --gp-shape:url(...) custom property whenever it sees this class --\n authors only ever type the class. threshold 0.2 ignores near-transparent\n anti-aliasing halos; shape-margin shares the float-gap vocabulary. */\nimg.gp-shape {\n shape-outside: var(--gp-shape);\n shape-image-threshold: 0.2;\n shape-margin: var(--gp-gap, 1em);\n}\n\n/* pin \u2014 within the nearest positioned ancestor (.page/.spread, rule above).\n inset:0 and the explicit centers are load-bearing; see doctrine comment. */\n.gp-pin {\n position: absolute;\n inset: 0;\n align-self: center;\n justify-self: center;\n margin: 0;\n max-width: 100%;\n}\n\n/* pin edge modifiers \u2014 AFTER .gp-pin to beat its center defaults;\n justify-self is inert on in-flow floats, so gp-left/gp-right safely do\n double duty as flow float + pin edge */\n.gp-top { align-self: start; }\n.gp-bottom { align-self: end; }\n.gp-left { justify-self: start; }\n.gp-right { justify-self: end; }\n\n/* wrapper-margin neutralization (same pattern and rationale as the\n .gp-bleed paragraph-margin note in the doctrine comment; for pin, the\n emptied paragraph would otherwise leave a phantom margin gap in flow) */\n:where(p:has(> img.gp-bleed:only-child)) { margin: 0; }\n:where(p:has(> img.gp-pin:only-child)) { margin: 0; }\n\n/* depth \u2014 a named ladder for z-index, so books stop hand-tuning bare\n integers. A real book measured 21 z-index declarations using only four\n distinct values (-1, 0, 1, 2), each written literally at its use site.\n The custom properties are the author-settable surface (a book needing a\n deeper stack raises them once); the classes are the shorthand.\n\n NOT named \"layer\": CSS Paged Media 3 \u00A73.1 already defines \"page layers\"\n (page background, canvas, borders, contents, margin boxes) and those are\n parts of the PAGE BOX, not a z-ladder for content. Reusing the word for a\n different concept would collide with the spec vocabulary this project\n tracks. The pin EDGE modifiers already own .gp-top/.gp-bottom, so the\n ladder avoids those words too.\n\n .gp-behind is the one that earns its place: it puts a pinned image UNDER\n the page's text, which is otherwise impossible to express without a bare\n negative z-index. \"Above\" needs no class \u2014 an out-of-flow pin already\n paints above in-flow content.\n\n Two things silently defeat .gp-behind, neither visible at the use site:\n - a stacking context on the .page/.spread ancestor (z-index, isolation,\n opacity, filter, transform on it traps the negative layer inside).\n Core keeps .page/.spread at 'position: relative; z-index: auto'\n precisely so they are not stacking contexts.\n - a clipping ancestor (overflow other than visible) \u2014 but only where\n the art actually overhangs that ancestor's clip box on a clipped\n axis: the overhang is cut off, the same mechanism that clips a\n .gp-bleed plate back to the wrapper's width. Clipping never reorders\n layers \u2014 within-bounds art under a clipping .page prints whole and\n still behind (measured; see the build audit's comment in\n engine/compiler/build.ts), and a static wrapper's overflow never\n binds an abspos .gp-pin at all.\n The build-time engine.layer.trapped audit reports both against the live\n ancestor chain. printsafe/page-containment is only an early source hint for\n declarations written directly on .page/.spread. */\n:root {\n --gp-z-behind: -1;\n --gp-z-base: 0;\n --gp-z-raised: 1;\n --gp-z-front: 2;\n}\n.gp-behind { z-index: var(--gp-z-behind); }\n.gp-base { z-index: var(--gp-z-base); }\n.gp-raised { z-index: var(--gp-z-raised); }\n.gp-front { z-index: var(--gp-z-front); }\n";
|
|
139
|
+
export declare const GUTTERPRESS_CSS = "\n/* gp-* author image/block vocabulary. One vocabulary, gp-* only \u2014 the\n pre-vocabulary utility names (.center/.float-left/.float-right/\n .full-width/.full-bleed) were REMOVED when gp-* shipped; books rename\n the classes in their markdown (see the migration note). Source ORDER is\n the contract \u2014 see the doctrine comment above. */\n\n/* flow positions */\n.gp-left {\n float: left;\n margin: 0 var(--gp-gap, 1em) var(--gp-gap, 1em) 0;\n max-width: 50%;\n}\n.gp-right {\n float: right;\n margin: 0 0 var(--gp-gap, 1em) var(--gp-gap, 1em);\n max-width: 50%;\n}\n.gp-center {\n display: block;\n float: none;\n margin-left: auto;\n margin-right: auto;\n max-width: 100%;\n}\n.gp-full {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n@page gp-full-bleed { margin-left: 0; margin-right: 0; }\n.gp-bleed {\n display: block;\n float: none;\n break-before: page;\n page: gp-full-bleed;\n max-width: none;\n width: 100%;\n margin-left: 0;\n margin-right: 0;\n}\n\n/* sizes \u2014 AFTER the flow positions so max-width:100% lifts the floats' 50%\n cap at equal specificity */\n.gp-small { width: 25%; max-width: 100%; }\n.gp-medium { width: 50%; max-width: 100%; }\n.gp-large { width: 75%; max-width: 100%; }\n\n/* float clearance presets \u2014 consumed by var(--gp-gap) in the float rules\n above and by .gp-shape's shape-margin below; --gp-gap itself is\n author-settable CSS */\n.gp-tight { --gp-gap: 0.5em; }\n.gp-loose { --gp-gap: 2em; }\n\n/* column runs \u2014 plain CSS Multi-column, exposed as author vocabulary so\n \"put this in two columns\" does not require borrowing a styled container\n from the book's own component layer. That borrowing is what this exists\n to prevent: a book whose theme paints .section chrome by default gives\n every author who opens a section just to start a column run a panel they\n did not ask for, and the book then needs a reset rule to take it back.\n With a neutral primitive the author opts into columns and nothing else.\n\n Permanent vocabulary, not a shim: Chromium implements multicol natively\n and these rules are the standard properties verbatim, so there is no\n spec gap here to remove later. Deliberately minimal \u2014 column-fill is\n NOT set, because the correct value depends on whether the run fragments\n across pages (auto packs each page's columns; the CSS initial balance is\n right for a run that fits on one page) and only the author knows which.\n --gp-column-gap is author-settable. */\n.gp-columns-2 { columns: 2; column-gap: var(--gp-column-gap, 1.5em); }\n.gp-columns-3 { columns: 3; column-gap: var(--gp-column-gap, 1.5em); }\n\n/* the per-shape decisions the paragraph above deliberately leaves to the\n author, named instead of left as raw CSS every book was reinventing\n (2026-09-01 CSS architecture review, findings C1/C7 \u2014 CLAUDE.md \u00A70:\n \"behavior broadly useful to non-technical authors belongs in core\").\n Permanent vocabulary, standard properties verbatim \u2014 same rationale as\n the column/grid runs above. One name each, no aliases:\n .gp-columns-all column-span: all a heading or block that\n spans every column in the\n run it sits inside.\n .gp-columns-flow column-fill: auto a run that FRAGMENTS across\n pages \u2014 every page's\n columns fill instead of\n only the last one\n balancing (the dead-column\n collapse the build's\n engine.multicol.dead-column\n warning names this fix\n for).\n .gp-columns-balanced column-fill: balance a run that fits on ONE\n page (the CSS initial\n value \u2014 ragged columns\n would be wrong here). */\n.gp-columns-all { column-span: all; }\n.gp-columns-flow { column-fill: auto; }\n.gp-columns-balanced { column-fill: balance; }\n\n/* grid runs \u2014 the SLOTTED counterpart to the column runs above. Grid places\n each child into the next cell, across then down (deterministic slots: card\n layouts, stat blocks, image-plus-caption pairs); columns FLOW one text run\n down then across. Same neutral-primitive rationale as .gp-columns-*, and\n permanent vocabulary for the same reason: standard CSS Grid verbatim, no\n spec gap to remove later. MEASURED (Chromium 151, gp-grid evidence pack):\n grid rows fragment across sheets with EXACT print/viewer parity \u2014 2- and\n 3-col, unequal item heights, mid-row cuts, multi-sheet overflow,\n break-inside:avoid, gap geometry \u2014 so a grid taller than the page is safe,\n no fit-one-page constraint. Two things to know, not fix:\n - on a min-height page root (MARKER_CSS), default align-content\n stretches rows apart to fill the page \u2014 identically in both engines.\n Authors wanting packed rows set align-content: start.\n - a @page-break / @column-break marker DIRECTLY inside a grid container\n becomes a grid item and corrupts placement (the one measured parity\n break); markers.js diagnoses it (break_inside_grid).\n --gp-grid-gap is author-settable. */\n.gp-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gp-grid-gap, 1.5em); }\n.gp-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gp-grid-gap, 1.5em); }\n\n/* fragmentation controls \u2014 generic pagination utilities for any element,\n independent of the column/grid vocabulary above (a book reached for\n these just as often outside a multicol run: keeping a card whole,\n forcing a section to start a fresh page). Standard properties only \u2014\n this project is Chromium-only (CLAUDE.md), so there are no legacy\n page-break-* twins to also emit. One name each. */\n.gp-no-break { break-inside: avoid; }\n.gp-break-before { break-before: page; }\n\n/* shape wrap \u2014 text follows the image's alpha silhouette instead of its\n rectangular box. shape-outside only applies to floats, so this is inert\n without .gp-left/.gp-right (and under .gp-pin, which un-floats). The\n shape URL cannot be written in CSS (url() contexts can't read attr()),\n so the image renderer rule (images.ts) mirrors the src into an inline\n --gp-shape:url(...) custom property whenever it sees this class --\n authors only ever type the class. threshold 0.2 ignores near-transparent\n anti-aliasing halos; shape-margin shares the float-gap vocabulary. */\nimg.gp-shape {\n shape-outside: var(--gp-shape);\n shape-image-threshold: 0.2;\n shape-margin: var(--gp-gap, 1em);\n}\n\n/* pin \u2014 within the nearest positioned ancestor (.page/.spread, rule above).\n inset:0 and the explicit centers are load-bearing; see doctrine comment. */\n.gp-pin {\n position: absolute;\n inset: 0;\n align-self: center;\n justify-self: center;\n margin: 0;\n max-width: 100%;\n}\n\n/* pin edge modifiers \u2014 AFTER .gp-pin to beat its center defaults;\n justify-self is inert on in-flow floats, so gp-left/gp-right safely do\n double duty as flow float + pin edge */\n.gp-top { align-self: start; }\n.gp-bottom { align-self: end; }\n.gp-left { justify-self: start; }\n.gp-right { justify-self: end; }\n\n/* wrapper-margin neutralization (same pattern and rationale as the\n .gp-bleed paragraph-margin note in the doctrine comment; for pin, the\n emptied paragraph would otherwise leave a phantom margin gap in flow) */\n:where(p:has(> img.gp-bleed:only-child)) { margin: 0; }\n:where(p:has(> img.gp-pin:only-child)) { margin: 0; }\n\n/* depth \u2014 a named ladder for z-index, so books stop hand-tuning bare\n integers. A real book measured 21 z-index declarations using only four\n distinct values (-1, 0, 1, 2), each written literally at its use site.\n The custom properties are the author-settable surface (a book needing a\n deeper stack raises them once); the classes are the shorthand.\n\n NOT named \"layer\": CSS Paged Media 3 \u00A73.1 already defines \"page layers\"\n (page background, canvas, borders, contents, margin boxes) and those are\n parts of the PAGE BOX, not a z-ladder for content. Reusing the word for a\n different concept would collide with the spec vocabulary this project\n tracks. The pin EDGE modifiers already own .gp-top/.gp-bottom, so the\n ladder avoids those words too.\n\n .gp-behind is the one that earns its place: it puts a pinned image UNDER\n the page's text, which is otherwise impossible to express without a bare\n negative z-index. \"Above\" needs no class \u2014 an out-of-flow pin already\n paints above in-flow content.\n\n Two things silently defeat .gp-behind, neither visible at the use site:\n - a stacking context on the .page/.spread ancestor (z-index, isolation,\n opacity, filter, transform on it traps the negative layer inside).\n Core keeps .page/.spread at 'position: relative; z-index: auto'\n precisely so they are not stacking contexts.\n - a clipping ancestor (overflow other than visible) \u2014 but only where\n the art actually overhangs that ancestor's clip box on a clipped\n axis: the overhang is cut off, the same mechanism that clips a\n .gp-bleed plate back to the wrapper's width. Clipping never reorders\n layers \u2014 within-bounds art under a clipping .page prints whole and\n still behind (measured; see the build audit's comment in\n engine/compiler/build.ts), and a static wrapper's overflow never\n binds an abspos .gp-pin at all.\n The build-time engine.layer.trapped audit reports both against the live\n ancestor chain. printsafe/page-containment is only an early source hint for\n declarations written directly on .page/.spread. */\n:root {\n --gp-z-behind: -1;\n --gp-z-base: 0;\n --gp-z-raised: 1;\n --gp-z-front: 2;\n}\n.gp-behind { z-index: var(--gp-z-behind); }\n.gp-base { z-index: var(--gp-z-base); }\n.gp-raised { z-index: var(--gp-z-raised); }\n.gp-front { z-index: var(--gp-z-front); }\n";
|
|
140
|
+
/**
|
|
141
|
+
* GP_CLASSES — every class an author may legitimately write with a `gp-`
|
|
142
|
+
* prefix: the `.gp-*` selectors in `GUTTERPRESS_CSS` above, the two
|
|
143
|
+
* structural classes `markers.js`'s `MARKER_CSS` styles (`gp-page-break`,
|
|
144
|
+
* `gp-column-break`), and two classes the marker plugin EMITS but that carry
|
|
145
|
+
* no CSS rule of their own — `gp-continued` (a `@continue`d section's marker,
|
|
146
|
+
* for author/theme styling — see markers.js's header) and `gp-flush` (a
|
|
147
|
+
* `.gp-pin` edge modifier implemented in the engine's layout code, not CSS —
|
|
148
|
+
* see the `.gp-flush` doctrine note above).
|
|
149
|
+
*
|
|
150
|
+
* This is the vocabulary `gp-pin-scope.js`'s `unknown_gp_class` diagnostic
|
|
151
|
+
* (#226) checks every author-facing class against: any `gp-`-prefixed class
|
|
152
|
+
* NOT in this set is either a typo or forgotten vocabulary, and is worth a
|
|
153
|
+
* warning either way — key on `gp-` only; `.dc-*`, `.fg-*`, and unprefixed
|
|
154
|
+
* classes are none of core's business.
|
|
155
|
+
*
|
|
156
|
+
* `gutterpress-css.test.ts` asserts this set and the `.gp-*` selectors
|
|
157
|
+
* textually present in `GUTTERPRESS_CSS` + `MARKER_CSS` agree (modulo the two
|
|
158
|
+
* marker-only exceptions above), so the two cannot silently drift apart —
|
|
159
|
+
* whoever adds a class to one CSS block and forgets this list finds out from
|
|
160
|
+
* a failing test, not from a future bug report.
|
|
161
|
+
*/
|
|
162
|
+
export declare const GP_CLASSES: ReadonlySet<string>;
|
|
@@ -2,8 +2,21 @@ export default function plugin(md: any, pluginOptions?: {}): void;
|
|
|
2
2
|
/**
|
|
3
3
|
* The minimal CSS the DOM this module emits requires. Author utility
|
|
4
4
|
* vocabulary lives in gutterpress-css.ts — see the ownership note above.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* The real contract (#227; a prior version of this paragraph said the
|
|
7
|
+
* opposite and was stale — 2026-09-01 CSS architecture review, C4):
|
|
8
|
+
* consumers inject this FIRST, wrapped in `@layer gp.marker` (assemble.ts
|
|
9
|
+
* declares `@layer gp.marker, gp.vocab;` before both core blocks). Author
|
|
10
|
+
* CSS — plugin CSS, every project stylesheet, anything loaded via
|
|
11
|
+
* `engineStyles.native` — stays UNLAYERED, and per the CSS Cascade Layers
|
|
12
|
+
* spec unlayered CSS always wins over layered CSS regardless of selector
|
|
13
|
+
* specificity. That is what makes "author wins" true now, not injection
|
|
14
|
+
* order. The `:where()` wrapping on the break/orphan/sizing rules below is
|
|
15
|
+
* a NARROWER, still-necessary guarantee that survives the layer: it keeps
|
|
16
|
+
* this block from out-specificity-ing whatever ELSE an author writes
|
|
17
|
+
* (their own plain `.section { break-inside: ... }`, say) — a concern
|
|
18
|
+
* `@layer` doesn't touch, since two unlayered rules still settle by
|
|
19
|
+
* ordinary specificity between themselves.
|
|
7
20
|
*
|
|
8
21
|
* `.page`/`.spread` are given `position: relative` so they are the containing
|
|
9
22
|
* block for any abspos descendant: a mispinned `bottom: 0` now fails LOCALLY
|
|
@@ -76,5 +89,68 @@ export default function plugin(md: any, pluginOptions?: {}): void;
|
|
|
76
89
|
* bottom` collapses the line box onto the image, keeping the image inline (so
|
|
77
90
|
* `text-align: center` still centers it — `display: block` would not).
|
|
78
91
|
*
|
|
92
|
+
* #231 — two engine-generic print fixes adopted from a real book's own
|
|
93
|
+
* engine sheet (2026-09-01 CSS architecture review, finding C8), because
|
|
94
|
+
* both are written entirely in terms of core's own published contract and
|
|
95
|
+
* fix a failure every book with big art or a `<figure>` hits, not a
|
|
96
|
+
* DC-brand-specific one:
|
|
97
|
+
*
|
|
98
|
+
* - A bare markdown placard (``, no class) taller than the
|
|
99
|
+
* page content box is monolithic replaced content, and the fragmenter
|
|
100
|
+
* SLICES it mid-image across the page break instead of moving it whole.
|
|
101
|
+
* Capping a bare image to `--gp-content-h` (core's own published page
|
|
102
|
+
* CONTENT height for the page context it is in — see the min-height
|
|
103
|
+
* rule above) with `object-fit: contain` letterboxes it onto one page
|
|
104
|
+
* instead. Scoped to `:not([class])` so it never touches a `.gp-full`,
|
|
105
|
+
* `.gp-bleed`, or any other explicitly sized/classed image — an author
|
|
106
|
+
* who already sized their own art has already made the call this rule
|
|
107
|
+
* exists to make for the ones who haven't. `--gp-content-h` is
|
|
108
|
+
* published by BOTH renderers (the compiler on `:root` plus every
|
|
109
|
+
* `page:` assignment selector; the viewer on each `.gp-strip`), so this
|
|
110
|
+
* rule cannot itself split preview from print.
|
|
111
|
+
*
|
|
112
|
+
* MEASURED on the field guide (295pp, adopted verbatim from its own
|
|
113
|
+
* native-furniture.css §9, which carried this exact rule for months):
|
|
114
|
+
* chapter-01's rabbit placard paints 717.0pt tall on the default page
|
|
115
|
+
* both before and after adoption (core's cap and the book's copy agree
|
|
116
|
+
* bit-for-bit — the book's copy is now a harmless duplicate of core's,
|
|
117
|
+
* not a competing rule). Chapter-03's full-sheet plate (an `@page` with
|
|
118
|
+
* zero margins, so the content box IS the sheet) paints 621.0 x 804.0pt
|
|
119
|
+
* inside its 621 x 810pt sheet both before and after — the OLD hand-
|
|
120
|
+
* computed version of this rule (`calc(var(--page-height) - 0.5in -
|
|
121
|
+
* 0.75in - 4px)`, hard-coding the DEFAULT page's margins) over-capped
|
|
122
|
+
* this named page's art to 553.5 x 717.0pt, a 1.3in band of bare wall
|
|
123
|
+
* along its foot; reading the cap off `--gp-content-h` instead fixed
|
|
124
|
+
* that BEFORE core ever adopted the rule, so adoption itself changes
|
|
125
|
+
* nothing further. Total page count unchanged at 295pp — no
|
|
126
|
+
* shrink-to-fit side effect from moving the rule into core.
|
|
127
|
+
*
|
|
128
|
+
* - `figure { break-inside: avoid }` keeps in-flow art (anything an
|
|
129
|
+
* author or a plugin wraps in a real `<figure>`) from being sliced
|
|
130
|
+
* across a page break the same way a bare placard would be — the
|
|
131
|
+
* `:where(p > img:only-child, figure > img)` sizing rule above already
|
|
132
|
+
* assumes the figure survives as one piece; without this it did not.
|
|
133
|
+
*
|
|
134
|
+
* Design-for-deletion note (CLAUDE.md): neither rule is a Chromium-gap shim
|
|
135
|
+
* with a removal trigger — both are permanent, standards-verbatim defaults
|
|
136
|
+
* in the same category as the min-height/box-sizing/vertical-align rules
|
|
137
|
+
* above (real print-fragmentation behavior, not a spec feature Chrome has
|
|
138
|
+
* yet to ship). They stay `:where()` so an author's own sizing or
|
|
139
|
+
* break-inside rule, at ANY specificity, still wins outright.
|
|
140
|
+
*
|
|
141
|
+
* The companion "spanner + unbreakable box" glue pattern (`column-span: all`
|
|
142
|
+
* followed by a `break-inside: avoid` box, native-engine-styling-guide.md
|
|
143
|
+
* §5) was measured for this same adoption pass and DELIBERATELY NOT
|
|
144
|
+
* adopted: a `:where()`-zero-specificity override can never win against
|
|
145
|
+
* whatever real-specificity rule gave the box its `break-inside: avoid` in
|
|
146
|
+
* the first place (a synthetic 2-column fixture with the candidate
|
|
147
|
+
* `:where(.gp-columns-all) + :where(*) { break-inside: auto }` produced
|
|
148
|
+
* BYTE-IDENTICAL output to the same fixture without it, across every
|
|
149
|
+
* geometry swept), and the field guide's own native-furniture.css §13 shows
|
|
150
|
+
* a context-blind version of the same idea is actively wrong for at least
|
|
151
|
+
* one real, shipped case (a short, non-fragmenting instance that needed its
|
|
152
|
+
* `avoid` restored after a general fragmenting-context rule took it away).
|
|
153
|
+
* It stays an author remedy, named in the styling guide (`.gp-columns-all`).
|
|
154
|
+
*
|
|
79
155
|
*/
|
|
80
|
-
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";
|
|
156
|
+
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(p) > :where(img:not([class])) { max-height: calc(var(--gp-content-h) - 4px); object-fit: contain; }\n:where(figure) { break-inside: avoid; }\n:where(.section, figure) > :where(:first-child) { break-before: avoid; }\n\n";
|