gutterpress 0.10.0-alpha.3 → 0.10.0-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/README.md +2 -0
- package/dist/api/index.js +2 -2
- package/dist/{audit-xxegv0t0.js → audit-85zn35v8.js} +4 -4
- package/dist/{build-w0vb6mgq.js → build-1faqmp0y.js} +6 -4
- package/dist/{cli-5czby0dd.js → cli-5tffrg48.js} +90 -23
- package/dist/{cli-f4rcbt2t.js → cli-7e79d7se.js} +1 -1
- package/dist/{cli-yp3p0sf2.js → cli-afm8pa0h.js} +327 -24
- package/dist/{cli-gk3wsdpb.js → cli-v935c2ws.js} +1 -1
- package/dist/{cli-revgt4pr.js → cli-yzf38679.js} +1 -0
- package/dist/cli.js +16 -16
- package/dist/{doctor-ctgn1dnt.js → doctor-69v5c9wy.js} +2 -2
- package/dist/engine/compiler/build.d.ts +1 -1
- package/dist/engine/shared/flush.d.ts +59 -0
- package/dist/engine/shared/margin-box-support.d.ts +24 -0
- package/dist/{engine-eng4xh2q.js → engine-hrtn0b28.js} +1 -1
- package/dist/{engine-q5skhgms.js → engine-nbfdy0pp.js} +2 -2
- package/dist/{gutterpress-agent-1ctgfz92.js → gutterpress-agent-cazqstr1.js} +49 -1
- package/dist/{gutterpress-viewer-cem7dmr5.js → gutterpress-viewer-fb6fhwsy.js} +137 -51
- package/dist/{index-s416qpdv.js → index-052x5fef.js} +89 -22
- package/dist/{index-mvehxh0e.js → index-c2b6gyeg.js} +47 -4
- package/dist/{index-a4kr77td.js → index-wsydvp08.js} +287 -23
- package/dist/index.js +3 -3
- package/dist/lib/build-runner.d.ts +7 -0
- package/dist/lib/engine.d.ts +9 -0
- package/dist/lib/markdown/gutterpress-css.d.ts +9 -1
- package/dist/lib/markdown/markers.d.ts +51 -1
- package/dist/{lint-gsh6qwrq.js → lint-17404bsx.js} +4 -4
- package/dist/{new-nkhycqy4.js → new-vsgpxzek.js} +4 -4
- package/dist/{plugin-129wcs93.js → plugin-s4yfpx6p.js} +4 -4
- package/dist/{preflight-41hcejg9.js → preflight-1h3apvqh.js} +4 -4
- package/dist/{preview-pftsvr9z.js → preview-cqf383g4.js} +6 -5
- package/dist/{preview-interface-fnqb2y4v.js → preview-interface-hg5eqv88.js} +193 -4
- package/dist/{publish-dnjvb4jg.js → publish-arvycera.js} +4 -4
- package/dist/render.js +83 -7
- package/dist/{repair-a3enjttn.js → repair-vmwh2bhe.js} +4 -4
- package/dist/{validate-ph1xffqy.js → validate-ztzq7p35.js} +4 -4
- package/package.json +1 -1
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getAssetPath,
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
isIgnoredMarginBoxProperty,
|
|
4
|
+
launchChromium,
|
|
5
|
+
marginBoxAlign,
|
|
6
|
+
marginBoxRectPt
|
|
7
|
+
} from "./index-c2b6gyeg.js";
|
|
5
8
|
|
|
6
9
|
// src/engine/compiler/build.ts
|
|
7
10
|
import { readFile } from "node:fs/promises";
|
|
@@ -568,6 +571,45 @@ function specificity(r) {
|
|
|
568
571
|
return (r.name ? 2 : 0) + r.pseudos.length;
|
|
569
572
|
}
|
|
570
573
|
|
|
574
|
+
// src/engine/shared/flush.ts
|
|
575
|
+
var FLUSH_EDGES = ["top", "right", "bottom", "left"];
|
|
576
|
+
function flushKey(edges) {
|
|
577
|
+
return FLUSH_EDGES.filter((e) => edges.includes(e)).map((e) => e[0]).join("");
|
|
578
|
+
}
|
|
579
|
+
function flushPageName(authorPage, edges) {
|
|
580
|
+
const safe = authorPage ? authorPage.replace(/[^A-Za-z0-9_-]/g, "_") : "";
|
|
581
|
+
return `gp--flush${safe ? `-${safe}` : ""}-${flushKey(edges)}`;
|
|
582
|
+
}
|
|
583
|
+
function flushMargins(margin, edges) {
|
|
584
|
+
return {
|
|
585
|
+
top: edges.includes("top") ? 0 : margin.top,
|
|
586
|
+
right: edges.includes("right") ? 0 : margin.right,
|
|
587
|
+
bottom: edges.includes("bottom") ? 0 : margin.bottom,
|
|
588
|
+
left: edges.includes("left") ? 0 : margin.left
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function marginBoxesOnEdges(edges) {
|
|
592
|
+
const owners = {
|
|
593
|
+
"top-left-corner": ["top", "left"],
|
|
594
|
+
"top-left": ["top"],
|
|
595
|
+
"top-center": ["top"],
|
|
596
|
+
"top-right": ["top"],
|
|
597
|
+
"top-right-corner": ["top", "right"],
|
|
598
|
+
"bottom-left-corner": ["bottom", "left"],
|
|
599
|
+
"bottom-left": ["bottom"],
|
|
600
|
+
"bottom-center": ["bottom"],
|
|
601
|
+
"bottom-right": ["bottom"],
|
|
602
|
+
"bottom-right-corner": ["bottom", "right"],
|
|
603
|
+
"left-top": ["left"],
|
|
604
|
+
"left-middle": ["left"],
|
|
605
|
+
"left-bottom": ["left"],
|
|
606
|
+
"right-top": ["right"],
|
|
607
|
+
"right-middle": ["right"],
|
|
608
|
+
"right-bottom": ["right"]
|
|
609
|
+
};
|
|
610
|
+
return Object.entries(owners).filter(([, own]) => own.some((e) => edges.includes(e))).map(([name]) => name);
|
|
611
|
+
}
|
|
612
|
+
|
|
571
613
|
// src/engine/shared/synthesis.ts
|
|
572
614
|
var RECTO_VERSO_VALUES = /^(right|recto|left|verso)$/;
|
|
573
615
|
function isRectoVersoBreak(decl) {
|
|
@@ -1284,7 +1326,8 @@ var BUILD_DIAGNOSTIC_CODES = [
|
|
|
1284
1326
|
"engine.layer.trapped",
|
|
1285
1327
|
"engine.multicol.dead-column",
|
|
1286
1328
|
"engine.content.overheight",
|
|
1287
|
-
"engine.image.low-dpi"
|
|
1329
|
+
"engine.image.low-dpi",
|
|
1330
|
+
"engine.flush.margin-box"
|
|
1288
1331
|
];
|
|
1289
1332
|
async function build(opts) {
|
|
1290
1333
|
const log = opts.onProgress ?? (() => {});
|
|
@@ -1303,8 +1346,7 @@ async function build(opts) {
|
|
|
1303
1346
|
await page.evaluate(AGENT);
|
|
1304
1347
|
await page.waitForReady();
|
|
1305
1348
|
const cssText = await page.evaluate(`window.__gp.collectCss()`);
|
|
1306
|
-
|
|
1307
|
-
const { tier3Reasons } = classify(model);
|
|
1349
|
+
let model = extract(cssText);
|
|
1308
1350
|
const baseGeom = resolvePage(model).geometry;
|
|
1309
1351
|
const sheetViewport = {
|
|
1310
1352
|
width: Math.max(1, Math.round(baseGeom.width * 96 / 72)),
|
|
@@ -1315,6 +1357,97 @@ async function build(opts) {
|
|
|
1315
1357
|
await page.send("Emulation.setDeviceMetricsOverride", sheetViewport);
|
|
1316
1358
|
await page.send("Emulation.setEmulatedMedia", { media: "print" });
|
|
1317
1359
|
log(`print media emulated for audits and measurement`);
|
|
1360
|
+
const flushRoots = await page.evaluate(`window.__gp.flushRoots()`);
|
|
1361
|
+
const flushGroups = new Map;
|
|
1362
|
+
for (const root of flushRoots) {
|
|
1363
|
+
if (flushGroups.has(root.key))
|
|
1364
|
+
continue;
|
|
1365
|
+
const authorPage = root.page === "auto" ? undefined : root.page;
|
|
1366
|
+
flushGroups.set(root.key, {
|
|
1367
|
+
authorPage,
|
|
1368
|
+
edges: root.edges,
|
|
1369
|
+
genName: flushPageName(authorPage, root.edges),
|
|
1370
|
+
key: root.key
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
if (flushGroups.size) {
|
|
1374
|
+
const lines = [];
|
|
1375
|
+
for (const group of flushGroups.values()) {
|
|
1376
|
+
const relocated = new Set(marginBoxesOnEdges(group.edges));
|
|
1377
|
+
const pseudoSets = [[]];
|
|
1378
|
+
if (group.authorPage) {
|
|
1379
|
+
for (const rule of model.pageRules) {
|
|
1380
|
+
if (rule.name !== group.authorPage)
|
|
1381
|
+
continue;
|
|
1382
|
+
const pseudo = rule.pseudos.length ? `:${rule.pseudos.join(":")}` : "";
|
|
1383
|
+
if (rule.pseudos.length && !pseudoSets.some((ps) => ps.length === rule.pseudos.length && rule.pseudos.every((x) => ps.includes(x))))
|
|
1384
|
+
pseudoSets.push(rule.pseudos);
|
|
1385
|
+
const body = [];
|
|
1386
|
+
for (const [prop, value] of Object.entries(rule.decls))
|
|
1387
|
+
body.push(` ${prop}: ${value};`);
|
|
1388
|
+
for (const [box, decls] of Object.entries(rule.marginBoxes)) {
|
|
1389
|
+
if (relocated.has(box.slice(1)))
|
|
1390
|
+
continue;
|
|
1391
|
+
body.push(` ${box} {`);
|
|
1392
|
+
for (const [prop, value] of Object.entries(decls))
|
|
1393
|
+
body.push(` ${prop}: ${value};`);
|
|
1394
|
+
body.push(` }`);
|
|
1395
|
+
}
|
|
1396
|
+
lines.push(`@page ${group.genName}${pseudo} {
|
|
1397
|
+
${body.join(`
|
|
1398
|
+
`)}
|
|
1399
|
+
}`);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
for (const pseudos of pseudoSets) {
|
|
1403
|
+
const pseudo = pseudos.length ? `:${pseudos.join(":")}` : "";
|
|
1404
|
+
const body = [];
|
|
1405
|
+
for (const edge of group.edges)
|
|
1406
|
+
body.push(` margin-${edge}: 0;`);
|
|
1407
|
+
for (const box of relocated)
|
|
1408
|
+
body.push(` @${box} { content: none; }`);
|
|
1409
|
+
lines.push(`@page ${group.genName}${pseudo} {
|
|
1410
|
+
${body.join(`
|
|
1411
|
+
`)}
|
|
1412
|
+
}`);
|
|
1413
|
+
}
|
|
1414
|
+
lines.push(`:where(.page, .spread)[data-gp-flush="${group.key}"][data-gp-flush][data-gp-flush][data-gp-flush] { page: ${group.genName}; }`);
|
|
1415
|
+
}
|
|
1416
|
+
const flushCss = lines.join(`
|
|
1417
|
+
`);
|
|
1418
|
+
await page.evaluate(`window.__gp.addCss("gp-flush-css", ${JSON.stringify(flushCss)})`);
|
|
1419
|
+
model = extract(`${cssText}
|
|
1420
|
+
${flushCss}`);
|
|
1421
|
+
log(`.gp-flush: ${flushRoots.length} pinned root(s) -> ${flushGroups.size} generated page context(s)`);
|
|
1422
|
+
}
|
|
1423
|
+
const { tier3Reasons } = classify(model);
|
|
1424
|
+
const pseudoContexts = pagePseudoContexts(model);
|
|
1425
|
+
const contentHeightPt = (name) => Math.min(...pseudoContexts.map((pseudos) => {
|
|
1426
|
+
const geometry = resolvePage(model, { name, pseudos }).geometry;
|
|
1427
|
+
return geometry.height - geometry.margin.top - geometry.margin.bottom;
|
|
1428
|
+
}));
|
|
1429
|
+
const contentHeightPx = contentHeightPt() * 96 / 72;
|
|
1430
|
+
const namedContentHeightsPx = Object.fromEntries(model.pageNames.map((name) => [name, contentHeightPt(name) * 96 / 72]));
|
|
1431
|
+
const minContentHeightPx = Math.min(contentHeightPt(), ...model.pageNames.map((name) => contentHeightPt(name))) * 96 / 72;
|
|
1432
|
+
const pageVarsCss = [
|
|
1433
|
+
`:root { --gp-content-h: ${contentHeightPx}px; }`,
|
|
1434
|
+
...model.pageAssignments.map((a) => `${a.selector} { --gp-content-h: ${namedContentHeightsPx[a.page] ?? contentHeightPx}px; }`)
|
|
1435
|
+
].join(`
|
|
1436
|
+
`);
|
|
1437
|
+
await page.evaluate(`window.__gp.addCss("gp-page-vars", ${JSON.stringify(pageVarsCss)})`);
|
|
1438
|
+
log(`page geometry published (--gp-content-h: ${Math.round(contentHeightPx)}px)`);
|
|
1439
|
+
const relocationBoxes = (group, pseudos) => {
|
|
1440
|
+
const ctx = resolvePage(model, { name: group.authorPage, pseudos });
|
|
1441
|
+
return marginBoxesOnEdges(group.edges).map((name) => [name, ctx.marginBoxes[`@${name}`]]).filter((pair) => {
|
|
1442
|
+
const c = pair[1]?.content?.trim();
|
|
1443
|
+
return !!c && c !== "none" && c !== "normal";
|
|
1444
|
+
});
|
|
1445
|
+
};
|
|
1446
|
+
const furnitureRoots = flushRoots.filter((root) => {
|
|
1447
|
+
const group = flushGroups.get(root.key);
|
|
1448
|
+
return pseudoContexts.some((pseudos) => relocationBoxes(group, pseudos).length > 0);
|
|
1449
|
+
});
|
|
1450
|
+
const flushDiagnosed = new Set;
|
|
1318
1451
|
const tier2 = synthesize({
|
|
1319
1452
|
model,
|
|
1320
1453
|
marks: opts.marks,
|
|
@@ -1334,11 +1467,15 @@ async function build(opts) {
|
|
|
1334
1467
|
}).join(`
|
|
1335
1468
|
`);
|
|
1336
1469
|
if (widthOffenders.boxes.length) {
|
|
1337
|
-
const
|
|
1470
|
+
const scale = shrinkScale(widthOffenders.boxes, widthOffenders.limitPx);
|
|
1471
|
+
const headline = scale === null ? `content outside the page content box risks Chromium print ` + `shrink-to-fit (the WHOLE book scales down, silently)` : `content wider than the page content box: Chromium print shrink-to-fit ` + `scales the WHOLE document — every page, every measurement — to about ` + `${scale.toFixed(2)}x its declared size (12pt type prints at ` + `${(12 * scale).toFixed(1)}pt). The page size and page count do not ` + `change, so the shrink is invisible in the PDF`;
|
|
1472
|
+
const msg = `${headline}:
|
|
1338
1473
|
${describe(widthOffenders.boxes)}`;
|
|
1339
1474
|
if (opts.allowShrink) {
|
|
1475
|
+
if (scale !== null)
|
|
1476
|
+
diagnose("engine.width.overflow", `${headline}.`);
|
|
1340
1477
|
for (const o of widthOffenders.boxes)
|
|
1341
|
-
diagnose("engine.width.overflow", `${o.desc} is
|
|
1478
|
+
diagnose("engine.width.overflow", `${o.desc} is ${Math.round(o.px)}px wide, past the ${Math.round(widthOffenders.limitPx)}px page content box. ${o.left < -1 ? "Keep it inside the page content box." : "Give it an explicit width that fits."}`);
|
|
1342
1479
|
log(`WARNING: ${msg}`);
|
|
1343
1480
|
} else {
|
|
1344
1481
|
throw new Error(`${msg}
|
|
@@ -1353,7 +1490,7 @@ ${describe(widthOffenders.intrinsics)}`;
|
|
|
1353
1490
|
log(`WARNING: ${msg}`);
|
|
1354
1491
|
}
|
|
1355
1492
|
const rectoDecls = model.breaks.filter(isRectoVersoBreak);
|
|
1356
|
-
const needsMeasure = tier3Reasons.length > 0 || consumedStrings(model).size > 0 || rectoDecls.length > 0 || model.counterResets.length > 0;
|
|
1493
|
+
const needsMeasure = tier3Reasons.length > 0 || consumedStrings(model).size > 0 || rectoDecls.length > 0 || model.counterResets.length > 0 || furnitureRoots.length > 0;
|
|
1357
1494
|
let tier = tier2.geometry.bleed > 0 || tier2.geometry.slug > 0 ? 2 : 1;
|
|
1358
1495
|
let passes = 1;
|
|
1359
1496
|
let pageMap = {};
|
|
@@ -1381,6 +1518,8 @@ ${describe(widthOffenders.intrinsics)}`;
|
|
|
1381
1518
|
targets.add(s.id);
|
|
1382
1519
|
for (const s of resetSites)
|
|
1383
1520
|
targets.add(s.id);
|
|
1521
|
+
for (const r of furnitureRoots)
|
|
1522
|
+
targets.add(r.id);
|
|
1384
1523
|
await page.evaluate(`window.__gp.instrument(${JSON.stringify([...targets])})`);
|
|
1385
1524
|
const targetText = await page.evaluate(`window.__gp.targetTexts(${JSON.stringify([...targets])})`);
|
|
1386
1525
|
const brokenXrefs = findBrokenXrefRefs(sites, targetText);
|
|
@@ -1465,6 +1604,73 @@ ${describe(widthOffenders.intrinsics)}`;
|
|
|
1465
1604
|
/* Tier 3 */
|
|
1466
1605
|
${mapCss}`;
|
|
1467
1606
|
}
|
|
1607
|
+
if (furnitureRoots.length) {
|
|
1608
|
+
const pageAt = (id) => map[id] ?? predictedForResult?.pageMap[id];
|
|
1609
|
+
const byName = new Map;
|
|
1610
|
+
for (const s of sources) {
|
|
1611
|
+
const p = pageAt(s.id);
|
|
1612
|
+
if (!p)
|
|
1613
|
+
continue;
|
|
1614
|
+
const list = byName.get(s.name) ?? [];
|
|
1615
|
+
list.push({ page: p, value: s.text });
|
|
1616
|
+
byName.set(s.name, list);
|
|
1617
|
+
}
|
|
1618
|
+
for (const entries of byName.values())
|
|
1619
|
+
entries.sort((a, b) => a.page - b.page);
|
|
1620
|
+
const PXPT = 96 / 72;
|
|
1621
|
+
const items = [];
|
|
1622
|
+
for (const root of furnitureRoots) {
|
|
1623
|
+
const physical = pageAt(root.id);
|
|
1624
|
+
if (!physical)
|
|
1625
|
+
continue;
|
|
1626
|
+
const group = flushGroups.get(root.key);
|
|
1627
|
+
const pseudos = [physical % 2 === 1 ? "right" : "left"];
|
|
1628
|
+
if (physical === 1)
|
|
1629
|
+
pseudos.push("first");
|
|
1630
|
+
const ctx = resolvePage(model, { name: group.authorPage, pseudos });
|
|
1631
|
+
const g = ctx.geometry;
|
|
1632
|
+
const eff = flushMargins(g.margin, group.edges);
|
|
1633
|
+
const boxes = [];
|
|
1634
|
+
for (const [name, decls] of relocationBoxes(group, pseudos)) {
|
|
1635
|
+
const text = evaluate(decls.content, {
|
|
1636
|
+
page: toFolioPage(physical, pageValues),
|
|
1637
|
+
pages: pageCount,
|
|
1638
|
+
strings: (n, w) => stringValueAt(byName.get(n) ?? [], physical, parseWhich(w)),
|
|
1639
|
+
targetPage: (u) => {
|
|
1640
|
+
const p = pageAt(u.replace(/^#/, ""));
|
|
1641
|
+
return p === undefined ? undefined : toFolioPage(p, pageValues);
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1644
|
+
if (!text) {
|
|
1645
|
+
if (!flushDiagnosed.has(name)) {
|
|
1646
|
+
flushDiagnosed.add(name);
|
|
1647
|
+
diagnose("engine.flush.margin-box", `@${name} sits on an edge a .gp-flush pin frees, and its content could not be ` + `re-homed into the page (unsupported content value: ${decls.content}). It will ` + `not print on that page — simplify the box's content, or drop .gp-flush there.`);
|
|
1648
|
+
}
|
|
1649
|
+
continue;
|
|
1650
|
+
}
|
|
1651
|
+
const r = marginBoxRectPt(name, g);
|
|
1652
|
+
const outDecls = {};
|
|
1653
|
+
for (const [prop, value] of Object.entries(decls)) {
|
|
1654
|
+
if (prop.toLowerCase() === "content" || isIgnoredMarginBoxProperty(prop))
|
|
1655
|
+
continue;
|
|
1656
|
+
outDecls[prop] = value;
|
|
1657
|
+
}
|
|
1658
|
+
boxes.push({
|
|
1659
|
+
box: name,
|
|
1660
|
+
x: (r.x - eff.left) * PXPT,
|
|
1661
|
+
y: (r.y - eff.top) * PXPT,
|
|
1662
|
+
w: r.w * PXPT,
|
|
1663
|
+
h: r.h * PXPT,
|
|
1664
|
+
align: marginBoxAlign(name),
|
|
1665
|
+
text,
|
|
1666
|
+
decls: outDecls
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
items.push({ id: root.id, boxes });
|
|
1670
|
+
}
|
|
1671
|
+
if (items.length)
|
|
1672
|
+
await page.evaluate(`window.__gp.setFlushFurniture(${JSON.stringify(items)})`);
|
|
1673
|
+
}
|
|
1468
1674
|
};
|
|
1469
1675
|
const predicted = await predictPageMap(browser, url, AGENT, VIEWER, {
|
|
1470
1676
|
stringSets: model.stringSets.map((s) => ({ selector: s.selector, name: s.name, value: s.value })),
|
|
@@ -1503,14 +1709,6 @@ ${mapCss}`;
|
|
|
1503
1709
|
log(`tier 3: NOT converged after ${passes} passes`);
|
|
1504
1710
|
}
|
|
1505
1711
|
}
|
|
1506
|
-
const pseudoContexts = pagePseudoContexts(model);
|
|
1507
|
-
const contentHeightPt = (name) => Math.min(...pseudoContexts.map((pseudos) => {
|
|
1508
|
-
const geometry = resolvePage(model, { name, pseudos }).geometry;
|
|
1509
|
-
return geometry.height - geometry.margin.top - geometry.margin.bottom;
|
|
1510
|
-
}));
|
|
1511
|
-
const contentHeightPx = contentHeightPt() * 96 / 72;
|
|
1512
|
-
const namedContentHeightsPx = Object.fromEntries(model.pageNames.map((name) => [name, contentHeightPt(name) * 96 / 72]));
|
|
1513
|
-
const minContentHeightPx = Math.min(contentHeightPt(), ...model.pageNames.map((name) => contentHeightPt(name))) * 96 / 72;
|
|
1514
1712
|
{
|
|
1515
1713
|
const audit = await page.evaluate(`window.__gp.auditContent(${JSON.stringify({
|
|
1516
1714
|
default: contentHeightPx,
|
|
@@ -1564,6 +1762,26 @@ ${mapCss}`;
|
|
|
1564
1762
|
return reasons;
|
|
1565
1763
|
};
|
|
1566
1764
|
|
|
1765
|
+
// Does this box establish the containing block for an absolutely
|
|
1766
|
+
// positioned descendant? Overflow clipping binds an abspos
|
|
1767
|
+
// .gp-pin only from its containing block outward — MEASURED: a
|
|
1768
|
+
// pin whose box lay entirely outside a STATIC overflow:hidden
|
|
1769
|
+
// (and overflow:clip) wrapper printed complete and still behind
|
|
1770
|
+
// the page text; the wrapper's clip never touched it.
|
|
1771
|
+
const establishesAbsContainingBlock = (cs) => {
|
|
1772
|
+
if (cs.position !== "static") return true;
|
|
1773
|
+
for (const prop of [
|
|
1774
|
+
"transform", "translate", "rotate", "scale", "perspective",
|
|
1775
|
+
"filter", "backdropFilter",
|
|
1776
|
+
]) {
|
|
1777
|
+
const value = cs[prop];
|
|
1778
|
+
if (value && value !== "none") return true;
|
|
1779
|
+
}
|
|
1780
|
+
if (/\\b(layout|paint|strict|content)\\b/.test(cs.contain)) return true;
|
|
1781
|
+
if (cs.containerType && cs.containerType !== "normal") return true;
|
|
1782
|
+
return /\\b(transform|translate|rotate|scale|perspective|filter)\\b/.test(cs.willChange);
|
|
1783
|
+
};
|
|
1784
|
+
|
|
1567
1785
|
for (const el of document.querySelectorAll("*")) {
|
|
1568
1786
|
const cs = getComputedStyle(el);
|
|
1569
1787
|
if (leaks.length < 20) {
|
|
@@ -1604,20 +1822,58 @@ ${mapCss}`;
|
|
|
1604
1822
|
) {
|
|
1605
1823
|
const boundary = el.closest(".page, .spread");
|
|
1606
1824
|
if (boundary) {
|
|
1825
|
+
const elRect = el.getBoundingClientRect();
|
|
1826
|
+
// Clip binding: an in-flow .gp-behind is bound by every
|
|
1827
|
+
// ancestor's overflow, but an abspos one only from its
|
|
1828
|
+
// containing block outward — static wrappers in between
|
|
1829
|
+
// never clip it (measured; see the pass comment above).
|
|
1830
|
+
let clipBinds = cs.position !== "absolute" && cs.position !== "fixed";
|
|
1607
1831
|
for (let ancestor = el.parentElement; ancestor; ancestor = ancestor.parentElement) {
|
|
1608
1832
|
const ancestorStyle = getComputedStyle(ancestor);
|
|
1833
|
+
if (!clipBinds && establishesAbsContainingBlock(ancestorStyle)) clipBinds = true;
|
|
1609
1834
|
const reasons = stackingReasons(ancestor, ancestorStyle);
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1835
|
+
// Clipping never reorders layers — it can only CUT the
|
|
1836
|
+
// art (measured, pass comment above): warn only where the
|
|
1837
|
+
// border box crosses a binding ancestor's clip edge on an
|
|
1838
|
+
// axis whose overflow is not \`visible\`. The clip edge is
|
|
1839
|
+
// the padding box, grown by overflow-clip-margin where
|
|
1840
|
+
// that axis's value is \`clip\` (px values only; keyword
|
|
1841
|
+
// forms parse NaN -> 0, i.e. the ungrown padding box).
|
|
1842
|
+
// 1px epsilon: the measured cut lands exactly at the
|
|
1843
|
+
// edge, and sub-pixel layout rounding is not an overhang.
|
|
1844
|
+
const cuts = [];
|
|
1845
|
+
if (clipBinds && (ancestorStyle.overflowX !== "visible" || ancestorStyle.overflowY !== "visible")) {
|
|
1846
|
+
const r = ancestor.getBoundingClientRect();
|
|
1847
|
+
const clipMargin = parseFloat(ancestorStyle.overflowClipMargin) || 0;
|
|
1848
|
+
if (ancestorStyle.overflowX !== "visible") {
|
|
1849
|
+
const grow = ancestorStyle.overflowX === "clip" ? clipMargin : 0;
|
|
1850
|
+
const left = r.left + parseFloat(ancestorStyle.borderLeftWidth) - grow;
|
|
1851
|
+
const right = r.right - parseFloat(ancestorStyle.borderRightWidth) + grow;
|
|
1852
|
+
if (elRect.left < left - 1)
|
|
1853
|
+
cuts.push(Math.round(left - elRect.left) + "px past its left clip edge");
|
|
1854
|
+
if (elRect.right > right + 1)
|
|
1855
|
+
cuts.push(Math.round(elRect.right - right) + "px past its right clip edge");
|
|
1856
|
+
}
|
|
1857
|
+
if (ancestorStyle.overflowY !== "visible") {
|
|
1858
|
+
const grow = ancestorStyle.overflowY === "clip" ? clipMargin : 0;
|
|
1859
|
+
const top = r.top + parseFloat(ancestorStyle.borderTopWidth) - grow;
|
|
1860
|
+
const bottom = r.bottom - parseFloat(ancestorStyle.borderBottomWidth) + grow;
|
|
1861
|
+
if (elRect.top < top - 1)
|
|
1862
|
+
cuts.push(Math.round(top - elRect.top) + "px past its top clip edge");
|
|
1863
|
+
if (elRect.bottom > bottom + 1)
|
|
1864
|
+
cuts.push(Math.round(elRect.bottom - bottom) + "px past its bottom clip edge");
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
if ((reasons.length || cuts.length) && !seenLayerTraps.has(ancestor)) {
|
|
1613
1868
|
seenLayerTraps.add(ancestor);
|
|
1614
1869
|
const effects = [];
|
|
1615
1870
|
if (reasons.length)
|
|
1616
1871
|
effects.push("creates a stacking context (" + reasons.join(", ") + ")");
|
|
1617
|
-
if (
|
|
1872
|
+
if (cuts.length)
|
|
1618
1873
|
effects.push(
|
|
1619
|
-
"clips
|
|
1620
|
-
", overflow-y: " + ancestorStyle.overflowY +
|
|
1874
|
+
"clips it (overflow-x: " + ancestorStyle.overflowX +
|
|
1875
|
+
", overflow-y: " + ancestorStyle.overflowY +
|
|
1876
|
+
") — the art extends " + cuts.join(" and ") + " and is cut off there"
|
|
1621
1877
|
);
|
|
1622
1878
|
layerTraps.push({
|
|
1623
1879
|
behind: desc(el),
|
|
@@ -1685,10 +1941,17 @@ async function printPdf(page) {
|
|
|
1685
1941
|
await page.waitForReady();
|
|
1686
1942
|
return page.printToPDF();
|
|
1687
1943
|
}
|
|
1944
|
+
var MAX_SHRINK = 1.5;
|
|
1945
|
+
function shrinkScale(boxes, limitPx) {
|
|
1946
|
+
const maxRight = Math.max(...boxes.map((o) => o.px + Math.min(0, o.left)));
|
|
1947
|
+
if (!(maxRight > limitPx))
|
|
1948
|
+
return null;
|
|
1949
|
+
return Math.max(1 / MAX_SHRINK, limitPx / maxRight);
|
|
1950
|
+
}
|
|
1688
1951
|
async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreViewport) {
|
|
1689
1952
|
const contexts = [
|
|
1690
1953
|
resolvePage(model),
|
|
1691
|
-
...model.pageNames.filter((n) => !n.startsWith("gp-")).map((n) => resolvePage(model, { name: n }))
|
|
1954
|
+
...model.pageNames.filter((n) => !n.startsWith("gp-") || n.startsWith("gp--flush-")).map((n) => resolvePage(model, { name: n }))
|
|
1692
1955
|
];
|
|
1693
1956
|
const maxContentPt = Math.max(...contexts.map((c) => c.geometry.width - c.geometry.margin.left - c.geometry.margin.right)) + bleedSlugExtensionPt;
|
|
1694
1957
|
const limitPx = maxContentPt * 96 / 72;
|
|
@@ -1775,6 +2038,7 @@ async function predictPageMap(browser, url, agentScript, viewerScript, args, she
|
|
|
1775
2038
|
await page.waitForReady();
|
|
1776
2039
|
await page.evaluate(`window.__GP_MANUAL__ = true;`);
|
|
1777
2040
|
await page.evaluate(viewerScript);
|
|
2041
|
+
await page.evaluate(`window.__gp.flushRoots()`);
|
|
1778
2042
|
await page.evaluate(`window.__gp.stringSources(${JSON.stringify(args.stringSets)})`);
|
|
1779
2043
|
if (args.rectoDecls.length)
|
|
1780
2044
|
await page.evaluate(`window.__gp.forcedBreakSites(${JSON.stringify(args.rectoDecls)})`);
|
package/dist/index.js
CHANGED
|
@@ -140,15 +140,15 @@ import {
|
|
|
140
140
|
validateProjectPlugins,
|
|
141
141
|
verifyRepoReadable,
|
|
142
142
|
writeAppHeartbeat
|
|
143
|
-
} from "./index-
|
|
143
|
+
} from "./index-052x5fef.js";
|
|
144
144
|
import {
|
|
145
145
|
BUILD_DIAGNOSTIC_CODES
|
|
146
|
-
} from "./index-
|
|
146
|
+
} from "./index-wsydvp08.js";
|
|
147
147
|
import {
|
|
148
148
|
BuildError,
|
|
149
149
|
DEFAULT_PRINT_OPTS,
|
|
150
150
|
readyProbeExpr
|
|
151
|
-
} from "./index-
|
|
151
|
+
} from "./index-c2b6gyeg.js";
|
|
152
152
|
import {
|
|
153
153
|
AUTO_SNAPSHOT_MESSAGE,
|
|
154
154
|
HISTORY_PAGE_LIMIT,
|
|
@@ -22,6 +22,13 @@ export interface BuildRunnerOptions {
|
|
|
22
22
|
skipLint?: boolean;
|
|
23
23
|
skipPreValidate?: boolean;
|
|
24
24
|
skipPostValidate?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Proceed past the engine's over-wide-content check (pdf/pdfx only), which
|
|
27
|
+
* otherwise hard-errors because Chromium silently scales the WHOLE book down
|
|
28
|
+
* to fit the offending box. Each offender is still reported as a warning +
|
|
29
|
+
* diagnostic — this buys an eyes-open build, not a clean one.
|
|
30
|
+
*/
|
|
31
|
+
allowShrink?: boolean;
|
|
25
32
|
/**
|
|
26
33
|
* Keep the pooled headless browser alive after the build returns. A one-shot
|
|
27
34
|
* CLI build leaves this false so the process can exit; a long-lived
|
package/dist/lib/engine.d.ts
CHANGED
|
@@ -6,6 +6,15 @@ export interface NativePdfOptions {
|
|
|
6
6
|
title?: string;
|
|
7
7
|
author?: string;
|
|
8
8
|
signature?: number;
|
|
9
|
+
/**
|
|
10
|
+
* Downgrade the engine's over-wide-content hard error to a warning +
|
|
11
|
+
* diagnostic. The engine's message tells the author to "pass allowShrink to
|
|
12
|
+
* build anyway"; without this the advice is unreachable from every product
|
|
13
|
+
* path (only a test and the parity gate could set it). The book still prints
|
|
14
|
+
* at Chromium's mystery shrink scale, which is why this is opt-in per build
|
|
15
|
+
* and never a config default.
|
|
16
|
+
*/
|
|
17
|
+
allowShrink?: boolean;
|
|
9
18
|
}
|
|
10
19
|
/**
|
|
11
20
|
* Render `htmlFile` to `outPdf` via the Gutterpress engine. No HTTP staging,
|
|
@@ -41,6 +41,14 @@
|
|
|
41
41
|
* .gp-pin — pins within the nearest @page/@spread container;
|
|
42
42
|
* centered on both axes unless combined with the edge
|
|
43
43
|
* modifiers .gp-top/.gp-bottom/.gp-left/.gp-right.
|
|
44
|
+
* .gp-flush — with .gp-pin + an edge, the art sits on the PAPER's
|
|
45
|
+
* edge rather than on the text block's. No CSS rule here:
|
|
46
|
+
* the class is a marker both ENGINES implement (see
|
|
47
|
+
* engine/shared/flush.ts), because reaching the paper
|
|
48
|
+
* requires freeing that page's margin — per page — and
|
|
49
|
+
* relocating the furniture that lived in it, neither of
|
|
50
|
+
* which a stylesheet can do. Inert without .gp-pin + an
|
|
51
|
+
* edge word, and inert under plain markdown-it.
|
|
44
52
|
* .gp-bleed — forces its own page (break-before) and spans it
|
|
45
53
|
* edge-to-edge horizontally. This does NOT cancel the
|
|
46
54
|
* top/bottom margins, extend past the trim into printer
|
|
@@ -122,4 +130,4 @@
|
|
|
122
130
|
* carrying gp-left could start shifting; that is standards-tracking per
|
|
123
131
|
* CLAUDE.md ("Chrome wins once it ships"), not a bug in the author's book.
|
|
124
132
|
*/
|
|
125
|
-
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/* 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), the same mechanism
|
|
133
|
+
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";
|
|
@@ -10,6 +10,56 @@ export default function plugin(md: any, pluginOptions?: {}): void;
|
|
|
10
10
|
* on its own page instead of resolving against the document canvas and
|
|
11
11
|
* painting on the last page of the book.
|
|
12
12
|
*
|
|
13
|
+
* `min-height: var(--gp-content-h)` is the other half of that contract: a
|
|
14
|
+
* page root that only shrink-wraps its text is a containing block whose
|
|
15
|
+
* `bottom` edge is the end of the PROSE, so `.gp-pin .gp-bottom` (and every
|
|
16
|
+
* hand-written `position: absolute; bottom: 0`) lands under the last
|
|
17
|
+
* paragraph instead of at the page foot. Paged.js used to supply this height
|
|
18
|
+
* for free — `.pagedjs_pagebox > .pagedjs_area > .pagedjs_page_content > div
|
|
19
|
+
* { height: inherit; }` stretched the page root to the page area — and
|
|
20
|
+
* deleting the polyfill (native-only migration, Phase 6) silently took
|
|
21
|
+
* page-boundary pinning with it. Both renderers now publish the page
|
|
22
|
+
* CONTENT height as `--gp-content-h` for the page context the element is in
|
|
23
|
+
* (the viewer on each `.gp-strip`; the compiler on `:root` plus every
|
|
24
|
+
* `page:` assignment selector), and custom properties inherit, so this one
|
|
25
|
+
* rule reaches a page root at any wrapper depth. Undefined var (plain
|
|
26
|
+
* markdown-it use, no engine) falls back to `1px`, which the cushion below
|
|
27
|
+
* takes back to zero — the rule is inert outside the pipeline, leaving the
|
|
28
|
+
* page root shrink-wrapped exactly as it is today.
|
|
29
|
+
*
|
|
30
|
+
* It is `min-height`, not `height`: a `.page` whose content runs past one
|
|
31
|
+
* sheet must still fragment normally. The whole rule is `:where()`, so an
|
|
32
|
+
* author who wants any of it back sets their own value at any specificity.
|
|
33
|
+
*
|
|
34
|
+
* `box-sizing` and `display` are what make a page-sized min-height safe, and
|
|
35
|
+
* both were found by MEASURING a real book (docs/fixtures/css-authoring-spike)
|
|
36
|
+
* print one extra sheet without them:
|
|
37
|
+
*
|
|
38
|
+
* - `border-box`, because a page root with padding (that fixture's cover
|
|
39
|
+
* has `padding-top: 2.5in`) would otherwise be one content box PLUS its
|
|
40
|
+
* padding tall, which is one sheet plus 2.5in — the overflow lands on a
|
|
41
|
+
* spurious blank sheet.
|
|
42
|
+
* - `flow-root`, because a child margin collapsing THROUGH the root's edge
|
|
43
|
+
* survives the forced break and eats into the new sheet: that fixture's
|
|
44
|
+
* last page opens with an `<h2>` whose 22.4px margin-top pushed a
|
|
45
|
+
* 719px-tall root down 22.4px on a 720px sheet, spilling it. A page root
|
|
46
|
+
* is a page; containing its children's margins is what a page does. The
|
|
47
|
+
* viewer already forces `flow-root` on displaced page roots for exactly
|
|
48
|
+
* this reason (`stabilizeFullHeightPageRoots`, fragment.ts) — declaring
|
|
49
|
+
* it once in core means both renderers start from the same box. Zero
|
|
50
|
+
* specificity, so an authored `display: flex`/`grid` page root still
|
|
51
|
+
* wins, which is the case that function's comment warns about.
|
|
52
|
+
*
|
|
53
|
+
* The `- 1px` is the fragmentation cushion, and it is load-bearing. A box
|
|
54
|
+
* whose bottom edge lands EXACTLY on the fragmentainer's edge costs an empty
|
|
55
|
+
* fragment: MEASURED (Chromium 148, 384x480px sheet, 24px margins, one
|
|
56
|
+
* short page root) `min-height: 432px` prints 2 sheets with the second
|
|
57
|
+
* blank while `431px` prints 1 — and the viewer's multicol strip does the
|
|
58
|
+
* same, which is how it first showed up (an 8pp preview of a 7pp book on
|
|
59
|
+
* the gp-image-positioning fixture). One px at 96dpi is 0.75pt: it moves a
|
|
60
|
+
* pinned foot up by less than a point and absorbs sub-pixel rounding in the
|
|
61
|
+
* published value. Do not "clean it up" — the sheet it costs is silent.
|
|
62
|
+
*
|
|
13
63
|
* The break/orphan rules below (`break-after` on headings, image sizing,
|
|
14
64
|
* first-child glue) are all `:where()` so they carry zero specificity —
|
|
15
65
|
* author CSS at any specificity wins outright, reusing this same
|
|
@@ -29,4 +79,4 @@ export default function plugin(md: any, pluginOptions?: {}): void;
|
|
|
29
79
|
* `text-align: center` still centers it — `display: block` would not).
|
|
30
80
|
*
|
|
31
81
|
*/
|
|
32
|
-
export const MARKER_CSS: "\n/* The UA default of 8px body margin is a screen affordance with no meaning\n in paged media, and engines disagree about it: a polyfill that treats the\n page div as the page box drops it, native print keeps it. Left in place it\n insets EVERY page's content by 8px per side, and -- measured, 300dpi,\n 6x4in sheet -- it is what stops a full-width block from reaching the\n paper: it lands at 0.080..5.917in of a 6in sheet instead of\n 0.000..6.000in, because width:100% resolves against the BODY content box,\n not the page's. Zeroing it here (first in the cascade) makes the two\n agree. Authors who want a body margin still just declare one. */\nbody { margin: 0; }\n\n.gp-page-break { break-before: page; }\n.page { break-before: page; }\n.spread { break-before: page; }\n:where(.page, .spread) { position: relative; }\n.gp-column-break { break-after: column; height: 0; font-size: 0; line-height: 0; visibility: hidden; }\n\n:where(h1,h2,h3,h4,h5,h6) { break-after: avoid; }\n:where(img, svg, video) { max-width: 100%; }\n:where(p > img:only-child, figure > img) { width: fit-content; max-width: 100%; height: auto; vertical-align: bottom; }\n:where(.section, figure) > :where(:first-child) { break-before: avoid; }\n\n";
|
|
82
|
+
export const MARKER_CSS: "\n/* The UA default of 8px body margin is a screen affordance with no meaning\n in paged media, and engines disagree about it: a polyfill that treats the\n page div as the page box drops it, native print keeps it. Left in place it\n insets EVERY page's content by 8px per side, and -- measured, 300dpi,\n 6x4in sheet -- it is what stops a full-width block from reaching the\n paper: it lands at 0.080..5.917in of a 6in sheet instead of\n 0.000..6.000in, because width:100% resolves against the BODY content box,\n not the page's. Zeroing it here (first in the cascade) makes the two\n agree. Authors who want a body margin still just declare one. */\nbody { margin: 0; }\n\n.gp-page-break { break-before: page; }\n.page { break-before: page; }\n.spread { break-before: page; }\n:where(.page, .spread) { position: relative; display: flow-root; box-sizing: border-box; min-height: calc(var(--gp-content-h, 1px) - 1px); }\n.gp-column-break { break-after: column; height: 0; font-size: 0; line-height: 0; visibility: hidden; }\n\n:where(h1,h2,h3,h4,h5,h6) { break-after: avoid; }\n:where(img, svg, video) { max-width: 100%; }\n:where(p > img:only-child, figure > img) { width: fit-content; max-width: 100%; height: auto; vertical-align: bottom; }\n:where(.section, figure) > :where(:first-child) { break-before: avoid; }\n\n";
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MANIFEST_FILENAMES,
|
|
3
3
|
runLint
|
|
4
|
-
} from "./cli-
|
|
4
|
+
} from "./cli-5tffrg48.js";
|
|
5
5
|
import {
|
|
6
6
|
log
|
|
7
|
-
} from "./cli-
|
|
7
|
+
} from "./cli-v935c2ws.js";
|
|
8
8
|
import {
|
|
9
9
|
UsageError,
|
|
10
10
|
rejectExtraPositionals,
|
|
11
11
|
rejectUnknownFlags
|
|
12
|
-
} from "./cli-
|
|
13
|
-
import"./cli-
|
|
12
|
+
} from "./cli-7e79d7se.js";
|
|
13
|
+
import"./cli-afm8pa0h.js";
|
|
14
14
|
import"./cli-c41yr7he.js";
|
|
15
15
|
import {
|
|
16
16
|
EXIT_CODES
|
|
@@ -5,16 +5,16 @@ import {
|
|
|
5
5
|
TARGETS,
|
|
6
6
|
TARGET_IDS,
|
|
7
7
|
scaffoldProject
|
|
8
|
-
} from "./cli-
|
|
8
|
+
} from "./cli-5tffrg48.js";
|
|
9
9
|
import {
|
|
10
10
|
resolveGhostscript
|
|
11
|
-
} from "./cli-
|
|
11
|
+
} from "./cli-v935c2ws.js";
|
|
12
12
|
import {
|
|
13
13
|
UsageError,
|
|
14
14
|
rejectExtraPositionals,
|
|
15
15
|
rejectUnknownFlags
|
|
16
|
-
} from "./cli-
|
|
17
|
-
import"./cli-
|
|
16
|
+
} from "./cli-7e79d7se.js";
|
|
17
|
+
import"./cli-afm8pa0h.js";
|
|
18
18
|
import {
|
|
19
19
|
isToolAvailable
|
|
20
20
|
} from "./cli-c41yr7he.js";
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
addNpmPlugin,
|
|
3
3
|
parseNpmPluginSpec
|
|
4
|
-
} from "./cli-
|
|
5
|
-
import"./cli-
|
|
4
|
+
} from "./cli-5tffrg48.js";
|
|
5
|
+
import"./cli-v935c2ws.js";
|
|
6
6
|
import {
|
|
7
7
|
UsageError,
|
|
8
8
|
rejectExtraPositionals,
|
|
9
9
|
rejectUnknownFlags
|
|
10
|
-
} from "./cli-
|
|
11
|
-
import"./cli-
|
|
10
|
+
} from "./cli-7e79d7se.js";
|
|
11
|
+
import"./cli-afm8pa0h.js";
|
|
12
12
|
import"./cli-c41yr7he.js";
|
|
13
13
|
import {
|
|
14
14
|
EXIT_CODES
|