gutterpress 0.11.20-alpha.3 → 0.11.20-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/api/index.js +2 -2
  2. package/dist/{build-a8ycsg80.js → build-8ffhvydf.js} +4 -4
  3. package/dist/{cli-hn9pk0hp.js → cli-553ce1d4.js} +1 -1
  4. package/dist/{cli-y9bcya7r.js → cli-6xym15vm.js} +2 -2
  5. package/dist/{cli-x4p99w8d.js → cli-cyej3p9v.js} +83 -41
  6. package/dist/{cli-9k2tb493.js → cli-sc5t7c74.js} +116 -43
  7. package/dist/cli.js +12 -12
  8. package/dist/{doctor-wbjkrcpc.js → doctor-gyes0hxt.js} +2 -2
  9. package/dist/engine/compiler/build.d.ts +1 -1
  10. package/dist/engine/shared/margin-box-support.d.ts +2 -2
  11. package/dist/engine/shared/pin-edges.d.ts +127 -0
  12. package/dist/engine/shared/print-area.d.ts +73 -0
  13. package/dist/{engine-yxnnp3hx.js → engine-4rkhv5ga.js} +1 -1
  14. package/dist/{engine-fdtv01a2.js → engine-5fw55dfb.js} +2 -2
  15. package/dist/{ext-pesyrs68.js → ext-x61yptd1.js} +4 -4
  16. package/dist/{gutterpress-agent-cazqstr1.js → gutterpress-agent-yvd8w8my.js} +79 -13
  17. package/dist/{gutterpress-viewer-1yse8cx3.js → gutterpress-viewer-w7njnkj7.js} +257 -71
  18. package/dist/{index-tjvrchxj.js → index-bta8hc2q.js} +225 -152
  19. package/dist/{index-zagagnwq.js → index-crys9951.js} +3 -3
  20. package/dist/{index-r87y7wzw.js → index-yw16cfm8.js} +82 -40
  21. package/dist/index.js +3 -3
  22. package/dist/lib/extension-manifest.d.ts +13 -0
  23. package/dist/lib/markdown/assemble.d.ts +13 -3
  24. package/dist/lib/markdown/gutterpress-css.d.ts +49 -33
  25. package/dist/lib/markdown/index.d.ts +5 -0
  26. package/dist/{manifest.schema-c9x8912a.json → manifest.schema-c4wfpg6g.json} +9 -3
  27. package/dist/{new-8yah8z91.js → new-87dqcxtn.js} +4 -4
  28. package/dist/{preflight-8ccx4kdc.js → preflight-nb4wndqf.js} +4 -4
  29. package/dist/preview/file-watcher.d.ts +5 -10
  30. package/dist/{preview-w7v9w36q.js → preview-7bcr5j2p.js} +4 -4
  31. package/dist/{publish-g5nh5myh.js → publish-4qfm5ch9.js} +4 -4
  32. package/dist/render.js +73 -13
  33. package/dist/schema/manifest.types.d.ts +25 -9
  34. package/dist/{validate-1yh59x0x.js → validate-dzht9qxk.js} +4 -4
  35. package/package.json +1 -1
  36. package/dist/engine/shared/flush.d.ts +0 -59
@@ -16,12 +16,12 @@ import {
16
16
  stripAnnotations,
17
17
  transparentPages,
18
18
  warn
19
- } from "./cli-y9bcya7r.js";
19
+ } from "./cli-6xym15vm.js";
20
20
  import {
21
21
  DEBOUNCE,
22
22
  UsageError,
23
23
  resolvePort
24
- } from "./cli-hn9pk0hp.js";
24
+ } from "./cli-553ce1d4.js";
25
25
  import {
26
26
  blackTextToGray
27
27
  } from "./cli-ht1bcqdd.js";
@@ -30,7 +30,7 @@ import {
30
30
  getAssetPath,
31
31
  getAssetsDir,
32
32
  lastPageMode
33
- } from "./cli-x4p99w8d.js";
33
+ } from "./cli-cyej3p9v.js";
34
34
  import {
35
35
  BuildError,
36
36
  EXIT_CODES
@@ -147,7 +147,8 @@ var DTRPG_PRESET = {
147
147
  tolerance: 0.5,
148
148
  bleed: 9,
149
149
  safe: 36,
150
- artSafe: 18
150
+ artSafe: 18,
151
+ gutter: undefined
151
152
  },
152
153
  ink: {
153
154
  maxTac: 240,
@@ -234,7 +235,8 @@ var BOOK_PRESET = {
234
235
  tolerance: 0.5,
235
236
  bleed: undefined,
236
237
  safe: 18,
237
- artSafe: undefined
238
+ artSafe: undefined,
239
+ gutter: undefined
238
240
  },
239
241
  ink: {
240
242
  maxTac: 400,
@@ -592,7 +594,8 @@ function resolveCustomPage(c, m) {
592
594
  tolerance: c.page?.tolerance ?? m.page?.tolerance ?? 0.5,
593
595
  bleed: c.page?.bleed ?? m.page?.bleed,
594
596
  safe: c.page?.safe ?? m.page?.safe,
595
- artSafe: c.page?.artSafe ?? m.page?.artSafe
597
+ artSafe: c.page?.artSafe ?? m.page?.artSafe,
598
+ gutter: c.page?.gutter ?? m.page?.gutter
596
599
  };
597
600
  }
598
601
  function resolveWithPreset(cliOverrides, manifest, preset) {
@@ -701,6 +704,7 @@ function resolveDeclaredStyles(rawStyles, baseDir, subject) {
701
704
  // src/lib/extension-manifest.ts
702
705
  import { readFile as readFile2 } from "node:fs/promises";
703
706
  import path from "node:path";
707
+ import { satisfies, validRange } from "semver";
704
708
  var EXTENSION_MANIFEST_FILENAME = "package.json";
705
709
  var GUTTERPRESS_KEY = "gutterpress";
706
710
  function object(value) {
@@ -724,6 +728,7 @@ function metaFromPackageJson(raw) {
724
728
  ...str(pkg.description) ? { description: str(pkg.description) } : {},
725
729
  ...keywords ? { keywords } : {},
726
730
  ...str(pkg.main) ? { main: str(pkg.main) } : {},
731
+ ...str(object(pkg.engines)?.gutterpress) ? { gutterpressRange: str(object(pkg.engines)?.gutterpress) } : {},
727
732
  ..."preview" in gp ? { preview: str(gp.preview) ?? null } : {},
728
733
  ...Array.isArray(gp.styles) ? { styles: declaredList(gp.styles) } : {},
729
734
  ...str(gp.tokensFile) ? { tokensFile: str(gp.tokensFile) } : {},
@@ -760,6 +765,17 @@ function extensionCarries(meta) {
760
765
  function pathEscapesFolder(rel) {
761
766
  return path.isAbsolute(rel) || rel.split(/[\\/]/).includes("..");
762
767
  }
768
+ function assertSupportsThisGutterpress(meta, ref) {
769
+ const range = meta.gutterpressRange;
770
+ if (range === undefined)
771
+ return;
772
+ if (!validRange(range)) {
773
+ throw new Error(`Extension "${ref}" has an invalid engines.gutterpress range "${range}".`);
774
+ }
775
+ if (!satisfies(PACKAGE_VERSION, range, { includePrerelease: true })) {
776
+ throw new Error(`Extension "${ref}" needs Gutterpress ${range} (its package.json engines.gutterpress); ` + `this is Gutterpress ${PACKAGE_VERSION}. Update Gutterpress, or pin an older version of the extension.`);
777
+ }
778
+ }
763
779
  function assertExtensionContained(meta) {
764
780
  if (meta.engineStyles !== undefined) {
765
781
  throw new Error("The extension's metadata declares `engineStyles`, which was removed — " + "move its entries to the end of `styles`.");
@@ -2029,9 +2045,15 @@ var GUTTERPRESS_CSS = `
2029
2045
 
2030
2046
  /* sizes — AFTER the flow positions so max-width:100% lifts the floats' 50%
2031
2047
  cap at equal specificity */
2032
- .gp-small { width: 25%; max-width: 100%; }
2033
- .gp-medium { width: 50%; max-width: 100%; }
2034
- .gp-large { width: 75%; max-width: 100%; }
2048
+ .gp-size-10 { width: 10%; max-width: 100%; }
2049
+ .gp-size-20 { width: 20%; max-width: 100%; }
2050
+ .gp-size-30 { width: 30%; max-width: 100%; }
2051
+ .gp-size-40 { width: 40%; max-width: 100%; }
2052
+ .gp-size-50 { width: 50%; max-width: 100%; }
2053
+ .gp-size-60 { width: 60%; max-width: 100%; }
2054
+ .gp-size-70 { width: 70%; max-width: 100%; }
2055
+ .gp-size-80 { width: 80%; max-width: 100%; }
2056
+ .gp-size-90 { width: 90%; max-width: 100%; }
2035
2057
 
2036
2058
  /* float clearance presets — consumed by var(--gp-gap) in the float rules
2037
2059
  above and by .gp-shape's shape-margin below; --gp-gap itself is
@@ -2089,10 +2111,16 @@ img.gp-shape {
2089
2111
  }
2090
2112
 
2091
2113
  /* pin — within the nearest positioned ancestor (.page/.spread, rule above).
2092
- inset:0 and the explicit centers are load-bearing; see doctrine comment. */
2114
+ The four insets are the pin box: 0 (the text block's edge) unless an
2115
+ engine set --gp-pin-<edge> on the page root for an edge a pin uses (the
2116
+ art-safe line). Non-auto insets and the explicit centers are
2117
+ load-bearing; see doctrine comment. */
2093
2118
  .gp-pin {
2094
2119
  position: absolute;
2095
- inset: 0;
2120
+ top: var(--gp-pin-top, 0px);
2121
+ right: var(--gp-pin-right, 0px);
2122
+ bottom: var(--gp-pin-bottom, 0px);
2123
+ left: var(--gp-pin-left, 0px);
2096
2124
  align-self: center;
2097
2125
  justify-self: center;
2098
2126
  margin: 0;
@@ -2107,6 +2135,16 @@ img.gp-shape {
2107
2135
  .gp-left { justify-self: start; }
2108
2136
  .gp-right { justify-self: end; }
2109
2137
 
2138
+ /* the escape hatch — the paper's edge, past the printer's safe area: the
2139
+ pin box loses its art-safe insets. Set on the pin itself, so it beats the
2140
+ values the page root hands down. */
2141
+ .gp-pin.gp-flush {
2142
+ --gp-pin-top: 0px;
2143
+ --gp-pin-right: 0px;
2144
+ --gp-pin-bottom: 0px;
2145
+ --gp-pin-left: 0px;
2146
+ }
2147
+
2110
2148
  /* wrapper-margin neutralization (same pattern and rationale as the
2111
2149
  .gp-bleed paragraph-margin note in the doctrine comment; for pin, the
2112
2150
  emptied paragraph would otherwise leave a phantom margin gap in flow) */
@@ -2213,9 +2251,15 @@ var GP_CLASSES = new Set([
2213
2251
  "gp-center",
2214
2252
  "gp-full",
2215
2253
  "gp-bleed",
2216
- "gp-small",
2217
- "gp-medium",
2218
- "gp-large",
2254
+ "gp-size-10",
2255
+ "gp-size-20",
2256
+ "gp-size-30",
2257
+ "gp-size-40",
2258
+ "gp-size-50",
2259
+ "gp-size-60",
2260
+ "gp-size-70",
2261
+ "gp-size-80",
2262
+ "gp-size-90",
2219
2263
  "gp-tight",
2220
2264
  "gp-loose",
2221
2265
  "gp-columns",
@@ -2232,6 +2276,7 @@ var GP_CLASSES = new Set([
2232
2276
  "gp-pin",
2233
2277
  "gp-top",
2234
2278
  "gp-bottom",
2279
+ "gp-flush",
2235
2280
  "gp-behind",
2236
2281
  "gp-base",
2237
2282
  "gp-raised",
@@ -2245,15 +2290,14 @@ var GP_CLASSES = new Set([
2245
2290
  "gp-alert-caution",
2246
2291
  "gp-page-break",
2247
2292
  "gp-column-break",
2248
- "gp-continued",
2249
- "gp-flush"
2293
+ "gp-continued"
2250
2294
  ]);
2251
2295
 
2252
2296
  // src/lib/markdown/gp-pin-scope.js
2253
- function warn3(env, line, type, message, marker) {
2297
+ function warn3(env, line, type, message, severity) {
2254
2298
  if (!env.layoutWarnings)
2255
2299
  env.layoutWarnings = [];
2256
- env.layoutWarnings.push({ line, type, message, marker });
2300
+ env.layoutWarnings.push(severity ? { line, type, message, severity } : { line, type, message });
2257
2301
  }
2258
2302
  function editDistance2(a, b) {
2259
2303
  let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
@@ -2283,6 +2327,21 @@ function hasPinClass(token) {
2283
2327
  const cls = token.attrGet && token.attrGet("class");
2284
2328
  return typeof cls === "string" && cls.split(/\s+/).includes("gp-pin");
2285
2329
  }
2330
+ var PIN_EDGES = ["gp-top", "gp-right", "gp-bottom", "gp-left"];
2331
+ var FLUSH_ART_MSG = "`.gp-flush` puts this image on the paper's edge, past the printer's safe area. Printers trim up to 0.125in there, so keep it for art that is meant to bleed off the page.";
2332
+ function checkFlushArt(token, env, line) {
2333
+ if (token.type !== "image")
2334
+ return;
2335
+ const cls = token.attrGet && token.attrGet("class");
2336
+ if (typeof cls !== "string")
2337
+ return;
2338
+ const classes = cls.split(/\s+/);
2339
+ if (!classes.includes("gp-pin") || !classes.includes("gp-flush"))
2340
+ return;
2341
+ if (!classes.some((c) => PIN_EDGES.includes(c)))
2342
+ return;
2343
+ warn3(env, line, "flush_art", FLUSH_ART_MSG, "info");
2344
+ }
2286
2345
  function elementNameFor(token) {
2287
2346
  switch (token.type) {
2288
2347
  case "layout_chapter_open":
@@ -2342,6 +2401,7 @@ function gpPinScope(md) {
2342
2401
  if (token.type === "inline" && token.children) {
2343
2402
  for (const child of token.children) {
2344
2403
  checkUnknownGpClasses(child, state.env, line);
2404
+ checkFlushArt(child, state.env, line);
2345
2405
  }
2346
2406
  }
2347
2407
  }
@@ -3369,6 +3429,7 @@ This folder still has a ${stale}. Gutterpress no longer reads it — ` + `descri
3369
3429
  async function loadExtensionFromDir(extensionDir, config, pluginRef) {
3370
3430
  const meta = await readExtensionMeta(extensionDir);
3371
3431
  assertExtensionContained(meta);
3432
+ assertSupportsThisGutterpress(meta, pluginRef);
3372
3433
  const resolved = resolveExtension(extensionDir, meta, `Plugin "${pluginRef}"`);
3373
3434
  const extensionStyles = resolved.styles ?? [];
3374
3435
  const name = config.name ?? meta.name ?? pluginRef;
@@ -3443,6 +3504,7 @@ async function loadPlugin(config, baseDir) {
3443
3504
  if (loaded.packageDir) {
3444
3505
  const meta = await readExtensionMeta(loaded.packageDir);
3445
3506
  assertExtensionContained(meta);
3507
+ assertSupportsThisGutterpress(meta, pluginRef);
3446
3508
  const resolved = resolveExtension(loaded.packageDir, { ...meta, main: undefined }, `Plugin "${pluginRef}"`);
3447
3509
  if (resolved.markdown && loaded.entryPath && resolve3(resolved.markdown) !== resolve3(loaded.entryPath)) {
3448
3510
  throw new Error(`its package.json declares gutterpress.markdown "${meta.markdown}" but the package entry is "${relative(loaded.packageDir, loaded.entryPath)}" — an npm extension's ` + "markdown-it plugin is its package entry, so declare that file or drop the field.");
@@ -3566,7 +3628,7 @@ import path3 from "node:path";
3566
3628
  import { Transform } from "node:stream";
3567
3629
  import { pipeline } from "node:stream/promises";
3568
3630
  import { createGunzip } from "node:zlib";
3569
- import { maxSatisfying, satisfies, validRange } from "semver";
3631
+ import { maxSatisfying, satisfies as satisfies2, validRange as validRange2 } from "semver";
3570
3632
 
3571
3633
  // src/lib/fetch-timeout.ts
3572
3634
  class FetchPassthroughError extends Error {
@@ -3807,7 +3869,7 @@ async function selectPackage(ctx, name, selector) {
3807
3869
  version = selector;
3808
3870
  } else if (typeof tags[selector] === "string") {
3809
3871
  version = tags[selector];
3810
- } else if (validRange(selector)) {
3872
+ } else if (validRange2(selector)) {
3811
3873
  version = maxSatisfying(Object.keys(versions), selector) ?? undefined;
3812
3874
  }
3813
3875
  if (!version || !isExactNpmVersion(version)) {
@@ -4025,7 +4087,7 @@ async function installPackage(ctx, name, selector, parentPackagePath, ancestors,
4025
4087
  }
4026
4088
  throw error2;
4027
4089
  }
4028
- const ancestor = ancestors.find((candidate) => candidate.name === name && (candidate.version === selected.version || (selector ? satisfies(candidate.version, selector) : false)));
4090
+ const ancestor = ancestors.find((candidate) => candidate.name === name && (candidate.version === selected.version || (selector ? satisfies2(candidate.version, selector) : false)));
4029
4091
  if (ancestor)
4030
4092
  return { path: ancestor.path };
4031
4093
  if (ctx.records.length >= ctx.limits.totalPackages) {
@@ -4990,11 +5052,11 @@ var inches = (pt) => `${+(pt / 72).toFixed(2)}in`;
4990
5052
  var check3 = {
4991
5053
  id: "pdf.print.safe-margins",
4992
5054
  name: "Safe margins",
4993
- description: "Fails when text sits closer to the trim than `page.safe` (DriveThruRPG: 0.5in on every edge). Text only: art is judged in the preview's safe-area overlay, not here. Skipped when `page.safe` is not set",
5055
+ description: "Fails when text sits closer to the trim than `page.safe` (DriveThruRPG: 0.5in on every edge), or than `page.gutter` at the binding edge where one is set. Text only: art is judged in the preview's safe-area overlay, not here. Skipped when `page.safe` is not set",
4994
5056
  category: "pdf",
4995
5057
  phase: "post-build",
4996
5058
  async run(ctx) {
4997
- const { width, height, bleed = 0, safe = 0 } = ctx.config.page;
5059
+ const { width, height, bleed = 0, safe = 0, gutter = safe } = ctx.config.page;
4998
5060
  if (!ctx.pdfPath || !safe)
4999
5061
  return [];
5000
5062
  const doc = await loadPdf(ctx.pdfPath);
@@ -5032,8 +5094,7 @@ var check3 = {
5032
5094
  ["bottom", Math.min(...ys) - trim.bottom],
5033
5095
  ["top", trim.top - Math.max(...ys)]
5034
5096
  ];
5035
- const [edge, gap] = gaps.reduce((worst, g) => g[1] < worst[1] ? g : worst);
5036
- const shortfall = safe - gap;
5097
+ const [edge, shortfall] = gaps.map(([name, gap]) => [name, (name === "binding" ? gutter : safe) - gap]).reduce((worst, g) => g[1] > worst[1] ? g : worst);
5037
5098
  if (shortfall <= TOLERANCE_PT)
5038
5099
  continue;
5039
5100
  const text = item.str.trim();
@@ -5041,7 +5102,7 @@ var check3 = {
5041
5102
  severity: "error",
5042
5103
  message: `Page ${pageNum}: "${text.slice(0, 40)}${text.length > 40 ? "…" : ""}" is ${inches(shortfall)} too close to the ${edge} edge.`,
5043
5104
  file: ctx.pdfPath,
5044
- detail: `${rule}: text must stay at least ${inches(safe)} inside the trim on every edge.`,
5105
+ detail: gutter === safe ? `${rule}: text must stay at least ${inches(safe)} inside the trim on every edge.` : `${rule}: text must stay at least ${inches(safe)} inside the trim, ${inches(gutter)} at the binding.`,
5045
5106
  code: "text-outside-safe-margin",
5046
5107
  data: { page: pageNum, edge, shortfallPt: +shortfall.toFixed(2) }
5047
5108
  }));
@@ -8885,6 +8946,21 @@ function canonicalChapterId(p) {
8885
8946
  return s;
8886
8947
  }
8887
8948
 
8949
+ // src/engine/shared/print-area.ts
8950
+ function printAreaMeta(page) {
8951
+ const area = JSON.stringify({
8952
+ width: page.width,
8953
+ height: page.height,
8954
+ bleed: page.bleed ?? 0,
8955
+ safe: page.safe ?? 0,
8956
+ artSafe: page.artSafe,
8957
+ gutter: page.gutter,
8958
+ preset: page.preset,
8959
+ spec: page.spec
8960
+ });
8961
+ return `<meta name="gp-print-area" content='${area.replace(/&/g, "&amp;").replace(/'/g, "&#39;")}'>`;
8962
+ }
8963
+
8888
8964
  // src/lib/markdown/assemble.ts
8889
8965
  async function assembleBookHtml(opts) {
8890
8966
  const title = opts.title ?? "Document";
@@ -8950,7 +9026,8 @@ ${projectCss.trim()}` : null
8950
9026
  <html lang="en"${printAttrs}>
8951
9027
  <head>
8952
9028
  <meta charset="UTF-8">
8953
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
9029
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">${opts.printArea ? `
9030
+ ${printAreaMeta(opts.printArea)}` : ""}
8954
9031
  <title>${title}</title>${preloadTags}
8955
9032
  <style data-project-css>
8956
9033
  ${inlineCss}
@@ -9010,6 +9087,7 @@ ${css.trim()}
9010
9087
  preloadImages: cssAssetCopies.map((c) => c.to),
9011
9088
  title: opts.title,
9012
9089
  print: opts.print,
9090
+ printArea: opts.printArea,
9013
9091
  plugins: opts.plugins,
9014
9092
  pluginCss,
9015
9093
  annotateSourceChapters: opts.annotateSourceChapters,
@@ -9022,6 +9100,7 @@ async function renderChaptersToFile(inputDir, outDir, opts = {}) {
9022
9100
  const html = await renderChapters(inputDir, {
9023
9101
  title: opts.title,
9024
9102
  print: opts.print,
9103
+ printArea: opts.printArea,
9025
9104
  styles: opts.styles,
9026
9105
  files: opts.files,
9027
9106
  plugins: opts.plugins,
@@ -9899,6 +9978,7 @@ async function renderBook(ctx) {
9899
9978
  const htmlFile = await renderChaptersToFile(renderDir, workDir, {
9900
9979
  title: config.title,
9901
9980
  print: config.print,
9981
+ printArea: { ...config.page, preset: config.preset, spec: config.spec },
9902
9982
  styles: config.styles,
9903
9983
  files: config.source.files,
9904
9984
  plugins,
@@ -10051,7 +10131,7 @@ class PdfOutput {
10051
10131
  const rawPdf = pdfxMode ? path13.join(stage, "raw.pdf") : path13.resolve(pdfFile);
10052
10132
  await fsp2.mkdir(path13.dirname(path13.resolve(pdfFile)), { recursive: true });
10053
10133
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
10054
- const { buildNativePdf } = await import("./engine-yxnnp3hx.js");
10134
+ const { buildNativePdf } = await import("./engine-4rkhv5ga.js");
10055
10135
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
10056
10136
  title: config.title,
10057
10137
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
@@ -10354,6 +10434,7 @@ async function renderPreviewBook(inputPath, config, opts) {
10354
10434
  return renderChapters(inputPath, {
10355
10435
  title: config.title ?? "Document",
10356
10436
  print: config.print,
10437
+ printArea: config.printArea,
10357
10438
  styles: config.styles,
10358
10439
  files: opts.files,
10359
10440
  plugins,
@@ -10368,19 +10449,8 @@ async function renderPreviewBook(inputPath, config, opts) {
10368
10449
  }
10369
10450
  });
10370
10451
  }
10371
- function injectPreviewScripts(html, page) {
10372
- const area = page ? JSON.stringify({
10373
- width: page.width,
10374
- height: page.height,
10375
- bleed: page.bleed ?? 0,
10376
- safe: page.safe ?? 0,
10377
- artSafe: page.artSafe,
10378
- preset: page.preset,
10379
- spec: page.spec
10380
- }) : "";
10381
- const meta = area ? ` <meta name="gp-print-area" content='${area.replace(/&/g, "&amp;").replace(/'/g, "&#39;")}'>
10382
- ` : "";
10383
- const scripts = meta + ` <script src="/engine/gutterpress-viewer.js"></script>
10452
+ function injectPreviewScripts(html) {
10453
+ const scripts = ` <script src="/engine/gutterpress-viewer.js"></script>
10384
10454
  ` + `<script src="/preview/scripts/preview-interface.js"></script>
10385
10455
  ` + `<script src="/preview/scripts/preview-bridge.js"></script>
10386
10456
  `;
@@ -10395,7 +10465,10 @@ async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets, image
10395
10465
  }
10396
10466
  const nextAssets = new Map;
10397
10467
  const nextImageRefs = new Set;
10398
- const html = await renderPreviewBook(inputPath, config, {
10468
+ const html = await renderPreviewBook(inputPath, {
10469
+ ...config,
10470
+ printArea: config.page && { ...config.page, preset: config.preset, spec: config.spec }
10471
+ }, {
10399
10472
  files: config.source?.files ?? null,
10400
10473
  onCssAssets: (copies) => {
10401
10474
  for (const copy of copies)
@@ -10414,7 +10487,7 @@ async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets, image
10414
10487
  for (const ref of nextImageRefs)
10415
10488
  imageRefs.add(ref);
10416
10489
  }
10417
- await fsp3.writeFile(path15.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html, config.page && { ...config.page, preset: config.preset, spec: config.spec }), "utf-8");
10490
+ await fsp3.writeFile(path15.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html), "utf-8");
10418
10491
  }
10419
10492
  function isDotPathUnderRoot(candidatePath, root) {
10420
10493
  const normalizedRoot = root.replace(/\\/g, "/").replace(/\/+$/, "");
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  UsageError,
7
7
  package_default,
8
8
  rejectUnknownFlags
9
- } from "./cli-hn9pk0hp.js";
9
+ } from "./cli-553ce1d4.js";
10
10
  import {
11
11
  EXIT_CODES
12
12
  } from "./cli-46ycxe6r.js";
@@ -18,14 +18,14 @@ import {
18
18
  import { defineCommand, parseArgs, runMain } from "citty";
19
19
  import { statSync } from "node:fs";
20
20
  var SUBCOMMANDS = {
21
- new: () => import("./new-8yah8z91.js").then((m) => m.default),
22
- preview: () => import("./preview-w7v9w36q.js").then((m) => m.default),
23
- build: () => import("./build-a8ycsg80.js").then((m) => m.default),
24
- publish: () => import("./publish-g5nh5myh.js").then((m) => m.default),
25
- validate: () => import("./validate-1yh59x0x.js").then((m) => m.default),
26
- preflight: () => import("./preflight-8ccx4kdc.js").then((m) => m.default),
27
- doctor: () => import("./doctor-wbjkrcpc.js").then((m) => m.default),
28
- ext: () => import("./ext-pesyrs68.js").then((m) => m.default)
21
+ new: () => import("./new-87dqcxtn.js").then((m) => m.default),
22
+ preview: () => import("./preview-7bcr5j2p.js").then((m) => m.default),
23
+ build: () => import("./build-8ffhvydf.js").then((m) => m.default),
24
+ publish: () => import("./publish-4qfm5ch9.js").then((m) => m.default),
25
+ validate: () => import("./validate-dzht9qxk.js").then((m) => m.default),
26
+ preflight: () => import("./preflight-nb4wndqf.js").then((m) => m.default),
27
+ doctor: () => import("./doctor-gyes0hxt.js").then((m) => m.default),
28
+ ext: () => import("./ext-x61yptd1.js").then((m) => m.default)
29
29
  };
30
30
  var VERSION = package_default.version;
31
31
  var main = defineCommand({
@@ -50,7 +50,7 @@ async function preflightRequiredInvocations(rawArgs) {
50
50
  const [command, ...commandArgs] = rawArgs;
51
51
  try {
52
52
  if (command === "new") {
53
- const { newArgs } = await import("./new-8yah8z91.js");
53
+ const { newArgs } = await import("./new-87dqcxtn.js");
54
54
  rejectUnknownFlags(commandArgs, newArgs, "new");
55
55
  const parsed = parseArgs(commandArgs, {
56
56
  ...newArgs,
@@ -62,7 +62,7 @@ async function preflightRequiredInvocations(rawArgs) {
62
62
  return;
63
63
  }
64
64
  if (command === "preflight") {
65
- const { preflightArgs } = await import("./preflight-8ccx4kdc.js");
65
+ const { preflightArgs } = await import("./preflight-nb4wndqf.js");
66
66
  rejectUnknownFlags(commandArgs, preflightArgs, "preflight");
67
67
  const parsed = parseArgs(commandArgs, {
68
68
  ...preflightArgs,
@@ -82,7 +82,7 @@ async function preflightRequiredInvocations(rawArgs) {
82
82
  }
83
83
  if (subcommand.startsWith("-"))
84
84
  return;
85
- const { EXT_SUBCOMMANDS, extAddArgs, extRemoveArgs, extEnableArgs, extDisableArgs } = await import("./ext-pesyrs68.js");
85
+ const { EXT_SUBCOMMANDS, extAddArgs, extRemoveArgs, extEnableArgs, extDisableArgs } = await import("./ext-x61yptd1.js");
86
86
  if (!EXT_SUBCOMMANDS.includes(subcommand)) {
87
87
  throw new UsageError(`gutterpress ext: unknown command "${subcommand}"`);
88
88
  }
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  getSystemDiagnostics,
3
3
  log
4
- } from "./cli-y9bcya7r.js";
4
+ } from "./cli-6xym15vm.js";
5
5
  import {
6
6
  UsageError,
7
7
  rejectExtraPositionals,
8
8
  rejectUnknownFlags
9
- } from "./cli-hn9pk0hp.js";
9
+ } from "./cli-553ce1d4.js";
10
10
  import"./cli-46ycxe6r.js";
11
11
  import"./cli-asap4t1z.js";
12
12
  import"./cli-37x76zdn.js";
@@ -8,7 +8,7 @@ import { type PostprocessResult } from "./postprocess.ts";
8
8
  * set is closed here so a surface's label table can be asserted complete
9
9
  * against it rather than drifting silently as checks are added.
10
10
  */
11
- export type BuildDiagnosticCode = "engine.width.overflow" | "engine.width.intrinsic" | "engine.xref.broken" | "engine.abspos.leak" | "engine.layer.trapped" | "engine.multicol.dead-column" | "engine.content.overheight" | "engine.image.low-dpi" | "engine.flush.margin-box" | "engine.page-background.unreferenced" | "engine.hyphenation.unsupported";
11
+ export type BuildDiagnosticCode = "engine.width.overflow" | "engine.width.intrinsic" | "engine.xref.broken" | "engine.abspos.leak" | "engine.layer.trapped" | "engine.multicol.dead-column" | "engine.content.overheight" | "engine.image.low-dpi" | "engine.pin.margin-box" | "engine.page-background.unreferenced" | "engine.hyphenation.unsupported";
12
12
  export declare const BUILD_DIAGNOSTIC_CODES: readonly BuildDiagnosticCode[];
13
13
  export interface BuildDiagnostic {
14
14
  code: BuildDiagnosticCode;
@@ -31,8 +31,8 @@ export declare function isIgnoredMarginBoxProperty(property: string): boolean;
31
31
  * Geometry of each of the 16 margin boxes, per CSS Paged Media §5.3, in pt
32
32
  * SHEET coordinates (origin at the sheet's top-left). One implementation for
33
33
  * both furniture painters: the viewer's `drawMarginBoxes` overlays and the
34
- * compiler's `.gp-flush` furniture relocation must place a box at the same
35
- * spot or preview and print disagree about where the folio sits.
34
+ * compiler's relocation of a pinned edge's furniture must place a box at the
35
+ * same spot or preview and print disagree about where the folio sits.
36
36
  */
37
37
  export declare function marginBoxRectPt(name: string, g: {
38
38
  width: number;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Pinned-art policy — shared by the compiler and the viewer, like every rule
3
+ * that DECIDES something (see synthesis.ts's header).
4
+ *
5
+ * THE MODEL: a pin box the text never shares. A `.gp-pin` with an edge word
6
+ * (`.gp-top`, `.gp-right`, `.gp-bottom`, `.gp-left`) positions itself inside
7
+ * its page root (`.page`/`.spread`, `position: relative` in MARKER_CSS), and
8
+ * where it sits has no effect on the text:
9
+ *
10
+ * 1. The page MARGIN on each pinned edge is freed. Chromium's printable area
11
+ * IS the page area — MEASURED, a pinned box pulled into the margin with
12
+ * negative insets fragments onto the NEXT sheet, one moved there with a
13
+ * transform is clipped away entirely, and a margin box in a ~zero margin
14
+ * (1px, with padding/height compensation) simply does not render. So the
15
+ * only way art reaches the paper is page GEOMETRY, per page:
16
+ * - the COMPILER aliases the root's own page context under a generated
17
+ * name (verbatim rule copies — never a resolved flatten, which is the
18
+ * "re-implement the @page cascade" trap tier2's history records),
19
+ * zeroes the pinned margins on the alias, and assigns the root to it;
20
+ * - the VIEWER keeps the author's page context and frees the margins in
21
+ * the run's strip geometry, from these same functions.
22
+ * 2. The root gets that same margin back as PADDING (`box-decoration-break:
23
+ * clone`, so every fragment of a root that spans pages keeps it), added
24
+ * to any padding the root has of its own. Its content box is therefore
25
+ * the author's content box — same width, same height, same place, same
26
+ * line and page breaks — whatever is pinned where. That is the whole
27
+ * point: freeing the margin once let the text grow into it. (What the
28
+ * root paints itself — a background, a border — does grow with it: the
29
+ * root's box is the page area, and that area reaches the paper.)
30
+ * 3. The pin box is the root's padding box — which now reaches the paper on
31
+ * the pinned edges — inset by the printer's ART-SAFE line: `--gp-pin-<edge>`
32
+ * on the root, read by `.gp-pin`'s four insets (GUTTERPRESS_CSS). On the
33
+ * top, bottom and outside edge that is the bleed plus `page.artSafe`; at
34
+ * the binding edge `page.gutter` (no bleed there) — the manifest's print
35
+ * area (print-area.ts), so a book with none sets 0, the paper's edge.
36
+ * 4. `.gp-flush` on the pin zeroes the four properties in plain CSS: the
37
+ * opt-in escape hatch to the paper's edge (the markdown pipeline notes it).
38
+ * Nothing else here knows about it.
39
+ *
40
+ * Sides. Recto values are the defaults (`@page :right`'s margins, the
41
+ * binding on the left). Mirrored `:left`/`:right` margins agree on the content
42
+ * SIZE, so the viewer paginates identically either way; each engine corrects a
43
+ * verso root once it knows the side — the viewer after it measures its
44
+ * strips, the compiler in its synthesis step (`setPinGeometry`).
45
+ *
46
+ * Margin boxes live IN the margin, so a freed edge's furniture (folio, running
47
+ * head) cannot render natively there — MEASURED, no compensation trick
48
+ * survives. Both renderers therefore keep painting that furniture themselves
49
+ * at its ORIGINAL coordinates: the viewer already synthesizes all furniture as
50
+ * DOM; the compiler injects the freed edge's boxes into the page root as
51
+ * positioned elements with engine-resolved text. Nothing the author declared
52
+ * is lost — that is the contract this module's box map exists to keep.
53
+ *
54
+ * Everything the author's document gets is standard CSS: a padding, a custom
55
+ * property per edge, absolute insets, and (print only) a named page.
56
+ */
57
+ import type { PageGeometry } from "./gcpm-extract.ts";
58
+ import type { PageSide, PrintArea } from "./print-area.ts";
59
+ export declare const PIN_EDGES: readonly ["top", "right", "bottom", "left"];
60
+ export type PinEdge = (typeof PIN_EDGES)[number];
61
+ /** Every pin that names an edge. */
62
+ export declare const EDGE_PIN_SELECTOR: string;
63
+ /** Minimal DOM shapes `pinEdgesIn` needs — keeps this module node-safe. */
64
+ interface PinElement {
65
+ /** an HTML element's containing block for abspos (`HTMLElement.offsetParent`) */
66
+ readonly offsetParent?: unknown;
67
+ readonly classList: {
68
+ contains(token: string): boolean;
69
+ };
70
+ }
71
+ interface QueryRoot {
72
+ querySelectorAll(selector: string): ArrayLike<PinElement>;
73
+ }
74
+ /**
75
+ * The edges the pins of page root `root` use, in canonical t/r/b/l order — a
76
+ * pin counts when `root` is its containing block (its `offsetParent`): a pin
77
+ * in a nested page root belongs to that root, and one a positioned card
78
+ * frames belongs to the card, so neither frees this root's margins. A pin
79
+ * without an edge word frees nothing and centres in the text block.
80
+ */
81
+ export declare function pinEdgesIn(root: QueryRoot): PinEdge[];
82
+ /** Canonical short key for an edge set: "b", "rb", "trbl"… (t/r/b/l order). */
83
+ export declare function pinKey(edges: readonly PinEdge[]): string;
84
+ /**
85
+ * Generated page name for (author page context, edge set). The `gp--` double
86
+ * dash marks an engine-generated page (the `gp--blank` convention); these
87
+ * exist only in builds that actually contain an edge pin, which is why the
88
+ * width check may treat them as real author contexts.
89
+ */
90
+ export declare function pinPageName(authorPage: string | undefined, edges: readonly PinEdge[]): string;
91
+ /** The page's margins with the pinned edges freed. */
92
+ export declare function flushMargins(margin: PageGeometry["margin"], edges: readonly PinEdge[]): PageGeometry["margin"];
93
+ /**
94
+ * The art-safe line's distance from the sheet edge on each pinned edge, in pt:
95
+ * the bleed plus `artSafe` on the top, bottom and outside edge, `gutter` on the
96
+ * binding edge (the left of a recto, the right of a verso), 0 on an edge the
97
+ * pins do not use (the pin box's edge is the text block's there). No print
98
+ * area means 0 everywhere: the paper's edge.
99
+ */
100
+ export declare function pinInsets(area: Pick<PrintArea, "bleed" | "artSafe" | "gutter"> | null, side: PageSide, edges: readonly PinEdge[]): Record<PinEdge, number>;
101
+ /** No padding of the root's own. */
102
+ export declare const NO_PADDING: Readonly<Record<PinEdge, number>>;
103
+ /**
104
+ * A pin root's own declarations for one side, in px (pt × 96/72, like the
105
+ * strip variables): the freed margins back as padding, cloned onto every
106
+ * fragment, and the four inset properties. `margin` is the ORIGINAL margin of
107
+ * the page context the root lands on; `padding` is the root's own (author)
108
+ * padding in px, which the freed margin is added to rather than replacing,
109
+ * so a padded root's text stays put too. All four properties are set, so a
110
+ * pin never reads another root's values.
111
+ */
112
+ export declare function pinRootStyle(margin: PageGeometry["margin"], area: Pick<PrintArea, "bleed" | "artSafe" | "gutter"> | null, side: PageSide, edges: readonly PinEdge[], padding?: Readonly<Record<PinEdge, number>>): Record<string, string>;
113
+ /** A computed style's four paddings, in px. */
114
+ export declare function paddingOf(cs: {
115
+ paddingTop: string;
116
+ paddingRight: string;
117
+ paddingBottom: string;
118
+ paddingLeft: string;
119
+ }): Record<PinEdge, number>;
120
+ /**
121
+ * Margin boxes that live (wholly or partly) in a freed edge's margin area —
122
+ * the set both renderers must keep painting themselves. A corner box belongs
123
+ * to BOTH of its edges: `bottom-left-corner` dies when either the bottom or
124
+ * the left margin is freed.
125
+ */
126
+ export declare function marginBoxesOnEdges(edges: readonly PinEdge[]): string[];
127
+ export {};