gutterpress 0.11.17-alpha.2 → 0.11.17-alpha.4

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 (53) hide show
  1. package/dist/api/index.d.ts +4 -0
  2. package/dist/api/index.js +11 -2
  3. package/dist/black-text-gray-7dz61456.js +15 -0
  4. package/dist/{build-1p999cmg.js → build-fam0tqg6.js} +5 -4
  5. package/dist/checks/pdf/black-text.d.ts +3 -0
  6. package/dist/checks/pdf/index.d.ts +2 -0
  7. package/dist/checks/pdf/signature.d.ts +3 -0
  8. package/dist/{cli-6z5zthzt.js → cli-2kc34kme.js} +470 -489
  9. package/dist/{cli-70xgehq2.js → cli-8apk8571.js} +1 -1
  10. package/dist/cli-ht1bcqdd.js +493 -0
  11. package/dist/{cli-gymfg04q.js → cli-jyy9pqf3.js} +15 -10
  12. package/dist/{cli-ppc0vz4b.js → cli-pqcb2nnb.js} +1 -1
  13. package/dist/cli.js +12 -12
  14. package/dist/{doctor-reqpnn3h.js → doctor-zre5k5fg.js} +2 -2
  15. package/dist/engine/compiler/build.d.ts +3 -2
  16. package/dist/engine/compiler/postprocess.d.ts +6 -4
  17. package/dist/engine/shared/synthesis.d.ts +29 -5
  18. package/dist/{engine-5jzhjjvv.js → engine-3xqgm82d.js} +2 -2
  19. package/dist/{engine-078ggczp.js → engine-zdb41ay1.js} +1 -1
  20. package/dist/{ext-dsbfejt3.js → ext-hfqcg6ax.js} +5 -4
  21. package/dist/{gutterpress-viewer-zkv2stfy.js → gutterpress-viewer-1n793gwd.js} +5 -3
  22. package/dist/{index-c275p061.js → index-gp503ze9.js} +1622 -1332
  23. package/dist/{index-5dcj1vts.js → index-hrx0gfkj.js} +23 -684
  24. package/dist/index-ht1bcqdd.js +493 -0
  25. package/dist/index-q1xqxrh9.js +949 -0
  26. package/dist/index.js +12 -3
  27. package/dist/lib/black-text-gray.d.ts +55 -8
  28. package/dist/lib/build-fingerprint.d.ts +2 -0
  29. package/dist/lib/engine.d.ts +2 -1
  30. package/dist/lib/lint-rules.d.ts +9 -0
  31. package/dist/lib/markdown/assemble.d.ts +3 -5
  32. package/dist/lib/markdown/index.d.ts +3 -8
  33. package/dist/lib/markdown/renderer.d.ts +9 -1
  34. package/dist/lib/page-size.d.ts +66 -0
  35. package/dist/lib/pdf-inspect.d.ts +6 -0
  36. package/dist/lib/printsafe.d.ts +8 -1
  37. package/dist/lib/project-scaffold.d.ts +3 -9
  38. package/dist/lib/remote-auth/github-repos.d.ts +7 -0
  39. package/dist/lib/remote-auth/online-backup.d.ts +51 -0
  40. package/dist/lib/snippets.d.ts +3 -0
  41. package/dist/{manifest.schema-40sfq49z.json → manifest.schema-gh0jjve9.json} +14 -2
  42. package/dist/{new-m5y2wepr.js → new-bjdf7bhg.js} +5 -4
  43. package/dist/{plugin.js-m4a7m3t4.tpl → plugin.js-cswq7wvd.tpl} +4 -1
  44. package/dist/{plugin.test.js-6n8ct5e3.tpl → plugin.test.js-xgxrd8hf.tpl} +4 -1
  45. package/dist/{preflight-bwpqwwny.js → preflight-gvw01661.js} +5 -4
  46. package/dist/preview/file-watcher.d.ts +2 -4
  47. package/dist/{preview-62y7qhkp.js → preview-1cschq8s.js} +5 -4
  48. package/dist/{publish-k2zyt3x6.js → publish-ddqrb8s9.js} +5 -4
  49. package/dist/render.js +30 -5
  50. package/dist/schema/manifest.types.d.ts +17 -0
  51. package/dist/{validate-fgwwxtbn.js → validate-3hjkgxrz.js} +5 -4
  52. package/package.json +1 -1
  53. package/dist/index-eyaj6nkc.js +0 -267
@@ -13,17 +13,21 @@ import {
13
13
  stampCreator,
14
14
  stripAnnotations,
15
15
  warn
16
- } from "./cli-70xgehq2.js";
16
+ } from "./cli-8apk8571.js";
17
17
  import {
18
18
  DEBOUNCE,
19
19
  UsageError,
20
20
  resolvePort
21
- } from "./cli-ppc0vz4b.js";
21
+ } from "./cli-pqcb2nnb.js";
22
+ import {
23
+ blackTextToGray
24
+ } from "./cli-ht1bcqdd.js";
22
25
  import {
23
26
  MARGIN_BOX_IGNORED_PROPERTIES,
24
27
  getAssetPath,
25
- getAssetsDir
26
- } from "./cli-gymfg04q.js";
28
+ getAssetsDir,
29
+ lastPageMode
30
+ } from "./cli-jyy9pqf3.js";
27
31
  import {
28
32
  BuildError,
29
33
  EXIT_CODES
@@ -106,7 +110,7 @@ var DTRPG_PRESET = {
106
110
  source: {
107
111
  files: null
108
112
  },
109
- print: { signature: 4, reserveLastPage: true },
113
+ print: { signature: 4, reserveLastPage: true, lastPage: undefined },
110
114
  pdfx: {
111
115
  flavor: "x1a",
112
116
  icc: "profiles/CGATS21_CRPC1.icc",
@@ -124,7 +128,8 @@ var DTRPG_PRESET = {
124
128
  },
125
129
  lint: {
126
130
  enabled: true,
127
- configPath: null
131
+ configPath: null,
132
+ rules: {}
128
133
  },
129
134
  validate: {
130
135
  enabled: true,
@@ -181,7 +186,7 @@ var BOOK_PRESET = {
181
186
  source: {
182
187
  files: null
183
188
  },
184
- print: { signature: 1, reserveLastPage: false },
189
+ print: { signature: 1, reserveLastPage: false, lastPage: undefined },
185
190
  pdfx: {
186
191
  flavor: "x1a",
187
192
  icc: "profiles/CGATS21_CRPC1.icc",
@@ -199,7 +204,8 @@ var BOOK_PRESET = {
199
204
  },
200
205
  lint: {
201
206
  enabled: true,
202
- configPath: null
207
+ configPath: null,
208
+ rules: {}
203
209
  },
204
210
  validate: {
205
211
  enabled: true,
@@ -566,6 +572,10 @@ function resolveWithPreset(cliOverrides, manifest, preset) {
566
572
  if (legacyPlugins !== undefined) {
567
573
  throw new UsageError(pluginsRemovedMessage(legacyPlugins));
568
574
  }
575
+ const lastPage = c.print?.lastPage ?? m.print?.lastPage;
576
+ if (lastPage !== undefined && lastPage !== "blank" && lastPage !== "short-by-one") {
577
+ throw new UsageError(`Manifest field \`print.lastPage\` must be "blank" or "short-by-one", got ${JSON.stringify(lastPage)}.`);
578
+ }
569
579
  const rawExtensions = c.extensions ?? m.extensions ?? [];
570
580
  if (!Array.isArray(rawExtensions)) {
571
581
  throw new UsageError("Manifest field `extensions` must be a list of specifiers.");
@@ -582,7 +592,10 @@ function resolveWithPreset(cliOverrides, manifest, preset) {
582
592
  print: mergeShape(c.print, m.print, preset.print),
583
593
  page: preset.page ? mergeShape(c.page, m.page, preset.page) : resolveCustomPage(c, m),
584
594
  ink: mergeShape(c.ink, m.ink, preset.ink),
585
- lint: mergeShape(c.lint, m.lint, preset.lint),
595
+ lint: {
596
+ ...mergeShape(c.lint, m.lint, preset.lint),
597
+ rules: { ...preset.lint.rules, ...m.lint?.rules, ...c.lint?.rules }
598
+ },
586
599
  validate: {
587
600
  enabled: c.validate?.enabled ?? m.validate?.enabled ?? preset.validate.enabled,
588
601
  checks: { ...preset.validate.checks, ...m.validate?.checks, ...c.validate?.checks },
@@ -1078,7 +1091,7 @@ function resolveContainerShape(name, pluginName, decl) {
1078
1091
  throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`section\` that is not a boolean.`);
1079
1092
  }
1080
1093
  if (decl.section) {
1081
- for (const field of ["tag", "label", "autoCloseAt"]) {
1094
+ for (const field of ["tag", "label", "autoCloseAt", "selfClosing"]) {
1082
1095
  if (decl[field] !== undefined) {
1083
1096
  throw new Error(`Plugin "${pluginName}"'s marker "@${name}" sets \`section: true\` and \`${field}\` — a section ` + `marker is a core @section, so \`${field}\` does not apply. Remove one of them.`);
1084
1097
  }
@@ -1135,11 +1148,26 @@ function resolveContainerShape(name, pluginName, decl) {
1135
1148
  }
1136
1149
  autoCloseAtEof = decl.autoCloseAt.includes("eof");
1137
1150
  }
1151
+ if (decl.selfClosing !== undefined && typeof decl.selfClosing !== "boolean") {
1152
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`selfClosing\` that is not a boolean.`);
1153
+ }
1154
+ if (decl.selfClosing && decl.autoCloseAt !== undefined) {
1155
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" sets \`selfClosing: true\` and \`autoCloseAt\` — a ` + `self-closing marker has no open container to auto-close. Remove one of them.`);
1156
+ }
1138
1157
  if (decl.validate !== undefined && typeof decl.validate !== "function") {
1139
1158
  throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`validate\` that is not a function.`);
1140
1159
  }
1141
1160
  const check = decl.validate ? { validate: decl.validate, validateOwner: pluginName } : {};
1142
- return { tag, classBase, variants, label, autoCloseAtEof, ...decl.section ? { section: true } : {}, ...check };
1161
+ return {
1162
+ tag,
1163
+ classBase,
1164
+ variants,
1165
+ label,
1166
+ autoCloseAtEof,
1167
+ ...decl.section ? { section: true } : {},
1168
+ ...decl.selfClosing ? { selfClosing: true } : {},
1169
+ ...check
1170
+ };
1143
1171
  }
1144
1172
  function resolveMarkerDeclaration(name, rawDecl, rawRegistry, originOf) {
1145
1173
  const pluginName = originOf.get(name);
@@ -1398,7 +1426,8 @@ function plugin(md, pluginOptions = {}) {
1398
1426
  addClasses(t, baseClass, meta.attrs && meta.attrs.class ? meta.attrs.class : "");
1399
1427
  attachDataAttrs(t, decl.baseKind, variant, meta.attrs || {});
1400
1428
  out.push(t);
1401
- declaredFrames.push({ kind: decl.baseKind, tag: decl.tag, decl, line: meta.__line });
1429
+ if (!decl.selfClosing)
1430
+ declaredFrames.push({ kind: decl.baseKind, tag: decl.tag, decl, line: meta.__line });
1402
1431
  if (decl.label) {
1403
1432
  const value = meta.attrs && meta.attrs[decl.label.attr];
1404
1433
  if (value) {
@@ -1409,6 +1438,8 @@ function plugin(md, pluginOptions = {}) {
1409
1438
  t.meta.labelled = true;
1410
1439
  }
1411
1440
  }
1441
+ if (decl.selfClosing)
1442
+ out.push(new state.Token("layout_component_close", decl.tag, -1));
1412
1443
  }
1413
1444
  const stack = {
1414
1445
  frames: [],
@@ -1687,7 +1718,7 @@ function plugin(md, pluginOptions = {}) {
1687
1718
  continue;
1688
1719
  }
1689
1720
  if (!closeDeclaredFrame(closeDecl.baseKind)) {
1690
- warn2(state.env, line, "declared_marker_close_without_open", `@${kind} used without an open @${closeDecl.baseKind}; ignoring marker.`, meta);
1721
+ warn2(state.env, line, "declared_marker_close_without_open", closeDecl.selfClosing ? `@${kind} is not a marker: @${closeDecl.baseKind} is self-closing and has nothing to close; ignoring marker.` : `@${kind} used without an open @${closeDecl.baseKind}; ignoring marker.`, meta);
1691
1722
  }
1692
1723
  continue;
1693
1724
  }
@@ -4987,6 +5018,32 @@ function getImagePixelSize(page, name, timeoutMs = 2000) {
4987
5018
  }
4988
5019
  });
4989
5020
  }
5021
+ var PAINT_OPS = new Set([
5022
+ "stroke",
5023
+ "closeStroke",
5024
+ "fill",
5025
+ "eoFill",
5026
+ "fillStroke",
5027
+ "eoFillStroke",
5028
+ "closeFillStroke",
5029
+ "closeEOFillStroke",
5030
+ "shadingFill",
5031
+ "paintImageXObject",
5032
+ "paintInlineImageXObject",
5033
+ "paintImageXObjectRepeat",
5034
+ "paintImageMaskXObject",
5035
+ "paintImageMaskXObjectGroup",
5036
+ "paintImageMaskXObjectRepeat",
5037
+ "paintSolidColorImageMask",
5038
+ "showText",
5039
+ "showSpacedText",
5040
+ "nextLineShowText",
5041
+ "nextLineSetSpacingShowText"
5042
+ ].map((name) => OPS[name]).filter((op) => op !== undefined));
5043
+ async function pageHasMarks(doc, pageNum) {
5044
+ const ops = await (await doc.getPage(pageNum)).getOperatorList();
5045
+ return ops.fnArray.some((fn, i) => fn === OPS.constructPath ? PAINT_OPS.has(ops.argsArray[i][0]) : PAINT_OPS.has(fn));
5046
+ }
4990
5047
  async function getImageResolutions(doc) {
4991
5048
  const { imagesByPage } = await getOpPass(doc);
4992
5049
  const out = [];
@@ -5490,8 +5547,100 @@ var check7 = {
5490
5547
  };
5491
5548
  registerCheck(check7);
5492
5549
 
5493
- // src/checks/pdf/bookmarks.ts
5550
+ // src/checks/pdf/black-text.ts
5551
+ import { readFile as readFile9 } from "node:fs/promises";
5494
5552
  var check8 = {
5553
+ id: "pdf.print.black-text",
5554
+ name: "Black Text",
5555
+ description: "Warns when text at or below 24pt is painted in a dark four-colour mix (black, or a near-black colour such as a dark brown)",
5556
+ category: "pdf",
5557
+ phase: "post-build",
5558
+ enabledWhen: (config) => config.pdfx.blackText === "k-only",
5559
+ async run(ctx) {
5560
+ if (!ctx.pdfPath)
5561
+ return [];
5562
+ try {
5563
+ const { findRichBlackText, LARGE_TEXT_PT } = await import("./black-text-gray-7dz61456.js");
5564
+ const found = await findRichBlackText(await readFile9(ctx.pdfPath));
5565
+ if (found.runs === 0)
5566
+ return [];
5567
+ return [
5568
+ finding(check8.id, {
5569
+ severity: "warning",
5570
+ message: `Dark four-colour text at ${LARGE_TEXT_PT}pt or smaller on page(s) ${found.pages.join(", ")}.`,
5571
+ file: ctx.pdfPath,
5572
+ detail: "Small text printed with all four inks can look fuzzy if the plates are slightly out of register, which is why print vendors such as DriveThruRPG want small black text on the black plate alone. `pdfx.blackText: k-only` already does that for near-black text; this text is either a dark colour (for example a dark brown, which keeps its colour) or painted in a way the build cannot rewrite (an image or a pattern). To print it on the black plate, make the text black (#000). If the colour is intended, set `lint.rules: { pdf.print.black-text: off }` in manifest.yaml.",
5573
+ code: "rich-black-text",
5574
+ data: { runs: found.runs, pages: found.pages }
5575
+ })
5576
+ ];
5577
+ } catch {
5578
+ return [
5579
+ inspectionFailed(check8.id, "Could not inspect the PDF's text colours.", {
5580
+ file: ctx.pdfPath
5581
+ })
5582
+ ];
5583
+ }
5584
+ }
5585
+ };
5586
+ registerCheck(check8);
5587
+
5588
+ // src/checks/pdf/signature.ts
5589
+ var check9 = {
5590
+ id: "pdf.print.signature",
5591
+ name: "Signature",
5592
+ description: "Fails when the interior's page count does not fit the print signature (a multiple of it ending on a blank page, or one short of a multiple)",
5593
+ category: "pdf",
5594
+ phase: "post-build",
5595
+ enabledWhen: (config) => config.print.signature > 1,
5596
+ async run(ctx) {
5597
+ if (!ctx.pdfPath)
5598
+ return [];
5599
+ const doc = await loadPdf(ctx.pdfPath);
5600
+ if (!doc) {
5601
+ return [inspectionFailed(check9.id, "Could not parse the PDF's page count.", { file: ctx.pdfPath })];
5602
+ }
5603
+ const size = ctx.config.print.signature;
5604
+ const pages = doc.numPages;
5605
+ if ((pages + 1) % size === 0)
5606
+ return [];
5607
+ const data = { pages, signature: size };
5608
+ if (pages % size !== 0) {
5609
+ return [
5610
+ finding(check9.id, {
5611
+ severity: "error",
5612
+ message: `Page count ${pages} does not fit a signature of ${size}: it must be a multiple of ${size}, or one short of one.`,
5613
+ file: ctx.pdfPath,
5614
+ detail: "Set `print.signature` in the manifest and rebuild; the build pads the interior for you.",
5615
+ code: "signature-page-count",
5616
+ data
5617
+ })
5618
+ ];
5619
+ }
5620
+ if (!lastPageMode(ctx.config.print))
5621
+ return [];
5622
+ try {
5623
+ if (!await pageHasMarks(doc, pages))
5624
+ return [];
5625
+ } catch {
5626
+ return [inspectionFailed(check9.id, "Could not read the last page.", { file: ctx.pdfPath })];
5627
+ }
5628
+ return [
5629
+ finding(check9.id, {
5630
+ severity: "error",
5631
+ message: `The last page (${pages}) has content, but the printer needs it blank for its own information.`,
5632
+ file: ctx.pdfPath,
5633
+ detail: "The build adds that page for you (`print.lastPage: blank`, or `short-by-one` to stop a page short of a multiple " + `of ${size} and let the printer add it). Check that the interior was built with this manifest, and that nothing in it ` + "forces content onto the final page.",
5634
+ code: "signature-last-page",
5635
+ data
5636
+ })
5637
+ ];
5638
+ }
5639
+ };
5640
+ registerCheck(check9);
5641
+
5642
+ // src/checks/pdf/bookmarks.ts
5643
+ var check10 = {
5495
5644
  id: "pdf.nav.bookmarks",
5496
5645
  name: "Bookmarks",
5497
5646
  description: "Checks for PDF outline (bookmarks) tree",
@@ -5505,14 +5654,14 @@ var check8 = {
5505
5654
  const doc = await loadPdf(ctx.pdfPath);
5506
5655
  if (!doc) {
5507
5656
  return [
5508
- inspectionFailed(check8.id, "Could not inspect PDF for bookmarks.", {
5657
+ inspectionFailed(check10.id, "Could not inspect PDF for bookmarks.", {
5509
5658
  file: ctx.pdfPath
5510
5659
  })
5511
5660
  ];
5512
5661
  }
5513
5662
  if (await getOutlineCount(doc) === 0) {
5514
5663
  return [
5515
- finding(check8.id, {
5664
+ finding(check10.id, {
5516
5665
  severity: "warning",
5517
5666
  message: "PDF does not contain bookmarks (outline tree).",
5518
5667
  file: ctx.pdfPath
@@ -5522,10 +5671,10 @@ var check8 = {
5522
5671
  return [];
5523
5672
  }
5524
5673
  };
5525
- registerCheck(check8);
5674
+ registerCheck(check10);
5526
5675
 
5527
5676
  // src/checks/pdf/toc-links.ts
5528
- var check9 = {
5677
+ var check11 = {
5529
5678
  id: "pdf.nav.toc-links",
5530
5679
  name: "TOC Links",
5531
5680
  description: "Detects link annotations on TOC pages",
@@ -5540,7 +5689,7 @@ var check9 = {
5540
5689
  if (!doc) {
5541
5690
  return [
5542
5691
  {
5543
- checkId: check9.id,
5692
+ checkId: check11.id,
5544
5693
  severity: "warning",
5545
5694
  message: "Could not inspect PDF for TOC links.",
5546
5695
  file: ctx.pdfPath
@@ -5551,7 +5700,7 @@ var check9 = {
5551
5700
  if (!hasLinks) {
5552
5701
  return [
5553
5702
  {
5554
- checkId: check9.id,
5703
+ checkId: check11.id,
5555
5704
  severity: "warning",
5556
5705
  message: "No link annotations found in PDF (expected for TOC).",
5557
5706
  file: ctx.pdfPath
@@ -5561,10 +5710,10 @@ var check9 = {
5561
5710
  return [];
5562
5711
  }
5563
5712
  };
5564
- registerCheck(check9);
5713
+ registerCheck(check11);
5565
5714
 
5566
5715
  // src/checks/pdf/cross-refs.ts
5567
- var check10 = {
5716
+ var check12 = {
5568
5717
  id: "pdf.nav.cross-refs",
5569
5718
  name: "Cross References",
5570
5719
  description: "Counts and verifies internal link annotations",
@@ -5577,7 +5726,7 @@ var check10 = {
5577
5726
  if (!doc) {
5578
5727
  return [
5579
5728
  {
5580
- checkId: check10.id,
5729
+ checkId: check12.id,
5581
5730
  severity: "info",
5582
5731
  message: "Could not inspect PDF for cross-references.",
5583
5732
  file: ctx.pdfPath
@@ -5587,7 +5736,7 @@ var check10 = {
5587
5736
  const linkCount = await countLinkAnnotations(doc);
5588
5737
  return [
5589
5738
  {
5590
- checkId: check10.id,
5739
+ checkId: check12.id,
5591
5740
  severity: "info",
5592
5741
  message: `PDF contains ${linkCount} internal link annotation${linkCount !== 1 ? "s" : ""}.`,
5593
5742
  file: ctx.pdfPath
@@ -5595,10 +5744,10 @@ var check10 = {
5595
5744
  ];
5596
5745
  }
5597
5746
  };
5598
- registerCheck(check10);
5747
+ registerCheck(check12);
5599
5748
 
5600
5749
  // src/checks/pdf/page-labels.ts
5601
- var check11 = {
5750
+ var check13 = {
5602
5751
  id: "pdf.nav.page-labels",
5603
5752
  name: "Page Labels",
5604
5753
  description: "Verifies PDF page labels / folio numbering",
@@ -5613,7 +5762,7 @@ var check11 = {
5613
5762
  const labels = await getPageLabels(doc);
5614
5763
  return [
5615
5764
  {
5616
- checkId: check11.id,
5765
+ checkId: check13.id,
5617
5766
  severity: "info",
5618
5767
  message: labels && labels.length > 0 ? "PDF contains page labels." : "PDF does not contain explicit page labels.",
5619
5768
  file: ctx.pdfPath
@@ -5621,10 +5770,10 @@ var check11 = {
5621
5770
  ];
5622
5771
  }
5623
5772
  };
5624
- registerCheck(check11);
5773
+ registerCheck(check13);
5625
5774
 
5626
5775
  // src/checks/pdf/image-resolution.ts
5627
- var check12 = {
5776
+ var check14 = {
5628
5777
  id: "pdf.print.image-resolution",
5629
5778
  name: "Image Resolution",
5630
5779
  description: "Checks DPI of images embedded in the PDF",
@@ -5651,7 +5800,7 @@ var check12 = {
5651
5800
  lowResPages.sort((a, b) => a - b);
5652
5801
  return [
5653
5802
  {
5654
- checkId: check12.id,
5803
+ checkId: check14.id,
5655
5804
  severity: "warning",
5656
5805
  message: `Low-resolution images (below ${minDpi} DPI) found on pages: ${lowResPages.join(", ")}`,
5657
5806
  file: ctx.pdfPath
@@ -5661,10 +5810,10 @@ var check12 = {
5661
5810
  return [];
5662
5811
  }
5663
5812
  };
5664
- registerCheck(check12);
5813
+ registerCheck(check14);
5665
5814
 
5666
5815
  // src/checks/pdf/transparency.ts
5667
- var check13 = {
5816
+ var check15 = {
5668
5817
  id: "pdf.print.transparency",
5669
5818
  name: "Transparency",
5670
5819
  description: "Checks for transparency markers (/Transparency, /SMask, /BM) in the PDF",
@@ -5686,7 +5835,7 @@ var check13 = {
5686
5835
  if (found.length > 0) {
5687
5836
  return [
5688
5837
  {
5689
- checkId: check13.id,
5838
+ checkId: check15.id,
5690
5839
  severity: "warning",
5691
5840
  message: `Transparency detected in PDF: ${found.join(", ")}. This may cause issues with some printers.`,
5692
5841
  file: ctx.pdfPath
@@ -5696,7 +5845,7 @@ var check13 = {
5696
5845
  return [];
5697
5846
  }
5698
5847
  };
5699
- registerCheck(check13);
5848
+ registerCheck(check15);
5700
5849
 
5701
5850
  // src/checks/pdf/bleed.ts
5702
5851
  function findBox(bytes, box) {
@@ -5706,7 +5855,7 @@ function findBox(bytes, box) {
5706
5855
  return null;
5707
5856
  return [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
5708
5857
  }
5709
- var check14 = {
5858
+ var check16 = {
5710
5859
  id: "pdf.print.bleed",
5711
5860
  name: "Bleed Box",
5712
5861
  description: "Compares MediaBox vs TrimBox/BleedBox to verify bleed area",
@@ -5720,7 +5869,7 @@ var check14 = {
5720
5869
  const bytes = await readPdfBytes(ctx.pdfPath).catch(() => "");
5721
5870
  if (!bytes) {
5722
5871
  return [
5723
- inspectionFailed(check14.id, "Could not inspect PDF for bleed boxes.", {
5872
+ inspectionFailed(check16.id, "Could not inspect PDF for bleed boxes.", {
5724
5873
  file: ctx.pdfPath
5725
5874
  })
5726
5875
  ];
@@ -5730,14 +5879,14 @@ var check14 = {
5730
5879
  const bleed = findBox(bytes, "BleedBox");
5731
5880
  if (!media) {
5732
5881
  return [
5733
- inspectionFailed(check14.id, "Could not parse MediaBox from PDF.", {
5882
+ inspectionFailed(check16.id, "Could not parse MediaBox from PDF.", {
5734
5883
  file: ctx.pdfPath
5735
5884
  })
5736
5885
  ];
5737
5886
  }
5738
5887
  if (!trim && !bleed) {
5739
5888
  return [
5740
- finding(check14.id, {
5889
+ finding(check16.id, {
5741
5890
  severity: "warning",
5742
5891
  message: "No TrimBox or BleedBox found. Bleed area cannot be verified.",
5743
5892
  file: ctx.pdfPath
@@ -5750,7 +5899,7 @@ var check14 = {
5750
5899
  const bleedSize = ctx.config.validate.pdf.bleedSize;
5751
5900
  if (mediaW - trimW < bleedSize * 2 * 0.9) {
5752
5901
  return [
5753
- finding(check14.id, {
5902
+ finding(check16.id, {
5754
5903
  severity: "warning",
5755
5904
  message: `Bleed area appears insufficient. Expected at least ${bleedSize}pt on each side.`,
5756
5905
  file: ctx.pdfPath
@@ -5761,10 +5910,10 @@ var check14 = {
5761
5910
  return [];
5762
5911
  }
5763
5912
  };
5764
- registerCheck(check14);
5913
+ registerCheck(check16);
5765
5914
 
5766
5915
  // src/checks/pdf/pdfx-metadata.ts
5767
- var check15 = {
5916
+ var check17 = {
5768
5917
  id: "pdf.print.pdfx-metadata",
5769
5918
  name: "PDF/X Metadata",
5770
5919
  description: "Verifies PDF/X DOCINFO metadata via qpdf JSON",
@@ -5783,25 +5932,25 @@ var check15 = {
5783
5932
  const objects = parseQpdfObjectsJson(stdout);
5784
5933
  if (!objects) {
5785
5934
  return [
5786
- inspectionFailed(check15.id, "Unable to parse qpdf JSON output for PDF/X metadata checks.", { file: ctx.pdfPath })
5935
+ inspectionFailed(check17.id, "Unable to parse qpdf JSON output for PDF/X metadata checks.", { file: ctx.pdfPath })
5787
5936
  ];
5788
5937
  }
5789
- return getPdfxMetadataIssues(objects, ctx.config.pdfx.flavor).map((message) => finding(check15.id, {
5938
+ return getPdfxMetadataIssues(objects, ctx.config.pdfx.flavor).map((message) => finding(check17.id, {
5790
5939
  severity: "error",
5791
5940
  message,
5792
5941
  file: ctx.pdfPath
5793
5942
  }));
5794
5943
  } catch {
5795
5944
  return [
5796
- inspectionFailed(check15.id, "Failed to inspect PDF objects with qpdf for PDF/X metadata.", { file: ctx.pdfPath })
5945
+ inspectionFailed(check17.id, "Failed to inspect PDF objects with qpdf for PDF/X metadata.", { file: ctx.pdfPath })
5797
5946
  ];
5798
5947
  }
5799
5948
  }
5800
5949
  };
5801
- registerCheck(check15);
5950
+ registerCheck(check17);
5802
5951
 
5803
5952
  // src/checks/source/markdownlint.ts
5804
- import { readFile as readFile9 } from "node:fs/promises";
5953
+ import { readFile as readFile10 } from "node:fs/promises";
5805
5954
  import { lint } from "markdownlint/sync";
5806
5955
  import { parse as parseYaml2 } from "yaml";
5807
5956
 
@@ -5834,7 +5983,7 @@ function stripJsonComments(s) {
5834
5983
  return s.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
5835
5984
  }
5836
5985
  async function loadConfig(configPath) {
5837
- const raw = await readFile9(configPath, "utf8");
5986
+ const raw = await readFile10(configPath, "utf8");
5838
5987
  let parsed;
5839
5988
  if (configPath.endsWith(".json")) {
5840
5989
  parsed = JSON.parse(raw);
@@ -5853,7 +6002,7 @@ async function loadConfig(configPath) {
5853
6002
  return {};
5854
6003
  }
5855
6004
  var MARKDOWNLINT_CHECK_ID = "source.markdownlint";
5856
- var check16 = {
6005
+ var check18 = {
5857
6006
  id: MARKDOWNLINT_CHECK_ID,
5858
6007
  name: "Markdownlint",
5859
6008
  description: "Runs markdownlint with project config to validate Markdown files",
@@ -5878,7 +6027,7 @@ var check16 = {
5878
6027
  continue;
5879
6028
  for (const v of violations) {
5880
6029
  out.push({
5881
- checkId: check16.id,
6030
+ checkId: check18.id,
5882
6031
  severity: "warning",
5883
6032
  message: `${v.ruleDescription} (${v.ruleNames.join("/")})`,
5884
6033
  file,
@@ -5891,15 +6040,15 @@ var check16 = {
5891
6040
  return out;
5892
6041
  }
5893
6042
  };
5894
- registerCheck(check16);
6043
+ registerCheck(check18);
5895
6044
 
5896
6045
  // src/checks/source/htmlhint.ts
5897
6046
  import { existsSync as existsSync7 } from "node:fs";
5898
- import { readFile as readFile10 } from "node:fs/promises";
6047
+ import { readFile as readFile11 } from "node:fs/promises";
5899
6048
  import htmlhint from "htmlhint";
5900
6049
  var { HTMLHint } = htmlhint;
5901
6050
  var CONFIG_NAMES2 = [".htmlhintrc"];
5902
- var check17 = {
6051
+ var check19 = {
5903
6052
  id: "source.htmlhint",
5904
6053
  name: "HTMLHint",
5905
6054
  description: "Runs htmlhint with project config to validate generated HTML",
@@ -5915,11 +6064,11 @@ var check17 = {
5915
6064
  const resolvedConfig = findConfigFile(ctx.inputDir, CONFIG_NAMES2, configPath);
5916
6065
  if (!resolvedConfig && !configPath)
5917
6066
  return [];
5918
- const ruleset = resolvedConfig ? JSON.parse(await readFile10(resolvedConfig, "utf8")) : HTMLHint.defaultRuleset;
5919
- const html = await readFile10(ctx.htmlPath, "utf8");
6067
+ const ruleset = resolvedConfig ? JSON.parse(await readFile11(resolvedConfig, "utf8")) : HTMLHint.defaultRuleset;
6068
+ const html = await readFile11(ctx.htmlPath, "utf8");
5920
6069
  const messages = HTMLHint.verify(html, ruleset);
5921
6070
  return messages.map((m) => ({
5922
- checkId: check17.id,
6071
+ checkId: check19.id,
5923
6072
  severity: m.type === "error" ? "error" : "warning",
5924
6073
  message: `${m.rule.id}: ${m.message}`,
5925
6074
  file: ctx.htmlPath,
@@ -5928,13 +6077,35 @@ var check17 = {
5928
6077
  }));
5929
6078
  }
5930
6079
  };
5931
- registerCheck(check17);
6080
+ registerCheck(check19);
5932
6081
 
5933
6082
  // src/checks/source/stylelint.ts
5934
- import { readFile as readFile11 } from "node:fs/promises";
6083
+ import { readFile as readFile12 } from "node:fs/promises";
5935
6084
 
5936
6085
  // src/lib/printsafe.ts
5937
6086
  import postcss from "postcss";
6087
+
6088
+ // src/lib/lint-rules.ts
6089
+ function ruleLevel(rules, id) {
6090
+ const v = rules?.[id];
6091
+ if (v === "off" || v === false)
6092
+ return "off";
6093
+ if (v === "warn" || v === "warning")
6094
+ return "warn";
6095
+ if (v === "error")
6096
+ return "error";
6097
+ return;
6098
+ }
6099
+ var DIRECTIVE = /^\s*gutterpress-disable-next-line\s+(.*?)\s*$/;
6100
+ function parseDisableDirective(commentText) {
6101
+ const m = DIRECTIVE.exec(commentText);
6102
+ if (!m)
6103
+ return null;
6104
+ const ids = m[1].split(/\s--(?:\s|$)/)[0].split(/[\s,]+/).filter(Boolean);
6105
+ return ids.length > 0 ? ids : null;
6106
+ }
6107
+
6108
+ // src/lib/printsafe.ts
5938
6109
  var ruleRemoteUrls = "printsafe/no-remote-urls";
5939
6110
  var ruleRiskyProps = "printsafe/no-risky-print-effects";
5940
6111
  var ruleSyntax = "printsafe/syntax-error";
@@ -5959,9 +6130,11 @@ var riskyProperties = new Set([
5959
6130
  "animation",
5960
6131
  "animation-name",
5961
6132
  "transition",
5962
- "will-change",
5963
- "clip-path"
6133
+ "will-change"
5964
6134
  ]);
6135
+ function isRasterizingClipPath(decl) {
6136
+ return decl.prop.toLowerCase() === "clip-path" && /\burl\(/i.test(decl.value);
6137
+ }
5965
6138
  var marginBoxAtRuleNames = new Set([
5966
6139
  "top-left-corner",
5967
6140
  "top-left",
@@ -6088,7 +6261,34 @@ function nodeLoc(node) {
6088
6261
  column: node.source?.start?.column ?? 1
6089
6262
  };
6090
6263
  }
6091
- function checkCss(css, from) {
6264
+ function checkCss(css, from, options = {}) {
6265
+ return filterFindings(checkCssRaw(css, from), css, options.rules);
6266
+ }
6267
+ function filterFindings(findings, css, rules) {
6268
+ const silenced = new Map;
6269
+ for (const m of css.matchAll(/\/\*([\s\S]*?)\*\//g)) {
6270
+ const ids = parseDisableDirective(m[1]);
6271
+ if (!ids)
6272
+ continue;
6273
+ const endLine = css.slice(0, m.index + m[0].length).split(`
6274
+ `).length;
6275
+ const set = silenced.get(endLine + 1) ?? new Set;
6276
+ for (const id of ids)
6277
+ set.add(id);
6278
+ silenced.set(endLine + 1, set);
6279
+ }
6280
+ const out = [];
6281
+ for (const f of findings) {
6282
+ if (silenced.get(f.line)?.has(f.rule))
6283
+ continue;
6284
+ const level = ruleLevel(rules, f.rule);
6285
+ if (level === "off")
6286
+ continue;
6287
+ out.push(level === "warn" ? { ...f, severity: "warning" } : level === "error" ? { ...f, severity: "error" } : f);
6288
+ }
6289
+ return out;
6290
+ }
6291
+ function checkCssRaw(css, from) {
6092
6292
  let root;
6093
6293
  try {
6094
6294
  root = postcss.parse(css, from ? { from } : undefined);
@@ -6137,6 +6337,13 @@ function checkCss(css, from) {
6137
6337
  message: `Property is high-risk for print/PDF: 'filter' rasterizes its subtree to a 300 DPI bitmap in the printed PDF (text becomes unselectable, unsearchable, and inaccessible), and it dominates build time (~90% measured; 57.0s -> 6.2s over 60pp when scoped — see ENGINE.md §10). Scope it to the smallest possible selector. It also means the render-parity gate cannot see a text-only change inside it (docs/render-parity-gate.md, "Known blind spot").`,
6138
6338
  ...nodeLoc(decl)
6139
6339
  });
6340
+ } else if (isRasterizingClipPath(decl)) {
6341
+ warnings.push({
6342
+ rule: ruleRiskyProps,
6343
+ severity: "warning",
6344
+ message: `Property is high-risk for print/PDF (can force rasterization): ${decl.prop} with url(...) references an SVG mask. Use a basic shape (polygon(), inset(), circle(), ellipse(), path()) instead — those print as vector clips.`,
6345
+ ...nodeLoc(decl)
6346
+ });
6140
6347
  } else if (riskyProperties.has(prop)) {
6141
6348
  warnings.push({
6142
6349
  rule: ruleRiskyProps,
@@ -6190,7 +6397,7 @@ function checkCss(css, from) {
6190
6397
  }
6191
6398
 
6192
6399
  // src/checks/source/stylelint.ts
6193
- var check18 = {
6400
+ var check20 = {
6194
6401
  id: "source.stylelint",
6195
6402
  name: "Print-safety CSS",
6196
6403
  description: "Checks CSS for print-safety issues (remote URLs, rasterizing effects, and page-layer containment)",
@@ -6207,19 +6414,19 @@ var check18 = {
6207
6414
  for (const file of files) {
6208
6415
  let css;
6209
6416
  try {
6210
- css = await readFile11(file, "utf8");
6417
+ css = await readFile12(file, "utf8");
6211
6418
  } catch (err) {
6212
6419
  results.push({
6213
- checkId: check18.id,
6420
+ checkId: check20.id,
6214
6421
  severity: "error",
6215
6422
  message: `Cannot read stylesheet: ${err instanceof Error ? err.message : String(err)}`,
6216
6423
  file
6217
6424
  });
6218
6425
  continue;
6219
6426
  }
6220
- for (const w of checkCss(css, file)) {
6427
+ for (const w of checkCss(css, file, { rules: ctx.config.lint.rules })) {
6221
6428
  results.push({
6222
- checkId: check18.id,
6429
+ checkId: check20.id,
6223
6430
  severity: w.severity,
6224
6431
  message: `${w.rule}: ${w.message}`,
6225
6432
  file,
@@ -6231,10 +6438,10 @@ var check18 = {
6231
6438
  return results;
6232
6439
  }
6233
6440
  };
6234
- registerCheck(check18);
6441
+ registerCheck(check20);
6235
6442
 
6236
6443
  // src/checks/source/css-ownership.ts
6237
- import { readFile as readFile12 } from "node:fs/promises";
6444
+ import { readFile as readFile13 } from "node:fs/promises";
6238
6445
 
6239
6446
  // src/lib/style-resolver.ts
6240
6447
  import { existsSync as existsSync8 } from "node:fs";
@@ -6302,7 +6509,7 @@ import postcss2 from "postcss";
6302
6509
  import { parse as parseYaml3 } from "yaml";
6303
6510
  var CONFIG_NAMES3 = [".gutterpress/css-contract.yaml", ".gutterpress/css-contract.yml"];
6304
6511
  async function loadContract(configPath) {
6305
- const raw = await readFile12(configPath, "utf8");
6512
+ const raw = await readFile13(configPath, "utf8");
6306
6513
  const parsed = parseYaml3(raw);
6307
6514
  if (parsed && typeof parsed === "object")
6308
6515
  return parsed;
@@ -6415,7 +6622,7 @@ function checkOwnership(root, file, maps) {
6415
6622
  });
6416
6623
  return out;
6417
6624
  }
6418
- var check19 = {
6625
+ var check21 = {
6419
6626
  id: "source.css-ownership",
6420
6627
  name: "CSS ownership contract",
6421
6628
  description: "Checks an optional per-project CSS ownership contract (.gutterpress/css-contract.yaml): which file may declare which properties/at-rules, and which selectors must never appear unscoped.",
@@ -6438,7 +6645,7 @@ var check19 = {
6438
6645
  } catch (err) {
6439
6646
  return [
6440
6647
  {
6441
- checkId: check19.id,
6648
+ checkId: check21.id,
6442
6649
  severity: "error",
6443
6650
  message: `Failed to parse CSS ownership contract "${configPath}": ${err instanceof Error ? err.message : String(err)}`,
6444
6651
  file: configPath
@@ -6453,7 +6660,7 @@ var check19 = {
6453
6660
  continue;
6454
6661
  let css;
6455
6662
  try {
6456
- css = await readFile12(absFile, "utf8");
6663
+ css = await readFile13(absFile, "utf8");
6457
6664
  } catch {
6458
6665
  continue;
6459
6666
  }
@@ -6480,7 +6687,7 @@ var check19 = {
6480
6687
  }
6481
6688
  for (const v of violations) {
6482
6689
  results.push({
6483
- checkId: check19.id,
6690
+ checkId: check21.id,
6484
6691
  severity: "warning",
6485
6692
  message: v.message,
6486
6693
  file: absFile,
@@ -6492,7 +6699,7 @@ var check19 = {
6492
6699
  return results;
6493
6700
  }
6494
6701
  };
6495
- registerCheck(check19);
6702
+ registerCheck(check21);
6496
6703
 
6497
6704
  // src/checks/source/local-refs.ts
6498
6705
  import { existsSync as existsSync9 } from "node:fs";
@@ -6500,7 +6707,7 @@ import { dirname as dirname3, resolve as resolve5 } from "node:path";
6500
6707
 
6501
6708
  // src/lib/asset-inline.ts
6502
6709
  import { createHash as createHash2 } from "node:crypto";
6503
- import { readFile as readFile13 } from "node:fs/promises";
6710
+ import { readFile as readFile14 } from "node:fs/promises";
6504
6711
  import path8 from "node:path";
6505
6712
  import postcss3 from "postcss";
6506
6713
  var FONT_EXTS = new Set([".woff2", ".woff", ".ttf", ".otf"]);
@@ -6565,7 +6772,7 @@ function mimeFor(ext) {
6565
6772
  }
6566
6773
  async function readOrThrow(absPath, kind, referencedFrom) {
6567
6774
  try {
6568
- return await readFile13(absPath);
6775
+ return await readFile14(absPath);
6569
6776
  } catch {
6570
6777
  throw new BuildError(`Missing ${kind}: ${absPath}
6571
6778
  referenced from ${referencedFrom}
@@ -6708,7 +6915,7 @@ async function collectStyleDependencies(projectDir, stylePaths) {
6708
6915
  found.add(abs);
6709
6916
  let source;
6710
6917
  try {
6711
- source = await readFile13(abs, "utf-8");
6918
+ source = await readFile14(abs, "utf-8");
6712
6919
  } catch {
6713
6920
  return;
6714
6921
  }
@@ -6785,7 +6992,7 @@ async function inlineShapeUrls(html, baseDir) {
6785
6992
  continue;
6786
6993
  let bytes;
6787
6994
  try {
6788
- bytes = await readFile13(abs);
6995
+ bytes = await readFile14(abs);
6789
6996
  } catch {
6790
6997
  continue;
6791
6998
  }
@@ -6803,7 +7010,7 @@ async function inlineShapeUrls(html, baseDir) {
6803
7010
  }
6804
7011
 
6805
7012
  // src/checks/source/local-ref-parser.ts
6806
- import { readFile as readFile14 } from "node:fs/promises";
7013
+ import { readFile as readFile15 } from "node:fs/promises";
6807
7014
  function newlineOffsets(content) {
6808
7015
  const offsets = [];
6809
7016
  for (let i = 0;i < content.length; i++)
@@ -6872,7 +7079,7 @@ async function* renderedLocalRefs(ctx, checkId, results) {
6872
7079
  for (const file of files) {
6873
7080
  let refs;
6874
7081
  try {
6875
- refs = collectRenderedLocalRefs(await readFile14(file, "utf8"));
7082
+ refs = collectRenderedLocalRefs(await readFile15(file, "utf8"));
6876
7083
  } catch {
6877
7084
  results.push(inspectionFailed(checkId, `Could not read source file: ${file}`, { file }));
6878
7085
  continue;
@@ -6883,7 +7090,7 @@ async function* renderedLocalRefs(ctx, checkId, results) {
6883
7090
  }
6884
7091
 
6885
7092
  // src/checks/source/local-refs.ts
6886
- var check20 = {
7093
+ var check22 = {
6887
7094
  id: "source.links.local-refs",
6888
7095
  name: "Local Markdown References",
6889
7096
  description: "Checks local markdown links and image refs resolve on disk",
@@ -6891,12 +7098,12 @@ var check20 = {
6891
7098
  phase: "pre-build",
6892
7099
  async run(ctx) {
6893
7100
  const results = [];
6894
- for await (const { ref, kind, line, file } of renderedLocalRefs(ctx, check20.id, results)) {
7101
+ for await (const { ref, kind, line, file } of renderedLocalRefs(ctx, check22.id, results)) {
6895
7102
  if (isNonFilesystemRef(ref))
6896
7103
  continue;
6897
7104
  const escape = kind === "image" ? proseImageRefError(ref, ctx.inputDir) : null;
6898
7105
  if (escape) {
6899
- results.push(finding(check20.id, {
7106
+ results.push(finding(check22.id, {
6900
7107
  severity: "error",
6901
7108
  message: escape,
6902
7109
  file,
@@ -6906,7 +7113,7 @@ var check20 = {
6906
7113
  }
6907
7114
  if (localRefExists(ref, kind, file, ctx.inputDir))
6908
7115
  continue;
6909
- results.push(finding(check20.id, {
7116
+ results.push(finding(check22.id, {
6910
7117
  severity: kind === "image" ? "warning" : "error",
6911
7118
  code: kind === "image" ? "missing-image-placeholder" : undefined,
6912
7119
  message: kind === "image" ? `Local image not found; build will substitute a magenta placeholder: ${ref}` : `Local reference not found: ${ref}`,
@@ -6931,7 +7138,7 @@ function localRefExists(ref, kind, sourceFile, projectRoot) {
6931
7138
  return existsSync9(fileRelative);
6932
7139
  }
6933
7140
  }
6934
- registerCheck(check20);
7141
+ registerCheck(check22);
6935
7142
 
6936
7143
  // src/lib/build-staging.ts
6937
7144
  import path9 from "node:path";
@@ -7197,7 +7404,7 @@ async function createStageRoot() {
7197
7404
  }
7198
7405
 
7199
7406
  // src/checks/source/dangling-links.ts
7200
- var check21 = {
7407
+ var check23 = {
7201
7408
  id: "source.links.dangling",
7202
7409
  name: "Dangling Links",
7203
7410
  description: "Flags relative links the printed book cannot open (their href is dropped at print time)",
@@ -7205,10 +7412,10 @@ var check21 = {
7205
7412
  phase: "pre-build",
7206
7413
  async run(ctx) {
7207
7414
  const results = [];
7208
- for await (const { ref, kind, line, file } of renderedLocalRefs(ctx, check21.id, results)) {
7415
+ for await (const { ref, kind, line, file } of renderedLocalRefs(ctx, check23.id, results)) {
7209
7416
  if (kind !== "link" || isPrintResolvableHref(ref))
7210
7417
  continue;
7211
- results.push(finding(check21.id, {
7418
+ results.push(finding(check23.id, {
7212
7419
  severity: "warning",
7213
7420
  code: "dangling-link",
7214
7421
  data: { ref },
@@ -7220,11 +7427,11 @@ var check21 = {
7220
7427
  return results;
7221
7428
  }
7222
7429
  };
7223
- registerCheck(check21);
7430
+ registerCheck(check23);
7224
7431
 
7225
7432
  // src/checks/source/accessibility-alt-text.ts
7226
- import { readFile as readFile15 } from "node:fs/promises";
7227
- var check22 = {
7433
+ import { readFile as readFile16 } from "node:fs/promises";
7434
+ var check24 = {
7228
7435
  id: "source.accessibility.alt-text",
7229
7436
  name: "Image Alt Text",
7230
7437
  description: "Checks markdown images include non-empty alt text",
@@ -7237,7 +7444,7 @@ var check22 = {
7237
7444
  const results = [];
7238
7445
  for (const file of files) {
7239
7446
  try {
7240
- const content = await readFile15(file, "utf8");
7447
+ const content = await readFile16(file, "utf8");
7241
7448
  const lines = content.split(`
7242
7449
  `);
7243
7450
  let inFence = false;
@@ -7253,7 +7460,7 @@ var check22 = {
7253
7460
  if (alt.trim())
7254
7461
  continue;
7255
7462
  results.push({
7256
- checkId: check22.id,
7463
+ checkId: check24.id,
7257
7464
  severity: "warning",
7258
7465
  message: "Image is missing alt text",
7259
7466
  file,
@@ -7278,11 +7485,11 @@ function extractImageAlts(line) {
7278
7485
  }
7279
7486
  return alts;
7280
7487
  }
7281
- registerCheck(check22);
7488
+ registerCheck(check24);
7282
7489
 
7283
7490
  // src/checks/source/accessibility-heading-order.ts
7284
- import { readFile as readFile16 } from "node:fs/promises";
7285
- var check23 = {
7491
+ import { readFile as readFile17 } from "node:fs/promises";
7492
+ var check25 = {
7286
7493
  id: "source.accessibility.heading-order",
7287
7494
  name: "Heading Order",
7288
7495
  description: "Checks markdown heading levels do not jump by more than one",
@@ -7295,11 +7502,12 @@ var check23 = {
7295
7502
  const results = [];
7296
7503
  for (const file of files) {
7297
7504
  try {
7298
- const content = await readFile16(file, "utf8");
7505
+ const content = await readFile17(file, "utf8");
7299
7506
  const lines = content.split(`
7300
7507
  `);
7301
7508
  let inFence = false;
7302
7509
  let prevLevel;
7510
+ let silencedLine = -1;
7303
7511
  for (let i = 0;i < lines.length; i++) {
7304
7512
  const line = lines[i] ?? "";
7305
7513
  if (/^\s*(```|~~~)/.test(line)) {
@@ -7308,13 +7516,18 @@ var check23 = {
7308
7516
  }
7309
7517
  if (inFence)
7310
7518
  continue;
7519
+ const comment = /^\s*<!--(.*)-->\s*$/.exec(line);
7520
+ if (comment && parseDisableDirective(comment[1])?.includes(check25.id)) {
7521
+ silencedLine = i + 1;
7522
+ continue;
7523
+ }
7311
7524
  const match = line.match(/^(#{1,6})\s+\S/);
7312
7525
  if (!match)
7313
7526
  continue;
7314
7527
  const level = match[1].length;
7315
- if (prevLevel != null && level > prevLevel + 1) {
7528
+ if (prevLevel != null && level > prevLevel + 1 && i !== silencedLine) {
7316
7529
  results.push({
7317
- checkId: check23.id,
7530
+ checkId: check25.id,
7318
7531
  severity: "warning",
7319
7532
  message: `Heading level jump from h${prevLevel} to h${level}`,
7320
7533
  file,
@@ -7328,11 +7541,11 @@ var check23 = {
7328
7541
  return results;
7329
7542
  }
7330
7543
  };
7331
- registerCheck(check23);
7544
+ registerCheck(check25);
7332
7545
 
7333
7546
  // src/checks/source/layout-markers.ts
7334
- import { readFile as readFile17 } from "node:fs/promises";
7335
- var check24 = {
7547
+ import { readFile as readFile18 } from "node:fs/promises";
7548
+ var check26 = {
7336
7549
  id: "source.markdown.layout-markers",
7337
7550
  name: "Layout Markers",
7338
7551
  description: "Reports @page/@section/@chapter marker arguments Gutterpress could not understand, plus any unknown gp-* class",
@@ -7344,16 +7557,16 @@ var check24 = {
7344
7557
  return [];
7345
7558
  const results = [];
7346
7559
  const plugins = await loadPlugins(ctx.config.extensions, ctx.inputDir, (ref, error2) => {
7347
- results.push(inspectionFailed(check24.id, `Plugin "${ref}" could not be loaded, so markers it defines were not checked: ${error2.message}`));
7560
+ results.push(inspectionFailed(check26.id, `Plugin "${ref}" could not be loaded, so markers it defines were not checked: ${error2.message}`));
7348
7561
  });
7349
7562
  const md = createMarkdownRenderer(plugins);
7350
7563
  for (const file of files) {
7351
7564
  try {
7352
- const content = await readFile17(file, "utf8");
7565
+ const content = await readFile18(file, "utf8");
7353
7566
  const env = {};
7354
7567
  md.render(content, env);
7355
7568
  for (const w of env.layoutWarnings ?? []) {
7356
- results.push(finding(check24.id, {
7569
+ results.push(finding(check26.id, {
7357
7570
  severity: w.severity ?? "warning",
7358
7571
  message: w.message,
7359
7572
  file,
@@ -7362,16 +7575,16 @@ var check24 = {
7362
7575
  }));
7363
7576
  }
7364
7577
  } catch (error2) {
7365
- results.push(inspectionFailed(check24.id, `Could not check layout markers in ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`, { file }));
7578
+ results.push(inspectionFailed(check26.id, `Could not check layout markers in ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`, { file }));
7366
7579
  }
7367
7580
  }
7368
7581
  return results;
7369
7582
  }
7370
7583
  };
7371
- registerCheck(check24);
7584
+ registerCheck(check26);
7372
7585
 
7373
7586
  // src/checks/source/merge-markers.ts
7374
- import { readFile as readFile18 } from "node:fs/promises";
7587
+ import { readFile as readFile19 } from "node:fs/promises";
7375
7588
  import path10 from "node:path";
7376
7589
 
7377
7590
  // src/checks/asset/extensions.ts
@@ -7401,7 +7614,7 @@ var CLOSE_SENTINEL = ">>>>>>> online version";
7401
7614
  function withoutOnlineTag(relPath) {
7402
7615
  return relPath.replace(/\.online(?=\.[^./]*$|$)/, "");
7403
7616
  }
7404
- var check25 = {
7617
+ var check27 = {
7405
7618
  id: "source.sync.merge-markers",
7406
7619
  name: "Combined Versions",
7407
7620
  description: "Finds passages and files still holding two versions (yours and the online copy) after a sync",
@@ -7413,9 +7626,9 @@ var check25 = {
7413
7626
  for (const file of textFiles) {
7414
7627
  let content;
7415
7628
  try {
7416
- content = await readFile18(file, "utf8");
7629
+ content = await readFile19(file, "utf8");
7417
7630
  } catch {
7418
- results.push(inspectionFailed(check25.id, `Could not read source file: ${file}`, { file }));
7631
+ results.push(inspectionFailed(check27.id, `Could not read source file: ${file}`, { file }));
7419
7632
  continue;
7420
7633
  }
7421
7634
  if (!content.includes(OPEN_SENTINEL) && !content.includes(CLOSE_SENTINEL))
@@ -7427,7 +7640,7 @@ var check25 = {
7427
7640
  const line = lines[i].endsWith("\r") ? lines[i].slice(0, -1) : lines[i];
7428
7641
  if (line === OPEN_SENTINEL) {
7429
7642
  inBlock = true;
7430
- results.push(finding(check25.id, {
7643
+ results.push(finding(check27.id, {
7431
7644
  severity: "error",
7432
7645
  code: "two-versions-passage",
7433
7646
  message: "This passage has two versions (yours and the online copy) — keep what you want, then delete the marker lines.",
@@ -7436,7 +7649,7 @@ var check25 = {
7436
7649
  }));
7437
7650
  } else if (line === CLOSE_SENTINEL) {
7438
7651
  if (!inBlock) {
7439
- results.push(finding(check25.id, {
7652
+ results.push(finding(check27.id, {
7440
7653
  severity: "error",
7441
7654
  code: "leftover-version-marker",
7442
7655
  message: "This marker line is left over from combining two versions — delete it.",
@@ -7457,7 +7670,7 @@ var check25 = {
7457
7670
  });
7458
7671
  for (const sibling of siblings.sort()) {
7459
7672
  const rel = path10.relative(ctx.inputDir, sibling).split(path10.sep).join("/");
7460
- results.push(finding(check25.id, {
7673
+ results.push(finding(check27.id, {
7461
7674
  severity: "warning",
7462
7675
  code: "kept-both-versions",
7463
7676
  message: `Two versions of ${withoutOnlineTag(rel)} are in your book — keep the one you want, then delete the other.`,
@@ -7465,18 +7678,18 @@ var check25 = {
7465
7678
  }));
7466
7679
  }
7467
7680
  } catch (error2) {
7468
- results.push(inspectionFailed(check25.id, `Could not scan for kept-both files: ${error2 instanceof Error ? error2.message : String(error2)}`));
7681
+ results.push(inspectionFailed(check27.id, `Could not scan for kept-both files: ${error2 instanceof Error ? error2.message : String(error2)}`));
7469
7682
  }
7470
7683
  return results;
7471
7684
  }
7472
7685
  };
7473
- registerCheck(check25);
7686
+ registerCheck(check27);
7474
7687
 
7475
7688
  // src/checks/asset/image-file-size.ts
7476
7689
  import { stat as stat4 } from "node:fs/promises";
7477
7690
 
7478
7691
  // src/lib/image-inspect.ts
7479
- import { stat as stat3, readFile as readFile19 } from "node:fs/promises";
7692
+ import { stat as stat3, readFile as readFile20 } from "node:fs/promises";
7480
7693
  var DEFAULT_DPI = 72;
7481
7694
  var cache = new Map;
7482
7695
  async function inspectImage(path11) {
@@ -7491,7 +7704,7 @@ async function inspectImage(path11) {
7491
7704
  return hit.info;
7492
7705
  let info2 = null;
7493
7706
  try {
7494
- const buf = await readFile19(path11);
7707
+ const buf = await readFile20(path11);
7495
7708
  info2 = parseImage(buf);
7496
7709
  } catch {
7497
7710
  info2 = null;
@@ -7696,7 +7909,7 @@ async function collectImageFiles(dirs, exts, ignore2 = ASSET_SCAN_IGNORE_GLOBS)
7696
7909
  }
7697
7910
 
7698
7911
  // src/checks/asset/image-file-size.ts
7699
- var check26 = {
7912
+ var check28 = {
7700
7913
  id: "asset.image.file-size",
7701
7914
  name: "Image File Size",
7702
7915
  description: "Checks that image files do not exceed the maximum size limit",
@@ -7717,14 +7930,14 @@ var check26 = {
7717
7930
  if (info2.size > maxSize) {
7718
7931
  const sizeMb = (info2.size / 1e6).toFixed(1);
7719
7932
  const maxMb = (maxSize / 1e6).toFixed(1);
7720
- results.push(finding(check26.id, {
7933
+ results.push(finding(check28.id, {
7721
7934
  severity: "warning",
7722
7935
  message: `Image file too large: ${sizeMb}MB (max ${maxMb}MB)`,
7723
7936
  file
7724
7937
  }));
7725
7938
  }
7726
7939
  } catch {
7727
- results.push(inspectionFailed(check26.id, `Could not stat image file: ${file}`, {
7940
+ results.push(inspectionFailed(check28.id, `Could not stat image file: ${file}`, {
7728
7941
  file
7729
7942
  }));
7730
7943
  }
@@ -7732,10 +7945,10 @@ var check26 = {
7732
7945
  return results;
7733
7946
  }
7734
7947
  };
7735
- registerCheck(check26);
7948
+ registerCheck(check28);
7736
7949
 
7737
7950
  // src/checks/asset/image-resolution.ts
7738
- var check27 = {
7951
+ var check29 = {
7739
7952
  id: "asset.image.resolution",
7740
7953
  name: "Image Resolution",
7741
7954
  description: "Checks source image DPI from embedded density metadata",
@@ -7757,7 +7970,7 @@ var check27 = {
7757
7970
  const { xDpi, yDpi } = info2;
7758
7971
  if (xDpi > 0 && yDpi > 0 && (xDpi < minDpi || yDpi < minDpi)) {
7759
7972
  results.push({
7760
- checkId: check27.id,
7973
+ checkId: check29.id,
7761
7974
  severity: "warning",
7762
7975
  message: `Image resolution too low: ${xDpi}x${yDpi} DPI (minimum ${minDpi} DPI)`,
7763
7976
  file
@@ -7767,7 +7980,7 @@ var check27 = {
7767
7980
  return results;
7768
7981
  }
7769
7982
  };
7770
- registerCheck(check27);
7983
+ registerCheck(check29);
7771
7984
 
7772
7985
  // src/checks/asset/image-color-space.ts
7773
7986
  var LABEL = {
@@ -7775,7 +7988,7 @@ var LABEL = {
7775
7988
  gray: "Gray",
7776
7989
  cmyk: "CMYK"
7777
7990
  };
7778
- var check28 = {
7991
+ var check30 = {
7779
7992
  id: "asset.image.color-space",
7780
7993
  name: "Image Color Space",
7781
7994
  description: "Validates image color spaces against allowed list",
@@ -7796,7 +8009,7 @@ var check28 = {
7796
8009
  const cs = info2?.colorSpace;
7797
8010
  if (cs && !allowedLower.has(cs)) {
7798
8011
  results.push({
7799
- checkId: check28.id,
8012
+ checkId: check30.id,
7800
8013
  severity: "warning",
7801
8014
  message: `Image uses ${LABEL[cs] ?? cs} color space (allowed: ${allowed.join(", ")})`,
7802
8015
  file
@@ -7806,10 +8019,10 @@ var check28 = {
7806
8019
  return results;
7807
8020
  }
7808
8021
  };
7809
- registerCheck(check28);
8022
+ registerCheck(check30);
7810
8023
 
7811
8024
  // src/checks/asset/image-alpha.ts
7812
- var check29 = {
8025
+ var check31 = {
7813
8026
  id: "asset.image.alpha-channel",
7814
8027
  name: "Image Alpha Channel",
7815
8028
  description: "Checks for alpha channels in PNG/TIFF images",
@@ -7827,7 +8040,7 @@ var check29 = {
7827
8040
  const info2 = await inspectImage(file);
7828
8041
  if (info2?.hasAlpha) {
7829
8042
  results.push({
7830
- checkId: check29.id,
8043
+ checkId: check31.id,
7831
8044
  severity: "warning",
7832
8045
  message: "Image contains alpha channel, which may cause print issues",
7833
8046
  file
@@ -7837,10 +8050,10 @@ var check29 = {
7837
8050
  return results;
7838
8051
  }
7839
8052
  };
7840
- registerCheck(check29);
8053
+ registerCheck(check31);
7841
8054
 
7842
8055
  // src/checks/asset/image-tac.ts
7843
- var check30 = {
8056
+ var check32 = {
7844
8057
  id: "asset.image.tac-raster",
7845
8058
  name: "Image TAC (Raster)",
7846
8059
  description: "Rasterizes and checks TAC per image using Ghostscript",
@@ -7859,7 +8072,7 @@ var check30 = {
7859
8072
  const ghostscript = await resolveGhostscript();
7860
8073
  if (!ghostscript) {
7861
8074
  return [
7862
- inspectionFailed(check30.id, "Could not inspect image ink coverage: Ghostscript executable not found")
8075
+ inspectionFailed(check32.id, "Could not inspect image ink coverage: Ghostscript executable not found")
7863
8076
  ];
7864
8077
  }
7865
8078
  for (const file of files) {
@@ -7877,7 +8090,7 @@ var check30 = {
7877
8090
  if (nums.length === 4 && nums.every((n) => Number.isFinite(n))) {
7878
8091
  const tac = (nums[0] + nums[1] + nums[2] + nums[3]) * 100;
7879
8092
  if (tac > maxTac) {
7880
- results.push(finding(check30.id, {
8093
+ results.push(finding(check32.id, {
7881
8094
  severity: "warning",
7882
8095
  message: `Image TAC exceeds limit: ${tac.toFixed(1)}% (max ${maxTac}%)`,
7883
8096
  file,
@@ -7889,16 +8102,16 @@ var check30 = {
7889
8102
  }
7890
8103
  }
7891
8104
  } catch {
7892
- results.push(inspectionFailed(check30.id, `Could not inspect image ink coverage: ${file}`, { file }));
8105
+ results.push(inspectionFailed(check32.id, `Could not inspect image ink coverage: ${file}`, { file }));
7893
8106
  }
7894
8107
  }
7895
8108
  return results;
7896
8109
  }
7897
8110
  };
7898
- registerCheck(check30);
8111
+ registerCheck(check32);
7899
8112
 
7900
8113
  // src/checks/asset/approved-fonts.ts
7901
- var check31 = {
8114
+ var check33 = {
7902
8115
  id: "asset.font.approved-files",
7903
8116
  name: "Approved Font Files",
7904
8117
  description: "Checks font files against the approved file patterns",
@@ -7937,7 +8150,7 @@ var check31 = {
7937
8150
  for (const font of allFonts) {
7938
8151
  if (!approvedFonts.has(font)) {
7939
8152
  results.push({
7940
- checkId: check31.id,
8153
+ checkId: check33.id,
7941
8154
  severity: "warning",
7942
8155
  message: "Font file not in approved list",
7943
8156
  file: font
@@ -7947,7 +8160,7 @@ var check31 = {
7947
8160
  return results;
7948
8161
  }
7949
8162
  };
7950
- registerCheck(check31);
8163
+ registerCheck(check33);
7951
8164
 
7952
8165
  // src/checks/asset/font-license.ts
7953
8166
  import { existsSync as existsSync10 } from "node:fs";
@@ -7962,7 +8175,7 @@ var LICENSE_NAMES = [
7962
8175
  "OFL-1.1.txt",
7963
8176
  "COPYING"
7964
8177
  ];
7965
- var check32 = {
8178
+ var check34 = {
7966
8179
  id: "asset.font.license",
7967
8180
  name: "Font License",
7968
8181
  description: "Checks for license files in font directories",
@@ -7989,7 +8202,7 @@ var check32 = {
7989
8202
  const hasLicense = LICENSE_NAMES.some((name) => existsSync10(resolve6(fontDir, name)));
7990
8203
  if (!hasLicense) {
7991
8204
  results.push({
7992
- checkId: check32.id,
8205
+ checkId: check34.id,
7993
8206
  severity: "warning",
7994
8207
  message: `No font license file found in directory`,
7995
8208
  file: fontDir
@@ -7999,10 +8212,10 @@ var check32 = {
7999
8212
  return results;
8000
8213
  }
8001
8214
  };
8002
- registerCheck(check32);
8215
+ registerCheck(check34);
8003
8216
 
8004
8217
  // src/checks/heuristic/text-density.ts
8005
- var check33 = {
8218
+ var check35 = {
8006
8219
  id: "heuristic.whitespace.text-density",
8007
8220
  name: "Text Density",
8008
8221
  description: "Checks characters-per-page ratio",
@@ -8031,7 +8244,7 @@ var check33 = {
8031
8244
  });
8032
8245
  if (lowPages.length > 0) {
8033
8246
  results.push({
8034
- checkId: check33.id,
8247
+ checkId: check35.id,
8035
8248
  severity: "info",
8036
8249
  message: `Low text density on pages: ${lowPages.join(", ")} (below ${range.min} chars)`,
8037
8250
  file: ctx.pdfPath
@@ -8039,7 +8252,7 @@ var check33 = {
8039
8252
  }
8040
8253
  if (highPages.length > 0) {
8041
8254
  results.push({
8042
- checkId: check33.id,
8255
+ checkId: check35.id,
8043
8256
  severity: "info",
8044
8257
  message: `High text density on pages: ${highPages.join(", ")} (above ${range.max} chars)`,
8045
8258
  file: ctx.pdfPath
@@ -8048,11 +8261,11 @@ var check33 = {
8048
8261
  return results;
8049
8262
  }
8050
8263
  };
8051
- registerCheck(check33);
8264
+ registerCheck(check35);
8052
8265
 
8053
8266
  // src/checks/heuristic/section-density.ts
8054
- import { readFile as readFile20 } from "node:fs/promises";
8055
- var check34 = {
8267
+ import { readFile as readFile21 } from "node:fs/promises";
8268
+ var check36 = {
8056
8269
  id: "heuristic.chunking.section-density",
8057
8270
  name: "Section Density",
8058
8271
  description: "Checks heading/paragraph/callout density from source Markdown",
@@ -8068,7 +8281,7 @@ var check34 = {
8068
8281
  const results = [];
8069
8282
  for (const file of files) {
8070
8283
  try {
8071
- const content = await readFile20(file, "utf8");
8284
+ const content = await readFile21(file, "utf8");
8072
8285
  const lines = content.split(`
8073
8286
  `);
8074
8287
  let currentSectionStart = 0;
@@ -8078,7 +8291,7 @@ var check34 = {
8078
8291
  const line = lines[i];
8079
8292
  if (/^#{1,6}\s/.test(line)) {
8080
8293
  if (paragraphCount > maxParas) {
8081
- results.push(finding(check34.id, {
8294
+ results.push(finding(check36.id, {
8082
8295
  severity: "info",
8083
8296
  message: `Section has ${paragraphCount} paragraphs (max recommended: ${maxParas})`,
8084
8297
  file,
@@ -8098,7 +8311,7 @@ var check34 = {
8098
8311
  }
8099
8312
  }
8100
8313
  if (paragraphCount > maxParas) {
8101
- results.push(finding(check34.id, {
8314
+ results.push(finding(check36.id, {
8102
8315
  severity: "info",
8103
8316
  message: `Section has ${paragraphCount} paragraphs (max recommended: ${maxParas})`,
8104
8317
  file,
@@ -8106,7 +8319,7 @@ var check34 = {
8106
8319
  }));
8107
8320
  }
8108
8321
  } catch {
8109
- results.push(inspectionFailed(check34.id, `Could not read source file: ${file}`, {
8322
+ results.push(inspectionFailed(check36.id, `Could not read source file: ${file}`, {
8110
8323
  file
8111
8324
  }));
8112
8325
  }
@@ -8114,10 +8327,10 @@ var check34 = {
8114
8327
  return results;
8115
8328
  }
8116
8329
  };
8117
- registerCheck(check34);
8330
+ registerCheck(check36);
8118
8331
 
8119
8332
  // src/checks/heuristic/layer-count.ts
8120
- var check35 = {
8333
+ var check37 = {
8121
8334
  id: "heuristic.decoration.layer-count",
8122
8335
  name: "Layer Count",
8123
8336
  description: "Counts image objects per page",
@@ -8142,7 +8355,7 @@ var check35 = {
8142
8355
  heavyPages.sort((a, b) => a - b);
8143
8356
  return [
8144
8357
  {
8145
- checkId: check35.id,
8358
+ checkId: check37.id,
8146
8359
  severity: "info",
8147
8360
  message: `Pages with many image layers (>${maxLayers}): ${heavyPages.join(", ")}`,
8148
8361
  file: ctx.pdfPath
@@ -8152,10 +8365,10 @@ var check35 = {
8152
8365
  return [];
8153
8366
  }
8154
8367
  };
8155
- registerCheck(check35);
8368
+ registerCheck(check37);
8156
8369
 
8157
8370
  // src/checks/heuristic/placement-variance.ts
8158
- var check36 = {
8371
+ var check38 = {
8159
8372
  id: "heuristic.layout.placement-variance",
8160
8373
  name: "Placement Variance",
8161
8374
  description: "Analyzes text baseline coordinates for layout consistency",
@@ -8173,7 +8386,7 @@ var check36 = {
8173
8386
  const uniqueX = new Set(positions.map((p) => Math.round(p.x)));
8174
8387
  return [
8175
8388
  {
8176
- checkId: check36.id,
8389
+ checkId: check38.id,
8177
8390
  severity: "info",
8178
8391
  message: `Layout analysis: ${uniqueX.size} unique horizontal text positions across ${positions.length} text blocks.`,
8179
8392
  file: ctx.pdfPath
@@ -8181,21 +8394,21 @@ var check36 = {
8181
8394
  ];
8182
8395
  }
8183
8396
  };
8184
- registerCheck(check36);
8397
+ registerCheck(check38);
8185
8398
 
8186
8399
  // src/checks/tool-check.ts
8187
8400
  async function checkToolAvailability(config, opts = {}) {
8188
8401
  const { checks: checks2 } = selectChecks(opts, config);
8189
8402
  const toolToChecks = new Map;
8190
- for (const check37 of checks2) {
8191
- if (!check37.requiredTools?.length)
8403
+ for (const check39 of checks2) {
8404
+ if (!check39.requiredTools?.length)
8192
8405
  continue;
8193
- for (const tool of check37.requiredTools) {
8406
+ for (const tool of check39.requiredTools) {
8194
8407
  const existing = toolToChecks.get(tool);
8195
8408
  if (existing) {
8196
- existing.push(check37.id);
8409
+ existing.push(check39.id);
8197
8410
  } else {
8198
- toolToChecks.set(tool, [check37.id]);
8411
+ toolToChecks.set(tool, [check39.id]);
8199
8412
  }
8200
8413
  }
8201
8414
  }
@@ -8211,11 +8424,11 @@ async function checkToolAvailability(config, opts = {}) {
8211
8424
  const missing = results.filter((r) => !r.found).map((r) => r.tool);
8212
8425
  const missingSet = new Set(missing);
8213
8426
  const skippedChecks = [];
8214
- for (const check37 of checks2) {
8215
- if (!check37.requiredTools?.length)
8427
+ for (const check39 of checks2) {
8428
+ if (!check39.requiredTools?.length)
8216
8429
  continue;
8217
- if (check37.requiredTools.some((t) => missingSet.has(t))) {
8218
- skippedChecks.push(check37.id);
8430
+ if (check39.requiredTools.some((t) => missingSet.has(t))) {
8431
+ skippedChecks.push(check39.id);
8219
8432
  }
8220
8433
  }
8221
8434
  return { available, missing, skippedChecks, toolToChecks };
@@ -8254,7 +8467,7 @@ function artifactName(title, format) {
8254
8467
  var BOOK_HTML_FILENAME = "book.html";
8255
8468
 
8256
8469
  // src/lib/markdown/index.ts
8257
- import { readdir as readdir3, readFile as readFile21, writeFile as writeFile2, mkdir as mkdir4 } from "node:fs/promises";
8470
+ import { readdir as readdir3, readFile as readFile22, writeFile as writeFile2, mkdir as mkdir4 } from "node:fs/promises";
8258
8471
 
8259
8472
  // src/lib/remote-auth/sync-messages.ts
8260
8473
  function isOnlineSibling(filepath) {
@@ -8333,7 +8546,8 @@ ${projectCss.trim()}` : null
8333
8546
  `);
8334
8547
  const preloadTags = (opts.preloadImages ?? []).map((href) => `
8335
8548
  <link rel="preload" as="image" href="${href}">`).join("");
8336
- const printAttrs = (opts.print?.signature && opts.print.signature > 1 ? ` data-gp-signature="${opts.print.signature}"` : "") + (opts.print?.reserveLastPage ? " data-gp-reserve-last-page" : "");
8549
+ const lastPage = opts.print && lastPageMode(opts.print);
8550
+ const printAttrs = (opts.print?.signature && opts.print.signature > 1 ? ` data-gp-signature="${opts.print.signature}"` : "") + (lastPage ? ` data-gp-last-page="${lastPage}"` : "");
8337
8551
  return `<!DOCTYPE html>
8338
8552
  <html lang="en"${printAttrs}>
8339
8553
  <head>
@@ -8393,7 +8607,7 @@ ${css.trim()}
8393
8607
  }
8394
8608
  return assembleBookHtml({
8395
8609
  files,
8396
- readText: (relPath) => readFile21(join2(inputDir, canonicalChapterId(relPath)), "utf-8"),
8610
+ readText: (relPath) => readFile22(join2(inputDir, canonicalChapterId(relPath)), "utf-8"),
8397
8611
  projectCss: inlined.css,
8398
8612
  preloadImages: cssAssetCopies.map((c) => c.to),
8399
8613
  title: opts.title,
@@ -8425,7 +8639,7 @@ async function renderChaptersToFile(inputDir, outDir, opts = {}) {
8425
8639
  }
8426
8640
 
8427
8641
  // src/lib/markdownlint-fix.ts
8428
- import { readFile as readFile22, writeFile as writeFile3 } from "node:fs/promises";
8642
+ import { readFile as readFile23, writeFile as writeFile3 } from "node:fs/promises";
8429
8643
  import { applyFixes } from "markdownlint";
8430
8644
  import { lint as lint2 } from "markdownlint/sync";
8431
8645
  async function uncommittedAmong(inputDir, files) {
@@ -8465,7 +8679,7 @@ async function applyMarkdownlintFixes(ctx) {
8465
8679
  for (const [file, violations] of Object.entries(results)) {
8466
8680
  if (!Array.isArray(violations) || violations.length === 0)
8467
8681
  continue;
8468
- const before = await readFile22(file, "utf8");
8682
+ const before = await readFile23(file, "utf8");
8469
8683
  const after = applyFixes(before, violations);
8470
8684
  if (after !== before)
8471
8685
  pending.push({ file, content: after });
@@ -8574,25 +8788,32 @@ async function runChecks(ctx, opts = {}) {
8574
8788
  }
8575
8789
  const release = retainPdfCache();
8576
8790
  try {
8577
- for (const check37 of checks2) {
8791
+ for (const check39 of checks2) {
8578
8792
  try {
8579
- const results = await check37.run(ctx);
8580
- const severityOverride = getCheckSeverityOverride(check37.id, ctx.config);
8793
+ let results = await check39.run(ctx);
8794
+ const severityOverride = getCheckSeverityOverride(check39.id, ctx.config);
8581
8795
  if (severityOverride) {
8582
8796
  for (const r of results) {
8583
8797
  r.severity = severityOverride;
8584
8798
  }
8585
8799
  }
8800
+ const lintLevel = ruleLevel(ctx.config.lint.rules, check39.id);
8801
+ if (lintLevel === "off")
8802
+ results = [];
8803
+ else if (lintLevel) {
8804
+ for (const r of results)
8805
+ r.severity = lintLevel === "warn" ? "warning" : "error";
8806
+ }
8586
8807
  if (results.length === 0) {
8587
- passed.push(check37.id);
8808
+ passed.push(check39.id);
8588
8809
  } else {
8589
8810
  allResults.push(...results);
8590
8811
  }
8591
8812
  } catch (err) {
8592
8813
  allResults.push({
8593
- checkId: check37.id,
8814
+ checkId: check39.id,
8594
8815
  severity: "error",
8595
- message: `Check "${check37.id}" threw: ${err instanceof Error ? err.message : String(err)}`
8816
+ message: `Check "${check39.id}" threw: ${err instanceof Error ? err.message : String(err)}`
8596
8817
  });
8597
8818
  }
8598
8819
  }
@@ -8884,7 +9105,7 @@ async function executeValidation(args) {
8884
9105
  };
8885
9106
  if (args.fix) {
8886
9107
  const selected = selectChecks(runnerOptions, config).checks;
8887
- if (selected.some((check37) => check37.id === MARKDOWNLINT_CHECK_ID)) {
9108
+ if (selected.some((check39) => check39.id === MARKDOWNLINT_CHECK_ID)) {
8888
9109
  await applyMarkdownlintFixes(context);
8889
9110
  } else {
8890
9111
  log.warn(`--fix: ${MARKDOWNLINT_CHECK_ID} is not part of this run — no files were fixed.`);
@@ -8971,7 +9192,7 @@ import { randomBytes } from "node:crypto";
8971
9192
 
8972
9193
  // src/lib/build-fingerprint.ts
8973
9194
  import * as fs from "node:fs";
8974
- import { mkdir as mkdir5, readFile as readFile23, writeFile as writeFile4 } from "node:fs/promises";
9195
+ import { mkdir as mkdir5, readFile as readFile24, writeFile as writeFile4 } from "node:fs/promises";
8975
9196
  import { createHash as createHash4 } from "node:crypto";
8976
9197
  import path12 from "node:path";
8977
9198
  import git from "isomorphic-git";
@@ -9058,8 +9279,8 @@ async function getEngineBundleHash() {
9058
9279
  getAssetPath("engine/gutterpress-agent.js")
9059
9280
  ]);
9060
9281
  const [viewerBytes, agentBytes] = await Promise.all([
9061
- readFile23(viewerPath),
9062
- readFile23(agentPath)
9282
+ readFile24(viewerPath),
9283
+ readFile24(agentPath)
9063
9284
  ]);
9064
9285
  const hash = createHash4("sha256");
9065
9286
  hash.update(viewerBytes);
@@ -9121,7 +9342,8 @@ async function writeBuildFingerprint(input) {
9121
9342
  requestedFlavor: input.pdfx.requestedFlavor,
9122
9343
  resolvedFlavor: input.pdfx.resolvedFlavor,
9123
9344
  iccPath: input.pdfx.iccPath,
9124
- stripAnnotations: input.pdfx.stripAnnotations
9345
+ stripAnnotations: input.pdfx.stripAnnotations,
9346
+ blackText: input.pdfx.blackText ?? null
9125
9347
  },
9126
9348
  outputDir: input.recordedOutputDir ? path12.resolve(input.recordedOutputDir) : outputDir
9127
9349
  },
@@ -9133,261 +9355,6 @@ async function writeBuildFingerprint(input) {
9133
9355
  return outPath;
9134
9356
  }
9135
9357
 
9136
- // src/lib/black-text-gray.ts
9137
- var MAX_CHANNEL = 0.12;
9138
- var MAX_SPREAD = 0.04;
9139
- function isNearBlack([r, g, b]) {
9140
- const max = Math.max(r, g, b);
9141
- return max <= MAX_CHANNEL && max - Math.min(r, g, b) <= MAX_SPREAD;
9142
- }
9143
- var WS = new Set([0, 9, 10, 12, 13, 32]);
9144
- var DELIM = new Set("()<>[]{}/%".split("").map((c) => c.charCodeAt(0)));
9145
- function* tokenize(s) {
9146
- let i = 0;
9147
- const n = s.length;
9148
- while (i < n) {
9149
- const c = s.charCodeAt(i);
9150
- if (WS.has(c)) {
9151
- i++;
9152
- continue;
9153
- }
9154
- const start = i;
9155
- if (c === 37) {
9156
- while (i < n && s[i] !== `
9157
- ` && s[i] !== "\r")
9158
- i++;
9159
- continue;
9160
- }
9161
- if (c === 40) {
9162
- let depth = 0;
9163
- while (i < n) {
9164
- const d = s[i++];
9165
- if (d === "\\")
9166
- i++;
9167
- else if (d === "(")
9168
- depth++;
9169
- else if (d === ")" && --depth === 0)
9170
- break;
9171
- }
9172
- yield { text: s.slice(start, i), start, end: i, op: false };
9173
- continue;
9174
- }
9175
- if (c === 60 && s[i + 1] === "<") {
9176
- i += 2;
9177
- yield { text: "<<", start, end: i, op: false };
9178
- continue;
9179
- }
9180
- if (c === 62 && s[i + 1] === ">") {
9181
- i += 2;
9182
- yield { text: ">>", start, end: i, op: false };
9183
- continue;
9184
- }
9185
- if (c === 60) {
9186
- while (i < n && s[i] !== ">")
9187
- i++;
9188
- i++;
9189
- yield { text: s.slice(start, i), start, end: i, op: false };
9190
- continue;
9191
- }
9192
- if (c === 91 || c === 93 || c === 62 || c === 123 || c === 125 || c === 41) {
9193
- i++;
9194
- yield { text: s[start], start, end: i, op: false };
9195
- continue;
9196
- }
9197
- i++;
9198
- while (i < n && !WS.has(s.charCodeAt(i)) && !DELIM.has(s.charCodeAt(i)))
9199
- i++;
9200
- const text = s.slice(start, i);
9201
- const isOperand = c === 47 || /^[+-]?(\d+\.?\d*|\.\d+)$/.test(text);
9202
- yield { text, start, end: i, op: !isOperand };
9203
- if (text === "ID") {
9204
- const m = /\sEI(?=\s|$)/g;
9205
- m.lastIndex = i;
9206
- const hit = m.exec(s);
9207
- i = hit ? hit.index + hit[0].length : n;
9208
- }
9209
- }
9210
- }
9211
- function rewriteBlackText(src) {
9212
- let fill = null;
9213
- let stroke = null;
9214
- const stack = [];
9215
- let inText = false;
9216
- let fillDirty = false;
9217
- let strokeDirty = false;
9218
- const edits = [];
9219
- let operands = [];
9220
- const rgbOf = () => {
9221
- if (operands.length !== 3)
9222
- return null;
9223
- const v = operands.map((t) => Number(t.text));
9224
- return v.some((x) => Number.isNaN(x)) ? null : { rgb: v, raw: operands.map((t) => t.text).join(" ") };
9225
- };
9226
- for (const t of tokenize(src)) {
9227
- if (!t.op) {
9228
- operands.push(t);
9229
- continue;
9230
- }
9231
- const op = t.text;
9232
- switch (op) {
9233
- case "q":
9234
- stack.push([fill, stroke]);
9235
- break;
9236
- case "Q": {
9237
- const p = stack.pop();
9238
- if (p)
9239
- [fill, stroke] = p;
9240
- break;
9241
- }
9242
- case "BT":
9243
- inText = true;
9244
- fillDirty = strokeDirty = false;
9245
- {
9246
- let ins = "";
9247
- if (fill && isNearBlack(fill.rgb)) {
9248
- ins += " 0 g";
9249
- fillDirty = true;
9250
- }
9251
- if (stroke && isNearBlack(stroke.rgb)) {
9252
- ins += " 0 G";
9253
- strokeDirty = true;
9254
- }
9255
- if (ins)
9256
- edits.push({ start: t.end, end: t.end, text: ins });
9257
- }
9258
- break;
9259
- case "ET":
9260
- if (inText) {
9261
- let ins = "";
9262
- if (fillDirty && fill)
9263
- ins += `${fill.raw} rg `;
9264
- if (strokeDirty && stroke)
9265
- ins += `${stroke.raw} RG `;
9266
- if (ins)
9267
- edits.push({ start: t.start, end: t.start, text: ins });
9268
- }
9269
- inText = false;
9270
- break;
9271
- case "rg":
9272
- case "RG": {
9273
- const c = rgbOf();
9274
- const isFill = op === "rg";
9275
- if (isFill)
9276
- fill = c;
9277
- else
9278
- stroke = c;
9279
- if (inText && c && isNearBlack(c.rgb)) {
9280
- edits.push({ start: operands[0].start, end: t.end, text: isFill ? "0 g" : "0 G" });
9281
- if (isFill)
9282
- fillDirty = true;
9283
- else
9284
- strokeDirty = true;
9285
- } else if (inText) {
9286
- if (isFill)
9287
- fillDirty = false;
9288
- else
9289
- strokeDirty = false;
9290
- }
9291
- break;
9292
- }
9293
- case "g":
9294
- case "k":
9295
- case "sc":
9296
- case "scn":
9297
- case "cs":
9298
- fill = null;
9299
- if (inText)
9300
- fillDirty = false;
9301
- break;
9302
- case "G":
9303
- case "K":
9304
- case "SC":
9305
- case "SCN":
9306
- case "CS":
9307
- stroke = null;
9308
- if (inText)
9309
- strokeDirty = false;
9310
- break;
9311
- }
9312
- operands = [];
9313
- }
9314
- return applyEdits(src, edits);
9315
- }
9316
- function applyEdits(s, edits) {
9317
- if (!edits.length)
9318
- return s;
9319
- let out = "";
9320
- let pos = 0;
9321
- for (const e of edits) {
9322
- out += s.slice(pos, e.start) + e.text;
9323
- pos = e.end;
9324
- }
9325
- return out + s.slice(pos);
9326
- }
9327
- async function blackTextToGray(pdf) {
9328
- const { PDFDocument, PDFName, PDFRawStream, PDFArray, PDFRef, decodePDFRawStream } = await import("pdf-lib");
9329
- const doc = await PDFDocument.load(pdf, { updateMetadata: false });
9330
- const ctx = doc.context;
9331
- const SKIP = ["Filter", "DecodeParms", "Length"].map((k) => PDFName.of(k));
9332
- let changed = false;
9333
- const decode = (s) => {
9334
- try {
9335
- return Buffer.from(decodePDFRawStream(s).decode()).toString("latin1");
9336
- } catch {
9337
- return null;
9338
- }
9339
- };
9340
- const replace = (ref, old, text) => {
9341
- const entries = {};
9342
- for (const [k, v] of old.dict.entries())
9343
- if (!SKIP.includes(k))
9344
- entries[k.decodeText()] = v;
9345
- ctx.assign(ref, ctx.flateStream(Buffer.from(text, "latin1"), entries));
9346
- changed = true;
9347
- };
9348
- for (const [ref, obj] of ctx.enumerateIndirectObjects()) {
9349
- if (!(obj instanceof PDFRawStream))
9350
- continue;
9351
- if (obj.dict.get(PDFName.of("Subtype")) !== PDFName.of("Form"))
9352
- continue;
9353
- const text = decode(obj);
9354
- if (text === null)
9355
- continue;
9356
- const next = rewriteBlackText(text);
9357
- if (next !== text)
9358
- replace(ref, obj, next);
9359
- }
9360
- for (const page of doc.getPages()) {
9361
- const contents = page.node.Contents();
9362
- const refs = [];
9363
- if (contents instanceof PDFArray) {
9364
- for (let i = 0;i < contents.size(); i++) {
9365
- const r = contents.get(i);
9366
- if (r instanceof PDFRef)
9367
- refs.push(r);
9368
- }
9369
- } else if (contents instanceof PDFRawStream) {
9370
- const r = page.node.get(PDFName.of("Contents"));
9371
- if (r instanceof PDFRef)
9372
- refs.push(r);
9373
- }
9374
- const streams = refs.map((r) => ctx.lookup(r));
9375
- if (!refs.length || !streams.every((s) => s instanceof PDFRawStream))
9376
- continue;
9377
- const parts = streams.map((s) => decode(s));
9378
- if (parts.some((p) => p === null))
9379
- continue;
9380
- const joined = parts.join(`
9381
- `);
9382
- const next = rewriteBlackText(joined);
9383
- if (next === joined)
9384
- continue;
9385
- replace(refs[0], streams[0], next);
9386
- page.node.set(PDFName.of("Contents"), refs[0]);
9387
- }
9388
- return changed ? doc.save({ updateFieldAppearances: false }) : pdf;
9389
- }
9390
-
9391
9358
  // src/lib/build-preflight.ts
9392
9359
  var INSTALL_HINTS2 = {
9393
9360
  gs: INSTALL_HINTS.gs.body,
@@ -9662,7 +9629,8 @@ class HtmlOutput {
9662
9629
  requestedFlavor: null,
9663
9630
  resolvedFlavor: config.pdfx.flavor,
9664
9631
  iccPath: null,
9665
- stripAnnotations: null
9632
+ stripAnnotations: null,
9633
+ blackText: null
9666
9634
  }
9667
9635
  }, `Wrote: ${path13.join(outDir, BOOK_HTML)}`, { htmlPath: htmlFile, pdfPath: null });
9668
9636
  }
@@ -9682,12 +9650,12 @@ class PdfOutput {
9682
9650
  const rawPdf = pdfxMode ? path13.join(stage, "raw.pdf") : path13.resolve(pdfFile);
9683
9651
  await fsp2.mkdir(path13.dirname(path13.resolve(pdfFile)), { recursive: true });
9684
9652
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
9685
- const { buildNativePdf } = await import("./engine-078ggczp.js");
9653
+ const { buildNativePdf } = await import("./engine-zdb41ay1.js");
9686
9654
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
9687
9655
  title: config.title,
9688
9656
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
9689
9657
  signature: config.print.signature,
9690
- reserveLastPage: config.print.reserveLastPage,
9658
+ lastPage: lastPageMode(config.print),
9691
9659
  allowShrink: opts.allowShrink
9692
9660
  }, browser);
9693
9661
  for (const d of engineDiagnostics)
@@ -9746,7 +9714,8 @@ class PdfOutput {
9746
9714
  requestedFlavor: pdfxMode ?? null,
9747
9715
  resolvedFlavor: config.pdfx.flavor,
9748
9716
  iccPath: effectiveIccPath,
9749
- stripAnnotations: shouldStripAnnotations
9717
+ stripAnnotations: shouldStripAnnotations,
9718
+ blackText: pdfxMode ? config.pdfx.blackText : null
9750
9719
  }
9751
9720
  }, `Wrote: ${reportedPdf}`, { htmlPath: htmlFile, pdfPath: reportedPdf }, pdfName, engineDiagnostics);
9752
9721
  } finally {
@@ -9816,7 +9785,7 @@ function openPath(filePath) {
9816
9785
  import path16 from "path";
9817
9786
  import { tmpdir } from "os";
9818
9787
  import { randomBytes as randomBytes2 } from "crypto";
9819
- import { readdir as readdir4, readFile as readFile24, writeFile as writeFile5 } from "node:fs/promises";
9788
+ import { readdir as readdir4, readFile as readFile25, writeFile as writeFile5 } from "node:fs/promises";
9820
9789
 
9821
9790
  // src/utils/file-utils.ts
9822
9791
  import { promises as fs3 } from "fs";
@@ -10365,7 +10334,7 @@ async function cleanupOrphanTempDirs() {
10365
10334
  const dir = path16.join(TEMP_DIR_BASE, entry);
10366
10335
  const pidFile = path16.join(dir, PID_FILE_NAME);
10367
10336
  try {
10368
- const raw = await readFile24(pidFile, "utf8");
10337
+ const raw = await readFile25(pidFile, "utf8");
10369
10338
  const pid = parseInt(raw.trim(), 10);
10370
10339
  if (!isProcessAlive(pid)) {
10371
10340
  debug(`Removing orphan temp dir ${dir} (pid ${pid} not alive)`);
@@ -10472,7 +10441,7 @@ function createServerState(inputPath, tempDir, config, options) {
10472
10441
  import http from "node:http";
10473
10442
  import net from "node:net";
10474
10443
  import { createHash as createHash5, randomUUID as randomUUID3 } from "node:crypto";
10475
- import { readFile as readFile25, stat as stat5 } from "node:fs/promises";
10444
+ import { readFile as readFile26, stat as stat5 } from "node:fs/promises";
10476
10445
  import path17 from "path";
10477
10446
  import { WebSocket, WebSocketServer } from "ws";
10478
10447
  var HMR_PATH = "/__gutterpress-hmr";
@@ -10541,7 +10510,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
10541
10510
  }
10542
10511
  let data;
10543
10512
  try {
10544
- data = await readFile25(filePath);
10513
+ data = await readFile26(filePath);
10545
10514
  } catch {
10546
10515
  res.writeHead(404);
10547
10516
  res.end("Not Found");
@@ -10590,7 +10559,7 @@ async function serveRevalidatedStatic(req, absPath, res, validators) {
10590
10559
  let data;
10591
10560
  if (!validator || validator.stamp !== stamp) {
10592
10561
  try {
10593
- data = await readFile25(absPath);
10562
+ data = await readFile26(absPath);
10594
10563
  } catch {
10595
10564
  await serveStatic(absPath, res);
10596
10565
  return;
@@ -10900,9 +10869,29 @@ async function startPreviewServer(options) {
10900
10869
  }
10901
10870
 
10902
10871
  // src/lib/project-scaffold.ts
10903
- import { access, copyFile, cp as cp2, mkdir as mkdir7, readdir as readdir5, readFile as readFile26, stat as stat6, writeFile as writeFile6 } from "node:fs/promises";
10872
+ import { access, copyFile, cp as cp2, mkdir as mkdir7, readdir as readdir5, readFile as readFile27, stat as stat6, writeFile as writeFile6 } from "node:fs/promises";
10904
10873
  import { constants as FS, existsSync as existsSync12 } from "node:fs";
10905
10874
  import path18 from "node:path";
10875
+
10876
+ // src/lib/page-size.ts
10877
+ import postcss4 from "postcss";
10878
+ import { Scalar as Scalar2 } from "yaml";
10879
+ function isPositivePoints(value) {
10880
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
10881
+ }
10882
+ function writePageBounds(doc, page) {
10883
+ const out = { width: page.width, height: page.height };
10884
+ const tolerance = page.tolerance ?? numberAt(doc, ["page", "tolerance"]);
10885
+ if (tolerance !== undefined)
10886
+ out.tolerance = tolerance;
10887
+ doc.set("page", doc.createNode(out));
10888
+ }
10889
+ function numberAt(doc, keyPath) {
10890
+ const v = doc.getIn(keyPath);
10891
+ return isPositivePoints(v) ? v : undefined;
10892
+ }
10893
+
10894
+ // src/lib/project-scaffold.ts
10906
10895
  function starterOverridesCss() {
10907
10896
  return `/**
10908
10897
  * Your book's own styles.
@@ -10931,9 +10920,6 @@ function escapeYamlScalar(value) {
10931
10920
  return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
10932
10921
  }
10933
10922
  var DEFAULT_AUTHOR = "Anonymous";
10934
- function isPositivePoints(value) {
10935
- return typeof value === "number" && Number.isFinite(value) && value > 0;
10936
- }
10937
10923
  function requirePreset(preset, customPage) {
10938
10924
  if (!preset || !PRESET_IDS.includes(preset)) {
10939
10925
  const got = preset ? ` (got "${preset}")` : "";
@@ -11037,16 +11023,8 @@ async function scaffoldProject(options) {
11037
11023
  const { doc, file } = await loadManifestDoc(projectDir);
11038
11024
  doc.set("preset", preset);
11039
11025
  doc.set("targets", doc.createNode(targets ?? []));
11040
- if (options.customPage) {
11041
- const page = {
11042
- width: options.customPage.width,
11043
- height: options.customPage.height
11044
- };
11045
- if (options.customPage.tolerance !== undefined) {
11046
- page.tolerance = options.customPage.tolerance;
11047
- }
11048
- doc.set("page", doc.createNode(page));
11049
- }
11026
+ if (options.customPage)
11027
+ writePageBounds(doc, options.customPage);
11050
11028
  await writeManifestDoc(file, doc);
11051
11029
  }
11052
11030
  const firstSource = await firstSourceFile(manifestPath);
@@ -11090,7 +11068,7 @@ async function scaffoldProject(options) {
11090
11068
  async function firstSourceFile(manifestPath) {
11091
11069
  let text;
11092
11070
  try {
11093
- text = await readFile26(manifestPath, "utf8");
11071
+ text = await readFile27(manifestPath, "utf8");
11094
11072
  } catch {
11095
11073
  return;
11096
11074
  }
@@ -11102,7 +11080,7 @@ async function firstSourceFile(manifestPath) {
11102
11080
  return m ? m[1].trim() : undefined;
11103
11081
  }
11104
11082
  async function fillTemplateFile(filePath, substitutions) {
11105
- let text = await readFile26(filePath, "utf8");
11083
+ let text = await readFile27(filePath, "utf8");
11106
11084
  for (const [token, value] of Object.entries(substitutions)) {
11107
11085
  text = text.split(token).join(value);
11108
11086
  }
@@ -11110,7 +11088,7 @@ async function fillTemplateFile(filePath, substitutions) {
11110
11088
  }
11111
11089
 
11112
11090
  // src/lib/extension-scaffold.ts
11113
- import { access as access2, mkdir as mkdir8, readFile as readFile27, writeFile as writeFile7 } from "node:fs/promises";
11091
+ import { access as access2, mkdir as mkdir8, readFile as readFile28, writeFile as writeFile7 } from "node:fs/promises";
11114
11092
  import { constants as FS2 } from "node:fs";
11115
11093
  import path19 from "node:path";
11116
11094
  var EXTENSION_KINDS = ["plugin", "theme"];
@@ -11227,7 +11205,7 @@ async function scaffoldExtension(options) {
11227
11205
  const src = await getAssetPath(`extension-templates/${kind}/${assetName}`);
11228
11206
  const dest = path19.join(extensionDir, relDest);
11229
11207
  await mkdir8(path19.dirname(dest), { recursive: true });
11230
- const text = await readFile27(src, "utf8");
11208
+ const text = await readFile28(src, "utf8");
11231
11209
  const table = relDest.endsWith(".json") ? jsonSafe : plain;
11232
11210
  await writeFile7(dest, substitute(text, table), "utf8");
11233
11211
  written.push(relDest);
@@ -11272,7 +11250,7 @@ var SKIP_ENTRIES = new Set([
11272
11250
 
11273
11251
  // src/lib/extension-import.ts
11274
11252
  import { existsSync as existsSync13 } from "node:fs";
11275
- import { cp as cp3, mkdir as mkdir9, mkdtemp as mkdtemp2, readFile as readFile28, rm as rm4, writeFile as writeFile8 } from "node:fs/promises";
11253
+ import { cp as cp3, mkdir as mkdir9, mkdtemp as mkdtemp2, readFile as readFile29, rm as rm4, writeFile as writeFile8 } from "node:fs/promises";
11276
11254
  import { tmpdir as tmpdir2 } from "node:os";
11277
11255
  import path20 from "node:path";
11278
11256
  import { unzipSync } from "fflate";
@@ -11345,7 +11323,7 @@ async function finalizeExtensionImport(projectDir, sourceDir, relPaths, fallback
11345
11323
  if (!existsSync13(cssPath)) {
11346
11324
  throw new Error("A theme must contain a theme.css file.");
11347
11325
  }
11348
- const css = await readFile28(cssPath, "utf8");
11326
+ const css = await readFile29(cssPath, "utf8");
11349
11327
  const { reject, warnings: printFindings } = classifyThemeCssFindings(checkCss(css, "theme.css"));
11350
11328
  if (reject) {
11351
11329
  throw new Error(`theme.css could not be parsed — ${reject.message}`);
@@ -11373,7 +11351,7 @@ async function finalizeExtensionImport(projectDir, sourceDir, relPaths, fallback
11373
11351
  for (let i = 0;i < extraSheets.length; i++) {
11374
11352
  const rel = extraSheets[i];
11375
11353
  const sheetPath = extraSheetPaths[i];
11376
- const sheetCss = await readFile28(sheetPath, "utf8");
11354
+ const sheetCss = await readFile29(sheetPath, "utf8");
11377
11355
  const { reject: sheetReject, warnings: sheetFindings } = classifyThemeCssFindings(checkCss(sheetCss, rel));
11378
11356
  if (sheetReject) {
11379
11357
  throw new Error(`${rel} could not be parsed — ${sheetReject.message}`);
@@ -11406,7 +11384,7 @@ async function completeLandedManifest(dest, id, meta, metaExists) {
11406
11384
  let pkg = {};
11407
11385
  if (metaExists) {
11408
11386
  try {
11409
- const parsed = JSON.parse(await readFile28(file, "utf8"));
11387
+ const parsed = JSON.parse(await readFile29(file, "utf8"));
11410
11388
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
11411
11389
  pkg = parsed;
11412
11390
  }
@@ -11508,11 +11486,11 @@ async function importExtensionFromCssText(projectDir, css, name) {
11508
11486
  async function importExtensionFromFile(projectDir, filePath) {
11509
11487
  const ext = path20.extname(filePath).toLowerCase();
11510
11488
  if (ext === ".zip") {
11511
- const buf = await readFile28(filePath);
11489
+ const buf = await readFile29(filePath);
11512
11490
  return importExtensionFromZip(projectDir, new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
11513
11491
  }
11514
11492
  if (ext === ".css") {
11515
- const css = await readFile28(filePath, "utf8");
11493
+ const css = await readFile29(filePath, "utf8");
11516
11494
  return importExtensionFromCssText(projectDir, css, prettify(path20.basename(filePath, ".css")));
11517
11495
  }
11518
11496
  throw new Error("Choose a .zip extension package or a .css stylesheet.");
@@ -12296,7 +12274,7 @@ var drivethrurpgProvider = {
12296
12274
  };
12297
12275
 
12298
12276
  // src/lib/publish/providers/gdrive.ts
12299
- import { mkdtemp as mkdtemp3, readFile as readFile29, readdir as readdir6, rm as rm5, stat as stat7, writeFile as writeFile10 } from "node:fs/promises";
12277
+ import { mkdtemp as mkdtemp3, readFile as readFile30, readdir as readdir6, rm as rm5, stat as stat7, writeFile as writeFile10 } from "node:fs/promises";
12300
12278
  import { tmpdir as tmpdir3 } from "node:os";
12301
12279
  import path22 from "node:path";
12302
12280
  import { Zip, ZipDeflate } from "fflate";
@@ -12688,12 +12666,12 @@ async function collectZipEntries(root, dir, out, onWarn) {
12688
12666
  await collectZipEntries(root, full, out, onWarn);
12689
12667
  } else if (entry.isFile()) {
12690
12668
  const rel = path22.relative(root, full).split(path22.sep).join("/");
12691
- out[rel] = await readFile29(full);
12669
+ out[rel] = await readFile30(full);
12692
12670
  } else if (entry.isSymbolicLink()) {
12693
12671
  const resolved = await stat7(full).catch(() => null);
12694
12672
  if (resolved?.isFile()) {
12695
12673
  const rel = path22.relative(root, full).split(path22.sep).join("/");
12696
- out[rel] = await readFile29(full);
12674
+ out[rel] = await readFile30(full);
12697
12675
  } else if (resolved?.isDirectory()) {
12698
12676
  await collectZipEntries(root, full, out, onWarn);
12699
12677
  } else {
@@ -13562,13 +13540,16 @@ async function disconnectPublishCredential(key, deps, options = {}) {
13562
13540
  }
13563
13541
  }
13564
13542
  // src/lib/manifest-config.ts
13565
- import { isSeq as isSeq4, isMap as isMap3, YAMLSeq as YAMLSeq3, Scalar as Scalar2 } from "yaml";
13543
+ import { isSeq as isSeq4, isMap as isMap3, YAMLSeq as YAMLSeq3, Scalar as Scalar3 } from "yaml";
13566
13544
  // src/lib/host-policy.ts
13567
13545
  var AUTO_SYNC_MAX_MINUTES = 24 * 60;
13568
13546
  // src/lib/remote-auth/github-repos.ts
13569
13547
  var MANIFEST_NAMES = new Set(MANIFEST_FILENAMES);
13570
- // src/lib/remote-auth/clone.ts
13571
- import git3 from "isomorphic-git";
13548
+ // src/lib/remote-auth/online-backup.ts
13549
+ import git5 from "isomorphic-git";
13550
+
13551
+ // src/lib/remote-auth/sync.ts
13552
+ import git4 from "isomorphic-git";
13572
13553
 
13573
13554
  // src/lib/remote-auth/git-http.ts
13574
13555
  import httpNode from "isomorphic-git/http/node";
@@ -13661,23 +13642,23 @@ function withIdleTimeout(http3, idleMs = GIT_HTTP_IDLE_TIMEOUT_MS, uploadMs = GI
13661
13642
  }
13662
13643
  var defaultGitHttp = withIdleTimeout(httpNode);
13663
13644
 
13664
- // src/lib/remote-auth/transport.ts
13645
+ // src/lib/remote-auth/converge-merge.ts
13665
13646
  import git2 from "isomorphic-git";
13647
+ import diff3Merge from "diff3";
13648
+
13649
+ // src/lib/remote-auth/transport.ts
13650
+ import git3 from "isomorphic-git";
13651
+ // src/lib/remote-auth/clone.ts
13652
+ import git6 from "isomorphic-git";
13666
13653
  // src/lib/remote-auth/test-access.ts
13667
- import git4 from "isomorphic-git";
13654
+ import git7 from "isomorphic-git";
13668
13655
  import httpNode2 from "isomorphic-git/http/node";
13669
- // src/lib/remote-auth/sync.ts
13670
- import git6 from "isomorphic-git";
13671
-
13672
- // src/lib/remote-auth/converge-merge.ts
13673
- import git5 from "isomorphic-git";
13674
- import diff3Merge from "diff3";
13675
13656
  // src/lib/remote-auth/repair.ts
13676
- import git7 from "isomorphic-git";
13677
- // src/lib/remote-auth/scorched-earth.ts
13678
13657
  import git8 from "isomorphic-git";
13658
+ // src/lib/remote-auth/scorched-earth.ts
13659
+ import git9 from "isomorphic-git";
13679
13660
  // src/lib/publish/selections.ts
13680
- import { chmod as chmod2, mkdir as mkdir13, readFile as readFile30, writeFile as writeFile12 } from "node:fs/promises";
13661
+ import { chmod as chmod2, mkdir as mkdir13, readFile as readFile31, writeFile as writeFile12 } from "node:fs/promises";
13681
13662
  import path26 from "node:path";
13682
13663
  class PublishSelectionsStore {
13683
13664
  filePath;
@@ -13687,7 +13668,7 @@ class PublishSelectionsStore {
13687
13668
  }
13688
13669
  async read() {
13689
13670
  try {
13690
- const raw = await readFile30(this.filePath, "utf8");
13671
+ const raw = await readFile31(this.filePath, "utf8");
13691
13672
  const parsed = JSON.parse(raw);
13692
13673
  if (parsed && typeof parsed === "object") {
13693
13674
  return {