gutterpress 0.11.17-alpha.1 → 0.11.17-alpha.3

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 (50) hide show
  1. package/dist/api/index.d.ts +4 -0
  2. package/dist/api/index.js +10 -2
  3. package/dist/{build-jfjzam95.js → build-394c1ee1.js} +4 -4
  4. package/dist/checks/pdf/index.d.ts +1 -0
  5. package/dist/checks/pdf/signature.d.ts +3 -0
  6. package/dist/{cli-0754wskb.js → cli-3p1e95zc.js} +1 -1
  7. package/dist/{cli-ew3gc4nt.js → cli-9nba5ve6.js} +562 -211
  8. package/dist/{cli-zce1zcqc.js → cli-d57bsfpb.js} +1 -1
  9. package/dist/{cli-xx5bryh6.js → cli-jyy9pqf3.js} +16 -11
  10. package/dist/cli.js +12 -12
  11. package/dist/{doctor-h01f71ws.js → doctor-c459qv5j.js} +2 -2
  12. package/dist/engine/compiler/build.d.ts +3 -2
  13. package/dist/engine/compiler/postprocess.d.ts +6 -4
  14. package/dist/engine/shared/synthesis.d.ts +29 -5
  15. package/dist/{engine-aaam2nz3.js → engine-3xqgm82d.js} +2 -2
  16. package/dist/{engine-rhmwgk9k.js → engine-zdb41ay1.js} +1 -1
  17. package/dist/{ext-ja4hy1c2.js → ext-ha7gg8h9.js} +4 -4
  18. package/dist/{gutterpress-viewer-5a6h4evk.js → gutterpress-viewer-1n793gwd.js} +10 -4
  19. package/dist/{index-r9t4acx5.js → index-bscfmg5r.js} +1714 -1054
  20. package/dist/{index-3nk5mnn6.js → index-hrx0gfkj.js} +23 -684
  21. package/dist/index-q1xqxrh9.js +949 -0
  22. package/dist/index.js +11 -3
  23. package/dist/lib/black-text-gray.d.ts +38 -8
  24. package/dist/lib/build-fingerprint.d.ts +2 -0
  25. package/dist/lib/engine.d.ts +2 -1
  26. package/dist/lib/lint-rules.d.ts +9 -0
  27. package/dist/lib/markdown/assemble.d.ts +3 -5
  28. package/dist/lib/markdown/index.d.ts +3 -8
  29. package/dist/lib/markdown/renderer.d.ts +9 -1
  30. package/dist/lib/page-size.d.ts +66 -0
  31. package/dist/lib/pdf-inspect.d.ts +6 -0
  32. package/dist/lib/printsafe.d.ts +8 -1
  33. package/dist/lib/project-scaffold.d.ts +3 -9
  34. package/dist/lib/remote-auth/github-repos.d.ts +7 -0
  35. package/dist/lib/remote-auth/online-backup.d.ts +51 -0
  36. package/dist/lib/snippets.d.ts +3 -0
  37. package/dist/{manifest.schema-40sfq49z.json → manifest.schema-gh0jjve9.json} +14 -2
  38. package/dist/{new-yzswp0ge.js → new-kky3z7kv.js} +4 -4
  39. package/dist/{plugin.js-m4a7m3t4.tpl → plugin.js-cswq7wvd.tpl} +4 -1
  40. package/dist/{plugin.test.js-6n8ct5e3.tpl → plugin.test.js-xgxrd8hf.tpl} +4 -1
  41. package/dist/{preflight-0s1vw6y1.js → preflight-t8v6szq6.js} +4 -4
  42. package/dist/preview/file-watcher.d.ts +2 -4
  43. package/dist/{preview-tam7azsr.js → preview-cxab4y5x.js} +4 -4
  44. package/dist/{preview-interface-5wnzrvhf.js → preview-interface-c44qk17f.js} +7 -2
  45. package/dist/{publish-ra8kyf0z.js → publish-s2mtenb5.js} +4 -4
  46. package/dist/render.js +30 -5
  47. package/dist/schema/manifest.types.d.ts +17 -0
  48. package/dist/{validate-n9vae7p8.js → validate-xaajdw4x.js} +4 -4
  49. package/package.json +1 -1
  50. package/dist/index-dmm31ys3.js +0 -267
@@ -13,17 +13,18 @@ import {
13
13
  stampCreator,
14
14
  stripAnnotations,
15
15
  warn
16
- } from "./cli-0754wskb.js";
16
+ } from "./cli-3p1e95zc.js";
17
17
  import {
18
18
  DEBOUNCE,
19
19
  UsageError,
20
20
  resolvePort
21
- } from "./cli-zce1zcqc.js";
21
+ } from "./cli-d57bsfpb.js";
22
22
  import {
23
23
  MARGIN_BOX_IGNORED_PROPERTIES,
24
24
  getAssetPath,
25
- getAssetsDir
26
- } from "./cli-xx5bryh6.js";
25
+ getAssetsDir,
26
+ lastPageMode
27
+ } from "./cli-jyy9pqf3.js";
27
28
  import {
28
29
  BuildError,
29
30
  EXIT_CODES
@@ -106,7 +107,7 @@ var DTRPG_PRESET = {
106
107
  source: {
107
108
  files: null
108
109
  },
109
- print: { signature: 4, reserveLastPage: true },
110
+ print: { signature: 4, reserveLastPage: true, lastPage: undefined },
110
111
  pdfx: {
111
112
  flavor: "x1a",
112
113
  icc: "profiles/CGATS21_CRPC1.icc",
@@ -124,7 +125,8 @@ var DTRPG_PRESET = {
124
125
  },
125
126
  lint: {
126
127
  enabled: true,
127
- configPath: null
128
+ configPath: null,
129
+ rules: {}
128
130
  },
129
131
  validate: {
130
132
  enabled: true,
@@ -181,7 +183,7 @@ var BOOK_PRESET = {
181
183
  source: {
182
184
  files: null
183
185
  },
184
- print: { signature: 1, reserveLastPage: false },
186
+ print: { signature: 1, reserveLastPage: false, lastPage: undefined },
185
187
  pdfx: {
186
188
  flavor: "x1a",
187
189
  icc: "profiles/CGATS21_CRPC1.icc",
@@ -199,7 +201,8 @@ var BOOK_PRESET = {
199
201
  },
200
202
  lint: {
201
203
  enabled: true,
202
- configPath: null
204
+ configPath: null,
205
+ rules: {}
203
206
  },
204
207
  validate: {
205
208
  enabled: true,
@@ -566,6 +569,10 @@ function resolveWithPreset(cliOverrides, manifest, preset) {
566
569
  if (legacyPlugins !== undefined) {
567
570
  throw new UsageError(pluginsRemovedMessage(legacyPlugins));
568
571
  }
572
+ const lastPage = c.print?.lastPage ?? m.print?.lastPage;
573
+ if (lastPage !== undefined && lastPage !== "blank" && lastPage !== "short-by-one") {
574
+ throw new UsageError(`Manifest field \`print.lastPage\` must be "blank" or "short-by-one", got ${JSON.stringify(lastPage)}.`);
575
+ }
569
576
  const rawExtensions = c.extensions ?? m.extensions ?? [];
570
577
  if (!Array.isArray(rawExtensions)) {
571
578
  throw new UsageError("Manifest field `extensions` must be a list of specifiers.");
@@ -582,7 +589,10 @@ function resolveWithPreset(cliOverrides, manifest, preset) {
582
589
  print: mergeShape(c.print, m.print, preset.print),
583
590
  page: preset.page ? mergeShape(c.page, m.page, preset.page) : resolveCustomPage(c, m),
584
591
  ink: mergeShape(c.ink, m.ink, preset.ink),
585
- lint: mergeShape(c.lint, m.lint, preset.lint),
592
+ lint: {
593
+ ...mergeShape(c.lint, m.lint, preset.lint),
594
+ rules: { ...preset.lint.rules, ...m.lint?.rules, ...c.lint?.rules }
595
+ },
586
596
  validate: {
587
597
  enabled: c.validate?.enabled ?? m.validate?.enabled ?? preset.validate.enabled,
588
598
  checks: { ...preset.validate.checks, ...m.validate?.checks, ...c.validate?.checks },
@@ -1078,7 +1088,7 @@ function resolveContainerShape(name, pluginName, decl) {
1078
1088
  throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`section\` that is not a boolean.`);
1079
1089
  }
1080
1090
  if (decl.section) {
1081
- for (const field of ["tag", "label", "autoCloseAt"]) {
1091
+ for (const field of ["tag", "label", "autoCloseAt", "selfClosing"]) {
1082
1092
  if (decl[field] !== undefined) {
1083
1093
  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
1094
  }
@@ -1135,11 +1145,26 @@ function resolveContainerShape(name, pluginName, decl) {
1135
1145
  }
1136
1146
  autoCloseAtEof = decl.autoCloseAt.includes("eof");
1137
1147
  }
1148
+ if (decl.selfClosing !== undefined && typeof decl.selfClosing !== "boolean") {
1149
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`selfClosing\` that is not a boolean.`);
1150
+ }
1151
+ if (decl.selfClosing && decl.autoCloseAt !== undefined) {
1152
+ 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.`);
1153
+ }
1138
1154
  if (decl.validate !== undefined && typeof decl.validate !== "function") {
1139
1155
  throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`validate\` that is not a function.`);
1140
1156
  }
1141
1157
  const check = decl.validate ? { validate: decl.validate, validateOwner: pluginName } : {};
1142
- return { tag, classBase, variants, label, autoCloseAtEof, ...decl.section ? { section: true } : {}, ...check };
1158
+ return {
1159
+ tag,
1160
+ classBase,
1161
+ variants,
1162
+ label,
1163
+ autoCloseAtEof,
1164
+ ...decl.section ? { section: true } : {},
1165
+ ...decl.selfClosing ? { selfClosing: true } : {},
1166
+ ...check
1167
+ };
1143
1168
  }
1144
1169
  function resolveMarkerDeclaration(name, rawDecl, rawRegistry, originOf) {
1145
1170
  const pluginName = originOf.get(name);
@@ -1398,7 +1423,8 @@ function plugin(md, pluginOptions = {}) {
1398
1423
  addClasses(t, baseClass, meta.attrs && meta.attrs.class ? meta.attrs.class : "");
1399
1424
  attachDataAttrs(t, decl.baseKind, variant, meta.attrs || {});
1400
1425
  out.push(t);
1401
- declaredFrames.push({ kind: decl.baseKind, tag: decl.tag, decl, line: meta.__line });
1426
+ if (!decl.selfClosing)
1427
+ declaredFrames.push({ kind: decl.baseKind, tag: decl.tag, decl, line: meta.__line });
1402
1428
  if (decl.label) {
1403
1429
  const value = meta.attrs && meta.attrs[decl.label.attr];
1404
1430
  if (value) {
@@ -1409,6 +1435,8 @@ function plugin(md, pluginOptions = {}) {
1409
1435
  t.meta.labelled = true;
1410
1436
  }
1411
1437
  }
1438
+ if (decl.selfClosing)
1439
+ out.push(new state.Token("layout_component_close", decl.tag, -1));
1412
1440
  }
1413
1441
  const stack = {
1414
1442
  frames: [],
@@ -1687,7 +1715,7 @@ function plugin(md, pluginOptions = {}) {
1687
1715
  continue;
1688
1716
  }
1689
1717
  if (!closeDeclaredFrame(closeDecl.baseKind)) {
1690
- warn2(state.env, line, "declared_marker_close_without_open", `@${kind} used without an open @${closeDecl.baseKind}; ignoring marker.`, meta);
1718
+ 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
1719
  }
1692
1720
  continue;
1693
1721
  }
@@ -4987,6 +5015,32 @@ function getImagePixelSize(page, name, timeoutMs = 2000) {
4987
5015
  }
4988
5016
  });
4989
5017
  }
5018
+ var PAINT_OPS = new Set([
5019
+ "stroke",
5020
+ "closeStroke",
5021
+ "fill",
5022
+ "eoFill",
5023
+ "fillStroke",
5024
+ "eoFillStroke",
5025
+ "closeFillStroke",
5026
+ "closeEOFillStroke",
5027
+ "shadingFill",
5028
+ "paintImageXObject",
5029
+ "paintInlineImageXObject",
5030
+ "paintImageXObjectRepeat",
5031
+ "paintImageMaskXObject",
5032
+ "paintImageMaskXObjectGroup",
5033
+ "paintImageMaskXObjectRepeat",
5034
+ "paintSolidColorImageMask",
5035
+ "showText",
5036
+ "showSpacedText",
5037
+ "nextLineShowText",
5038
+ "nextLineSetSpacingShowText"
5039
+ ].map((name) => OPS[name]).filter((op) => op !== undefined));
5040
+ async function pageHasMarks(doc, pageNum) {
5041
+ const ops = await (await doc.getPage(pageNum)).getOperatorList();
5042
+ return ops.fnArray.some((fn, i) => fn === OPS.constructPath ? PAINT_OPS.has(ops.argsArray[i][0]) : PAINT_OPS.has(fn));
5043
+ }
4990
5044
  async function getImageResolutions(doc) {
4991
5045
  const { imagesByPage } = await getOpPass(doc);
4992
5046
  const out = [];
@@ -5490,8 +5544,62 @@ var check7 = {
5490
5544
  };
5491
5545
  registerCheck(check7);
5492
5546
 
5493
- // src/checks/pdf/bookmarks.ts
5547
+ // src/checks/pdf/signature.ts
5494
5548
  var check8 = {
5549
+ id: "pdf.print.signature",
5550
+ name: "Signature",
5551
+ 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)",
5552
+ category: "pdf",
5553
+ phase: "post-build",
5554
+ enabledWhen: (config) => config.print.signature > 1,
5555
+ async run(ctx) {
5556
+ if (!ctx.pdfPath)
5557
+ return [];
5558
+ const doc = await loadPdf(ctx.pdfPath);
5559
+ if (!doc) {
5560
+ return [inspectionFailed(check8.id, "Could not parse the PDF's page count.", { file: ctx.pdfPath })];
5561
+ }
5562
+ const size = ctx.config.print.signature;
5563
+ const pages = doc.numPages;
5564
+ if ((pages + 1) % size === 0)
5565
+ return [];
5566
+ const data = { pages, signature: size };
5567
+ if (pages % size !== 0) {
5568
+ return [
5569
+ finding(check8.id, {
5570
+ severity: "error",
5571
+ message: `Page count ${pages} does not fit a signature of ${size}: it must be a multiple of ${size}, or one short of one.`,
5572
+ file: ctx.pdfPath,
5573
+ detail: "Set `print.signature` in the manifest and rebuild; the build pads the interior for you.",
5574
+ code: "signature-page-count",
5575
+ data
5576
+ })
5577
+ ];
5578
+ }
5579
+ if (!lastPageMode(ctx.config.print))
5580
+ return [];
5581
+ try {
5582
+ if (!await pageHasMarks(doc, pages))
5583
+ return [];
5584
+ } catch {
5585
+ return [inspectionFailed(check8.id, "Could not read the last page.", { file: ctx.pdfPath })];
5586
+ }
5587
+ return [
5588
+ finding(check8.id, {
5589
+ severity: "error",
5590
+ message: `The last page (${pages}) has content, but the printer needs it blank for its own information.`,
5591
+ file: ctx.pdfPath,
5592
+ 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.",
5593
+ code: "signature-last-page",
5594
+ data
5595
+ })
5596
+ ];
5597
+ }
5598
+ };
5599
+ registerCheck(check8);
5600
+
5601
+ // src/checks/pdf/bookmarks.ts
5602
+ var check9 = {
5495
5603
  id: "pdf.nav.bookmarks",
5496
5604
  name: "Bookmarks",
5497
5605
  description: "Checks for PDF outline (bookmarks) tree",
@@ -5505,14 +5613,14 @@ var check8 = {
5505
5613
  const doc = await loadPdf(ctx.pdfPath);
5506
5614
  if (!doc) {
5507
5615
  return [
5508
- inspectionFailed(check8.id, "Could not inspect PDF for bookmarks.", {
5616
+ inspectionFailed(check9.id, "Could not inspect PDF for bookmarks.", {
5509
5617
  file: ctx.pdfPath
5510
5618
  })
5511
5619
  ];
5512
5620
  }
5513
5621
  if (await getOutlineCount(doc) === 0) {
5514
5622
  return [
5515
- finding(check8.id, {
5623
+ finding(check9.id, {
5516
5624
  severity: "warning",
5517
5625
  message: "PDF does not contain bookmarks (outline tree).",
5518
5626
  file: ctx.pdfPath
@@ -5522,10 +5630,10 @@ var check8 = {
5522
5630
  return [];
5523
5631
  }
5524
5632
  };
5525
- registerCheck(check8);
5633
+ registerCheck(check9);
5526
5634
 
5527
5635
  // src/checks/pdf/toc-links.ts
5528
- var check9 = {
5636
+ var check10 = {
5529
5637
  id: "pdf.nav.toc-links",
5530
5638
  name: "TOC Links",
5531
5639
  description: "Detects link annotations on TOC pages",
@@ -5540,7 +5648,7 @@ var check9 = {
5540
5648
  if (!doc) {
5541
5649
  return [
5542
5650
  {
5543
- checkId: check9.id,
5651
+ checkId: check10.id,
5544
5652
  severity: "warning",
5545
5653
  message: "Could not inspect PDF for TOC links.",
5546
5654
  file: ctx.pdfPath
@@ -5551,7 +5659,7 @@ var check9 = {
5551
5659
  if (!hasLinks) {
5552
5660
  return [
5553
5661
  {
5554
- checkId: check9.id,
5662
+ checkId: check10.id,
5555
5663
  severity: "warning",
5556
5664
  message: "No link annotations found in PDF (expected for TOC).",
5557
5665
  file: ctx.pdfPath
@@ -5561,10 +5669,10 @@ var check9 = {
5561
5669
  return [];
5562
5670
  }
5563
5671
  };
5564
- registerCheck(check9);
5672
+ registerCheck(check10);
5565
5673
 
5566
5674
  // src/checks/pdf/cross-refs.ts
5567
- var check10 = {
5675
+ var check11 = {
5568
5676
  id: "pdf.nav.cross-refs",
5569
5677
  name: "Cross References",
5570
5678
  description: "Counts and verifies internal link annotations",
@@ -5577,7 +5685,7 @@ var check10 = {
5577
5685
  if (!doc) {
5578
5686
  return [
5579
5687
  {
5580
- checkId: check10.id,
5688
+ checkId: check11.id,
5581
5689
  severity: "info",
5582
5690
  message: "Could not inspect PDF for cross-references.",
5583
5691
  file: ctx.pdfPath
@@ -5587,7 +5695,7 @@ var check10 = {
5587
5695
  const linkCount = await countLinkAnnotations(doc);
5588
5696
  return [
5589
5697
  {
5590
- checkId: check10.id,
5698
+ checkId: check11.id,
5591
5699
  severity: "info",
5592
5700
  message: `PDF contains ${linkCount} internal link annotation${linkCount !== 1 ? "s" : ""}.`,
5593
5701
  file: ctx.pdfPath
@@ -5595,10 +5703,10 @@ var check10 = {
5595
5703
  ];
5596
5704
  }
5597
5705
  };
5598
- registerCheck(check10);
5706
+ registerCheck(check11);
5599
5707
 
5600
5708
  // src/checks/pdf/page-labels.ts
5601
- var check11 = {
5709
+ var check12 = {
5602
5710
  id: "pdf.nav.page-labels",
5603
5711
  name: "Page Labels",
5604
5712
  description: "Verifies PDF page labels / folio numbering",
@@ -5613,7 +5721,7 @@ var check11 = {
5613
5721
  const labels = await getPageLabels(doc);
5614
5722
  return [
5615
5723
  {
5616
- checkId: check11.id,
5724
+ checkId: check12.id,
5617
5725
  severity: "info",
5618
5726
  message: labels && labels.length > 0 ? "PDF contains page labels." : "PDF does not contain explicit page labels.",
5619
5727
  file: ctx.pdfPath
@@ -5621,10 +5729,10 @@ var check11 = {
5621
5729
  ];
5622
5730
  }
5623
5731
  };
5624
- registerCheck(check11);
5732
+ registerCheck(check12);
5625
5733
 
5626
5734
  // src/checks/pdf/image-resolution.ts
5627
- var check12 = {
5735
+ var check13 = {
5628
5736
  id: "pdf.print.image-resolution",
5629
5737
  name: "Image Resolution",
5630
5738
  description: "Checks DPI of images embedded in the PDF",
@@ -5651,7 +5759,7 @@ var check12 = {
5651
5759
  lowResPages.sort((a, b) => a - b);
5652
5760
  return [
5653
5761
  {
5654
- checkId: check12.id,
5762
+ checkId: check13.id,
5655
5763
  severity: "warning",
5656
5764
  message: `Low-resolution images (below ${minDpi} DPI) found on pages: ${lowResPages.join(", ")}`,
5657
5765
  file: ctx.pdfPath
@@ -5661,10 +5769,10 @@ var check12 = {
5661
5769
  return [];
5662
5770
  }
5663
5771
  };
5664
- registerCheck(check12);
5772
+ registerCheck(check13);
5665
5773
 
5666
5774
  // src/checks/pdf/transparency.ts
5667
- var check13 = {
5775
+ var check14 = {
5668
5776
  id: "pdf.print.transparency",
5669
5777
  name: "Transparency",
5670
5778
  description: "Checks for transparency markers (/Transparency, /SMask, /BM) in the PDF",
@@ -5686,7 +5794,7 @@ var check13 = {
5686
5794
  if (found.length > 0) {
5687
5795
  return [
5688
5796
  {
5689
- checkId: check13.id,
5797
+ checkId: check14.id,
5690
5798
  severity: "warning",
5691
5799
  message: `Transparency detected in PDF: ${found.join(", ")}. This may cause issues with some printers.`,
5692
5800
  file: ctx.pdfPath
@@ -5696,7 +5804,7 @@ var check13 = {
5696
5804
  return [];
5697
5805
  }
5698
5806
  };
5699
- registerCheck(check13);
5807
+ registerCheck(check14);
5700
5808
 
5701
5809
  // src/checks/pdf/bleed.ts
5702
5810
  function findBox(bytes, box) {
@@ -5706,7 +5814,7 @@ function findBox(bytes, box) {
5706
5814
  return null;
5707
5815
  return [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
5708
5816
  }
5709
- var check14 = {
5817
+ var check15 = {
5710
5818
  id: "pdf.print.bleed",
5711
5819
  name: "Bleed Box",
5712
5820
  description: "Compares MediaBox vs TrimBox/BleedBox to verify bleed area",
@@ -5720,7 +5828,7 @@ var check14 = {
5720
5828
  const bytes = await readPdfBytes(ctx.pdfPath).catch(() => "");
5721
5829
  if (!bytes) {
5722
5830
  return [
5723
- inspectionFailed(check14.id, "Could not inspect PDF for bleed boxes.", {
5831
+ inspectionFailed(check15.id, "Could not inspect PDF for bleed boxes.", {
5724
5832
  file: ctx.pdfPath
5725
5833
  })
5726
5834
  ];
@@ -5730,14 +5838,14 @@ var check14 = {
5730
5838
  const bleed = findBox(bytes, "BleedBox");
5731
5839
  if (!media) {
5732
5840
  return [
5733
- inspectionFailed(check14.id, "Could not parse MediaBox from PDF.", {
5841
+ inspectionFailed(check15.id, "Could not parse MediaBox from PDF.", {
5734
5842
  file: ctx.pdfPath
5735
5843
  })
5736
5844
  ];
5737
5845
  }
5738
5846
  if (!trim && !bleed) {
5739
5847
  return [
5740
- finding(check14.id, {
5848
+ finding(check15.id, {
5741
5849
  severity: "warning",
5742
5850
  message: "No TrimBox or BleedBox found. Bleed area cannot be verified.",
5743
5851
  file: ctx.pdfPath
@@ -5750,7 +5858,7 @@ var check14 = {
5750
5858
  const bleedSize = ctx.config.validate.pdf.bleedSize;
5751
5859
  if (mediaW - trimW < bleedSize * 2 * 0.9) {
5752
5860
  return [
5753
- finding(check14.id, {
5861
+ finding(check15.id, {
5754
5862
  severity: "warning",
5755
5863
  message: `Bleed area appears insufficient. Expected at least ${bleedSize}pt on each side.`,
5756
5864
  file: ctx.pdfPath
@@ -5761,10 +5869,10 @@ var check14 = {
5761
5869
  return [];
5762
5870
  }
5763
5871
  };
5764
- registerCheck(check14);
5872
+ registerCheck(check15);
5765
5873
 
5766
5874
  // src/checks/pdf/pdfx-metadata.ts
5767
- var check15 = {
5875
+ var check16 = {
5768
5876
  id: "pdf.print.pdfx-metadata",
5769
5877
  name: "PDF/X Metadata",
5770
5878
  description: "Verifies PDF/X DOCINFO metadata via qpdf JSON",
@@ -5783,22 +5891,22 @@ var check15 = {
5783
5891
  const objects = parseQpdfObjectsJson(stdout);
5784
5892
  if (!objects) {
5785
5893
  return [
5786
- inspectionFailed(check15.id, "Unable to parse qpdf JSON output for PDF/X metadata checks.", { file: ctx.pdfPath })
5894
+ inspectionFailed(check16.id, "Unable to parse qpdf JSON output for PDF/X metadata checks.", { file: ctx.pdfPath })
5787
5895
  ];
5788
5896
  }
5789
- return getPdfxMetadataIssues(objects, ctx.config.pdfx.flavor).map((message) => finding(check15.id, {
5897
+ return getPdfxMetadataIssues(objects, ctx.config.pdfx.flavor).map((message) => finding(check16.id, {
5790
5898
  severity: "error",
5791
5899
  message,
5792
5900
  file: ctx.pdfPath
5793
5901
  }));
5794
5902
  } catch {
5795
5903
  return [
5796
- inspectionFailed(check15.id, "Failed to inspect PDF objects with qpdf for PDF/X metadata.", { file: ctx.pdfPath })
5904
+ inspectionFailed(check16.id, "Failed to inspect PDF objects with qpdf for PDF/X metadata.", { file: ctx.pdfPath })
5797
5905
  ];
5798
5906
  }
5799
5907
  }
5800
5908
  };
5801
- registerCheck(check15);
5909
+ registerCheck(check16);
5802
5910
 
5803
5911
  // src/checks/source/markdownlint.ts
5804
5912
  import { readFile as readFile9 } from "node:fs/promises";
@@ -5853,7 +5961,7 @@ async function loadConfig(configPath) {
5853
5961
  return {};
5854
5962
  }
5855
5963
  var MARKDOWNLINT_CHECK_ID = "source.markdownlint";
5856
- var check16 = {
5964
+ var check17 = {
5857
5965
  id: MARKDOWNLINT_CHECK_ID,
5858
5966
  name: "Markdownlint",
5859
5967
  description: "Runs markdownlint with project config to validate Markdown files",
@@ -5878,7 +5986,7 @@ var check16 = {
5878
5986
  continue;
5879
5987
  for (const v of violations) {
5880
5988
  out.push({
5881
- checkId: check16.id,
5989
+ checkId: check17.id,
5882
5990
  severity: "warning",
5883
5991
  message: `${v.ruleDescription} (${v.ruleNames.join("/")})`,
5884
5992
  file,
@@ -5891,7 +5999,7 @@ var check16 = {
5891
5999
  return out;
5892
6000
  }
5893
6001
  };
5894
- registerCheck(check16);
6002
+ registerCheck(check17);
5895
6003
 
5896
6004
  // src/checks/source/htmlhint.ts
5897
6005
  import { existsSync as existsSync7 } from "node:fs";
@@ -5899,7 +6007,7 @@ import { readFile as readFile10 } from "node:fs/promises";
5899
6007
  import htmlhint from "htmlhint";
5900
6008
  var { HTMLHint } = htmlhint;
5901
6009
  var CONFIG_NAMES2 = [".htmlhintrc"];
5902
- var check17 = {
6010
+ var check18 = {
5903
6011
  id: "source.htmlhint",
5904
6012
  name: "HTMLHint",
5905
6013
  description: "Runs htmlhint with project config to validate generated HTML",
@@ -5919,7 +6027,7 @@ var check17 = {
5919
6027
  const html = await readFile10(ctx.htmlPath, "utf8");
5920
6028
  const messages = HTMLHint.verify(html, ruleset);
5921
6029
  return messages.map((m) => ({
5922
- checkId: check17.id,
6030
+ checkId: check18.id,
5923
6031
  severity: m.type === "error" ? "error" : "warning",
5924
6032
  message: `${m.rule.id}: ${m.message}`,
5925
6033
  file: ctx.htmlPath,
@@ -5928,13 +6036,35 @@ var check17 = {
5928
6036
  }));
5929
6037
  }
5930
6038
  };
5931
- registerCheck(check17);
6039
+ registerCheck(check18);
5932
6040
 
5933
6041
  // src/checks/source/stylelint.ts
5934
6042
  import { readFile as readFile11 } from "node:fs/promises";
5935
6043
 
5936
6044
  // src/lib/printsafe.ts
5937
6045
  import postcss from "postcss";
6046
+
6047
+ // src/lib/lint-rules.ts
6048
+ function ruleLevel(rules, id) {
6049
+ const v = rules?.[id];
6050
+ if (v === "off" || v === false)
6051
+ return "off";
6052
+ if (v === "warn" || v === "warning")
6053
+ return "warn";
6054
+ if (v === "error")
6055
+ return "error";
6056
+ return;
6057
+ }
6058
+ var DIRECTIVE = /^\s*gutterpress-disable-next-line\s+(.*?)\s*$/;
6059
+ function parseDisableDirective(commentText) {
6060
+ const m = DIRECTIVE.exec(commentText);
6061
+ if (!m)
6062
+ return null;
6063
+ const ids = m[1].split(/\s--(?:\s|$)/)[0].split(/[\s,]+/).filter(Boolean);
6064
+ return ids.length > 0 ? ids : null;
6065
+ }
6066
+
6067
+ // src/lib/printsafe.ts
5938
6068
  var ruleRemoteUrls = "printsafe/no-remote-urls";
5939
6069
  var ruleRiskyProps = "printsafe/no-risky-print-effects";
5940
6070
  var ruleSyntax = "printsafe/syntax-error";
@@ -5959,9 +6089,11 @@ var riskyProperties = new Set([
5959
6089
  "animation",
5960
6090
  "animation-name",
5961
6091
  "transition",
5962
- "will-change",
5963
- "clip-path"
6092
+ "will-change"
5964
6093
  ]);
6094
+ function isRasterizingClipPath(decl) {
6095
+ return decl.prop.toLowerCase() === "clip-path" && /\burl\(/i.test(decl.value);
6096
+ }
5965
6097
  var marginBoxAtRuleNames = new Set([
5966
6098
  "top-left-corner",
5967
6099
  "top-left",
@@ -6088,7 +6220,34 @@ function nodeLoc(node) {
6088
6220
  column: node.source?.start?.column ?? 1
6089
6221
  };
6090
6222
  }
6091
- function checkCss(css, from) {
6223
+ function checkCss(css, from, options = {}) {
6224
+ return filterFindings(checkCssRaw(css, from), css, options.rules);
6225
+ }
6226
+ function filterFindings(findings, css, rules) {
6227
+ const silenced = new Map;
6228
+ for (const m of css.matchAll(/\/\*([\s\S]*?)\*\//g)) {
6229
+ const ids = parseDisableDirective(m[1]);
6230
+ if (!ids)
6231
+ continue;
6232
+ const endLine = css.slice(0, m.index + m[0].length).split(`
6233
+ `).length;
6234
+ const set = silenced.get(endLine + 1) ?? new Set;
6235
+ for (const id of ids)
6236
+ set.add(id);
6237
+ silenced.set(endLine + 1, set);
6238
+ }
6239
+ const out = [];
6240
+ for (const f of findings) {
6241
+ if (silenced.get(f.line)?.has(f.rule))
6242
+ continue;
6243
+ const level = ruleLevel(rules, f.rule);
6244
+ if (level === "off")
6245
+ continue;
6246
+ out.push(level === "warn" ? { ...f, severity: "warning" } : level === "error" ? { ...f, severity: "error" } : f);
6247
+ }
6248
+ return out;
6249
+ }
6250
+ function checkCssRaw(css, from) {
6092
6251
  let root;
6093
6252
  try {
6094
6253
  root = postcss.parse(css, from ? { from } : undefined);
@@ -6137,6 +6296,13 @@ function checkCss(css, from) {
6137
6296
  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
6297
  ...nodeLoc(decl)
6139
6298
  });
6299
+ } else if (isRasterizingClipPath(decl)) {
6300
+ warnings.push({
6301
+ rule: ruleRiskyProps,
6302
+ severity: "warning",
6303
+ 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.`,
6304
+ ...nodeLoc(decl)
6305
+ });
6140
6306
  } else if (riskyProperties.has(prop)) {
6141
6307
  warnings.push({
6142
6308
  rule: ruleRiskyProps,
@@ -6190,7 +6356,7 @@ function checkCss(css, from) {
6190
6356
  }
6191
6357
 
6192
6358
  // src/checks/source/stylelint.ts
6193
- var check18 = {
6359
+ var check19 = {
6194
6360
  id: "source.stylelint",
6195
6361
  name: "Print-safety CSS",
6196
6362
  description: "Checks CSS for print-safety issues (remote URLs, rasterizing effects, and page-layer containment)",
@@ -6210,16 +6376,16 @@ var check18 = {
6210
6376
  css = await readFile11(file, "utf8");
6211
6377
  } catch (err) {
6212
6378
  results.push({
6213
- checkId: check18.id,
6379
+ checkId: check19.id,
6214
6380
  severity: "error",
6215
6381
  message: `Cannot read stylesheet: ${err instanceof Error ? err.message : String(err)}`,
6216
6382
  file
6217
6383
  });
6218
6384
  continue;
6219
6385
  }
6220
- for (const w of checkCss(css, file)) {
6386
+ for (const w of checkCss(css, file, { rules: ctx.config.lint.rules })) {
6221
6387
  results.push({
6222
- checkId: check18.id,
6388
+ checkId: check19.id,
6223
6389
  severity: w.severity,
6224
6390
  message: `${w.rule}: ${w.message}`,
6225
6391
  file,
@@ -6231,7 +6397,7 @@ var check18 = {
6231
6397
  return results;
6232
6398
  }
6233
6399
  };
6234
- registerCheck(check18);
6400
+ registerCheck(check19);
6235
6401
 
6236
6402
  // src/checks/source/css-ownership.ts
6237
6403
  import { readFile as readFile12 } from "node:fs/promises";
@@ -6415,7 +6581,7 @@ function checkOwnership(root, file, maps) {
6415
6581
  });
6416
6582
  return out;
6417
6583
  }
6418
- var check19 = {
6584
+ var check20 = {
6419
6585
  id: "source.css-ownership",
6420
6586
  name: "CSS ownership contract",
6421
6587
  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 +6604,7 @@ var check19 = {
6438
6604
  } catch (err) {
6439
6605
  return [
6440
6606
  {
6441
- checkId: check19.id,
6607
+ checkId: check20.id,
6442
6608
  severity: "error",
6443
6609
  message: `Failed to parse CSS ownership contract "${configPath}": ${err instanceof Error ? err.message : String(err)}`,
6444
6610
  file: configPath
@@ -6480,7 +6646,7 @@ var check19 = {
6480
6646
  }
6481
6647
  for (const v of violations) {
6482
6648
  results.push({
6483
- checkId: check19.id,
6649
+ checkId: check20.id,
6484
6650
  severity: "warning",
6485
6651
  message: v.message,
6486
6652
  file: absFile,
@@ -6492,7 +6658,7 @@ var check19 = {
6492
6658
  return results;
6493
6659
  }
6494
6660
  };
6495
- registerCheck(check19);
6661
+ registerCheck(check20);
6496
6662
 
6497
6663
  // src/checks/source/local-refs.ts
6498
6664
  import { existsSync as existsSync9 } from "node:fs";
@@ -6883,7 +7049,7 @@ async function* renderedLocalRefs(ctx, checkId, results) {
6883
7049
  }
6884
7050
 
6885
7051
  // src/checks/source/local-refs.ts
6886
- var check20 = {
7052
+ var check21 = {
6887
7053
  id: "source.links.local-refs",
6888
7054
  name: "Local Markdown References",
6889
7055
  description: "Checks local markdown links and image refs resolve on disk",
@@ -6891,12 +7057,12 @@ var check20 = {
6891
7057
  phase: "pre-build",
6892
7058
  async run(ctx) {
6893
7059
  const results = [];
6894
- for await (const { ref, kind, line, file } of renderedLocalRefs(ctx, check20.id, results)) {
7060
+ for await (const { ref, kind, line, file } of renderedLocalRefs(ctx, check21.id, results)) {
6895
7061
  if (isNonFilesystemRef(ref))
6896
7062
  continue;
6897
7063
  const escape = kind === "image" ? proseImageRefError(ref, ctx.inputDir) : null;
6898
7064
  if (escape) {
6899
- results.push(finding(check20.id, {
7065
+ results.push(finding(check21.id, {
6900
7066
  severity: "error",
6901
7067
  message: escape,
6902
7068
  file,
@@ -6906,7 +7072,7 @@ var check20 = {
6906
7072
  }
6907
7073
  if (localRefExists(ref, kind, file, ctx.inputDir))
6908
7074
  continue;
6909
- results.push(finding(check20.id, {
7075
+ results.push(finding(check21.id, {
6910
7076
  severity: kind === "image" ? "warning" : "error",
6911
7077
  code: kind === "image" ? "missing-image-placeholder" : undefined,
6912
7078
  message: kind === "image" ? `Local image not found; build will substitute a magenta placeholder: ${ref}` : `Local reference not found: ${ref}`,
@@ -6931,7 +7097,7 @@ function localRefExists(ref, kind, sourceFile, projectRoot) {
6931
7097
  return existsSync9(fileRelative);
6932
7098
  }
6933
7099
  }
6934
- registerCheck(check20);
7100
+ registerCheck(check21);
6935
7101
 
6936
7102
  // src/lib/build-staging.ts
6937
7103
  import path9 from "node:path";
@@ -7197,7 +7363,7 @@ async function createStageRoot() {
7197
7363
  }
7198
7364
 
7199
7365
  // src/checks/source/dangling-links.ts
7200
- var check21 = {
7366
+ var check22 = {
7201
7367
  id: "source.links.dangling",
7202
7368
  name: "Dangling Links",
7203
7369
  description: "Flags relative links the printed book cannot open (their href is dropped at print time)",
@@ -7205,10 +7371,10 @@ var check21 = {
7205
7371
  phase: "pre-build",
7206
7372
  async run(ctx) {
7207
7373
  const results = [];
7208
- for await (const { ref, kind, line, file } of renderedLocalRefs(ctx, check21.id, results)) {
7374
+ for await (const { ref, kind, line, file } of renderedLocalRefs(ctx, check22.id, results)) {
7209
7375
  if (kind !== "link" || isPrintResolvableHref(ref))
7210
7376
  continue;
7211
- results.push(finding(check21.id, {
7377
+ results.push(finding(check22.id, {
7212
7378
  severity: "warning",
7213
7379
  code: "dangling-link",
7214
7380
  data: { ref },
@@ -7220,11 +7386,11 @@ var check21 = {
7220
7386
  return results;
7221
7387
  }
7222
7388
  };
7223
- registerCheck(check21);
7389
+ registerCheck(check22);
7224
7390
 
7225
7391
  // src/checks/source/accessibility-alt-text.ts
7226
7392
  import { readFile as readFile15 } from "node:fs/promises";
7227
- var check22 = {
7393
+ var check23 = {
7228
7394
  id: "source.accessibility.alt-text",
7229
7395
  name: "Image Alt Text",
7230
7396
  description: "Checks markdown images include non-empty alt text",
@@ -7253,7 +7419,7 @@ var check22 = {
7253
7419
  if (alt.trim())
7254
7420
  continue;
7255
7421
  results.push({
7256
- checkId: check22.id,
7422
+ checkId: check23.id,
7257
7423
  severity: "warning",
7258
7424
  message: "Image is missing alt text",
7259
7425
  file,
@@ -7278,11 +7444,11 @@ function extractImageAlts(line) {
7278
7444
  }
7279
7445
  return alts;
7280
7446
  }
7281
- registerCheck(check22);
7447
+ registerCheck(check23);
7282
7448
 
7283
7449
  // src/checks/source/accessibility-heading-order.ts
7284
7450
  import { readFile as readFile16 } from "node:fs/promises";
7285
- var check23 = {
7451
+ var check24 = {
7286
7452
  id: "source.accessibility.heading-order",
7287
7453
  name: "Heading Order",
7288
7454
  description: "Checks markdown heading levels do not jump by more than one",
@@ -7300,6 +7466,7 @@ var check23 = {
7300
7466
  `);
7301
7467
  let inFence = false;
7302
7468
  let prevLevel;
7469
+ let silencedLine = -1;
7303
7470
  for (let i = 0;i < lines.length; i++) {
7304
7471
  const line = lines[i] ?? "";
7305
7472
  if (/^\s*(```|~~~)/.test(line)) {
@@ -7308,13 +7475,18 @@ var check23 = {
7308
7475
  }
7309
7476
  if (inFence)
7310
7477
  continue;
7478
+ const comment = /^\s*<!--(.*)-->\s*$/.exec(line);
7479
+ if (comment && parseDisableDirective(comment[1])?.includes(check24.id)) {
7480
+ silencedLine = i + 1;
7481
+ continue;
7482
+ }
7311
7483
  const match = line.match(/^(#{1,6})\s+\S/);
7312
7484
  if (!match)
7313
7485
  continue;
7314
7486
  const level = match[1].length;
7315
- if (prevLevel != null && level > prevLevel + 1) {
7487
+ if (prevLevel != null && level > prevLevel + 1 && i !== silencedLine) {
7316
7488
  results.push({
7317
- checkId: check23.id,
7489
+ checkId: check24.id,
7318
7490
  severity: "warning",
7319
7491
  message: `Heading level jump from h${prevLevel} to h${level}`,
7320
7492
  file,
@@ -7328,11 +7500,11 @@ var check23 = {
7328
7500
  return results;
7329
7501
  }
7330
7502
  };
7331
- registerCheck(check23);
7503
+ registerCheck(check24);
7332
7504
 
7333
7505
  // src/checks/source/layout-markers.ts
7334
7506
  import { readFile as readFile17 } from "node:fs/promises";
7335
- var check24 = {
7507
+ var check25 = {
7336
7508
  id: "source.markdown.layout-markers",
7337
7509
  name: "Layout Markers",
7338
7510
  description: "Reports @page/@section/@chapter marker arguments Gutterpress could not understand, plus any unknown gp-* class",
@@ -7344,7 +7516,7 @@ var check24 = {
7344
7516
  return [];
7345
7517
  const results = [];
7346
7518
  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}`));
7519
+ results.push(inspectionFailed(check25.id, `Plugin "${ref}" could not be loaded, so markers it defines were not checked: ${error2.message}`));
7348
7520
  });
7349
7521
  const md = createMarkdownRenderer(plugins);
7350
7522
  for (const file of files) {
@@ -7353,7 +7525,7 @@ var check24 = {
7353
7525
  const env = {};
7354
7526
  md.render(content, env);
7355
7527
  for (const w of env.layoutWarnings ?? []) {
7356
- results.push(finding(check24.id, {
7528
+ results.push(finding(check25.id, {
7357
7529
  severity: w.severity ?? "warning",
7358
7530
  message: w.message,
7359
7531
  file,
@@ -7362,13 +7534,13 @@ var check24 = {
7362
7534
  }));
7363
7535
  }
7364
7536
  } catch (error2) {
7365
- results.push(inspectionFailed(check24.id, `Could not check layout markers in ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`, { file }));
7537
+ results.push(inspectionFailed(check25.id, `Could not check layout markers in ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`, { file }));
7366
7538
  }
7367
7539
  }
7368
7540
  return results;
7369
7541
  }
7370
7542
  };
7371
- registerCheck(check24);
7543
+ registerCheck(check25);
7372
7544
 
7373
7545
  // src/checks/source/merge-markers.ts
7374
7546
  import { readFile as readFile18 } from "node:fs/promises";
@@ -7401,7 +7573,7 @@ var CLOSE_SENTINEL = ">>>>>>> online version";
7401
7573
  function withoutOnlineTag(relPath) {
7402
7574
  return relPath.replace(/\.online(?=\.[^./]*$|$)/, "");
7403
7575
  }
7404
- var check25 = {
7576
+ var check26 = {
7405
7577
  id: "source.sync.merge-markers",
7406
7578
  name: "Combined Versions",
7407
7579
  description: "Finds passages and files still holding two versions (yours and the online copy) after a sync",
@@ -7415,7 +7587,7 @@ var check25 = {
7415
7587
  try {
7416
7588
  content = await readFile18(file, "utf8");
7417
7589
  } catch {
7418
- results.push(inspectionFailed(check25.id, `Could not read source file: ${file}`, { file }));
7590
+ results.push(inspectionFailed(check26.id, `Could not read source file: ${file}`, { file }));
7419
7591
  continue;
7420
7592
  }
7421
7593
  if (!content.includes(OPEN_SENTINEL) && !content.includes(CLOSE_SENTINEL))
@@ -7427,7 +7599,7 @@ var check25 = {
7427
7599
  const line = lines[i].endsWith("\r") ? lines[i].slice(0, -1) : lines[i];
7428
7600
  if (line === OPEN_SENTINEL) {
7429
7601
  inBlock = true;
7430
- results.push(finding(check25.id, {
7602
+ results.push(finding(check26.id, {
7431
7603
  severity: "error",
7432
7604
  code: "two-versions-passage",
7433
7605
  message: "This passage has two versions (yours and the online copy) — keep what you want, then delete the marker lines.",
@@ -7436,7 +7608,7 @@ var check25 = {
7436
7608
  }));
7437
7609
  } else if (line === CLOSE_SENTINEL) {
7438
7610
  if (!inBlock) {
7439
- results.push(finding(check25.id, {
7611
+ results.push(finding(check26.id, {
7440
7612
  severity: "error",
7441
7613
  code: "leftover-version-marker",
7442
7614
  message: "This marker line is left over from combining two versions — delete it.",
@@ -7457,7 +7629,7 @@ var check25 = {
7457
7629
  });
7458
7630
  for (const sibling of siblings.sort()) {
7459
7631
  const rel = path10.relative(ctx.inputDir, sibling).split(path10.sep).join("/");
7460
- results.push(finding(check25.id, {
7632
+ results.push(finding(check26.id, {
7461
7633
  severity: "warning",
7462
7634
  code: "kept-both-versions",
7463
7635
  message: `Two versions of ${withoutOnlineTag(rel)} are in your book — keep the one you want, then delete the other.`,
@@ -7465,12 +7637,12 @@ var check25 = {
7465
7637
  }));
7466
7638
  }
7467
7639
  } catch (error2) {
7468
- results.push(inspectionFailed(check25.id, `Could not scan for kept-both files: ${error2 instanceof Error ? error2.message : String(error2)}`));
7640
+ results.push(inspectionFailed(check26.id, `Could not scan for kept-both files: ${error2 instanceof Error ? error2.message : String(error2)}`));
7469
7641
  }
7470
7642
  return results;
7471
7643
  }
7472
7644
  };
7473
- registerCheck(check25);
7645
+ registerCheck(check26);
7474
7646
 
7475
7647
  // src/checks/asset/image-file-size.ts
7476
7648
  import { stat as stat4 } from "node:fs/promises";
@@ -7696,7 +7868,7 @@ async function collectImageFiles(dirs, exts, ignore2 = ASSET_SCAN_IGNORE_GLOBS)
7696
7868
  }
7697
7869
 
7698
7870
  // src/checks/asset/image-file-size.ts
7699
- var check26 = {
7871
+ var check27 = {
7700
7872
  id: "asset.image.file-size",
7701
7873
  name: "Image File Size",
7702
7874
  description: "Checks that image files do not exceed the maximum size limit",
@@ -7717,14 +7889,14 @@ var check26 = {
7717
7889
  if (info2.size > maxSize) {
7718
7890
  const sizeMb = (info2.size / 1e6).toFixed(1);
7719
7891
  const maxMb = (maxSize / 1e6).toFixed(1);
7720
- results.push(finding(check26.id, {
7892
+ results.push(finding(check27.id, {
7721
7893
  severity: "warning",
7722
7894
  message: `Image file too large: ${sizeMb}MB (max ${maxMb}MB)`,
7723
7895
  file
7724
7896
  }));
7725
7897
  }
7726
7898
  } catch {
7727
- results.push(inspectionFailed(check26.id, `Could not stat image file: ${file}`, {
7899
+ results.push(inspectionFailed(check27.id, `Could not stat image file: ${file}`, {
7728
7900
  file
7729
7901
  }));
7730
7902
  }
@@ -7732,10 +7904,10 @@ var check26 = {
7732
7904
  return results;
7733
7905
  }
7734
7906
  };
7735
- registerCheck(check26);
7907
+ registerCheck(check27);
7736
7908
 
7737
7909
  // src/checks/asset/image-resolution.ts
7738
- var check27 = {
7910
+ var check28 = {
7739
7911
  id: "asset.image.resolution",
7740
7912
  name: "Image Resolution",
7741
7913
  description: "Checks source image DPI from embedded density metadata",
@@ -7757,7 +7929,7 @@ var check27 = {
7757
7929
  const { xDpi, yDpi } = info2;
7758
7930
  if (xDpi > 0 && yDpi > 0 && (xDpi < minDpi || yDpi < minDpi)) {
7759
7931
  results.push({
7760
- checkId: check27.id,
7932
+ checkId: check28.id,
7761
7933
  severity: "warning",
7762
7934
  message: `Image resolution too low: ${xDpi}x${yDpi} DPI (minimum ${minDpi} DPI)`,
7763
7935
  file
@@ -7767,7 +7939,7 @@ var check27 = {
7767
7939
  return results;
7768
7940
  }
7769
7941
  };
7770
- registerCheck(check27);
7942
+ registerCheck(check28);
7771
7943
 
7772
7944
  // src/checks/asset/image-color-space.ts
7773
7945
  var LABEL = {
@@ -7775,7 +7947,7 @@ var LABEL = {
7775
7947
  gray: "Gray",
7776
7948
  cmyk: "CMYK"
7777
7949
  };
7778
- var check28 = {
7950
+ var check29 = {
7779
7951
  id: "asset.image.color-space",
7780
7952
  name: "Image Color Space",
7781
7953
  description: "Validates image color spaces against allowed list",
@@ -7796,7 +7968,7 @@ var check28 = {
7796
7968
  const cs = info2?.colorSpace;
7797
7969
  if (cs && !allowedLower.has(cs)) {
7798
7970
  results.push({
7799
- checkId: check28.id,
7971
+ checkId: check29.id,
7800
7972
  severity: "warning",
7801
7973
  message: `Image uses ${LABEL[cs] ?? cs} color space (allowed: ${allowed.join(", ")})`,
7802
7974
  file
@@ -7806,10 +7978,10 @@ var check28 = {
7806
7978
  return results;
7807
7979
  }
7808
7980
  };
7809
- registerCheck(check28);
7981
+ registerCheck(check29);
7810
7982
 
7811
7983
  // src/checks/asset/image-alpha.ts
7812
- var check29 = {
7984
+ var check30 = {
7813
7985
  id: "asset.image.alpha-channel",
7814
7986
  name: "Image Alpha Channel",
7815
7987
  description: "Checks for alpha channels in PNG/TIFF images",
@@ -7827,7 +7999,7 @@ var check29 = {
7827
7999
  const info2 = await inspectImage(file);
7828
8000
  if (info2?.hasAlpha) {
7829
8001
  results.push({
7830
- checkId: check29.id,
8002
+ checkId: check30.id,
7831
8003
  severity: "warning",
7832
8004
  message: "Image contains alpha channel, which may cause print issues",
7833
8005
  file
@@ -7837,10 +8009,10 @@ var check29 = {
7837
8009
  return results;
7838
8010
  }
7839
8011
  };
7840
- registerCheck(check29);
8012
+ registerCheck(check30);
7841
8013
 
7842
8014
  // src/checks/asset/image-tac.ts
7843
- var check30 = {
8015
+ var check31 = {
7844
8016
  id: "asset.image.tac-raster",
7845
8017
  name: "Image TAC (Raster)",
7846
8018
  description: "Rasterizes and checks TAC per image using Ghostscript",
@@ -7859,7 +8031,7 @@ var check30 = {
7859
8031
  const ghostscript = await resolveGhostscript();
7860
8032
  if (!ghostscript) {
7861
8033
  return [
7862
- inspectionFailed(check30.id, "Could not inspect image ink coverage: Ghostscript executable not found")
8034
+ inspectionFailed(check31.id, "Could not inspect image ink coverage: Ghostscript executable not found")
7863
8035
  ];
7864
8036
  }
7865
8037
  for (const file of files) {
@@ -7877,7 +8049,7 @@ var check30 = {
7877
8049
  if (nums.length === 4 && nums.every((n) => Number.isFinite(n))) {
7878
8050
  const tac = (nums[0] + nums[1] + nums[2] + nums[3]) * 100;
7879
8051
  if (tac > maxTac) {
7880
- results.push(finding(check30.id, {
8052
+ results.push(finding(check31.id, {
7881
8053
  severity: "warning",
7882
8054
  message: `Image TAC exceeds limit: ${tac.toFixed(1)}% (max ${maxTac}%)`,
7883
8055
  file,
@@ -7889,16 +8061,16 @@ var check30 = {
7889
8061
  }
7890
8062
  }
7891
8063
  } catch {
7892
- results.push(inspectionFailed(check30.id, `Could not inspect image ink coverage: ${file}`, { file }));
8064
+ results.push(inspectionFailed(check31.id, `Could not inspect image ink coverage: ${file}`, { file }));
7893
8065
  }
7894
8066
  }
7895
8067
  return results;
7896
8068
  }
7897
8069
  };
7898
- registerCheck(check30);
8070
+ registerCheck(check31);
7899
8071
 
7900
8072
  // src/checks/asset/approved-fonts.ts
7901
- var check31 = {
8073
+ var check32 = {
7902
8074
  id: "asset.font.approved-files",
7903
8075
  name: "Approved Font Files",
7904
8076
  description: "Checks font files against the approved file patterns",
@@ -7937,7 +8109,7 @@ var check31 = {
7937
8109
  for (const font of allFonts) {
7938
8110
  if (!approvedFonts.has(font)) {
7939
8111
  results.push({
7940
- checkId: check31.id,
8112
+ checkId: check32.id,
7941
8113
  severity: "warning",
7942
8114
  message: "Font file not in approved list",
7943
8115
  file: font
@@ -7947,7 +8119,7 @@ var check31 = {
7947
8119
  return results;
7948
8120
  }
7949
8121
  };
7950
- registerCheck(check31);
8122
+ registerCheck(check32);
7951
8123
 
7952
8124
  // src/checks/asset/font-license.ts
7953
8125
  import { existsSync as existsSync10 } from "node:fs";
@@ -7962,7 +8134,7 @@ var LICENSE_NAMES = [
7962
8134
  "OFL-1.1.txt",
7963
8135
  "COPYING"
7964
8136
  ];
7965
- var check32 = {
8137
+ var check33 = {
7966
8138
  id: "asset.font.license",
7967
8139
  name: "Font License",
7968
8140
  description: "Checks for license files in font directories",
@@ -7989,7 +8161,7 @@ var check32 = {
7989
8161
  const hasLicense = LICENSE_NAMES.some((name) => existsSync10(resolve6(fontDir, name)));
7990
8162
  if (!hasLicense) {
7991
8163
  results.push({
7992
- checkId: check32.id,
8164
+ checkId: check33.id,
7993
8165
  severity: "warning",
7994
8166
  message: `No font license file found in directory`,
7995
8167
  file: fontDir
@@ -7999,10 +8171,10 @@ var check32 = {
7999
8171
  return results;
8000
8172
  }
8001
8173
  };
8002
- registerCheck(check32);
8174
+ registerCheck(check33);
8003
8175
 
8004
8176
  // src/checks/heuristic/text-density.ts
8005
- var check33 = {
8177
+ var check34 = {
8006
8178
  id: "heuristic.whitespace.text-density",
8007
8179
  name: "Text Density",
8008
8180
  description: "Checks characters-per-page ratio",
@@ -8031,7 +8203,7 @@ var check33 = {
8031
8203
  });
8032
8204
  if (lowPages.length > 0) {
8033
8205
  results.push({
8034
- checkId: check33.id,
8206
+ checkId: check34.id,
8035
8207
  severity: "info",
8036
8208
  message: `Low text density on pages: ${lowPages.join(", ")} (below ${range.min} chars)`,
8037
8209
  file: ctx.pdfPath
@@ -8039,7 +8211,7 @@ var check33 = {
8039
8211
  }
8040
8212
  if (highPages.length > 0) {
8041
8213
  results.push({
8042
- checkId: check33.id,
8214
+ checkId: check34.id,
8043
8215
  severity: "info",
8044
8216
  message: `High text density on pages: ${highPages.join(", ")} (above ${range.max} chars)`,
8045
8217
  file: ctx.pdfPath
@@ -8048,11 +8220,11 @@ var check33 = {
8048
8220
  return results;
8049
8221
  }
8050
8222
  };
8051
- registerCheck(check33);
8223
+ registerCheck(check34);
8052
8224
 
8053
8225
  // src/checks/heuristic/section-density.ts
8054
8226
  import { readFile as readFile20 } from "node:fs/promises";
8055
- var check34 = {
8227
+ var check35 = {
8056
8228
  id: "heuristic.chunking.section-density",
8057
8229
  name: "Section Density",
8058
8230
  description: "Checks heading/paragraph/callout density from source Markdown",
@@ -8078,7 +8250,7 @@ var check34 = {
8078
8250
  const line = lines[i];
8079
8251
  if (/^#{1,6}\s/.test(line)) {
8080
8252
  if (paragraphCount > maxParas) {
8081
- results.push(finding(check34.id, {
8253
+ results.push(finding(check35.id, {
8082
8254
  severity: "info",
8083
8255
  message: `Section has ${paragraphCount} paragraphs (max recommended: ${maxParas})`,
8084
8256
  file,
@@ -8098,7 +8270,7 @@ var check34 = {
8098
8270
  }
8099
8271
  }
8100
8272
  if (paragraphCount > maxParas) {
8101
- results.push(finding(check34.id, {
8273
+ results.push(finding(check35.id, {
8102
8274
  severity: "info",
8103
8275
  message: `Section has ${paragraphCount} paragraphs (max recommended: ${maxParas})`,
8104
8276
  file,
@@ -8106,7 +8278,7 @@ var check34 = {
8106
8278
  }));
8107
8279
  }
8108
8280
  } catch {
8109
- results.push(inspectionFailed(check34.id, `Could not read source file: ${file}`, {
8281
+ results.push(inspectionFailed(check35.id, `Could not read source file: ${file}`, {
8110
8282
  file
8111
8283
  }));
8112
8284
  }
@@ -8114,10 +8286,10 @@ var check34 = {
8114
8286
  return results;
8115
8287
  }
8116
8288
  };
8117
- registerCheck(check34);
8289
+ registerCheck(check35);
8118
8290
 
8119
8291
  // src/checks/heuristic/layer-count.ts
8120
- var check35 = {
8292
+ var check36 = {
8121
8293
  id: "heuristic.decoration.layer-count",
8122
8294
  name: "Layer Count",
8123
8295
  description: "Counts image objects per page",
@@ -8142,7 +8314,7 @@ var check35 = {
8142
8314
  heavyPages.sort((a, b) => a - b);
8143
8315
  return [
8144
8316
  {
8145
- checkId: check35.id,
8317
+ checkId: check36.id,
8146
8318
  severity: "info",
8147
8319
  message: `Pages with many image layers (>${maxLayers}): ${heavyPages.join(", ")}`,
8148
8320
  file: ctx.pdfPath
@@ -8152,10 +8324,10 @@ var check35 = {
8152
8324
  return [];
8153
8325
  }
8154
8326
  };
8155
- registerCheck(check35);
8327
+ registerCheck(check36);
8156
8328
 
8157
8329
  // src/checks/heuristic/placement-variance.ts
8158
- var check36 = {
8330
+ var check37 = {
8159
8331
  id: "heuristic.layout.placement-variance",
8160
8332
  name: "Placement Variance",
8161
8333
  description: "Analyzes text baseline coordinates for layout consistency",
@@ -8173,7 +8345,7 @@ var check36 = {
8173
8345
  const uniqueX = new Set(positions.map((p) => Math.round(p.x)));
8174
8346
  return [
8175
8347
  {
8176
- checkId: check36.id,
8348
+ checkId: check37.id,
8177
8349
  severity: "info",
8178
8350
  message: `Layout analysis: ${uniqueX.size} unique horizontal text positions across ${positions.length} text blocks.`,
8179
8351
  file: ctx.pdfPath
@@ -8181,21 +8353,21 @@ var check36 = {
8181
8353
  ];
8182
8354
  }
8183
8355
  };
8184
- registerCheck(check36);
8356
+ registerCheck(check37);
8185
8357
 
8186
8358
  // src/checks/tool-check.ts
8187
8359
  async function checkToolAvailability(config, opts = {}) {
8188
8360
  const { checks: checks2 } = selectChecks(opts, config);
8189
8361
  const toolToChecks = new Map;
8190
- for (const check37 of checks2) {
8191
- if (!check37.requiredTools?.length)
8362
+ for (const check38 of checks2) {
8363
+ if (!check38.requiredTools?.length)
8192
8364
  continue;
8193
- for (const tool of check37.requiredTools) {
8365
+ for (const tool of check38.requiredTools) {
8194
8366
  const existing = toolToChecks.get(tool);
8195
8367
  if (existing) {
8196
- existing.push(check37.id);
8368
+ existing.push(check38.id);
8197
8369
  } else {
8198
- toolToChecks.set(tool, [check37.id]);
8370
+ toolToChecks.set(tool, [check38.id]);
8199
8371
  }
8200
8372
  }
8201
8373
  }
@@ -8211,11 +8383,11 @@ async function checkToolAvailability(config, opts = {}) {
8211
8383
  const missing = results.filter((r) => !r.found).map((r) => r.tool);
8212
8384
  const missingSet = new Set(missing);
8213
8385
  const skippedChecks = [];
8214
- for (const check37 of checks2) {
8215
- if (!check37.requiredTools?.length)
8386
+ for (const check38 of checks2) {
8387
+ if (!check38.requiredTools?.length)
8216
8388
  continue;
8217
- if (check37.requiredTools.some((t) => missingSet.has(t))) {
8218
- skippedChecks.push(check37.id);
8389
+ if (check38.requiredTools.some((t) => missingSet.has(t))) {
8390
+ skippedChecks.push(check38.id);
8219
8391
  }
8220
8392
  }
8221
8393
  return { available, missing, skippedChecks, toolToChecks };
@@ -8333,7 +8505,8 @@ ${projectCss.trim()}` : null
8333
8505
  `);
8334
8506
  const preloadTags = (opts.preloadImages ?? []).map((href) => `
8335
8507
  <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" : "");
8508
+ const lastPage = opts.print && lastPageMode(opts.print);
8509
+ const printAttrs = (opts.print?.signature && opts.print.signature > 1 ? ` data-gp-signature="${opts.print.signature}"` : "") + (lastPage ? ` data-gp-last-page="${lastPage}"` : "");
8337
8510
  return `<!DOCTYPE html>
8338
8511
  <html lang="en"${printAttrs}>
8339
8512
  <head>
@@ -8574,25 +8747,32 @@ async function runChecks(ctx, opts = {}) {
8574
8747
  }
8575
8748
  const release = retainPdfCache();
8576
8749
  try {
8577
- for (const check37 of checks2) {
8750
+ for (const check38 of checks2) {
8578
8751
  try {
8579
- const results = await check37.run(ctx);
8580
- const severityOverride = getCheckSeverityOverride(check37.id, ctx.config);
8752
+ let results = await check38.run(ctx);
8753
+ const severityOverride = getCheckSeverityOverride(check38.id, ctx.config);
8581
8754
  if (severityOverride) {
8582
8755
  for (const r of results) {
8583
8756
  r.severity = severityOverride;
8584
8757
  }
8585
8758
  }
8759
+ const lintLevel = ruleLevel(ctx.config.lint.rules, check38.id);
8760
+ if (lintLevel === "off")
8761
+ results = [];
8762
+ else if (lintLevel) {
8763
+ for (const r of results)
8764
+ r.severity = lintLevel === "warn" ? "warning" : "error";
8765
+ }
8586
8766
  if (results.length === 0) {
8587
- passed.push(check37.id);
8767
+ passed.push(check38.id);
8588
8768
  } else {
8589
8769
  allResults.push(...results);
8590
8770
  }
8591
8771
  } catch (err) {
8592
8772
  allResults.push({
8593
- checkId: check37.id,
8773
+ checkId: check38.id,
8594
8774
  severity: "error",
8595
- message: `Check "${check37.id}" threw: ${err instanceof Error ? err.message : String(err)}`
8775
+ message: `Check "${check38.id}" threw: ${err instanceof Error ? err.message : String(err)}`
8596
8776
  });
8597
8777
  }
8598
8778
  }
@@ -8884,7 +9064,7 @@ async function executeValidation(args) {
8884
9064
  };
8885
9065
  if (args.fix) {
8886
9066
  const selected = selectChecks(runnerOptions, config).checks;
8887
- if (selected.some((check37) => check37.id === MARKDOWNLINT_CHECK_ID)) {
9067
+ if (selected.some((check38) => check38.id === MARKDOWNLINT_CHECK_ID)) {
8888
9068
  await applyMarkdownlintFixes(context);
8889
9069
  } else {
8890
9070
  log.warn(`--fix: ${MARKDOWNLINT_CHECK_ID} is not part of this run — no files were fixed.`);
@@ -9121,7 +9301,8 @@ async function writeBuildFingerprint(input) {
9121
9301
  requestedFlavor: input.pdfx.requestedFlavor,
9122
9302
  resolvedFlavor: input.pdfx.resolvedFlavor,
9123
9303
  iccPath: input.pdfx.iccPath,
9124
- stripAnnotations: input.pdfx.stripAnnotations
9304
+ stripAnnotations: input.pdfx.stripAnnotations,
9305
+ blackText: input.pdfx.blackText ?? null
9125
9306
  },
9126
9307
  outputDir: input.recordedOutputDir ? path12.resolve(input.recordedOutputDir) : outputDir
9127
9308
  },
@@ -9208,13 +9389,64 @@ function* tokenize(s) {
9208
9389
  }
9209
9390
  }
9210
9391
  }
9211
- function rewriteBlackText(src) {
9392
+ var LARGE_TEXT_PT = 24;
9393
+ var SIZE_EPS = 0.05;
9394
+ var area = (m) => Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));
9395
+
9396
+ class TextScale {
9397
+ ctm;
9398
+ font = 0;
9399
+ tm = 1;
9400
+ stack = [];
9401
+ constructor(ctm) {
9402
+ this.ctm = ctm;
9403
+ }
9404
+ get unit() {
9405
+ return this.ctm;
9406
+ }
9407
+ get size() {
9408
+ return this.font * this.tm * this.ctm;
9409
+ }
9410
+ op(op, nums) {
9411
+ switch (op) {
9412
+ case "q":
9413
+ this.stack.push([this.ctm, this.font]);
9414
+ return false;
9415
+ case "Q": {
9416
+ const p = this.stack.pop();
9417
+ if (p)
9418
+ [this.ctm, this.font] = p;
9419
+ return true;
9420
+ }
9421
+ case "cm":
9422
+ if (nums.length === 6)
9423
+ this.ctm *= area(nums);
9424
+ return true;
9425
+ case "BT":
9426
+ this.tm = 1;
9427
+ return true;
9428
+ case "Tm":
9429
+ if (nums.length === 6)
9430
+ this.tm = area(nums);
9431
+ return true;
9432
+ case "Tf":
9433
+ this.font = Math.abs(nums[nums.length - 1] ?? 0) || 0;
9434
+ return true;
9435
+ }
9436
+ return false;
9437
+ }
9438
+ }
9439
+ function rewriteBlackText(src, ctx = {}) {
9212
9440
  let fill = null;
9213
9441
  let stroke = null;
9214
9442
  const stack = [];
9443
+ const scale = new TextScale(ctx.baseScale ?? 1);
9215
9444
  let inText = false;
9216
9445
  let fillDirty = false;
9217
9446
  let strokeDirty = false;
9447
+ const spans = [];
9448
+ let lastOp = "";
9449
+ let rect = null;
9218
9450
  const edits = [];
9219
9451
  let operands = [];
9220
9452
  const rgbOf = () => {
@@ -9223,12 +9455,46 @@ function rewriteBlackText(src) {
9223
9455
  const v = operands.map((t) => Number(t.text));
9224
9456
  return v.some((x) => Number.isNaN(x)) ? null : { rgb: v, raw: operands.map((t) => t.text).join(" ") };
9225
9457
  };
9458
+ const isSmall = () => scale.size <= LARGE_TEXT_PT + SIZE_EPS;
9459
+ let fillIns = null;
9460
+ let strokeIns = null;
9461
+ const syncOne = (colour, dirty, ins, at, gray, op) => {
9462
+ if (!colour || !isNearBlack(colour.rgb))
9463
+ return [dirty, ins];
9464
+ const small = isSmall();
9465
+ if (small && !dirty) {
9466
+ const e = { start: at, end: at, text: ` ${gray}` };
9467
+ edits.push(e);
9468
+ return [true, e];
9469
+ }
9470
+ if (!small && dirty) {
9471
+ if (ins)
9472
+ ins.text = "";
9473
+ else
9474
+ edits.push({ start: at, end: at, text: ` ${colour.raw} ${op}` });
9475
+ return [false, null];
9476
+ }
9477
+ return [dirty, ins];
9478
+ };
9479
+ const sync = (at) => {
9480
+ [fillDirty, fillIns] = syncOne(fill, fillDirty, fillIns, at, "0 g", "rg");
9481
+ [strokeDirty, strokeIns] = syncOne(stroke, strokeDirty, strokeIns, at, "0 G", "RG");
9482
+ };
9483
+ const flush = (s) => {
9484
+ for (const r of s.rects) {
9485
+ if (r.thickness > 0.2 * s.maxSmall)
9486
+ continue;
9487
+ edits.push({ start: r.start, end: r.start, text: "q 0 g " }, { start: r.end, end: r.end, text: " Q" });
9488
+ }
9489
+ };
9226
9490
  for (const t of tokenize(src)) {
9227
9491
  if (!t.op) {
9228
9492
  operands.push(t);
9229
9493
  continue;
9230
9494
  }
9231
9495
  const op = t.text;
9496
+ const nums = operands.map((o) => Number(o.text));
9497
+ const sizeMayHaveChanged = scale.op(op, nums);
9232
9498
  switch (op) {
9233
9499
  case "q":
9234
9500
  stack.push([fill, stroke]);
@@ -9242,19 +9508,13 @@ function rewriteBlackText(src) {
9242
9508
  case "BT":
9243
9509
  inText = true;
9244
9510
  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
- }
9511
+ fillIns = strokeIns = null;
9512
+ sync(t.end);
9513
+ break;
9514
+ case "Tf":
9515
+ case "Tm":
9516
+ if (inText && sizeMayHaveChanged)
9517
+ sync(t.end);
9258
9518
  break;
9259
9519
  case "ET":
9260
9520
  if (inText) {
@@ -9267,6 +9527,7 @@ function rewriteBlackText(src) {
9267
9527
  edits.push({ start: t.start, end: t.start, text: ins });
9268
9528
  }
9269
9529
  inText = false;
9530
+ fillIns = strokeIns = null;
9270
9531
  break;
9271
9532
  case "rg":
9272
9533
  case "RG": {
@@ -9276,7 +9537,7 @@ function rewriteBlackText(src) {
9276
9537
  fill = c;
9277
9538
  else
9278
9539
  stroke = c;
9279
- if (inText && c && isNearBlack(c.rgb)) {
9540
+ if (inText && c && isNearBlack(c.rgb) && isSmall()) {
9280
9541
  edits.push({ start: operands[0].start, end: t.end, text: isFill ? "0 g" : "0 G" });
9281
9542
  if (isFill)
9282
9543
  fillDirty = true;
@@ -9308,7 +9569,54 @@ function rewriteBlackText(src) {
9308
9569
  if (inText)
9309
9570
  strokeDirty = false;
9310
9571
  break;
9572
+ case "Do": {
9573
+ const name = operands[operands.length - 1]?.text;
9574
+ if (name?.startsWith("/"))
9575
+ ctx.onDo?.(name.slice(1), scale.unit);
9576
+ break;
9577
+ }
9578
+ case "BDC":
9579
+ case "BMC":
9580
+ spans.push({ hasSmall: false, maxSmall: 0, rects: [] });
9581
+ break;
9582
+ case "EMC": {
9583
+ const s = spans.pop();
9584
+ if (!s)
9585
+ break;
9586
+ const parent = spans[spans.length - 1];
9587
+ if (s.hasSmall)
9588
+ flush(s);
9589
+ else
9590
+ parent?.rects.push(...s.rects);
9591
+ if (parent && s.hasSmall) {
9592
+ parent.hasSmall = true;
9593
+ parent.maxSmall = Math.max(parent.maxSmall, s.maxSmall);
9594
+ }
9595
+ break;
9596
+ }
9597
+ case "Tj":
9598
+ case "TJ":
9599
+ case "'":
9600
+ case '"': {
9601
+ fillIns = strokeIns = null;
9602
+ const span = spans[spans.length - 1];
9603
+ if (span && inText && isSmall()) {
9604
+ span.hasSmall = true;
9605
+ span.maxSmall = Math.max(span.maxSmall, scale.size);
9606
+ }
9607
+ break;
9608
+ }
9609
+ case "re":
9610
+ rect = !inText && fill && isNearBlack(fill.rgb) && operands.length === 4 && nums.every((x) => !Number.isNaN(x)) ? { start: operands[0].start, thickness: Math.min(Math.abs(nums[2]), Math.abs(nums[3])) * scale.unit } : null;
9611
+ break;
9612
+ case "f":
9613
+ case "F":
9614
+ case "f*":
9615
+ if (rect && lastOp === "re")
9616
+ spans[spans.length - 1]?.rects.push({ ...rect, end: t.end });
9617
+ break;
9311
9618
  }
9619
+ lastOp = op;
9312
9620
  operands = [];
9313
9621
  }
9314
9622
  return applyEdits(src, edits);
@@ -9316,6 +9624,7 @@ function rewriteBlackText(src) {
9316
9624
  function applyEdits(s, edits) {
9317
9625
  if (!edits.length)
9318
9626
  return s;
9627
+ edits.sort((a, b) => a.start - b.start);
9319
9628
  let out = "";
9320
9629
  let pos = 0;
9321
9630
  for (const e of edits) {
@@ -9324,9 +9633,8 @@ function applyEdits(s, edits) {
9324
9633
  }
9325
9634
  return out + s.slice(pos);
9326
9635
  }
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 });
9636
+ async function visitContent(doc, visit) {
9637
+ const { PDFName, PDFRawStream, PDFArray, PDFRef, PDFDict, decodePDFRawStream } = await import("pdf-lib");
9330
9638
  const ctx = doc.context;
9331
9639
  const SKIP = ["Filter", "DecodeParms", "Length"].map((k) => PDFName.of(k));
9332
9640
  let changed = false;
@@ -9345,19 +9653,15 @@ async function blackTextToGray(pdf) {
9345
9653
  ctx.assign(ref, ctx.flateStream(Buffer.from(text, "latin1"), entries));
9346
9654
  changed = true;
9347
9655
  };
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()) {
9656
+ const formScale = new Map;
9657
+ const formMatrixScale = (form) => {
9658
+ const m = form.dict.lookupMaybe(PDFName.of("Matrix"), PDFArray);
9659
+ const nums = m?.asArray().map((n) => Number(n.toString()));
9660
+ return nums?.length === 6 && !nums.some(Number.isNaN) ? area(nums) : 1;
9661
+ };
9662
+ const pages = doc.getPages();
9663
+ for (let p = 0;p < pages.length; p++) {
9664
+ const page = pages[p];
9361
9665
  const contents = page.node.Contents();
9362
9666
  const refs = [];
9363
9667
  if (contents instanceof PDFArray) {
@@ -9375,16 +9679,49 @@ async function blackTextToGray(pdf) {
9375
9679
  if (!refs.length || !streams.every((s) => s instanceof PDFRawStream))
9376
9680
  continue;
9377
9681
  const parts = streams.map((s) => decode(s));
9378
- if (parts.some((p) => p === null))
9682
+ if (parts.some((q) => q === null))
9379
9683
  continue;
9380
9684
  const joined = parts.join(`
9381
9685
  `);
9382
- const next = rewriteBlackText(joined);
9686
+ const xobjects = page.node.Resources()?.lookupMaybe(PDFName.of("XObject"), PDFDict);
9687
+ const next = visit(joined, {
9688
+ baseScale: 1,
9689
+ onDo: (name, scale) => {
9690
+ const ref = xobjects?.get(PDFName.of(name));
9691
+ const form = ref instanceof PDFRef ? ctx.lookup(ref) : undefined;
9692
+ if (!(ref instanceof PDFRef) || !(form instanceof PDFRawStream))
9693
+ return;
9694
+ if (form.dict.get(PDFName.of("Subtype")) !== PDFName.of("Form"))
9695
+ return;
9696
+ const at = scale * formMatrixScale(form);
9697
+ formScale.set(ref.tag, Math.min(formScale.get(ref.tag) ?? Infinity, at));
9698
+ }
9699
+ });
9383
9700
  if (next === joined)
9384
9701
  continue;
9385
9702
  replace(refs[0], streams[0], next);
9386
9703
  page.node.set(PDFName.of("Contents"), refs[0]);
9387
9704
  }
9705
+ for (const [ref, obj] of ctx.enumerateIndirectObjects()) {
9706
+ if (!(obj instanceof PDFRawStream))
9707
+ continue;
9708
+ if (obj.dict.get(PDFName.of("Subtype")) !== PDFName.of("Form"))
9709
+ continue;
9710
+ const text = decode(obj);
9711
+ if (text === null)
9712
+ continue;
9713
+ const next = visit(text, {
9714
+ baseScale: formScale.get(ref.tag) ?? 0
9715
+ });
9716
+ if (next !== text)
9717
+ replace(ref, obj, next);
9718
+ }
9719
+ return changed;
9720
+ }
9721
+ async function blackTextToGray(pdf) {
9722
+ const { PDFDocument } = await import("pdf-lib");
9723
+ const doc = await PDFDocument.load(pdf, { updateMetadata: false });
9724
+ const changed = await visitContent(doc, (src, info2) => rewriteBlackText(src, info2));
9388
9725
  return changed ? doc.save({ updateFieldAppearances: false }) : pdf;
9389
9726
  }
9390
9727
 
@@ -9662,7 +9999,8 @@ class HtmlOutput {
9662
9999
  requestedFlavor: null,
9663
10000
  resolvedFlavor: config.pdfx.flavor,
9664
10001
  iccPath: null,
9665
- stripAnnotations: null
10002
+ stripAnnotations: null,
10003
+ blackText: null
9666
10004
  }
9667
10005
  }, `Wrote: ${path13.join(outDir, BOOK_HTML)}`, { htmlPath: htmlFile, pdfPath: null });
9668
10006
  }
@@ -9682,12 +10020,12 @@ class PdfOutput {
9682
10020
  const rawPdf = pdfxMode ? path13.join(stage, "raw.pdf") : path13.resolve(pdfFile);
9683
10021
  await fsp2.mkdir(path13.dirname(path13.resolve(pdfFile)), { recursive: true });
9684
10022
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
9685
- const { buildNativePdf } = await import("./engine-rhmwgk9k.js");
10023
+ const { buildNativePdf } = await import("./engine-zdb41ay1.js");
9686
10024
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
9687
10025
  title: config.title,
9688
10026
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
9689
10027
  signature: config.print.signature,
9690
- reserveLastPage: config.print.reserveLastPage,
10028
+ lastPage: lastPageMode(config.print),
9691
10029
  allowShrink: opts.allowShrink
9692
10030
  }, browser);
9693
10031
  for (const d of engineDiagnostics)
@@ -9746,7 +10084,8 @@ class PdfOutput {
9746
10084
  requestedFlavor: pdfxMode ?? null,
9747
10085
  resolvedFlavor: config.pdfx.flavor,
9748
10086
  iccPath: effectiveIccPath,
9749
- stripAnnotations: shouldStripAnnotations
10087
+ stripAnnotations: shouldStripAnnotations,
10088
+ blackText: pdfxMode ? config.pdfx.blackText : null
9750
10089
  }
9751
10090
  }, `Wrote: ${reportedPdf}`, { htmlPath: htmlFile, pdfPath: reportedPdf }, pdfName, engineDiagnostics);
9752
10091
  } finally {
@@ -10903,6 +11242,26 @@ async function startPreviewServer(options) {
10903
11242
  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";
10904
11243
  import { constants as FS, existsSync as existsSync12 } from "node:fs";
10905
11244
  import path18 from "node:path";
11245
+
11246
+ // src/lib/page-size.ts
11247
+ import postcss4 from "postcss";
11248
+ import { Scalar as Scalar2 } from "yaml";
11249
+ function isPositivePoints(value) {
11250
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
11251
+ }
11252
+ function writePageBounds(doc, page) {
11253
+ const out = { width: page.width, height: page.height };
11254
+ const tolerance = page.tolerance ?? numberAt(doc, ["page", "tolerance"]);
11255
+ if (tolerance !== undefined)
11256
+ out.tolerance = tolerance;
11257
+ doc.set("page", doc.createNode(out));
11258
+ }
11259
+ function numberAt(doc, keyPath) {
11260
+ const v = doc.getIn(keyPath);
11261
+ return isPositivePoints(v) ? v : undefined;
11262
+ }
11263
+
11264
+ // src/lib/project-scaffold.ts
10906
11265
  function starterOverridesCss() {
10907
11266
  return `/**
10908
11267
  * Your book's own styles.
@@ -10931,9 +11290,6 @@ function escapeYamlScalar(value) {
10931
11290
  return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
10932
11291
  }
10933
11292
  var DEFAULT_AUTHOR = "Anonymous";
10934
- function isPositivePoints(value) {
10935
- return typeof value === "number" && Number.isFinite(value) && value > 0;
10936
- }
10937
11293
  function requirePreset(preset, customPage) {
10938
11294
  if (!preset || !PRESET_IDS.includes(preset)) {
10939
11295
  const got = preset ? ` (got "${preset}")` : "";
@@ -11037,16 +11393,8 @@ async function scaffoldProject(options) {
11037
11393
  const { doc, file } = await loadManifestDoc(projectDir);
11038
11394
  doc.set("preset", preset);
11039
11395
  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
- }
11396
+ if (options.customPage)
11397
+ writePageBounds(doc, options.customPage);
11050
11398
  await writeManifestDoc(file, doc);
11051
11399
  }
11052
11400
  const firstSource = await firstSourceFile(manifestPath);
@@ -13562,13 +13910,16 @@ async function disconnectPublishCredential(key, deps, options = {}) {
13562
13910
  }
13563
13911
  }
13564
13912
  // src/lib/manifest-config.ts
13565
- import { isSeq as isSeq4, isMap as isMap3, YAMLSeq as YAMLSeq3, Scalar as Scalar2 } from "yaml";
13913
+ import { isSeq as isSeq4, isMap as isMap3, YAMLSeq as YAMLSeq3, Scalar as Scalar3 } from "yaml";
13566
13914
  // src/lib/host-policy.ts
13567
13915
  var AUTO_SYNC_MAX_MINUTES = 24 * 60;
13568
13916
  // src/lib/remote-auth/github-repos.ts
13569
13917
  var MANIFEST_NAMES = new Set(MANIFEST_FILENAMES);
13570
- // src/lib/remote-auth/clone.ts
13571
- import git3 from "isomorphic-git";
13918
+ // src/lib/remote-auth/online-backup.ts
13919
+ import git5 from "isomorphic-git";
13920
+
13921
+ // src/lib/remote-auth/sync.ts
13922
+ import git4 from "isomorphic-git";
13572
13923
 
13573
13924
  // src/lib/remote-auth/git-http.ts
13574
13925
  import httpNode from "isomorphic-git/http/node";
@@ -13661,21 +14012,21 @@ function withIdleTimeout(http3, idleMs = GIT_HTTP_IDLE_TIMEOUT_MS, uploadMs = GI
13661
14012
  }
13662
14013
  var defaultGitHttp = withIdleTimeout(httpNode);
13663
14014
 
13664
- // src/lib/remote-auth/transport.ts
14015
+ // src/lib/remote-auth/converge-merge.ts
13665
14016
  import git2 from "isomorphic-git";
14017
+ import diff3Merge from "diff3";
14018
+
14019
+ // src/lib/remote-auth/transport.ts
14020
+ import git3 from "isomorphic-git";
14021
+ // src/lib/remote-auth/clone.ts
14022
+ import git6 from "isomorphic-git";
13666
14023
  // src/lib/remote-auth/test-access.ts
13667
- import git4 from "isomorphic-git";
14024
+ import git7 from "isomorphic-git";
13668
14025
  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
14026
  // src/lib/remote-auth/repair.ts
13676
- import git7 from "isomorphic-git";
13677
- // src/lib/remote-auth/scorched-earth.ts
13678
14027
  import git8 from "isomorphic-git";
14028
+ // src/lib/remote-auth/scorched-earth.ts
14029
+ import git9 from "isomorphic-git";
13679
14030
  // src/lib/publish/selections.ts
13680
14031
  import { chmod as chmod2, mkdir as mkdir13, readFile as readFile30, writeFile as writeFile12 } from "node:fs/promises";
13681
14032
  import path26 from "node:path";