gutterpress 0.0.0 → 0.9.0-alpha.2

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 (82) hide show
  1. package/README.md +21 -16
  2. package/dist/api/index.d.ts +6 -2
  3. package/dist/api/index.js +15 -3
  4. package/dist/{audit-1jrwwacz.js → audit-nhn2pjz3.js} +5 -5
  5. package/dist/{build-5s92kgpc.js → build-san7fv2z.js} +5 -5
  6. package/dist/checks/types.d.ts +6 -0
  7. package/dist/{cli-hvqjs342.js → cli-e5zhb0xs.js} +2 -2
  8. package/dist/{cli-7ykx7pp0.js → cli-k4bd06sd.js} +982 -331
  9. package/dist/{cli-h7phg3ka.js → cli-najycadg.js} +258 -258
  10. package/dist/{index-1tyj4bk3.js → cli-wchtvxvw.js} +4 -1
  11. package/dist/{cli-5bxx0c4y.js → cli-yzrh708h.js} +2 -2
  12. package/dist/cli.js +15 -15
  13. package/dist/{doctor-25zjt8dz.js → doctor-hrk0kxxz.js} +2 -2
  14. package/dist/{index-3h3fef7r.js → index-bynn850m.js} +1200 -410
  15. package/dist/{cli-1tyj4bk3.js → index-wchtvxvw.js} +4 -1
  16. package/dist/{index-5bxx0c4y.js → index-yzrh708h.js} +2 -2
  17. package/dist/index.js +15 -3
  18. package/dist/lib/asset-inline.d.ts +19 -0
  19. package/dist/lib/build-error.d.ts +0 -1
  20. package/dist/lib/build-fingerprint.d.ts +14 -0
  21. package/dist/lib/build-runner.d.ts +21 -1
  22. package/dist/lib/ghostscript.d.ts +0 -9
  23. package/dist/lib/manifest-config.d.ts +7 -0
  24. package/dist/lib/manifest.d.ts +8 -0
  25. package/dist/lib/markdown/assemble.d.ts +4 -6
  26. package/dist/lib/markdown/chapter-id.d.ts +1 -1
  27. package/dist/lib/markdown/index.d.ts +1 -2
  28. package/dist/lib/npm-plugin-installer.d.ts +1 -1
  29. package/dist/lib/output-paths.d.ts +0 -1
  30. package/dist/lib/pagination.d.ts +0 -20
  31. package/dist/lib/pdf-inspect.d.ts +2 -2
  32. package/dist/lib/pdf-parse.d.ts +2 -1
  33. package/dist/lib/plugin-manager.d.ts +0 -2
  34. package/dist/lib/presets.d.ts +36 -7
  35. package/dist/lib/project-scaffold.d.ts +39 -2
  36. package/dist/lib/project-source.d.ts +11 -9
  37. package/dist/lib/project-templates.d.ts +45 -7
  38. package/dist/lib/publish/providers/azure-swa.d.ts +0 -1
  39. package/dist/lib/publish/providers/itch.d.ts +0 -1
  40. package/dist/lib/publish/providers/shopify.d.ts +0 -1
  41. package/dist/lib/remote-auth/github-repos.d.ts +0 -2
  42. package/dist/lib/remote-auth/recovery/dispatch.d.ts +0 -5
  43. package/dist/lib/remote-auth/recovery/outcome-mapping.d.ts +3 -2
  44. package/dist/lib/remote-auth/recovery/recover-stale-lock.d.ts +0 -8
  45. package/dist/lib/remote-auth/sync.d.ts +2 -10
  46. package/dist/lib/remote-auth/test-access.d.ts +0 -2
  47. package/dist/lib/remote-auth/test-support/git-http-server.d.ts +0 -37
  48. package/dist/lib/source-provider.d.ts +0 -14
  49. package/dist/lib/static-serve.d.ts +39 -0
  50. package/dist/lib/style-resolver.d.ts +10 -1
  51. package/dist/lib/targets.d.ts +84 -0
  52. package/dist/lib/theme-import.d.ts +0 -2
  53. package/dist/lib/theme-manager.d.ts +1 -1
  54. package/dist/lib/validation-exec.d.ts +4 -3
  55. package/dist/{lint-f63nxjfj.js → lint-96j9hrj4.js} +5 -5
  56. package/dist/{manifest.schema-rn1dpdpn.json → manifest.schema-z61rzw44.json} +12 -6
  57. package/dist/new-8p38wavc.js +185 -0
  58. package/dist/{pagedjs-interface-80g3xa8p.js → pagedjs-interface-qxvzgwd7.js} +92 -33
  59. package/dist/{plugin-wf2cbmwc.js → plugin-ees6nhkc.js} +5 -5
  60. package/dist/{preflight-tg4mpz1h.js → preflight-1q6c2edh.js} +24 -29
  61. package/dist/preview/file-watcher.d.ts +48 -39
  62. package/dist/preview/http-server.d.ts +5 -2
  63. package/dist/preview/server-context.d.ts +24 -0
  64. package/dist/preview-shell-6dqexx1m.js +581 -0
  65. package/dist/{preview-h2bwbb2x.js → preview-y5a2zen1.js} +5 -5
  66. package/dist/{project-source-0tg0r6vm.js → project-source-p0gn1wd5.js} +3 -3
  67. package/dist/{publish-ztaet53r.js → publish-rm9yb3wh.js} +5 -5
  68. package/dist/render.js +10 -19
  69. package/dist/{repair-jkp4n3j8.js → repair-zgq7q2g6.js} +5 -5
  70. package/dist/schema/manifest.types.d.ts +14 -3
  71. package/dist/server.d.ts +2 -0
  72. package/dist/{source-provider-hp96b5jf.js → source-provider-c1rjm2c0.js} +2 -8
  73. package/dist/types.d.ts +0 -1
  74. package/dist/{validate-k2drs19j.js → validate-nr0xa6sa.js} +8 -8
  75. package/package.json +2 -2
  76. package/dist/chapter-01-jj8ftn4c.md +0 -29
  77. package/dist/lib/validation-profile.d.ts +0 -18
  78. package/dist/manifest-ryyg71nf.yaml +0 -9
  79. package/dist/new-7b08a42h.js +0 -105
  80. package/dist/preview-shell-rghtfmc8.js +0 -155
  81. package/dist/theme-0yzvthe8.css +0 -65
  82. package/dist/theme-6a2fngc8.json +0 -6
@@ -20,13 +20,13 @@ import {
20
20
  stampCreator,
21
21
  stripAnnotations,
22
22
  warn
23
- } from "./cli-h7phg3ka.js";
23
+ } from "./cli-najycadg.js";
24
24
  import {
25
25
  BuildError,
26
26
  DEBOUNCE,
27
27
  UsageError,
28
28
  resolvePort
29
- } from "./cli-hvqjs342.js";
29
+ } from "./cli-e5zhb0xs.js";
30
30
  import {
31
31
  gitDirFor,
32
32
  gitScopeFor,
@@ -40,11 +40,11 @@ import {
40
40
  snapshotWorkingTreeUnlocked,
41
41
  stageChanges,
42
42
  withRepoLock
43
- } from "./cli-5bxx0c4y.js";
43
+ } from "./cli-yzrh708h.js";
44
44
  import {
45
45
  detectProjectSource,
46
46
  syncRemoteFor
47
- } from "./cli-1tyj4bk3.js";
47
+ } from "./cli-wchtvxvw.js";
48
48
  import {
49
49
  defaultGitHttp
50
50
  } from "./cli-yja077f6.js";
@@ -52,14 +52,10 @@ import {
52
52
  __require
53
53
  } from "./cli-37x76zdn.js";
54
54
 
55
- // src/lib/manifest.ts
56
- import { readFile } from "node:fs/promises";
57
- import { existsSync, statSync } from "node:fs";
58
- import { resolve, dirname } from "node:path";
59
- import { parse as parseYaml, YAMLParseError } from "yaml";
60
-
61
55
  // src/lib/presets.ts
56
+ var PRESET_IDS = ["dtrpg", "book", "custom"];
62
57
  var DTRPG_PRESET = {
58
+ defaultTargets: ["dtrpg"],
63
59
  plugins: [],
64
60
  source: {
65
61
  files: null
@@ -131,6 +127,7 @@ var DTRPG_PRESET = {
131
127
  }
132
128
  };
133
129
  var BOOK_PRESET = {
130
+ defaultTargets: [],
134
131
  plugins: [],
135
132
  source: {
136
133
  files: null
@@ -195,9 +192,15 @@ var BOOK_PRESET = {
195
192
  }
196
193
  }
197
194
  };
195
+ var CUSTOM_PRESET = {
196
+ ...BOOK_PRESET,
197
+ page: null,
198
+ defaultTargets: []
199
+ };
198
200
  var PRESETS = {
199
201
  dtrpg: DTRPG_PRESET,
200
- book: BOOK_PRESET
202
+ book: BOOK_PRESET,
203
+ custom: CUSTOM_PRESET
201
204
  };
202
205
  var warnedIds = new Set;
203
206
  function warnOnce(id, message, sink = defaultWarnSink) {
@@ -211,7 +214,7 @@ function defaultWarnSink(message) {
211
214
  }
212
215
  function resolvePreset(presetName) {
213
216
  if (presetName === undefined) {
214
- warnOnce("no-preset-set", '[gutterpress] No `preset` set in manifest.yaml — defaulting to "dtrpg" ' + "(DriveThruRPG trim size, TAC cap 240, forced PDF/X) for backward " + "compatibility. Set `preset: dtrpg` to keep this explicitly, or " + "`preset: book` for a neutral 6x9in trade-book default with no " + "vendor TAC cap or PDF/X forcing.");
217
+ warnOnce("no-preset-set", '[gutterpress] No `preset` in manifest.yaml — using "dtrpg" ' + "(DriveThruRPG print-ready defaults). Set `preset: dtrpg`, `book`, " + "or `custom` to choose explicitly.");
215
218
  return DTRPG_PRESET;
216
219
  }
217
220
  const preset = PRESETS[presetName];
@@ -221,7 +224,126 @@ function resolvePreset(presetName) {
221
224
  return preset;
222
225
  }
223
226
 
227
+ // src/lib/targets.ts
228
+ var DTRPG_TARGET = {
229
+ id: "dtrpg",
230
+ label: "DriveThruRPG",
231
+ overlay: {
232
+ pdfx: { flavor: DTRPG_PRESET.pdfx.flavor },
233
+ ink: {
234
+ maxTac: DTRPG_PRESET.ink.maxTac,
235
+ tacTolerance: DTRPG_PRESET.ink.tacTolerance
236
+ },
237
+ validate: {
238
+ checks: {
239
+ "pdf.structure.qpdf": { enabled: true, severity: "error" },
240
+ "pdf.print.pdfx-markers": { enabled: true, severity: "error" },
241
+ "pdf.print.pdfx-metadata": { enabled: true, severity: "error" },
242
+ "pdf.print.embedded-fonts": { enabled: true, severity: "error" }
243
+ },
244
+ assets: {
245
+ allowedColorSpaces: [...DTRPG_PRESET.validate.assets.allowedColorSpaces]
246
+ },
247
+ pdf: {
248
+ forbidTransparency: DTRPG_PRESET.validate.pdf.forbidTransparency,
249
+ minImageResolution: DTRPG_PRESET.validate.pdf.minImageResolution
250
+ }
251
+ }
252
+ },
253
+ requiredChecks: [
254
+ "pdf.structure.qpdf",
255
+ "pdf.print.pdfx-markers",
256
+ "pdf.print.pdfx-metadata",
257
+ "pdf.print.embedded-fonts"
258
+ ],
259
+ requiredTools: ["qpdf", "gs"]
260
+ };
261
+ var ITCH_TARGET = {
262
+ id: "itch",
263
+ label: "itch.io",
264
+ overlay: {
265
+ validate: {
266
+ checks: {
267
+ "pdf.structure.qpdf": { enabled: true, severity: "error" },
268
+ "pdf.print.embedded-fonts": { enabled: true, severity: "error" },
269
+ "pdf.print.pdfx-markers": false,
270
+ "pdf.print.pdfx-metadata": false,
271
+ "pdf.print.ink-coverage": false,
272
+ "asset.image.tac-raster": false
273
+ },
274
+ assets: {
275
+ allowedColorSpaces: ["RGB", "CMYK", "Grayscale"],
276
+ allowAlpha: true
277
+ },
278
+ pdf: {
279
+ forbidTransparency: false,
280
+ requireBleed: false
281
+ }
282
+ }
283
+ },
284
+ requiredChecks: ["pdf.structure.qpdf", "pdf.print.embedded-fonts"],
285
+ requiredTools: []
286
+ };
287
+ var TARGETS = {
288
+ dtrpg: DTRPG_TARGET,
289
+ itch: ITCH_TARGET
290
+ };
291
+ var TARGET_IDS = Object.keys(TARGETS);
292
+ function publishTargetFor(id) {
293
+ const target = TARGETS[id];
294
+ if (!target) {
295
+ throw new UsageError(`Unknown publish target "${id}". Known targets: ${TARGET_IDS.join(", ")}.`);
296
+ }
297
+ return target;
298
+ }
299
+ function resolveTargets(requested, fallback) {
300
+ const ids = requested ?? fallback;
301
+ const seen = new Set;
302
+ const out = [];
303
+ for (const id of ids) {
304
+ publishTargetFor(id);
305
+ if (!seen.has(id)) {
306
+ seen.add(id);
307
+ out.push(id);
308
+ }
309
+ }
310
+ return out;
311
+ }
312
+ function overlayPreset(preset, target) {
313
+ const o = target.overlay;
314
+ return {
315
+ ...preset,
316
+ pdfx: { ...preset.pdfx, ...o.pdfx },
317
+ ink: { ...preset.ink, ...o.ink },
318
+ validate: {
319
+ ...preset.validate,
320
+ checks: { ...preset.validate.checks, ...o.validate?.checks },
321
+ assets: { ...preset.validate.assets, ...o.validate?.assets },
322
+ pdf: { ...preset.validate.pdf, ...o.validate?.pdf }
323
+ }
324
+ };
325
+ }
326
+ var DEFAULT_STRICT_PDF_CHECKS = [
327
+ "pdf.structure.qpdf",
328
+ "pdf.print.pdfx-markers",
329
+ "pdf.print.pdfx-metadata",
330
+ "pdf.print.embedded-fonts"
331
+ ];
332
+ function applyDefaultPdfStrictChecks(config) {
333
+ const next = structuredClone(config);
334
+ for (const checkId of DEFAULT_STRICT_PDF_CHECKS) {
335
+ if (next.validate.checks[checkId] === undefined) {
336
+ next.validate.checks[checkId] = { enabled: true, severity: "error" };
337
+ }
338
+ }
339
+ return next;
340
+ }
341
+
224
342
  // src/lib/manifest.ts
343
+ import { readFile } from "node:fs/promises";
344
+ import { existsSync, statSync } from "node:fs";
345
+ import { resolve, dirname } from "node:path";
346
+ import { parse as parseYaml, YAMLParseError } from "yaml";
225
347
  var MANIFEST_FILENAMES = [
226
348
  "manifest.yaml"
227
349
  ];
@@ -298,8 +420,34 @@ function mergeShape(cli, manifest, preset) {
298
420
  return out;
299
421
  }
300
422
  function resolveConfig(cliOverrides, manifest) {
423
+ const presetName = cliOverrides.preset ?? manifest.preset;
424
+ return resolveWithPreset(cliOverrides, manifest, resolvePreset(presetName));
425
+ }
426
+ function resolveConfigForTarget(cliOverrides, manifest, targetId) {
301
427
  const presetName = cliOverrides.preset ?? manifest.preset;
302
428
  const preset = resolvePreset(presetName);
429
+ return resolveWithPreset(cliOverrides, manifest, overlayPreset(preset, publishTargetFor(targetId)));
430
+ }
431
+ function resolveCustomPage(c, m) {
432
+ const width = c.page?.width ?? m.page?.width;
433
+ const height = c.page?.height ?? m.page?.height;
434
+ const missing = [
435
+ width === undefined ? "page.width" : null,
436
+ height === undefined ? "page.height" : null
437
+ ].filter((f) => f !== null);
438
+ if (missing.length > 0) {
439
+ throw new UsageError(`preset: custom requires ${missing.join(" and ")} in manifest.yaml ` + `(points; 72pt = 1in). Example for US Letter:
440
+ ` + ` page:
441
+ width: 612
442
+ height: 792`);
443
+ }
444
+ return {
445
+ width,
446
+ height,
447
+ tolerance: c.page?.tolerance ?? m.page?.tolerance ?? 0.5
448
+ };
449
+ }
450
+ function resolveWithPreset(cliOverrides, manifest, preset) {
303
451
  const m = manifest;
304
452
  const c = cliOverrides;
305
453
  const legacy = m;
@@ -321,9 +469,10 @@ function resolveConfig(cliOverrides, manifest) {
321
469
  authors: c.authors ?? m.authors ?? [],
322
470
  styles: c.styles ?? m.styles,
323
471
  plugins,
472
+ targets: resolveTargets(c.targets ?? m.targets, preset.defaultTargets),
324
473
  source: mergeShape(c.source, m.source, preset.source),
325
474
  pdfx: mergeShape(c.pdfx, m.pdfx, preset.pdfx),
326
- page: mergeShape(c.page, m.page, preset.page),
475
+ page: preset.page ? mergeShape(c.page, m.page, preset.page) : resolveCustomPage(c, m),
327
476
  ink: mergeShape(c.ink, m.ink, preset.ink),
328
477
  lint: mergeShape(c.lint, m.lint, preset.lint),
329
478
  validate: {
@@ -2575,6 +2724,22 @@ function decodeRef(ref) {
2575
2724
  function toPosix(p) {
2576
2725
  return p.split(path3.sep).join("/");
2577
2726
  }
2727
+ function escapesProjectRoot(projectDir, absPath) {
2728
+ const rel = path3.relative(projectDir, absPath);
2729
+ return rel === ".." || rel.startsWith(`..${path3.sep}`) || path3.isAbsolute(rel);
2730
+ }
2731
+ function proseImageRefError(ref, projectDir) {
2732
+ const cleaned = stripUrlSuffix(decodeRef(ref));
2733
+ if (path3.isAbsolute(cleaned)) {
2734
+ return `Image reference must be relative to the project: ${ref}
2735
+ ` + ` Copy the file into your project folder and reference it from there.`;
2736
+ }
2737
+ if (escapesProjectRoot(projectDir, path3.resolve(projectDir, cleaned))) {
2738
+ return `Image reference points outside the project: ${ref}
2739
+ ` + ` Copy the file into your project folder and reference it from there.`;
2740
+ }
2741
+ return null;
2742
+ }
2578
2743
  function mimeFor(ext) {
2579
2744
  return MIME_BY_EXT[ext] ?? "application/octet-stream";
2580
2745
  }
@@ -2677,7 +2842,7 @@ async function inlineOne(cssPath, projectDir, copies, warnings, seen) {
2677
2842
  return dataUri(bytes, ext);
2678
2843
  }
2679
2844
  const projectRel = path3.relative(projectDir, absAsset);
2680
- const dest = projectRel && !projectRel.startsWith("..") && !path3.isAbsolute(projectRel) ? toPosix(projectRel) : `${HASHED_ASSET_DIR}/${contentHash(bytes)}${ext}`;
2845
+ const dest = projectRel && !escapesProjectRoot(projectDir, absAsset) ? toPosix(projectRel) : `${HASHED_ASSET_DIR}/${contentHash(bytes)}${ext}`;
2681
2846
  copies.set(dest, { from: absAsset, to: dest });
2682
2847
  return dest;
2683
2848
  });
@@ -2768,19 +2933,14 @@ async function planImageCopies(projectDir, refs) {
2768
2933
  for (const ref of refs) {
2769
2934
  if (isNonFileUrl(ref))
2770
2935
  continue;
2771
- const cleaned = stripUrlSuffix(decodeRef(ref));
2772
- if (path3.isAbsolute(cleaned)) {
2773
- errors.push(`Image reference must be relative to the project: ${ref}
2774
- ` + ` Copy the file into your project folder and reference it from there.`);
2936
+ const refError = proseImageRefError(ref, projectDir);
2937
+ if (refError) {
2938
+ errors.push(refError);
2775
2939
  continue;
2776
2940
  }
2941
+ const cleaned = stripUrlSuffix(decodeRef(ref));
2777
2942
  const abs = path3.resolve(projectDir, cleaned);
2778
2943
  const rel = path3.relative(projectDir, abs);
2779
- if (rel.startsWith("..")) {
2780
- errors.push(`Image reference points outside the project: ${ref}
2781
- ` + ` Copy the file into your project folder and reference it from there.`);
2782
- continue;
2783
- }
2784
2944
  const dest = toPosix(rel);
2785
2945
  copies.set(dest, { from: abs, to: dest });
2786
2946
  }
@@ -2814,6 +2974,16 @@ var check19 = {
2814
2974
  if (inFence)
2815
2975
  continue;
2816
2976
  for (const { ref, kind } of extractLocalRefs(line)) {
2977
+ const escape = kind === "image" ? proseImageRefError(ref, ctx.inputDir) : null;
2978
+ if (escape) {
2979
+ results.push(finding(check19.id, {
2980
+ severity: "error",
2981
+ message: escape,
2982
+ file,
2983
+ line: i + 1
2984
+ }));
2985
+ continue;
2986
+ }
2817
2987
  if (localRefExists(ref, kind, file, ctx.inputDir))
2818
2988
  continue;
2819
2989
  results.push(finding(check19.id, {
@@ -3796,7 +3966,7 @@ function reportMissingTools(result) {
3796
3966
 
3797
3967
  // src/lib/validation-exec.ts
3798
3968
  import { existsSync as existsSync7 } from "node:fs";
3799
- import { join as join2, resolve as resolve6 } from "node:path";
3969
+ import { dirname as dirname3, join as join2, resolve as resolve6 } from "node:path";
3800
3970
 
3801
3971
  // src/lib/output-paths.ts
3802
3972
  import path4 from "node:path";
@@ -4396,17 +4566,6 @@ async function assembleBookHtml(opts) {
4396
4566
  throw new Error("No markdown files to render");
4397
4567
  }
4398
4568
  const md = createMarkdownRenderer(opts.plugins);
4399
- let sourceChapter = null;
4400
- if (opts.wrapChapters) {
4401
- const renderToken = md.renderer.renderToken.bind(md.renderer);
4402
- md.renderer.renderToken = (tokens, idx, options) => {
4403
- const token = tokens[idx];
4404
- if (sourceChapter !== null && token.level === 0 && token.map !== null && token.type.endsWith("_open")) {
4405
- token.attrSet("data-chapter-src", sourceChapter);
4406
- }
4407
- return renderToken(tokens, idx, options);
4408
- };
4409
- }
4410
4569
  let bodyContent = "";
4411
4570
  const imageRefs = new Set;
4412
4571
  for (const file of files) {
@@ -4419,20 +4578,22 @@ async function assembleBookHtml(opts) {
4419
4578
  throw new Error(`Failed to read file ${file}: ${errorMsg}`);
4420
4579
  }
4421
4580
  const env = {};
4422
- sourceChapter = opts.wrapChapters ? chapterId : null;
4423
- let rendered;
4424
- try {
4425
- rendered = md.render(content, env);
4426
- } finally {
4427
- sourceChapter = null;
4428
- }
4581
+ const rendered = md.render(content, env);
4429
4582
  if (env.layoutWarnings && env.layoutWarnings.length > 0) {
4430
4583
  opts.onChapterWarnings?.(chapterId, env.layoutWarnings);
4431
4584
  }
4432
4585
  for (const ref of env.imageRefs ?? [])
4433
4586
  imageRefs.add(ref);
4434
- bodyContent += rendered + `
4587
+ if (opts.wrapChapters) {
4588
+ const safe = chapterId.replace(/&/g, "&").replace(/"/g, """);
4589
+ bodyContent += `<div class="gutterpress-chapter" data-chapter-src="${safe}">
4590
+ ${rendered}
4591
+ </div>
4592
+ `;
4593
+ } else {
4594
+ bodyContent += rendered + `
4435
4595
  `;
4596
+ }
4436
4597
  }
4437
4598
  if (opts.onImageRefs) {
4438
4599
  for (const ref of collectHtmlImageRefs(bodyContent))
@@ -4523,19 +4684,19 @@ function formatReport(report, format = "text") {
4523
4684
  }
4524
4685
  function formatText(report) {
4525
4686
  for (const r of report.infos) {
4526
- const loc = formatLocation(r.file, r.line, r.column);
4687
+ const loc = targetPrefix(r.target) + formatLocation(r.file, r.line, r.column);
4527
4688
  log.info(`${loc}${r.message}`);
4528
4689
  if (r.detail)
4529
4690
  log.info(` ${r.detail}`);
4530
4691
  }
4531
4692
  for (const r of report.warnings) {
4532
- const loc = formatLocation(r.file, r.line, r.column);
4693
+ const loc = targetPrefix(r.target) + formatLocation(r.file, r.line, r.column);
4533
4694
  log.warn(`${loc}${r.message}`);
4534
4695
  if (r.detail)
4535
4696
  log.warn(` ${r.detail}`);
4536
4697
  }
4537
4698
  for (const r of report.errors) {
4538
- const loc = formatLocation(r.file, r.line, r.column);
4699
+ const loc = targetPrefix(r.target) + formatLocation(r.file, r.line, r.column);
4539
4700
  log.error(`${loc}${r.message}`);
4540
4701
  if (r.detail)
4541
4702
  log.error(` ${r.detail}`);
@@ -4549,6 +4710,9 @@ function formatText(report) {
4549
4710
  log.success("VALIDATION PASSED");
4550
4711
  }
4551
4712
  }
4713
+ function targetPrefix(target) {
4714
+ return target ? `[${target}] ` : "";
4715
+ }
4552
4716
  function formatJson(report) {
4553
4717
  console.log(JSON.stringify({
4554
4718
  results: report.results,
@@ -4650,58 +4814,46 @@ function emptyReport() {
4650
4814
  };
4651
4815
  }
4652
4816
 
4653
- // src/lib/validation-profile.ts
4654
- var DTRPG_STRICT_PDF_CHECKS = [
4655
- "pdf.structure.qpdf",
4656
- "pdf.print.pdfx-markers",
4657
- "pdf.print.pdfx-metadata",
4658
- "pdf.print.embedded-fonts"
4659
- ];
4660
- function enforceStrictPdfChecks(config, overwrite = false) {
4661
- for (const checkId of DTRPG_STRICT_PDF_CHECKS) {
4662
- if (overwrite || config.validate.checks[checkId] === undefined) {
4663
- config.validate.checks[checkId] = {
4664
- enabled: true,
4665
- severity: "error"
4666
- };
4667
- }
4817
+ // src/lib/validation-exec.ts
4818
+ var SHIPPED_ASSET_EXTS = new Set([
4819
+ ".woff2",
4820
+ ".woff",
4821
+ ".ttf",
4822
+ ".otf",
4823
+ ".png",
4824
+ ".jpg",
4825
+ ".jpeg",
4826
+ ".gif",
4827
+ ".webp",
4828
+ ".avif",
4829
+ ".svg",
4830
+ ".tif",
4831
+ ".tiff"
4832
+ ]);
4833
+ async function sharedAssetDirs(manifestDir, relStyles, inputDir) {
4834
+ let closure;
4835
+ try {
4836
+ closure = await collectStyleDependencies(manifestDir, relStyles);
4837
+ } catch {
4838
+ return [];
4668
4839
  }
4669
- }
4670
- function applyDefaultPdfStrictChecks(config) {
4671
- const next = structuredClone(config);
4672
- enforceStrictPdfChecks(next, false);
4673
- return next;
4674
- }
4675
- var applyDtrpgPdfDefaults = applyDefaultPdfStrictChecks;
4676
- function applyValidationProfile(config, profile) {
4677
- const next = structuredClone(config);
4678
- if (profile === "dtrpg") {
4679
- next.pdfx.flavor = DTRPG_PRESET.pdfx.flavor;
4680
- next.ink.maxTac = DTRPG_PRESET.ink.maxTac;
4681
- next.ink.tacTolerance = DTRPG_PRESET.ink.tacTolerance;
4682
- next.validate.pdf.forbidTransparency = DTRPG_PRESET.validate.pdf.forbidTransparency;
4683
- next.validate.pdf.minImageResolution = DTRPG_PRESET.validate.pdf.minImageResolution;
4684
- next.validate.assets.allowedColorSpaces = [
4685
- ...DTRPG_PRESET.validate.assets.allowedColorSpaces
4686
- ];
4687
- enforceStrictPdfChecks(next, true);
4840
+ const projectRoot = resolve6(inputDir);
4841
+ const dirs = new Set;
4842
+ for (const file of closure) {
4843
+ const ext = file.slice(file.lastIndexOf(".")).toLowerCase();
4844
+ if (!SHIPPED_ASSET_EXTS.has(ext))
4845
+ continue;
4846
+ const dir = dirname3(resolve6(file));
4847
+ if (escapesProjectRoot(projectRoot, dir))
4848
+ dirs.add(dir);
4688
4849
  }
4689
- return next;
4850
+ return [...dirs];
4690
4851
  }
4691
-
4692
- // src/lib/validation-exec.ts
4693
4852
  function parseCsv(value) {
4694
4853
  if (!value)
4695
4854
  return;
4696
4855
  return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4697
4856
  }
4698
- function parseProfile(raw) {
4699
- if (!raw)
4700
- return;
4701
- if (raw === "dtrpg")
4702
- return raw;
4703
- throw new Error(`Unsupported profile: ${raw}. Supported profiles: dtrpg`);
4704
- }
4705
4857
  function resolvePhaseArg(raw) {
4706
4858
  const normalized = raw.trim().toLowerCase();
4707
4859
  if (normalized === "pre" || normalized === "pre-build")
@@ -4724,20 +4876,16 @@ function resolveCategoryArg(raw) {
4724
4876
  }
4725
4877
  return parsed;
4726
4878
  }
4727
- function withProfileRequiredCheckErrors(report, tools, profile) {
4728
- if (!profile)
4729
- return report;
4730
- const required = profile === "dtrpg" ? DTRPG_STRICT_PDF_CHECKS : [];
4731
- if (required.length === 0)
4732
- return report;
4879
+ function withTargetRequiredCheckErrors(report, tools, target) {
4733
4880
  const skippedSet = new Set(tools.skippedChecks);
4734
- const missingRequired = required.filter((checkId) => skippedSet.has(checkId));
4881
+ const missingRequired = target.requiredChecks.filter((checkId) => skippedSet.has(checkId));
4735
4882
  if (missingRequired.length === 0)
4736
4883
  return report;
4737
4884
  const syntheticErrors = missingRequired.map((checkId) => ({
4738
4885
  checkId,
4886
+ target: target.id,
4739
4887
  severity: "error",
4740
- message: `Profile ${profile} requires check ${checkId}, but it was skipped because required tools are not available.`
4888
+ message: `Target ${target.id} requires check ${checkId}, but it was skipped because required tools are not available.`
4741
4889
  }));
4742
4890
  const results = [...report.results, ...syntheticErrors];
4743
4891
  const errors = [...report.errors, ...syntheticErrors];
@@ -4751,19 +4899,115 @@ function withProfileRequiredCheckErrors(report, tools, profile) {
4751
4899
  }
4752
4900
  };
4753
4901
  }
4902
+ var TARGET_INDEPENDENT_CATEGORIES = ["source", "heuristic"];
4903
+ var TARGET_DEPENDENT_CATEGORIES = ["asset", "pdf"];
4904
+ function tagReport(report, targetId) {
4905
+ const tag = (r) => ({ ...r, target: targetId });
4906
+ return {
4907
+ results: report.results.map(tag),
4908
+ errors: report.errors.map(tag),
4909
+ warnings: report.warnings.map(tag),
4910
+ infos: report.infos.map(tag),
4911
+ passed: report.passed,
4912
+ summary: report.summary
4913
+ };
4914
+ }
4915
+ function mergeReports(reports) {
4916
+ const merged = {
4917
+ results: [],
4918
+ errors: [],
4919
+ warnings: [],
4920
+ infos: [],
4921
+ passed: [],
4922
+ summary: { total: 0, errors: 0, warnings: 0, infos: 0, passed: 0 }
4923
+ };
4924
+ for (const r of reports) {
4925
+ merged.results.push(...r.results);
4926
+ merged.errors.push(...r.errors);
4927
+ merged.warnings.push(...r.warnings);
4928
+ merged.infos.push(...r.infos);
4929
+ merged.passed.push(...r.passed);
4930
+ merged.summary.total += r.summary.total;
4931
+ merged.summary.errors += r.summary.errors;
4932
+ merged.summary.warnings += r.summary.warnings;
4933
+ merged.summary.infos += r.summary.infos;
4934
+ merged.summary.passed += r.summary.passed;
4935
+ }
4936
+ return merged;
4937
+ }
4938
+ function mergeTools(results) {
4939
+ const available = new Set;
4940
+ const missing = new Set;
4941
+ const skippedChecks = new Set;
4942
+ const toolToChecks = new Map;
4943
+ for (const r of results) {
4944
+ for (const t of r.available)
4945
+ available.add(t);
4946
+ for (const t of r.missing)
4947
+ missing.add(t);
4948
+ for (const id of r.skippedChecks)
4949
+ skippedChecks.add(id);
4950
+ for (const [tool, ids] of r.toolToChecks) {
4951
+ const merged = new Set([...toolToChecks.get(tool) ?? [], ...ids]);
4952
+ toolToChecks.set(tool, [...merged]);
4953
+ }
4954
+ }
4955
+ return {
4956
+ available: [...available],
4957
+ missing: [...missing],
4958
+ skippedChecks: [...skippedChecks],
4959
+ toolToChecks
4960
+ };
4961
+ }
4962
+ function planRuns(opts) {
4963
+ const { manifest, baseConfig, targetIds, pdfPath, categories, only } = opts;
4964
+ const finalize = (config) => pdfPath ? applyDefaultPdfStrictChecks(config) : config;
4965
+ if (targetIds.length === 0) {
4966
+ return [{ targetId: null, config: finalize(baseConfig), category: categories, only }];
4967
+ }
4968
+ const requested = categories ?? [...TARGET_INDEPENDENT_CATEGORIES, ...TARGET_DEPENDENT_CATEGORIES];
4969
+ const independentCats = requested.filter((c) => TARGET_INDEPENDENT_CATEGORIES.includes(c));
4970
+ const dependentCats = requested.filter((c) => TARGET_DEPENDENT_CATEGORIES.includes(c));
4971
+ let baseOnly;
4972
+ let dependentOnly;
4973
+ if (only && only.length > 0) {
4974
+ const { resolved, unmatched } = resolveCheckSelectors(only);
4975
+ const categoryById = new Map(getChecks({}).map((c) => [c.id, c.category]));
4976
+ const independentIds = resolved.filter((id) => TARGET_INDEPENDENT_CATEGORIES.includes(categoryById.get(id) ?? ""));
4977
+ const dependentIds = resolved.filter((id) => TARGET_DEPENDENT_CATEGORIES.includes(categoryById.get(id) ?? ""));
4978
+ baseOnly = [...independentIds, ...unmatched];
4979
+ dependentOnly = dependentIds;
4980
+ }
4981
+ const runs = [];
4982
+ const baseWanted = only ? (baseOnly?.length ?? 0) > 0 : independentCats.length > 0;
4983
+ if (baseWanted) {
4984
+ runs.push({
4985
+ targetId: null,
4986
+ config: baseConfig,
4987
+ category: only ? undefined : independentCats,
4988
+ only: baseOnly
4989
+ });
4990
+ }
4991
+ const dependentWanted = only ? (dependentOnly?.length ?? 0) > 0 : dependentCats.length > 0;
4992
+ if (dependentWanted) {
4993
+ for (const targetId of targetIds) {
4994
+ runs.push({
4995
+ targetId,
4996
+ config: finalize(resolveConfigForTarget({}, manifest, targetId)),
4997
+ category: only ? undefined : dependentCats,
4998
+ only: dependentOnly
4999
+ });
5000
+ }
5001
+ }
5002
+ return runs;
5003
+ }
4754
5004
  async function executeValidation(args) {
4755
5005
  const manifestPath = typeof args.manifest === "string" ? args.manifest : undefined;
4756
5006
  const { manifest, manifestDir } = await loadManifestWithPath(manifestPath ?? args.input ?? undefined, { explicit: manifestPath !== undefined });
4757
- const profile = parseProfile(typeof args.profile === "string" ? args.profile : undefined);
4758
- let config = resolveConfig({}, manifest);
5007
+ const config = resolveConfig({}, manifest);
5008
+ const targetIds = resolveTargets(parseCsv(typeof args.target === "string" ? args.target : undefined), config.targets);
4759
5009
  const pdfPath = typeof args.pdf === "string" ? resolve6(args.pdf) : undefined;
4760
5010
  const inputDir = typeof args.input === "string" ? resolve6(args.input) : undefined;
4761
- if (pdfPath) {
4762
- config = applyDtrpgPdfDefaults(config);
4763
- }
4764
- if (profile) {
4765
- config = applyValidationProfile(config, profile);
4766
- }
4767
5011
  if (pdfPath && !existsSync7(pdfPath)) {
4768
5012
  throw new Error(`File not found: ${pdfPath}`);
4769
5013
  }
@@ -4792,11 +5036,11 @@ async function executeValidation(args) {
4792
5036
  let htmlPath;
4793
5037
  const outDir = resolveOutputDir(manifestDir, config.title);
4794
5038
  if (inputDir) {
4795
- const relMarkdown = await resolveActiveMarkdownFiles(inputDir, config.source.files);
4796
- markdownFiles = relMarkdown.map((f) => join2(inputDir, canonicalChapterId(f)));
4797
- const relStyles = await resolveActiveStyles(inputDir, config.styles);
4798
- cssFiles = relStyles.map((rel) => resolve6(inputDir, rel)).filter((f) => !f.endsWith(".min.css"));
4799
- assetDirs = [inputDir];
5039
+ const relMarkdown = await resolveActiveMarkdownFiles(manifestDir, config.source.files);
5040
+ markdownFiles = relMarkdown.map((f) => join2(manifestDir, canonicalChapterId(f)));
5041
+ const relStyles = await resolveActiveStyles(manifestDir, config.styles);
5042
+ cssFiles = relStyles.map((rel) => resolve6(manifestDir, rel)).filter((f) => !f.endsWith(".min.css"));
5043
+ assetDirs = [inputDir, ...await sharedAssetDirs(manifestDir, relStyles, inputDir)];
4800
5044
  const possibleHtml = join2(outDir, BOOK_HTML_FILENAME);
4801
5045
  if (existsSync7(possibleHtml)) {
4802
5046
  htmlPath = possibleHtml;
@@ -4818,15 +5062,30 @@ async function executeValidation(args) {
4818
5062
  only,
4819
5063
  skip
4820
5064
  };
4821
- const tools = await checkToolAvailability(config, runnerOptions);
4822
- const initialReport = await runChecks(context, {
4823
- ...runnerOptions,
4824
- skipMissingTools: tools.skippedChecks
4825
- });
4826
- const report = withProfileRequiredCheckErrors(initialReport, tools, profile);
5065
+ const runs = planRuns({ manifest, baseConfig: config, targetIds, pdfPath, categories, only });
5066
+ const reports = [];
5067
+ const toolResults = [];
5068
+ for (const run of runs) {
5069
+ const runOptions = {
5070
+ category: run.category,
5071
+ phase,
5072
+ only: run.only,
5073
+ skip
5074
+ };
5075
+ const runTools = await checkToolAvailability(run.config, runOptions);
5076
+ toolResults.push(runTools);
5077
+ let runReport = await runChecks({ ...context, config: run.config }, { ...runOptions, skipMissingTools: runTools.skippedChecks });
5078
+ if (run.targetId !== null) {
5079
+ runReport = withTargetRequiredCheckErrors(runReport, runTools, publishTargetFor(run.targetId));
5080
+ runReport = tagReport(runReport, run.targetId);
5081
+ }
5082
+ reports.push(runReport);
5083
+ }
5084
+ const tools = mergeTools(toolResults);
5085
+ const report = mergeReports(reports);
4827
5086
  return {
4828
5087
  config,
4829
- profile,
5088
+ targets: targetIds,
4830
5089
  context,
4831
5090
  runnerOptions,
4832
5091
  tools,
@@ -4887,7 +5146,7 @@ import { createHash as createHash3 } from "node:crypto";
4887
5146
  import { existsSync as existsSync8, lstatSync, realpathSync, rmSync, statSync as statSync2, unlinkSync } from "node:fs";
4888
5147
  import { cp, link, mkdir as mkdir2, mkdtemp, readFile as readFile16, rm, unlink, writeFile as writeFile2 } from "node:fs/promises";
4889
5148
  import { tmpdir } from "node:os";
4890
- import { resolve as resolve7, join as join3, dirname as dirname3, basename, extname, relative, isAbsolute } from "node:path";
5149
+ import { resolve as resolve7, join as join3, dirname as dirname4, basename, extname, relative, isAbsolute } from "node:path";
4891
5150
  import { fileURLToPath, pathToFileURL } from "node:url";
4892
5151
  import Module, { builtinModules, createRequire } from "node:module";
4893
5152
  import { parse as parseJavaScript } from "acorn";
@@ -5256,7 +5515,7 @@ async function createIsolatedVendorTree(verified, address) {
5256
5515
  for (let index = 0;index < sorted.length; index++) {
5257
5516
  const source = sorted[index];
5258
5517
  const destination = join3(loadRoot, "packages", String(index), "package");
5259
- await mkdir2(dirname3(destination), { recursive: true });
5518
+ await mkdir2(dirname4(destination), { recursive: true });
5260
5519
  await cp(source.packageDir, destination, {
5261
5520
  recursive: true,
5262
5521
  filter: (candidate) => isolatedPackageCopyFilter(source.packageDir, candidate)
@@ -5315,7 +5574,7 @@ async function snapshotAndIsolateVendoredPlugin(baseDir, packageName, version) {
5315
5574
  const snapshotProject = join3(container, "project");
5316
5575
  const snapshotRoot = vendoredNpmPluginRoot(snapshotProject, packageName, version);
5317
5576
  try {
5318
- await mkdir2(dirname3(snapshotRoot), { recursive: true });
5577
+ await mkdir2(dirname4(snapshotRoot), { recursive: true });
5319
5578
  await cp(sourceRoot, snapshotRoot, {
5320
5579
  recursive: true,
5321
5580
  dereference: false,
@@ -5453,7 +5712,7 @@ function shadowPathFor(pluginPath, mtimeMs) {
5453
5712
  const ext = extname(pluginPath);
5454
5713
  const stem = basename(pluginPath, ext);
5455
5714
  const token = String(mtimeMs).replace(/\./g, "-");
5456
- return join3(dirname3(pluginPath), `.${stem}.gutterpress-reload-${token}${ext}`);
5715
+ return join3(dirname4(pluginPath), `.${stem}.gutterpress-reload-${process.pid}-${token}${ext}`);
5457
5716
  }
5458
5717
  async function loadCachedPathPluginModule(pluginPath) {
5459
5718
  const mtimeMs = statSync2(pluginPath).mtimeMs;
@@ -5731,7 +5990,7 @@ async function writeBuildFingerprint(input) {
5731
5990
  iccPath: input.pdfx.iccPath,
5732
5991
  stripAnnotations: input.pdfx.stripAnnotations
5733
5992
  },
5734
- outputDir
5993
+ outputDir: input.recordedOutputDir ? path5.resolve(input.recordedOutputDir) : outputDir
5735
5994
  },
5736
5995
  sourceRevision,
5737
5996
  tools
@@ -5745,23 +6004,23 @@ async function writeBuildFingerprint(input) {
5745
6004
  import { existsSync as existsSync9 } from "node:fs";
5746
6005
  import { mkdtemp as mkdtemp2, mkdir as mkdir4, writeFile as writeFile4, readFile as readFile17 } from "node:fs/promises";
5747
6006
  import { tmpdir as tmpdir2 } from "node:os";
5748
- import { dirname as dirname4, join as join4, resolve as resolve8 } from "node:path";
6007
+ import { dirname as dirname5, join as join4, resolve as resolve8 } from "node:path";
5749
6008
  import { fileURLToPath as fileURLToPath2 } from "node:url";
5750
6009
 
5751
6010
  // src/assets/favicon.ico
5752
6011
  var favicon_default = "./favicon-wkbm9cjn.ico";
5753
6012
 
5754
6013
  // src/assets/manifest.schema.json
5755
- var manifest_schema_default = "./manifest.schema-rn1dpdpn.json";
6014
+ var manifest_schema_default = "./manifest.schema-z61rzw44.json";
5756
6015
 
5757
6016
  // src/assets/preview/scripts/pagedjs-interface.js
5758
- var pagedjs_interface_default = "./pagedjs-interface-80g3xa8p.js";
6017
+ var pagedjs_interface_default = "./pagedjs-interface-qxvzgwd7.js";
5759
6018
 
5760
6019
  // src/assets/preview/scripts/pagedjs-bridge.js
5761
6020
  var pagedjs_bridge_default = "./pagedjs-bridge-vn4hk9fx.js";
5762
6021
 
5763
6022
  // src/assets/preview/scripts/preview-shell.js
5764
- var preview_shell_default = "./preview-shell-rghtfmc8.js";
6023
+ var preview_shell_default = "./preview-shell-6dqexx1m.js";
5765
6024
 
5766
6025
  // src/assets/vendor/paged.polyfill.js
5767
6026
  var paged_polyfill_default = "./paged.polyfill-n95pbxfn.js";
@@ -5775,23 +6034,17 @@ var manifest_default = "./manifest-n1gh3qw5.yaml";
5775
6034
  // src/assets/templates/book/chapter-01.md
5776
6035
  var chapter_01_default = "./chapter-01-rkpyvjna.md";
5777
6036
 
5778
- // src/assets/templates/ttrpg/manifest.yaml
5779
- var manifest_default2 = "./manifest-ryyg71nf.yaml";
5780
-
5781
- // src/assets/templates/ttrpg/chapter-01.md
5782
- var chapter_01_default2 = "./chapter-01-jj8ftn4c.md";
5783
-
5784
6037
  // src/assets/templates/zine/manifest.yaml
5785
- var manifest_default3 = "./manifest-n1gh3qw5.yaml";
6038
+ var manifest_default2 = "./manifest-n1gh3qw5.yaml";
5786
6039
 
5787
6040
  // src/assets/templates/zine/chapter-01.md
5788
- var chapter_01_default3 = "./chapter-01-7jwqg1ct.md";
6041
+ var chapter_01_default2 = "./chapter-01-7jwqg1ct.md";
5789
6042
 
5790
6043
  // src/assets/templates/technical/manifest.yaml
5791
- var manifest_default4 = "./manifest-n1gh3qw5.yaml";
6044
+ var manifest_default3 = "./manifest-n1gh3qw5.yaml";
5792
6045
 
5793
6046
  // src/assets/templates/technical/chapter-01.md
5794
- var chapter_01_default4 = "./chapter-01-6fn9pvd0.md";
6047
+ var chapter_01_default3 = "./chapter-01-6fn9pvd0.md";
5795
6048
 
5796
6049
  // src/assets/themes/clean-book/theme.css
5797
6050
  var theme_default = "./theme-570zmh2t.css";
@@ -5799,26 +6052,20 @@ var theme_default = "./theme-570zmh2t.css";
5799
6052
  // src/assets/themes/clean-book/theme.json
5800
6053
  var theme_default2 = "./theme-xt0hjb1b.json";
5801
6054
 
5802
- // src/assets/themes/ttrpg-supplement/theme.css
5803
- var theme_default3 = "./theme-0yzvthe8.css";
5804
-
5805
- // src/assets/themes/ttrpg-supplement/theme.json
5806
- var theme_default4 = "./theme-6a2fngc8.json";
5807
-
5808
6055
  // src/assets/themes/zine/theme.css
5809
- var theme_default5 = "./theme-zz2ktzqs.css";
6056
+ var theme_default3 = "./theme-zz2ktzqs.css";
5810
6057
 
5811
6058
  // src/assets/themes/zine/theme.json
5812
- var theme_default6 = "./theme-cd6efzjw.json";
6059
+ var theme_default4 = "./theme-cd6efzjw.json";
5813
6060
 
5814
6061
  // src/assets/themes/technical-doc/theme.css
5815
- var theme_default7 = "./theme-nya4nqh6.css";
6062
+ var theme_default5 = "./theme-nya4nqh6.css";
5816
6063
 
5817
6064
  // src/assets/themes/technical-doc/theme.json
5818
- var theme_default8 = "./theme-tqfypc1w.json";
6065
+ var theme_default6 = "./theme-tqfypc1w.json";
5819
6066
 
5820
6067
  // src/lib/embedded-assets.ts
5821
- var __libdir = dirname4(fileURLToPath2(import.meta.url));
6068
+ var __libdir = dirname5(fileURLToPath2(import.meta.url));
5822
6069
  var abs = (p) => resolve8(__libdir, p);
5823
6070
  var filePath = (v) => v;
5824
6071
  var EMBEDDED_ASSETS = {
@@ -5831,27 +6078,23 @@ var EMBEDDED_ASSETS = {
5831
6078
  "profiles/CGATS21_CRPC1.icc": abs(filePath(CGATS21_CRPC1_default)),
5832
6079
  "templates/book/manifest.yaml": abs(filePath(manifest_default)),
5833
6080
  "templates/book/chapter-01.md": abs(filePath(chapter_01_default)),
5834
- "templates/ttrpg/manifest.yaml": abs(filePath(manifest_default2)),
5835
- "templates/ttrpg/chapter-01.md": abs(filePath(chapter_01_default2)),
5836
- "templates/zine/manifest.yaml": abs(filePath(manifest_default3)),
5837
- "templates/zine/chapter-01.md": abs(filePath(chapter_01_default3)),
5838
- "templates/technical/manifest.yaml": abs(filePath(manifest_default4)),
5839
- "templates/technical/chapter-01.md": abs(filePath(chapter_01_default4)),
6081
+ "templates/zine/manifest.yaml": abs(filePath(manifest_default2)),
6082
+ "templates/zine/chapter-01.md": abs(filePath(chapter_01_default2)),
6083
+ "templates/technical/manifest.yaml": abs(filePath(manifest_default3)),
6084
+ "templates/technical/chapter-01.md": abs(filePath(chapter_01_default3)),
5840
6085
  "themes/clean-book/theme.css": abs(filePath(theme_default)),
5841
6086
  "themes/clean-book/theme.json": abs(filePath(theme_default2)),
5842
- "themes/ttrpg-supplement/theme.css": abs(filePath(theme_default3)),
5843
- "themes/ttrpg-supplement/theme.json": abs(filePath(theme_default4)),
5844
- "themes/zine/theme.css": abs(filePath(theme_default5)),
5845
- "themes/zine/theme.json": abs(filePath(theme_default6)),
5846
- "themes/technical-doc/theme.css": abs(filePath(theme_default7)),
5847
- "themes/technical-doc/theme.json": abs(filePath(theme_default8))
6087
+ "themes/zine/theme.css": abs(filePath(theme_default3)),
6088
+ "themes/zine/theme.json": abs(filePath(theme_default4)),
6089
+ "themes/technical-doc/theme.css": abs(filePath(theme_default5)),
6090
+ "themes/technical-doc/theme.json": abs(filePath(theme_default6))
5848
6091
  };
5849
6092
  var extractPromise = null;
5850
6093
  async function extractAssets() {
5851
6094
  const root = await mkdtemp2(join4(tmpdir2(), "gutterpress-assets-"));
5852
6095
  for (const [relPath, srcPath] of Object.entries(EMBEDDED_ASSETS)) {
5853
6096
  const dest = join4(root, relPath);
5854
- await mkdir4(dirname4(dest), { recursive: true });
6097
+ await mkdir4(dirname5(dest), { recursive: true });
5855
6098
  const bytes = await readFile17(srcPath);
5856
6099
  await writeFile4(dest, bytes);
5857
6100
  }
@@ -5905,7 +6148,7 @@ ${m.installHint}`).join(`
5905
6148
 
5906
6149
  ${list}
5907
6150
 
5908
- Install the missing tools and re-run, or set GHOSTSCRIPT_PATH, CHROMIUM_PATH, or system PATH so gutterpress can find them. See the User Guide Chapter 8 (System Setup) at examples/gutterpress-user-guide/08-system-setup.md for the full per-feature matrix.`, 2);
6151
+ Install the missing tools and re-run, or set GHOSTSCRIPT_PATH, CHROMIUM_PATH, or system PATH so gutterpress can find them. See the User Guide Chapter 7 (System Setup) at examples/gutterpress-user-guide/07-system-setup.md for the full per-feature matrix.`, 2);
5909
6152
  }
5910
6153
  function computeGates(format, opts, config) {
5911
6154
  if (format === "html") {
@@ -5999,6 +6242,15 @@ function resolveWithinRoot(relPath, root) {
5999
6242
  }
6000
6243
  return candidate;
6001
6244
  }
6245
+ function hasDotSegment(urlPathname) {
6246
+ let decoded;
6247
+ try {
6248
+ decoded = decodeURIComponent(urlPathname);
6249
+ } catch {
6250
+ return true;
6251
+ }
6252
+ return decoded.split(/[/\\]/).some((segment) => segment.startsWith("."));
6253
+ }
6002
6254
  function resolveStaticPath(urlPathname, root) {
6003
6255
  let decoded;
6004
6256
  try {
@@ -6397,14 +6649,25 @@ async function resolveBuildContext(opts) {
6397
6649
  const outDir = target.kind === "file" ? path9.dirname(target.file) : target.dir;
6398
6650
  const gates = computeGates(format, opts, config);
6399
6651
  const workDir = target.kind === "project" ? path9.join(path9.dirname(target.dir), `.${path9.basename(target.dir)}-build-${randomBytes(6).toString("hex")}`) : path9.join(os2.tmpdir(), `gutterpress-build-${randomBytes(6).toString("hex")}`);
6400
- return { opts, format, inputDir, outDir, workDir, target, manifestDir, config, gates };
6652
+ return {
6653
+ opts,
6654
+ format,
6655
+ inputDir,
6656
+ renderDir: manifestDir,
6657
+ outDir,
6658
+ workDir,
6659
+ target,
6660
+ manifestDir,
6661
+ config,
6662
+ gates
6663
+ };
6401
6664
  }
6402
6665
  async function runQualityGates(ctx) {
6403
- const { gates, opts, inputDir } = ctx;
6666
+ const { gates, opts, renderDir } = ctx;
6404
6667
  if (gates.lint) {
6405
6668
  log.info("Lint: CSS print-safety");
6406
6669
  const lintResult = await runLint({
6407
- manifest: opts.manifestPath ?? inputDir
6670
+ manifest: opts.manifestPath ?? renderDir
6408
6671
  });
6409
6672
  if (!lintResult.ok) {
6410
6673
  throw new BuildError("CSS lint failed", 2);
@@ -6413,7 +6676,7 @@ async function runQualityGates(ctx) {
6413
6676
  if (gates.preValidate) {
6414
6677
  log.info("Pre-build validation");
6415
6678
  const result = await executeAndReport({
6416
- input: inputDir,
6679
+ input: renderDir,
6417
6680
  phase: "pre-build",
6418
6681
  manifest: opts.manifestPath
6419
6682
  }, "text");
@@ -6423,7 +6686,7 @@ async function runQualityGates(ctx) {
6423
6686
  }
6424
6687
  }
6425
6688
  async function renderBook(ctx) {
6426
- const { config, manifestDir, inputDir, workDir } = ctx;
6689
+ const { config, renderDir, workDir } = ctx;
6427
6690
  if (config.source.files && config.source.files.length > 0) {
6428
6691
  log.info(`Using specified files (${config.source.files.length} total)`);
6429
6692
  } else {
@@ -6432,13 +6695,13 @@ async function renderBook(ctx) {
6432
6695
  if (config.plugins.length > 0) {
6433
6696
  log.info(`Loading ${config.plugins.length} plugin(s)...`);
6434
6697
  }
6435
- const { plugins, pluginCss } = await loadPluginsWithCss(config.plugins, manifestDir);
6698
+ const { plugins, pluginCss } = await loadPluginsWithCss(config.plugins, renderDir);
6436
6699
  if (plugins && plugins.length > 0) {
6437
6700
  log.success(`Loaded ${plugins.length} plugin(s)`);
6438
6701
  }
6439
6702
  const imageRefs = [];
6440
6703
  const cssAssets = [];
6441
- const htmlFile = await renderChaptersToFile(inputDir, workDir, {
6704
+ const htmlFile = await renderChaptersToFile(renderDir, workDir, {
6442
6705
  title: config.title,
6443
6706
  styles: config.styles,
6444
6707
  files: config.source.files,
@@ -6457,7 +6720,7 @@ async function renderBook(ctx) {
6457
6720
  }
6458
6721
  });
6459
6722
  log.success(`Wrote ${htmlFile}`);
6460
- const { copies: imageCopies, errors } = await planImageCopies(inputDir, imageRefs);
6723
+ const { copies: imageCopies, errors } = await planImageCopies(renderDir, imageRefs);
6461
6724
  if (errors.length > 0) {
6462
6725
  throw new BuildError(`Cannot resolve ${errors.length} image reference(s):
6463
6726
  ` + errors.map((e) => ` - ${e}`).join(`
@@ -6496,7 +6759,8 @@ async function resolveIccProfile(icc, manifestDir, explicitIccPath) {
6496
6759
  async function finalizeBuild(ctx, fingerprint, wroteMessage, paths, artifactName2 = null) {
6497
6760
  const workFingerprint = await writeBuildFingerprint({
6498
6761
  ...fingerprint,
6499
- outputDir: ctx.workDir
6762
+ outputDir: ctx.workDir,
6763
+ recordedOutputDir: fingerprint.outputDir
6500
6764
  });
6501
6765
  await publishBuild(ctx, artifactName2);
6502
6766
  const delivered = ctx.target.kind !== "file";
@@ -6587,7 +6851,9 @@ class PdfOutput {
6587
6851
  await renderHtmlToPdf(htmlFile, rawPdf, opts.pdfRenderer, staticHtmlRaw);
6588
6852
  if (staticHtmlRaw && fs2.existsSync(staticHtmlRaw)) {
6589
6853
  await finalizeStaticBook(await fsp3.readFile(staticHtmlRaw, "utf-8"), htmlFile, workDir);
6590
- log.success(`Wrote static desktop: ${path9.join(outDir, BOOK_HTML)}`);
6854
+ if (ctx.target.kind !== "file") {
6855
+ log.success(`Wrote static desktop: ${path9.join(outDir, BOOK_HTML)}`);
6856
+ }
6591
6857
  }
6592
6858
  if (!pdfxMode) {
6593
6859
  try {
@@ -6749,6 +7015,7 @@ async function renderPreviewBook(inputPath, config, opts) {
6749
7015
  plugins,
6750
7016
  pluginCss,
6751
7017
  wrapChapters: opts.wrapChapters,
7018
+ ...opts.onCssAssets ? { onCssAssets: opts.onCssAssets } : {},
6752
7019
  onChapterWarnings: (file, warnings) => {
6753
7020
  for (const w of warnings) {
6754
7021
  warn(` ${file}, line ${w.line}: ${w.message}`);
@@ -6756,23 +7023,70 @@ async function renderPreviewBook(inputPath, config, opts) {
6756
7023
  }
6757
7024
  });
6758
7025
  }
6759
- function injectPreviewScripts(html) {
7026
+ function injectPreviewScripts(html, pageIsolateChapters) {
6760
7027
  const iface = `<script src="/preview/scripts/pagedjs-interface.js"></script>
6761
7028
  ` + `<script src="/preview/scripts/pagedjs-bridge.js"></script>
6762
7029
  `;
6763
- return html.replace(pagedjsPolyfillTagRegex(), iface + BREAK_INSIDE_HANDLER + `
7030
+ let output = html.replace(pagedjsPolyfillTagRegex(), iface + BREAK_INSIDE_HANDLER + `
6764
7031
  <script src="/vendor/paged.polyfill.js"></script>`);
7032
+ if (pageIsolateChapters && /<\/head>/i.test(output)) {
7033
+ output = output.replace(/<\/head>/i, `<style>.gutterpress-chapter{break-before:page}</style>
7034
+ </head>`);
7035
+ }
7036
+ return output;
6765
7037
  }
6766
- async function generateAndWriteHtml(inputPath, tempDir, config) {
7038
+ async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets) {
6767
7039
  if (!inputPath) {
7040
+ cssAssets.clear();
6768
7041
  await fsp4.writeFile(path10.join(tempDir, BOOK_HTML_FILENAME), EMPTY_BOOK_HTML, "utf-8");
6769
7042
  return;
6770
7043
  }
7044
+ const nextAssets = new Map;
7045
+ const incremental = incrementalPreviewEnabled();
6771
7046
  const html = await renderPreviewBook(inputPath, config, {
6772
7047
  files: config.source?.files ?? null,
7048
+ wrapChapters: incremental,
7049
+ onCssAssets: (copies) => {
7050
+ for (const copy of copies)
7051
+ nextAssets.set(copy.to, copy.from);
7052
+ }
7053
+ });
7054
+ cssAssets.clear();
7055
+ for (const [to, from] of nextAssets)
7056
+ cssAssets.set(to, from);
7057
+ await fsp4.writeFile(path10.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html, incremental), "utf-8");
7058
+ }
7059
+ async function renderChapterPreviewHtml(inputPath, file, config) {
7060
+ const html = await renderPreviewBook(inputPath, config, {
7061
+ files: [canonicalChapterId(file)],
6773
7062
  wrapChapters: true
6774
7063
  });
6775
- await fsp4.writeFile(path10.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html), "utf-8");
7064
+ return injectPreviewScripts(html, true);
7065
+ }
7066
+ function describeChanges(changes, inputResolved) {
7067
+ const files = [];
7068
+ for (const [changedPath, event] of changes) {
7069
+ const relative2 = path10.relative(inputResolved, path10.resolve(changedPath));
7070
+ if (relative2 === "" || relative2 === ".." || relative2.startsWith(`..${path10.sep}`) || path10.isAbsolute(relative2))
7071
+ continue;
7072
+ files.push({
7073
+ relativePath: canonicalChapterId(relative2),
7074
+ ext: path10.extname(changedPath).toLowerCase(),
7075
+ event
7076
+ });
7077
+ }
7078
+ return files;
7079
+ }
7080
+ function decideBroadcast(files, changeCount, incremental) {
7081
+ const only = files.length === 1 ? files[0] : null;
7082
+ if (incremental && changeCount === 1 && only?.ext === ".md" && only.event !== "unlink" && only.event !== "unlinkDir") {
7083
+ return {
7084
+ kind: "chapter-splice",
7085
+ chapterId: canonicalChapterId(only.relativePath),
7086
+ relativePath: only.relativePath
7087
+ };
7088
+ }
7089
+ return { kind: "full-reload" };
6776
7090
  }
6777
7091
  function isDotPathUnderRoot(candidatePath, root) {
6778
7092
  const normalizedRoot = root.replace(/\\/g, "/").replace(/\/+$/, "");
@@ -6789,11 +7103,24 @@ function isDotPathUnderRoot(candidatePath, root) {
6789
7103
  return false;
6790
7104
  return rel.split("/").some((segment) => segment.startsWith("."));
6791
7105
  }
7106
+ function isGeneratedProjectPath(relative2) {
7107
+ const segments = relative2.split("/");
7108
+ for (let i = 0;i < segments.length; i++) {
7109
+ const segment = segments[i];
7110
+ if (segment === "dist" || segment === "node_modules")
7111
+ return true;
7112
+ if (segment === "plugins" && segments[i + 1] === "npm")
7113
+ return true;
7114
+ }
7115
+ return false;
7116
+ }
6792
7117
  function isIgnoredWatchPath(candidatePath, projectRoot) {
6793
7118
  const normalizedRoot = projectRoot.replace(/\\/g, "/").replace(/\/+$/, "");
6794
7119
  const normalizedPath = candidatePath.replace(/\\/g, "/");
6795
- const inProject = normalizedPath === normalizedRoot || normalizedPath.startsWith(normalizedRoot + "/");
6796
- return inProject ? isDotPathUnderRoot(candidatePath, projectRoot) : false;
7120
+ if (!normalizedPath.startsWith(normalizedRoot + "/"))
7121
+ return false;
7122
+ const relative2 = normalizedPath.slice(normalizedRoot.length + 1);
7123
+ return isDotPathUnderRoot(candidatePath, projectRoot) || isGeneratedProjectPath(relative2);
6797
7124
  }
6798
7125
  function isExternalWatchCandidate(candidatePath, targets) {
6799
7126
  const candidate = path10.resolve(candidatePath);
@@ -6878,21 +7205,43 @@ function createFileWatcher(state) {
6878
7205
  let closed = false;
6879
7206
  const pendingChanges = new Map;
6880
7207
  const suppressInitialExternalAdds = new Set;
7208
+ const settledWrites = new Map;
7209
+ let immediatePending = false;
6881
7210
  function scheduleRebuild() {
6882
7211
  if (closed)
6883
7212
  return;
6884
7213
  if (state.rebuildTimer)
6885
7214
  clearTimeout(state.rebuildTimer);
6886
- state.rebuildTimer = setTimeout(runRebuild, DEBOUNCE.FILE_WATCH);
6887
- }
6888
- function recordChange(event, filePath2) {
7215
+ const timer = setTimeout(() => {
7216
+ if (state.rebuildTimer === timer)
7217
+ state.rebuildTimer = null;
7218
+ runRebuild();
7219
+ }, DEBOUNCE.FILE_WATCH);
7220
+ state.rebuildTimer = timer;
7221
+ }
7222
+ async function recordChange(event, filePath2) {
6889
7223
  if (closed)
6890
7224
  return;
6891
- debug(`File ${event}: ${filePath2}`);
6892
- pendingChanges.set(path10.resolve(filePath2), event);
7225
+ const target = path10.resolve(filePath2);
7226
+ const settled = settledWrites.get(target);
7227
+ if (settled && event !== "unlink" && Date.now() <= settled.expiresAt) {
7228
+ const diskContent = await fsp4.readFile(target, "utf8").catch(() => null);
7229
+ if (closed)
7230
+ return;
7231
+ if (settledWrites.get(target) === settled && diskContent === settled.content) {
7232
+ debug(`Suppressed settled-write watcher echo: ${target}`);
7233
+ return;
7234
+ }
7235
+ }
7236
+ if (settledWrites.get(target) === settled)
7237
+ settledWrites.delete(target);
7238
+ debug(`File ${event}: ${target}`);
7239
+ pendingChanges.set(target, event);
6893
7240
  scheduleRebuild();
6894
7241
  }
6895
- watcher.on("all", recordChange);
7242
+ watcher.on("all", (event, filePath2) => {
7243
+ recordChange(event, filePath2);
7244
+ });
6896
7245
  externalWatcher.on("all", (event, filePath2) => {
6897
7246
  const target = path10.resolve(filePath2);
6898
7247
  if (!desiredExternals.has(target))
@@ -6901,6 +7250,43 @@ function createFileWatcher(state) {
6901
7250
  return;
6902
7251
  recordChange(event, target);
6903
7252
  });
7253
+ function isWatchedSource(target) {
7254
+ const rel = path10.relative(inputResolved, target);
7255
+ const inProject = rel === "" || rel !== ".." && !rel.startsWith(`..${path10.sep}`) && !path10.isAbsolute(rel);
7256
+ if (inProject)
7257
+ return !isIgnoredWatchPath(target, inputResolved);
7258
+ return desiredExternals.has(target);
7259
+ }
7260
+ function acceptSettledWrite(target, writtenContent) {
7261
+ const settled = { content: writtenContent, expiresAt: Date.now() + 2000 };
7262
+ settledWrites.set(target, settled);
7263
+ const expiry = setTimeout(() => {
7264
+ if (settledWrites.get(target) === settled)
7265
+ settledWrites.delete(target);
7266
+ }, 2000);
7267
+ expiry.unref?.();
7268
+ pendingChanges.set(target, "change");
7269
+ immediatePending = true;
7270
+ if (state.rebuildTimer) {
7271
+ clearTimeout(state.rebuildTimer);
7272
+ state.rebuildTimer = null;
7273
+ }
7274
+ if (!state.isRebuilding)
7275
+ runRebuild();
7276
+ }
7277
+ state.notifySettledWrite = (filePath2, writtenContent) => {
7278
+ if (closed)
7279
+ return;
7280
+ const target = path10.resolve(filePath2);
7281
+ if (isWatchedSource(target)) {
7282
+ acceptSettledWrite(target, writtenContent);
7283
+ return;
7284
+ }
7285
+ syncQueue.then(() => {
7286
+ if (!closed && isWatchedSource(target))
7287
+ acceptSettledWrite(target, writtenContent);
7288
+ });
7289
+ };
6904
7290
  let watchedExternalRoots = new Set;
6905
7291
  let syncQueue = Promise.resolve();
6906
7292
  function syncExternalWatches() {
@@ -6952,10 +7338,15 @@ function createFileWatcher(state) {
6952
7338
  async function runRebuild() {
6953
7339
  if (closed || state.isRebuilding)
6954
7340
  return;
7341
+ if (state.rebuildTimer) {
7342
+ clearTimeout(state.rebuildTimer);
7343
+ state.rebuildTimer = null;
7344
+ }
6955
7345
  const changes = [...pendingChanges.entries()];
6956
7346
  pendingChanges.clear();
6957
7347
  if (changes.length === 0)
6958
7348
  return;
7349
+ immediatePending = false;
6959
7350
  state.isRebuilding = true;
6960
7351
  let resolveInFlight;
6961
7352
  const inFlight = new Promise((resolve9) => {
@@ -6972,11 +7363,17 @@ function createFileWatcher(state) {
6972
7363
  await syncExternalWatches();
6973
7364
  if (closed)
6974
7365
  return;
6975
- await generateAndWriteHtml(inputResolved, state.tempDir, updatedConfig);
7366
+ await generateAndWriteHtml(inputResolved, state.tempDir, updatedConfig, state.cssAssets);
6976
7367
  if (closed)
6977
7368
  return;
6978
- state.previewServer?.broadcastReload();
6979
- info(changes.length > 1 ? `Preview updated (${changes.length} files changed — full reload)` : "Preview updated");
7369
+ const decision = decideBroadcast(describeChanges(changes, inputResolved), changes.length, incrementalPreviewEnabled());
7370
+ if (decision.kind === "chapter-splice") {
7371
+ state.previewServer?.broadcastContentUpdate(decision.chapterId);
7372
+ info(`Chapter updated: ${decision.relativePath}`);
7373
+ } else {
7374
+ state.previewServer?.broadcastReload();
7375
+ info(changes.length > 1 ? `Preview updated (${changes.length} files changed — full reload)` : "Preview updated");
7376
+ }
6980
7377
  } catch (err) {
6981
7378
  error("Failed to regenerate preview:", err);
6982
7379
  } finally {
@@ -6984,13 +7381,19 @@ function createFileWatcher(state) {
6984
7381
  resolveInFlight();
6985
7382
  if (state.rebuildPromise === inFlight)
6986
7383
  state.rebuildPromise = null;
6987
- if (!closed && pendingChanges.size > 0)
6988
- scheduleRebuild();
7384
+ if (!closed && pendingChanges.size > 0) {
7385
+ if (immediatePending)
7386
+ runRebuild();
7387
+ else
7388
+ scheduleRebuild();
7389
+ }
6989
7390
  }
6990
7391
  }
6991
7392
  const closeBookWatcher = watcher.close.bind(watcher);
6992
7393
  watcher.close = async () => {
6993
7394
  closed = true;
7395
+ state.notifySettledWrite = null;
7396
+ settledWrites.clear();
6994
7397
  await syncQueue.catch(() => {});
6995
7398
  await state.rebuildPromise?.catch(() => {});
6996
7399
  await externalWatcher.close();
@@ -7089,7 +7492,7 @@ async function restartPreview(newInputPath, state) {
7089
7492
  state.currentInputPath = newInputPath;
7090
7493
  const manifest = await loadManifest(newInputPath);
7091
7494
  state.config = resolveConfig({}, manifest);
7092
- await generateAndWriteHtml(newInputPath, state.tempDir, state.config);
7495
+ await generateAndWriteHtml(newInputPath, state.tempDir, state.config, state.cssAssets);
7093
7496
  startFileWatcher(state);
7094
7497
  state.previewServer?.broadcastReload();
7095
7498
  info("Preview restarted successfully");
@@ -7143,22 +7546,26 @@ function createServerState(inputPath, tempDir, config, options) {
7143
7546
  rebuildTimer: null,
7144
7547
  isRebuilding: false,
7145
7548
  rebuildPromise: null,
7549
+ notifySettledWrite: null,
7146
7550
  previewServer: null,
7147
7551
  isShuttingDown: false,
7148
7552
  tempDir,
7149
7553
  config,
7150
- options
7554
+ options,
7555
+ cssAssets: new Map
7151
7556
  };
7152
7557
  }
7153
7558
 
7154
7559
  // src/preview/http-server.ts
7155
7560
  import http2 from "node:http";
7156
7561
  import net from "node:net";
7562
+ import { randomUUID } from "node:crypto";
7157
7563
  import { readFile as readFile20, stat as stat4 } from "node:fs/promises";
7158
7564
  import path12 from "path";
7159
7565
  import { WebSocket, WebSocketServer } from "ws";
7160
7566
  var HMR_PATH = "/__gutterpress-hmr";
7161
- var HMR_CLIENT_SNIPPET = `
7567
+ function hmrClientSnippet(initialRevision, instanceId) {
7568
+ return `
7162
7569
  <script>
7163
7570
  (function () {
7164
7571
  // When loaded by the preview SHELL (iframe double-buffer), the shell loads us
@@ -7168,6 +7575,10 @@ var HMR_CLIENT_SNIPPET = `
7168
7575
  // for scroll-anchor and reload.
7169
7576
  if (/[?&]gutterpressshell=1/.test(location.search)) return;
7170
7577
  var ANCHOR_KEY = 'gutterpress-scroll-anchor';
7578
+ var appliedRevision = ${initialRevision};
7579
+ var appliedInstance = ${JSON.stringify(instanceId)};
7580
+ var readyToAcknowledge = false;
7581
+ var reloadRequested = false;
7171
7582
 
7172
7583
  // Find the element nearest the top of the viewport that carries a source
7173
7584
  // line (markdown-it-source-map emits data-source-line on block elements).
@@ -7218,7 +7629,13 @@ var HMR_CLIENT_SNIPPET = `
7218
7629
  }
7219
7630
  }
7220
7631
  if (el) {
7221
- window.scrollBy(0, el.getBoundingClientRect().top - a.offset);
7632
+ window.scrollBy({
7633
+ top: el.getBoundingClientRect().top - a.offset,
7634
+ behavior: 'instant'
7635
+ });
7636
+ if (window.previewAPI && typeof window.previewAPI.refresh === 'function') {
7637
+ window.previewAPI.refresh();
7638
+ }
7222
7639
  return;
7223
7640
  }
7224
7641
  if (tries++ < 240) setTimeout(attempt, 25); // wait for pagination
@@ -7226,44 +7643,100 @@ var HMR_CLIENT_SNIPPET = `
7226
7643
  }
7227
7644
  var restored = false;
7228
7645
  function restoreOnce() { if (restored) return; restored = true; restoreAnchor(); }
7646
+ function finishInitialRender() {
7647
+ restoreOnce();
7648
+ readyToAcknowledge = true;
7649
+ acknowledge();
7650
+ }
7229
7651
  // CRITICAL ordering: when the Paged.js polyfill is present it re-paginates on
7230
- // load and its PagedConfig.after calls scrollTo(0,0) THEN fires
7652
+ // load and its PagedConfig.after scrolls to the document start THEN fires
7231
7653
  // 'renderingComplete'. So in engine mode we MUST wait for that event —
7232
- // restoring earlier would be wiped by the scrollTo(0,0). In static mode (no
7654
+ // restoring earlier would be wiped by that initial scroll. In static mode (no
7233
7655
  // engine) the content is final immediately, so restore right after load.
7234
- var hasEngine = !!document.querySelector('script[src*="paged.polyfill"], script[src*="pagedjs"]');
7235
- window.addEventListener('renderingComplete', restoreOnce);
7656
+ var hasEngine = !!document.querySelector('script[src*="paged.polyfill"]');
7657
+ window.addEventListener('renderingComplete', finishInitialRender);
7236
7658
  if (!hasEngine) {
7237
7659
  if (document.readyState === 'loading') {
7238
- document.addEventListener('DOMContentLoaded', function () { setTimeout(restoreOnce, 50); });
7660
+ document.addEventListener('DOMContentLoaded', function () { setTimeout(finishInitialRender, 50); });
7239
7661
  } else {
7240
- setTimeout(restoreOnce, 50);
7662
+ setTimeout(finishInitialRender, 50);
7241
7663
  }
7242
7664
  }
7243
- setTimeout(restoreOnce, 10000); // safety net if renderingComplete never fires
7665
+ var ws = null;
7666
+ var reconnectTimer = null;
7667
+ var reconnectDelay = 250;
7668
+ var stopped = false;
7669
+
7670
+ function acknowledge() {
7671
+ if (!readyToAcknowledge || !ws || ws.readyState !== 1) return;
7672
+ try {
7673
+ ws.send(JSON.stringify({
7674
+ type: 'reload-applied',
7675
+ instance: appliedInstance,
7676
+ revision: appliedRevision
7677
+ }));
7678
+ } catch (_) {}
7679
+ }
7244
7680
 
7245
- var ws = new WebSocket(location.origin.replace(/^http/, 'ws') + '${HMR_PATH}');
7246
- ws.onmessage = function (e) {
7247
- var msg;
7248
- try { msg = JSON.parse(e.data); } catch (_) { return; }
7249
- if (msg.type === 'full-reload') {
7681
+ function connect() {
7682
+ if (stopped) return;
7683
+ ws = new WebSocket(location.origin.replace(/^http/, 'ws') + '${HMR_PATH}');
7684
+ ws.onopen = function () {
7685
+ reconnectDelay = 250;
7686
+ acknowledge();
7687
+ };
7688
+ ws.onmessage = function (e) {
7689
+ var msg;
7690
+ try { msg = JSON.parse(e.data); } catch (_) { return; }
7691
+ if (
7692
+ msg.type !== 'reload-state' &&
7693
+ msg.type !== 'full-reload' &&
7694
+ msg.type !== 'content-update'
7695
+ ) return;
7696
+ var instance = typeof msg.instance === 'string' ? msg.instance : null;
7697
+ var revision = Number(msg.revision);
7698
+ if (!instance || !Number.isSafeInteger(revision) || revision < 0) return;
7699
+ if (instance === appliedInstance && revision <= appliedRevision) {
7700
+ acknowledge();
7701
+ return;
7702
+ }
7703
+ if (reloadRequested) return;
7704
+ reloadRequested = true;
7250
7705
  try { var a = captureAnchor(); if (a) sessionStorage.setItem(ANCHOR_KEY, JSON.stringify(a)); } catch (_) {}
7251
7706
  location.reload();
7252
- return;
7253
- }
7254
- };
7707
+ };
7708
+ ws.onclose = function () {
7709
+ ws = null;
7710
+ if (stopped || reconnectTimer !== null) return;
7711
+ reconnectTimer = setTimeout(function () {
7712
+ reconnectTimer = null;
7713
+ connect();
7714
+ }, reconnectDelay);
7715
+ reconnectDelay = Math.min(reconnectDelay * 2, 5000);
7716
+ };
7717
+ ws.onerror = function () { try { ws.close(); } catch (_) {} };
7718
+ }
7719
+
7720
+ window.addEventListener('beforeunload', function () {
7721
+ stopped = true;
7722
+ if (reconnectTimer !== null) clearTimeout(reconnectTimer);
7723
+ });
7724
+ connect();
7255
7725
  })();
7256
7726
  </script>
7257
7727
  `;
7258
- var SHELL_HTML = `<!doctype html>
7728
+ }
7729
+ function shellHtml(initialRevision, instanceId) {
7730
+ return `<!doctype html>
7259
7731
  <html lang="en"><head><meta charset="utf-8"><title>gutterpress preview</title>
7260
7732
  <style>html,body{margin:0;height:100%;background:#fff;overflow:hidden}
7261
7733
  iframe{position:absolute;inset:0;width:100%;height:100%;border:0;display:block}</style>
7262
7734
  </head><body>
7263
7735
  <iframe id="gutterpress-active" src="/book.html?gutterpressshell=1" title="preview"></iframe>
7264
- <script>window.__GUTTERPRESS_HMR=${JSON.stringify(HMR_PATH)};</script>
7736
+ <script>window.__GUTTERPRESS_HMR=${JSON.stringify(HMR_PATH)};window.__GUTTERPRESS_INSTANCE=${JSON.stringify(instanceId)};window.__GUTTERPRESS_REVISION=${initialRevision};</script>
7265
7737
  <script src="/preview/scripts/preview-shell.js"></script>
7266
7738
  </body></html>`;
7739
+ }
7267
7740
  function probePort(port, host) {
7268
7741
  return new Promise((resolve9) => {
7269
7742
  const srv = net.createServer();
@@ -7305,13 +7778,14 @@ async function findAvailablePort(startPort, host = "127.0.0.1") {
7305
7778
  }
7306
7779
  throw new BuildError(`Could not find an available port on ${host} from ${firstPort} to ${port - 1}; all are already in use. ` + "Stop another preview server or pass --port with an available port.", undefined, { cause: lastBusyError });
7307
7780
  }
7308
- function injectHmrClient(html) {
7781
+ function injectHmrClient(html, revision, instanceId) {
7782
+ const snippet = hmrClientSnippet(revision, instanceId);
7309
7783
  const closingBody = html.lastIndexOf("</body>");
7310
7784
  if (closingBody === -1)
7311
- return html + HMR_CLIENT_SNIPPET;
7312
- return html.slice(0, closingBody) + HMR_CLIENT_SNIPPET + html.slice(closingBody);
7785
+ return html + snippet;
7786
+ return html.slice(0, closingBody) + snippet + html.slice(closingBody);
7313
7787
  }
7314
- async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders = {}) {
7788
+ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders = {}, hmrRevision = 0, hmrInstance = "") {
7315
7789
  let filePath2 = absPath;
7316
7790
  try {
7317
7791
  const s = await stat4(filePath2);
@@ -7332,7 +7806,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
7332
7806
  }
7333
7807
  const isHtml = filePath2.endsWith(".html") || filePath2.endsWith(".htm");
7334
7808
  if (isHtml) {
7335
- const withHmr = injectHmrClient(data.toString("utf-8"));
7809
+ const withHmr = injectHmrClient(data.toString("utf-8"), hmrRevision, hmrInstance);
7336
7810
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
7337
7811
  res.end(withHmr);
7338
7812
  return;
@@ -7350,21 +7824,89 @@ function matchesEmbedded(urlPathname) {
7350
7824
  return true;
7351
7825
  return EMBEDDED_PREFIXES.some((p) => urlPathname.startsWith(p));
7352
7826
  }
7353
- function isDotfileRequest(urlPathname) {
7354
- let decoded;
7355
- try {
7356
- decoded = decodeURIComponent(urlPathname);
7357
- } catch {
7358
- return true;
7359
- }
7360
- return decoded.split("/").some((segment) => segment.startsWith("."));
7361
- }
7362
7827
  async function createPreviewServer(state, port) {
7363
- const clients = new Set;
7828
+ const ACK_RETRY_MS = 2000;
7829
+ const MAX_ACK_RETRIES = 95;
7830
+ const instanceId = randomUUID();
7831
+ let reloadRevision = 0;
7832
+ const clients = new Map;
7833
+ function sendRevision(ws, type, file) {
7834
+ if (ws.readyState !== WebSocket.OPEN)
7835
+ return;
7836
+ try {
7837
+ ws.send(JSON.stringify({
7838
+ type,
7839
+ instance: instanceId,
7840
+ revision: reloadRevision,
7841
+ ...file ? { file } : {}
7842
+ }));
7843
+ } catch {}
7844
+ }
7845
+ function waitForAcknowledgement(ws) {
7846
+ const client = clients.get(ws);
7847
+ if (!client)
7848
+ return;
7849
+ if (client.acknowledgedRevision >= reloadRevision) {
7850
+ if (client.retryTimer !== null)
7851
+ clearTimeout(client.retryTimer);
7852
+ client.retryTimer = null;
7853
+ client.retryAttempts = 0;
7854
+ return;
7855
+ }
7856
+ if (client.retryTimer !== null)
7857
+ return;
7858
+ client.retryTimer = setTimeout(() => {
7859
+ const current = clients.get(ws);
7860
+ if (!current)
7861
+ return;
7862
+ current.retryTimer = null;
7863
+ if (current.retryAttempts >= MAX_ACK_RETRIES) {
7864
+ ws.terminate();
7865
+ return;
7866
+ }
7867
+ current.retryAttempts++;
7868
+ sendRevision(ws, "reload-state");
7869
+ waitForAcknowledgement(ws);
7870
+ }, ACK_RETRY_MS);
7871
+ client.retryTimer.unref?.();
7872
+ }
7873
+ function removeClient(ws) {
7874
+ const client = clients.get(ws);
7875
+ if (client && client.retryTimer !== null)
7876
+ clearTimeout(client.retryTimer);
7877
+ clients.delete(ws);
7878
+ }
7364
7879
  const wss = new WebSocketServer({ noServer: true });
7365
7880
  wss.on("connection", (ws) => {
7366
- clients.add(ws);
7367
- ws.on("close", () => clients.delete(ws));
7881
+ clients.set(ws, { acknowledgedRevision: -1, retryAttempts: 0, retryTimer: null });
7882
+ ws.on("message", (raw) => {
7883
+ let message;
7884
+ try {
7885
+ message = JSON.parse(raw.toString());
7886
+ } catch {
7887
+ return;
7888
+ }
7889
+ if (!message || typeof message !== "object")
7890
+ return;
7891
+ const payload = message;
7892
+ if (payload.type !== "reload-applied")
7893
+ return;
7894
+ const revision = Number(payload.revision);
7895
+ if (payload.instance !== instanceId || !Number.isSafeInteger(revision) || revision < 0 || revision > reloadRevision)
7896
+ return;
7897
+ const client = clients.get(ws);
7898
+ if (!client)
7899
+ return;
7900
+ client.acknowledgedRevision = Math.max(client.acknowledgedRevision, revision);
7901
+ waitForAcknowledgement(ws);
7902
+ });
7903
+ ws.on("close", () => removeClient(ws));
7904
+ ws.on("error", () => {
7905
+ removeClient(ws);
7906
+ ws.terminate();
7907
+ });
7908
+ sendRevision(ws, "reload-state");
7909
+ waitForAcknowledgement(ws);
7368
7910
  });
7369
7911
  const server = http2.createServer(async (req, res) => {
7370
7912
  const url = new URL(req.url, "http://127.0.0.1");
@@ -7387,6 +7929,34 @@ async function createPreviewServer(state, port) {
7387
7929
  res.end("Not Found");
7388
7930
  return;
7389
7931
  }
7932
+ if (url.pathname === "/__chapter" && req.method === "GET") {
7933
+ const rawFile = url.searchParams.get("file");
7934
+ const file = rawFile ? canonicalChapterId(rawFile) : "";
7935
+ const revision = Number(url.searchParams.get("revision"));
7936
+ const configuredFiles = state.config.source?.files;
7937
+ if (!file || !state.currentInputPath || !incrementalPreviewEnabled() || !Number.isSafeInteger(revision) || revision !== reloadRevision || path12.extname(file).toLowerCase() !== ".md" || hasDotSegment(file) || !resolveWithinRoot(file, state.currentInputPath) || Array.isArray(configuredFiles) && configuredFiles.length > 0 && !configuredFiles.some((configured) => canonicalChapterId(configured) === file)) {
7938
+ res.writeHead(400);
7939
+ res.end("Bad Request");
7940
+ return;
7941
+ }
7942
+ try {
7943
+ const html = await renderChapterPreviewHtml(state.currentInputPath, file, state.config);
7944
+ if (revision !== reloadRevision) {
7945
+ res.writeHead(409);
7946
+ res.end("Superseded");
7947
+ return;
7948
+ }
7949
+ res.writeHead(200, {
7950
+ "Content-Type": "text/html; charset=utf-8",
7951
+ "Cache-Control": "no-store"
7952
+ });
7953
+ res.end(html);
7954
+ } catch (error2) {
7955
+ res.writeHead(500);
7956
+ res.end(error2 instanceof Error ? error2.message : String(error2));
7957
+ }
7958
+ return;
7959
+ }
7390
7960
  if (matchesEmbedded(url.pathname)) {
7391
7961
  if (req.headers["if-none-match"] === EMBEDDED_ETAG) {
7392
7962
  res.writeHead(304, { ETag: EMBEDDED_ETAG, "Cache-Control": EMBEDDED_CACHE_CONTROL });
@@ -7405,12 +7975,22 @@ async function createPreviewServer(state, port) {
7405
7975
  }
7406
7976
  if (url.pathname === "/" && incrementalPreviewEnabled()) {
7407
7977
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
7408
- res.end(SHELL_HTML);
7978
+ res.end(shellHtml(reloadRevision, instanceId));
7409
7979
  return;
7410
7980
  }
7411
7981
  const pathname = url.pathname === "/" ? "/book.html" : url.pathname;
7982
+ const cssAssetSource = state.cssAssets.get(pathname.replace(/^\/+/, ""));
7983
+ if (cssAssetSource) {
7984
+ await serveStatic(cssAssetSource, res).catch((err) => {
7985
+ if (!res.headersSent) {
7986
+ res.writeHead(500);
7987
+ res.end(`Internal Server Error: ${err.message}`);
7988
+ }
7989
+ });
7990
+ return;
7991
+ }
7412
7992
  const servingProjectRoot = pathname !== "/book.html";
7413
- if (servingProjectRoot && (!state.currentInputPath || isDotfileRequest(pathname))) {
7993
+ if (servingProjectRoot && (!state.currentInputPath || hasDotSegment(pathname))) {
7414
7994
  res.writeHead(404);
7415
7995
  res.end("Not Found");
7416
7996
  return;
@@ -7422,7 +8002,7 @@ async function createPreviewServer(state, port) {
7422
8002
  res.end("Not Found");
7423
8003
  return;
7424
8004
  }
7425
- await serveStatic(absPath, res).catch((err) => {
8005
+ await serveStatic(absPath, res, "no-store", {}, reloadRevision, instanceId).catch((err) => {
7426
8006
  if (!res.headersSent) {
7427
8007
  res.writeHead(500);
7428
8008
  res.end(`Internal Server Error: ${err.message}`);
@@ -7449,23 +8029,33 @@ async function createPreviewServer(state, port) {
7449
8029
  openPath(serverUrl).catch(() => {});
7450
8030
  }
7451
8031
  let stopped = false;
7452
- const broadcast = (payload) => {
8032
+ const broadcastUpdate = (type, file) => {
7453
8033
  if (stopped)
7454
8034
  return;
7455
- const message = JSON.stringify(payload);
7456
- for (const ws of clients) {
7457
- if (ws.readyState === WebSocket.OPEN)
7458
- ws.send(message);
8035
+ reloadRevision++;
8036
+ for (const [ws] of clients) {
8037
+ sendRevision(ws, type, file);
8038
+ waitForAcknowledgement(ws);
7459
8039
  }
7460
8040
  };
8041
+ const updateInFlight = () => {
8042
+ for (const client of clients.values()) {
8043
+ if (client.acknowledgedRevision < reloadRevision)
8044
+ return true;
8045
+ }
8046
+ return false;
8047
+ };
7461
8048
  return {
7462
8049
  port: boundPort,
7463
8050
  async close() {
7464
8051
  if (stopped)
7465
8052
  return;
7466
8053
  stopped = true;
7467
- for (const ws of clients)
8054
+ for (const [ws, client] of clients) {
8055
+ if (client.retryTimer !== null)
8056
+ clearTimeout(client.retryTimer);
7468
8057
  ws.terminate();
8058
+ }
7469
8059
  clients.clear();
7470
8060
  wss.close();
7471
8061
  await new Promise((resolve9) => {
@@ -7476,7 +8066,13 @@ async function createPreviewServer(state, port) {
7476
8066
  });
7477
8067
  },
7478
8068
  broadcastReload() {
7479
- broadcast({ type: "full-reload" });
8069
+ broadcastUpdate("full-reload");
8070
+ },
8071
+ broadcastContentUpdate(file) {
8072
+ if (updateInFlight())
8073
+ broadcastUpdate("full-reload");
8074
+ else
8075
+ broadcastUpdate("content-update", canonicalChapterId(file));
7480
8076
  }
7481
8077
  };
7482
8078
  }
@@ -7495,8 +8091,8 @@ async function startPreviewServer(options) {
7495
8091
  }
7496
8092
  const config = await initializeConfiguration(inputPath);
7497
8093
  const tempDir = await initializePreviewDirectories();
7498
- await generateAndWriteHtml(inputPath, tempDir, config);
7499
8094
  const state = createServerState(inputPath, tempDir, config, options);
8095
+ await generateAndWriteHtml(inputPath, tempDir, config, state.cssAssets);
7500
8096
  const restartPreview2 = async (newInputPath) => {
7501
8097
  await restartPreview(newInputPath, state);
7502
8098
  };
@@ -7529,17 +8125,74 @@ async function startPreviewServer(options) {
7529
8125
  }
7530
8126
  await shutdownServer(state);
7531
8127
  },
7532
- restart: restartPreview2
8128
+ restart: restartPreview2,
8129
+ notifySettledWrite: (filePath2, writtenContent) => {
8130
+ state.notifySettledWrite?.(filePath2, writtenContent);
8131
+ }
7533
8132
  };
7534
8133
  }
7535
8134
 
7536
8135
  // src/lib/project-scaffold.ts
7537
- import { access, copyFile, cp as cp2, mkdir as mkdir6, readdir as readdir5, readFile as readFile21, stat as stat5, writeFile as writeFile6 } from "node:fs/promises";
7538
- import { constants as FS, existsSync as existsSync10 } from "node:fs";
8136
+ import { access, copyFile, cp as cp2, mkdir as mkdir7, readdir as readdir5, readFile as readFile22, stat as stat5, writeFile as writeFile6 } from "node:fs/promises";
8137
+ import { constants as FS, existsSync as existsSync11 } from "node:fs";
8138
+ import path14 from "node:path";
8139
+
8140
+ // src/lib/manifest-doc.ts
8141
+ import { existsSync as existsSync10 } from "node:fs";
8142
+ import { randomUUID as randomUUID2 } from "node:crypto";
8143
+ import { mkdir as mkdir6, open, readFile as readFile21, rename, rm as rm2 } from "node:fs/promises";
7539
8144
  import path13 from "node:path";
8145
+ import { parseDocument, isSeq, YAMLSeq } from "yaml";
8146
+ function resolveManifestPath(projectDir) {
8147
+ const existing = MANIFEST_FILENAMES.find((name) => existsSync10(path13.join(projectDir, name)));
8148
+ return path13.join(projectDir, existing ?? MANIFEST_FILENAMES[0]);
8149
+ }
8150
+ async function loadManifestDoc(projectDir) {
8151
+ const file = resolveManifestPath(projectDir);
8152
+ let text = "";
8153
+ try {
8154
+ text = await readFile21(file, "utf8");
8155
+ } catch (error2) {
8156
+ if (error2.code !== "ENOENT")
8157
+ throw error2;
8158
+ }
8159
+ return { doc: parseDocument(text), file };
8160
+ }
8161
+ async function writeManifestDoc(file, doc) {
8162
+ const dir = path13.dirname(file);
8163
+ await mkdir6(dir, { recursive: true });
8164
+ const temporary = path13.join(dir, `.${path13.basename(file)}.tmp-${process.pid}-${randomUUID2()}`);
8165
+ try {
8166
+ const handle = await open(temporary, "wx", 438);
8167
+ try {
8168
+ await handle.writeFile(doc.toString(), "utf8");
8169
+ await handle.sync();
8170
+ } finally {
8171
+ await handle.close();
8172
+ }
8173
+ await rename(temporary, file);
8174
+ const directory = await open(dir, "r").catch(() => null);
8175
+ if (directory) {
8176
+ await directory.sync().catch(() => {});
8177
+ await directory.close();
8178
+ }
8179
+ } finally {
8180
+ await rm2(temporary, { force: true });
8181
+ }
8182
+ }
8183
+ function ensureSeq(doc, key) {
8184
+ let seq = doc.get(key, true);
8185
+ if (!isSeq(seq)) {
8186
+ const fresh = new YAMLSeq(doc.schema);
8187
+ doc.set(key, fresh);
8188
+ seq = fresh;
8189
+ }
8190
+ return seq;
8191
+ }
8192
+
8193
+ // src/lib/project-scaffold.ts
7540
8194
  var STARTER_THEME_FOR_TEMPLATE = {
7541
8195
  book: "clean-book",
7542
- ttrpg: "ttrpg-supplement",
7543
8196
  zine: "zine",
7544
8197
  technical: "technical-doc"
7545
8198
  };
@@ -7559,6 +8212,39 @@ function escapeYamlScalar(value) {
7559
8212
  return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
7560
8213
  }
7561
8214
  var DEFAULT_AUTHOR = "Anonymous";
8215
+ function isPositivePoints(value) {
8216
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
8217
+ }
8218
+ function requirePreset(preset, customPage) {
8219
+ if (!preset || !PRESET_IDS.includes(preset)) {
8220
+ const got = preset ? ` (got "${preset}")` : "";
8221
+ throw new CreateProjectErrorImpl("preset-required", `A preset is required to create a book${got}. Choose one of: ${PRESET_IDS.join(", ")}.`);
8222
+ }
8223
+ if (preset === "custom") {
8224
+ const missing = [];
8225
+ if (!customPage || !isPositivePoints(customPage.width))
8226
+ missing.push("page width");
8227
+ if (!customPage || !isPositivePoints(customPage.height))
8228
+ missing.push("page height");
8229
+ if (missing.length > 0) {
8230
+ throw new CreateProjectErrorImpl("custom-page-required", `The custom preset needs a trim size: ${missing.join(" and ")} in points ` + `(72pt = 1in — e.g. US Letter is 612 x 792).`);
8231
+ }
8232
+ } else if (customPage && (!isPositivePoints(customPage.width) || !isPositivePoints(customPage.height))) {
8233
+ throw new CreateProjectErrorImpl("custom-page-required", "Page overrides need a positive width and height in points (72pt = 1in).");
8234
+ }
8235
+ if (customPage?.tolerance !== undefined && !isPositivePoints(customPage.tolerance)) {
8236
+ throw new CreateProjectErrorImpl("custom-page-required", "Page tolerance must be a positive number of points.");
8237
+ }
8238
+ return preset;
8239
+ }
8240
+ function resolveScaffoldTargets(requested, preset) {
8241
+ const ids = requested ?? [...PRESETS[preset].defaultTargets];
8242
+ const unknown = ids.filter((id) => !TARGETS[id]);
8243
+ if (unknown.length > 0) {
8244
+ throw new CreateProjectErrorImpl("invalid-targets", `Unknown publish target${unknown.length > 1 ? "s" : ""} "${unknown.join('", "')}". ` + `Known targets: ${TARGET_IDS.join(", ")}.`);
8245
+ }
8246
+ return [...new Set(ids)];
8247
+ }
7562
8248
  async function scaffoldProject(options) {
7563
8249
  const name = (options.name ?? "").trim();
7564
8250
  if (!name) {
@@ -7570,7 +8256,7 @@ async function scaffoldProject(options) {
7570
8256
  throw new CreateProjectErrorImpl("invalid-name", `Could not derive a valid folder name from "${name}".`);
7571
8257
  }
7572
8258
  const parentDir = options.parentDir;
7573
- if (!parentDir || !path13.isAbsolute(parentDir)) {
8259
+ if (!parentDir || !path14.isAbsolute(parentDir)) {
7574
8260
  throw new CreateProjectErrorImpl("parent-not-writable", "The save location must be an absolute path.");
7575
8261
  }
7576
8262
  try {
@@ -7578,7 +8264,7 @@ async function scaffoldProject(options) {
7578
8264
  } catch {
7579
8265
  throw new CreateProjectErrorImpl("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
7580
8266
  }
7581
- const projectDir = path13.join(parentDir, folderName);
8267
+ const projectDir = path14.join(parentDir, folderName);
7582
8268
  let targetExists = true;
7583
8269
  try {
7584
8270
  await access(projectDir, FS.F_OK);
@@ -7590,26 +8276,28 @@ async function scaffoldProject(options) {
7590
8276
  }
7591
8277
  const template = options.template ?? "book";
7592
8278
  const customTemplateDir = options.templateDir;
8279
+ const preset = customTemplateDir && options.preset === undefined ? undefined : requirePreset(options.preset, options.customPage);
8280
+ const targets = preset ? resolveScaffoldTargets(options.targets, preset) : undefined;
7593
8281
  try {
7594
- await mkdir6(projectDir, { recursive: true });
8282
+ await mkdir7(projectDir, { recursive: true });
7595
8283
  if (customTemplateDir) {
7596
8284
  const entries = await readdir5(customTemplateDir, { withFileTypes: true });
7597
8285
  for (const entry of entries) {
7598
8286
  if (entry.name === ".gutterpress-template.json")
7599
8287
  continue;
7600
- await cp2(path13.join(customTemplateDir, entry.name), path13.join(projectDir, entry.name), { recursive: true });
8288
+ await cp2(path14.join(customTemplateDir, entry.name), path14.join(projectDir, entry.name), { recursive: true });
7601
8289
  }
7602
- await mkdir6(path13.join(projectDir, "assets"), { recursive: true });
8290
+ await mkdir7(path14.join(projectDir, "assets"), { recursive: true });
7603
8291
  } else {
7604
8292
  const tplManifest = await getAssetPath(`templates/${template}/manifest.yaml`);
7605
8293
  const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
7606
- await mkdir6(path13.join(projectDir, "assets"), { recursive: true });
7607
- await copyFile(tplManifest, path13.join(projectDir, MANIFEST_FILENAMES[0]));
7608
- await copyFile(tplChapter, path13.join(projectDir, "chapter-01.md"));
8294
+ await mkdir7(path14.join(projectDir, "assets"), { recursive: true });
8295
+ await copyFile(tplManifest, path14.join(projectDir, MANIFEST_FILENAMES[0]));
8296
+ await copyFile(tplChapter, path14.join(projectDir, "chapter-01.md"));
7609
8297
  const starterThemeId = STARTER_THEME_FOR_TEMPLATE[template] ?? "clean-book";
7610
8298
  const starterCss = await getAssetPath(`themes/${starterThemeId}/theme.css`);
7611
- await mkdir6(path13.join(projectDir, "styles"), { recursive: true });
7612
- await copyFile(starterCss, path13.join(projectDir, "styles", "book.css"));
8299
+ await mkdir7(path14.join(projectDir, "styles"), { recursive: true });
8300
+ await copyFile(starterCss, path14.join(projectDir, "styles", "book.css"));
7613
8301
  }
7614
8302
  } catch (e) {
7615
8303
  throw new CreateProjectErrorImpl("scaffold-io", `Could not create the project files: ${e instanceof Error ? e.message : String(e)}`);
@@ -7619,18 +8307,34 @@ async function scaffoldProject(options) {
7619
8307
  } catch (e) {
7620
8308
  throw new CreateProjectErrorImpl("scaffold-io", `Could not write .gitignore: ${e instanceof Error ? e.message : String(e)}`);
7621
8309
  }
7622
- const manifestPath = MANIFEST_FILENAMES.map((name2) => path13.join(projectDir, name2)).find(existsSync10) ?? path13.join(projectDir, MANIFEST_FILENAMES[0]);
8310
+ const manifestPath = MANIFEST_FILENAMES.map((name2) => path14.join(projectDir, name2)).find(existsSync11) ?? path14.join(projectDir, MANIFEST_FILENAMES[0]);
7623
8311
  const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
7624
8312
  const substitutions = {
7625
8313
  "{{TITLE}}": escapeYamlScalar(name),
7626
8314
  "{{AUTHOR}}": escapeYamlScalar(author)
7627
8315
  };
7628
- let openFile = path13.join(projectDir, "chapter-01.md");
8316
+ let openFile = path14.join(projectDir, "chapter-01.md");
7629
8317
  try {
7630
8318
  await fillTemplateFile(manifestPath, substitutions);
8319
+ if (preset) {
8320
+ const { doc, file } = await loadManifestDoc(projectDir);
8321
+ doc.set("preset", preset);
8322
+ doc.set("targets", doc.createNode(targets ?? []));
8323
+ if (options.customPage) {
8324
+ const page = {
8325
+ width: options.customPage.width,
8326
+ height: options.customPage.height
8327
+ };
8328
+ if (options.customPage.tolerance !== undefined) {
8329
+ page.tolerance = options.customPage.tolerance;
8330
+ }
8331
+ doc.set("page", doc.createNode(page));
8332
+ }
8333
+ await writeManifestDoc(file, doc);
8334
+ }
7631
8335
  const firstSource = await firstSourceFile(manifestPath);
7632
8336
  if (firstSource)
7633
- openFile = path13.join(projectDir, firstSource);
8337
+ openFile = path14.join(projectDir, firstSource);
7634
8338
  await fillTemplateFile(openFile, { "{{TITLE}}": name }).catch(() => {});
7635
8339
  } catch (e) {
7636
8340
  throw new CreateProjectErrorImpl("scaffold-io", `Could not finalise the project files: ${e instanceof Error ? e.message : String(e)}`);
@@ -7640,7 +8344,7 @@ async function scaffoldProject(options) {
7640
8344
  let versionHistoryError;
7641
8345
  if (requested === "local-git") {
7642
8346
  try {
7643
- const { providerFor } = await import("./source-provider-hp96b5jf.js");
8347
+ const { providerFor } = await import("./source-provider-c1rjm2c0.js");
7644
8348
  const provider = providerFor({ type: "local-folder", path: projectDir });
7645
8349
  await provider.initVersionHistory({
7646
8350
  projectDir,
@@ -7650,9 +8354,9 @@ async function scaffoldProject(options) {
7650
8354
  versionHistory = "local-git";
7651
8355
  } catch (e) {
7652
8356
  versionHistory = "none";
7653
- const { findEnclosingRepoDir } = await import("./project-source-0tg0r6vm.js");
8357
+ const { findEnclosingRepoDir } = await import("./project-source-p0gn1wd5.js");
7654
8358
  const enclosingRepoDir = await findEnclosingRepoDir(projectDir);
7655
- versionHistoryError = enclosingRepoDir ? `This book was added to ${path13.basename(enclosingRepoDir)}'s shared version history; save a snapshot to record it.` : e instanceof Error ? e.message : String(e);
8359
+ versionHistoryError = enclosingRepoDir ? `This book was added to ${path14.basename(enclosingRepoDir)}'s shared version history; save a snapshot to record it.` : e instanceof Error ? e.message : String(e);
7656
8360
  }
7657
8361
  }
7658
8362
  const result = {
@@ -7667,10 +8371,10 @@ async function scaffoldProject(options) {
7667
8371
  return result;
7668
8372
  }
7669
8373
  async function ensureGitignoreHasDist(projectDir) {
7670
- const gitignorePath = path13.join(projectDir, ".gitignore");
8374
+ const gitignorePath = path14.join(projectDir, ".gitignore");
7671
8375
  let existing = null;
7672
8376
  try {
7673
- existing = await readFile21(gitignorePath, "utf8");
8377
+ existing = await readFile22(gitignorePath, "utf8");
7674
8378
  } catch {}
7675
8379
  if (existing === null) {
7676
8380
  await writeFile6(gitignorePath, `dist/
@@ -7690,7 +8394,7 @@ async function ensureGitignoreHasDist(projectDir) {
7690
8394
  async function firstSourceFile(manifestPath) {
7691
8395
  let text;
7692
8396
  try {
7693
- text = await readFile21(manifestPath, "utf8");
8397
+ text = await readFile22(manifestPath, "utf8");
7694
8398
  } catch {
7695
8399
  return;
7696
8400
  }
@@ -7702,7 +8406,7 @@ async function firstSourceFile(manifestPath) {
7702
8406
  return m ? m[1].trim() : undefined;
7703
8407
  }
7704
8408
  async function fillTemplateFile(filePath2, substitutions) {
7705
- let text = await readFile21(filePath2, "utf8");
8409
+ let text = await readFile22(filePath2, "utf8");
7706
8410
  for (const [token, value] of Object.entries(substitutions)) {
7707
8411
  text = text.split(token).join(value);
7708
8412
  }
@@ -7710,9 +8414,9 @@ async function fillTemplateFile(filePath2, substitutions) {
7710
8414
  }
7711
8415
 
7712
8416
  // src/lib/project-templates.ts
8417
+ import { isMap, isScalar, isSeq as isSeq2, parseDocument as parseDocument2 } from "yaml";
7713
8418
  var BUILT_IN_TEMPLATE_IDS = [
7714
8419
  "book",
7715
- "ttrpg",
7716
8420
  "zine",
7717
8421
  "technical"
7718
8422
  ];
@@ -7728,63 +8432,10 @@ var SKIP_ENTRIES = new Set([
7728
8432
 
7729
8433
  // src/lib/plugin-manager.ts
7730
8434
  import path16 from "node:path";
7731
- import { isSeq as isSeq2, isMap, isScalar, YAMLMap, Scalar } from "yaml";
7732
-
7733
- // src/lib/manifest-doc.ts
7734
- import { existsSync as existsSync11 } from "node:fs";
7735
- import { randomUUID } from "node:crypto";
7736
- import { mkdir as mkdir7, open, readFile as readFile22, rename, rm as rm2 } from "node:fs/promises";
7737
- import path14 from "node:path";
7738
- import { parseDocument, isSeq, YAMLSeq } from "yaml";
7739
- function resolveManifestPath(projectDir) {
7740
- const existing = MANIFEST_FILENAMES.find((name) => existsSync11(path14.join(projectDir, name)));
7741
- return path14.join(projectDir, existing ?? MANIFEST_FILENAMES[0]);
7742
- }
7743
- async function loadManifestDoc(projectDir) {
7744
- const file = resolveManifestPath(projectDir);
7745
- let text = "";
7746
- try {
7747
- text = await readFile22(file, "utf8");
7748
- } catch (error2) {
7749
- if (error2.code !== "ENOENT")
7750
- throw error2;
7751
- }
7752
- return { doc: parseDocument(text), file };
7753
- }
7754
- async function writeManifestDoc(file, doc) {
7755
- const dir = path14.dirname(file);
7756
- await mkdir7(dir, { recursive: true });
7757
- const temporary = path14.join(dir, `.${path14.basename(file)}.tmp-${process.pid}-${randomUUID()}`);
7758
- try {
7759
- const handle = await open(temporary, "wx", 438);
7760
- try {
7761
- await handle.writeFile(doc.toString(), "utf8");
7762
- await handle.sync();
7763
- } finally {
7764
- await handle.close();
7765
- }
7766
- await rename(temporary, file);
7767
- const directory = await open(dir, "r").catch(() => null);
7768
- if (directory) {
7769
- await directory.sync().catch(() => {});
7770
- await directory.close();
7771
- }
7772
- } finally {
7773
- await rm2(temporary, { force: true });
7774
- }
7775
- }
7776
- function ensureSeq(doc, key) {
7777
- let seq = doc.get(key, true);
7778
- if (!isSeq(seq)) {
7779
- const fresh = new YAMLSeq(doc.schema);
7780
- doc.set(key, fresh);
7781
- seq = fresh;
7782
- }
7783
- return seq;
7784
- }
8435
+ import { isSeq as isSeq3, isMap as isMap2, isScalar as isScalar2, YAMLMap, Scalar } from "yaml";
7785
8436
 
7786
8437
  // src/lib/npm-plugin-installer.ts
7787
- import { createHash as createHash4, randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
8438
+ import { createHash as createHash4, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
7788
8439
  import { createReadStream as createReadStream2 } from "node:fs";
7789
8440
  import {
7790
8441
  lstat as lstat2,
@@ -8460,7 +9111,7 @@ async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
8460
9111
  }
8461
9112
  try {
8462
9113
  await lstat2(finalRoot);
8463
- backupRoot = `${finalRoot}.backup-${randomUUID2()}`;
9114
+ backupRoot = `${finalRoot}.backup-${randomUUID3()}`;
8464
9115
  await rename2(finalRoot, backupRoot);
8465
9116
  } catch (error2) {
8466
9117
  if (error2.code !== "ENOENT")
@@ -8549,9 +9200,9 @@ function isLocalRef2(ref) {
8549
9200
  return ref.startsWith("./") || ref.startsWith("../") || ref.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(ref) || /\.(m?js|cjs|ts)$/i.test(ref) || !ref.startsWith("@") && ref.includes("/");
8550
9201
  }
8551
9202
  function itemRef(item) {
8552
- if (isScalar(item))
9203
+ if (isScalar2(item))
8553
9204
  return typeof item.value === "string" ? item.value : null;
8554
- if (isMap(item)) {
9205
+ if (isMap2(item)) {
8555
9206
  const p = item.get("path");
8556
9207
  const n = item.get("name");
8557
9208
  if (typeof p === "string")
@@ -8567,7 +9218,7 @@ function refKind(ref) {
8567
9218
  async function listProjectPlugins(projectDir) {
8568
9219
  const { doc } = await loadManifestDoc(projectDir);
8569
9220
  const seq = doc.get("plugins", true);
8570
- if (!isSeq2(seq))
9221
+ if (!isSeq3(seq))
8571
9222
  return [];
8572
9223
  const out = [];
8573
9224
  for (const item of seq.items) {
@@ -8578,7 +9229,7 @@ async function listProjectPlugins(projectDir) {
8578
9229
  let kind;
8579
9230
  let version;
8580
9231
  let exportName;
8581
- if (isMap(item)) {
9232
+ if (isMap2(item)) {
8582
9233
  const e = item.get("enabled");
8583
9234
  if (e === false)
8584
9235
  enabled = false;
@@ -8623,7 +9274,7 @@ async function recordInstalledNpmPlugin(projectDir, name, version, exportName) {
8623
9274
  const seq = ensureSeq(doc, "plugins");
8624
9275
  const idx = indexOfRef(seq, name);
8625
9276
  let map;
8626
- if (idx >= 0 && isMap(seq.items[idx])) {
9277
+ if (idx >= 0 && isMap2(seq.items[idx])) {
8627
9278
  map = seq.items[idx];
8628
9279
  } else {
8629
9280
  map = new YAMLMap(doc.schema);
@@ -9955,8 +10606,8 @@ var drivethrurpgProvider = {
9955
10606
  }
9956
10607
  issues.push({
9957
10608
  severity: "info",
9958
- id: "drivethrurpg/validate-profile",
9959
- message: "Tip: run `gutterpress validate --profile dtrpg` for DriveThruRPG's print-compliance checks before uploading."
10609
+ id: "drivethrurpg/validate-target",
10610
+ message: "Tip: run `gutterpress validate --target dtrpg` for DriveThruRPG's print-compliance checks before uploading."
9960
10611
  });
9961
10612
  return issues;
9962
10613
  },
@@ -10070,7 +10721,7 @@ var info4 = {
10070
10721
  label: "itch.io",
10071
10722
  kind: "api",
10072
10723
  format: "pdf",
10073
- description: "Upload the PDF to your itch.io project page (games, TTRPG supplements, zines).",
10724
+ description: "Upload the PDF to your itch.io project page (games, zines, and other printable projects).",
10074
10725
  configFields: [
10075
10726
  { key: "target", label: "Project (user/game)", placeholder: "you/your-book" },
10076
10727
  { key: "channel", label: "Channel", placeholder: "pdf" }
@@ -12253,7 +12904,7 @@ async function dispatchToHandler(kind, ctx, error2) {
12253
12904
  return result;
12254
12905
  }
12255
12906
  // src/lib/theme-manager.ts
12256
- import { isSeq as isSeq3, Scalar as Scalar2 } from "yaml";
12907
+ import { isSeq as isSeq4, Scalar as Scalar2 } from "yaml";
12257
12908
  var THEMES_DIR = "themes";
12258
12909
  var THEME_HREF_RE = new RegExp(`^${THEMES_DIR}/([^/]+)/theme\\.css$`);
12259
12910
  // src/lib/theme-import.ts
@@ -12277,7 +12928,7 @@ var ALLOWED_ASSET_EXTS = new Set([
12277
12928
  ".css"
12278
12929
  ]);
12279
12930
  // src/lib/manifest-config.ts
12280
- import { isSeq as isSeq4, isMap as isMap2, YAMLSeq as YAMLSeq3, Scalar as Scalar3 } from "yaml";
12931
+ import { isSeq as isSeq5, isMap as isMap3, YAMLSeq as YAMLSeq3, Scalar as Scalar3 } from "yaml";
12281
12932
  // src/lib/host-policy.ts
12282
12933
  var AUTO_SNAPSHOT_MAX_MINUTES = 24 * 60;
12283
12934
  var AUTO_SYNC_MAX_MINUTES = 24 * 60;
@@ -12374,5 +13025,5 @@ function normalizeProjectKey(projectDir) {
12374
13025
  return projectDir.replace(/[\\/]+$/, "");
12375
13026
  }
12376
13027
  // src/lib/publish/manifest-publish.ts
12377
- import { isMap as isMap3 } from "yaml";
12378
- export { MANIFEST_FILENAMES, parseNpmPluginSpec, runLint, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, scaffoldProject, BUILT_IN_TEMPLATE_IDS, addNpmPlugin, isAppHeartbeatFresh, classifyFromHealth, classifyGitError, makeManualGuidance, inspectRepo, verifyRepoReadable, isUnbornRepo, publishCredentialKey, publishConnectionStatus, listPublishAccounts, listPublishProviders, publishProviderFor, runPublish, connectPublishProvider, buildRecoveryContext, recover16 as recover };
13028
+ import { isMap as isMap4 } from "yaml";
13029
+ export { PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, parseNpmPluginSpec, runLint, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, scaffoldProject, BUILT_IN_TEMPLATE_IDS, addNpmPlugin, isAppHeartbeatFresh, classifyFromHealth, classifyGitError, makeManualGuidance, inspectRepo, verifyRepoReadable, isUnbornRepo, publishCredentialKey, publishConnectionStatus, listPublishAccounts, listPublishProviders, publishProviderFor, runPublish, connectPublishProvider, buildRecoveryContext, recover16 as recover };