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.
- package/README.md +21 -16
- package/dist/api/index.d.ts +6 -2
- package/dist/api/index.js +15 -3
- package/dist/{audit-1jrwwacz.js → audit-nhn2pjz3.js} +5 -5
- package/dist/{build-5s92kgpc.js → build-san7fv2z.js} +5 -5
- package/dist/checks/types.d.ts +6 -0
- package/dist/{cli-hvqjs342.js → cli-e5zhb0xs.js} +2 -2
- package/dist/{cli-7ykx7pp0.js → cli-k4bd06sd.js} +982 -331
- package/dist/{cli-h7phg3ka.js → cli-najycadg.js} +258 -258
- package/dist/{index-1tyj4bk3.js → cli-wchtvxvw.js} +4 -1
- package/dist/{cli-5bxx0c4y.js → cli-yzrh708h.js} +2 -2
- package/dist/cli.js +15 -15
- package/dist/{doctor-25zjt8dz.js → doctor-hrk0kxxz.js} +2 -2
- package/dist/{index-3h3fef7r.js → index-bynn850m.js} +1200 -410
- package/dist/{cli-1tyj4bk3.js → index-wchtvxvw.js} +4 -1
- package/dist/{index-5bxx0c4y.js → index-yzrh708h.js} +2 -2
- package/dist/index.js +15 -3
- package/dist/lib/asset-inline.d.ts +19 -0
- package/dist/lib/build-error.d.ts +0 -1
- package/dist/lib/build-fingerprint.d.ts +14 -0
- package/dist/lib/build-runner.d.ts +21 -1
- package/dist/lib/ghostscript.d.ts +0 -9
- package/dist/lib/manifest-config.d.ts +7 -0
- package/dist/lib/manifest.d.ts +8 -0
- package/dist/lib/markdown/assemble.d.ts +4 -6
- package/dist/lib/markdown/chapter-id.d.ts +1 -1
- package/dist/lib/markdown/index.d.ts +1 -2
- package/dist/lib/npm-plugin-installer.d.ts +1 -1
- package/dist/lib/output-paths.d.ts +0 -1
- package/dist/lib/pagination.d.ts +0 -20
- package/dist/lib/pdf-inspect.d.ts +2 -2
- package/dist/lib/pdf-parse.d.ts +2 -1
- package/dist/lib/plugin-manager.d.ts +0 -2
- package/dist/lib/presets.d.ts +36 -7
- package/dist/lib/project-scaffold.d.ts +39 -2
- package/dist/lib/project-source.d.ts +11 -9
- package/dist/lib/project-templates.d.ts +45 -7
- package/dist/lib/publish/providers/azure-swa.d.ts +0 -1
- package/dist/lib/publish/providers/itch.d.ts +0 -1
- package/dist/lib/publish/providers/shopify.d.ts +0 -1
- package/dist/lib/remote-auth/github-repos.d.ts +0 -2
- package/dist/lib/remote-auth/recovery/dispatch.d.ts +0 -5
- package/dist/lib/remote-auth/recovery/outcome-mapping.d.ts +3 -2
- package/dist/lib/remote-auth/recovery/recover-stale-lock.d.ts +0 -8
- package/dist/lib/remote-auth/sync.d.ts +2 -10
- package/dist/lib/remote-auth/test-access.d.ts +0 -2
- package/dist/lib/remote-auth/test-support/git-http-server.d.ts +0 -37
- package/dist/lib/source-provider.d.ts +0 -14
- package/dist/lib/static-serve.d.ts +39 -0
- package/dist/lib/style-resolver.d.ts +10 -1
- package/dist/lib/targets.d.ts +84 -0
- package/dist/lib/theme-import.d.ts +0 -2
- package/dist/lib/theme-manager.d.ts +1 -1
- package/dist/lib/validation-exec.d.ts +4 -3
- package/dist/{lint-f63nxjfj.js → lint-96j9hrj4.js} +5 -5
- package/dist/{manifest.schema-rn1dpdpn.json → manifest.schema-z61rzw44.json} +12 -6
- package/dist/new-8p38wavc.js +185 -0
- package/dist/{pagedjs-interface-80g3xa8p.js → pagedjs-interface-qxvzgwd7.js} +92 -33
- package/dist/{plugin-wf2cbmwc.js → plugin-ees6nhkc.js} +5 -5
- package/dist/{preflight-tg4mpz1h.js → preflight-1q6c2edh.js} +24 -29
- package/dist/preview/file-watcher.d.ts +48 -39
- package/dist/preview/http-server.d.ts +5 -2
- package/dist/preview/server-context.d.ts +24 -0
- package/dist/preview-shell-6dqexx1m.js +581 -0
- package/dist/{preview-h2bwbb2x.js → preview-y5a2zen1.js} +5 -5
- package/dist/{project-source-0tg0r6vm.js → project-source-p0gn1wd5.js} +3 -3
- package/dist/{publish-ztaet53r.js → publish-rm9yb3wh.js} +5 -5
- package/dist/render.js +10 -19
- package/dist/{repair-jkp4n3j8.js → repair-zgq7q2g6.js} +5 -5
- package/dist/schema/manifest.types.d.ts +14 -3
- package/dist/server.d.ts +2 -0
- package/dist/{source-provider-hp96b5jf.js → source-provider-c1rjm2c0.js} +2 -8
- package/dist/types.d.ts +0 -1
- package/dist/{validate-k2drs19j.js → validate-nr0xa6sa.js} +8 -8
- package/package.json +2 -2
- package/dist/chapter-01-jj8ftn4c.md +0 -29
- package/dist/lib/validation-profile.d.ts +0 -18
- package/dist/manifest-ryyg71nf.yaml +0 -9
- package/dist/new-7b08a42h.js +0 -105
- package/dist/preview-shell-rghtfmc8.js +0 -155
- package/dist/theme-0yzvthe8.css +0 -65
- package/dist/theme-6a2fngc8.json +0 -6
|
@@ -11,11 +11,11 @@ import {
|
|
|
11
11
|
snapshotWorkingTreeUnlocked,
|
|
12
12
|
stageChanges,
|
|
13
13
|
withRepoLock
|
|
14
|
-
} from "./index-
|
|
14
|
+
} from "./index-yzrh708h.js";
|
|
15
15
|
import {
|
|
16
16
|
detectProjectSource,
|
|
17
17
|
syncRemoteFor
|
|
18
|
-
} from "./index-
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1157
|
-
|
|
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 && !
|
|
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
|
|
1413
|
-
if (
|
|
1414
|
-
errors.push(
|
|
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.
|
|
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
|
-
"
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
3666
|
+
var manifest_default2 = "./manifest-n1gh3qw5.yaml";
|
|
3507
3667
|
|
|
3508
3668
|
// src/assets/templates/zine/chapter-01.md
|
|
3509
|
-
var
|
|
3669
|
+
var chapter_01_default2 = "./chapter-01-7jwqg1ct.md";
|
|
3510
3670
|
|
|
3511
3671
|
// src/assets/templates/technical/manifest.yaml
|
|
3512
|
-
var
|
|
3672
|
+
var manifest_default3 = "./manifest-n1gh3qw5.yaml";
|
|
3513
3673
|
|
|
3514
3674
|
// src/assets/templates/technical/chapter-01.md
|
|
3515
|
-
var
|
|
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
|
|
3684
|
+
var theme_default3 = "./theme-zz2ktzqs.css";
|
|
3531
3685
|
|
|
3532
3686
|
// src/assets/themes/zine/theme.json
|
|
3533
|
-
var
|
|
3687
|
+
var theme_default4 = "./theme-cd6efzjw.json";
|
|
3534
3688
|
|
|
3535
3689
|
// src/assets/themes/technical-doc/theme.css
|
|
3536
|
-
var
|
|
3690
|
+
var theme_default5 = "./theme-nya4nqh6.css";
|
|
3537
3691
|
|
|
3538
3692
|
// src/assets/themes/technical-doc/theme.json
|
|
3539
|
-
var
|
|
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/
|
|
3556
|
-
"templates/
|
|
3557
|
-
"templates/
|
|
3558
|
-
"templates/
|
|
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/
|
|
3564
|
-
"themes/
|
|
3565
|
-
"themes/
|
|
3566
|
-
"themes/
|
|
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-
|
|
6305
|
-
var
|
|
6306
|
-
"
|
|
6307
|
-
"
|
|
6308
|
-
"
|
|
6309
|
-
"
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
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
|
-
|
|
6322
|
-
const
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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: `
|
|
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
|
|
6409
|
-
|
|
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(
|
|
6447
|
-
markdownFiles = relMarkdown.map((f) => join6(
|
|
6448
|
-
const relStyles = await resolveActiveStyles(
|
|
6449
|
-
cssFiles = relStyles.map((rel) => resolve9(
|
|
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
|
|
6473
|
-
const
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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,
|
|
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 ??
|
|
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:
|
|
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,
|
|
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,
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7417
|
-
|
|
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
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
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
|
-
|
|
7513
|
-
|
|
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",
|
|
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
|
-
|
|
7600
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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"]
|
|
7886
|
-
window.addEventListener('renderingComplete',
|
|
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(
|
|
8326
|
+
document.addEventListener('DOMContentLoaded', function () { setTimeout(finishInitialRender, 50); });
|
|
7890
8327
|
} else {
|
|
7891
|
-
setTimeout(
|
|
8328
|
+
setTimeout(finishInitialRender, 50);
|
|
7892
8329
|
}
|
|
7893
8330
|
}
|
|
7894
|
-
|
|
8331
|
+
var ws = null;
|
|
8332
|
+
var reconnectTimer = null;
|
|
8333
|
+
var reconnectDelay = 250;
|
|
8334
|
+
var stopped = false;
|
|
7895
8335
|
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 +
|
|
7963
|
-
return html.slice(0, 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
|
|
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.
|
|
8018
|
-
ws.on("
|
|
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(
|
|
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 ||
|
|
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
|
|
8698
|
+
const broadcastUpdate = (type, file) => {
|
|
8104
8699
|
if (stopped)
|
|
8105
8700
|
return;
|
|
8106
|
-
|
|
8107
|
-
for (const ws of clients) {
|
|
8108
|
-
|
|
8109
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
|
8445
|
-
import { constants as FS, existsSync as
|
|
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 || !
|
|
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 =
|
|
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
|
|
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(
|
|
9217
|
+
await cp2(path15.join(customTemplateDir, entry.name), path15.join(projectDir, entry.name), { recursive: true });
|
|
8508
9218
|
}
|
|
8509
|
-
await
|
|
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
|
|
8514
|
-
await copyFile(tplManifest,
|
|
8515
|
-
await copyFile(tplChapter,
|
|
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
|
|
8519
|
-
await copyFile(starterCss,
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
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-
|
|
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-
|
|
9286
|
+
const { findEnclosingRepoDir } = await import("./project-source-p0gn1wd5.js");
|
|
8561
9287
|
const enclosingRepoDir = await findEnclosingRepoDir(projectDir);
|
|
8562
|
-
versionHistoryError = enclosingRepoDir ? `This book was added to ${
|
|
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" || !
|
|
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) =>
|
|
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(
|
|
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,
|
|
8607
|
-
await fillTemplateFile(
|
|
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
|
|
8612
|
-
const bookCssPath =
|
|
8613
|
-
if (!
|
|
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(
|
|
8628
|
-
await
|
|
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-
|
|
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:
|
|
8656
|
-
openFile:
|
|
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 =
|
|
9393
|
+
const gitignorePath = path15.join(projectDir, ".gitignore");
|
|
8665
9394
|
let existing = null;
|
|
8666
9395
|
try {
|
|
8667
|
-
existing = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
8779
|
-
await writeFile9(
|
|
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
|
|
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 =
|
|
9641
|
+
const dir = path16.join(templatesRoot, entry.name);
|
|
8810
9642
|
let label = prettify(entry.name);
|
|
8811
9643
|
try {
|
|
8812
|
-
const meta = JSON.parse(await
|
|
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({
|
|
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(
|
|
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
|
|
8832
|
-
import
|
|
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 =
|
|
8852
|
-
const full =
|
|
8853
|
-
if (full !==
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
8894
|
-
await
|
|
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
|
|
9738
|
+
await rm3(resolveSnippetPath(projectDir, fileName), { force: true });
|
|
8900
9739
|
}
|
|
8901
9740
|
// src/lib/plugin-manager.ts
|
|
8902
|
-
import { randomUUID as
|
|
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
|
|
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
|
|
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-${
|
|
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 (
|
|
10512
|
+
if (isScalar2(item))
|
|
9734
10513
|
return typeof item.value === "string" ? item.value : null;
|
|
9735
|
-
if (
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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 (
|
|
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 &&
|
|
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-${
|
|
9924
|
-
const backup = path19.join(destDir, `.backup-${
|
|
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
|
|
10779
|
+
import { existsSync as existsSync15 } from "node:fs";
|
|
10001
10780
|
import path20 from "node:path";
|
|
10002
|
-
import { isSeq as
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
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 (
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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-
|
|
13388
|
-
message: "Tip: run `gutterpress validate --
|
|
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,
|
|
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
|
|
14970
|
+
import { isMap as isMap4 } from "yaml";
|
|
14181
14971
|
function settingsFromDoc(doc) {
|
|
14182
14972
|
const node = doc.get("publish", true);
|
|
14183
|
-
if (!
|
|
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 (
|
|
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 (
|
|
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 };
|