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
@@ -11,11 +11,11 @@ import {
11
11
  snapshotWorkingTreeUnlocked,
12
12
  stageChanges,
13
13
  withRepoLock
14
- } from "./index-5bxx0c4y.js";
14
+ } from "./index-yzrh708h.js";
15
15
  import {
16
16
  detectProjectSource,
17
17
  syncRemoteFor
18
- } from "./index-1tyj4bk3.js";
18
+ } from "./index-wchtvxvw.js";
19
19
  import {
20
20
  defaultGitHttp
21
21
  } from "./index-yja077f6.js";
@@ -84,7 +84,9 @@ function resolvePort(raw) {
84
84
  }
85
85
 
86
86
  // src/lib/presets.ts
87
+ var PRESET_IDS = ["dtrpg", "book", "custom"];
87
88
  var DTRPG_PRESET = {
89
+ defaultTargets: ["dtrpg"],
88
90
  plugins: [],
89
91
  source: {
90
92
  files: null
@@ -156,6 +158,7 @@ var DTRPG_PRESET = {
156
158
  }
157
159
  };
158
160
  var BOOK_PRESET = {
161
+ defaultTargets: [],
159
162
  plugins: [],
160
163
  source: {
161
164
  files: null
@@ -220,9 +223,15 @@ var BOOK_PRESET = {
220
223
  }
221
224
  }
222
225
  };
226
+ var CUSTOM_PRESET = {
227
+ ...BOOK_PRESET,
228
+ page: null,
229
+ defaultTargets: []
230
+ };
223
231
  var PRESETS = {
224
232
  dtrpg: DTRPG_PRESET,
225
- book: BOOK_PRESET
233
+ book: BOOK_PRESET,
234
+ custom: CUSTOM_PRESET
226
235
  };
227
236
  var warnedIds = new Set;
228
237
  function warnOnce(id, message, sink = defaultWarnSink) {
@@ -236,7 +245,7 @@ function defaultWarnSink(message) {
236
245
  }
237
246
  function resolvePreset(presetName) {
238
247
  if (presetName === undefined) {
239
- 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.");
248
+ 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.");
240
249
  return DTRPG_PRESET;
241
250
  }
242
251
  const preset = PRESETS[presetName];
@@ -246,6 +255,121 @@ function resolvePreset(presetName) {
246
255
  return preset;
247
256
  }
248
257
 
258
+ // src/lib/targets.ts
259
+ var DTRPG_TARGET = {
260
+ id: "dtrpg",
261
+ label: "DriveThruRPG",
262
+ overlay: {
263
+ pdfx: { flavor: DTRPG_PRESET.pdfx.flavor },
264
+ ink: {
265
+ maxTac: DTRPG_PRESET.ink.maxTac,
266
+ tacTolerance: DTRPG_PRESET.ink.tacTolerance
267
+ },
268
+ validate: {
269
+ checks: {
270
+ "pdf.structure.qpdf": { enabled: true, severity: "error" },
271
+ "pdf.print.pdfx-markers": { enabled: true, severity: "error" },
272
+ "pdf.print.pdfx-metadata": { enabled: true, severity: "error" },
273
+ "pdf.print.embedded-fonts": { enabled: true, severity: "error" }
274
+ },
275
+ assets: {
276
+ allowedColorSpaces: [...DTRPG_PRESET.validate.assets.allowedColorSpaces]
277
+ },
278
+ pdf: {
279
+ forbidTransparency: DTRPG_PRESET.validate.pdf.forbidTransparency,
280
+ minImageResolution: DTRPG_PRESET.validate.pdf.minImageResolution
281
+ }
282
+ }
283
+ },
284
+ requiredChecks: [
285
+ "pdf.structure.qpdf",
286
+ "pdf.print.pdfx-markers",
287
+ "pdf.print.pdfx-metadata",
288
+ "pdf.print.embedded-fonts"
289
+ ],
290
+ requiredTools: ["qpdf", "gs"]
291
+ };
292
+ var ITCH_TARGET = {
293
+ id: "itch",
294
+ label: "itch.io",
295
+ overlay: {
296
+ validate: {
297
+ checks: {
298
+ "pdf.structure.qpdf": { enabled: true, severity: "error" },
299
+ "pdf.print.embedded-fonts": { enabled: true, severity: "error" },
300
+ "pdf.print.pdfx-markers": false,
301
+ "pdf.print.pdfx-metadata": false,
302
+ "pdf.print.ink-coverage": false,
303
+ "asset.image.tac-raster": false
304
+ },
305
+ assets: {
306
+ allowedColorSpaces: ["RGB", "CMYK", "Grayscale"],
307
+ allowAlpha: true
308
+ },
309
+ pdf: {
310
+ forbidTransparency: false,
311
+ requireBleed: false
312
+ }
313
+ }
314
+ },
315
+ requiredChecks: ["pdf.structure.qpdf", "pdf.print.embedded-fonts"],
316
+ requiredTools: []
317
+ };
318
+ var TARGETS = {
319
+ dtrpg: DTRPG_TARGET,
320
+ itch: ITCH_TARGET
321
+ };
322
+ var TARGET_IDS = Object.keys(TARGETS);
323
+ function publishTargetFor(id) {
324
+ const target = TARGETS[id];
325
+ if (!target) {
326
+ throw new UsageError(`Unknown publish target "${id}". Known targets: ${TARGET_IDS.join(", ")}.`);
327
+ }
328
+ return target;
329
+ }
330
+ function resolveTargets(requested, fallback) {
331
+ const ids = requested ?? fallback;
332
+ const seen = new Set;
333
+ const out = [];
334
+ for (const id of ids) {
335
+ publishTargetFor(id);
336
+ if (!seen.has(id)) {
337
+ seen.add(id);
338
+ out.push(id);
339
+ }
340
+ }
341
+ return out;
342
+ }
343
+ function overlayPreset(preset, target) {
344
+ const o = target.overlay;
345
+ return {
346
+ ...preset,
347
+ pdfx: { ...preset.pdfx, ...o.pdfx },
348
+ ink: { ...preset.ink, ...o.ink },
349
+ validate: {
350
+ ...preset.validate,
351
+ checks: { ...preset.validate.checks, ...o.validate?.checks },
352
+ assets: { ...preset.validate.assets, ...o.validate?.assets },
353
+ pdf: { ...preset.validate.pdf, ...o.validate?.pdf }
354
+ }
355
+ };
356
+ }
357
+ var DEFAULT_STRICT_PDF_CHECKS = [
358
+ "pdf.structure.qpdf",
359
+ "pdf.print.pdfx-markers",
360
+ "pdf.print.pdfx-metadata",
361
+ "pdf.print.embedded-fonts"
362
+ ];
363
+ function applyDefaultPdfStrictChecks(config) {
364
+ const next = structuredClone(config);
365
+ for (const checkId of DEFAULT_STRICT_PDF_CHECKS) {
366
+ if (next.validate.checks[checkId] === undefined) {
367
+ next.validate.checks[checkId] = { enabled: true, severity: "error" };
368
+ }
369
+ }
370
+ return next;
371
+ }
372
+
249
373
  // src/lib/manifest.ts
250
374
  var MANIFEST_FILENAMES = [
251
375
  "manifest.yaml"
@@ -333,8 +457,34 @@ function mergeShape(cli, manifest, preset) {
333
457
  return out;
334
458
  }
335
459
  function resolveConfig(cliOverrides, manifest) {
460
+ const presetName = cliOverrides.preset ?? manifest.preset;
461
+ return resolveWithPreset(cliOverrides, manifest, resolvePreset(presetName));
462
+ }
463
+ function resolveConfigForTarget(cliOverrides, manifest, targetId) {
336
464
  const presetName = cliOverrides.preset ?? manifest.preset;
337
465
  const preset = resolvePreset(presetName);
466
+ return resolveWithPreset(cliOverrides, manifest, overlayPreset(preset, publishTargetFor(targetId)));
467
+ }
468
+ function resolveCustomPage(c, m) {
469
+ const width = c.page?.width ?? m.page?.width;
470
+ const height = c.page?.height ?? m.page?.height;
471
+ const missing = [
472
+ width === undefined ? "page.width" : null,
473
+ height === undefined ? "page.height" : null
474
+ ].filter((f) => f !== null);
475
+ if (missing.length > 0) {
476
+ throw new UsageError(`preset: custom requires ${missing.join(" and ")} in manifest.yaml ` + `(points; 72pt = 1in). Example for US Letter:
477
+ ` + ` page:
478
+ width: 612
479
+ height: 792`);
480
+ }
481
+ return {
482
+ width,
483
+ height,
484
+ tolerance: c.page?.tolerance ?? m.page?.tolerance ?? 0.5
485
+ };
486
+ }
487
+ function resolveWithPreset(cliOverrides, manifest, preset) {
338
488
  const m = manifest;
339
489
  const c = cliOverrides;
340
490
  const legacy = m;
@@ -356,9 +506,10 @@ function resolveConfig(cliOverrides, manifest) {
356
506
  authors: c.authors ?? m.authors ?? [],
357
507
  styles: c.styles ?? m.styles,
358
508
  plugins,
509
+ targets: resolveTargets(c.targets ?? m.targets, preset.defaultTargets),
359
510
  source: mergeShape(c.source, m.source, preset.source),
360
511
  pdfx: mergeShape(c.pdfx, m.pdfx, preset.pdfx),
361
- page: mergeShape(c.page, m.page, preset.page),
512
+ page: preset.page ? mergeShape(c.page, m.page, preset.page) : resolveCustomPage(c, m),
362
513
  ink: mergeShape(c.ink, m.ink, preset.ink),
363
514
  lint: mergeShape(c.lint, m.lint, preset.lint),
364
515
  validate: {
@@ -1013,17 +1164,6 @@ async function assembleBookHtml(opts) {
1013
1164
  throw new Error("No markdown files to render");
1014
1165
  }
1015
1166
  const md = createMarkdownRenderer(opts.plugins);
1016
- let sourceChapter = null;
1017
- if (opts.wrapChapters) {
1018
- const renderToken = md.renderer.renderToken.bind(md.renderer);
1019
- md.renderer.renderToken = (tokens, idx, options) => {
1020
- const token = tokens[idx];
1021
- if (sourceChapter !== null && token.level === 0 && token.map !== null && token.type.endsWith("_open")) {
1022
- token.attrSet("data-chapter-src", sourceChapter);
1023
- }
1024
- return renderToken(tokens, idx, options);
1025
- };
1026
- }
1027
1167
  let bodyContent = "";
1028
1168
  const imageRefs = new Set;
1029
1169
  for (const file of files) {
@@ -1036,20 +1176,22 @@ async function assembleBookHtml(opts) {
1036
1176
  throw new Error(`Failed to read file ${file}: ${errorMsg}`);
1037
1177
  }
1038
1178
  const env = {};
1039
- sourceChapter = opts.wrapChapters ? chapterId : null;
1040
- let rendered;
1041
- try {
1042
- rendered = md.render(content, env);
1043
- } finally {
1044
- sourceChapter = null;
1045
- }
1179
+ const rendered = md.render(content, env);
1046
1180
  if (env.layoutWarnings && env.layoutWarnings.length > 0) {
1047
1181
  opts.onChapterWarnings?.(chapterId, env.layoutWarnings);
1048
1182
  }
1049
1183
  for (const ref of env.imageRefs ?? [])
1050
1184
  imageRefs.add(ref);
1051
- bodyContent += rendered + `
1185
+ if (opts.wrapChapters) {
1186
+ const safe = chapterId.replace(/&/g, "&").replace(/"/g, """);
1187
+ bodyContent += `<div class="gutterpress-chapter" data-chapter-src="${safe}">
1188
+ ${rendered}
1189
+ </div>
1190
+ `;
1191
+ } else {
1192
+ bodyContent += rendered + `
1052
1193
  `;
1194
+ }
1053
1195
  }
1054
1196
  if (opts.onImageRefs) {
1055
1197
  for (const ref of collectHtmlImageRefs(bodyContent))
@@ -1153,11 +1295,24 @@ async function resolveActiveStyles(projectDir, manifestStyles) {
1153
1295
  }
1154
1296
  return [];
1155
1297
  }
1156
- async function listProjectStyles(projectDir) {
1157
- const [{ manifest }, discoveredAbs] = await Promise.all([
1298
+ var SHARED_STYLE_ROOTS = ["shared", "."];
1299
+ async function discoverSharedCssFiles(repoRoot) {
1300
+ const found = new Set;
1301
+ for (const rel of SHARED_STYLE_ROOTS) {
1302
+ const base = path.resolve(repoRoot, rel);
1303
+ for (const abs of await discoverCssFiles(base))
1304
+ found.add(abs);
1305
+ }
1306
+ return [...found];
1307
+ }
1308
+ async function listProjectStyles(projectDir, opts = {}) {
1309
+ const repoRoot = opts.repoRoot && path.resolve(opts.repoRoot) !== path.resolve(projectDir) ? path.resolve(opts.repoRoot) : null;
1310
+ const [{ manifest }, ownCss, sharedCss] = await Promise.all([
1158
1311
  loadManifestWithPath(projectDir),
1159
- discoverCssFiles(projectDir)
1312
+ discoverCssFiles(projectDir),
1313
+ repoRoot ? discoverSharedCssFiles(repoRoot) : Promise.resolve([])
1160
1314
  ]);
1315
+ const discoveredAbs = [...new Set([...ownCss, ...sharedCss])];
1161
1316
  const activeRels = await resolveActiveStyles(projectDir, Array.isArray(manifest.styles) ? manifest.styles : undefined);
1162
1317
  const out = [];
1163
1318
  const seen = new Set;
@@ -1216,6 +1371,22 @@ function decodeRef(ref) {
1216
1371
  function toPosix(p) {
1217
1372
  return p.split(path2.sep).join("/");
1218
1373
  }
1374
+ function escapesProjectRoot(projectDir, absPath) {
1375
+ const rel = path2.relative(projectDir, absPath);
1376
+ return rel === ".." || rel.startsWith(`..${path2.sep}`) || path2.isAbsolute(rel);
1377
+ }
1378
+ function proseImageRefError(ref, projectDir) {
1379
+ const cleaned = stripUrlSuffix(decodeRef(ref));
1380
+ if (path2.isAbsolute(cleaned)) {
1381
+ return `Image reference must be relative to the project: ${ref}
1382
+ ` + ` Copy the file into your project folder and reference it from there.`;
1383
+ }
1384
+ if (escapesProjectRoot(projectDir, path2.resolve(projectDir, cleaned))) {
1385
+ return `Image reference points outside the project: ${ref}
1386
+ ` + ` Copy the file into your project folder and reference it from there.`;
1387
+ }
1388
+ return null;
1389
+ }
1219
1390
  function mimeFor(ext) {
1220
1391
  return MIME_BY_EXT[ext] ?? "application/octet-stream";
1221
1392
  }
@@ -1318,7 +1489,7 @@ async function inlineOne(cssPath, projectDir, copies, warnings, seen) {
1318
1489
  return dataUri(bytes, ext);
1319
1490
  }
1320
1491
  const projectRel = path2.relative(projectDir, absAsset);
1321
- const dest = projectRel && !projectRel.startsWith("..") && !path2.isAbsolute(projectRel) ? toPosix(projectRel) : `${HASHED_ASSET_DIR}/${contentHash(bytes)}${ext}`;
1492
+ const dest = projectRel && !escapesProjectRoot(projectDir, absAsset) ? toPosix(projectRel) : `${HASHED_ASSET_DIR}/${contentHash(bytes)}${ext}`;
1322
1493
  copies.set(dest, { from: absAsset, to: dest });
1323
1494
  return dest;
1324
1495
  });
@@ -1409,19 +1580,14 @@ async function planImageCopies(projectDir, refs) {
1409
1580
  for (const ref of refs) {
1410
1581
  if (isNonFileUrl(ref))
1411
1582
  continue;
1412
- const cleaned = stripUrlSuffix(decodeRef(ref));
1413
- if (path2.isAbsolute(cleaned)) {
1414
- errors.push(`Image reference must be relative to the project: ${ref}
1415
- ` + ` Copy the file into your project folder and reference it from there.`);
1583
+ const refError = proseImageRefError(ref, projectDir);
1584
+ if (refError) {
1585
+ errors.push(refError);
1416
1586
  continue;
1417
1587
  }
1588
+ const cleaned = stripUrlSuffix(decodeRef(ref));
1418
1589
  const abs = path2.resolve(projectDir, cleaned);
1419
1590
  const rel = path2.relative(projectDir, abs);
1420
- if (rel.startsWith("..")) {
1421
- errors.push(`Image reference points outside the project: ${ref}
1422
- ` + ` Copy the file into your project folder and reference it from there.`);
1423
- continue;
1424
- }
1425
1591
  const dest = toPosix(rel);
1426
1592
  copies.set(dest, { from: abs, to: dest });
1427
1593
  }
@@ -2616,7 +2782,7 @@ function shadowPathFor(pluginPath, mtimeMs) {
2616
2782
  const ext = extname(pluginPath);
2617
2783
  const stem = basename(pluginPath, ext);
2618
2784
  const token = String(mtimeMs).replace(/\./g, "-");
2619
- return join2(dirname2(pluginPath), `.${stem}.gutterpress-reload-${token}${ext}`);
2785
+ return join2(dirname2(pluginPath), `.${stem}.gutterpress-reload-${process.pid}-${token}${ext}`);
2620
2786
  }
2621
2787
  async function loadCachedPathPluginModule(pluginPath) {
2622
2788
  const mtimeMs = statSync2(pluginPath).mtimeMs;
@@ -3212,7 +3378,7 @@ import git from "isomorphic-git";
3212
3378
  // package.json
3213
3379
  var package_default = {
3214
3380
  name: "gutterpress",
3215
- version: "0.0.0",
3381
+ version: "0.9.0-alpha.2",
3216
3382
  description: "Markdown-to-PDF converter for professional print layout using Paged.js and Ghostscript.",
3217
3383
  author: "itlackey",
3218
3384
  license: "MPL-2.0",
@@ -3224,7 +3390,7 @@ var package_default = {
3224
3390
  "ghostscript",
3225
3391
  "book",
3226
3392
  "publishing",
3227
- "ttrpg"
3393
+ "zine"
3228
3394
  ],
3229
3395
  repository: {
3230
3396
  type: "git",
@@ -3452,7 +3618,7 @@ async function writeBuildFingerprint(input) {
3452
3618
  iccPath: input.pdfx.iccPath,
3453
3619
  stripAnnotations: input.pdfx.stripAnnotations
3454
3620
  },
3455
- outputDir
3621
+ outputDir: input.recordedOutputDir ? path5.resolve(input.recordedOutputDir) : outputDir
3456
3622
  },
3457
3623
  sourceRevision,
3458
3624
  tools
@@ -3473,16 +3639,16 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
3473
3639
  var favicon_default = "./favicon-wkbm9cjn.ico";
3474
3640
 
3475
3641
  // src/assets/manifest.schema.json
3476
- var manifest_schema_default = "./manifest.schema-rn1dpdpn.json";
3642
+ var manifest_schema_default = "./manifest.schema-z61rzw44.json";
3477
3643
 
3478
3644
  // src/assets/preview/scripts/pagedjs-interface.js
3479
- var pagedjs_interface_default = "./pagedjs-interface-80g3xa8p.js";
3645
+ var pagedjs_interface_default = "./pagedjs-interface-qxvzgwd7.js";
3480
3646
 
3481
3647
  // src/assets/preview/scripts/pagedjs-bridge.js
3482
3648
  var pagedjs_bridge_default = "./pagedjs-bridge-vn4hk9fx.js";
3483
3649
 
3484
3650
  // src/assets/preview/scripts/preview-shell.js
3485
- var preview_shell_default = "./preview-shell-rghtfmc8.js";
3651
+ var preview_shell_default = "./preview-shell-6dqexx1m.js";
3486
3652
 
3487
3653
  // src/assets/vendor/paged.polyfill.js
3488
3654
  var paged_polyfill_default = "./paged.polyfill-n95pbxfn.js";
@@ -3496,23 +3662,17 @@ var manifest_default = "./manifest-n1gh3qw5.yaml";
3496
3662
  // src/assets/templates/book/chapter-01.md
3497
3663
  var chapter_01_default = "./chapter-01-rkpyvjna.md";
3498
3664
 
3499
- // src/assets/templates/ttrpg/manifest.yaml
3500
- var manifest_default2 = "./manifest-ryyg71nf.yaml";
3501
-
3502
- // src/assets/templates/ttrpg/chapter-01.md
3503
- var chapter_01_default2 = "./chapter-01-jj8ftn4c.md";
3504
-
3505
3665
  // src/assets/templates/zine/manifest.yaml
3506
- var manifest_default3 = "./manifest-n1gh3qw5.yaml";
3666
+ var manifest_default2 = "./manifest-n1gh3qw5.yaml";
3507
3667
 
3508
3668
  // src/assets/templates/zine/chapter-01.md
3509
- var chapter_01_default3 = "./chapter-01-7jwqg1ct.md";
3669
+ var chapter_01_default2 = "./chapter-01-7jwqg1ct.md";
3510
3670
 
3511
3671
  // src/assets/templates/technical/manifest.yaml
3512
- var manifest_default4 = "./manifest-n1gh3qw5.yaml";
3672
+ var manifest_default3 = "./manifest-n1gh3qw5.yaml";
3513
3673
 
3514
3674
  // src/assets/templates/technical/chapter-01.md
3515
- var chapter_01_default4 = "./chapter-01-6fn9pvd0.md";
3675
+ var chapter_01_default3 = "./chapter-01-6fn9pvd0.md";
3516
3676
 
3517
3677
  // src/assets/themes/clean-book/theme.css
3518
3678
  var theme_default = "./theme-570zmh2t.css";
@@ -3520,23 +3680,17 @@ var theme_default = "./theme-570zmh2t.css";
3520
3680
  // src/assets/themes/clean-book/theme.json
3521
3681
  var theme_default2 = "./theme-xt0hjb1b.json";
3522
3682
 
3523
- // src/assets/themes/ttrpg-supplement/theme.css
3524
- var theme_default3 = "./theme-0yzvthe8.css";
3525
-
3526
- // src/assets/themes/ttrpg-supplement/theme.json
3527
- var theme_default4 = "./theme-6a2fngc8.json";
3528
-
3529
3683
  // src/assets/themes/zine/theme.css
3530
- var theme_default5 = "./theme-zz2ktzqs.css";
3684
+ var theme_default3 = "./theme-zz2ktzqs.css";
3531
3685
 
3532
3686
  // src/assets/themes/zine/theme.json
3533
- var theme_default6 = "./theme-cd6efzjw.json";
3687
+ var theme_default4 = "./theme-cd6efzjw.json";
3534
3688
 
3535
3689
  // src/assets/themes/technical-doc/theme.css
3536
- var theme_default7 = "./theme-nya4nqh6.css";
3690
+ var theme_default5 = "./theme-nya4nqh6.css";
3537
3691
 
3538
3692
  // src/assets/themes/technical-doc/theme.json
3539
- var theme_default8 = "./theme-tqfypc1w.json";
3693
+ var theme_default6 = "./theme-tqfypc1w.json";
3540
3694
 
3541
3695
  // src/lib/embedded-assets.ts
3542
3696
  var __libdir = dirname3(fileURLToPath2(import.meta.url));
@@ -3552,20 +3706,16 @@ var EMBEDDED_ASSETS = {
3552
3706
  "profiles/CGATS21_CRPC1.icc": abs(filePath(CGATS21_CRPC1_default)),
3553
3707
  "templates/book/manifest.yaml": abs(filePath(manifest_default)),
3554
3708
  "templates/book/chapter-01.md": abs(filePath(chapter_01_default)),
3555
- "templates/ttrpg/manifest.yaml": abs(filePath(manifest_default2)),
3556
- "templates/ttrpg/chapter-01.md": abs(filePath(chapter_01_default2)),
3557
- "templates/zine/manifest.yaml": abs(filePath(manifest_default3)),
3558
- "templates/zine/chapter-01.md": abs(filePath(chapter_01_default3)),
3559
- "templates/technical/manifest.yaml": abs(filePath(manifest_default4)),
3560
- "templates/technical/chapter-01.md": abs(filePath(chapter_01_default4)),
3709
+ "templates/zine/manifest.yaml": abs(filePath(manifest_default2)),
3710
+ "templates/zine/chapter-01.md": abs(filePath(chapter_01_default2)),
3711
+ "templates/technical/manifest.yaml": abs(filePath(manifest_default3)),
3712
+ "templates/technical/chapter-01.md": abs(filePath(chapter_01_default3)),
3561
3713
  "themes/clean-book/theme.css": abs(filePath(theme_default)),
3562
3714
  "themes/clean-book/theme.json": abs(filePath(theme_default2)),
3563
- "themes/ttrpg-supplement/theme.css": abs(filePath(theme_default3)),
3564
- "themes/ttrpg-supplement/theme.json": abs(filePath(theme_default4)),
3565
- "themes/zine/theme.css": abs(filePath(theme_default5)),
3566
- "themes/zine/theme.json": abs(filePath(theme_default6)),
3567
- "themes/technical-doc/theme.css": abs(filePath(theme_default7)),
3568
- "themes/technical-doc/theme.json": abs(filePath(theme_default8))
3715
+ "themes/zine/theme.css": abs(filePath(theme_default3)),
3716
+ "themes/zine/theme.json": abs(filePath(theme_default4)),
3717
+ "themes/technical-doc/theme.css": abs(filePath(theme_default5)),
3718
+ "themes/technical-doc/theme.json": abs(filePath(theme_default6))
3569
3719
  };
3570
3720
  var extractPromise = null;
3571
3721
  async function extractAssets() {
@@ -3797,7 +3947,7 @@ async function runLint(opts = {}) {
3797
3947
 
3798
3948
  // src/lib/validation-exec.ts
3799
3949
  import { existsSync as existsSync11 } from "node:fs";
3800
- import { join as join6, resolve as resolve9 } from "node:path";
3950
+ import { dirname as dirname5, join as join6, resolve as resolve9 } from "node:path";
3801
3951
 
3802
3952
  // src/checks/formatter.ts
3803
3953
  function formatReport(report, format = "text") {
@@ -3809,19 +3959,19 @@ function formatReport(report, format = "text") {
3809
3959
  }
3810
3960
  function formatText(report) {
3811
3961
  for (const r of report.infos) {
3812
- const loc = formatLocation(r.file, r.line, r.column);
3962
+ const loc = targetPrefix(r.target) + formatLocation(r.file, r.line, r.column);
3813
3963
  log.info(`${loc}${r.message}`);
3814
3964
  if (r.detail)
3815
3965
  log.info(` ${r.detail}`);
3816
3966
  }
3817
3967
  for (const r of report.warnings) {
3818
- const loc = formatLocation(r.file, r.line, r.column);
3968
+ const loc = targetPrefix(r.target) + formatLocation(r.file, r.line, r.column);
3819
3969
  log.warn(`${loc}${r.message}`);
3820
3970
  if (r.detail)
3821
3971
  log.warn(` ${r.detail}`);
3822
3972
  }
3823
3973
  for (const r of report.errors) {
3824
- const loc = formatLocation(r.file, r.line, r.column);
3974
+ const loc = targetPrefix(r.target) + formatLocation(r.file, r.line, r.column);
3825
3975
  log.error(`${loc}${r.message}`);
3826
3976
  if (r.detail)
3827
3977
  log.error(` ${r.detail}`);
@@ -3835,6 +3985,9 @@ function formatText(report) {
3835
3985
  log.success("VALIDATION PASSED");
3836
3986
  }
3837
3987
  }
3988
+ function targetPrefix(target) {
3989
+ return target ? `[${target}] ` : "";
3990
+ }
3838
3991
  function formatJson(report) {
3839
3992
  console.log(JSON.stringify({
3840
3993
  results: report.results,
@@ -5239,6 +5392,16 @@ var check19 = {
5239
5392
  if (inFence)
5240
5393
  continue;
5241
5394
  for (const { ref, kind } of extractLocalRefs(line)) {
5395
+ const escape = kind === "image" ? proseImageRefError(ref, ctx.inputDir) : null;
5396
+ if (escape) {
5397
+ results.push(finding(check19.id, {
5398
+ severity: "error",
5399
+ message: escape,
5400
+ file,
5401
+ line: i + 1
5402
+ }));
5403
+ continue;
5404
+ }
5242
5405
  if (localRefExists(ref, kind, file, ctx.inputDir))
5243
5406
  continue;
5244
5407
  results.push(finding(check19.id, {
@@ -6301,58 +6464,46 @@ function reportMissingTools(result) {
6301
6464
  }
6302
6465
  }
6303
6466
 
6304
- // src/lib/validation-profile.ts
6305
- var DTRPG_STRICT_PDF_CHECKS = [
6306
- "pdf.structure.qpdf",
6307
- "pdf.print.pdfx-markers",
6308
- "pdf.print.pdfx-metadata",
6309
- "pdf.print.embedded-fonts"
6310
- ];
6311
- function enforceStrictPdfChecks(config, overwrite = false) {
6312
- for (const checkId of DTRPG_STRICT_PDF_CHECKS) {
6313
- if (overwrite || config.validate.checks[checkId] === undefined) {
6314
- config.validate.checks[checkId] = {
6315
- enabled: true,
6316
- severity: "error"
6317
- };
6318
- }
6467
+ // src/lib/validation-exec.ts
6468
+ var SHIPPED_ASSET_EXTS = new Set([
6469
+ ".woff2",
6470
+ ".woff",
6471
+ ".ttf",
6472
+ ".otf",
6473
+ ".png",
6474
+ ".jpg",
6475
+ ".jpeg",
6476
+ ".gif",
6477
+ ".webp",
6478
+ ".avif",
6479
+ ".svg",
6480
+ ".tif",
6481
+ ".tiff"
6482
+ ]);
6483
+ async function sharedAssetDirs(manifestDir, relStyles, inputDir) {
6484
+ let closure;
6485
+ try {
6486
+ closure = await collectStyleDependencies(manifestDir, relStyles);
6487
+ } catch {
6488
+ return [];
6319
6489
  }
6320
- }
6321
- function applyDefaultPdfStrictChecks(config) {
6322
- const next = structuredClone(config);
6323
- enforceStrictPdfChecks(next, false);
6324
- return next;
6325
- }
6326
- var applyDtrpgPdfDefaults = applyDefaultPdfStrictChecks;
6327
- function applyValidationProfile(config, profile) {
6328
- const next = structuredClone(config);
6329
- if (profile === "dtrpg") {
6330
- next.pdfx.flavor = DTRPG_PRESET.pdfx.flavor;
6331
- next.ink.maxTac = DTRPG_PRESET.ink.maxTac;
6332
- next.ink.tacTolerance = DTRPG_PRESET.ink.tacTolerance;
6333
- next.validate.pdf.forbidTransparency = DTRPG_PRESET.validate.pdf.forbidTransparency;
6334
- next.validate.pdf.minImageResolution = DTRPG_PRESET.validate.pdf.minImageResolution;
6335
- next.validate.assets.allowedColorSpaces = [
6336
- ...DTRPG_PRESET.validate.assets.allowedColorSpaces
6337
- ];
6338
- enforceStrictPdfChecks(next, true);
6490
+ const projectRoot = resolve9(inputDir);
6491
+ const dirs = new Set;
6492
+ for (const file of closure) {
6493
+ const ext = file.slice(file.lastIndexOf(".")).toLowerCase();
6494
+ if (!SHIPPED_ASSET_EXTS.has(ext))
6495
+ continue;
6496
+ const dir = dirname5(resolve9(file));
6497
+ if (escapesProjectRoot(projectRoot, dir))
6498
+ dirs.add(dir);
6339
6499
  }
6340
- return next;
6500
+ return [...dirs];
6341
6501
  }
6342
-
6343
- // src/lib/validation-exec.ts
6344
6502
  function parseCsv(value) {
6345
6503
  if (!value)
6346
6504
  return;
6347
6505
  return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
6348
6506
  }
6349
- function parseProfile(raw) {
6350
- if (!raw)
6351
- return;
6352
- if (raw === "dtrpg")
6353
- return raw;
6354
- throw new Error(`Unsupported profile: ${raw}. Supported profiles: dtrpg`);
6355
- }
6356
6507
  function resolvePhaseArg(raw) {
6357
6508
  const normalized = raw.trim().toLowerCase();
6358
6509
  if (normalized === "pre" || normalized === "pre-build")
@@ -6375,20 +6526,16 @@ function resolveCategoryArg(raw) {
6375
6526
  }
6376
6527
  return parsed;
6377
6528
  }
6378
- function withProfileRequiredCheckErrors(report, tools, profile) {
6379
- if (!profile)
6380
- return report;
6381
- const required = profile === "dtrpg" ? DTRPG_STRICT_PDF_CHECKS : [];
6382
- if (required.length === 0)
6383
- return report;
6529
+ function withTargetRequiredCheckErrors(report, tools, target) {
6384
6530
  const skippedSet = new Set(tools.skippedChecks);
6385
- const missingRequired = required.filter((checkId) => skippedSet.has(checkId));
6531
+ const missingRequired = target.requiredChecks.filter((checkId) => skippedSet.has(checkId));
6386
6532
  if (missingRequired.length === 0)
6387
6533
  return report;
6388
6534
  const syntheticErrors = missingRequired.map((checkId) => ({
6389
6535
  checkId,
6536
+ target: target.id,
6390
6537
  severity: "error",
6391
- message: `Profile ${profile} requires check ${checkId}, but it was skipped because required tools are not available.`
6538
+ message: `Target ${target.id} requires check ${checkId}, but it was skipped because required tools are not available.`
6392
6539
  }));
6393
6540
  const results = [...report.results, ...syntheticErrors];
6394
6541
  const errors = [...report.errors, ...syntheticErrors];
@@ -6402,19 +6549,115 @@ function withProfileRequiredCheckErrors(report, tools, profile) {
6402
6549
  }
6403
6550
  };
6404
6551
  }
6552
+ var TARGET_INDEPENDENT_CATEGORIES = ["source", "heuristic"];
6553
+ var TARGET_DEPENDENT_CATEGORIES = ["asset", "pdf"];
6554
+ function tagReport(report, targetId) {
6555
+ const tag = (r) => ({ ...r, target: targetId });
6556
+ return {
6557
+ results: report.results.map(tag),
6558
+ errors: report.errors.map(tag),
6559
+ warnings: report.warnings.map(tag),
6560
+ infos: report.infos.map(tag),
6561
+ passed: report.passed,
6562
+ summary: report.summary
6563
+ };
6564
+ }
6565
+ function mergeReports(reports) {
6566
+ const merged = {
6567
+ results: [],
6568
+ errors: [],
6569
+ warnings: [],
6570
+ infos: [],
6571
+ passed: [],
6572
+ summary: { total: 0, errors: 0, warnings: 0, infos: 0, passed: 0 }
6573
+ };
6574
+ for (const r of reports) {
6575
+ merged.results.push(...r.results);
6576
+ merged.errors.push(...r.errors);
6577
+ merged.warnings.push(...r.warnings);
6578
+ merged.infos.push(...r.infos);
6579
+ merged.passed.push(...r.passed);
6580
+ merged.summary.total += r.summary.total;
6581
+ merged.summary.errors += r.summary.errors;
6582
+ merged.summary.warnings += r.summary.warnings;
6583
+ merged.summary.infos += r.summary.infos;
6584
+ merged.summary.passed += r.summary.passed;
6585
+ }
6586
+ return merged;
6587
+ }
6588
+ function mergeTools(results) {
6589
+ const available = new Set;
6590
+ const missing = new Set;
6591
+ const skippedChecks = new Set;
6592
+ const toolToChecks = new Map;
6593
+ for (const r of results) {
6594
+ for (const t of r.available)
6595
+ available.add(t);
6596
+ for (const t of r.missing)
6597
+ missing.add(t);
6598
+ for (const id of r.skippedChecks)
6599
+ skippedChecks.add(id);
6600
+ for (const [tool, ids] of r.toolToChecks) {
6601
+ const merged = new Set([...toolToChecks.get(tool) ?? [], ...ids]);
6602
+ toolToChecks.set(tool, [...merged]);
6603
+ }
6604
+ }
6605
+ return {
6606
+ available: [...available],
6607
+ missing: [...missing],
6608
+ skippedChecks: [...skippedChecks],
6609
+ toolToChecks
6610
+ };
6611
+ }
6612
+ function planRuns(opts) {
6613
+ const { manifest, baseConfig, targetIds, pdfPath, categories, only } = opts;
6614
+ const finalize = (config) => pdfPath ? applyDefaultPdfStrictChecks(config) : config;
6615
+ if (targetIds.length === 0) {
6616
+ return [{ targetId: null, config: finalize(baseConfig), category: categories, only }];
6617
+ }
6618
+ const requested = categories ?? [...TARGET_INDEPENDENT_CATEGORIES, ...TARGET_DEPENDENT_CATEGORIES];
6619
+ const independentCats = requested.filter((c) => TARGET_INDEPENDENT_CATEGORIES.includes(c));
6620
+ const dependentCats = requested.filter((c) => TARGET_DEPENDENT_CATEGORIES.includes(c));
6621
+ let baseOnly;
6622
+ let dependentOnly;
6623
+ if (only && only.length > 0) {
6624
+ const { resolved, unmatched } = resolveCheckSelectors(only);
6625
+ const categoryById = new Map(getChecks({}).map((c) => [c.id, c.category]));
6626
+ const independentIds = resolved.filter((id) => TARGET_INDEPENDENT_CATEGORIES.includes(categoryById.get(id) ?? ""));
6627
+ const dependentIds = resolved.filter((id) => TARGET_DEPENDENT_CATEGORIES.includes(categoryById.get(id) ?? ""));
6628
+ baseOnly = [...independentIds, ...unmatched];
6629
+ dependentOnly = dependentIds;
6630
+ }
6631
+ const runs = [];
6632
+ const baseWanted = only ? (baseOnly?.length ?? 0) > 0 : independentCats.length > 0;
6633
+ if (baseWanted) {
6634
+ runs.push({
6635
+ targetId: null,
6636
+ config: baseConfig,
6637
+ category: only ? undefined : independentCats,
6638
+ only: baseOnly
6639
+ });
6640
+ }
6641
+ const dependentWanted = only ? (dependentOnly?.length ?? 0) > 0 : dependentCats.length > 0;
6642
+ if (dependentWanted) {
6643
+ for (const targetId of targetIds) {
6644
+ runs.push({
6645
+ targetId,
6646
+ config: finalize(resolveConfigForTarget({}, manifest, targetId)),
6647
+ category: only ? undefined : dependentCats,
6648
+ only: dependentOnly
6649
+ });
6650
+ }
6651
+ }
6652
+ return runs;
6653
+ }
6405
6654
  async function executeValidation(args) {
6406
6655
  const manifestPath = typeof args.manifest === "string" ? args.manifest : undefined;
6407
6656
  const { manifest, manifestDir } = await loadManifestWithPath(manifestPath ?? args.input ?? undefined, { explicit: manifestPath !== undefined });
6408
- const profile = parseProfile(typeof args.profile === "string" ? args.profile : undefined);
6409
- let config = resolveConfig({}, manifest);
6657
+ const config = resolveConfig({}, manifest);
6658
+ const targetIds = resolveTargets(parseCsv(typeof args.target === "string" ? args.target : undefined), config.targets);
6410
6659
  const pdfPath = typeof args.pdf === "string" ? resolve9(args.pdf) : undefined;
6411
6660
  const inputDir = typeof args.input === "string" ? resolve9(args.input) : undefined;
6412
- if (pdfPath) {
6413
- config = applyDtrpgPdfDefaults(config);
6414
- }
6415
- if (profile) {
6416
- config = applyValidationProfile(config, profile);
6417
- }
6418
6661
  if (pdfPath && !existsSync11(pdfPath)) {
6419
6662
  throw new Error(`File not found: ${pdfPath}`);
6420
6663
  }
@@ -6443,11 +6686,11 @@ async function executeValidation(args) {
6443
6686
  let htmlPath;
6444
6687
  const outDir = resolveOutputDir(manifestDir, config.title);
6445
6688
  if (inputDir) {
6446
- const relMarkdown = await resolveActiveMarkdownFiles(inputDir, config.source.files);
6447
- markdownFiles = relMarkdown.map((f) => join6(inputDir, canonicalChapterId(f)));
6448
- const relStyles = await resolveActiveStyles(inputDir, config.styles);
6449
- cssFiles = relStyles.map((rel) => resolve9(inputDir, rel)).filter((f) => !f.endsWith(".min.css"));
6450
- assetDirs = [inputDir];
6689
+ const relMarkdown = await resolveActiveMarkdownFiles(manifestDir, config.source.files);
6690
+ markdownFiles = relMarkdown.map((f) => join6(manifestDir, canonicalChapterId(f)));
6691
+ const relStyles = await resolveActiveStyles(manifestDir, config.styles);
6692
+ cssFiles = relStyles.map((rel) => resolve9(manifestDir, rel)).filter((f) => !f.endsWith(".min.css"));
6693
+ assetDirs = [inputDir, ...await sharedAssetDirs(manifestDir, relStyles, inputDir)];
6451
6694
  const possibleHtml = join6(outDir, BOOK_HTML_FILENAME);
6452
6695
  if (existsSync11(possibleHtml)) {
6453
6696
  htmlPath = possibleHtml;
@@ -6469,15 +6712,30 @@ async function executeValidation(args) {
6469
6712
  only,
6470
6713
  skip
6471
6714
  };
6472
- const tools = await checkToolAvailability(config, runnerOptions);
6473
- const initialReport = await runChecks(context, {
6474
- ...runnerOptions,
6475
- skipMissingTools: tools.skippedChecks
6476
- });
6477
- const report = withProfileRequiredCheckErrors(initialReport, tools, profile);
6715
+ const runs = planRuns({ manifest, baseConfig: config, targetIds, pdfPath, categories, only });
6716
+ const reports = [];
6717
+ const toolResults = [];
6718
+ for (const run2 of runs) {
6719
+ const runOptions = {
6720
+ category: run2.category,
6721
+ phase,
6722
+ only: run2.only,
6723
+ skip
6724
+ };
6725
+ const runTools = await checkToolAvailability(run2.config, runOptions);
6726
+ toolResults.push(runTools);
6727
+ let runReport = await runChecks({ ...context, config: run2.config }, { ...runOptions, skipMissingTools: runTools.skippedChecks });
6728
+ if (run2.targetId !== null) {
6729
+ runReport = withTargetRequiredCheckErrors(runReport, runTools, publishTargetFor(run2.targetId));
6730
+ runReport = tagReport(runReport, run2.targetId);
6731
+ }
6732
+ reports.push(runReport);
6733
+ }
6734
+ const tools = mergeTools(toolResults);
6735
+ const report = mergeReports(reports);
6478
6736
  return {
6479
6737
  config,
6480
- profile,
6738
+ targets: targetIds,
6481
6739
  context,
6482
6740
  runnerOptions,
6483
6741
  tools,
@@ -6555,7 +6813,7 @@ ${m.installHint}`).join(`
6555
6813
 
6556
6814
  ${list}
6557
6815
 
6558
- 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);
6816
+ 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);
6559
6817
  }
6560
6818
  function computeGates(format, opts, config) {
6561
6819
  if (format === "html") {
@@ -6649,6 +6907,15 @@ function resolveWithinRoot(relPath, root) {
6649
6907
  }
6650
6908
  return candidate;
6651
6909
  }
6910
+ function hasDotSegment(urlPathname) {
6911
+ let decoded;
6912
+ try {
6913
+ decoded = decodeURIComponent(urlPathname);
6914
+ } catch {
6915
+ return true;
6916
+ }
6917
+ return decoded.split(/[/\\]/).some((segment) => segment.startsWith("."));
6918
+ }
6652
6919
  function resolveStaticPath(urlPathname, root) {
6653
6920
  let decoded;
6654
6921
  try {
@@ -7047,14 +7314,25 @@ async function resolveBuildContext(opts) {
7047
7314
  const outDir = target.kind === "file" ? path9.dirname(target.file) : target.dir;
7048
7315
  const gates = computeGates(format, opts, config);
7049
7316
  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")}`);
7050
- return { opts, format, inputDir, outDir, workDir, target, manifestDir, config, gates };
7317
+ return {
7318
+ opts,
7319
+ format,
7320
+ inputDir,
7321
+ renderDir: manifestDir,
7322
+ outDir,
7323
+ workDir,
7324
+ target,
7325
+ manifestDir,
7326
+ config,
7327
+ gates
7328
+ };
7051
7329
  }
7052
7330
  async function runQualityGates(ctx) {
7053
- const { gates, opts, inputDir } = ctx;
7331
+ const { gates, opts, renderDir } = ctx;
7054
7332
  if (gates.lint) {
7055
7333
  log.info("Lint: CSS print-safety");
7056
7334
  const lintResult = await runLint({
7057
- manifest: opts.manifestPath ?? inputDir
7335
+ manifest: opts.manifestPath ?? renderDir
7058
7336
  });
7059
7337
  if (!lintResult.ok) {
7060
7338
  throw new BuildError("CSS lint failed", 2);
@@ -7063,7 +7341,7 @@ async function runQualityGates(ctx) {
7063
7341
  if (gates.preValidate) {
7064
7342
  log.info("Pre-build validation");
7065
7343
  const result = await executeAndReport({
7066
- input: inputDir,
7344
+ input: renderDir,
7067
7345
  phase: "pre-build",
7068
7346
  manifest: opts.manifestPath
7069
7347
  }, "text");
@@ -7073,7 +7351,7 @@ async function runQualityGates(ctx) {
7073
7351
  }
7074
7352
  }
7075
7353
  async function renderBook(ctx) {
7076
- const { config, manifestDir, inputDir, workDir } = ctx;
7354
+ const { config, renderDir, workDir } = ctx;
7077
7355
  if (config.source.files && config.source.files.length > 0) {
7078
7356
  log.info(`Using specified files (${config.source.files.length} total)`);
7079
7357
  } else {
@@ -7082,13 +7360,13 @@ async function renderBook(ctx) {
7082
7360
  if (config.plugins.length > 0) {
7083
7361
  log.info(`Loading ${config.plugins.length} plugin(s)...`);
7084
7362
  }
7085
- const { plugins, pluginCss } = await loadPluginsWithCss(config.plugins, manifestDir);
7363
+ const { plugins, pluginCss } = await loadPluginsWithCss(config.plugins, renderDir);
7086
7364
  if (plugins && plugins.length > 0) {
7087
7365
  log.success(`Loaded ${plugins.length} plugin(s)`);
7088
7366
  }
7089
7367
  const imageRefs = [];
7090
7368
  const cssAssets = [];
7091
- const htmlFile = await renderChaptersToFile(inputDir, workDir, {
7369
+ const htmlFile = await renderChaptersToFile(renderDir, workDir, {
7092
7370
  title: config.title,
7093
7371
  styles: config.styles,
7094
7372
  files: config.source.files,
@@ -7107,7 +7385,7 @@ async function renderBook(ctx) {
7107
7385
  }
7108
7386
  });
7109
7387
  log.success(`Wrote ${htmlFile}`);
7110
- const { copies: imageCopies, errors } = await planImageCopies(inputDir, imageRefs);
7388
+ const { copies: imageCopies, errors } = await planImageCopies(renderDir, imageRefs);
7111
7389
  if (errors.length > 0) {
7112
7390
  throw new BuildError(`Cannot resolve ${errors.length} image reference(s):
7113
7391
  ` + errors.map((e) => ` - ${e}`).join(`
@@ -7146,7 +7424,8 @@ async function resolveIccProfile(icc, manifestDir, explicitIccPath) {
7146
7424
  async function finalizeBuild(ctx, fingerprint, wroteMessage, paths, artifactName2 = null) {
7147
7425
  const workFingerprint = await writeBuildFingerprint({
7148
7426
  ...fingerprint,
7149
- outputDir: ctx.workDir
7427
+ outputDir: ctx.workDir,
7428
+ recordedOutputDir: fingerprint.outputDir
7150
7429
  });
7151
7430
  await publishBuild(ctx, artifactName2);
7152
7431
  const delivered = ctx.target.kind !== "file";
@@ -7237,7 +7516,9 @@ class PdfOutput {
7237
7516
  await renderHtmlToPdf(htmlFile, rawPdf, opts.pdfRenderer, staticHtmlRaw);
7238
7517
  if (staticHtmlRaw && fs2.existsSync(staticHtmlRaw)) {
7239
7518
  await finalizeStaticBook(await fsp3.readFile(staticHtmlRaw, "utf-8"), htmlFile, workDir);
7240
- log.success(`Wrote static desktop: ${path9.join(outDir, BOOK_HTML)}`);
7519
+ if (ctx.target.kind !== "file") {
7520
+ log.success(`Wrote static desktop: ${path9.join(outDir, BOOK_HTML)}`);
7521
+ }
7241
7522
  }
7242
7523
  if (!pdfxMode) {
7243
7524
  try {
@@ -7370,6 +7651,7 @@ async function renderPreviewBook(inputPath, config, opts) {
7370
7651
  plugins,
7371
7652
  pluginCss,
7372
7653
  wrapChapters: opts.wrapChapters,
7654
+ ...opts.onCssAssets ? { onCssAssets: opts.onCssAssets } : {},
7373
7655
  onChapterWarnings: (file, warnings) => {
7374
7656
  for (const w of warnings) {
7375
7657
  warn2(` ${file}, line ${w.line}: ${w.message}`);
@@ -7377,23 +7659,70 @@ async function renderPreviewBook(inputPath, config, opts) {
7377
7659
  }
7378
7660
  });
7379
7661
  }
7380
- function injectPreviewScripts(html) {
7662
+ function injectPreviewScripts(html, pageIsolateChapters) {
7381
7663
  const iface = `<script src="/preview/scripts/pagedjs-interface.js"></script>
7382
7664
  ` + `<script src="/preview/scripts/pagedjs-bridge.js"></script>
7383
7665
  `;
7384
- return html.replace(pagedjsPolyfillTagRegex(), iface + BREAK_INSIDE_HANDLER + `
7666
+ let output = html.replace(pagedjsPolyfillTagRegex(), iface + BREAK_INSIDE_HANDLER + `
7385
7667
  <script src="/vendor/paged.polyfill.js"></script>`);
7668
+ if (pageIsolateChapters && /<\/head>/i.test(output)) {
7669
+ output = output.replace(/<\/head>/i, `<style>.gutterpress-chapter{break-before:page}</style>
7670
+ </head>`);
7671
+ }
7672
+ return output;
7386
7673
  }
7387
- async function generateAndWriteHtml(inputPath, tempDir, config) {
7674
+ async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets) {
7388
7675
  if (!inputPath) {
7676
+ cssAssets.clear();
7389
7677
  await fsp4.writeFile(path10.join(tempDir, BOOK_HTML_FILENAME), EMPTY_BOOK_HTML, "utf-8");
7390
7678
  return;
7391
7679
  }
7680
+ const nextAssets = new Map;
7681
+ const incremental = incrementalPreviewEnabled();
7392
7682
  const html = await renderPreviewBook(inputPath, config, {
7393
7683
  files: config.source?.files ?? null,
7684
+ wrapChapters: incremental,
7685
+ onCssAssets: (copies) => {
7686
+ for (const copy of copies)
7687
+ nextAssets.set(copy.to, copy.from);
7688
+ }
7689
+ });
7690
+ cssAssets.clear();
7691
+ for (const [to, from] of nextAssets)
7692
+ cssAssets.set(to, from);
7693
+ await fsp4.writeFile(path10.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html, incremental), "utf-8");
7694
+ }
7695
+ async function renderChapterPreviewHtml(inputPath, file, config) {
7696
+ const html = await renderPreviewBook(inputPath, config, {
7697
+ files: [canonicalChapterId(file)],
7394
7698
  wrapChapters: true
7395
7699
  });
7396
- await fsp4.writeFile(path10.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html), "utf-8");
7700
+ return injectPreviewScripts(html, true);
7701
+ }
7702
+ function describeChanges(changes, inputResolved) {
7703
+ const files = [];
7704
+ for (const [changedPath, event] of changes) {
7705
+ const relative2 = path10.relative(inputResolved, path10.resolve(changedPath));
7706
+ if (relative2 === "" || relative2 === ".." || relative2.startsWith(`..${path10.sep}`) || path10.isAbsolute(relative2))
7707
+ continue;
7708
+ files.push({
7709
+ relativePath: canonicalChapterId(relative2),
7710
+ ext: path10.extname(changedPath).toLowerCase(),
7711
+ event
7712
+ });
7713
+ }
7714
+ return files;
7715
+ }
7716
+ function decideBroadcast(files, changeCount, incremental) {
7717
+ const only = files.length === 1 ? files[0] : null;
7718
+ if (incremental && changeCount === 1 && only?.ext === ".md" && only.event !== "unlink" && only.event !== "unlinkDir") {
7719
+ return {
7720
+ kind: "chapter-splice",
7721
+ chapterId: canonicalChapterId(only.relativePath),
7722
+ relativePath: only.relativePath
7723
+ };
7724
+ }
7725
+ return { kind: "full-reload" };
7397
7726
  }
7398
7727
  function isDotPathUnderRoot(candidatePath, root) {
7399
7728
  const normalizedRoot = root.replace(/\\/g, "/").replace(/\/+$/, "");
@@ -7410,11 +7739,24 @@ function isDotPathUnderRoot(candidatePath, root) {
7410
7739
  return false;
7411
7740
  return rel.split("/").some((segment) => segment.startsWith("."));
7412
7741
  }
7742
+ function isGeneratedProjectPath(relative2) {
7743
+ const segments = relative2.split("/");
7744
+ for (let i = 0;i < segments.length; i++) {
7745
+ const segment = segments[i];
7746
+ if (segment === "dist" || segment === "node_modules")
7747
+ return true;
7748
+ if (segment === "plugins" && segments[i + 1] === "npm")
7749
+ return true;
7750
+ }
7751
+ return false;
7752
+ }
7413
7753
  function isIgnoredWatchPath(candidatePath, projectRoot) {
7414
7754
  const normalizedRoot = projectRoot.replace(/\\/g, "/").replace(/\/+$/, "");
7415
7755
  const normalizedPath = candidatePath.replace(/\\/g, "/");
7416
- const inProject = normalizedPath === normalizedRoot || normalizedPath.startsWith(normalizedRoot + "/");
7417
- return inProject ? isDotPathUnderRoot(candidatePath, projectRoot) : false;
7756
+ if (!normalizedPath.startsWith(normalizedRoot + "/"))
7757
+ return false;
7758
+ const relative2 = normalizedPath.slice(normalizedRoot.length + 1);
7759
+ return isDotPathUnderRoot(candidatePath, projectRoot) || isGeneratedProjectPath(relative2);
7418
7760
  }
7419
7761
  function isExternalWatchCandidate(candidatePath, targets) {
7420
7762
  const candidate = path10.resolve(candidatePath);
@@ -7499,21 +7841,43 @@ function createFileWatcher(state) {
7499
7841
  let closed = false;
7500
7842
  const pendingChanges = new Map;
7501
7843
  const suppressInitialExternalAdds = new Set;
7844
+ const settledWrites = new Map;
7845
+ let immediatePending = false;
7502
7846
  function scheduleRebuild() {
7503
7847
  if (closed)
7504
7848
  return;
7505
7849
  if (state.rebuildTimer)
7506
7850
  clearTimeout(state.rebuildTimer);
7507
- state.rebuildTimer = setTimeout(runRebuild, DEBOUNCE.FILE_WATCH);
7508
- }
7509
- function recordChange(event, filePath2) {
7851
+ const timer = setTimeout(() => {
7852
+ if (state.rebuildTimer === timer)
7853
+ state.rebuildTimer = null;
7854
+ runRebuild();
7855
+ }, DEBOUNCE.FILE_WATCH);
7856
+ state.rebuildTimer = timer;
7857
+ }
7858
+ async function recordChange(event, filePath2) {
7510
7859
  if (closed)
7511
7860
  return;
7512
- debug(`File ${event}: ${filePath2}`);
7513
- pendingChanges.set(path10.resolve(filePath2), event);
7861
+ const target = path10.resolve(filePath2);
7862
+ const settled = settledWrites.get(target);
7863
+ if (settled && event !== "unlink" && Date.now() <= settled.expiresAt) {
7864
+ const diskContent = await fsp4.readFile(target, "utf8").catch(() => null);
7865
+ if (closed)
7866
+ return;
7867
+ if (settledWrites.get(target) === settled && diskContent === settled.content) {
7868
+ debug(`Suppressed settled-write watcher echo: ${target}`);
7869
+ return;
7870
+ }
7871
+ }
7872
+ if (settledWrites.get(target) === settled)
7873
+ settledWrites.delete(target);
7874
+ debug(`File ${event}: ${target}`);
7875
+ pendingChanges.set(target, event);
7514
7876
  scheduleRebuild();
7515
7877
  }
7516
- watcher.on("all", recordChange);
7878
+ watcher.on("all", (event, filePath2) => {
7879
+ recordChange(event, filePath2);
7880
+ });
7517
7881
  externalWatcher.on("all", (event, filePath2) => {
7518
7882
  const target = path10.resolve(filePath2);
7519
7883
  if (!desiredExternals.has(target))
@@ -7522,6 +7886,43 @@ function createFileWatcher(state) {
7522
7886
  return;
7523
7887
  recordChange(event, target);
7524
7888
  });
7889
+ function isWatchedSource(target) {
7890
+ const rel = path10.relative(inputResolved, target);
7891
+ const inProject = rel === "" || rel !== ".." && !rel.startsWith(`..${path10.sep}`) && !path10.isAbsolute(rel);
7892
+ if (inProject)
7893
+ return !isIgnoredWatchPath(target, inputResolved);
7894
+ return desiredExternals.has(target);
7895
+ }
7896
+ function acceptSettledWrite(target, writtenContent) {
7897
+ const settled = { content: writtenContent, expiresAt: Date.now() + 2000 };
7898
+ settledWrites.set(target, settled);
7899
+ const expiry = setTimeout(() => {
7900
+ if (settledWrites.get(target) === settled)
7901
+ settledWrites.delete(target);
7902
+ }, 2000);
7903
+ expiry.unref?.();
7904
+ pendingChanges.set(target, "change");
7905
+ immediatePending = true;
7906
+ if (state.rebuildTimer) {
7907
+ clearTimeout(state.rebuildTimer);
7908
+ state.rebuildTimer = null;
7909
+ }
7910
+ if (!state.isRebuilding)
7911
+ runRebuild();
7912
+ }
7913
+ state.notifySettledWrite = (filePath2, writtenContent) => {
7914
+ if (closed)
7915
+ return;
7916
+ const target = path10.resolve(filePath2);
7917
+ if (isWatchedSource(target)) {
7918
+ acceptSettledWrite(target, writtenContent);
7919
+ return;
7920
+ }
7921
+ syncQueue.then(() => {
7922
+ if (!closed && isWatchedSource(target))
7923
+ acceptSettledWrite(target, writtenContent);
7924
+ });
7925
+ };
7525
7926
  let watchedExternalRoots = new Set;
7526
7927
  let syncQueue = Promise.resolve();
7527
7928
  function syncExternalWatches() {
@@ -7573,10 +7974,15 @@ function createFileWatcher(state) {
7573
7974
  async function runRebuild() {
7574
7975
  if (closed || state.isRebuilding)
7575
7976
  return;
7977
+ if (state.rebuildTimer) {
7978
+ clearTimeout(state.rebuildTimer);
7979
+ state.rebuildTimer = null;
7980
+ }
7576
7981
  const changes = [...pendingChanges.entries()];
7577
7982
  pendingChanges.clear();
7578
7983
  if (changes.length === 0)
7579
7984
  return;
7985
+ immediatePending = false;
7580
7986
  state.isRebuilding = true;
7581
7987
  let resolveInFlight;
7582
7988
  const inFlight = new Promise((resolve10) => {
@@ -7593,11 +7999,17 @@ function createFileWatcher(state) {
7593
7999
  await syncExternalWatches();
7594
8000
  if (closed)
7595
8001
  return;
7596
- await generateAndWriteHtml(inputResolved, state.tempDir, updatedConfig);
8002
+ await generateAndWriteHtml(inputResolved, state.tempDir, updatedConfig, state.cssAssets);
7597
8003
  if (closed)
7598
8004
  return;
7599
- state.previewServer?.broadcastReload();
7600
- info(changes.length > 1 ? `Preview updated (${changes.length} files changed — full reload)` : "Preview updated");
8005
+ const decision = decideBroadcast(describeChanges(changes, inputResolved), changes.length, incrementalPreviewEnabled());
8006
+ if (decision.kind === "chapter-splice") {
8007
+ state.previewServer?.broadcastContentUpdate(decision.chapterId);
8008
+ info(`Chapter updated: ${decision.relativePath}`);
8009
+ } else {
8010
+ state.previewServer?.broadcastReload();
8011
+ info(changes.length > 1 ? `Preview updated (${changes.length} files changed — full reload)` : "Preview updated");
8012
+ }
7601
8013
  } catch (err) {
7602
8014
  error("Failed to regenerate preview:", err);
7603
8015
  } finally {
@@ -7605,13 +8017,19 @@ function createFileWatcher(state) {
7605
8017
  resolveInFlight();
7606
8018
  if (state.rebuildPromise === inFlight)
7607
8019
  state.rebuildPromise = null;
7608
- if (!closed && pendingChanges.size > 0)
7609
- scheduleRebuild();
8020
+ if (!closed && pendingChanges.size > 0) {
8021
+ if (immediatePending)
8022
+ runRebuild();
8023
+ else
8024
+ scheduleRebuild();
8025
+ }
7610
8026
  }
7611
8027
  }
7612
8028
  const closeBookWatcher = watcher.close.bind(watcher);
7613
8029
  watcher.close = async () => {
7614
8030
  closed = true;
8031
+ state.notifySettledWrite = null;
8032
+ settledWrites.clear();
7615
8033
  await syncQueue.catch(() => {});
7616
8034
  await state.rebuildPromise?.catch(() => {});
7617
8035
  await externalWatcher.close();
@@ -7710,7 +8128,7 @@ async function restartPreview(newInputPath, state) {
7710
8128
  state.currentInputPath = newInputPath;
7711
8129
  const manifest = await loadManifest(newInputPath);
7712
8130
  state.config = resolveConfig({}, manifest);
7713
- await generateAndWriteHtml(newInputPath, state.tempDir, state.config);
8131
+ await generateAndWriteHtml(newInputPath, state.tempDir, state.config, state.cssAssets);
7714
8132
  startFileWatcher(state);
7715
8133
  state.previewServer?.broadcastReload();
7716
8134
  info("Preview restarted successfully");
@@ -7764,17 +8182,20 @@ function createServerState(inputPath, tempDir, config, options) {
7764
8182
  rebuildTimer: null,
7765
8183
  isRebuilding: false,
7766
8184
  rebuildPromise: null,
8185
+ notifySettledWrite: null,
7767
8186
  previewServer: null,
7768
8187
  isShuttingDown: false,
7769
8188
  tempDir,
7770
8189
  config,
7771
- options
8190
+ options,
8191
+ cssAssets: new Map
7772
8192
  };
7773
8193
  }
7774
8194
 
7775
8195
  // src/preview/http-server.ts
7776
8196
  import http2 from "node:http";
7777
8197
  import net from "node:net";
8198
+ import { randomUUID } from "node:crypto";
7778
8199
  import { readFile as readFile21, stat as stat4 } from "node:fs/promises";
7779
8200
  import path12 from "path";
7780
8201
  import { WebSocket, WebSocketServer } from "ws";
@@ -7809,7 +8230,8 @@ function openPath(filePath2) {
7809
8230
 
7810
8231
  // src/preview/http-server.ts
7811
8232
  var HMR_PATH = "/__gutterpress-hmr";
7812
- var HMR_CLIENT_SNIPPET = `
8233
+ function hmrClientSnippet(initialRevision, instanceId) {
8234
+ return `
7813
8235
  <script>
7814
8236
  (function () {
7815
8237
  // When loaded by the preview SHELL (iframe double-buffer), the shell loads us
@@ -7819,6 +8241,10 @@ var HMR_CLIENT_SNIPPET = `
7819
8241
  // for scroll-anchor and reload.
7820
8242
  if (/[?&]gutterpressshell=1/.test(location.search)) return;
7821
8243
  var ANCHOR_KEY = 'gutterpress-scroll-anchor';
8244
+ var appliedRevision = ${initialRevision};
8245
+ var appliedInstance = ${JSON.stringify(instanceId)};
8246
+ var readyToAcknowledge = false;
8247
+ var reloadRequested = false;
7822
8248
 
7823
8249
  // Find the element nearest the top of the viewport that carries a source
7824
8250
  // line (markdown-it-source-map emits data-source-line on block elements).
@@ -7869,7 +8295,13 @@ var HMR_CLIENT_SNIPPET = `
7869
8295
  }
7870
8296
  }
7871
8297
  if (el) {
7872
- window.scrollBy(0, el.getBoundingClientRect().top - a.offset);
8298
+ window.scrollBy({
8299
+ top: el.getBoundingClientRect().top - a.offset,
8300
+ behavior: 'instant'
8301
+ });
8302
+ if (window.previewAPI && typeof window.previewAPI.refresh === 'function') {
8303
+ window.previewAPI.refresh();
8304
+ }
7873
8305
  return;
7874
8306
  }
7875
8307
  if (tries++ < 240) setTimeout(attempt, 25); // wait for pagination
@@ -7877,44 +8309,100 @@ var HMR_CLIENT_SNIPPET = `
7877
8309
  }
7878
8310
  var restored = false;
7879
8311
  function restoreOnce() { if (restored) return; restored = true; restoreAnchor(); }
8312
+ function finishInitialRender() {
8313
+ restoreOnce();
8314
+ readyToAcknowledge = true;
8315
+ acknowledge();
8316
+ }
7880
8317
  // CRITICAL ordering: when the Paged.js polyfill is present it re-paginates on
7881
- // load and its PagedConfig.after calls scrollTo(0,0) THEN fires
8318
+ // load and its PagedConfig.after scrolls to the document start THEN fires
7882
8319
  // 'renderingComplete'. So in engine mode we MUST wait for that event —
7883
- // restoring earlier would be wiped by the scrollTo(0,0). In static mode (no
8320
+ // restoring earlier would be wiped by that initial scroll. In static mode (no
7884
8321
  // engine) the content is final immediately, so restore right after load.
7885
- var hasEngine = !!document.querySelector('script[src*="paged.polyfill"], script[src*="pagedjs"]');
7886
- window.addEventListener('renderingComplete', restoreOnce);
8322
+ var hasEngine = !!document.querySelector('script[src*="paged.polyfill"]');
8323
+ window.addEventListener('renderingComplete', finishInitialRender);
7887
8324
  if (!hasEngine) {
7888
8325
  if (document.readyState === 'loading') {
7889
- document.addEventListener('DOMContentLoaded', function () { setTimeout(restoreOnce, 50); });
8326
+ document.addEventListener('DOMContentLoaded', function () { setTimeout(finishInitialRender, 50); });
7890
8327
  } else {
7891
- setTimeout(restoreOnce, 50);
8328
+ setTimeout(finishInitialRender, 50);
7892
8329
  }
7893
8330
  }
7894
- setTimeout(restoreOnce, 10000); // safety net if renderingComplete never fires
8331
+ var ws = null;
8332
+ var reconnectTimer = null;
8333
+ var reconnectDelay = 250;
8334
+ var stopped = false;
7895
8335
 
7896
- var ws = new WebSocket(location.origin.replace(/^http/, 'ws') + '${HMR_PATH}');
7897
- ws.onmessage = function (e) {
7898
- var msg;
7899
- try { msg = JSON.parse(e.data); } catch (_) { return; }
7900
- if (msg.type === 'full-reload') {
8336
+ function acknowledge() {
8337
+ if (!readyToAcknowledge || !ws || ws.readyState !== 1) return;
8338
+ try {
8339
+ ws.send(JSON.stringify({
8340
+ type: 'reload-applied',
8341
+ instance: appliedInstance,
8342
+ revision: appliedRevision
8343
+ }));
8344
+ } catch (_) {}
8345
+ }
8346
+
8347
+ function connect() {
8348
+ if (stopped) return;
8349
+ ws = new WebSocket(location.origin.replace(/^http/, 'ws') + '${HMR_PATH}');
8350
+ ws.onopen = function () {
8351
+ reconnectDelay = 250;
8352
+ acknowledge();
8353
+ };
8354
+ ws.onmessage = function (e) {
8355
+ var msg;
8356
+ try { msg = JSON.parse(e.data); } catch (_) { return; }
8357
+ if (
8358
+ msg.type !== 'reload-state' &&
8359
+ msg.type !== 'full-reload' &&
8360
+ msg.type !== 'content-update'
8361
+ ) return;
8362
+ var instance = typeof msg.instance === 'string' ? msg.instance : null;
8363
+ var revision = Number(msg.revision);
8364
+ if (!instance || !Number.isSafeInteger(revision) || revision < 0) return;
8365
+ if (instance === appliedInstance && revision <= appliedRevision) {
8366
+ acknowledge();
8367
+ return;
8368
+ }
8369
+ if (reloadRequested) return;
8370
+ reloadRequested = true;
7901
8371
  try { var a = captureAnchor(); if (a) sessionStorage.setItem(ANCHOR_KEY, JSON.stringify(a)); } catch (_) {}
7902
8372
  location.reload();
7903
- return;
7904
- }
7905
- };
8373
+ };
8374
+ ws.onclose = function () {
8375
+ ws = null;
8376
+ if (stopped || reconnectTimer !== null) return;
8377
+ reconnectTimer = setTimeout(function () {
8378
+ reconnectTimer = null;
8379
+ connect();
8380
+ }, reconnectDelay);
8381
+ reconnectDelay = Math.min(reconnectDelay * 2, 5000);
8382
+ };
8383
+ ws.onerror = function () { try { ws.close(); } catch (_) {} };
8384
+ }
8385
+
8386
+ window.addEventListener('beforeunload', function () {
8387
+ stopped = true;
8388
+ if (reconnectTimer !== null) clearTimeout(reconnectTimer);
8389
+ });
8390
+ connect();
7906
8391
  })();
7907
8392
  </script>
7908
8393
  `;
7909
- var SHELL_HTML = `<!doctype html>
8394
+ }
8395
+ function shellHtml(initialRevision, instanceId) {
8396
+ return `<!doctype html>
7910
8397
  <html lang="en"><head><meta charset="utf-8"><title>gutterpress preview</title>
7911
8398
  <style>html,body{margin:0;height:100%;background:#fff;overflow:hidden}
7912
8399
  iframe{position:absolute;inset:0;width:100%;height:100%;border:0;display:block}</style>
7913
8400
  </head><body>
7914
8401
  <iframe id="gutterpress-active" src="/book.html?gutterpressshell=1" title="preview"></iframe>
7915
- <script>window.__GUTTERPRESS_HMR=${JSON.stringify(HMR_PATH)};</script>
8402
+ <script>window.__GUTTERPRESS_HMR=${JSON.stringify(HMR_PATH)};window.__GUTTERPRESS_INSTANCE=${JSON.stringify(instanceId)};window.__GUTTERPRESS_REVISION=${initialRevision};</script>
7916
8403
  <script src="/preview/scripts/preview-shell.js"></script>
7917
8404
  </body></html>`;
8405
+ }
7918
8406
  function probePort(port, host) {
7919
8407
  return new Promise((resolve10) => {
7920
8408
  const srv = net.createServer();
@@ -7956,13 +8444,14 @@ async function findAvailablePort(startPort, host = "127.0.0.1") {
7956
8444
  }
7957
8445
  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 });
7958
8446
  }
7959
- function injectHmrClient(html) {
8447
+ function injectHmrClient(html, revision, instanceId) {
8448
+ const snippet = hmrClientSnippet(revision, instanceId);
7960
8449
  const closingBody = html.lastIndexOf("</body>");
7961
8450
  if (closingBody === -1)
7962
- return html + HMR_CLIENT_SNIPPET;
7963
- return html.slice(0, closingBody) + HMR_CLIENT_SNIPPET + html.slice(closingBody);
8451
+ return html + snippet;
8452
+ return html.slice(0, closingBody) + snippet + html.slice(closingBody);
7964
8453
  }
7965
- async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders = {}) {
8454
+ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders = {}, hmrRevision = 0, hmrInstance = "") {
7966
8455
  let filePath2 = absPath;
7967
8456
  try {
7968
8457
  const s = await stat4(filePath2);
@@ -7983,7 +8472,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
7983
8472
  }
7984
8473
  const isHtml = filePath2.endsWith(".html") || filePath2.endsWith(".htm");
7985
8474
  if (isHtml) {
7986
- const withHmr = injectHmrClient(data.toString("utf-8"));
8475
+ const withHmr = injectHmrClient(data.toString("utf-8"), hmrRevision, hmrInstance);
7987
8476
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
7988
8477
  res.end(withHmr);
7989
8478
  return;
@@ -8001,21 +8490,89 @@ function matchesEmbedded(urlPathname) {
8001
8490
  return true;
8002
8491
  return EMBEDDED_PREFIXES.some((p) => urlPathname.startsWith(p));
8003
8492
  }
8004
- function isDotfileRequest(urlPathname) {
8005
- let decoded;
8006
- try {
8007
- decoded = decodeURIComponent(urlPathname);
8008
- } catch {
8009
- return true;
8010
- }
8011
- return decoded.split("/").some((segment) => segment.startsWith("."));
8012
- }
8013
8493
  async function createPreviewServer(state, port) {
8014
- const clients = new Set;
8494
+ const ACK_RETRY_MS = 2000;
8495
+ const MAX_ACK_RETRIES = 95;
8496
+ const instanceId = randomUUID();
8497
+ let reloadRevision = 0;
8498
+ const clients = new Map;
8499
+ function sendRevision(ws, type, file) {
8500
+ if (ws.readyState !== WebSocket.OPEN)
8501
+ return;
8502
+ try {
8503
+ ws.send(JSON.stringify({
8504
+ type,
8505
+ instance: instanceId,
8506
+ revision: reloadRevision,
8507
+ ...file ? { file } : {}
8508
+ }));
8509
+ } catch {}
8510
+ }
8511
+ function waitForAcknowledgement(ws) {
8512
+ const client = clients.get(ws);
8513
+ if (!client)
8514
+ return;
8515
+ if (client.acknowledgedRevision >= reloadRevision) {
8516
+ if (client.retryTimer !== null)
8517
+ clearTimeout(client.retryTimer);
8518
+ client.retryTimer = null;
8519
+ client.retryAttempts = 0;
8520
+ return;
8521
+ }
8522
+ if (client.retryTimer !== null)
8523
+ return;
8524
+ client.retryTimer = setTimeout(() => {
8525
+ const current = clients.get(ws);
8526
+ if (!current)
8527
+ return;
8528
+ current.retryTimer = null;
8529
+ if (current.retryAttempts >= MAX_ACK_RETRIES) {
8530
+ ws.terminate();
8531
+ return;
8532
+ }
8533
+ current.retryAttempts++;
8534
+ sendRevision(ws, "reload-state");
8535
+ waitForAcknowledgement(ws);
8536
+ }, ACK_RETRY_MS);
8537
+ client.retryTimer.unref?.();
8538
+ }
8539
+ function removeClient(ws) {
8540
+ const client = clients.get(ws);
8541
+ if (client && client.retryTimer !== null)
8542
+ clearTimeout(client.retryTimer);
8543
+ clients.delete(ws);
8544
+ }
8015
8545
  const wss = new WebSocketServer({ noServer: true });
8016
8546
  wss.on("connection", (ws) => {
8017
- clients.add(ws);
8018
- ws.on("close", () => clients.delete(ws));
8547
+ clients.set(ws, { acknowledgedRevision: -1, retryAttempts: 0, retryTimer: null });
8548
+ ws.on("message", (raw) => {
8549
+ let message;
8550
+ try {
8551
+ message = JSON.parse(raw.toString());
8552
+ } catch {
8553
+ return;
8554
+ }
8555
+ if (!message || typeof message !== "object")
8556
+ return;
8557
+ const payload = message;
8558
+ if (payload.type !== "reload-applied")
8559
+ return;
8560
+ const revision = Number(payload.revision);
8561
+ if (payload.instance !== instanceId || !Number.isSafeInteger(revision) || revision < 0 || revision > reloadRevision)
8562
+ return;
8563
+ const client = clients.get(ws);
8564
+ if (!client)
8565
+ return;
8566
+ client.acknowledgedRevision = Math.max(client.acknowledgedRevision, revision);
8567
+ waitForAcknowledgement(ws);
8568
+ });
8569
+ ws.on("close", () => removeClient(ws));
8570
+ ws.on("error", () => {
8571
+ removeClient(ws);
8572
+ ws.terminate();
8573
+ });
8574
+ sendRevision(ws, "reload-state");
8575
+ waitForAcknowledgement(ws);
8019
8576
  });
8020
8577
  const server = http2.createServer(async (req, res) => {
8021
8578
  const url = new URL(req.url, "http://127.0.0.1");
@@ -8038,6 +8595,34 @@ async function createPreviewServer(state, port) {
8038
8595
  res.end("Not Found");
8039
8596
  return;
8040
8597
  }
8598
+ if (url.pathname === "/__chapter" && req.method === "GET") {
8599
+ const rawFile = url.searchParams.get("file");
8600
+ const file = rawFile ? canonicalChapterId(rawFile) : "";
8601
+ const revision = Number(url.searchParams.get("revision"));
8602
+ const configuredFiles = state.config.source?.files;
8603
+ 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)) {
8604
+ res.writeHead(400);
8605
+ res.end("Bad Request");
8606
+ return;
8607
+ }
8608
+ try {
8609
+ const html = await renderChapterPreviewHtml(state.currentInputPath, file, state.config);
8610
+ if (revision !== reloadRevision) {
8611
+ res.writeHead(409);
8612
+ res.end("Superseded");
8613
+ return;
8614
+ }
8615
+ res.writeHead(200, {
8616
+ "Content-Type": "text/html; charset=utf-8",
8617
+ "Cache-Control": "no-store"
8618
+ });
8619
+ res.end(html);
8620
+ } catch (error2) {
8621
+ res.writeHead(500);
8622
+ res.end(error2 instanceof Error ? error2.message : String(error2));
8623
+ }
8624
+ return;
8625
+ }
8041
8626
  if (matchesEmbedded(url.pathname)) {
8042
8627
  if (req.headers["if-none-match"] === EMBEDDED_ETAG) {
8043
8628
  res.writeHead(304, { ETag: EMBEDDED_ETAG, "Cache-Control": EMBEDDED_CACHE_CONTROL });
@@ -8056,12 +8641,22 @@ async function createPreviewServer(state, port) {
8056
8641
  }
8057
8642
  if (url.pathname === "/" && incrementalPreviewEnabled()) {
8058
8643
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
8059
- res.end(SHELL_HTML);
8644
+ res.end(shellHtml(reloadRevision, instanceId));
8060
8645
  return;
8061
8646
  }
8062
8647
  const pathname = url.pathname === "/" ? "/book.html" : url.pathname;
8648
+ const cssAssetSource = state.cssAssets.get(pathname.replace(/^\/+/, ""));
8649
+ if (cssAssetSource) {
8650
+ await serveStatic(cssAssetSource, res).catch((err) => {
8651
+ if (!res.headersSent) {
8652
+ res.writeHead(500);
8653
+ res.end(`Internal Server Error: ${err.message}`);
8654
+ }
8655
+ });
8656
+ return;
8657
+ }
8063
8658
  const servingProjectRoot = pathname !== "/book.html";
8064
- if (servingProjectRoot && (!state.currentInputPath || isDotfileRequest(pathname))) {
8659
+ if (servingProjectRoot && (!state.currentInputPath || hasDotSegment(pathname))) {
8065
8660
  res.writeHead(404);
8066
8661
  res.end("Not Found");
8067
8662
  return;
@@ -8073,7 +8668,7 @@ async function createPreviewServer(state, port) {
8073
8668
  res.end("Not Found");
8074
8669
  return;
8075
8670
  }
8076
- await serveStatic(absPath, res).catch((err) => {
8671
+ await serveStatic(absPath, res, "no-store", {}, reloadRevision, instanceId).catch((err) => {
8077
8672
  if (!res.headersSent) {
8078
8673
  res.writeHead(500);
8079
8674
  res.end(`Internal Server Error: ${err.message}`);
@@ -8100,23 +8695,33 @@ async function createPreviewServer(state, port) {
8100
8695
  openPath(serverUrl).catch(() => {});
8101
8696
  }
8102
8697
  let stopped = false;
8103
- const broadcast = (payload) => {
8698
+ const broadcastUpdate = (type, file) => {
8104
8699
  if (stopped)
8105
8700
  return;
8106
- const message = JSON.stringify(payload);
8107
- for (const ws of clients) {
8108
- if (ws.readyState === WebSocket.OPEN)
8109
- ws.send(message);
8701
+ reloadRevision++;
8702
+ for (const [ws] of clients) {
8703
+ sendRevision(ws, type, file);
8704
+ waitForAcknowledgement(ws);
8110
8705
  }
8111
8706
  };
8707
+ const updateInFlight = () => {
8708
+ for (const client of clients.values()) {
8709
+ if (client.acknowledgedRevision < reloadRevision)
8710
+ return true;
8711
+ }
8712
+ return false;
8713
+ };
8112
8714
  return {
8113
8715
  port: boundPort,
8114
8716
  async close() {
8115
8717
  if (stopped)
8116
8718
  return;
8117
8719
  stopped = true;
8118
- for (const ws of clients)
8720
+ for (const [ws, client] of clients) {
8721
+ if (client.retryTimer !== null)
8722
+ clearTimeout(client.retryTimer);
8119
8723
  ws.terminate();
8724
+ }
8120
8725
  clients.clear();
8121
8726
  wss.close();
8122
8727
  await new Promise((resolve10) => {
@@ -8127,7 +8732,13 @@ async function createPreviewServer(state, port) {
8127
8732
  });
8128
8733
  },
8129
8734
  broadcastReload() {
8130
- broadcast({ type: "full-reload" });
8735
+ broadcastUpdate("full-reload");
8736
+ },
8737
+ broadcastContentUpdate(file) {
8738
+ if (updateInFlight())
8739
+ broadcastUpdate("full-reload");
8740
+ else
8741
+ broadcastUpdate("content-update", canonicalChapterId(file));
8131
8742
  }
8132
8743
  };
8133
8744
  }
@@ -8146,8 +8757,8 @@ async function startPreviewServer(options) {
8146
8757
  }
8147
8758
  const config = await initializeConfiguration(inputPath);
8148
8759
  const tempDir = await initializePreviewDirectories();
8149
- await generateAndWriteHtml(inputPath, tempDir, config);
8150
8760
  const state = createServerState(inputPath, tempDir, config, options);
8761
+ await generateAndWriteHtml(inputPath, tempDir, config, state.cssAssets);
8151
8762
  const restartPreview2 = async (newInputPath) => {
8152
8763
  await restartPreview(newInputPath, state);
8153
8764
  };
@@ -8180,7 +8791,10 @@ async function startPreviewServer(options) {
8180
8791
  }
8181
8792
  await shutdownServer(state);
8182
8793
  },
8183
- restart: restartPreview2
8794
+ restart: restartPreview2,
8795
+ notifySettledWrite: (filePath2, writtenContent) => {
8796
+ state.notifySettledWrite?.(filePath2, writtenContent);
8797
+ }
8184
8798
  };
8185
8799
  }
8186
8800
  // src/lib/diagnostics.ts
@@ -8437,16 +9051,77 @@ async function getSystemDiagnostics() {
8437
9051
  },
8438
9052
  tools: [chromium, ...tools],
8439
9053
  configDir: defaultConfigDir(),
8440
- docsUrl: "https://github.com/dimm-city/gutterpress/blob/main/examples/gutterpress-user-guide/08-system-setup.md"
9054
+ docsUrl: "https://github.com/dimm-city/gutterpress/blob/main/examples/gutterpress-user-guide/07-system-setup.md"
8441
9055
  };
8442
9056
  }
8443
9057
  // src/lib/project-scaffold.ts
8444
- import { access, copyFile, cp as cp2, mkdir as mkdir7, readdir as readdir6, readFile as readFile23, stat as stat5, writeFile as writeFile8 } from "node:fs/promises";
8445
- import { constants as FS, existsSync as existsSync12 } from "node:fs";
9058
+ import { access, copyFile, cp as cp2, mkdir as mkdir8, readdir as readdir6, readFile as readFile24, stat as stat5, writeFile as writeFile8 } from "node:fs/promises";
9059
+ import { constants as FS, existsSync as existsSync13 } from "node:fs";
9060
+ import path15 from "node:path";
9061
+
9062
+ // src/lib/manifest-doc.ts
9063
+ import { existsSync as existsSync12 } from "node:fs";
9064
+ import { randomUUID as randomUUID2 } from "node:crypto";
9065
+ import { mkdir as mkdir7, open, readFile as readFile23, rename as rename3, rm as rm2 } from "node:fs/promises";
8446
9066
  import path14 from "node:path";
9067
+ import { parseDocument, isSeq, YAMLSeq } from "yaml";
9068
+ function resolveManifestPath(projectDir) {
9069
+ const existing = MANIFEST_FILENAMES.find((name) => existsSync12(path14.join(projectDir, name)));
9070
+ return path14.join(projectDir, existing ?? MANIFEST_FILENAMES[0]);
9071
+ }
9072
+ async function loadManifestDoc(projectDir) {
9073
+ const file = resolveManifestPath(projectDir);
9074
+ let text = "";
9075
+ try {
9076
+ text = await readFile23(file, "utf8");
9077
+ } catch (error2) {
9078
+ if (error2.code !== "ENOENT")
9079
+ throw error2;
9080
+ }
9081
+ return { doc: parseDocument(text), file };
9082
+ }
9083
+ async function writeManifestDoc(file, doc) {
9084
+ const dir = path14.dirname(file);
9085
+ await mkdir7(dir, { recursive: true });
9086
+ const temporary = path14.join(dir, `.${path14.basename(file)}.tmp-${process.pid}-${randomUUID2()}`);
9087
+ try {
9088
+ const handle = await open(temporary, "wx", 438);
9089
+ try {
9090
+ await handle.writeFile(doc.toString(), "utf8");
9091
+ await handle.sync();
9092
+ } finally {
9093
+ await handle.close();
9094
+ }
9095
+ await rename3(temporary, file);
9096
+ const directory = await open(dir, "r").catch(() => null);
9097
+ if (directory) {
9098
+ await directory.sync().catch(() => {});
9099
+ await directory.close();
9100
+ }
9101
+ } finally {
9102
+ await rm2(temporary, { force: true });
9103
+ }
9104
+ }
9105
+ function ensureSeq(doc, key) {
9106
+ let seq = doc.get(key, true);
9107
+ if (!isSeq(seq)) {
9108
+ const fresh = new YAMLSeq(doc.schema);
9109
+ doc.set(key, fresh);
9110
+ seq = fresh;
9111
+ }
9112
+ return seq;
9113
+ }
9114
+ function scalarString(item) {
9115
+ if (item && typeof item === "object" && "value" in item) {
9116
+ const v = item.value;
9117
+ return typeof v === "string" ? v : null;
9118
+ }
9119
+ return typeof item === "string" ? item : null;
9120
+ }
9121
+
9122
+ // src/lib/project-scaffold.ts
8447
9123
  var STARTER_THEME_FOR_TEMPLATE = {
8448
9124
  book: "clean-book",
8449
- ttrpg: "ttrpg-supplement",
8450
9125
  zine: "zine",
8451
9126
  technical: "technical-doc"
8452
9127
  };
@@ -8466,6 +9141,39 @@ function escapeYamlScalar(value) {
8466
9141
  return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
8467
9142
  }
8468
9143
  var DEFAULT_AUTHOR = "Anonymous";
9144
+ function isPositivePoints(value) {
9145
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
9146
+ }
9147
+ function requirePreset(preset, customPage) {
9148
+ if (!preset || !PRESET_IDS.includes(preset)) {
9149
+ const got = preset ? ` (got "${preset}")` : "";
9150
+ throw new CreateProjectErrorImpl("preset-required", `A preset is required to create a book${got}. Choose one of: ${PRESET_IDS.join(", ")}.`);
9151
+ }
9152
+ if (preset === "custom") {
9153
+ const missing = [];
9154
+ if (!customPage || !isPositivePoints(customPage.width))
9155
+ missing.push("page width");
9156
+ if (!customPage || !isPositivePoints(customPage.height))
9157
+ missing.push("page height");
9158
+ if (missing.length > 0) {
9159
+ 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).`);
9160
+ }
9161
+ } else if (customPage && (!isPositivePoints(customPage.width) || !isPositivePoints(customPage.height))) {
9162
+ throw new CreateProjectErrorImpl("custom-page-required", "Page overrides need a positive width and height in points (72pt = 1in).");
9163
+ }
9164
+ if (customPage?.tolerance !== undefined && !isPositivePoints(customPage.tolerance)) {
9165
+ throw new CreateProjectErrorImpl("custom-page-required", "Page tolerance must be a positive number of points.");
9166
+ }
9167
+ return preset;
9168
+ }
9169
+ function resolveScaffoldTargets(requested, preset) {
9170
+ const ids = requested ?? [...PRESETS[preset].defaultTargets];
9171
+ const unknown = ids.filter((id) => !TARGETS[id]);
9172
+ if (unknown.length > 0) {
9173
+ throw new CreateProjectErrorImpl("invalid-targets", `Unknown publish target${unknown.length > 1 ? "s" : ""} "${unknown.join('", "')}". ` + `Known targets: ${TARGET_IDS.join(", ")}.`);
9174
+ }
9175
+ return [...new Set(ids)];
9176
+ }
8469
9177
  async function scaffoldProject(options) {
8470
9178
  const name = (options.name ?? "").trim();
8471
9179
  if (!name) {
@@ -8477,7 +9185,7 @@ async function scaffoldProject(options) {
8477
9185
  throw new CreateProjectErrorImpl("invalid-name", `Could not derive a valid folder name from "${name}".`);
8478
9186
  }
8479
9187
  const parentDir = options.parentDir;
8480
- if (!parentDir || !path14.isAbsolute(parentDir)) {
9188
+ if (!parentDir || !path15.isAbsolute(parentDir)) {
8481
9189
  throw new CreateProjectErrorImpl("parent-not-writable", "The save location must be an absolute path.");
8482
9190
  }
8483
9191
  try {
@@ -8485,7 +9193,7 @@ async function scaffoldProject(options) {
8485
9193
  } catch {
8486
9194
  throw new CreateProjectErrorImpl("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
8487
9195
  }
8488
- const projectDir = path14.join(parentDir, folderName);
9196
+ const projectDir = path15.join(parentDir, folderName);
8489
9197
  let targetExists = true;
8490
9198
  try {
8491
9199
  await access(projectDir, FS.F_OK);
@@ -8497,26 +9205,28 @@ async function scaffoldProject(options) {
8497
9205
  }
8498
9206
  const template = options.template ?? "book";
8499
9207
  const customTemplateDir = options.templateDir;
9208
+ const preset = customTemplateDir && options.preset === undefined ? undefined : requirePreset(options.preset, options.customPage);
9209
+ const targets = preset ? resolveScaffoldTargets(options.targets, preset) : undefined;
8500
9210
  try {
8501
- await mkdir7(projectDir, { recursive: true });
9211
+ await mkdir8(projectDir, { recursive: true });
8502
9212
  if (customTemplateDir) {
8503
9213
  const entries = await readdir6(customTemplateDir, { withFileTypes: true });
8504
9214
  for (const entry of entries) {
8505
9215
  if (entry.name === ".gutterpress-template.json")
8506
9216
  continue;
8507
- await cp2(path14.join(customTemplateDir, entry.name), path14.join(projectDir, entry.name), { recursive: true });
9217
+ await cp2(path15.join(customTemplateDir, entry.name), path15.join(projectDir, entry.name), { recursive: true });
8508
9218
  }
8509
- await mkdir7(path14.join(projectDir, "assets"), { recursive: true });
9219
+ await mkdir8(path15.join(projectDir, "assets"), { recursive: true });
8510
9220
  } else {
8511
9221
  const tplManifest = await getAssetPath(`templates/${template}/manifest.yaml`);
8512
9222
  const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
8513
- await mkdir7(path14.join(projectDir, "assets"), { recursive: true });
8514
- await copyFile(tplManifest, path14.join(projectDir, MANIFEST_FILENAMES[0]));
8515
- await copyFile(tplChapter, path14.join(projectDir, "chapter-01.md"));
9223
+ await mkdir8(path15.join(projectDir, "assets"), { recursive: true });
9224
+ await copyFile(tplManifest, path15.join(projectDir, MANIFEST_FILENAMES[0]));
9225
+ await copyFile(tplChapter, path15.join(projectDir, "chapter-01.md"));
8516
9226
  const starterThemeId = STARTER_THEME_FOR_TEMPLATE[template] ?? "clean-book";
8517
9227
  const starterCss = await getAssetPath(`themes/${starterThemeId}/theme.css`);
8518
- await mkdir7(path14.join(projectDir, "styles"), { recursive: true });
8519
- await copyFile(starterCss, path14.join(projectDir, "styles", "book.css"));
9228
+ await mkdir8(path15.join(projectDir, "styles"), { recursive: true });
9229
+ await copyFile(starterCss, path15.join(projectDir, "styles", "book.css"));
8520
9230
  }
8521
9231
  } catch (e) {
8522
9232
  throw new CreateProjectErrorImpl("scaffold-io", `Could not create the project files: ${e instanceof Error ? e.message : String(e)}`);
@@ -8526,18 +9236,34 @@ async function scaffoldProject(options) {
8526
9236
  } catch (e) {
8527
9237
  throw new CreateProjectErrorImpl("scaffold-io", `Could not write .gitignore: ${e instanceof Error ? e.message : String(e)}`);
8528
9238
  }
8529
- const manifestPath = MANIFEST_FILENAMES.map((name2) => path14.join(projectDir, name2)).find(existsSync12) ?? path14.join(projectDir, MANIFEST_FILENAMES[0]);
9239
+ const manifestPath = MANIFEST_FILENAMES.map((name2) => path15.join(projectDir, name2)).find(existsSync13) ?? path15.join(projectDir, MANIFEST_FILENAMES[0]);
8530
9240
  const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
8531
9241
  const substitutions = {
8532
9242
  "{{TITLE}}": escapeYamlScalar(name),
8533
9243
  "{{AUTHOR}}": escapeYamlScalar(author)
8534
9244
  };
8535
- let openFile = path14.join(projectDir, "chapter-01.md");
9245
+ let openFile = path15.join(projectDir, "chapter-01.md");
8536
9246
  try {
8537
9247
  await fillTemplateFile(manifestPath, substitutions);
9248
+ if (preset) {
9249
+ const { doc, file } = await loadManifestDoc(projectDir);
9250
+ doc.set("preset", preset);
9251
+ doc.set("targets", doc.createNode(targets ?? []));
9252
+ if (options.customPage) {
9253
+ const page = {
9254
+ width: options.customPage.width,
9255
+ height: options.customPage.height
9256
+ };
9257
+ if (options.customPage.tolerance !== undefined) {
9258
+ page.tolerance = options.customPage.tolerance;
9259
+ }
9260
+ doc.set("page", doc.createNode(page));
9261
+ }
9262
+ await writeManifestDoc(file, doc);
9263
+ }
8538
9264
  const firstSource = await firstSourceFile(manifestPath);
8539
9265
  if (firstSource)
8540
- openFile = path14.join(projectDir, firstSource);
9266
+ openFile = path15.join(projectDir, firstSource);
8541
9267
  await fillTemplateFile(openFile, { "{{TITLE}}": name }).catch(() => {});
8542
9268
  } catch (e) {
8543
9269
  throw new CreateProjectErrorImpl("scaffold-io", `Could not finalise the project files: ${e instanceof Error ? e.message : String(e)}`);
@@ -8547,7 +9273,7 @@ async function scaffoldProject(options) {
8547
9273
  let versionHistoryError;
8548
9274
  if (requested === "local-git") {
8549
9275
  try {
8550
- const { providerFor } = await import("./source-provider-hp96b5jf.js");
9276
+ const { providerFor } = await import("./source-provider-c1rjm2c0.js");
8551
9277
  const provider = providerFor({ type: "local-folder", path: projectDir });
8552
9278
  await provider.initVersionHistory({
8553
9279
  projectDir,
@@ -8557,9 +9283,9 @@ async function scaffoldProject(options) {
8557
9283
  versionHistory = "local-git";
8558
9284
  } catch (e) {
8559
9285
  versionHistory = "none";
8560
- const { findEnclosingRepoDir } = await import("./project-source-0tg0r6vm.js");
9286
+ const { findEnclosingRepoDir } = await import("./project-source-p0gn1wd5.js");
8561
9287
  const enclosingRepoDir = await findEnclosingRepoDir(projectDir);
8562
- 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);
9288
+ versionHistoryError = enclosingRepoDir ? `This book was added to ${path15.basename(enclosingRepoDir)}'s shared version history; save a snapshot to record it.` : e instanceof Error ? e.message : String(e);
8563
9289
  }
8564
9290
  }
8565
9291
  const result = {
@@ -8579,7 +9305,7 @@ function prettifyFolderName(base) {
8579
9305
  }
8580
9306
  async function adoptFolder(options) {
8581
9307
  const dir = options.dir;
8582
- if (typeof dir !== "string" || !path14.isAbsolute(dir)) {
9308
+ if (typeof dir !== "string" || !path15.isAbsolute(dir)) {
8583
9309
  throw new CreateProjectErrorImpl("scaffold-io", "An absolute folder path is required.");
8584
9310
  }
8585
9311
  let st;
@@ -8591,11 +9317,11 @@ async function adoptFolder(options) {
8591
9317
  if (!st.isDirectory()) {
8592
9318
  throw new CreateProjectErrorImpl("scaffold-io", `Not a folder: ${dir}`);
8593
9319
  }
8594
- if (MANIFEST_FILENAMES.some((name) => existsSync12(path14.join(dir, name)))) {
9320
+ if (MANIFEST_FILENAMES.some((name) => existsSync13(path15.join(dir, name)))) {
8595
9321
  throw new CreateProjectErrorImpl("target-exists", "This folder is already a Gutterpress project.");
8596
9322
  }
8597
9323
  const template = options.template ?? "book";
8598
- const title = (options.title ?? "").trim() || prettifyFolderName(path14.basename(dir));
9324
+ const title = (options.title ?? "").trim() || prettifyFolderName(path15.basename(dir));
8599
9325
  const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
8600
9326
  let mdFiles;
8601
9327
  try {
@@ -8603,14 +9329,14 @@ async function adoptFolder(options) {
8603
9329
  mdFiles = entries.filter((e) => e.isFile() && /\.(md|markdown)$/i.test(e.name)).map((e) => e.name).sort((a, b) => a.localeCompare(b));
8604
9330
  if (mdFiles.length === 0) {
8605
9331
  const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
8606
- await copyFile(tplChapter, path14.join(dir, "chapter-01.md"));
8607
- await fillTemplateFile(path14.join(dir, "chapter-01.md"), { "{{TITLE}}": title }).catch(() => {});
9332
+ await copyFile(tplChapter, path15.join(dir, "chapter-01.md"));
9333
+ await fillTemplateFile(path15.join(dir, "chapter-01.md"), { "{{TITLE}}": title }).catch(() => {});
8608
9334
  mdFiles = ["chapter-01.md"];
8609
9335
  }
8610
9336
  const themeId = STARTER_THEME_FOR_TEMPLATE[template] ?? "clean-book";
8611
- await mkdir7(path14.join(dir, "styles"), { recursive: true });
8612
- const bookCssPath = path14.join(dir, "styles", "book.css");
8613
- if (!existsSync12(bookCssPath)) {
9337
+ await mkdir8(path15.join(dir, "styles"), { recursive: true });
9338
+ const bookCssPath = path15.join(dir, "styles", "book.css");
9339
+ if (!existsSync13(bookCssPath)) {
8614
9340
  await copyFile(await getAssetPath(`themes/${themeId}/theme.css`), bookCssPath);
8615
9341
  }
8616
9342
  const filesYaml = mdFiles.map((f) => ` - "${escapeYamlScalar(f)}"`).join(`
@@ -8618,14 +9344,17 @@ async function adoptFolder(options) {
8618
9344
  const manifest = `title: "${escapeYamlScalar(title)}"
8619
9345
  authors:
8620
9346
  - "${escapeYamlScalar(author)}"
9347
+ preset: dtrpg
9348
+ targets:
9349
+ - dtrpg
8621
9350
  source:
8622
9351
  files:
8623
9352
  ${filesYaml}
8624
9353
  styles:
8625
9354
  - styles/book.css
8626
9355
  `;
8627
- await writeFile8(path14.join(dir, MANIFEST_FILENAMES[0]), manifest, "utf8");
8628
- await mkdir7(path14.join(dir, "assets"), { recursive: true });
9356
+ await writeFile8(path15.join(dir, MANIFEST_FILENAMES[0]), manifest, "utf8");
9357
+ await mkdir8(path15.join(dir, "assets"), { recursive: true });
8629
9358
  await ensureGitignoreHasDist(dir);
8630
9359
  } catch (e) {
8631
9360
  if (e instanceof CreateProjectErrorImpl)
@@ -8637,7 +9366,7 @@ styles:
8637
9366
  let versionHistoryError;
8638
9367
  if (requested === "local-git") {
8639
9368
  try {
8640
- const { providerFor } = await import("./source-provider-hp96b5jf.js");
9369
+ const { providerFor } = await import("./source-provider-c1rjm2c0.js");
8641
9370
  const provider = providerFor({ type: "local-folder", path: dir });
8642
9371
  await provider.initVersionHistory({
8643
9372
  projectDir: dir,
@@ -8652,8 +9381,8 @@ styles:
8652
9381
  }
8653
9382
  const result = {
8654
9383
  projectDir: dir,
8655
- manifestPath: path14.join(dir, MANIFEST_FILENAMES[0]),
8656
- openFile: path14.join(dir, mdFiles[0]),
9384
+ manifestPath: path15.join(dir, MANIFEST_FILENAMES[0]),
9385
+ openFile: path15.join(dir, mdFiles[0]),
8657
9386
  versionHistory
8658
9387
  };
8659
9388
  if (versionHistoryError !== undefined)
@@ -8661,10 +9390,10 @@ styles:
8661
9390
  return result;
8662
9391
  }
8663
9392
  async function ensureGitignoreHasDist(projectDir) {
8664
- const gitignorePath = path14.join(projectDir, ".gitignore");
9393
+ const gitignorePath = path15.join(projectDir, ".gitignore");
8665
9394
  let existing = null;
8666
9395
  try {
8667
- existing = await readFile23(gitignorePath, "utf8");
9396
+ existing = await readFile24(gitignorePath, "utf8");
8668
9397
  } catch {}
8669
9398
  if (existing === null) {
8670
9399
  await writeFile8(gitignorePath, `dist/
@@ -8684,7 +9413,7 @@ async function ensureGitignoreHasDist(projectDir) {
8684
9413
  async function firstSourceFile(manifestPath) {
8685
9414
  let text;
8686
9415
  try {
8687
- text = await readFile23(manifestPath, "utf8");
9416
+ text = await readFile24(manifestPath, "utf8");
8688
9417
  } catch {
8689
9418
  return;
8690
9419
  }
@@ -8696,32 +9425,45 @@ async function firstSourceFile(manifestPath) {
8696
9425
  return m ? m[1].trim() : undefined;
8697
9426
  }
8698
9427
  async function fillTemplateFile(filePath2, substitutions) {
8699
- let text = await readFile23(filePath2, "utf8");
9428
+ let text = await readFile24(filePath2, "utf8");
8700
9429
  for (const [token, value] of Object.entries(substitutions)) {
8701
9430
  text = text.split(token).join(value);
8702
9431
  }
8703
9432
  await writeFile8(filePath2, text, "utf8");
8704
9433
  }
8705
9434
  // src/lib/project-templates.ts
8706
- import { cp as cp3, mkdir as mkdir8, readFile as readFile24, readdir as readdir7, stat as stat6, writeFile as writeFile9 } from "node:fs/promises";
8707
- import { constants as FS2 } from "node:fs";
9435
+ import { cp as cp3, mkdir as mkdir9, readFile as readFile25, readdir as readdir7, stat as stat6, writeFile as writeFile9 } from "node:fs/promises";
9436
+ import { constants as FS2, existsSync as existsSync14 } from "node:fs";
8708
9437
  import { access as access2 } from "node:fs/promises";
8709
- import path15 from "node:path";
9438
+ import path16 from "node:path";
9439
+ import { isMap, isScalar, isSeq as isSeq2, parseDocument as parseDocument2 } from "yaml";
8710
9440
  var BUILT_IN_TEMPLATE_IDS = [
8711
9441
  "book",
8712
- "ttrpg",
8713
9442
  "zine",
8714
9443
  "technical"
8715
9444
  ];
9445
+ async function readTemplateChoices(manifestPath) {
9446
+ try {
9447
+ const text = await readFile25(manifestPath, "utf8");
9448
+ const doc = parseDocument2(text);
9449
+ const out = {};
9450
+ const preset = doc.get("preset");
9451
+ if (typeof preset === "string" && preset)
9452
+ out.preset = preset;
9453
+ const targets = doc.get("targets", true);
9454
+ if (isSeq2(targets)) {
9455
+ out.targets = targets.items.map((i) => scalarString(i)).filter((s) => s !== null);
9456
+ }
9457
+ return out;
9458
+ } catch {
9459
+ return {};
9460
+ }
9461
+ }
8716
9462
  var BUILT_IN_META = {
8717
9463
  book: {
8718
9464
  label: "Book",
8719
9465
  description: "A clean starting point for a novel, memoir, or any long-form book."
8720
9466
  },
8721
- ttrpg: {
8722
- label: "TTRPG supplement",
8723
- description: "Rules, stat blocks, and tables for a tabletop roleplaying supplement."
8724
- },
8725
9467
  zine: {
8726
9468
  label: "Zine",
8727
9469
  description: "A short, personal zine made to be printed, folded, and shared."
@@ -8732,12 +9474,13 @@ var BUILT_IN_META = {
8732
9474
  }
8733
9475
  };
8734
9476
  async function listBuiltInTemplates() {
8735
- return BUILT_IN_TEMPLATE_IDS.map((id) => ({
9477
+ return Promise.all(BUILT_IN_TEMPLATE_IDS.map(async (id) => ({
8736
9478
  id,
8737
9479
  label: BUILT_IN_META[id].label,
8738
9480
  description: BUILT_IN_META[id].description,
8739
- kind: "builtin"
8740
- }));
9481
+ kind: "builtin",
9482
+ ...await readTemplateChoices(await getAssetPath(`templates/${id}/manifest.yaml`))
9483
+ })));
8741
9484
  }
8742
9485
  var SKIP_ENTRIES = new Set([
8743
9486
  ".git",
@@ -8748,13 +9491,100 @@ var SKIP_ENTRIES = new Set([
8748
9491
  ".gutterpress",
8749
9492
  ".DS_Store"
8750
9493
  ]);
9494
+ var VENDOR_DIR = "shared";
9495
+ function collectEscaping(nodes, getValue, escapes) {
9496
+ const out = [];
9497
+ for (const node of nodes) {
9498
+ const value = getValue(node);
9499
+ if (value !== null && escapes(value))
9500
+ out.push({ node, value });
9501
+ }
9502
+ return out;
9503
+ }
9504
+ function pluginEntryPath(node) {
9505
+ return isMap(node) ? scalarString(node.get("path", true)) : scalarString(node);
9506
+ }
9507
+ function dropEscaping(doc, seq, key, escaping) {
9508
+ if (!isSeq2(seq))
9509
+ return;
9510
+ const drop = new Set(escaping.map((e) => e.node));
9511
+ seq.items = seq.items.filter((node) => !drop.has(node));
9512
+ if (seq.items.length === 0)
9513
+ doc.delete(key);
9514
+ }
9515
+ function rewriteEntry(node, newPath) {
9516
+ if (isMap(node))
9517
+ node.set("path", newPath);
9518
+ else if (isScalar(node))
9519
+ node.value = newPath;
9520
+ }
9521
+ function commonAncestorDir(absFiles) {
9522
+ const dirSegs = absFiles.map((f) => path16.resolve(f).split(path16.sep).slice(0, -1));
9523
+ if (dirSegs.length === 0)
9524
+ return "";
9525
+ let common = dirSegs[0];
9526
+ for (const segs of dirSegs.slice(1)) {
9527
+ let i = 0;
9528
+ while (i < common.length && i < segs.length && common[i] === segs[i])
9529
+ i++;
9530
+ common = common.slice(0, i);
9531
+ }
9532
+ return common.join(path16.sep) || path16.sep;
9533
+ }
9534
+ function pickVendorDir(templateDir, base) {
9535
+ const preferred = path16.basename(base) || VENDOR_DIR;
9536
+ if (!existsSync14(path16.join(templateDir, preferred)))
9537
+ return preferred;
9538
+ for (let n = 1;; n++) {
9539
+ const candidate = `${preferred}-${n}`;
9540
+ if (!existsSync14(path16.join(templateDir, candidate)))
9541
+ return candidate;
9542
+ }
9543
+ }
8751
9544
  var TEMPLATE_META_FILE = ".gutterpress-template.json";
9545
+ async function reconcileSharedRefs(templateDir, sourceProjectDir, mode) {
9546
+ const { doc, file } = await loadManifestDoc(templateDir);
9547
+ const escapes = (p) => escapesProjectRoot(sourceProjectDir, path16.resolve(sourceProjectDir, p));
9548
+ const stylesSeq = doc.get("styles", true);
9549
+ const pluginsSeq = doc.get("plugins", true);
9550
+ const escapingStyles = collectEscaping(isSeq2(stylesSeq) ? stylesSeq.items : [], scalarString, escapes);
9551
+ const escapingPlugins = collectEscaping(isSeq2(pluginsSeq) ? pluginsSeq.items : [], pluginEntryPath, escapes);
9552
+ if (escapingStyles.length === 0 && escapingPlugins.length === 0) {
9553
+ return { vendoredRefs: [], excludedRefs: [] };
9554
+ }
9555
+ if (mode === "exclude") {
9556
+ const excludedRefs = [...escapingStyles, ...escapingPlugins].map((e) => e.value);
9557
+ dropEscaping(doc, stylesSeq, "styles", escapingStyles);
9558
+ dropEscaping(doc, pluginsSeq, "plugins", escapingPlugins);
9559
+ await writeManifestDoc(file, doc);
9560
+ return { vendoredRefs: [], excludedRefs };
9561
+ }
9562
+ const styleClosure = await collectStyleDependencies(sourceProjectDir, escapingStyles.map((s) => s.value));
9563
+ const pluginFiles = escapingPlugins.map((p) => path16.resolve(sourceProjectDir, p.value));
9564
+ const externalFiles = [...new Set([...styleClosure, ...pluginFiles])].filter((f) => escapesProjectRoot(sourceProjectDir, f));
9565
+ const base = commonAncestorDir(externalFiles);
9566
+ const vendorDir = pickVendorDir(templateDir, base);
9567
+ await Promise.all(externalFiles.filter((abs2) => existsSync14(abs2)).map(async (abs2) => {
9568
+ const dest = path16.join(templateDir, vendorDir, path16.relative(base, abs2));
9569
+ await mkdir9(path16.dirname(dest), { recursive: true });
9570
+ await cp3(abs2, dest);
9571
+ }));
9572
+ const bookLocal = (value) => toPosixPath(path16.join(vendorDir, path16.relative(base, path16.resolve(sourceProjectDir, value))));
9573
+ const vendoredRefs = [];
9574
+ for (const { node, value } of [...escapingStyles, ...escapingPlugins]) {
9575
+ const rewritten = bookLocal(value);
9576
+ rewriteEntry(node, rewritten);
9577
+ vendoredRefs.push(rewritten);
9578
+ }
9579
+ await writeManifestDoc(file, doc);
9580
+ return { vendoredRefs, excludedRefs: [] };
9581
+ }
8752
9582
  async function saveProjectAsTemplate(options) {
8753
9583
  const { projectDir, name, templatesRoot } = options;
8754
9584
  const id = slugify(name);
8755
9585
  if (!id)
8756
9586
  throw new Error(`Could not derive a template id from "${name}".`);
8757
- const dir = path15.join(templatesRoot, id);
9587
+ const dir = path16.join(templatesRoot, id);
8758
9588
  let exists = true;
8759
9589
  try {
8760
9590
  await access2(dir, FS2.F_OK);
@@ -8764,31 +9594,33 @@ async function saveProjectAsTemplate(options) {
8764
9594
  if (exists) {
8765
9595
  throw new Error(`A template named "${name}" already exists. Choose a different name.`);
8766
9596
  }
8767
- await mkdir8(dir, { recursive: true });
9597
+ await mkdir9(dir, { recursive: true });
8768
9598
  const entries = await readdir7(projectDir, { withFileTypes: true });
8769
9599
  for (const entry of entries) {
8770
9600
  if (SKIP_ENTRIES.has(entry.name))
8771
9601
  continue;
8772
9602
  if (entry.name === TEMPLATE_META_FILE)
8773
9603
  continue;
8774
- await cp3(path15.join(projectDir, entry.name), path15.join(dir, entry.name), { recursive: true });
9604
+ await cp3(path16.join(projectDir, entry.name), path16.join(dir, entry.name), { recursive: true });
8775
9605
  }
8776
9606
  const manifestName = MANIFEST_FILENAMES.find((name2) => entries.some((entry) => entry.name === name2));
9607
+ const shared = manifestName ? await reconcileSharedRefs(dir, projectDir, options.sharedRefs ?? "vendor") : { vendoredRefs: [], excludedRefs: [] };
8777
9608
  if (manifestName)
8778
- await retokeniseManifest(path15.join(dir, manifestName));
8779
- await writeFile9(path15.join(dir, TEMPLATE_META_FILE), JSON.stringify({ label: name }, null, 2), "utf8");
9609
+ await retokeniseManifest(path16.join(dir, manifestName));
9610
+ await writeFile9(path16.join(dir, TEMPLATE_META_FILE), JSON.stringify({ label: name }, null, 2), "utf8");
8780
9611
  return {
8781
9612
  id,
8782
9613
  label: name,
8783
9614
  description: "Your saved template.",
8784
9615
  kind: "custom",
8785
- dir
9616
+ dir,
9617
+ ...shared
8786
9618
  };
8787
9619
  }
8788
9620
  async function retokeniseManifest(manifestPath) {
8789
9621
  let text;
8790
9622
  try {
8791
- text = await readFile24(manifestPath, "utf8");
9623
+ text = await readFile25(manifestPath, "utf8");
8792
9624
  } catch {
8793
9625
  return;
8794
9626
  }
@@ -8806,14 +9638,21 @@ async function listCustomTemplates(templatesRoot) {
8806
9638
  for (const entry of entries) {
8807
9639
  if (!entry.isDirectory())
8808
9640
  continue;
8809
- const dir = path15.join(templatesRoot, entry.name);
9641
+ const dir = path16.join(templatesRoot, entry.name);
8810
9642
  let label = prettify(entry.name);
8811
9643
  try {
8812
- const meta = JSON.parse(await readFile24(path15.join(dir, TEMPLATE_META_FILE), "utf8"));
9644
+ const meta = JSON.parse(await readFile25(path16.join(dir, TEMPLATE_META_FILE), "utf8"));
8813
9645
  if (typeof meta.label === "string" && meta.label)
8814
9646
  label = meta.label;
8815
9647
  } catch {}
8816
- out.push({ id: entry.name, label, description: "Your saved template.", kind: "custom", dir });
9648
+ out.push({
9649
+ id: entry.name,
9650
+ label,
9651
+ description: "Your saved template.",
9652
+ kind: "custom",
9653
+ dir,
9654
+ ...await readTemplateChoices(path16.join(dir, MANIFEST_FILENAMES[0]))
9655
+ });
8817
9656
  }
8818
9657
  out.sort((a, b) => a.label.localeCompare(b.label));
8819
9658
  return out;
@@ -8824,12 +9663,12 @@ async function importTemplateFromFolder(options) {
8824
9663
  if (!srcStat.isDirectory()) {
8825
9664
  throw new Error("The chosen path is not a folder.");
8826
9665
  }
8827
- const name = options.name?.trim() || prettify(path15.basename(sourceDir));
9666
+ const name = options.name?.trim() || prettify(path16.basename(sourceDir));
8828
9667
  return saveProjectAsTemplate({ projectDir: sourceDir, name, templatesRoot });
8829
9668
  }
8830
9669
  // src/lib/snippets.ts
8831
- import { readdir as readdir8, readFile as readFile25, writeFile as writeFile10, mkdir as mkdir9, rm as rm2 } from "node:fs/promises";
8832
- import path16 from "node:path";
9670
+ import { readdir as readdir8, readFile as readFile26, writeFile as writeFile10, mkdir as mkdir10, rm as rm3 } from "node:fs/promises";
9671
+ import path17 from "node:path";
8833
9672
  var SNIPPETS_DIR = "snippets";
8834
9673
  var PLACEHOLDER_RE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
8835
9674
  function extractVariables(template) {
@@ -8848,15 +9687,15 @@ function substituteVariables(template, values) {
8848
9687
  return template.replace(PLACEHOLDER_RE, (_full, name) => Object.prototype.hasOwnProperty.call(values, name) ? values[name] : "");
8849
9688
  }
8850
9689
  function resolveSnippetPath(projectDir, fileName) {
8851
- const dir = path16.resolve(projectDir, SNIPPETS_DIR);
8852
- const full = path16.resolve(dir, fileName);
8853
- if (full !== path16.join(dir, path16.basename(fileName)) || path16.dirname(full) !== dir) {
9690
+ const dir = path17.resolve(projectDir, SNIPPETS_DIR);
9691
+ const full = path17.resolve(dir, fileName);
9692
+ if (full !== path17.join(dir, path17.basename(fileName)) || path17.dirname(full) !== dir) {
8854
9693
  throw new Error(`Unsafe snippet filename: ${fileName}`);
8855
9694
  }
8856
9695
  return full;
8857
9696
  }
8858
9697
  async function listSnippets(projectDir) {
8859
- const dir = path16.join(projectDir, SNIPPETS_DIR);
9698
+ const dir = path17.join(projectDir, SNIPPETS_DIR);
8860
9699
  let names;
8861
9700
  try {
8862
9701
  names = await readdir8(dir);
@@ -8869,7 +9708,7 @@ async function listSnippets(projectDir) {
8869
9708
  continue;
8870
9709
  let body = "";
8871
9710
  try {
8872
- body = await readFile25(path16.join(dir, fileName), "utf8");
9711
+ body = await readFile26(path17.join(dir, fileName), "utf8");
8873
9712
  } catch {
8874
9713
  continue;
8875
9714
  }
@@ -8883,89 +9722,29 @@ async function listSnippets(projectDir) {
8883
9722
  return entries;
8884
9723
  }
8885
9724
  async function readSnippet(projectDir, fileName) {
8886
- return readFile25(resolveSnippetPath(projectDir, fileName), "utf8");
9725
+ return readFile26(resolveSnippetPath(projectDir, fileName), "utf8");
8887
9726
  }
8888
9727
  async function saveSnippet(projectDir, name, body) {
8889
9728
  const stem = slugify(name);
8890
9729
  if (!stem)
8891
9730
  throw new Error(`Could not derive a filename from "${name}".`);
8892
9731
  const fileName = `${stem}.md`;
8893
- const dir = path16.join(projectDir, SNIPPETS_DIR);
8894
- await mkdir9(dir, { recursive: true });
9732
+ const dir = path17.join(projectDir, SNIPPETS_DIR);
9733
+ await mkdir10(dir, { recursive: true });
8895
9734
  await writeFile10(resolveSnippetPath(projectDir, fileName), body, "utf8");
8896
9735
  return { name, fileName, variables: extractVariables(body) };
8897
9736
  }
8898
9737
  async function deleteSnippet(projectDir, fileName) {
8899
- await rm2(resolveSnippetPath(projectDir, fileName), { force: true });
9738
+ await rm3(resolveSnippetPath(projectDir, fileName), { force: true });
8900
9739
  }
8901
9740
  // src/lib/plugin-manager.ts
8902
- import { randomUUID as randomUUID3 } from "node:crypto";
9741
+ import { randomUUID as randomUUID4 } from "node:crypto";
8903
9742
  import { cp as cp4, lstat as lstat3, mkdir as mkdir12, rename as rename5, rm as rm5, stat as stat7 } from "node:fs/promises";
8904
9743
  import path19 from "node:path";
8905
- import { isSeq as isSeq2, isMap, isScalar, YAMLMap, Scalar } from "yaml";
8906
-
8907
- // src/lib/manifest-doc.ts
8908
- import { existsSync as existsSync13 } from "node:fs";
8909
- import { randomUUID } from "node:crypto";
8910
- import { mkdir as mkdir10, open, readFile as readFile26, rename as rename3, rm as rm3 } from "node:fs/promises";
8911
- import path17 from "node:path";
8912
- import { parseDocument, isSeq, YAMLSeq } from "yaml";
8913
- function resolveManifestPath(projectDir) {
8914
- const existing = MANIFEST_FILENAMES.find((name) => existsSync13(path17.join(projectDir, name)));
8915
- return path17.join(projectDir, existing ?? MANIFEST_FILENAMES[0]);
8916
- }
8917
- async function loadManifestDoc(projectDir) {
8918
- const file = resolveManifestPath(projectDir);
8919
- let text = "";
8920
- try {
8921
- text = await readFile26(file, "utf8");
8922
- } catch (error2) {
8923
- if (error2.code !== "ENOENT")
8924
- throw error2;
8925
- }
8926
- return { doc: parseDocument(text), file };
8927
- }
8928
- async function writeManifestDoc(file, doc) {
8929
- const dir = path17.dirname(file);
8930
- await mkdir10(dir, { recursive: true });
8931
- const temporary = path17.join(dir, `.${path17.basename(file)}.tmp-${process.pid}-${randomUUID()}`);
8932
- try {
8933
- const handle = await open(temporary, "wx", 438);
8934
- try {
8935
- await handle.writeFile(doc.toString(), "utf8");
8936
- await handle.sync();
8937
- } finally {
8938
- await handle.close();
8939
- }
8940
- await rename3(temporary, file);
8941
- const directory = await open(dir, "r").catch(() => null);
8942
- if (directory) {
8943
- await directory.sync().catch(() => {});
8944
- await directory.close();
8945
- }
8946
- } finally {
8947
- await rm3(temporary, { force: true });
8948
- }
8949
- }
8950
- function ensureSeq(doc, key) {
8951
- let seq = doc.get(key, true);
8952
- if (!isSeq(seq)) {
8953
- const fresh = new YAMLSeq(doc.schema);
8954
- doc.set(key, fresh);
8955
- seq = fresh;
8956
- }
8957
- return seq;
8958
- }
8959
- function scalarString(item) {
8960
- if (item && typeof item === "object" && "value" in item) {
8961
- const v = item.value;
8962
- return typeof v === "string" ? v : null;
8963
- }
8964
- return typeof item === "string" ? item : null;
8965
- }
9744
+ import { isSeq as isSeq3, isMap as isMap2, isScalar as isScalar2, YAMLMap, Scalar } from "yaml";
8966
9745
 
8967
9746
  // src/lib/npm-plugin-installer.ts
8968
- import { createHash as createHash4, randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
9747
+ import { createHash as createHash4, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
8969
9748
  import { createReadStream as createReadStream2 } from "node:fs";
8970
9749
  import {
8971
9750
  lstat as lstat2,
@@ -9641,7 +10420,7 @@ async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
9641
10420
  }
9642
10421
  try {
9643
10422
  await lstat2(finalRoot);
9644
- backupRoot = `${finalRoot}.backup-${randomUUID2()}`;
10423
+ backupRoot = `${finalRoot}.backup-${randomUUID3()}`;
9645
10424
  await rename4(finalRoot, backupRoot);
9646
10425
  } catch (error2) {
9647
10426
  if (error2.code !== "ENOENT")
@@ -9730,9 +10509,9 @@ function isLocalRef2(ref) {
9730
10509
  return ref.startsWith("./") || ref.startsWith("../") || ref.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(ref) || /\.(m?js|cjs|ts)$/i.test(ref) || !ref.startsWith("@") && ref.includes("/");
9731
10510
  }
9732
10511
  function itemRef(item) {
9733
- if (isScalar(item))
10512
+ if (isScalar2(item))
9734
10513
  return typeof item.value === "string" ? item.value : null;
9735
- if (isMap(item)) {
10514
+ if (isMap2(item)) {
9736
10515
  const p = item.get("path");
9737
10516
  const n = item.get("name");
9738
10517
  if (typeof p === "string")
@@ -9748,7 +10527,7 @@ function refKind(ref) {
9748
10527
  async function listProjectPlugins(projectDir) {
9749
10528
  const { doc } = await loadManifestDoc(projectDir);
9750
10529
  const seq = doc.get("plugins", true);
9751
- if (!isSeq2(seq))
10530
+ if (!isSeq3(seq))
9752
10531
  return [];
9753
10532
  const out = [];
9754
10533
  for (const item of seq.items) {
@@ -9759,7 +10538,7 @@ async function listProjectPlugins(projectDir) {
9759
10538
  let kind;
9760
10539
  let version;
9761
10540
  let exportName;
9762
- if (isMap(item)) {
10541
+ if (isMap2(item)) {
9763
10542
  const e = item.get("enabled");
9764
10543
  if (e === false)
9765
10544
  enabled = false;
@@ -9797,7 +10576,7 @@ async function setPluginEnabled(projectDir, ref, enabled) {
9797
10576
  async function setPluginEnabledUnlocked(projectDir, ref, enabled) {
9798
10577
  const { doc, file } = await loadManifestDoc(projectDir);
9799
10578
  const seq = doc.get("plugins", true);
9800
- if (!isSeq2(seq)) {
10579
+ if (!isSeq3(seq)) {
9801
10580
  throw new Error(`No plugins are configured in ${path19.basename(file)}.`);
9802
10581
  }
9803
10582
  const idx = indexOfRef(seq, ref);
@@ -9806,7 +10585,7 @@ async function setPluginEnabledUnlocked(projectDir, ref, enabled) {
9806
10585
  }
9807
10586
  const item = seq.items[idx];
9808
10587
  let map;
9809
- if (isMap(item)) {
10588
+ if (isMap2(item)) {
9810
10589
  map = item;
9811
10590
  } else {
9812
10591
  map = new YAMLMap(doc.schema);
@@ -9834,7 +10613,7 @@ async function recordInstalledNpmPlugin(projectDir, name, version, exportName) {
9834
10613
  const seq = ensureSeq(doc, "plugins");
9835
10614
  const idx = indexOfRef(seq, name);
9836
10615
  let map;
9837
- if (idx >= 0 && isMap(seq.items[idx])) {
10616
+ if (idx >= 0 && isMap2(seq.items[idx])) {
9838
10617
  map = seq.items[idx];
9839
10618
  } else {
9840
10619
  map = new YAMLMap(doc.schema);
@@ -9920,8 +10699,8 @@ async function addLocalPluginUnlocked(projectDir, sourcePath) {
9920
10699
  const destDir = path19.join(projectDir, PLUGINS_DIR);
9921
10700
  await mkdir12(destDir, { recursive: true });
9922
10701
  const dest = path19.join(destDir, base);
9923
- const staging = path19.join(destDir, `.import-${randomUUID3()}`);
9924
- const backup = path19.join(destDir, `.backup-${randomUUID3()}`);
10702
+ const staging = path19.join(destDir, `.import-${randomUUID4()}`);
10703
+ const backup = path19.join(destDir, `.backup-${randomUUID4()}`);
9925
10704
  let hasBackup = false;
9926
10705
  try {
9927
10706
  if (info2.isDirectory()) {
@@ -9997,13 +10776,12 @@ async function validateProjectPlugins(projectDir) {
9997
10776
  }
9998
10777
  // src/lib/theme-manager.ts
9999
10778
  import { cp as cp5, mkdir as mkdir13, readFile as readFile28, readdir as readdir9, stat as stat8, writeFile as writeFile11, rm as rm6 } from "node:fs/promises";
10000
- import { existsSync as existsSync14 } from "node:fs";
10779
+ import { existsSync as existsSync15 } from "node:fs";
10001
10780
  import path20 from "node:path";
10002
- import { isSeq as isSeq3, Scalar as Scalar2 } from "yaml";
10781
+ import { isSeq as isSeq4, Scalar as Scalar2 } from "yaml";
10003
10782
  var THEMES_DIR = "themes";
10004
10783
  var BUILT_IN_THEME_IDS = [
10005
10784
  "clean-book",
10006
- "ttrpg-supplement",
10007
10785
  "zine",
10008
10786
  "technical-doc"
10009
10787
  ];
@@ -10012,10 +10790,6 @@ var BUILT_IN_FALLBACK_META = {
10012
10790
  name: "Clean Book",
10013
10791
  description: "A calm, classic book look: serif body, generous margins."
10014
10792
  },
10015
- "ttrpg-supplement": {
10016
- name: "TTRPG Supplement",
10017
- description: "Bold display headings, parchment fills, boxed stat blocks."
10018
- },
10019
10793
  zine: {
10020
10794
  name: "Zine",
10021
10795
  description: "High-contrast, punchy sans-serif for short printed zines."
@@ -10077,7 +10851,7 @@ async function resolveBuiltInTheme(id) {
10077
10851
  }
10078
10852
  const cssPath = await getAssetPath(`${THEMES_DIR}/${id}/theme.css`);
10079
10853
  const jsonPath = await getAssetPath(`${THEMES_DIR}/${id}/theme.json`);
10080
- if (!existsSync14(cssPath)) {
10854
+ if (!existsSync15(cssPath)) {
10081
10855
  throw new Error(`Built-in theme "${id}" is missing its theme.css.`);
10082
10856
  }
10083
10857
  const meta = await readThemeMeta(jsonPath);
@@ -10101,7 +10875,7 @@ async function listProjectThemes(projectDir) {
10101
10875
  if (!entry.isDirectory())
10102
10876
  continue;
10103
10877
  const dir = path20.join(root, entry.name);
10104
- if (!existsSync14(path20.join(dir, "theme.css")))
10878
+ if (!existsSync15(path20.join(dir, "theme.css")))
10105
10879
  continue;
10106
10880
  const meta = await readThemeMeta(path20.join(dir, "theme.json"));
10107
10881
  out.push(themeInfo(entry.name, "project", meta));
@@ -10126,7 +10900,7 @@ function activeThemeIdInSeq(seq) {
10126
10900
  async function getActiveTheme(projectDir) {
10127
10901
  const { doc } = await loadManifestDoc(projectDir);
10128
10902
  const seq = doc.get("styles", true);
10129
- if (!isSeq3(seq))
10903
+ if (!isSeq4(seq))
10130
10904
  return null;
10131
10905
  for (const item of seq.items) {
10132
10906
  const href = scalarString(item);
@@ -10137,7 +10911,7 @@ async function getActiveTheme(projectDir) {
10137
10911
  continue;
10138
10912
  const id = m[1];
10139
10913
  const dir = path20.join(projectDir, THEMES_DIR, id);
10140
- if (!existsSync14(path20.join(dir, "theme.css")))
10914
+ if (!existsSync15(path20.join(dir, "theme.css")))
10141
10915
  continue;
10142
10916
  const meta = await readThemeMeta(path20.join(dir, "theme.json"));
10143
10917
  return themeInfo(id, "project", meta);
@@ -10174,7 +10948,7 @@ async function getPreviousTheme(projectDir) {
10174
10948
  if (!id)
10175
10949
  return null;
10176
10950
  const dir = themeDirFor(projectDir, id);
10177
- if (!existsSync14(path20.join(dir, "theme.css")))
10951
+ if (!existsSync15(path20.join(dir, "theme.css")))
10178
10952
  return null;
10179
10953
  const active = await getActiveTheme(projectDir);
10180
10954
  if (active && active.id === id)
@@ -10194,7 +10968,7 @@ async function applyTheme(projectDir, target) {
10194
10968
  if (target.kind === "builtin") {
10195
10969
  const resolved = await resolveBuiltInTheme(target.id);
10196
10970
  let destId = target.id;
10197
- if (existsSync14(path20.join(projectDir, THEMES_DIR, destId, "theme.css"))) {
10971
+ if (existsSync15(path20.join(projectDir, THEMES_DIR, destId, "theme.css"))) {
10198
10972
  destId = await uniqueThemeId(projectDir, target.id);
10199
10973
  }
10200
10974
  const destDir = path20.join(projectDir, THEMES_DIR, destId);
@@ -10203,7 +10977,7 @@ async function applyTheme(projectDir, target) {
10203
10977
  info2 = themeInfo(destId, "project", await readThemeMeta(path20.join(destDir, "theme.json")));
10204
10978
  } else {
10205
10979
  const dir = themeDirFor(projectDir, target.id);
10206
- if (!existsSync14(path20.join(dir, "theme.css"))) {
10980
+ if (!existsSync15(path20.join(dir, "theme.css"))) {
10207
10981
  throw new Error(`Theme "${target.id}" is not present in this project.`);
10208
10982
  }
10209
10983
  info2 = themeInfo(target.id, "project", await readThemeMeta(path20.join(dir, "theme.json")));
@@ -10215,7 +10989,7 @@ async function uniqueThemeId(projectDir, base) {
10215
10989
  const root = path20.join(projectDir, THEMES_DIR);
10216
10990
  let id = themeSlug(base);
10217
10991
  let n = 2;
10218
- while (existsSync14(path20.join(root, id))) {
10992
+ while (existsSync15(path20.join(root, id))) {
10219
10993
  id = `${themeSlug(base)}-${n++}`;
10220
10994
  }
10221
10995
  return id;
@@ -10230,7 +11004,7 @@ async function importThemeFromFolder(projectDir, sourceDir) {
10230
11004
  if (!info2.isDirectory()) {
10231
11005
  throw new Error("The chosen path is not a folder.");
10232
11006
  }
10233
- if (!existsSync14(path20.join(sourceDir, "theme.css"))) {
11007
+ if (!existsSync15(path20.join(sourceDir, "theme.css"))) {
10234
11008
  throw new Error("A theme folder must contain a theme.css file.");
10235
11009
  }
10236
11010
  const meta = await readThemeMeta(path20.join(sourceDir, "theme.json"));
@@ -10323,20 +11097,20 @@ async function readThemeCss(projectDir, source) {
10323
11097
  if (!projectDir)
10324
11098
  throw new Error("A project is required to read a project theme.");
10325
11099
  const cssPath = path20.join(themeDirFor(projectDir, source.id), "theme.css");
10326
- if (!existsSync14(cssPath)) {
11100
+ if (!existsSync15(cssPath)) {
10327
11101
  throw new Error(`Theme "${source.id}" has no theme.css in this project.`);
10328
11102
  }
10329
11103
  return readFile28(cssPath, "utf8");
10330
11104
  }
10331
11105
  async function removeProjectTheme(projectDir, id) {
10332
11106
  const dir = themeDirFor(projectDir, id);
10333
- if (existsSync14(dir)) {
11107
+ if (existsSync15(dir)) {
10334
11108
  await rm6(dir, { recursive: true, force: true });
10335
11109
  }
10336
11110
  const { doc, file } = await loadManifestDoc(projectDir);
10337
11111
  let changed = false;
10338
11112
  const seq = doc.get("styles", true);
10339
- if (isSeq3(seq)) {
11113
+ if (isSeq4(seq)) {
10340
11114
  const href = themeStyleHref(id);
10341
11115
  const before = seq.items.length;
10342
11116
  seq.items = seq.items.filter((item) => scalarString(item) !== href);
@@ -10352,7 +11126,7 @@ async function removeProjectTheme(projectDir, id) {
10352
11126
  }
10353
11127
  }
10354
11128
  // src/lib/theme-import.ts
10355
- import { existsSync as existsSync15 } from "node:fs";
11129
+ import { existsSync as existsSync16 } from "node:fs";
10356
11130
  import { mkdir as mkdir14, mkdtemp as mkdtemp4, readFile as readFile29, rm as rm7, writeFile as writeFile12 } from "node:fs/promises";
10357
11131
  import { tmpdir as tmpdir4 } from "node:os";
10358
11132
  import path21 from "node:path";
@@ -10427,7 +11201,7 @@ async function readThemeJson(p) {
10427
11201
  }
10428
11202
  async function finalizeThemeImport(projectDir, sourceDir, relPaths) {
10429
11203
  const cssPath = path21.join(sourceDir, "theme.css");
10430
- if (!existsSync15(cssPath)) {
11204
+ if (!existsSync16(cssPath)) {
10431
11205
  throw new Error("A theme must contain a theme.css file.");
10432
11206
  }
10433
11207
  const css = await readFile29(cssPath, "utf8");
@@ -10440,7 +11214,7 @@ async function finalizeThemeImport(projectDir, sourceDir, relPaths) {
10440
11214
  warnings.push({ code: "print-safety", message: w.message });
10441
11215
  }
10442
11216
  const jsonPath = path21.join(sourceDir, "theme.json");
10443
- if (!existsSync15(jsonPath)) {
11217
+ if (!existsSync16(jsonPath)) {
10444
11218
  warnings.push({
10445
11219
  code: "no-theme-json",
10446
11220
  message: "No theme.json found — the theme's name was taken from the file/folder name."
@@ -10564,7 +11338,7 @@ async function importThemeFromFile(projectDir, filePath2) {
10564
11338
  throw new Error("Choose a .zip theme package or a .css stylesheet.");
10565
11339
  }
10566
11340
  // src/lib/manifest-config.ts
10567
- import { isSeq as isSeq4, isMap as isMap2, YAMLSeq as YAMLSeq3, Scalar as Scalar3 } from "yaml";
11341
+ import { isSeq as isSeq5, isMap as isMap3, YAMLSeq as YAMLSeq3, Scalar as Scalar3 } from "yaml";
10568
11342
  function hasKey(obj, key) {
10569
11343
  return Object.prototype.hasOwnProperty.call(obj, key);
10570
11344
  }
@@ -10585,12 +11359,24 @@ async function setManifestFields(projectDir, updates) {
10585
11359
  doc.set("authors", a);
10586
11360
  }
10587
11361
  }
11362
+ if (hasKey(updates, "targets")) {
11363
+ const t = updates.targets;
11364
+ if (t === undefined) {
11365
+ doc.delete("targets");
11366
+ } else {
11367
+ const unknown = t.filter((id) => !TARGETS[id]);
11368
+ if (unknown.length > 0) {
11369
+ throw new UsageError(`Unknown publish target${unknown.length > 1 ? "s" : ""} "${unknown.join('", "')}". ` + `Known targets: ${TARGET_IDS.join(", ")}.`);
11370
+ }
11371
+ doc.set("targets", [...new Set(t)]);
11372
+ }
11373
+ }
10588
11374
  if (hasKey(updates, "sourceFiles")) {
10589
11375
  const f = updates.sourceFiles;
10590
11376
  if (f === null || Array.isArray(f) && f.length === 0) {
10591
11377
  doc.deleteIn(["source", "files"]);
10592
11378
  const source = doc.get("source", true);
10593
- if (isMap2(source) && source.items.length === 0) {
11379
+ if (isMap3(source) && source.items.length === 0) {
10594
11380
  doc.delete("source");
10595
11381
  }
10596
11382
  } else {
@@ -10608,15 +11394,19 @@ async function readManifestFields(projectDir) {
10608
11394
  if (typeof title === "string")
10609
11395
  out.title = title;
10610
11396
  const authors = doc.get("authors", true);
10611
- if (isSeq4(authors)) {
11397
+ if (isSeq5(authors)) {
10612
11398
  out.authors = authors.items.map((i) => scalarString(i) ?? "");
10613
11399
  }
11400
+ const targetsNode = doc.get("targets", true);
11401
+ if (isSeq5(targetsNode)) {
11402
+ out.targets = targetsNode.items.map((i) => scalarString(i)).filter((s) => s !== null);
11403
+ }
10614
11404
  const filesNode = doc.getIn(["source", "files"], true);
10615
- if (isSeq4(filesNode)) {
11405
+ if (isSeq5(filesNode)) {
10616
11406
  out.sourceFiles = filesNode.items.map((i) => scalarString(i) ?? "");
10617
11407
  } else {
10618
11408
  const sourceNode = doc.get("source", true);
10619
- if (isMap2(sourceNode) && sourceNode.has("files"))
11409
+ if (isMap3(sourceNode) && sourceNode.has("files"))
10620
11410
  out.sourceFiles = null;
10621
11411
  }
10622
11412
  return out;
@@ -12643,12 +13433,12 @@ async function buildResolutionPlan(resolutions, localTip, remoteId, deps) {
12643
13433
 
12644
13434
  // src/lib/remote-auth/conflict-resolution.ts
12645
13435
  function onlineCopyPath(filepath, counter) {
12646
- const dirname5 = path27.posix.dirname(filepath);
13436
+ const dirname6 = path27.posix.dirname(filepath);
12647
13437
  const ext = path27.posix.extname(filepath);
12648
13438
  const stem = path27.posix.basename(filepath, ext);
12649
13439
  const label = counter && counter >= 2 ? `online copy ${counter}` : "online copy";
12650
13440
  const renamed = `${stem} (${label})${ext}`;
12651
- return dirname5 === "." ? renamed : `${dirname5}/${renamed}`;
13441
+ return dirname6 === "." ? renamed : `${dirname6}/${renamed}`;
12652
13442
  }
12653
13443
  async function uniqueOnlineCopyPath(dir, filepath, oids, cache2) {
12654
13444
  const taken = async (candidate2) => {
@@ -13384,8 +14174,8 @@ var drivethrurpgProvider = {
13384
14174
  }
13385
14175
  issues.push({
13386
14176
  severity: "info",
13387
- id: "drivethrurpg/validate-profile",
13388
- message: "Tip: run `gutterpress validate --profile dtrpg` for DriveThruRPG's print-compliance checks before uploading."
14177
+ id: "drivethrurpg/validate-target",
14178
+ message: "Tip: run `gutterpress validate --target dtrpg` for DriveThruRPG's print-compliance checks before uploading."
13389
14179
  });
13390
14180
  return issues;
13391
14181
  },
@@ -13499,7 +14289,7 @@ var info4 = {
13499
14289
  label: "itch.io",
13500
14290
  kind: "api",
13501
14291
  format: "pdf",
13502
- description: "Upload the PDF to your itch.io project page (games, TTRPG supplements, zines).",
14292
+ description: "Upload the PDF to your itch.io project page (games, zines, and other printable projects).",
13503
14293
  configFields: [
13504
14294
  { key: "target", label: "Project (user/game)", placeholder: "you/your-book" },
13505
14295
  { key: "channel", label: "Channel", placeholder: "pdf" }
@@ -14177,10 +14967,10 @@ function normalizeProjectKey(projectDir) {
14177
14967
  return projectDir.replace(/[\\/]+$/, "");
14178
14968
  }
14179
14969
  // src/lib/publish/manifest-publish.ts
14180
- import { isMap as isMap3 } from "yaml";
14970
+ import { isMap as isMap4 } from "yaml";
14181
14971
  function settingsFromDoc(doc) {
14182
14972
  const node = doc.get("publish", true);
14183
- if (!isMap3(node))
14973
+ if (!isMap4(node))
14184
14974
  return {};
14185
14975
  const json = node.toJSON();
14186
14976
  const out = {};
@@ -14205,11 +14995,11 @@ async function setPublishProviderConfig(projectDir, providerId, values) {
14205
14995
  }
14206
14996
  }
14207
14997
  const section = doc.getIn(["publish", providerId], true);
14208
- if (isMap3(section) && section.items.length === 0) {
14998
+ if (isMap4(section) && section.items.length === 0) {
14209
14999
  doc.deleteIn(["publish", providerId]);
14210
15000
  }
14211
15001
  const publish = doc.get("publish", true);
14212
- if (isMap3(publish) && publish.items.length === 0) {
15002
+ if (isMap4(publish) && publish.items.length === 0) {
14213
15003
  doc.delete("publish");
14214
15004
  }
14215
15005
  await writeManifestDoc(file, doc);
@@ -15215,4 +16005,4 @@ async function dispatchToHandler(kind, ctx, error2) {
15215
16005
  }
15216
16006
  return result;
15217
16007
  }
15218
- export { BuildError, MANIFEST_FILENAMES, hasProjectManifest, loadManifest, loadManifestWithPath, resolveConfig, log, resolveActiveStyles, listProjectStyles, PLUGINS_DIR, ruleRemoteUrls, ruleRiskyProps, rulePagedjsCrashSelectors, ruleSyntax, checkCss, runLint, formatReport, getChecks, getCheckById, resolveCheckSelectors, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, redactCredential, defaultConfigDir, FileTokenStore, extractUrlCredential, migrateUrlCredential, getSystemDiagnostics, slugifyProjectName, escapeYamlScalar, scaffoldProject, adoptFolder, BUILT_IN_TEMPLATE_IDS, listBuiltInTemplates, saveProjectAsTemplate, listCustomTemplates, importTemplateFromFolder, SNIPPETS_DIR, extractVariables, substituteVariables, listSnippets, readSnippet, saveSnippet, deleteSnippet, RECOMMENDED_PLUGINS, listProjectPlugins, setPluginEnabled, addNpmPlugin, addLocalPlugin, validateProjectPlugins, THEMES_DIR, BUILT_IN_THEME_IDS, listBuiltInThemes, resolveBuiltInTheme, listProjectThemes, getActiveTheme, getPreviousTheme, revertTheme, applyTheme, importThemeFromFolder, importThemeFromUrl, readThemeCss, removeProjectTheme, importThemeFromFile, setManifestFields, readManifestFields, setActiveStyles, APP_HEARTBEAT_FRESH_MS, heartbeatTtlMs, appHeartbeatPath, writeAppHeartbeat, removeAppHeartbeat, readAppHeartbeat, isAppHeartbeatFresh, AUTO_SNAPSHOT_MIN_MINUTES, AUTO_SNAPSHOT_MAX_MINUTES, AUTO_SNAPSHOT_DEFAULT_MINUTES, AUTO_SYNC_MIN_MINUTES, AUTO_SYNC_MAX_MINUTES, AUTO_SYNC_DEFAULT_MINUTES, autoSnapshotDelayMs, autoSyncDelayMs, isGitInternalPath, resolveGitHubClientId, GITHUB_HOST, GitHubAuthProvider, listGitHubRepositories, listRepoBooks, listGitHubBranches, RepoNeedsRecoveryError, isRepoNeedsRecoveryError, classifyFromHealth, classifyGitError, SYNC_SNAPSHOT_MESSAGE, provenancePath, readProjectProvenance, sanitizeCloneFolderName, cloneRepository, isSshRemoteUrl, testRemoteAccess, normalizeForgeHost, knownForgeTokenUrl, connectGenericHost, GenericTokenAuthProvider, parseRemoteOrigin, forgeKindForHost, diagnoseProjectRemote, inspectRepo, verifyRepoReadable, isUnbornRepo, preflightStructuralReason, buildPreflightDiagnostics, onlineCopyPath, resolveConflicts, syncProject, pullChanges, pushChanges, publishCredentialKey, publishConnectionStatus, listPublishAccounts, listPublishProviders, publishProviderFor, resolvePublishRequest, runPublish, connectPublishProvider, PublishSelectionsStore, readPublishSettings, setPublishProviderConfig, buildRecoveryContext, recover16 as recover };
16008
+ export { BuildError, PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, hasProjectManifest, loadManifest, loadManifestWithPath, resolveConfig, log, resolveActiveStyles, listProjectStyles, PLUGINS_DIR, ruleRemoteUrls, ruleRiskyProps, rulePagedjsCrashSelectors, ruleSyntax, checkCss, runLint, formatReport, getChecks, getCheckById, resolveCheckSelectors, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, redactCredential, defaultConfigDir, FileTokenStore, extractUrlCredential, migrateUrlCredential, getSystemDiagnostics, slugifyProjectName, escapeYamlScalar, scaffoldProject, adoptFolder, BUILT_IN_TEMPLATE_IDS, listBuiltInTemplates, saveProjectAsTemplate, listCustomTemplates, importTemplateFromFolder, SNIPPETS_DIR, extractVariables, substituteVariables, listSnippets, readSnippet, saveSnippet, deleteSnippet, RECOMMENDED_PLUGINS, listProjectPlugins, setPluginEnabled, addNpmPlugin, addLocalPlugin, validateProjectPlugins, THEMES_DIR, BUILT_IN_THEME_IDS, listBuiltInThemes, resolveBuiltInTheme, listProjectThemes, getActiveTheme, getPreviousTheme, revertTheme, applyTheme, importThemeFromFolder, importThemeFromUrl, readThemeCss, removeProjectTheme, importThemeFromFile, setManifestFields, readManifestFields, setActiveStyles, APP_HEARTBEAT_FRESH_MS, heartbeatTtlMs, appHeartbeatPath, writeAppHeartbeat, removeAppHeartbeat, readAppHeartbeat, isAppHeartbeatFresh, AUTO_SNAPSHOT_MIN_MINUTES, AUTO_SNAPSHOT_MAX_MINUTES, AUTO_SNAPSHOT_DEFAULT_MINUTES, AUTO_SYNC_MIN_MINUTES, AUTO_SYNC_MAX_MINUTES, AUTO_SYNC_DEFAULT_MINUTES, autoSnapshotDelayMs, autoSyncDelayMs, isGitInternalPath, resolveGitHubClientId, GITHUB_HOST, GitHubAuthProvider, listGitHubRepositories, listRepoBooks, listGitHubBranches, RepoNeedsRecoveryError, isRepoNeedsRecoveryError, classifyFromHealth, classifyGitError, SYNC_SNAPSHOT_MESSAGE, provenancePath, readProjectProvenance, sanitizeCloneFolderName, cloneRepository, isSshRemoteUrl, testRemoteAccess, normalizeForgeHost, knownForgeTokenUrl, connectGenericHost, GenericTokenAuthProvider, parseRemoteOrigin, forgeKindForHost, diagnoseProjectRemote, inspectRepo, verifyRepoReadable, isUnbornRepo, preflightStructuralReason, buildPreflightDiagnostics, onlineCopyPath, resolveConflicts, syncProject, pullChanges, pushChanges, publishCredentialKey, publishConnectionStatus, listPublishAccounts, listPublishProviders, publishProviderFor, resolvePublishRequest, runPublish, connectPublishProvider, PublishSelectionsStore, readPublishSettings, setPublishProviderConfig, buildRecoveryContext, recover16 as recover };