gutterpress 0.10.0-alpha.4 → 0.10.0

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 (38) hide show
  1. package/README.md +2 -0
  2. package/dist/api/index.js +2 -2
  3. package/dist/{audit-k1vnfwvc.js → audit-cvarpa72.js} +4 -4
  4. package/dist/{build-hqmwgvdw.js → build-6r502chw.js} +6 -4
  5. package/dist/{cli-0r0tq16s.js → cli-149edp6b.js} +1 -1
  6. package/dist/{cli-ra0ed2xt.js → cli-cqtggsng.js} +1 -1
  7. package/dist/{cli-n25qycwz.js → cli-eq5naw4m.js} +222 -141
  8. package/dist/{cli-revgt4pr.js → cli-yzf38679.js} +1 -0
  9. package/dist/{cli-hp9r2pzt.js → cli-zfcryxg8.js} +327 -24
  10. package/dist/cli.js +16 -16
  11. package/dist/{doctor-dxms7ehm.js → doctor-akvxbtjb.js} +2 -2
  12. package/dist/engine/compiler/build.d.ts +1 -1
  13. package/dist/engine/shared/flush.d.ts +59 -0
  14. package/dist/engine/shared/margin-box-support.d.ts +24 -0
  15. package/dist/{engine-z4p9sr4h.js → engine-b159tbns.js} +2 -2
  16. package/dist/{engine-wa7y9av9.js → engine-ft4cr3ep.js} +1 -1
  17. package/dist/{gutterpress-agent-1ctgfz92.js → gutterpress-agent-cazqstr1.js} +49 -1
  18. package/dist/{gutterpress-viewer-cem7dmr5.js → gutterpress-viewer-te8g5grx.js} +191 -71
  19. package/dist/{index-05y3dnxq.js → index-ge7q9xj3.js} +330 -249
  20. package/dist/{index-xxg4zfrg.js → index-wq3r5pj7.js} +287 -23
  21. package/dist/{index-9tyq9kks.js → index-ycpvr0am.js} +47 -4
  22. package/dist/index.js +3 -3
  23. package/dist/lib/build-runner.d.ts +7 -0
  24. package/dist/lib/build-staging.d.ts +51 -0
  25. package/dist/lib/engine.d.ts +9 -0
  26. package/dist/lib/markdown/gutterpress-css.d.ts +9 -1
  27. package/dist/lib/markdown/markers.d.ts +51 -1
  28. package/dist/{lint-xjwm5ep8.js → lint-p2sw53d9.js} +4 -4
  29. package/dist/{new-kwdwpf0j.js → new-hvq0x91q.js} +4 -4
  30. package/dist/{plugin-rg4tnn96.js → plugin-pssmk0dx.js} +4 -4
  31. package/dist/{preflight-3127y25z.js → preflight-4j00yd0g.js} +4 -4
  32. package/dist/{preview-ncgfhqmw.js → preview-d4s6gk6p.js} +6 -5
  33. package/dist/{preview-interface-435cczt5.js → preview-interface-0ssk8bmm.js} +153 -6
  34. package/dist/{publish-pr0rwh6p.js → publish-tkc26en7.js} +4 -4
  35. package/dist/render.js +83 -7
  36. package/dist/{repair-8270smfw.js → repair-2va4w12t.js} +4 -4
  37. package/dist/{validate-54e17rae.js → validate-w9vfefrw.js} +4 -4
  38. package/package.json +1 -1
@@ -2,6 +2,7 @@ import {
2
2
  BuildError,
3
3
  EXIT_CODES,
4
4
  INSTALL_HINTS,
5
+ MARGIN_BOX_IGNORED_PROPERTIES,
5
6
  RENDER_TIMEOUT_MS,
6
7
  REQUIRED_MILESTONE,
7
8
  closeBrowser,
@@ -16,7 +17,7 @@ import {
16
17
  resolveChromiumExecutable,
17
18
  run,
18
19
  spawnCapture
19
- } from "./index-9tyq9kks.js";
20
+ } from "./index-ycpvr0am.js";
20
21
  import {
21
22
  gitDirFor,
22
23
  gitScopeFor,
@@ -731,6 +732,9 @@ function warn(env, line, type, message, marker) {
731
732
  env.layoutWarnings = [];
732
733
  env.layoutWarnings.push({ line, type, message, marker });
733
734
  }
735
+ function proseEscapeHint(kind) {
736
+ return ` If this line is prose that happens to begin with "@${kind}" — a wrapped sentence, for` + ` instance — escape it as \\@${kind} or write it as \`@${kind}\` in backticks, and it stays text.`;
737
+ }
734
738
  function addClasses(token, baseClass, extraClass) {
735
739
  const cls = [];
736
740
  if (baseClass)
@@ -792,14 +796,14 @@ function plugin(md, pluginOptions = {}) {
792
796
  const unknown = parsed.__unknownTokens;
793
797
  delete parsed.__unknownTokens;
794
798
  for (const t of unknown) {
795
- warn(state.env, startLine + 1, "unrecognized_marker_token", `@${parsed.kind}: "${t}" is not something a marker understands, so it was kept verbatim as a class name. Write a class as .my-class (or {.my-class}), an id as #my-id, and anything else as key=value.`, parsed);
799
+ warn(state.env, startLine + 1, "unrecognized_marker_token", `@${parsed.kind}: "${t}" is not something a marker understands, so it was kept verbatim as a class name. Write a class as .my-class (or {.my-class}), an id as #my-id, and anything else as key=value.` + proseEscapeHint(parsed.kind), parsed);
796
800
  }
797
801
  }
798
802
  if (parsed.__extraBareTokens) {
799
803
  const { tokens, named } = parsed.__extraBareTokens;
800
804
  delete parsed.__extraBareTokens;
801
805
  const list = tokens.map((t) => `"${t}"`).join(", ");
802
- warn(state.env, startLine + 1, "extra_bare_marker_token", named ? `@${parsed.kind}: only the first plain word (${list.split(", ")[0]}) is used as the name; the rest (${tokens.slice(1).map((t) => `"${t}"`).join(", ")}) became class names instead. Quote a name that contains spaces ("${tokens.join(" ")}"), or write classes as .${tokens.slice(1).join(" .")}.` : `@${parsed.kind}: ${list} are several plain words, but a marker has only one name slot — so NONE of them was used as the name and they all became class names instead. Quote a name that contains spaces ("${tokens.join(" ")}"), or write classes as .${tokens.join(" .")}.`, parsed);
806
+ warn(state.env, startLine + 1, "extra_bare_marker_token", named ? `@${parsed.kind}: only the first plain word (${list.split(", ")[0]}) is used as the name; the rest (${tokens.slice(1).map((t) => `"${t}"`).join(", ")}) became class names instead. Quote a name that contains spaces ("${tokens.join(" ")}"), or write classes as .${tokens.slice(1).join(" .")}.` : `@${parsed.kind}: ${list} are several plain words, but a marker has only one name slot — so NONE of them was used as the name and they all became class names instead. Quote a name that contains spaces ("${tokens.join(" ")}"), or write classes as .${tokens.join(" .")}.` + proseEscapeHint(parsed.kind), parsed);
803
807
  }
804
808
  state.line = startLine + 1;
805
809
  return true;
@@ -891,7 +895,12 @@ function plugin(md, pluginOptions = {}) {
891
895
  t.meta = { line: meta.__line };
892
896
  addClasses(t, "spread", meta.attrs && meta.attrs.class ? meta.attrs.class : "");
893
897
  attachDataAttrs(t, "spread", meta.name, meta.attrs || {});
894
- stack.open({ kind: "spread", noPagesYet: true, sawAnyPage: false }, t);
898
+ stack.open({
899
+ kind: "spread",
900
+ classes: meta.attrs && meta.attrs.class || "",
901
+ noPagesYet: true,
902
+ sawAnyPage: false
903
+ }, t);
895
904
  }
896
905
  function openPage(meta) {
897
906
  const chapter = stack.get("chapter");
@@ -906,7 +915,7 @@ function plugin(md, pluginOptions = {}) {
906
915
  const label = chapter ? chapter.label : "";
907
916
  if (label)
908
917
  t.attrSet("data-chapter-label", label);
909
- stack.open({ kind: "page" }, t);
918
+ stack.open({ kind: "page", classes: explicit }, t);
910
919
  if (label && !chapter.openerEmitted) {
911
920
  const opener = new state.Token("html_block", "", 0);
912
921
  opener.content = `<div class="chapter-opener" data-chapter-label="${escapeAttr(label)}">${escapeHtml(label)}</div>
@@ -929,13 +938,51 @@ function plugin(md, pluginOptions = {}) {
929
938
  t.meta = { hasColumnBreak: false, line: meta.__line };
930
939
  stack.open({
931
940
  kind: "section",
941
+ classes: meta.attrs && meta.attrs.class || "",
942
+ sawContent: false,
932
943
  meta: { name: meta.name || null, attrs: { ...meta.attrs || {} } },
933
944
  openToken: t
934
945
  }, t);
935
946
  }
947
+ function breakHost() {
948
+ return stack.get("section") || stack.get("page") || stack.get("spread");
949
+ }
950
+ function gridClassOf(frame) {
951
+ if (!frame || !frame.classes)
952
+ return null;
953
+ return frame.classes.split(/\s+/).find((c) => c.startsWith("gp-grid-")) || null;
954
+ }
955
+ function warnBreakInsideGrid(kind, line, meta) {
956
+ const host = breakHost();
957
+ const gridClass = gridClassOf(host);
958
+ if (!gridClass)
959
+ return;
960
+ warn(state.env, line, "break_inside_grid", `@${kind} inside a grid container: the enclosing @${host.kind} carries .${gridClass}, so this break's <div> becomes a grid ITEM — it takes a cell of its own, corrupts the grid's placement, and print and the live preview then disagree about which page the content after it lands on. Move the break outside the grid @${host.kind}, or remove it — grid rows already flow and fragment across pages on their own.`, meta);
961
+ }
962
+ function warnIfEmptyDecoratedSection(closingKind, closingLine) {
963
+ const sec = stack.get("section");
964
+ if (!sec || sec.sawContent)
965
+ return;
966
+ const attrs = sec.meta && sec.meta.attrs || {};
967
+ const decorations = [];
968
+ for (const c of (attrs.class || "").split(/\s+/).filter(Boolean))
969
+ decorations.push(`.${c}`);
970
+ for (const [k, v] of Object.entries(attrs)) {
971
+ if (k === "class")
972
+ continue;
973
+ decorations.push(k === "id" ? `#${v}` : `${k}=${v}`);
974
+ }
975
+ if (!decorations.length)
976
+ return;
977
+ const openLine = sec.openToken && sec.openToken.meta && Number.isFinite(sec.openToken.meta.line) ? sec.openToken.meta.line : closingLine;
978
+ warn(state.env, openLine, "empty_section", `This @section (${decorations.join(" ")}) was closed by the @${closingKind} on line ${closingLine} with no content between the two markers, so its styling applies to an empty element and nothing prints the layout it asked for. Delete one of the two markers, or move the content that belongs inside the section between them.`, null);
979
+ }
936
980
  for (let i = 0;i < state.tokens.length; i++) {
937
981
  const tok = state.tokens[i];
938
982
  if (tok.type !== "layout_marker") {
983
+ const openSectionFrame = stack.get("section");
984
+ if (openSectionFrame)
985
+ openSectionFrame.sawContent = true;
939
986
  out.push(tok);
940
987
  continue;
941
988
  }
@@ -961,6 +1008,7 @@ function plugin(md, pluginOptions = {}) {
961
1008
  continue;
962
1009
  }
963
1010
  if (kind === "section") {
1011
+ warnIfEmptyDecoratedSection("section", line);
964
1012
  stack.close("section");
965
1013
  const spread = stack.get("spread");
966
1014
  if (spread && spread.noPagesYet) {
@@ -989,6 +1037,7 @@ function plugin(md, pluginOptions = {}) {
989
1037
  continue;
990
1038
  }
991
1039
  if (kind === "page-break") {
1040
+ warnBreakInsideGrid("page-break", line, meta);
992
1041
  const t = new state.Token("layout_page_break", "div", 0);
993
1042
  t.meta = { line };
994
1043
  t.attrSet("class", "gp-page-break");
@@ -997,6 +1046,7 @@ function plugin(md, pluginOptions = {}) {
997
1046
  continue;
998
1047
  }
999
1048
  if (kind === "column-break") {
1049
+ warnBreakInsideGrid("column-break", line, meta);
1000
1050
  const openSectionFrame = stack.get("section");
1001
1051
  if (openSectionFrame && openSectionFrame.openToken && !openSectionFrame.openToken.meta.hasColumnBreak) {
1002
1052
  const sectionCls = openSectionFrame.openToken.attrGet("class") || "";
@@ -1012,6 +1062,7 @@ function plugin(md, pluginOptions = {}) {
1012
1062
  continue;
1013
1063
  }
1014
1064
  if (kind === "end-section") {
1065
+ warnIfEmptyDecoratedSection("end-section", line);
1015
1066
  stack.close("section");
1016
1067
  continue;
1017
1068
  }
@@ -1099,7 +1150,7 @@ body { margin: 0; }
1099
1150
  .gp-page-break { break-before: page; }
1100
1151
  .page { break-before: page; }
1101
1152
  .spread { break-before: page; }
1102
- :where(.page, .spread) { position: relative; }
1153
+ :where(.page, .spread) { position: relative; display: flow-root; box-sizing: border-box; min-height: calc(var(--gp-content-h, 1px) - 1px); }
1103
1154
  .gp-column-break { break-after: column; height: 0; font-size: 0; line-height: 0; visibility: hidden; }
1104
1155
 
1105
1156
  :where(h1,h2,h3,h4,h5,h6) { break-after: avoid; }
@@ -1183,6 +1234,26 @@ var GUTTERPRESS_CSS = `
1183
1234
  .gp-columns-2 { columns: 2; column-gap: var(--gp-column-gap, 1.5em); }
1184
1235
  .gp-columns-3 { columns: 3; column-gap: var(--gp-column-gap, 1.5em); }
1185
1236
 
1237
+ /* grid runs — the SLOTTED counterpart to the column runs above. Grid places
1238
+ each child into the next cell, across then down (deterministic slots: card
1239
+ layouts, stat blocks, image-plus-caption pairs); columns FLOW one text run
1240
+ down then across. Same neutral-primitive rationale as .gp-columns-*, and
1241
+ permanent vocabulary for the same reason: standard CSS Grid verbatim, no
1242
+ spec gap to remove later. MEASURED (Chromium 151, gp-grid evidence pack):
1243
+ grid rows fragment across sheets with EXACT print/viewer parity — 2- and
1244
+ 3-col, unequal item heights, mid-row cuts, multi-sheet overflow,
1245
+ break-inside:avoid, gap geometry — so a grid taller than the page is safe,
1246
+ no fit-one-page constraint. Two things to know, not fix:
1247
+ - on a min-height page root (MARKER_CSS), default align-content
1248
+ stretches rows apart to fill the page — identically in both engines.
1249
+ Authors wanting packed rows set align-content: start.
1250
+ - a @page-break / @column-break marker DIRECTLY inside a grid container
1251
+ becomes a grid item and corrupts placement (the one measured parity
1252
+ break); markers.js diagnoses it (break_inside_grid).
1253
+ --gp-grid-gap is author-settable. */
1254
+ .gp-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gp-grid-gap, 1.5em); }
1255
+ .gp-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gp-grid-gap, 1.5em); }
1256
+
1186
1257
  /* shape wrap — text follows the image's alpha silhouette instead of its
1187
1258
  rectangular box. shape-outside only applies to floats, so this is inert
1188
1259
  without .gp-left/.gp-right (and under .gp-pin, which un-floats). The
@@ -1245,8 +1316,14 @@ img.gp-shape {
1245
1316
  opacity, filter, transform on it traps the negative layer inside).
1246
1317
  Core keeps .page/.spread at 'position: relative; z-index: auto'
1247
1318
  precisely so they are not stacking contexts.
1248
- - a clipping ancestor (overflow other than visible), the same mechanism
1249
- that clips a .gp-bleed plate back to the wrapper's width.
1319
+ - a clipping ancestor (overflow other than visible) but only where
1320
+ the art actually overhangs that ancestor's clip box on a clipped
1321
+ axis: the overhang is cut off, the same mechanism that clips a
1322
+ .gp-bleed plate back to the wrapper's width. Clipping never reorders
1323
+ layers — within-bounds art under a clipping .page prints whole and
1324
+ still behind (measured; see the build audit's comment in
1325
+ engine/compiler/build.ts), and a static wrapper's overflow never
1326
+ binds an abspos .gp-pin at all.
1250
1327
  The build-time engine.layer.trapped audit reports both against the live
1251
1328
  ancestor chain. printsafe/page-containment is only an early source hint for
1252
1329
  declarations written directly on .page/.spread. */
@@ -3873,7 +3950,7 @@ import git from "isomorphic-git";
3873
3950
  // package.json
3874
3951
  var package_default = {
3875
3952
  name: "gutterpress",
3876
- version: "0.10.0-alpha.4",
3953
+ version: "0.10.0",
3877
3954
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
3878
3955
  author: "itlackey",
3879
3956
  license: "MPL-2.0",
@@ -4142,200 +4219,12 @@ async function writeBuildFingerprint(input) {
4142
4219
  return outPath;
4143
4220
  }
4144
4221
 
4145
- // src/lib/missing-asset-placeholder.ts
4146
- import { createHash as createHash5 } from "node:crypto";
4147
- import { deflateSync } from "node:zlib";
4148
- function placeholderOutputPath(missingOutputPath) {
4149
- const hash = createHash5("sha256").update(missingOutputPath).digest("hex").slice(0, 16);
4150
- return `assets/gutterpress-missing/${hash}.png`;
4151
- }
4152
- function decodeHtmlAttribute(value) {
4153
- return value.replace(/&quot;/g, '"').replace(/&#39;|&apos;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
4154
- }
4155
- function rewriteMissingImageReferences(html, replacements) {
4156
- if (replacements.size === 0)
4157
- return html;
4158
- const replacementFor = (raw) => replacements.get(raw) ?? replacements.get(decodeHtmlAttribute(raw));
4159
- const cssUrl = /url\(\s*(?:"([^"]*)"|'([^']*)'|&quot;((?:(?!&quot;).)*)&quot;|([^'"()\s]*))\s*\)/giy;
4160
- const rewriteCssUrls = (css) => {
4161
- let out2 = "";
4162
- let copiedThrough = 0;
4163
- let index = 0;
4164
- while (index < css.length) {
4165
- if (css.startsWith("/*", index)) {
4166
- const end = css.indexOf("*/", index + 2);
4167
- index = end < 0 ? css.length : end + 2;
4168
- continue;
4169
- }
4170
- const char = css[index];
4171
- if (char === '"' || char === "'") {
4172
- const quote = char;
4173
- index++;
4174
- while (index < css.length) {
4175
- if (css[index] === "\\") {
4176
- index += 2;
4177
- continue;
4178
- }
4179
- const current = css[index++];
4180
- if (current === quote)
4181
- break;
4182
- }
4183
- continue;
4184
- }
4185
- const previous = index > 0 ? css[index - 1] : "";
4186
- if (!/[a-z0-9_-]/i.test(previous) && css.slice(index, index + 3).toLowerCase() === "url") {
4187
- cssUrl.lastIndex = index;
4188
- const match = cssUrl.exec(css);
4189
- if (match) {
4190
- const raw = match[1] ?? match[2] ?? match[3] ?? match[4] ?? "";
4191
- const replacement = replacementFor(raw);
4192
- if (replacement) {
4193
- out2 += css.slice(copiedThrough, index);
4194
- out2 += match[3] !== undefined ? `url(&quot;${replacement}&quot;)` : `url("${replacement}")`;
4195
- index = cssUrl.lastIndex;
4196
- copiedThrough = index;
4197
- continue;
4198
- }
4199
- }
4200
- }
4201
- index++;
4202
- }
4203
- return copiedThrough === 0 ? css : out2 + css.slice(copiedThrough);
4204
- };
4205
- const rewriteTag = (tag) => {
4206
- let out2 = tag;
4207
- if (/^<img\b/i.test(tag)) {
4208
- out2 = out2.replace(/(\s+src\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi, (whole, prefix, double, single, bare) => {
4209
- const raw = double ?? single ?? bare ?? "";
4210
- const replacement = replacementFor(raw);
4211
- if (!replacement)
4212
- return whole;
4213
- if (double !== undefined)
4214
- return `${prefix}"${replacement}"`;
4215
- if (single !== undefined)
4216
- return `${prefix}'${replacement}'`;
4217
- return `${prefix}${replacement}`;
4218
- });
4219
- }
4220
- if (/^<(?:img|source)\b/i.test(tag)) {
4221
- out2 = out2.replace(/(\s+srcset\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi, (whole, prefix, double, single, bare) => {
4222
- const value = double ?? single ?? bare ?? "";
4223
- let rewritten = "";
4224
- let copiedThrough = 0;
4225
- let changed = false;
4226
- for (const candidate of parseSrcsetUrlCandidates(value)) {
4227
- const replacement = replacementFor(candidate.url);
4228
- if (!replacement)
4229
- continue;
4230
- rewritten += value.slice(copiedThrough, candidate.start) + replacement;
4231
- copiedThrough = candidate.end;
4232
- changed = true;
4233
- }
4234
- if (!changed)
4235
- return whole;
4236
- rewritten += value.slice(copiedThrough);
4237
- if (double !== undefined)
4238
- return `${prefix}"${rewritten}"`;
4239
- if (single !== undefined)
4240
- return `${prefix}'${rewritten}'`;
4241
- return `${prefix}${rewritten}`;
4242
- });
4243
- }
4244
- out2 = out2.replace(/(\s+style\s*=\s*)(["'])(.*?)\2/gi, (whole, prefix, quote, value) => {
4245
- const rewritten = rewriteCssUrls(value);
4246
- return rewritten === value ? whole : `${prefix}${quote}${rewritten}${quote}`;
4247
- });
4248
- return out2;
4249
- };
4250
- const rewriteActiveHtml = (active) => active.replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g, (tag) => rewriteTag(tag));
4251
- const protectedRegion = /<!--[\s\S]*?-->|<(script|style|pre|code|textarea)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
4252
- let out = "";
4253
- let last = 0;
4254
- for (const match of html.matchAll(protectedRegion)) {
4255
- out += rewriteActiveHtml(html.slice(last, match.index));
4256
- if (match[1]?.toLowerCase() === "style") {
4257
- const style = /^(<style\b[^>]*>)([\s\S]*)(<\/style\s*>)$/i.exec(match[0]);
4258
- out += style ? `${rewriteTag(style[1])}${rewriteCssUrls(style[2])}${style[3]}` : match[0];
4259
- } else {
4260
- out += match[0];
4261
- }
4262
- last = (match.index ?? 0) + match[0].length;
4263
- }
4264
- out += rewriteActiveHtml(html.slice(last));
4265
- return out;
4266
- }
4267
- function crc32(buf) {
4268
- let c = ~0;
4269
- for (let i = 0;i < buf.length; i++) {
4270
- c ^= buf[i];
4271
- for (let k = 0;k < 8; k++)
4272
- c = c >>> 1 ^ 3988292384 & -(c & 1);
4273
- }
4274
- return ~c >>> 0;
4275
- }
4276
- function chunk(type, data) {
4277
- const out = new Uint8Array(data.length + 12);
4278
- const view = new DataView(out.buffer);
4279
- view.setUint32(0, data.length);
4280
- for (let i = 0;i < 4; i++)
4281
- out[4 + i] = type.charCodeAt(i);
4282
- out.set(data, 8);
4283
- view.setUint32(data.length + 8, crc32(out.subarray(4, data.length + 8)));
4284
- return out;
4285
- }
4286
- function placeholderPng(width = 640, height = 480, cell = 32) {
4287
- const A = [217, 70, 239];
4288
- const B = [26, 26, 26];
4289
- const raw = new Uint8Array(height * (1 + width * 3));
4290
- let p = 0;
4291
- for (let y = 0;y < height; y++) {
4292
- raw[p++] = 0;
4293
- for (let x = 0;x < width; x++) {
4294
- const c = ((x / cell | 0) + (y / cell | 0)) % 2 === 0 ? A : B;
4295
- raw[p++] = c[0];
4296
- raw[p++] = c[1];
4297
- raw[p++] = c[2];
4298
- }
4299
- }
4300
- const ihdr = new Uint8Array(13);
4301
- const hv = new DataView(ihdr.buffer);
4302
- hv.setUint32(0, width);
4303
- hv.setUint32(4, height);
4304
- ihdr[8] = 8;
4305
- ihdr[9] = 2;
4306
- const parts = [
4307
- new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
4308
- chunk("IHDR", ihdr),
4309
- chunk("IDAT", new Uint8Array(deflateSync(raw))),
4310
- chunk("IEND", new Uint8Array(0))
4311
- ];
4312
- const total = parts.reduce((n, x) => n + x.length, 0);
4313
- const png = new Uint8Array(total);
4314
- let o = 0;
4315
- for (const part of parts) {
4316
- png.set(part, o);
4317
- o += part.length;
4318
- }
4319
- return png;
4320
- }
4321
-
4322
4222
  // src/lib/lint-runner.ts
4323
4223
  import { resolve as resolve4 } from "node:path";
4324
4224
  import { readFile as readFile8 } from "node:fs/promises";
4325
4225
 
4326
4226
  // src/lib/printsafe.ts
4327
4227
  import postcss2 from "postcss";
4328
-
4329
- // src/engine/shared/margin-box-support.ts
4330
- var MARGIN_BOX_IGNORED_PROPERTIES = new Set([
4331
- "transform",
4332
- "rotate",
4333
- "translate",
4334
- "scale",
4335
- "box-shadow"
4336
- ]);
4337
-
4338
- // src/lib/printsafe.ts
4339
4228
  var ruleRemoteUrls = "printsafe/no-remote-urls";
4340
4229
  var ruleRiskyProps = "printsafe/no-risky-print-effects";
4341
4230
  var ruleSyntax = "printsafe/syntax-error";
@@ -7549,6 +7438,185 @@ function computeGates(format, opts, config) {
7549
7438
  import path6 from "node:path";
7550
7439
  import os from "node:os";
7551
7440
  import fsp from "node:fs/promises";
7441
+
7442
+ // src/lib/missing-asset-placeholder.ts
7443
+ import { createHash as createHash5 } from "node:crypto";
7444
+ import { deflateSync } from "node:zlib";
7445
+ function placeholderOutputPath(missingOutputPath) {
7446
+ const hash = createHash5("sha256").update(missingOutputPath).digest("hex").slice(0, 16);
7447
+ return `assets/gutterpress-missing/${hash}.png`;
7448
+ }
7449
+ function decodeHtmlAttribute(value) {
7450
+ return value.replace(/&quot;/g, '"').replace(/&#39;|&apos;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
7451
+ }
7452
+ function rewriteMissingImageReferences(html, replacements) {
7453
+ if (replacements.size === 0)
7454
+ return html;
7455
+ const replacementFor = (raw) => replacements.get(raw) ?? replacements.get(decodeHtmlAttribute(raw));
7456
+ const cssUrl = /url\(\s*(?:"([^"]*)"|'([^']*)'|&quot;((?:(?!&quot;).)*)&quot;|([^'"()\s]*))\s*\)/giy;
7457
+ const rewriteCssUrls = (css) => {
7458
+ let out2 = "";
7459
+ let copiedThrough = 0;
7460
+ let index = 0;
7461
+ while (index < css.length) {
7462
+ if (css.startsWith("/*", index)) {
7463
+ const end = css.indexOf("*/", index + 2);
7464
+ index = end < 0 ? css.length : end + 2;
7465
+ continue;
7466
+ }
7467
+ const char = css[index];
7468
+ if (char === '"' || char === "'") {
7469
+ const quote = char;
7470
+ index++;
7471
+ while (index < css.length) {
7472
+ if (css[index] === "\\") {
7473
+ index += 2;
7474
+ continue;
7475
+ }
7476
+ const current = css[index++];
7477
+ if (current === quote)
7478
+ break;
7479
+ }
7480
+ continue;
7481
+ }
7482
+ const previous = index > 0 ? css[index - 1] : "";
7483
+ if (!/[a-z0-9_-]/i.test(previous) && css.slice(index, index + 3).toLowerCase() === "url") {
7484
+ cssUrl.lastIndex = index;
7485
+ const match = cssUrl.exec(css);
7486
+ if (match) {
7487
+ const raw = match[1] ?? match[2] ?? match[3] ?? match[4] ?? "";
7488
+ const replacement = replacementFor(raw);
7489
+ if (replacement) {
7490
+ out2 += css.slice(copiedThrough, index);
7491
+ out2 += match[3] !== undefined ? `url(&quot;${replacement}&quot;)` : `url("${replacement}")`;
7492
+ index = cssUrl.lastIndex;
7493
+ copiedThrough = index;
7494
+ continue;
7495
+ }
7496
+ }
7497
+ }
7498
+ index++;
7499
+ }
7500
+ return copiedThrough === 0 ? css : out2 + css.slice(copiedThrough);
7501
+ };
7502
+ const rewriteTag = (tag) => {
7503
+ let out2 = tag;
7504
+ if (/^<img\b/i.test(tag)) {
7505
+ out2 = out2.replace(/(\s+src\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi, (whole, prefix, double, single, bare) => {
7506
+ const raw = double ?? single ?? bare ?? "";
7507
+ const replacement = replacementFor(raw);
7508
+ if (!replacement)
7509
+ return whole;
7510
+ if (double !== undefined)
7511
+ return `${prefix}"${replacement}"`;
7512
+ if (single !== undefined)
7513
+ return `${prefix}'${replacement}'`;
7514
+ return `${prefix}${replacement}`;
7515
+ });
7516
+ }
7517
+ if (/^<(?:img|source)\b/i.test(tag)) {
7518
+ out2 = out2.replace(/(\s+srcset\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi, (whole, prefix, double, single, bare) => {
7519
+ const value = double ?? single ?? bare ?? "";
7520
+ let rewritten = "";
7521
+ let copiedThrough = 0;
7522
+ let changed = false;
7523
+ for (const candidate of parseSrcsetUrlCandidates(value)) {
7524
+ const replacement = replacementFor(candidate.url);
7525
+ if (!replacement)
7526
+ continue;
7527
+ rewritten += value.slice(copiedThrough, candidate.start) + replacement;
7528
+ copiedThrough = candidate.end;
7529
+ changed = true;
7530
+ }
7531
+ if (!changed)
7532
+ return whole;
7533
+ rewritten += value.slice(copiedThrough);
7534
+ if (double !== undefined)
7535
+ return `${prefix}"${rewritten}"`;
7536
+ if (single !== undefined)
7537
+ return `${prefix}'${rewritten}'`;
7538
+ return `${prefix}${rewritten}`;
7539
+ });
7540
+ }
7541
+ out2 = out2.replace(/(\s+style\s*=\s*)(["'])(.*?)\2/gi, (whole, prefix, quote, value) => {
7542
+ const rewritten = rewriteCssUrls(value);
7543
+ return rewritten === value ? whole : `${prefix}${quote}${rewritten}${quote}`;
7544
+ });
7545
+ return out2;
7546
+ };
7547
+ const rewriteActiveHtml = (active) => active.replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g, (tag) => rewriteTag(tag));
7548
+ const protectedRegion = /<!--[\s\S]*?-->|<(script|style|pre|code|textarea)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
7549
+ let out = "";
7550
+ let last = 0;
7551
+ for (const match of html.matchAll(protectedRegion)) {
7552
+ out += rewriteActiveHtml(html.slice(last, match.index));
7553
+ if (match[1]?.toLowerCase() === "style") {
7554
+ const style = /^(<style\b[^>]*>)([\s\S]*)(<\/style\s*>)$/i.exec(match[0]);
7555
+ out += style ? `${rewriteTag(style[1])}${rewriteCssUrls(style[2])}${style[3]}` : match[0];
7556
+ } else {
7557
+ out += match[0];
7558
+ }
7559
+ last = (match.index ?? 0) + match[0].length;
7560
+ }
7561
+ out += rewriteActiveHtml(html.slice(last));
7562
+ return out;
7563
+ }
7564
+ function crc32(buf) {
7565
+ let c = ~0;
7566
+ for (let i = 0;i < buf.length; i++) {
7567
+ c ^= buf[i];
7568
+ for (let k = 0;k < 8; k++)
7569
+ c = c >>> 1 ^ 3988292384 & -(c & 1);
7570
+ }
7571
+ return ~c >>> 0;
7572
+ }
7573
+ function chunk(type, data) {
7574
+ const out = new Uint8Array(data.length + 12);
7575
+ const view = new DataView(out.buffer);
7576
+ view.setUint32(0, data.length);
7577
+ for (let i = 0;i < 4; i++)
7578
+ out[4 + i] = type.charCodeAt(i);
7579
+ out.set(data, 8);
7580
+ view.setUint32(data.length + 8, crc32(out.subarray(4, data.length + 8)));
7581
+ return out;
7582
+ }
7583
+ function placeholderPng(width = 640, height = 480, cell = 32) {
7584
+ const A = [217, 70, 239];
7585
+ const B = [26, 26, 26];
7586
+ const raw = new Uint8Array(height * (1 + width * 3));
7587
+ let p = 0;
7588
+ for (let y = 0;y < height; y++) {
7589
+ raw[p++] = 0;
7590
+ for (let x = 0;x < width; x++) {
7591
+ const c = ((x / cell | 0) + (y / cell | 0)) % 2 === 0 ? A : B;
7592
+ raw[p++] = c[0];
7593
+ raw[p++] = c[1];
7594
+ raw[p++] = c[2];
7595
+ }
7596
+ }
7597
+ const ihdr = new Uint8Array(13);
7598
+ const hv = new DataView(ihdr.buffer);
7599
+ hv.setUint32(0, width);
7600
+ hv.setUint32(4, height);
7601
+ ihdr[8] = 8;
7602
+ ihdr[9] = 2;
7603
+ const parts = [
7604
+ new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
7605
+ chunk("IHDR", ihdr),
7606
+ chunk("IDAT", new Uint8Array(deflateSync(raw))),
7607
+ chunk("IEND", new Uint8Array(0))
7608
+ ];
7609
+ const total = parts.reduce((n, x) => n + x.length, 0);
7610
+ const png = new Uint8Array(total);
7611
+ let o = 0;
7612
+ for (const part of parts) {
7613
+ png.set(part, o);
7614
+ o += part.length;
7615
+ }
7616
+ return png;
7617
+ }
7618
+
7619
+ // src/lib/build-staging.ts
7552
7620
  async function shipViewerHtml(htmlFile, outDir) {
7553
7621
  await fsp.mkdir(path6.join(outDir, "engine"), { recursive: true });
7554
7622
  await fsp.copyFile(await getAssetPath("engine/gutterpress-viewer.js"), path6.join(outDir, "engine/gutterpress-viewer.js"));
@@ -7557,6 +7625,50 @@ async function shipViewerHtml(htmlFile, outDir) {
7557
7625
  const html = await fsp.readFile(htmlFile, "utf-8");
7558
7626
  await fsp.writeFile(htmlFile, /<\/head>/i.test(html) ? html.replace(/<\/head>/i, tag + "</head>") : tag + html, "utf-8");
7559
7627
  }
7628
+ async function stageBookAssets(options) {
7629
+ const { renderDir, outDir, htmlFile, imageRefs, cssAssets, onPlan } = options;
7630
+ const { copies: imageCopies, errors, destinations } = await planImageCopies(renderDir, imageRefs);
7631
+ const copies = [...cssAssets, ...imageCopies];
7632
+ onPlan?.({ unresolved: errors, copyCount: copies.length });
7633
+ const missingPlaceholders = copies.length ? await copyReferencedAssets(copies, outDir) : new Map;
7634
+ let staged = await fsp.readFile(htmlFile, "utf8");
7635
+ if (missingPlaceholders.size > 0) {
7636
+ const rewrites = new Map(missingPlaceholders);
7637
+ for (const [ref, dest] of destinations) {
7638
+ const placeholder = missingPlaceholders.get(dest);
7639
+ if (placeholder)
7640
+ rewrites.set(ref, placeholder);
7641
+ }
7642
+ staged = rewriteMissingImageReferences(staged, rewrites);
7643
+ await fsp.writeFile(htmlFile, staged, "utf8");
7644
+ }
7645
+ if (staged.includes("--gp-shape:")) {
7646
+ await fsp.writeFile(htmlFile, await inlineShapeUrls(staged, outDir), "utf8");
7647
+ }
7648
+ return { missing: [...missingPlaceholders.keys()].sort() };
7649
+ }
7650
+ async function copyReferencedAssets(copies, outDir) {
7651
+ const dirs = new Set(copies.map((c) => path6.dirname(path6.resolve(outDir, c.to))));
7652
+ await Promise.all([...dirs].map((d) => fsp.mkdir(d, { recursive: true })));
7653
+ const missing = new Map;
7654
+ await Promise.all(copies.map(async (c) => {
7655
+ const dest = path6.resolve(outDir, c.to);
7656
+ try {
7657
+ await fsp.copyFile(c.from, dest);
7658
+ } catch (err) {
7659
+ if (err?.code === "ENOENT") {
7660
+ const placeholder = placeholderOutputPath(c.to);
7661
+ const placeholderDest = path6.resolve(outDir, placeholder);
7662
+ await fsp.mkdir(path6.dirname(placeholderDest), { recursive: true });
7663
+ await fsp.writeFile(placeholderDest, placeholderPng());
7664
+ missing.set(c.to, placeholder);
7665
+ return;
7666
+ }
7667
+ throw new BuildError(`Could not copy asset ${c.from} → ${c.to}: ` + (err instanceof Error ? err.message : String(err)), 1);
7668
+ }
7669
+ }));
7670
+ return missing;
7671
+ }
7560
7672
  async function createStageRoot() {
7561
7673
  return fsp.mkdtemp(path6.join(os.tmpdir(), "gutterpress-stage-"));
7562
7674
  }
@@ -7669,67 +7781,35 @@ async function renderBook(ctx) {
7669
7781
  }
7670
7782
  },
7671
7783
  onImageRefs: (refs) => imageRefs.push(...refs),
7672
- onCssAssets: (copies2) => cssAssets.push(...copies2),
7784
+ onCssAssets: (copies) => cssAssets.push(...copies),
7673
7785
  onStyleWarnings: (warnings) => {
7674
7786
  for (const w of warnings)
7675
7787
  log.warn(` ${w}`);
7676
7788
  }
7677
7789
  });
7678
7790
  log.success(`Wrote ${htmlFile}`);
7679
- const { copies: imageCopies, errors, destinations } = await planImageCopies(renderDir, imageRefs);
7680
- if (errors.length > 0) {
7681
- throw new BuildError(`Cannot resolve ${errors.length} image reference(s):
7682
- ` + errors.map((e) => ` - ${e}`).join(`
7791
+ const { missing } = await stageBookAssets({
7792
+ renderDir,
7793
+ outDir: workDir,
7794
+ htmlFile,
7795
+ imageRefs,
7796
+ cssAssets,
7797
+ onPlan: ({ unresolved, copyCount }) => {
7798
+ if (unresolved.length > 0) {
7799
+ throw new BuildError(`Cannot resolve ${unresolved.length} image reference(s):
7800
+ ` + unresolved.map((e) => ` - ${e}`).join(`
7683
7801
  `), 1);
7684
- }
7685
- const copies = [...cssAssets, ...imageCopies];
7686
- let missingPlaceholders = new Map;
7687
- if (copies.length > 0) {
7688
- log.info(`Copying ${copies.length} referenced asset(s)`);
7689
- missingPlaceholders = await copyReferencedAssets(copies, workDir);
7690
- }
7691
- let staged = await fsp2.readFile(htmlFile, "utf8");
7692
- if (missingPlaceholders.size > 0) {
7693
- const rewrites = new Map(missingPlaceholders);
7694
- for (const [ref, dest] of destinations) {
7695
- const placeholder = missingPlaceholders.get(dest);
7696
- if (placeholder)
7697
- rewrites.set(ref, placeholder);
7698
- }
7699
- staged = rewriteMissingImageReferences(staged, rewrites);
7700
- await fsp2.writeFile(htmlFile, staged, "utf8");
7701
- }
7702
- if (staged.includes("--gp-shape:")) {
7703
- await fsp2.writeFile(htmlFile, await inlineShapeUrls(staged, workDir), "utf8");
7704
- }
7705
- return htmlFile;
7706
- }
7707
- async function copyReferencedAssets(copies, outDir) {
7708
- const dirs = new Set(copies.map((c) => path7.dirname(path7.resolve(outDir, c.to))));
7709
- await Promise.all([...dirs].map((d) => fsp2.mkdir(d, { recursive: true })));
7710
- const missing = new Map;
7711
- await Promise.all(copies.map(async (c) => {
7712
- const dest = path7.resolve(outDir, c.to);
7713
- try {
7714
- await fsp2.copyFile(c.from, dest);
7715
- } catch (err) {
7716
- if (err?.code === "ENOENT") {
7717
- const placeholder = placeholderOutputPath(c.to);
7718
- const placeholderDest = path7.resolve(outDir, placeholder);
7719
- await fsp2.mkdir(path7.dirname(placeholderDest), { recursive: true });
7720
- await fsp2.writeFile(placeholderDest, placeholderPng());
7721
- missing.set(c.to, placeholder);
7722
- return;
7723
7802
  }
7724
- throw new BuildError(`Could not copy asset ${c.from} → ${c.to}: ` + (err instanceof Error ? err.message : String(err)), 1);
7803
+ if (copyCount > 0)
7804
+ log.info(`Copying ${copyCount} referenced asset(s)`);
7725
7805
  }
7726
- }));
7727
- if (missing.size > 0) {
7728
- log.warn(`${missing.size} referenced image(s) do not exist — a magenta placeholder ` + `was substituted so the build could finish. Each one is a visible hole ` + `in the PDF:`);
7729
- for (const m of [...missing.keys()].sort())
7806
+ });
7807
+ if (missing.length > 0) {
7808
+ log.warn(`${missing.length} referenced image(s) do not exist — a magenta placeholder ` + `was substituted so the build could finish. Each one is a visible hole ` + `in the PDF:`);
7809
+ for (const m of missing)
7730
7810
  log.warn(` missing: ${m}`);
7731
7811
  }
7732
- return missing;
7812
+ return htmlFile;
7733
7813
  }
7734
7814
  async function resolveIccProfile(icc, manifestDir, explicitIccPath) {
7735
7815
  const iccCandidates = path7.isAbsolute(icc) ? [icc] : [path7.resolve(manifestDir, icc), path7.resolve(icc)];
@@ -7827,11 +7907,12 @@ class PdfOutput {
7827
7907
  const rawPdf = pdfxMode ? path7.join(stage, "raw.pdf") : path7.resolve(pdfFile);
7828
7908
  await fsp2.mkdir(path7.dirname(path7.resolve(pdfFile)), { recursive: true });
7829
7909
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
7830
- const { buildNativePdf } = await import("./engine-z4p9sr4h.js");
7910
+ const { buildNativePdf } = await import("./engine-b159tbns.js");
7831
7911
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
7832
7912
  title: config.title,
7833
7913
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
7834
- signature: config.print.signature
7914
+ signature: config.print.signature,
7915
+ allowShrink: opts.allowShrink
7835
7916
  }, opts.engineBrowser);
7836
7917
  for (const d of engineDiagnostics)
7837
7918
  log.warn(d.message);