gutterpress 0.10.7 → 0.10.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +10 -3
  2. package/dist/README-aaqapam1.md +186 -0
  3. package/dist/README-mdq34cmb.md +155 -0
  4. package/dist/api/index.d.ts +6 -2
  5. package/dist/api/index.js +30 -2
  6. package/dist/{audit-19e2431j.js → audit-m3ce9pvq.js} +4 -4
  7. package/dist/base-ypqh9n7f.css +123 -0
  8. package/dist/book-ted3tzrp.css +58 -0
  9. package/dist/{build-gqqew2vr.js → build-qnecb3a1.js} +4 -4
  10. package/dist/callout-hv56m16h.md +7 -0
  11. package/dist/{cli-kzf64c2g.js → cli-1hx14cgk.js} +946 -401
  12. package/dist/{cli-s8q42x3r.js → cli-4vtgt2ng.js} +78 -2
  13. package/dist/{cli-h8ejbtyr.js → cli-vcv5ewqj.js} +1 -1
  14. package/dist/{cli-pvk2s9d7.js → cli-vxf151ea.js} +1 -1
  15. package/dist/cli.js +16 -16
  16. package/dist/components-0mmswvvm.yaml +51 -0
  17. package/dist/components-aam0fe2z.css +94 -0
  18. package/dist/{doctor-3ccz35yz.js → doctor-qvqye1wd.js} +2 -2
  19. package/dist/{engine-pb6h089e.js → engine-daad33qn.js} +1 -1
  20. package/dist/{engine-88mynx9a.js → engine-db8632w6.js} +2 -2
  21. package/dist/expected-ycmacem8.html +9 -0
  22. package/dist/fixture-2qnyqgb5.md +10 -0
  23. package/dist/gutterpress-m98e2w2d.json +8 -0
  24. package/dist/gutterpress-x2ycdsb8.json +17 -0
  25. package/dist/{index-837htyjy.js → index-9pbaznww.js} +78 -2
  26. package/dist/{index-r4ny4fyr.js → index-dfw8hn8q.js} +1147 -523
  27. package/dist/{index-a0xpm188.js → index-rpt4vtby.js} +1 -1
  28. package/dist/index.js +31 -3
  29. package/dist/lib/build-runner.d.ts +52 -0
  30. package/dist/lib/extension-manifest.d.ts +164 -0
  31. package/dist/lib/extension-scaffold.d.ts +65 -0
  32. package/dist/lib/lint-runner.d.ts +12 -0
  33. package/dist/lib/markdown/assemble.d.ts +12 -0
  34. package/dist/lib/markdown/markers.d.ts +20 -0
  35. package/dist/lib/markdown/plugins.d.ts +1 -1
  36. package/dist/lib/markdown/renderer.d.ts +99 -0
  37. package/dist/lib/snippets.d.ts +121 -5
  38. package/dist/lib/theme-import.d.ts +8 -4
  39. package/dist/lib/theme-manager.d.ts +52 -39
  40. package/dist/lib/validation-exec.d.ts +12 -0
  41. package/dist/{lint-mpz7vfc8.js → lint-3p1s8agk.js} +4 -4
  42. package/dist/{manifest.schema-kwnrwv8b.json → manifest.schema-1q35yxs9.json} +2 -2
  43. package/dist/{new-nbrpsw7t.js → new-xffarp40.js} +111 -6
  44. package/dist/package-rm9d2tht.json +29 -0
  45. package/dist/page-rules-nsj3aw42.css +128 -0
  46. package/dist/page-templates-7md4ev7d.css +60 -0
  47. package/dist/{plugin-gg5mk26h.js → plugin-b05ck7zd.js} +4 -4
  48. package/dist/plugin-m2w7g47v.css +114 -0
  49. package/dist/plugin.js-aszz0zgh.tpl +172 -0
  50. package/dist/plugin.test.js-mv0ghv5n.tpl +232 -0
  51. package/dist/{preflight-jnmd4mg2.js → preflight-z6xpty9w.js} +4 -4
  52. package/dist/{preview-hsr9d12d.js → preview-xa1n7why.js} +4 -4
  53. package/dist/{publish-pcrrn6fd.js → publish-zgmr99kd.js} +4 -4
  54. package/dist/render.js +275 -8
  55. package/dist/term-box-4y0w7y32.md +7 -0
  56. package/dist/{theme-6fa0yjzq.js → theme-m9dtxkhj.js} +4 -4
  57. package/dist/tokens-e4xk3x4v.css +86 -0
  58. package/dist/{validate-7n25fx0g.js → validate-43hqgzz5.js} +4 -4
  59. package/package.json +1 -1
@@ -10,6 +10,7 @@ import {
10
10
  findTool,
11
11
  fullInstallHint,
12
12
  getAssetPath,
13
+ getAssetsDir,
13
14
  getBrowser,
14
15
  isToolAvailable,
15
16
  prewarmBrowser,
@@ -17,7 +18,7 @@ import {
17
18
  resolveChromiumExecutable,
18
19
  run,
19
20
  spawnCapture
20
- } from "./index-837htyjy.js";
21
+ } from "./index-9pbaznww.js";
21
22
  import {
22
23
  gitFs,
23
24
  gitScopeFor,
@@ -38,7 +39,7 @@ import {
38
39
  } from "./index-37x76zdn.js";
39
40
 
40
41
  // src/lib/build-runner.ts
41
- import path8 from "node:path";
42
+ import path9 from "node:path";
42
43
  import fs2 from "node:fs";
43
44
  import fsp2 from "node:fs/promises";
44
45
  import os2 from "node:os";
@@ -643,7 +644,7 @@ function escapeHtml(s) {
643
644
  function escapeAttr(s) {
644
645
  return String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
645
646
  }
646
- function parseMarkerLine(line) {
647
+ function parseMarkerLine(line, declaredWords) {
647
648
  const trimmed = line.trim();
648
649
  if (!trimmed.startsWith("@"))
649
650
  return null;
@@ -677,9 +678,9 @@ function parseMarkerLine(line) {
677
678
  tokens[i] = unwrapAttrBraces(tokens[i]);
678
679
  const head = tokens[0];
679
680
  const kind = head.slice(1);
680
- if (!KNOWN_KINDS.includes(kind))
681
+ if (!KNOWN_KINDS.includes(kind) && !(declaredWords && declaredWords.has(kind)))
681
682
  return null;
682
- if (kind === "page-break" || kind === "column-break" || kind === "end-section" || kind === "continue") {
683
+ if (kind === "page-break" || kind === "column-break" || kind === "continue" || kind.startsWith("end-")) {
683
684
  return { kind, name: null, attrs: {} };
684
685
  }
685
686
  const body = tokens.slice(1);
@@ -782,12 +783,14 @@ function attachDataAttrs(token, kind, name, attrs) {
782
783
  if (name) {
783
784
  if (kind === "chapter")
784
785
  token.attrSet("data-chapter-label", name);
785
- if (kind === "spread")
786
+ else if (kind === "spread")
786
787
  token.attrSet("data-spread", name);
787
- if (kind === "page")
788
+ else if (kind === "page")
788
789
  token.attrSet("data-page", name);
789
- if (kind === "section")
790
+ else if (kind === "section")
790
791
  token.attrSet("data-section", name);
792
+ else
793
+ token.attrSet(`data-${kind}`, name);
791
794
  }
792
795
  if (attrs.template)
793
796
  token.attrSet("data-template", attrs.template);
@@ -803,16 +806,193 @@ function attachDataAttrs(token, kind, name, attrs) {
803
806
  token.attrSet(`data-${k}`, v);
804
807
  }
805
808
  }
809
+ var DECLARED_NAME_RE = /^[a-z][a-z0-9-]*$/;
810
+ function validateDeclaredMarkerName(name, pluginName) {
811
+ if (!DECLARED_NAME_RE.test(name)) {
812
+ throw new Error(`Plugin "${pluginName}" declares marker "@${name}" with an invalid name — marker names must be ` + `lower-case letters, digits and hyphens, starting with a letter (like core's own "page-break").`);
813
+ }
814
+ if (name.startsWith("end-")) {
815
+ throw new Error(`Plugin "${pluginName}" declares marker "@${name}", but names starting with "end-" are reserved ` + `for the auto-derived closer of another declared marker (a container named "${name.slice(4)}" ` + `would collide with it). Rename this marker.`);
816
+ }
817
+ }
818
+ var TAG_NAME_RE = /^[a-z][a-z0-9-]*$/;
819
+ function resolveContainerShape(name, pluginName, decl) {
820
+ const tag = decl.tag !== undefined ? decl.tag : "div";
821
+ if (typeof tag !== "string" || !TAG_NAME_RE.test(tag)) {
822
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has an invalid \`tag\` (${JSON.stringify(decl.tag)}) — ` + `must be a lower-case HTML tag name.`);
823
+ }
824
+ const classBase = decl.class !== undefined ? decl.class : "";
825
+ if (typeof classBase !== "string") {
826
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`class\` that is not a string.`);
827
+ }
828
+ let variants;
829
+ if (decl.variants !== undefined) {
830
+ if (typeof decl.variants !== "object" || decl.variants === null || Array.isArray(decl.variants)) {
831
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has \`variants\` that is not a plain object.`);
832
+ }
833
+ variants = {};
834
+ for (const [variantName, variantClass] of Object.entries(decl.variants)) {
835
+ if (typeof variantClass !== "string") {
836
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" variant "${variantName}" is not a string.`);
837
+ }
838
+ variants[variantName] = variantClass;
839
+ }
840
+ }
841
+ let label;
842
+ if (decl.label !== undefined) {
843
+ if (typeof decl.label !== "object" || decl.label === null || Array.isArray(decl.label)) {
844
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`label\` that is not an object.`);
845
+ }
846
+ if (typeof decl.label.class !== "string" || !decl.label.class) {
847
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`label.class\` that is not a non-empty string.`);
848
+ }
849
+ const match = typeof decl.label.from === "string" ? /^attr:([A-Za-z_][A-Za-z0-9_-]*)$/.exec(decl.label.from) : null;
850
+ if (!match) {
851
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`label.from\` (${JSON.stringify(decl.label.from)}) that is not "attr:<name>" — that is the only supported form today.`);
852
+ }
853
+ const labelTag = decl.label.tag !== undefined ? decl.label.tag : "div";
854
+ if (typeof labelTag !== "string" || !TAG_NAME_RE.test(labelTag)) {
855
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has an invalid \`label.tag\` (${JSON.stringify(decl.label.tag)}).`);
856
+ }
857
+ label = { class: decl.label.class, attr: match[1], tag: labelTag };
858
+ }
859
+ let autoCloseAtEof = false;
860
+ if (decl.autoCloseAt !== undefined) {
861
+ if (!Array.isArray(decl.autoCloseAt)) {
862
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has an \`autoCloseAt\` that is not an array.`);
863
+ }
864
+ for (const value of decl.autoCloseAt) {
865
+ if (value !== "eof") {
866
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" declares an unsupported \`autoCloseAt\` value ` + `${JSON.stringify(value)} — only "eof" is currently supported.`);
867
+ }
868
+ }
869
+ autoCloseAtEof = decl.autoCloseAt.includes("eof");
870
+ }
871
+ return { tag, classBase, variants, label, autoCloseAtEof };
872
+ }
873
+ function resolveMarkerDeclaration(name, rawDecl, rawRegistry, originOf) {
874
+ const pluginName = originOf.get(name);
875
+ if (typeof rawDecl !== "object" || rawDecl === null || Array.isArray(rawDecl)) {
876
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" is not a plain object.`);
877
+ }
878
+ if (rawDecl.deprecated !== undefined) {
879
+ if (typeof rawDecl.deprecated !== "string" || !rawDecl.deprecated) {
880
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`deprecated\` value that is not a non-empty string.`);
881
+ }
882
+ return { name, deprecated: rawDecl.deprecated };
883
+ }
884
+ if (rawDecl.alias !== undefined) {
885
+ if (typeof rawDecl.alias !== "string" || !rawDecl.alias) {
886
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has an \`alias\` value that is not a non-empty string.`);
887
+ }
888
+ const targetRaw = rawRegistry.get(rawDecl.alias);
889
+ if (!targetRaw) {
890
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" aliases unknown marker "@${rawDecl.alias}" — no ` + `loaded plugin declares it.`);
891
+ }
892
+ if (targetRaw.deprecated !== undefined) {
893
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" aliases "@${rawDecl.alias}", which is deprecated: ` + `${targetRaw.deprecated}`);
894
+ }
895
+ if (targetRaw.alias !== undefined) {
896
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" aliases "@${rawDecl.alias}", which is itself an alias. ` + `Chained aliases are not supported — alias the base marker directly.`);
897
+ }
898
+ const preset = rawDecl.preset !== undefined ? rawDecl.preset : {};
899
+ if (typeof preset !== "object" || preset === null || Array.isArray(preset)) {
900
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`preset\` that is not a plain object.`);
901
+ }
902
+ if (preset.variant !== undefined && typeof preset.variant !== "string") {
903
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`preset.variant\` that is not a string.`);
904
+ }
905
+ const container = resolveContainerShape(rawDecl.alias, originOf.get(rawDecl.alias), targetRaw);
906
+ return { name, baseKind: rawDecl.alias, presetVariant: preset.variant, ...container };
907
+ }
908
+ return { name, baseKind: name, ...resolveContainerShape(name, pluginName, rawDecl) };
909
+ }
910
+ function buildDeclaredMarkerRegistry(sources) {
911
+ const rawRegistry = new Map;
912
+ const originOf = new Map;
913
+ for (const { pluginName, markers } of sources) {
914
+ if (!markers)
915
+ continue;
916
+ for (const [name, rawDecl] of Object.entries(markers)) {
917
+ validateDeclaredMarkerName(name, pluginName);
918
+ if (KNOWN_KINDS.includes(name)) {
919
+ throw new Error(`Plugin "${pluginName}" declares marker "@${name}", which is a core Gutterpress marker name ` + `(${KNOWN_KINDS.map((k) => `@${k}`).join(", ")}). Core marker names cannot be shadowed — ` + `rename the plugin's marker.`);
920
+ }
921
+ if (rawRegistry.has(name)) {
922
+ throw new Error(`Marker "@${name}" is declared by both plugin "${originOf.get(name)}" and plugin "${pluginName}". ` + `Declared marker names must be unique across every loaded plugin — rename one of them.`);
923
+ }
924
+ rawRegistry.set(name, rawDecl);
925
+ originOf.set(name, pluginName);
926
+ }
927
+ }
928
+ const resolved = new Map;
929
+ for (const [name, rawDecl] of rawRegistry) {
930
+ resolved.set(name, resolveMarkerDeclaration(name, rawDecl, rawRegistry, originOf));
931
+ }
932
+ return resolved;
933
+ }
934
+ function declaredKindWords(declaredMarkers) {
935
+ if (!declaredMarkers)
936
+ return null;
937
+ const words = new Set;
938
+ for (const name of declaredMarkers.keys()) {
939
+ words.add(name);
940
+ words.add(`end-${name}`);
941
+ }
942
+ return words;
943
+ }
944
+ function nearestDeclaredMarkerName(word, declaredMarkers) {
945
+ const w = word.toLowerCase();
946
+ const threshold = Math.max(2, Math.floor(w.length / 3));
947
+ let best = null;
948
+ let bestDistance = Infinity;
949
+ for (const known of declaredMarkers.keys()) {
950
+ const d = editDistance(w, known);
951
+ if (d < bestDistance) {
952
+ bestDistance = d;
953
+ best = known;
954
+ }
955
+ }
956
+ return best !== null && bestDistance > 0 && bestDistance <= threshold ? best : null;
957
+ }
958
+ function scanForUnknownDeclaredMarkers(state, declaredMarkers) {
959
+ if (!declaredMarkers)
960
+ return;
961
+ for (let tokenIndex = 0;tokenIndex < state.tokens.length; tokenIndex++) {
962
+ const tok = state.tokens[tokenIndex];
963
+ if (tok.type !== "inline" || !tok.map)
964
+ continue;
965
+ const opener = state.tokens[tokenIndex - 1];
966
+ if (!opener || opener.type !== "paragraph_open")
967
+ continue;
968
+ const lines = tok.content.split(`
969
+ `);
970
+ for (let i = 0;i < lines.length; i++) {
971
+ const m = /^@([A-Za-z][A-Za-z0-9-]*)(?=\s|$)/.exec(lines[i].trim());
972
+ if (!m)
973
+ continue;
974
+ const word = m[1];
975
+ if (KNOWN_KINDS.includes(word) || declaredMarkers.has(word))
976
+ continue;
977
+ const suggestion = nearestDeclaredMarkerName(word, declaredMarkers);
978
+ if (!suggestion)
979
+ continue;
980
+ warn(state.env, tok.map[0] + i + 1, "unknown_marker", `Unknown marker "@${word}". Did you mean "@${suggestion}"?`, null);
981
+ }
982
+ }
983
+ }
806
984
  function plugin(md, pluginOptions = {}) {
807
985
  const options = {
808
986
  preferPagesInSpreads: false,
809
987
  ...pluginOptions
810
988
  };
989
+ const declaredMarkers = options.declaredMarkers instanceof Map && options.declaredMarkers.size > 0 ? options.declaredMarkers : null;
990
+ const declaredWords = declaredKindWords(declaredMarkers);
811
991
  function markerBlock(state, startLine, endLine, silent) {
812
992
  const pos = state.bMarks[startLine] + state.tShift[startLine];
813
993
  const max = state.eMarks[startLine];
814
994
  const line = state.src.slice(pos, max);
815
- const parsed = parseMarkerLine(line);
995
+ const parsed = parseMarkerLine(line, declaredWords);
816
996
  if (!parsed)
817
997
  return false;
818
998
  if (silent)
@@ -869,10 +1049,64 @@ function plugin(md, pluginOptions = {}) {
869
1049
  }
870
1050
  }
871
1051
  md.core.ruler.after("block", "layout_transform", function(state) {
1052
+ scanForUnknownDeclaredMarkers(state, declaredMarkers);
872
1053
  if (!state.env.__layoutMarkersUsed)
873
1054
  return;
874
1055
  scanForMistypedMarkers(state);
875
1056
  const out = [];
1057
+ const declaredFrames = [];
1058
+ function declaredFrameIndex(kind) {
1059
+ for (let i = declaredFrames.length - 1;i >= 0; i--) {
1060
+ if (declaredFrames[i].kind === kind)
1061
+ return i;
1062
+ }
1063
+ return -1;
1064
+ }
1065
+ function closeDeclaredFrame(kind) {
1066
+ const at = declaredFrameIndex(kind);
1067
+ if (at === -1)
1068
+ return false;
1069
+ while (declaredFrames.length > at) {
1070
+ const frame = declaredFrames.pop();
1071
+ out.push(new state.Token("layout_component_close", frame.tag, -1));
1072
+ }
1073
+ return true;
1074
+ }
1075
+ function drainDeclaredFrames() {
1076
+ while (declaredFrames.length) {
1077
+ const frame = declaredFrames.pop();
1078
+ out.push(new state.Token("layout_component_close", frame.tag, -1));
1079
+ }
1080
+ }
1081
+ function drainDeclaredFramesAtEof() {
1082
+ while (declaredFrames.length) {
1083
+ const frame = declaredFrames.pop();
1084
+ if (!frame.decl.autoCloseAtEof) {
1085
+ warn(state.env, frame.line || 0, "declared_marker_eof_close", `An open @${frame.kind} reached end-of-document; closing it automatically. Add ` + `@end-${frame.kind} to close it explicitly, or declare autoCloseAt: ["eof"] on this ` + `marker if running to end-of-document is expected.`, null);
1086
+ }
1087
+ out.push(new state.Token("layout_component_close", frame.tag, -1));
1088
+ }
1089
+ }
1090
+ function openDeclaredMarker(meta, decl) {
1091
+ const t = new state.Token("layout_component_open", decl.tag, 1);
1092
+ t.meta = { line: meta.__line };
1093
+ const variant = meta.name || decl.presetVariant || null;
1094
+ const variantClass = variant && decl.variants && decl.variants[variant] || "";
1095
+ const baseClass = [decl.classBase, variantClass].filter(Boolean).join(" ");
1096
+ addClasses(t, baseClass, meta.attrs && meta.attrs.class ? meta.attrs.class : "");
1097
+ attachDataAttrs(t, decl.baseKind, variant, meta.attrs || {});
1098
+ out.push(t);
1099
+ declaredFrames.push({ kind: decl.baseKind, tag: decl.tag, decl, line: meta.__line });
1100
+ if (decl.label) {
1101
+ const value = meta.attrs && meta.attrs[decl.label.attr];
1102
+ if (value) {
1103
+ const labelToken = new state.Token("html_block", "", 0);
1104
+ labelToken.content = `<${decl.label.tag} class="${escapeAttr(decl.label.class)}">${escapeHtml(value)}</${decl.label.tag}>
1105
+ `;
1106
+ out.push(labelToken);
1107
+ }
1108
+ }
1109
+ }
876
1110
  const stack = {
877
1111
  frames: [],
878
1112
  get(kind) {
@@ -1022,6 +1256,7 @@ function plugin(md, pluginOptions = {}) {
1022
1256
  const kind = meta.kind;
1023
1257
  const line = meta.__line || 0;
1024
1258
  if (kind === "chapter") {
1259
+ drainDeclaredFrames();
1025
1260
  stack.close("chapter");
1026
1261
  openChapter(meta);
1027
1262
  continue;
@@ -1030,16 +1265,19 @@ function plugin(md, pluginOptions = {}) {
1030
1265
  if (stack.has("spread")) {
1031
1266
  warn(state.env, line, "nested_spread", "@spread encountered while another spread is open; closing the previous spread automatically.", meta);
1032
1267
  }
1268
+ drainDeclaredFrames();
1033
1269
  stack.close("spread");
1034
1270
  openSpread(meta);
1035
1271
  continue;
1036
1272
  }
1037
1273
  if (kind === "page") {
1274
+ drainDeclaredFrames();
1038
1275
  stack.close("page");
1039
1276
  openPage(meta);
1040
1277
  continue;
1041
1278
  }
1042
1279
  if (kind === "section") {
1280
+ drainDeclaredFrames();
1043
1281
  warnIfEmptyDecoratedSection("section", line);
1044
1282
  stack.close("section");
1045
1283
  const spread = stack.get("spread");
@@ -1055,6 +1293,7 @@ function plugin(md, pluginOptions = {}) {
1055
1293
  warn(state.env, line, "continue_without_section", "@continue used without an open @section; ignoring marker.", meta);
1056
1294
  continue;
1057
1295
  }
1296
+ drainDeclaredFrames();
1058
1297
  const contMeta = {
1059
1298
  name: section.meta.name,
1060
1299
  attrs: { ...section.meta.attrs || {} }
@@ -1087,15 +1326,42 @@ function plugin(md, pluginOptions = {}) {
1087
1326
  continue;
1088
1327
  }
1089
1328
  if (kind === "end-section") {
1329
+ drainDeclaredFrames();
1090
1330
  warnIfEmptyDecoratedSection("end-section", line);
1091
1331
  stack.close("section");
1092
1332
  continue;
1093
1333
  }
1334
+ if (declaredMarkers) {
1335
+ const openDecl = declaredMarkers.get(kind);
1336
+ if (openDecl) {
1337
+ if (openDecl.deprecated) {
1338
+ warn(state.env, line, "deprecated_marker", `@${kind} is deprecated: ${openDecl.deprecated}`, meta);
1339
+ continue;
1340
+ }
1341
+ closeDeclaredFrame(openDecl.baseKind);
1342
+ openDeclaredMarker(meta, openDecl);
1343
+ continue;
1344
+ }
1345
+ if (kind.startsWith("end-")) {
1346
+ const closeDecl = declaredMarkers.get(kind.slice(4));
1347
+ if (closeDecl) {
1348
+ if (closeDecl.deprecated) {
1349
+ warn(state.env, line, "deprecated_marker", `@${kind} is deprecated: ${closeDecl.deprecated}`, meta);
1350
+ continue;
1351
+ }
1352
+ if (!closeDeclaredFrame(closeDecl.baseKind)) {
1353
+ warn(state.env, line, "declared_marker_close_without_open", `@${kind} used without an open @${closeDecl.baseKind}; ignoring marker.`, meta);
1354
+ }
1355
+ continue;
1356
+ }
1357
+ }
1358
+ }
1094
1359
  }
1095
1360
  const eofSpread = stack.get("spread");
1096
1361
  if (eofSpread && !eofSpread.sawAnyPage) {
1097
1362
  warn(state.env, 0, "spread_eof_close", "An open @spread reached end-of-document; closing it automatically.", null);
1098
1363
  }
1364
+ drainDeclaredFramesAtEof();
1099
1365
  stack.closeAll();
1100
1366
  state.tokens = out;
1101
1367
  });
@@ -2154,7 +2420,9 @@ function createMarkdownRenderer(customPlugins) {
2154
2420
  md.use(unwrapPlugin(markdownItFootnote));
2155
2421
  md.use(unwrapPlugin(markdownItDeflist));
2156
2422
  md.use(unwrapPlugin(markdownItSourceMap));
2157
- md.use(plugin);
2423
+ const declaredMarkerSources = (customPlugins ?? []).filter((p) => !!p.markers).map((p) => ({ pluginName: p.name, markers: p.markers }));
2424
+ const declaredMarkers = buildDeclaredMarkerRegistry(declaredMarkerSources);
2425
+ md.use(plugin, { declaredMarkers });
2158
2426
  md.use(gpPinScope);
2159
2427
  registerImageRule(md);
2160
2428
  if (customPlugins && customPlugins.length > 0) {
@@ -2734,8 +3002,8 @@ async function renderChaptersToFile(inputDir, outDir, opts = {}) {
2734
3002
 
2735
3003
  // src/lib/markdown/plugins.ts
2736
3004
  import { createHash as createHash3 } from "node:crypto";
2737
- import { existsSync as existsSync4, lstatSync, realpathSync, rmSync, statSync as statSync2, unlinkSync } from "node:fs";
2738
- import { cp, link, mkdir as mkdir2, mkdtemp, readFile as readFile5, rm, unlink, writeFile as writeFile2 } from "node:fs/promises";
3005
+ import { existsSync as existsSync5, lstatSync, realpathSync, rmSync, statSync as statSync2, unlinkSync } from "node:fs";
3006
+ import { cp, link, mkdir as mkdir2, mkdtemp, readFile as readFile6, rm, unlink, writeFile as writeFile2 } from "node:fs/promises";
2739
3007
  import { tmpdir } from "node:os";
2740
3008
  import { resolve as resolve3, join as join2, dirname as dirname2, basename, extname, relative, isAbsolute } from "node:path";
2741
3009
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -3327,6 +3595,70 @@ function resolveDeclaredStyles(rawStyles, baseDir, subject) {
3327
3595
  return abs;
3328
3596
  });
3329
3597
  }
3598
+
3599
+ // src/lib/extension-manifest.ts
3600
+ import { existsSync as existsSync4 } from "node:fs";
3601
+ import { readFile as readFile5 } from "node:fs/promises";
3602
+ import path4 from "node:path";
3603
+ var EXTENSION_MANIFEST_FILENAME = "gutterpress.json";
3604
+ var LEGACY_THEME_MANIFEST_FILENAME = "theme.json";
3605
+ async function readJsonMetaFile(jsonPath) {
3606
+ try {
3607
+ const parsed = JSON.parse(await readFile5(jsonPath, "utf8"));
3608
+ return parsed && typeof parsed === "object" ? parsed : {};
3609
+ } catch {
3610
+ return {};
3611
+ }
3612
+ }
3613
+ async function readExtensionMeta(dir) {
3614
+ const extensionPath = path4.join(dir, EXTENSION_MANIFEST_FILENAME);
3615
+ if (existsSync4(extensionPath)) {
3616
+ return readJsonMetaFile(extensionPath);
3617
+ }
3618
+ return readJsonMetaFile(path4.join(dir, LEGACY_THEME_MANIFEST_FILENAME));
3619
+ }
3620
+ function declaredList(value) {
3621
+ return Array.isArray(value) ? value.filter((s) => typeof s === "string" && s.trim().length > 0) : [];
3622
+ }
3623
+ function extensionStyleList(meta) {
3624
+ return declaredList(meta.styles);
3625
+ }
3626
+ function extensionEngineStyleList(meta) {
3627
+ return declaredList(meta.engineStyles?.native);
3628
+ }
3629
+ function pathEscapesFolder(rel) {
3630
+ return path4.isAbsolute(rel) || rel.split(/[\\/]/).includes("..");
3631
+ }
3632
+ function assertExtensionContained(meta) {
3633
+ const declared = [
3634
+ ...extensionStyleList(meta),
3635
+ ...extensionEngineStyleList(meta),
3636
+ ...meta.markdown ? [meta.markdown] : [],
3637
+ ...meta.components ? [meta.components] : [],
3638
+ ...meta.snippets ? [meta.snippets] : [],
3639
+ ...meta.tokensFile ? [meta.tokensFile] : []
3640
+ ];
3641
+ for (const rel of declared) {
3642
+ if (pathEscapesFolder(rel)) {
3643
+ throw new Error(`Extension metadata declares "${rel}" outside its own folder; an extension must be self-contained.`);
3644
+ }
3645
+ }
3646
+ }
3647
+ function resolveExtension(dir, meta, subject) {
3648
+ const styles = resolveDeclaredStyles(extensionStyleList(meta), dir, subject);
3649
+ const engineStyles = resolveDeclaredStyles(extensionEngineStyleList(meta), dir, subject);
3650
+ const [markdown] = resolveDeclaredStyles(meta.markdown ? [meta.markdown] : undefined, dir, subject) ?? [];
3651
+ const [components] = resolveDeclaredStyles(meta.components ? [meta.components] : undefined, dir, subject) ?? [];
3652
+ const [snippets] = resolveDeclaredStyles(meta.snippets ? [meta.snippets] : undefined, dir, subject) ?? [];
3653
+ return {
3654
+ ...markdown ? { markdown } : {},
3655
+ ...styles ? { styles } : {},
3656
+ ...engineStyles ? { engineStyles } : {},
3657
+ ...meta.tokensFile?.trim() ? { tokensFile: meta.tokensFile.trim() } : {},
3658
+ ...components ? { components } : {},
3659
+ ...snippets ? { snippets } : {}
3660
+ };
3661
+ }
3330
3662
  // src/lib/markdown/plugins.ts
3331
3663
  var moduleInternals = Module;
3332
3664
  var vendorCjsTrees = new Map;
@@ -3617,7 +3949,7 @@ async function rewriteReachableModuleImports(entryPath, tree) {
3617
3949
  const pkg = packageOwningFile(tree, absolute);
3618
3950
  if (!pkg)
3619
3951
  throw new Error(`Reachable plugin module is outside the verified package tree: ${absolute}`);
3620
- const source = await readFile5(absolute, "utf8");
3952
+ const source = await readFile6(absolute, "utf8");
3621
3953
  const [imports] = parseModuleImports(source, absolute);
3622
3954
  const replacements = [];
3623
3955
  const targets = new Set;
@@ -3848,12 +4180,22 @@ function validateStylesExport(value, pluginRef) {
3848
4180
  }
3849
4181
  return value.length > 0 ? value : undefined;
3850
4182
  }
4183
+ function validateMarkersExport(value, pluginRef) {
4184
+ if (value === undefined)
4185
+ return;
4186
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
4187
+ throw new Error(`Plugin "${pluginRef}" exports \`markers\` that is not a plain object.`);
4188
+ }
4189
+ const table = value;
4190
+ return Object.keys(table).length > 0 ? table : undefined;
4191
+ }
3851
4192
  function extractPluginExports(pluginModule, pluginRef, exportName) {
3852
4193
  const mod = pluginModule !== null && (typeof pluginModule === "object" || typeof pluginModule === "function") ? pluginModule : {};
3853
4194
  let plugin2;
3854
4195
  let metadata = mod.metadata;
3855
4196
  let css = mod.css;
3856
4197
  let styles = validateStylesExport(mod.styles, pluginRef);
4198
+ let markers = validateMarkersExport(mod.markers, pluginRef);
3857
4199
  if (exportName && typeof mod[exportName] === "function") {
3858
4200
  plugin2 = mod[exportName];
3859
4201
  } else if (exportName) {
@@ -3869,11 +4211,12 @@ function extractPluginExports(pluginModule, pluginRef, exportName) {
3869
4211
  metadata = inner.metadata ?? metadata;
3870
4212
  css = inner.css ?? css;
3871
4213
  styles = validateStylesExport(inner.styles, pluginRef) ?? styles;
4214
+ markers = validateMarkersExport(inner.markers, pluginRef) ?? markers;
3872
4215
  }
3873
4216
  if (typeof plugin2 !== "function") {
3874
4217
  throw new Error(`Plugin "${pluginRef}" does not export a valid plugin function. Expected \`export default function (md, options) { ... }\` or CommonJS \`module.exports = function (md, options) { ... }\`.`);
3875
4218
  }
3876
- return { plugin: plugin2, metadata, css, styles };
4219
+ return { plugin: plugin2, metadata, css, styles, markers };
3877
4220
  }
3878
4221
  var pathPluginCache = new Map;
3879
4222
  var liveShadowPaths = new Set;
@@ -3944,6 +4287,48 @@ function resolvePluginStyles(rawStyles, moduleDir, pluginRef) {
3944
4287
  }
3945
4288
  return resolveDeclaredStyles(rawStyles, moduleDir, `Plugin "${pluginRef}"`);
3946
4289
  }
4290
+ function noopPlugin() {}
4291
+ function extensionMetadata(meta) {
4292
+ if (!meta.name && !meta.description && !meta.author)
4293
+ return;
4294
+ return {
4295
+ ...meta.name ? { name: meta.name } : {},
4296
+ ...meta.description ? { description: meta.description } : {},
4297
+ ...meta.author ? { author: meta.author } : {}
4298
+ };
4299
+ }
4300
+ async function loadExtensionFromDir(extensionDir, config, pluginRef) {
4301
+ const meta = await readExtensionMeta(extensionDir);
4302
+ assertExtensionContained(meta);
4303
+ const resolved = resolveExtension(extensionDir, meta, `Plugin "${pluginRef}"`);
4304
+ const extensionStyles = [...resolved.styles ?? [], ...resolved.engineStyles ?? []];
4305
+ const name = config.name ?? meta.name ?? pluginRef;
4306
+ if (!resolved.markdown && extensionStyles.length === 0) {
4307
+ throw new Error(`Extension folder "${pluginRef}" declares neither \`markdown\` nor \`styles\`/` + "`engineStyles` (in its gutterpress.json or theme.json) — there is nothing to " + "load. Point `path` at a JS file directly for a plain plugin, or add a metadata file declaring at least one.");
4308
+ }
4309
+ if (!resolved.markdown) {
4310
+ return {
4311
+ name,
4312
+ plugin: noopPlugin,
4313
+ ...extensionMetadata(meta) ? { metadata: extensionMetadata(meta) } : {},
4314
+ ...extensionStyles.length > 0 ? { styles: extensionStyles } : {},
4315
+ options: config.options
4316
+ };
4317
+ }
4318
+ const pluginModule = await loadCachedPathPluginModule(resolved.markdown);
4319
+ const { plugin: plugin2, metadata, css, styles: rawStyles, markers } = extractPluginExports(pluginModule, pluginRef, config.export);
4320
+ const ownStyles = resolvePluginStyles(rawStyles, dirname2(resolved.markdown), pluginRef);
4321
+ const styles = [...extensionStyles, ...ownStyles ?? []];
4322
+ return {
4323
+ name,
4324
+ plugin: plugin2,
4325
+ metadata: metadata ?? extensionMetadata(meta),
4326
+ css,
4327
+ ...styles.length > 0 ? { styles } : {},
4328
+ ...markers ? { markers } : {},
4329
+ options: config.options
4330
+ };
4331
+ }
3947
4332
  async function loadPlugin(config, baseDir) {
3948
4333
  const pluginRef = config.path ?? config.name ?? "(unspecified)";
3949
4334
  let pluginModule;
@@ -3959,10 +4344,21 @@ async function loadPlugin(config, baseDir) {
3959
4344
  options: config.options
3960
4345
  };
3961
4346
  }
4347
+ if (config.path) {
4348
+ const candidatePath = resolve3(baseDir, config.path);
4349
+ if (existsSync5(candidatePath) && statSync2(candidatePath).isDirectory()) {
4350
+ try {
4351
+ return await loadExtensionFromDir(candidatePath, config, pluginRef);
4352
+ } catch (error2) {
4353
+ const errorMsg = error2 instanceof Error ? error2.message : String(error2);
4354
+ throw new Error(`Failed to load plugin "${pluginRef}": ${errorMsg}`);
4355
+ }
4356
+ }
4357
+ }
3962
4358
  try {
3963
4359
  if (config.path) {
3964
4360
  const pluginPath = resolve3(baseDir, config.path);
3965
- if (!existsSync4(pluginPath)) {
4361
+ if (!existsSync5(pluginPath)) {
3966
4362
  throw new Error(`Plugin file not found: ${pluginPath} (resolved from manifest entry path="${config.path}")`);
3967
4363
  }
3968
4364
  pluginModule = await loadCachedPathPluginModule(pluginPath);
@@ -3978,7 +4374,7 @@ async function loadPlugin(config, baseDir) {
3978
4374
  const errorMsg = error2 instanceof Error ? error2.message : String(error2);
3979
4375
  throw new Error(`Failed to load plugin "${pluginRef}": ${errorMsg}`);
3980
4376
  }
3981
- const { plugin: plugin2, metadata, css, styles: rawStyles } = extractPluginExports(pluginModule, pluginRef, config.export);
4377
+ const { plugin: plugin2, metadata, css, styles: rawStyles, markers } = extractPluginExports(pluginModule, pluginRef, config.export);
3982
4378
  const styles = resolvePluginStyles(rawStyles, moduleDir, pluginRef);
3983
4379
  return {
3984
4380
  name: pluginName,
@@ -3986,6 +4382,7 @@ async function loadPlugin(config, baseDir) {
3986
4382
  metadata,
3987
4383
  css,
3988
4384
  styles,
4385
+ markers,
3989
4386
  options: config.options
3990
4387
  };
3991
4388
  }
@@ -4017,7 +4414,7 @@ async function loadPluginsWithCss(configs, baseDir, onError) {
4017
4414
  }
4018
4415
 
4019
4416
  // src/lib/output-paths.ts
4020
- import path4 from "node:path";
4417
+ import path5 from "node:path";
4021
4418
 
4022
4419
  // src/lib/slug.ts
4023
4420
  function slugify(name, fallback = "") {
@@ -4035,15 +4432,15 @@ function bookSlug(title) {
4035
4432
  return slugify(title ?? "", "book");
4036
4433
  }
4037
4434
  function resolveOutputDir(manifestDir, title) {
4038
- return path4.resolve(manifestDir, DIST_DIRNAME, bookSlug(title));
4435
+ return path5.resolve(manifestDir, DIST_DIRNAME, bookSlug(title));
4039
4436
  }
4040
4437
  function artifactName(title, format) {
4041
4438
  return `${bookSlug(title)}-${format}.pdf`;
4042
4439
  }
4043
4440
 
4044
4441
  // src/lib/ghostscript.ts
4045
- import { existsSync as existsSync5 } from "node:fs";
4046
- import { readFile as readFile6, writeFile as writeFile3, unlink as unlink2, rename, readdir as readdir4 } from "node:fs/promises";
4442
+ import { existsSync as existsSync6 } from "node:fs";
4443
+ import { readFile as readFile7, writeFile as writeFile3, unlink as unlink2, rename, readdir as readdir4 } from "node:fs/promises";
4047
4444
  import { join as join3, basename as basename2, resolve as resolve4 } from "node:path";
4048
4445
  var WINDOWS_GHOSTSCRIPT_NAMES = ["gswin64c", "gswin32c", "gs"];
4049
4446
  var POSIX_GHOSTSCRIPT_NAMES = ["gs"];
@@ -4077,7 +4474,7 @@ async function findWindowsGhostscriptInstall(env) {
4077
4474
  for (const version of versions) {
4078
4475
  for (const executable of ["gswin64c.exe", "gswin32c.exe"]) {
4079
4476
  const candidate = join3(installRoot, version.name, "bin", executable);
4080
- if (existsSync5(candidate))
4477
+ if (existsSync6(candidate))
4081
4478
  return candidate;
4082
4479
  }
4083
4480
  }
@@ -4086,7 +4483,7 @@ async function findWindowsGhostscriptInstall(env) {
4086
4483
  }
4087
4484
  async function resolveGhostscript(targetPlatform = process.platform, env = process.env) {
4088
4485
  const override = envValue(env, "GHOSTSCRIPT_PATH");
4089
- if (override && existsSync5(override))
4486
+ if (override && existsSync6(override))
4090
4487
  return override;
4091
4488
  const names = targetPlatform === "win32" ? WINDOWS_GHOSTSCRIPT_NAMES : POSIX_GHOSTSCRIPT_NAMES;
4092
4489
  for (const name of names) {
@@ -4145,7 +4542,7 @@ async function stripAnnotations(pdfPath, stagingDir) {
4145
4542
  await unlink2(tmp).catch(() => {});
4146
4543
  }
4147
4544
  const { PDFDocument, PDFName } = await import("pdf-lib");
4148
- const bytes = await readFile6(pdfPath);
4545
+ const bytes = await readFile7(pdfPath);
4149
4546
  const doc = await PDFDocument.load(bytes, { updateMetadata: false });
4150
4547
  const annots = PDFName.of("Annots");
4151
4548
  for (const page of doc.getPages())
@@ -4220,7 +4617,7 @@ ${docInfoConformance}/DOCINFO pdfmark
4220
4617
  }
4221
4618
  async function stampCreator(pdfPath) {
4222
4619
  const { PDFDocument } = await import("pdf-lib");
4223
- const bytes = await readFile6(pdfPath);
4620
+ const bytes = await readFile7(pdfPath);
4224
4621
  const doc = await PDFDocument.load(bytes, { updateMetadata: false });
4225
4622
  doc.setCreator("gutterpress");
4226
4623
  const out = await doc.save();
@@ -4271,14 +4668,14 @@ async function convertToPdfxCmyk(inputPdf, outPdf, config) {
4271
4668
 
4272
4669
  // src/lib/build-fingerprint.ts
4273
4670
  import * as fs from "node:fs";
4274
- import { mkdir as mkdir3, readFile as readFile7, writeFile as writeFile4 } from "node:fs/promises";
4671
+ import { mkdir as mkdir3, readFile as readFile8, writeFile as writeFile4 } from "node:fs/promises";
4275
4672
  import { createHash as createHash4 } from "node:crypto";
4276
- import path5 from "node:path";
4673
+ import path6 from "node:path";
4277
4674
  import git from "isomorphic-git";
4278
4675
  // package.json
4279
4676
  var package_default = {
4280
4677
  name: "gutterpress",
4281
- version: "0.10.7",
4678
+ version: "0.10.8",
4282
4679
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
4283
4680
  author: "itlackey",
4284
4681
  license: "MPL-2.0",
@@ -4435,7 +4832,7 @@ async function getGitRevision(sourceDir) {
4435
4832
  const candidateDirs = [sourceDir, process.cwd()].filter((v) => Boolean(v));
4436
4833
  const seen = new Set;
4437
4834
  for (const dir of candidateDirs) {
4438
- const abs = path5.resolve(dir);
4835
+ const abs = path6.resolve(dir);
4439
4836
  if (seen.has(abs))
4440
4837
  continue;
4441
4838
  seen.add(abs);
@@ -4471,8 +4868,8 @@ async function getEngineBundleHash() {
4471
4868
  getAssetPath("engine/gutterpress-agent.js")
4472
4869
  ]);
4473
4870
  const [viewerBytes, agentBytes] = await Promise.all([
4474
- readFile7(viewerPath),
4475
- readFile7(agentPath)
4871
+ readFile8(viewerPath),
4872
+ readFile8(agentPath)
4476
4873
  ]);
4477
4874
  const hash = createHash4("sha256");
4478
4875
  hash.update(viewerBytes);
@@ -4520,8 +4917,8 @@ function sanitizeArgs(args) {
4520
4917
  return out;
4521
4918
  }
4522
4919
  async function writeBuildFingerprint(input) {
4523
- const outputDir = path5.resolve(input.outputDir);
4524
- const outPath = path5.join(outputDir, FINGERPRINT_FILENAME);
4920
+ const outputDir = path6.resolve(input.outputDir);
4921
+ const outPath = path6.join(outputDir, FINGERPRINT_FILENAME);
4525
4922
  const [tools, sourceRevision] = await Promise.all([
4526
4923
  getToolVersions(),
4527
4924
  getGitRevision(input.sourceDir)
@@ -4537,7 +4934,7 @@ async function writeBuildFingerprint(input) {
4537
4934
  iccPath: input.pdfx.iccPath,
4538
4935
  stripAnnotations: input.pdfx.stripAnnotations
4539
4936
  },
4540
- outputDir: input.recordedOutputDir ? path5.resolve(input.recordedOutputDir) : outputDir
4937
+ outputDir: input.recordedOutputDir ? path6.resolve(input.recordedOutputDir) : outputDir
4541
4938
  },
4542
4939
  sourceRevision,
4543
4940
  tools
@@ -4549,7 +4946,7 @@ async function writeBuildFingerprint(input) {
4549
4946
 
4550
4947
  // src/lib/lint-runner.ts
4551
4948
  import { resolve as resolve5 } from "node:path";
4552
- import { readFile as readFile8 } from "node:fs/promises";
4949
+ import { readFile as readFile9 } from "node:fs/promises";
4553
4950
 
4554
4951
  // src/lib/printsafe.ts
4555
4952
  import postcss2 from "postcss";
@@ -4790,7 +5187,10 @@ async function runLint(opts = {}) {
4790
5187
  } else {
4791
5188
  const relStyles = await resolveActiveStyles(manifestDir, resolved.styles);
4792
5189
  const projectFiles = relStyles.map((rel) => resolve5(manifestDir, rel));
4793
- const { pluginStylePaths } = await loadPluginsWithCss(resolved.plugins, manifestDir, (ref, err) => log.warn(`Skipping plugin "${ref}" for lint — ${err.message}`));
5190
+ let pluginStylePaths = opts.pluginStylePaths;
5191
+ if (pluginStylePaths === undefined) {
5192
+ ({ pluginStylePaths } = await loadPluginsWithCss(resolved.plugins, manifestDir, (ref, err) => log.warn(`Skipping plugin "${ref}" for lint — ${err.message}`)));
5193
+ }
4794
5194
  files = [...projectFiles, ...pluginStylePaths].filter((f) => !f.endsWith(".min.css"));
4795
5195
  }
4796
5196
  if (files.length === 0) {
@@ -4804,7 +5204,7 @@ async function runLint(opts = {}) {
4804
5204
  for (const file of files) {
4805
5205
  let css;
4806
5206
  try {
4807
- css = await readFile8(file, "utf8");
5207
+ css = await readFile9(file, "utf8");
4808
5208
  } catch (err) {
4809
5209
  log.error(` ${file}`);
4810
5210
  log.error(` cannot read stylesheet: ${err instanceof Error ? err.message : String(err)}`);
@@ -4837,7 +5237,7 @@ async function runLint(opts = {}) {
4837
5237
  }
4838
5238
 
4839
5239
  // src/lib/validation-exec.ts
4840
- import { existsSync as existsSync10 } from "node:fs";
5240
+ import { existsSync as existsSync11 } from "node:fs";
4841
5241
  import { dirname as dirname4, join as join4, resolve as resolve9 } from "node:path";
4842
5242
 
4843
5243
  // src/checks/formatter.ts
@@ -5008,7 +5408,7 @@ function inspectionFailed(checkId, message, extra = {}) {
5008
5408
  }
5009
5409
 
5010
5410
  // src/lib/pdf-inspect.ts
5011
- import { stat, readFile as readFile9 } from "node:fs/promises";
5411
+ import { stat, readFile as readFile10 } from "node:fs/promises";
5012
5412
  import { getDocumentProxy } from "unpdf";
5013
5413
  import * as pdfjs from "unpdf/pdfjs";
5014
5414
  function getOps(mod) {
@@ -5044,17 +5444,17 @@ function evictLru() {
5044
5444
  destroyEntryAfterGrace(evicted);
5045
5445
  }
5046
5446
  }
5047
- async function loadPdf(path6) {
5447
+ async function loadPdf(path7) {
5048
5448
  let st;
5049
5449
  try {
5050
- st = await stat(path6);
5450
+ st = await stat(path7);
5051
5451
  } catch {
5052
5452
  return null;
5053
5453
  }
5054
- const hit = docCache.get(path6);
5454
+ const hit = docCache.get(path7);
5055
5455
  if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) {
5056
- docCache.delete(path6);
5057
- docCache.set(path6, hit);
5456
+ docCache.delete(path7);
5457
+ docCache.set(path7, hit);
5058
5458
  try {
5059
5459
  return await hit.doc;
5060
5460
  } catch {
@@ -5063,15 +5463,15 @@ async function loadPdf(path6) {
5063
5463
  }
5064
5464
  if (hit)
5065
5465
  destroyEntry(hit);
5066
- const docPromise = readFile9(path6).then((buf) => getDocumentProxy(new Uint8Array(buf)));
5067
- docCache.delete(path6);
5068
- docCache.set(path6, { mtimeMs: st.mtimeMs, size: st.size, doc: docPromise });
5466
+ const docPromise = readFile10(path7).then((buf) => getDocumentProxy(new Uint8Array(buf)));
5467
+ docCache.delete(path7);
5468
+ docCache.set(path7, { mtimeMs: st.mtimeMs, size: st.size, doc: docPromise });
5069
5469
  evictLru();
5070
5470
  try {
5071
5471
  return await docPromise;
5072
5472
  } catch {
5073
- if (docCache.get(path6)?.doc === docPromise)
5074
- docCache.delete(path6);
5473
+ if (docCache.get(path7)?.doc === docPromise)
5474
+ docCache.delete(path7);
5075
5475
  return null;
5076
5476
  }
5077
5477
  }
@@ -5522,9 +5922,9 @@ var check3 = {
5522
5922
  registerCheck(check3);
5523
5923
 
5524
5924
  // src/lib/pdf-parse.ts
5525
- import { readFile as readFile10 } from "node:fs/promises";
5925
+ import { readFile as readFile11 } from "node:fs/promises";
5526
5926
  async function readPdfBytes(pdfPath) {
5527
- return readFile10(pdfPath, "latin1");
5927
+ return readFile11(pdfPath, "latin1");
5528
5928
  }
5529
5929
  function parseInkCov(out) {
5530
5930
  const lines = out.split(/\r?\n/).filter((l) => l.includes("CMYK"));
@@ -6095,21 +6495,21 @@ var check15 = {
6095
6495
  registerCheck(check15);
6096
6496
 
6097
6497
  // src/checks/source/markdownlint.ts
6098
- import { readFile as readFile11 } from "node:fs/promises";
6498
+ import { readFile as readFile12 } from "node:fs/promises";
6099
6499
  import { lint } from "markdownlint/sync";
6100
6500
  import { parse as parseYaml2 } from "yaml";
6101
6501
 
6102
6502
  // src/checks/source/config-file.ts
6103
- import { existsSync as existsSync6 } from "node:fs";
6503
+ import { existsSync as existsSync7 } from "node:fs";
6104
6504
  import { resolve as resolve6 } from "node:path";
6105
6505
  function findConfigFile(inputDir, names, explicit) {
6106
6506
  if (explicit) {
6107
6507
  const p = resolve6(inputDir, explicit);
6108
- return existsSync6(p) ? p : null;
6508
+ return existsSync7(p) ? p : null;
6109
6509
  }
6110
6510
  for (const name of names) {
6111
6511
  const p = resolve6(inputDir, name);
6112
- if (existsSync6(p))
6512
+ if (existsSync7(p))
6113
6513
  return p;
6114
6514
  }
6115
6515
  return null;
@@ -6128,7 +6528,7 @@ function stripJsonComments(s) {
6128
6528
  return s.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
6129
6529
  }
6130
6530
  async function loadConfig(configPath) {
6131
- const raw = await readFile11(configPath, "utf8");
6531
+ const raw = await readFile12(configPath, "utf8");
6132
6532
  let parsed;
6133
6533
  if (configPath.endsWith(".json")) {
6134
6534
  parsed = JSON.parse(raw);
@@ -6187,8 +6587,8 @@ var check16 = {
6187
6587
  registerCheck(check16);
6188
6588
 
6189
6589
  // src/checks/source/htmlhint.ts
6190
- import { existsSync as existsSync7 } from "node:fs";
6191
- import { readFile as readFile12 } from "node:fs/promises";
6590
+ import { existsSync as existsSync8 } from "node:fs";
6591
+ import { readFile as readFile13 } from "node:fs/promises";
6192
6592
  import htmlhint from "htmlhint";
6193
6593
  var { HTMLHint } = htmlhint;
6194
6594
  var CONFIG_NAMES2 = [".htmlhintrc"];
@@ -6202,14 +6602,14 @@ var check17 = {
6202
6602
  const sourceConfig = ctx.config.validate.source;
6203
6603
  if (sourceConfig.htmlhint === false)
6204
6604
  return [];
6205
- if (!ctx.htmlPath || !existsSync7(ctx.htmlPath))
6605
+ if (!ctx.htmlPath || !existsSync8(ctx.htmlPath))
6206
6606
  return [];
6207
6607
  const configPath = typeof sourceConfig.htmlhint === "string" ? sourceConfig.htmlhint : null;
6208
6608
  const resolvedConfig = findConfigFile(ctx.inputDir, CONFIG_NAMES2, configPath);
6209
6609
  if (!resolvedConfig && !configPath)
6210
6610
  return [];
6211
- const ruleset = resolvedConfig ? JSON.parse(await readFile12(resolvedConfig, "utf8")) : HTMLHint.defaultRuleset;
6212
- const html = await readFile12(ctx.htmlPath, "utf8");
6611
+ const ruleset = resolvedConfig ? JSON.parse(await readFile13(resolvedConfig, "utf8")) : HTMLHint.defaultRuleset;
6612
+ const html = await readFile13(ctx.htmlPath, "utf8");
6213
6613
  const messages = HTMLHint.verify(html, ruleset);
6214
6614
  return messages.map((m) => ({
6215
6615
  checkId: check17.id,
@@ -6224,7 +6624,7 @@ var check17 = {
6224
6624
  registerCheck(check17);
6225
6625
 
6226
6626
  // src/checks/source/stylelint.ts
6227
- import { readFile as readFile13 } from "node:fs/promises";
6627
+ import { readFile as readFile14 } from "node:fs/promises";
6228
6628
  var check18 = {
6229
6629
  id: "source.stylelint",
6230
6630
  name: "Print-safety CSS",
@@ -6242,7 +6642,7 @@ var check18 = {
6242
6642
  for (const file of files) {
6243
6643
  let css;
6244
6644
  try {
6245
- css = await readFile13(file, "utf8");
6645
+ css = await readFile14(file, "utf8");
6246
6646
  } catch {
6247
6647
  continue;
6248
6648
  }
@@ -6263,12 +6663,12 @@ var check18 = {
6263
6663
  registerCheck(check18);
6264
6664
 
6265
6665
  // src/checks/source/css-ownership.ts
6266
- import { readFile as readFile14 } from "node:fs/promises";
6666
+ import { readFile as readFile15 } from "node:fs/promises";
6267
6667
  import postcss3 from "postcss";
6268
6668
  import { parse as parseYaml3 } from "yaml";
6269
6669
  var CONFIG_NAMES3 = [".gutterpress/css-contract.yaml", ".gutterpress/css-contract.yml"];
6270
6670
  async function loadContract(configPath) {
6271
- const raw = await readFile14(configPath, "utf8");
6671
+ const raw = await readFile15(configPath, "utf8");
6272
6672
  const parsed = parseYaml3(raw);
6273
6673
  if (parsed && typeof parsed === "object")
6274
6674
  return parsed;
@@ -6419,7 +6819,7 @@ var check19 = {
6419
6819
  continue;
6420
6820
  let css;
6421
6821
  try {
6422
- css = await readFile14(absFile, "utf8");
6822
+ css = await readFile15(absFile, "utf8");
6423
6823
  } catch {
6424
6824
  continue;
6425
6825
  }
@@ -6461,8 +6861,8 @@ var check19 = {
6461
6861
  registerCheck(check19);
6462
6862
 
6463
6863
  // src/checks/source/local-refs.ts
6464
- import { existsSync as existsSync8 } from "node:fs";
6465
- import { readFile as readFile15 } from "node:fs/promises";
6864
+ import { existsSync as existsSync9 } from "node:fs";
6865
+ import { readFile as readFile16 } from "node:fs/promises";
6466
6866
  import { dirname as dirname3, resolve as resolve7 } from "node:path";
6467
6867
 
6468
6868
  // src/checks/source/local-ref-parser.ts
@@ -6542,7 +6942,7 @@ var check20 = {
6542
6942
  const collectRenderedLocalRefs = createRenderedLocalRefCollector(plugins);
6543
6943
  for (const file of files) {
6544
6944
  try {
6545
- const content = await readFile15(file, "utf8");
6945
+ const content = await readFile16(file, "utf8");
6546
6946
  for (const { ref, kind, line } of collectRenderedLocalRefs(content)) {
6547
6947
  if (isNonFilesystemRef(ref))
6548
6948
  continue;
@@ -6584,15 +6984,15 @@ function localRefExists(ref, kind, sourceFile, projectRoot) {
6584
6984
  const rootRelative = resolve7(projectRoot, cleaned);
6585
6985
  switch (kind) {
6586
6986
  case "image":
6587
- return existsSync8(rootRelative);
6987
+ return existsSync9(rootRelative);
6588
6988
  case "link":
6589
- return existsSync8(fileRelative);
6989
+ return existsSync9(fileRelative);
6590
6990
  }
6591
6991
  }
6592
6992
  registerCheck(check20);
6593
6993
 
6594
6994
  // src/checks/source/accessibility-alt-text.ts
6595
- import { readFile as readFile16 } from "node:fs/promises";
6995
+ import { readFile as readFile17 } from "node:fs/promises";
6596
6996
  var check21 = {
6597
6997
  id: "source.accessibility.alt-text",
6598
6998
  name: "Image Alt Text",
@@ -6606,7 +7006,7 @@ var check21 = {
6606
7006
  const results = [];
6607
7007
  for (const file of files) {
6608
7008
  try {
6609
- const content = await readFile16(file, "utf8");
7009
+ const content = await readFile17(file, "utf8");
6610
7010
  const lines = content.split(`
6611
7011
  `);
6612
7012
  let inFence = false;
@@ -6650,7 +7050,7 @@ function extractImageAlts(line) {
6650
7050
  registerCheck(check21);
6651
7051
 
6652
7052
  // src/checks/source/accessibility-heading-order.ts
6653
- import { readFile as readFile17 } from "node:fs/promises";
7053
+ import { readFile as readFile18 } from "node:fs/promises";
6654
7054
  var check22 = {
6655
7055
  id: "source.accessibility.heading-order",
6656
7056
  name: "Heading Order",
@@ -6664,7 +7064,7 @@ var check22 = {
6664
7064
  const results = [];
6665
7065
  for (const file of files) {
6666
7066
  try {
6667
- const content = await readFile17(file, "utf8");
7067
+ const content = await readFile18(file, "utf8");
6668
7068
  const lines = content.split(`
6669
7069
  `);
6670
7070
  let inFence = false;
@@ -6700,7 +7100,7 @@ var check22 = {
6700
7100
  registerCheck(check22);
6701
7101
 
6702
7102
  // src/checks/source/layout-markers.ts
6703
- import { readFile as readFile18 } from "node:fs/promises";
7103
+ import { readFile as readFile19 } from "node:fs/promises";
6704
7104
  var check23 = {
6705
7105
  id: "source.markdown.layout-markers",
6706
7106
  name: "Layout Markers",
@@ -6718,7 +7118,7 @@ var check23 = {
6718
7118
  const md = createMarkdownRenderer(plugins);
6719
7119
  for (const file of files) {
6720
7120
  try {
6721
- const content = await readFile18(file, "utf8");
7121
+ const content = await readFile19(file, "utf8");
6722
7122
  const env = {};
6723
7123
  md.render(content, env);
6724
7124
  for (const w of env.layoutWarnings ?? []) {
@@ -6740,8 +7140,8 @@ var check23 = {
6740
7140
  registerCheck(check23);
6741
7141
 
6742
7142
  // src/checks/source/merge-markers.ts
6743
- import { readFile as readFile19 } from "node:fs/promises";
6744
- import path6 from "node:path";
7143
+ import { readFile as readFile20 } from "node:fs/promises";
7144
+ import path7 from "node:path";
6745
7145
 
6746
7146
  // src/checks/asset/extensions.ts
6747
7147
  var RASTER_INSPECTABLE_EXTS = [
@@ -6782,7 +7182,7 @@ var check24 = {
6782
7182
  for (const file of textFiles) {
6783
7183
  let content;
6784
7184
  try {
6785
- content = await readFile19(file, "utf8");
7185
+ content = await readFile20(file, "utf8");
6786
7186
  } catch {
6787
7187
  results.push(inspectionFailed(check24.id, `Could not read source file: ${file}`, { file }));
6788
7188
  continue;
@@ -6825,7 +7225,7 @@ var check24 = {
6825
7225
  ignore: [...ASSET_SCAN_IGNORE_GLOBS]
6826
7226
  });
6827
7227
  for (const sibling of siblings.sort()) {
6828
- const rel = path6.relative(ctx.inputDir, sibling).split(path6.sep).join("/");
7228
+ const rel = path7.relative(ctx.inputDir, sibling).split(path7.sep).join("/");
6829
7229
  results.push(finding(check24.id, {
6830
7230
  severity: "warning",
6831
7231
  code: "kept-both-versions",
@@ -6845,27 +7245,27 @@ registerCheck(check24);
6845
7245
  import { stat as stat3 } from "node:fs/promises";
6846
7246
 
6847
7247
  // src/lib/image-inspect.ts
6848
- import { stat as stat2, readFile as readFile20 } from "node:fs/promises";
7248
+ import { stat as stat2, readFile as readFile21 } from "node:fs/promises";
6849
7249
  var DEFAULT_DPI = 72;
6850
7250
  var cache = new Map;
6851
- async function inspectImage(path7) {
7251
+ async function inspectImage(path8) {
6852
7252
  let st;
6853
7253
  try {
6854
- st = await stat2(path7);
7254
+ st = await stat2(path8);
6855
7255
  } catch {
6856
7256
  return null;
6857
7257
  }
6858
- const hit = cache.get(path7);
7258
+ const hit = cache.get(path8);
6859
7259
  if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size)
6860
7260
  return hit.info;
6861
7261
  let info2 = null;
6862
7262
  try {
6863
- const buf = await readFile20(path7);
7263
+ const buf = await readFile21(path8);
6864
7264
  info2 = parseImage(buf);
6865
7265
  } catch {
6866
7266
  info2 = null;
6867
7267
  }
6868
- cache.set(path7, { mtimeMs: st.mtimeMs, size: st.size, info: info2 });
7268
+ cache.set(path8, { mtimeMs: st.mtimeMs, size: st.size, info: info2 });
6869
7269
  return info2;
6870
7270
  }
6871
7271
  function parseImage(b) {
@@ -7319,7 +7719,7 @@ var check30 = {
7319
7719
  registerCheck(check30);
7320
7720
 
7321
7721
  // src/checks/asset/font-license.ts
7322
- import { existsSync as existsSync9 } from "node:fs";
7722
+ import { existsSync as existsSync10 } from "node:fs";
7323
7723
  import { resolve as resolve8 } from "node:path";
7324
7724
  var LICENSE_NAMES = [
7325
7725
  "LICENSE",
@@ -7355,7 +7755,7 @@ var check31 = {
7355
7755
  }
7356
7756
  const results = [];
7357
7757
  for (const fontDir of fontDirs) {
7358
- const hasLicense = LICENSE_NAMES.some((name) => existsSync9(resolve8(fontDir, name)));
7758
+ const hasLicense = LICENSE_NAMES.some((name) => existsSync10(resolve8(fontDir, name)));
7359
7759
  if (!hasLicense) {
7360
7760
  results.push({
7361
7761
  checkId: check31.id,
@@ -7420,7 +7820,7 @@ var check32 = {
7420
7820
  registerCheck(check32);
7421
7821
 
7422
7822
  // src/checks/heuristic/section-density.ts
7423
- import { readFile as readFile21 } from "node:fs/promises";
7823
+ import { readFile as readFile22 } from "node:fs/promises";
7424
7824
  var check33 = {
7425
7825
  id: "heuristic.chunking.section-density",
7426
7826
  name: "Section Density",
@@ -7437,7 +7837,7 @@ var check33 = {
7437
7837
  const results = [];
7438
7838
  for (const file of files) {
7439
7839
  try {
7440
- const content = await readFile21(file, "utf8");
7840
+ const content = await readFile22(file, "utf8");
7441
7841
  const lines = content.split(`
7442
7842
  `);
7443
7843
  let currentSectionStart = 0;
@@ -7877,7 +8277,7 @@ async function executeValidation(args) {
7877
8277
  const targetIds = resolveTargets(parseCsv(typeof args.target === "string" ? args.target : undefined), config.targets);
7878
8278
  const pdfPath = typeof args.pdf === "string" ? resolve9(args.pdf) : undefined;
7879
8279
  const inputDir = typeof args.input === "string" ? resolve9(args.input) : undefined;
7880
- if (pdfPath && !existsSync10(pdfPath)) {
8280
+ if (pdfPath && !existsSync11(pdfPath)) {
7881
8281
  throw new Error(`File not found: ${pdfPath}`);
7882
8282
  }
7883
8283
  const categories = resolveCategoryArg(typeof args.category === "string" ? args.category : undefined);
@@ -7908,10 +8308,15 @@ async function executeValidation(args) {
7908
8308
  const relMarkdown = await resolveActiveMarkdownFiles(manifestDir, config.source.files);
7909
8309
  markdownFiles = relMarkdown.map((f) => join4(manifestDir, canonicalChapterId(f)));
7910
8310
  const relStyles = await resolveActiveStyles(manifestDir, config.styles);
7911
- cssFiles = relStyles.map((rel) => resolve9(manifestDir, rel)).filter((f) => !f.endsWith(".min.css"));
8311
+ const projectCssFiles = relStyles.map((rel) => resolve9(manifestDir, rel));
8312
+ let pluginStylePaths = args.pluginStylePaths;
8313
+ if (pluginStylePaths === undefined) {
8314
+ ({ pluginStylePaths } = await loadPluginsWithCss(config.plugins, manifestDir, (ref, err) => log.warn(`Skipping plugin "${ref}" for validation — ${err.message}`)));
8315
+ }
8316
+ cssFiles = [...projectCssFiles, ...pluginStylePaths].filter((f) => !f.endsWith(".min.css"));
7912
8317
  assetDirs = [inputDir, ...await sharedAssetDirs(manifestDir, relStyles, inputDir)];
7913
8318
  const possibleHtml = join4(outDir, BOOK_HTML_FILENAME);
7914
- if (existsSync10(possibleHtml)) {
8319
+ if (existsSync11(possibleHtml)) {
7915
8320
  htmlPath = possibleHtml;
7916
8321
  }
7917
8322
  }
@@ -8069,7 +8474,7 @@ function computeGates(format, opts, config) {
8069
8474
  }
8070
8475
 
8071
8476
  // src/lib/build-staging.ts
8072
- import path7 from "node:path";
8477
+ import path8 from "node:path";
8073
8478
  import os from "node:os";
8074
8479
  import fsp from "node:fs/promises";
8075
8480
 
@@ -8252,8 +8657,8 @@ function placeholderPng(width = 640, height = 480, cell = 32) {
8252
8657
 
8253
8658
  // src/lib/build-staging.ts
8254
8659
  async function shipViewerHtml(htmlFile, outDir) {
8255
- await fsp.mkdir(path7.join(outDir, "engine"), { recursive: true });
8256
- await fsp.copyFile(await getAssetPath("engine/gutterpress-viewer.js"), path7.join(outDir, "engine/gutterpress-viewer.js"));
8660
+ await fsp.mkdir(path8.join(outDir, "engine"), { recursive: true });
8661
+ await fsp.copyFile(await getAssetPath("engine/gutterpress-viewer.js"), path8.join(outDir, "engine/gutterpress-viewer.js"));
8257
8662
  const tag = ` <script src="engine/gutterpress-viewer.js"></script>
8258
8663
  `;
8259
8664
  const html = await fsp.readFile(htmlFile, "utf-8");
@@ -8282,18 +8687,18 @@ async function stageBookAssets(options) {
8282
8687
  return { missing: [...missingPlaceholders.keys()].sort() };
8283
8688
  }
8284
8689
  async function copyReferencedAssets(copies, outDir) {
8285
- const dirs = new Set(copies.map((c) => path7.dirname(path7.resolve(outDir, c.to))));
8690
+ const dirs = new Set(copies.map((c) => path8.dirname(path8.resolve(outDir, c.to))));
8286
8691
  await Promise.all([...dirs].map((d) => fsp.mkdir(d, { recursive: true })));
8287
8692
  const missing = new Map;
8288
8693
  await Promise.all(copies.map(async (c) => {
8289
- const dest = path7.resolve(outDir, c.to);
8694
+ const dest = path8.resolve(outDir, c.to);
8290
8695
  try {
8291
8696
  await fsp.copyFile(c.from, dest);
8292
8697
  } catch (err) {
8293
8698
  if (err?.code === "ENOENT") {
8294
8699
  const placeholder = placeholderOutputPath(c.to);
8295
- const placeholderDest = path7.resolve(outDir, placeholder);
8296
- await fsp.mkdir(path7.dirname(placeholderDest), { recursive: true });
8700
+ const placeholderDest = path8.resolve(outDir, placeholder);
8701
+ await fsp.mkdir(path8.dirname(placeholderDest), { recursive: true });
8297
8702
  await fsp.writeFile(placeholderDest, placeholderPng());
8298
8703
  missing.set(c.to, placeholder);
8299
8704
  return;
@@ -8304,7 +8709,7 @@ async function copyReferencedAssets(copies, outDir) {
8304
8709
  return missing;
8305
8710
  }
8306
8711
  async function createStageRoot() {
8307
- return fsp.mkdtemp(path7.join(os.tmpdir(), "gutterpress-stage-"));
8712
+ return fsp.mkdtemp(path8.join(os.tmpdir(), "gutterpress-stage-"));
8308
8713
  }
8309
8714
 
8310
8715
  // src/lib/build-runner.ts
@@ -8312,18 +8717,18 @@ function splitOutPath(outArg, format) {
8312
8717
  if (typeof outArg !== "string" || outArg.length === 0) {
8313
8718
  return { outDir: undefined, pdfFileOverride: null };
8314
8719
  }
8315
- const resolved = path8.resolve(outArg);
8720
+ const resolved = path9.resolve(outArg);
8316
8721
  if ((format === "pdf" || format === "pdfx") && resolved.toLowerCase().endsWith(".pdf")) {
8317
- return { outDir: path8.dirname(resolved), pdfFileOverride: resolved };
8722
+ return { outDir: path9.dirname(resolved), pdfFileOverride: resolved };
8318
8723
  }
8319
8724
  return { outDir: resolved, pdfFileOverride: null };
8320
8725
  }
8321
8726
  function layoutWarningKey(file, line, message) {
8322
- return `${path8.resolve(file)}\x00${line ?? 0}\x00${message}`;
8727
+ return `${path9.resolve(file)}\x00${line ?? 0}\x00${message}`;
8323
8728
  }
8324
8729
  async function resolveBuildContext(opts) {
8325
8730
  const { format } = opts;
8326
- const inputDir = path8.resolve(opts.inputDir);
8731
+ const inputDir = path9.resolve(opts.inputDir);
8327
8732
  const { manifest, manifestDir, manifestPath } = await loadManifestWithPath(opts.manifestPath ?? inputDir, { explicit: opts.manifestPath !== undefined });
8328
8733
  if (manifestPath === null) {
8329
8734
  throw new UsageError(`No project manifest found in ${inputDir}. Looked for ${MANIFEST_FILENAMES.join(" or ")}. ` + "Run from your project folder or pass that folder with `gutterpress build <project-dir>`. " + "For a custom manifest filename, pass `--manifest <path>`.");
@@ -8338,10 +8743,10 @@ async function resolveBuildContext(opts) {
8338
8743
  pdfx: pdfxConfigOverride,
8339
8744
  engine: opts.engine
8340
8745
  }, manifest);
8341
- const target = opts.pdfFileOverride ? { kind: "file", file: path8.resolve(opts.pdfFileOverride) } : opts.outDir ? { kind: "directory", dir: opts.outDir } : { kind: "project", dir: resolveOutputDir(manifestDir, config.title) };
8342
- const outDir = target.kind === "file" ? path8.dirname(target.file) : target.dir;
8746
+ const target = opts.pdfFileOverride ? { kind: "file", file: path9.resolve(opts.pdfFileOverride) } : opts.outDir ? { kind: "directory", dir: opts.outDir } : { kind: "project", dir: resolveOutputDir(manifestDir, config.title) };
8747
+ const outDir = target.kind === "file" ? path9.dirname(target.file) : target.dir;
8343
8748
  const gates = computeGates(format, opts, config);
8344
- const workDir = target.kind === "project" ? path8.join(path8.dirname(target.dir), `.${path8.basename(target.dir)}-build-${randomBytes(6).toString("hex")}`) : path8.join(os2.tmpdir(), `gutterpress-build-${randomBytes(6).toString("hex")}`);
8749
+ 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")}`);
8345
8750
  return {
8346
8751
  opts,
8347
8752
  format,
@@ -8353,15 +8758,32 @@ async function resolveBuildContext(opts) {
8353
8758
  manifestDir,
8354
8759
  config,
8355
8760
  gates,
8356
- prevalidatedLayoutWarningKeys: new Set
8761
+ prevalidatedLayoutWarningKeys: new Set,
8762
+ plugins: null
8357
8763
  };
8358
8764
  }
8765
+ async function loadBuildPlugins(ctx) {
8766
+ if (ctx.plugins)
8767
+ return ctx.plugins;
8768
+ const { config, renderDir } = ctx;
8769
+ if (config.plugins.length > 0) {
8770
+ log.info(`Loading ${config.plugins.length} plugin(s)...`);
8771
+ }
8772
+ const loaded = await loadPluginsWithCss(config.plugins, renderDir);
8773
+ if (loaded.plugins && loaded.plugins.length > 0) {
8774
+ log.success(`Loaded ${loaded.plugins.length} plugin(s)`);
8775
+ }
8776
+ ctx.plugins = loaded;
8777
+ return loaded;
8778
+ }
8359
8779
  async function runQualityGates(ctx) {
8360
8780
  const { gates, opts, renderDir } = ctx;
8781
+ const pluginStylePaths = gates.lint || gates.preValidate ? (await loadBuildPlugins(ctx)).pluginStylePaths : undefined;
8361
8782
  if (gates.lint) {
8362
8783
  log.info("Lint: CSS print-safety");
8363
8784
  const lintResult = await runLint({
8364
- manifest: opts.manifestPath ?? renderDir
8785
+ manifest: opts.manifestPath ?? renderDir,
8786
+ pluginStylePaths
8365
8787
  });
8366
8788
  if (!lintResult.ok) {
8367
8789
  throw new BuildError("CSS lint failed", 2);
@@ -8372,7 +8794,8 @@ async function runQualityGates(ctx) {
8372
8794
  const result = await executeAndReport({
8373
8795
  input: renderDir,
8374
8796
  phase: "pre-build",
8375
- manifest: opts.manifestPath
8797
+ manifest: opts.manifestPath,
8798
+ pluginStylePaths
8376
8799
  }, "text");
8377
8800
  for (const finding2 of result.execution.report.results) {
8378
8801
  if (finding2.checkId === "source.markdown.layout-markers" && finding2.file && finding2.code !== "inspect-failed") {
@@ -8391,13 +8814,7 @@ async function renderBook(ctx) {
8391
8814
  } else {
8392
8815
  log.info("Using all .md files in alphabetical order");
8393
8816
  }
8394
- if (config.plugins.length > 0) {
8395
- log.info(`Loading ${config.plugins.length} plugin(s)...`);
8396
- }
8397
- const { plugins, pluginCss, pluginStylePaths } = await loadPluginsWithCss(config.plugins, renderDir);
8398
- if (plugins && plugins.length > 0) {
8399
- log.success(`Loaded ${plugins.length} plugin(s)`);
8400
- }
8817
+ const { plugins, pluginCss, pluginStylePaths } = await loadBuildPlugins(ctx);
8401
8818
  const imageRefs = [];
8402
8819
  const cssAssets = [];
8403
8820
  const htmlFile = await renderChaptersToFile(renderDir, workDir, {
@@ -8409,7 +8826,7 @@ async function renderBook(ctx) {
8409
8826
  pluginStylePaths,
8410
8827
  onChapterWarnings: (file, warnings) => {
8411
8828
  for (const w of warnings) {
8412
- const key = layoutWarningKey(path8.resolve(renderDir, file), w.line, w.message);
8829
+ const key = layoutWarningKey(path9.resolve(renderDir, file), w.line, w.message);
8413
8830
  if (!ctx.prevalidatedLayoutWarningKeys.has(key)) {
8414
8831
  log.warn(` ${file}, line ${w.line}: ${w.message}`);
8415
8832
  }
@@ -8447,9 +8864,9 @@ async function renderBook(ctx) {
8447
8864
  return htmlFile;
8448
8865
  }
8449
8866
  async function resolveIccProfile(icc, manifestDir, explicitIccPath) {
8450
- const iccCandidates = path8.isAbsolute(icc) ? [icc] : [path8.resolve(manifestDir, icc), path8.resolve(icc)];
8867
+ const iccCandidates = path9.isAbsolute(icc) ? [icc] : [path9.resolve(manifestDir, icc), path9.resolve(icc)];
8451
8868
  let effectiveIccPath = iccCandidates.find((candidate) => fs2.existsSync(candidate)) ?? null;
8452
- if (!effectiveIccPath && !explicitIccPath && path8.basename(icc) === "CGATS21_CRPC1.icc") {
8869
+ if (!effectiveIccPath && !explicitIccPath && path9.basename(icc) === "CGATS21_CRPC1.icc") {
8453
8870
  effectiveIccPath = await getAssetPath("profiles/CGATS21_CRPC1.icc");
8454
8871
  }
8455
8872
  if (!effectiveIccPath) {
@@ -8465,13 +8882,13 @@ async function finalizeBuild(ctx, fingerprint, wroteMessage, paths, artifactName
8465
8882
  });
8466
8883
  await publishBuild(ctx, artifactName2);
8467
8884
  const delivered = ctx.target.kind !== "file";
8468
- const fingerprintPath = delivered ? path8.join(ctx.outDir, path8.basename(workFingerprint)) : null;
8885
+ const fingerprintPath = delivered ? path9.join(ctx.outDir, path9.basename(workFingerprint)) : null;
8469
8886
  log.success(wroteMessage);
8470
8887
  if (fingerprintPath)
8471
8888
  log.info(`Fingerprint: ${fingerprintPath}`);
8472
8889
  return {
8473
8890
  outDir: ctx.outDir,
8474
- htmlPath: delivered ? path8.join(ctx.outDir, path8.relative(ctx.workDir, paths.htmlPath)) : null,
8891
+ htmlPath: delivered ? path9.join(ctx.outDir, path9.relative(ctx.workDir, paths.htmlPath)) : null,
8475
8892
  pdfPath: paths.pdfPath,
8476
8893
  fingerprintPath,
8477
8894
  diagnostics
@@ -8482,8 +8899,8 @@ async function publishBuild(ctx, artifactName2) {
8482
8899
  if (target.kind === "file") {
8483
8900
  if (!artifactName2)
8484
8901
  throw new BuildError("No artifact to write for this format", 1);
8485
- await fsp2.mkdir(path8.dirname(target.file), { recursive: true });
8486
- await fsp2.copyFile(path8.join(workDir, artifactName2), target.file);
8902
+ await fsp2.mkdir(path9.dirname(target.file), { recursive: true });
8903
+ await fsp2.copyFile(path9.join(workDir, artifactName2), target.file);
8487
8904
  return;
8488
8905
  }
8489
8906
  if (target.kind === "directory") {
@@ -8492,12 +8909,12 @@ async function publishBuild(ctx, artifactName2) {
8492
8909
  return;
8493
8910
  }
8494
8911
  for (const name of await listPdfs(target.dir)) {
8495
- if (!fs2.existsSync(path8.join(workDir, name))) {
8496
- await fsp2.rename(path8.join(target.dir, name), path8.join(workDir, name));
8912
+ if (!fs2.existsSync(path9.join(workDir, name))) {
8913
+ await fsp2.rename(path9.join(target.dir, name), path9.join(workDir, name));
8497
8914
  }
8498
8915
  }
8499
8916
  await fsp2.rm(target.dir, { recursive: true, force: true });
8500
- await fsp2.mkdir(path8.dirname(target.dir), { recursive: true });
8917
+ await fsp2.mkdir(path9.dirname(target.dir), { recursive: true });
8501
8918
  await fsp2.rename(workDir, target.dir);
8502
8919
  }
8503
8920
  async function listPdfs(dir) {
@@ -8513,7 +8930,7 @@ class HtmlOutput {
8513
8930
  const { workDir, outDir, inputDir, config, opts } = ctx;
8514
8931
  log.info("Shipping self-contained HTML + viewer bundle (native engine)");
8515
8932
  await shipViewerHtml(htmlFile, workDir);
8516
- await fsp2.writeFile(path8.join(workDir, "index.html"), `<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=book.html"><title>Gutterpress</title></head><body></body></html>
8933
+ await fsp2.writeFile(path9.join(workDir, "index.html"), `<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=book.html"><title>Gutterpress</title></head><body></body></html>
8517
8934
  `, "utf-8");
8518
8935
  return finalizeBuild(ctx, {
8519
8936
  command: "build",
@@ -8526,7 +8943,7 @@ class HtmlOutput {
8526
8943
  iccPath: null,
8527
8944
  stripAnnotations: null
8528
8945
  }
8529
- }, `Wrote: ${path8.join(outDir, BOOK_HTML)}`, { htmlPath: htmlFile, pdfPath: null });
8946
+ }, `Wrote: ${path9.join(outDir, BOOK_HTML)}`, { htmlPath: htmlFile, pdfPath: null });
8530
8947
  }
8531
8948
  }
8532
8949
 
@@ -8535,14 +8952,14 @@ class PdfOutput {
8535
8952
  const { workDir, outDir, inputDir, config, opts, format, manifestDir, gates } = ctx;
8536
8953
  const pdfxMode = format === "pdfx" ? opts.pdfxFlavor ?? config.pdfx.flavor : undefined;
8537
8954
  const pdfName = artifactName(config.title, pdfxMode ? "pdfx" : "pdf");
8538
- const pdfFile = path8.join(workDir, pdfName);
8539
- const reportedPdf = ctx.target.kind === "file" ? ctx.target.file : path8.join(outDir, pdfName);
8955
+ const pdfFile = path9.join(workDir, pdfName);
8956
+ const reportedPdf = ctx.target.kind === "file" ? ctx.target.file : path9.join(outDir, pdfName);
8540
8957
  const stage = await createStageRoot();
8541
8958
  try {
8542
- const rawPdf = pdfxMode ? path8.join(stage, "raw.pdf") : path8.resolve(pdfFile);
8543
- await fsp2.mkdir(path8.dirname(path8.resolve(pdfFile)), { recursive: true });
8959
+ const rawPdf = pdfxMode ? path9.join(stage, "raw.pdf") : path9.resolve(pdfFile);
8960
+ await fsp2.mkdir(path9.dirname(path9.resolve(pdfFile)), { recursive: true });
8544
8961
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
8545
- const { buildNativePdf } = await import("./engine-88mynx9a.js");
8962
+ const { buildNativePdf } = await import("./engine-db8632w6.js");
8546
8963
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
8547
8964
  title: config.title,
8548
8965
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
@@ -8573,7 +8990,7 @@ class PdfOutput {
8573
8990
  await stripAnnotations(rawPdf, stage);
8574
8991
  }
8575
8992
  log.info(`Converting to CMYK PDF/X (${pdfxMode})`);
8576
- await convertToPdfxCmyk(rawPdf, path8.resolve(pdfFile), {
8993
+ await convertToPdfxCmyk(rawPdf, path9.resolve(pdfFile), {
8577
8994
  iccPath: effectiveIccPath,
8578
8995
  pdfx: pdfxMode,
8579
8996
  title: config.title,
@@ -8633,10 +9050,10 @@ async function runBuild(opts) {
8633
9050
  }
8634
9051
  }
8635
9052
  // src/preview/lifecycle.ts
8636
- import path10 from "path";
9053
+ import path11 from "path";
8637
9054
  import { tmpdir as tmpdir2 } from "os";
8638
9055
  import { randomBytes as randomBytes2 } from "crypto";
8639
- import { readdir as readdir5, readFile as readFile22, writeFile as writeFile5 } from "node:fs/promises";
9056
+ import { readdir as readdir5, readFile as readFile23, writeFile as writeFile5 } from "node:fs/promises";
8640
9057
 
8641
9058
  // src/utils/file-utils.ts
8642
9059
  import { promises as fs3 } from "fs";
@@ -8658,7 +9075,7 @@ async function mkdir4(dirPath) {
8658
9075
  // src/preview/file-watcher.ts
8659
9076
  import { watch } from "chokidar";
8660
9077
  import fsp3 from "node:fs/promises";
8661
- import path9 from "path";
9078
+ import path10 from "path";
8662
9079
  var EMPTY_BOOK_HTML = `<!doctype html>
8663
9080
  <html lang="en">
8664
9081
  <head>
@@ -8712,7 +9129,7 @@ function injectPreviewScripts(html, pageIsolateChapters) {
8712
9129
  async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets) {
8713
9130
  if (!inputPath) {
8714
9131
  cssAssets.clear();
8715
- await fsp3.writeFile(path9.join(tempDir, BOOK_HTML_FILENAME), EMPTY_BOOK_HTML, "utf-8");
9132
+ await fsp3.writeFile(path10.join(tempDir, BOOK_HTML_FILENAME), EMPTY_BOOK_HTML, "utf-8");
8716
9133
  return;
8717
9134
  }
8718
9135
  const nextAssets = new Map;
@@ -8727,7 +9144,7 @@ async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets) {
8727
9144
  cssAssets.clear();
8728
9145
  for (const [to, from] of nextAssets)
8729
9146
  cssAssets.set(to, from);
8730
- await fsp3.writeFile(path9.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html, false), "utf-8");
9147
+ await fsp3.writeFile(path10.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html, false), "utf-8");
8731
9148
  }
8732
9149
  async function renderChapterPreviewHtml(inputPath, file, config) {
8733
9150
  const html = await renderPreviewBook(inputPath, config, {
@@ -8739,12 +9156,12 @@ async function renderChapterPreviewHtml(inputPath, file, config) {
8739
9156
  function describeChanges(changes, inputResolved) {
8740
9157
  const files = [];
8741
9158
  for (const [changedPath, event] of changes) {
8742
- const relative2 = path9.relative(inputResolved, path9.resolve(changedPath));
8743
- if (relative2 === "" || relative2 === ".." || relative2.startsWith(`..${path9.sep}`) || path9.isAbsolute(relative2))
9159
+ const relative2 = path10.relative(inputResolved, path10.resolve(changedPath));
9160
+ if (relative2 === "" || relative2 === ".." || relative2.startsWith(`..${path10.sep}`) || path10.isAbsolute(relative2))
8744
9161
  continue;
8745
9162
  files.push({
8746
9163
  relativePath: canonicalChapterId(relative2),
8747
- ext: path9.extname(changedPath).toLowerCase(),
9164
+ ext: path10.extname(changedPath).toLowerCase(),
8748
9165
  event
8749
9166
  });
8750
9167
  }
@@ -8796,23 +9213,23 @@ function isIgnoredWatchPath(candidatePath, projectRoot) {
8796
9213
  return isDotPathUnderRoot(candidatePath, projectRoot) || isGeneratedProjectPath(relative2);
8797
9214
  }
8798
9215
  function isExternalWatchCandidate(candidatePath, targets) {
8799
- const candidate = path9.resolve(candidatePath);
9216
+ const candidate = path10.resolve(candidatePath);
8800
9217
  for (const rawTarget of targets) {
8801
- const target = path9.resolve(rawTarget);
8802
- const rel = path9.relative(candidate, target);
8803
- if (rel === "" || rel !== ".." && !rel.startsWith(`..${path9.sep}`) && !path9.isAbsolute(rel))
9218
+ const target = path10.resolve(rawTarget);
9219
+ const rel = path10.relative(candidate, target);
9220
+ if (rel === "" || rel !== ".." && !rel.startsWith(`..${path10.sep}`) && !path10.isAbsolute(rel))
8804
9221
  return true;
8805
9222
  }
8806
9223
  return false;
8807
9224
  }
8808
9225
  async function nearestExistingDirectory(start) {
8809
- let current = path9.resolve(start);
9226
+ let current = path10.resolve(start);
8810
9227
  while (true) {
8811
9228
  try {
8812
9229
  if ((await fsp3.stat(current)).isDirectory())
8813
9230
  return current;
8814
9231
  } catch {}
8815
- const parent = path9.dirname(current);
9232
+ const parent = path10.dirname(current);
8816
9233
  if (parent === current)
8817
9234
  return current;
8818
9235
  current = parent;
@@ -8821,12 +9238,12 @@ async function nearestExistingDirectory(start) {
8821
9238
  async function externalWatchRoots(targets) {
8822
9239
  const roots = new Set;
8823
9240
  for (const target of targets) {
8824
- roots.add(await nearestExistingDirectory(path9.dirname(path9.resolve(target))));
9241
+ roots.add(await nearestExistingDirectory(path10.dirname(path10.resolve(target))));
8825
9242
  }
8826
9243
  return [...roots];
8827
9244
  }
8828
9245
  async function externalWatchTargets(projectDir, config) {
8829
- const root = path9.resolve(projectDir);
9246
+ const root = path10.resolve(projectDir);
8830
9247
  let canonicalRoot = root;
8831
9248
  try {
8832
9249
  canonicalRoot = await fsp3.realpath(root);
@@ -8834,21 +9251,21 @@ async function externalWatchTargets(projectDir, config) {
8834
9251
  const styles = await resolveActiveStyles(root, config.styles);
8835
9252
  const candidates = [
8836
9253
  ...await collectStyleDependencies(root, styles),
8837
- ...(config.plugins ?? []).map((p) => p.path).filter((p) => !!p).map((p) => path9.resolve(root, p))
9254
+ ...(config.plugins ?? []).map((p) => p.path).filter((p) => !!p).map((p) => path10.resolve(root, p))
8838
9255
  ];
8839
9256
  const external = new Set;
8840
9257
  for (const abs of candidates) {
8841
- const authoredPath = path9.resolve(abs);
8842
- const authoredRel = path9.relative(root, authoredPath);
8843
- if (authoredRel !== "" && (authoredRel === ".." || authoredRel.startsWith(`..${path9.sep}`) || path9.isAbsolute(authoredRel))) {
9258
+ const authoredPath = path10.resolve(abs);
9259
+ const authoredRel = path10.relative(root, authoredPath);
9260
+ if (authoredRel !== "" && (authoredRel === ".." || authoredRel.startsWith(`..${path10.sep}`) || path10.isAbsolute(authoredRel))) {
8844
9261
  external.add(authoredPath);
8845
9262
  }
8846
9263
  let canonicalPath = authoredPath;
8847
9264
  try {
8848
9265
  canonicalPath = await fsp3.realpath(canonicalPath);
8849
9266
  } catch {}
8850
- const rel = path9.relative(canonicalRoot, canonicalPath);
8851
- if (rel === "" || rel !== ".." && !rel.startsWith(`..${path9.sep}`) && !path9.isAbsolute(rel))
9267
+ const rel = path10.relative(canonicalRoot, canonicalPath);
9268
+ if (rel === "" || rel !== ".." && !rel.startsWith(`..${path10.sep}`) && !path10.isAbsolute(rel))
8852
9269
  continue;
8853
9270
  external.add(canonicalPath);
8854
9271
  }
@@ -8875,7 +9292,7 @@ async function addAndAwaitWatch(watcher, roots) {
8875
9292
  }
8876
9293
  }
8877
9294
  function createFileWatcher(state) {
8878
- const inputResolved = path9.resolve(state.currentInputPath);
9295
+ const inputResolved = path10.resolve(state.currentInputPath);
8879
9296
  const watcher = watch(inputResolved, {
8880
9297
  persistent: true,
8881
9298
  ignoreInitial: true,
@@ -8915,7 +9332,7 @@ function createFileWatcher(state) {
8915
9332
  async function recordChange(event, filePath) {
8916
9333
  if (closed)
8917
9334
  return;
8918
- const target = path9.resolve(filePath);
9335
+ const target = path10.resolve(filePath);
8919
9336
  const settled = settledWrites.get(target);
8920
9337
  if (settled && event !== "unlink" && Date.now() <= settled.expiresAt) {
8921
9338
  const diskContent = await fsp3.readFile(target, "utf8").catch(() => null);
@@ -8936,7 +9353,7 @@ function createFileWatcher(state) {
8936
9353
  recordChange(event, filePath);
8937
9354
  });
8938
9355
  externalWatcher.on("all", (event, filePath) => {
8939
- const target = path9.resolve(filePath);
9356
+ const target = path10.resolve(filePath);
8940
9357
  if (!desiredExternals.has(target))
8941
9358
  return;
8942
9359
  if (event === "add" && suppressInitialExternalAdds.delete(target))
@@ -8944,8 +9361,8 @@ function createFileWatcher(state) {
8944
9361
  recordChange(event, target);
8945
9362
  });
8946
9363
  function isWatchedSource(target) {
8947
- const rel = path9.relative(inputResolved, target);
8948
- const inProject = rel === "" || rel !== ".." && !rel.startsWith(`..${path9.sep}`) && !path9.isAbsolute(rel);
9364
+ const rel = path10.relative(inputResolved, target);
9365
+ const inProject = rel === "" || rel !== ".." && !rel.startsWith(`..${path10.sep}`) && !path10.isAbsolute(rel);
8949
9366
  if (inProject)
8950
9367
  return !isIgnoredWatchPath(target, inputResolved);
8951
9368
  return desiredExternals.has(target);
@@ -8970,7 +9387,7 @@ function createFileWatcher(state) {
8970
9387
  state.notifySettledWrite = (filePath, writtenContent) => {
8971
9388
  if (closed)
8972
9389
  return;
8973
- const target = path9.resolve(filePath);
9390
+ const target = path10.resolve(filePath);
8974
9391
  if (isWatchedSource(target)) {
8975
9392
  acceptSettledWrite(target, writtenContent);
8976
9393
  return;
@@ -8991,7 +9408,7 @@ function createFileWatcher(state) {
8991
9408
  return;
8992
9409
  let nextTargets;
8993
9410
  try {
8994
- nextTargets = new Set((await externalWatchTargets(inputResolved, state.config)).map((target) => path9.resolve(target)));
9411
+ nextTargets = new Set((await externalWatchTargets(inputResolved, state.config)).map((target) => path10.resolve(target)));
8995
9412
  } catch (err) {
8996
9413
  debug(`Could not resolve external watch targets: ${err}`);
8997
9414
  return;
@@ -9123,7 +9540,7 @@ async function stopFileWatcher(state) {
9123
9540
  }
9124
9541
 
9125
9542
  // src/preview/lifecycle.ts
9126
- var TEMP_DIR_BASE = path10.join(tmpdir2(), "gutterpress-preview");
9543
+ var TEMP_DIR_BASE = path11.join(tmpdir2(), "gutterpress-preview");
9127
9544
  var PID_FILE_NAME = ".gutterpress.pid";
9128
9545
  function isProcessAlive(pid) {
9129
9546
  if (!Number.isFinite(pid) || pid <= 0)
@@ -9143,10 +9560,10 @@ async function cleanupOrphanTempDirs() {
9143
9560
  return;
9144
9561
  }
9145
9562
  for (const entry of entries) {
9146
- const dir = path10.join(TEMP_DIR_BASE, entry);
9147
- const pidFile = path10.join(dir, PID_FILE_NAME);
9563
+ const dir = path11.join(TEMP_DIR_BASE, entry);
9564
+ const pidFile = path11.join(dir, PID_FILE_NAME);
9148
9565
  try {
9149
- const raw = await readFile22(pidFile, "utf8");
9566
+ const raw = await readFile23(pidFile, "utf8");
9150
9567
  const pid = parseInt(raw.trim(), 10);
9151
9568
  if (!isProcessAlive(pid)) {
9152
9569
  debug(`Removing orphan temp dir ${dir} (pid ${pid} not alive)`);
@@ -9158,10 +9575,10 @@ async function cleanupOrphanTempDirs() {
9158
9575
  async function initializePreviewDirectories() {
9159
9576
  await cleanupOrphanTempDirs();
9160
9577
  const tempDirSuffix = randomBytes2(8).toString("hex");
9161
- const tempDir = path10.join(TEMP_DIR_BASE, tempDirSuffix);
9578
+ const tempDir = path11.join(TEMP_DIR_BASE, tempDirSuffix);
9162
9579
  await mkdir4(tempDir);
9163
9580
  debug(`Created temporary directory: ${tempDir}`);
9164
- await writeFile5(path10.join(tempDir, PID_FILE_NAME), `${process.pid}
9581
+ await writeFile5(path11.join(tempDir, PID_FILE_NAME), `${process.pid}
9165
9582
  `, "utf8");
9166
9583
  return tempDir;
9167
9584
  }
@@ -9252,8 +9669,8 @@ function createServerState(inputPath, tempDir, config, options) {
9252
9669
  import http from "node:http";
9253
9670
  import net from "node:net";
9254
9671
  import { createHash as createHash6, randomUUID } from "node:crypto";
9255
- import { readFile as readFile23, stat as stat4 } from "node:fs/promises";
9256
- import path12 from "path";
9672
+ import { readFile as readFile24, stat as stat4 } from "node:fs/promises";
9673
+ import path13 from "path";
9257
9674
  import { WebSocket, WebSocketServer } from "ws";
9258
9675
 
9259
9676
  // src/lib/open-path.ts
@@ -9285,7 +9702,7 @@ function openPath(filePath) {
9285
9702
  }
9286
9703
 
9287
9704
  // src/lib/static-serve.ts
9288
- import path11 from "node:path";
9705
+ import path12 from "node:path";
9289
9706
  var STATIC_MIME = {
9290
9707
  ".html": "text/html; charset=utf-8",
9291
9708
  ".htm": "text/html; charset=utf-8",
@@ -9307,9 +9724,9 @@ var STATIC_MIME = {
9307
9724
  ".otf": "font/otf"
9308
9725
  };
9309
9726
  function resolveWithinRoot(relPath, root) {
9310
- const resolvedRoot = path11.resolve(root);
9311
- const candidate = path11.resolve(resolvedRoot, "." + (relPath.startsWith("/") ? relPath : "/" + relPath));
9312
- if (candidate !== resolvedRoot && !candidate.startsWith(resolvedRoot + path11.sep)) {
9727
+ const resolvedRoot = path12.resolve(root);
9728
+ const candidate = path12.resolve(resolvedRoot, "." + (relPath.startsWith("/") ? relPath : "/" + relPath));
9729
+ if (candidate !== resolvedRoot && !candidate.startsWith(resolvedRoot + path12.sep)) {
9313
9730
  return null;
9314
9731
  }
9315
9732
  return candidate;
@@ -9562,7 +9979,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
9562
9979
  try {
9563
9980
  const s = await stat4(filePath);
9564
9981
  if (s.isDirectory())
9565
- filePath = path12.join(filePath, "index.html");
9982
+ filePath = path13.join(filePath, "index.html");
9566
9983
  } catch {
9567
9984
  res.writeHead(404);
9568
9985
  res.end("Not Found");
@@ -9570,7 +9987,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
9570
9987
  }
9571
9988
  let data;
9572
9989
  try {
9573
- data = await readFile23(filePath);
9990
+ data = await readFile24(filePath);
9574
9991
  } catch {
9575
9992
  res.writeHead(404);
9576
9993
  res.end("Not Found");
@@ -9583,7 +10000,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
9583
10000
  res.end(withHmr);
9584
10001
  return;
9585
10002
  }
9586
- const ct = STATIC_MIME[path12.extname(filePath).toLowerCase()] ?? "application/octet-stream";
10003
+ const ct = STATIC_MIME[path13.extname(filePath).toLowerCase()] ?? "application/octet-stream";
9587
10004
  res.writeHead(200, { "Content-Type": ct, "Cache-Control": cacheControl, ...extraHeaders });
9588
10005
  res.end(data);
9589
10006
  }
@@ -9593,7 +10010,7 @@ var EMBEDDED_CACHE_CONTROL = "public, no-cache";
9593
10010
  var EMBEDDED_ETAG = `"gutterpress-${PACKAGE_VERSION}"`;
9594
10011
  var PROJECT_ASSET_CACHE_CONTROL = "private, no-cache";
9595
10012
  async function serveRevalidatedStatic(req, absPath, res, validators) {
9596
- const extension = path12.extname(absPath).toLowerCase();
10013
+ const extension = path13.extname(absPath).toLowerCase();
9597
10014
  if (extension === ".html" || extension === ".htm") {
9598
10015
  await serveStatic(absPath, res);
9599
10016
  return;
@@ -9620,7 +10037,7 @@ async function serveRevalidatedStatic(req, absPath, res, validators) {
9620
10037
  let data;
9621
10038
  if (!validator || validator.stamp !== stamp) {
9622
10039
  try {
9623
- data = await readFile23(absPath);
10040
+ data = await readFile24(absPath);
9624
10041
  } catch {
9625
10042
  await serveStatic(absPath, res);
9626
10043
  return;
@@ -9759,7 +10176,7 @@ async function createPreviewServer(state, port) {
9759
10176
  const file = rawFile ? canonicalChapterId(rawFile) : "";
9760
10177
  const revision = Number(url.searchParams.get("revision"));
9761
10178
  const configuredFiles = state.config.source?.files;
9762
- 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)) {
10179
+ if (!file || !state.currentInputPath || !incrementalPreviewEnabled() || !Number.isSafeInteger(revision) || revision !== reloadRevision || path13.extname(file).toLowerCase() !== ".md" || hasDotSegment(file) || !resolveWithinRoot(file, state.currentInputPath) || Array.isArray(configuredFiles) && configuredFiles.length > 0 && !configuredFiles.some((configured) => canonicalChapterId(configured) === file)) {
9763
10180
  res.writeHead(400);
9764
10181
  res.end("Bad Request");
9765
10182
  return;
@@ -9961,9 +10378,9 @@ async function startPreviewServer(options) {
9961
10378
  import { platform, arch, release } from "node:os";
9962
10379
 
9963
10380
  // src/lib/remote-auth/token-store.ts
9964
- import { chmod, mkdir as mkdir5, readFile as readFile24, rename as rename2, writeFile as writeFile6 } from "node:fs/promises";
10381
+ import { chmod, mkdir as mkdir5, readFile as readFile25, rename as rename2, writeFile as writeFile6 } from "node:fs/promises";
9965
10382
  import os3 from "node:os";
9966
- import path13 from "node:path";
10383
+ import path14 from "node:path";
9967
10384
  function credentialHostKey(hostOrUrl) {
9968
10385
  const trimmed = String(hostOrUrl ?? "").trim();
9969
10386
  if (!trimmed)
@@ -9990,22 +10407,22 @@ function defaultConfigDir() {
9990
10407
  if (override)
9991
10408
  return override;
9992
10409
  if (process.platform === "win32" && process.env.APPDATA) {
9993
- return path13.join(process.env.APPDATA, "gutterpress");
10410
+ return path14.join(process.env.APPDATA, "gutterpress");
9994
10411
  }
9995
10412
  const xdg = process.env.XDG_CONFIG_HOME?.trim();
9996
- return path13.join(xdg || path13.join(os3.homedir(), ".config"), "gutterpress");
10413
+ return path14.join(xdg || path14.join(os3.homedir(), ".config"), "gutterpress");
9997
10414
  }
9998
10415
 
9999
10416
  class FileTokenStore {
10000
10417
  filePath;
10001
10418
  queue = Promise.resolve();
10002
10419
  constructor(filePath) {
10003
- this.filePath = filePath ?? path13.join(defaultConfigDir(), "credentials.json");
10420
+ this.filePath = filePath ?? path14.join(defaultConfigDir(), "credentials.json");
10004
10421
  }
10005
10422
  async read() {
10006
10423
  let raw;
10007
10424
  try {
10008
- raw = await readFile24(this.filePath, "utf8");
10425
+ raw = await readFile25(this.filePath, "utf8");
10009
10426
  } catch (e) {
10010
10427
  if (e?.code === "ENOENT") {
10011
10428
  return { version: 1, credentials: {} };
@@ -10025,7 +10442,7 @@ class FileTokenStore {
10025
10442
  return { version: 1, credentials: {} };
10026
10443
  }
10027
10444
  async write(data) {
10028
- await mkdir5(path13.dirname(this.filePath), { recursive: true });
10445
+ await mkdir5(path14.dirname(this.filePath), { recursive: true });
10029
10446
  const tmpPath = `${this.filePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
10030
10447
  await writeFile6(tmpPath, JSON.stringify(data, null, 2), {
10031
10448
  encoding: "utf8",
@@ -10174,18 +10591,18 @@ async function getSystemDiagnostics() {
10174
10591
  installHint: INSTALL_HINTS3[t.hintKey]
10175
10592
  };
10176
10593
  }
10177
- const [path14, available] = await Promise.all([
10594
+ const [path15, available] = await Promise.all([
10178
10595
  findTool(t.bin),
10179
10596
  isToolAvailable(t.bin)
10180
10597
  ]);
10181
- const found = available || !!path14;
10598
+ const found = available || !!path15;
10182
10599
  const version = found ? await getVersion(t.bin) : undefined;
10183
10600
  return {
10184
10601
  id: t.id,
10185
10602
  name: t.name,
10186
10603
  bin: t.bin,
10187
10604
  found,
10188
- path: path14,
10605
+ path: path15,
10189
10606
  version,
10190
10607
  usedBy: t.usedBy,
10191
10608
  installHint: INSTALL_HINTS3[t.hintKey]
@@ -10205,25 +10622,25 @@ async function getSystemDiagnostics() {
10205
10622
  };
10206
10623
  }
10207
10624
  // src/lib/project-scaffold.ts
10208
- import { access, copyFile, cp as cp3, mkdir as mkdir8, readdir as readdir7, readFile as readFile27, stat as stat6, writeFile as writeFile8 } from "node:fs/promises";
10209
- import { constants as FS, existsSync as existsSync13 } from "node:fs";
10210
- import path16 from "node:path";
10625
+ import { access, copyFile, cp as cp3, mkdir as mkdir8, readdir as readdir7, readFile as readFile28, stat as stat6, writeFile as writeFile8 } from "node:fs/promises";
10626
+ import { constants as FS, existsSync as existsSync14 } from "node:fs";
10627
+ import path17 from "node:path";
10211
10628
 
10212
10629
  // src/lib/manifest-doc.ts
10213
- import { existsSync as existsSync11 } from "node:fs";
10630
+ import { existsSync as existsSync12 } from "node:fs";
10214
10631
  import { randomUUID as randomUUID2 } from "node:crypto";
10215
- import { mkdir as mkdir6, open, readFile as readFile25, rename as rename3, rm as rm2 } from "node:fs/promises";
10216
- import path14 from "node:path";
10632
+ import { mkdir as mkdir6, open, readFile as readFile26, rename as rename3, rm as rm2 } from "node:fs/promises";
10633
+ import path15 from "node:path";
10217
10634
  import { parseDocument, isSeq, YAMLSeq } from "yaml";
10218
10635
  function resolveManifestPath(projectDir) {
10219
- const existing = MANIFEST_FILENAMES.find((name) => existsSync11(path14.join(projectDir, name)));
10220
- return path14.join(projectDir, existing ?? MANIFEST_FILENAMES[0]);
10636
+ const existing = MANIFEST_FILENAMES.find((name) => existsSync12(path15.join(projectDir, name)));
10637
+ return path15.join(projectDir, existing ?? MANIFEST_FILENAMES[0]);
10221
10638
  }
10222
10639
  async function loadManifestDoc(projectDir) {
10223
10640
  const file = resolveManifestPath(projectDir);
10224
10641
  let text = "";
10225
10642
  try {
10226
- text = await readFile25(file, "utf8");
10643
+ text = await readFile26(file, "utf8");
10227
10644
  } catch (error2) {
10228
10645
  if (error2.code !== "ENOENT")
10229
10646
  throw error2;
@@ -10231,9 +10648,9 @@ async function loadManifestDoc(projectDir) {
10231
10648
  return { doc: parseDocument(text), file };
10232
10649
  }
10233
10650
  async function writeManifestDoc(file, doc) {
10234
- const dir = path14.dirname(file);
10651
+ const dir = path15.dirname(file);
10235
10652
  await mkdir6(dir, { recursive: true });
10236
- const temporary = path14.join(dir, `.${path14.basename(file)}.tmp-${process.pid}-${randomUUID2()}`);
10653
+ const temporary = path15.join(dir, `.${path15.basename(file)}.tmp-${process.pid}-${randomUUID2()}`);
10237
10654
  try {
10238
10655
  const handle = await open(temporary, "wx", 438);
10239
10656
  try {
@@ -10279,9 +10696,9 @@ function scalarString(item) {
10279
10696
  }
10280
10697
 
10281
10698
  // src/lib/theme-manager.ts
10282
- import { cp as cp2, mkdir as mkdir7, readFile as readFile26, readdir as readdir6, stat as stat5, writeFile as writeFile7, rm as rm3 } from "node:fs/promises";
10283
- import { existsSync as existsSync12 } from "node:fs";
10284
- import path15 from "node:path";
10699
+ import { cp as cp2, mkdir as mkdir7, readFile as readFile27, readdir as readdir6, stat as stat5, writeFile as writeFile7, rm as rm3 } from "node:fs/promises";
10700
+ import { existsSync as existsSync13 } from "node:fs";
10701
+ import path16 from "node:path";
10285
10702
  import { isSeq as isSeq2, Scalar } from "yaml";
10286
10703
 
10287
10704
  // src/lib/fetch-timeout.ts
@@ -10338,29 +10755,13 @@ var BUILT_IN_FALLBACK_META = {
10338
10755
  description: "Clean sans-serif manual with clear hierarchy and code styling."
10339
10756
  }
10340
10757
  };
10341
- async function readThemeMeta(jsonPath) {
10342
- try {
10343
- const text = await readFile26(jsonPath, "utf8");
10344
- const parsed = JSON.parse(text);
10345
- return parsed && typeof parsed === "object" ? parsed : {};
10346
- } catch {
10347
- return {};
10348
- }
10349
- }
10758
+ var readThemeMeta = readExtensionMeta;
10350
10759
  function themeStyleList(meta) {
10351
- const declared = Array.isArray(meta.styles) ? meta.styles.filter((s) => typeof s === "string" && s.trim().length > 0) : [];
10760
+ const declared = extensionStyleList(meta);
10352
10761
  return declared.length > 0 ? declared : ["theme.css"];
10353
10762
  }
10354
- function assertThemeSheetsContained(meta) {
10355
- for (const rel of [...themeStyleList(meta), ...themeEngineStyleList(meta)]) {
10356
- if (path15.isAbsolute(rel) || rel.split(/[\\/]/).includes("..")) {
10357
- throw new Error(`theme.json declares stylesheet "${rel}" outside the theme folder; a theme must be self-contained.`);
10358
- }
10359
- }
10360
- }
10361
- function themeEngineStyleList(meta) {
10362
- return Array.isArray(meta.engineStyles?.native) ? meta.engineStyles.native.filter((s) => typeof s === "string" && s.trim().length > 0) : [];
10363
- }
10763
+ var assertThemeSheetsContained = assertExtensionContained;
10764
+ var themeEngineStyleList = extensionEngineStyleList;
10364
10765
  function themeInfo(id, kind, meta) {
10365
10766
  const styles = themeStyleList(meta);
10366
10767
  return {
@@ -10371,7 +10772,10 @@ function themeInfo(id, kind, meta) {
10371
10772
  kind,
10372
10773
  preview: meta.preview ?? null,
10373
10774
  styles,
10374
- tokensFile: meta.tokensFile?.trim() || styles[0]
10775
+ tokensFile: meta.tokensFile?.trim() || styles[0],
10776
+ markdown: meta.markdown?.trim() || undefined,
10777
+ components: meta.components?.trim() || undefined,
10778
+ snippets: meta.snippets?.trim() || undefined
10375
10779
  };
10376
10780
  }
10377
10781
  function themeSlug(name) {
@@ -10381,14 +10785,14 @@ function themeDirFor(projectDir, id) {
10381
10785
  if (typeof id !== "string" || id.length === 0 || themeSlug(id) !== id) {
10382
10786
  throw new Error(`Invalid theme id "${id}".`);
10383
10787
  }
10384
- return path15.join(projectDir, THEMES_DIR, id);
10788
+ return path16.join(projectDir, THEMES_DIR, id);
10385
10789
  }
10386
10790
  async function listBuiltInThemes() {
10387
10791
  const out = [];
10388
10792
  for (const id of BUILT_IN_THEME_IDS) {
10389
10793
  let meta = {};
10390
10794
  try {
10391
- meta = await readThemeMeta(await getAssetPath(`${THEMES_DIR}/${id}/theme.json`));
10795
+ meta = await readThemeMeta(path16.join(await getAssetsDir(), THEMES_DIR, id));
10392
10796
  } catch {
10393
10797
  meta = {};
10394
10798
  }
@@ -10406,20 +10810,19 @@ async function resolveBuiltInTheme(id) {
10406
10810
  throw new Error(`Unknown built-in theme: "${id}".`);
10407
10811
  }
10408
10812
  const cssPath = await getAssetPath(`${THEMES_DIR}/${id}/theme.css`);
10409
- const jsonPath = await getAssetPath(`${THEMES_DIR}/${id}/theme.json`);
10410
- if (!existsSync12(cssPath)) {
10813
+ if (!existsSync13(cssPath)) {
10411
10814
  throw new Error(`Built-in theme "${id}" is missing its theme.css.`);
10412
10815
  }
10413
- const meta = await readThemeMeta(jsonPath);
10816
+ const meta = await readThemeMeta(path16.join(await getAssetsDir(), THEMES_DIR, id));
10414
10817
  if (!meta.name)
10415
10818
  meta.name = BUILT_IN_FALLBACK_META[id]?.name;
10416
10819
  if (!meta.description) {
10417
10820
  meta.description = BUILT_IN_FALLBACK_META[id]?.description;
10418
10821
  }
10419
- return { info: themeInfo(id, "builtin", meta), cssPath, dir: path15.dirname(cssPath) };
10822
+ return { info: themeInfo(id, "builtin", meta), cssPath, dir: path16.dirname(cssPath) };
10420
10823
  }
10421
10824
  async function listProjectThemes(projectDir) {
10422
- const root = path15.join(projectDir, THEMES_DIR);
10825
+ const root = path16.join(projectDir, THEMES_DIR);
10423
10826
  let entries;
10424
10827
  try {
10425
10828
  entries = await readdir6(root, { withFileTypes: true });
@@ -10430,10 +10833,10 @@ async function listProjectThemes(projectDir) {
10430
10833
  for (const entry of entries) {
10431
10834
  if (!entry.isDirectory())
10432
10835
  continue;
10433
- const dir = path15.join(root, entry.name);
10434
- const meta = await readThemeMeta(path15.join(dir, "theme.json"));
10836
+ const dir = path16.join(root, entry.name);
10837
+ const meta = await readThemeMeta(dir);
10435
10838
  const primary = themeStyleList(meta)[0];
10436
- if (!existsSync12(path15.join(dir, primary)))
10839
+ if (!existsSync13(path16.join(dir, primary)))
10437
10840
  continue;
10438
10841
  out.push(themeInfo(entry.name, "project", meta));
10439
10842
  }
@@ -10484,10 +10887,10 @@ async function getActiveTheme(projectDir) {
10484
10887
  if (!m)
10485
10888
  continue;
10486
10889
  const id = m[1];
10487
- const dir = path15.join(projectDir, THEMES_DIR, id);
10488
- const meta = await readThemeMeta(path15.join(dir, "theme.json"));
10890
+ const dir = path16.join(projectDir, THEMES_DIR, id);
10891
+ const meta = await readThemeMeta(dir);
10489
10892
  const primary = themeStyleList(meta)[0];
10490
- if (!existsSync12(path15.join(dir, primary)))
10893
+ if (!existsSync13(path16.join(dir, primary)))
10491
10894
  continue;
10492
10895
  return themeInfo(id, "project", meta);
10493
10896
  }
@@ -10525,9 +10928,9 @@ async function getPreviousTheme(projectDir) {
10525
10928
  if (!id)
10526
10929
  return null;
10527
10930
  const dir = themeDirFor(projectDir, id);
10528
- const meta = await readThemeMeta(path15.join(dir, "theme.json"));
10931
+ const meta = await readThemeMeta(dir);
10529
10932
  const primary = themeStyleList(meta)[0];
10530
- if (!existsSync12(path15.join(dir, primary)))
10933
+ if (!existsSync13(path16.join(dir, primary)))
10531
10934
  return null;
10532
10935
  const active = await getActiveTheme(projectDir);
10533
10936
  if (active && active.id === id)
@@ -10547,23 +10950,23 @@ async function applyTheme(projectDir, target) {
10547
10950
  if (target.kind === "builtin") {
10548
10951
  const resolved = await resolveBuiltInTheme(target.id);
10549
10952
  let destId = target.id;
10550
- if (existsSync12(path15.join(projectDir, THEMES_DIR, destId, "theme.css"))) {
10953
+ if (existsSync13(path16.join(projectDir, THEMES_DIR, destId, "theme.css"))) {
10551
10954
  destId = await uniqueThemeId(projectDir, target.id);
10552
10955
  }
10553
- const destDir = path15.join(projectDir, THEMES_DIR, destId);
10956
+ const destDir = path16.join(projectDir, THEMES_DIR, destId);
10554
10957
  await mkdir7(destDir, { recursive: true });
10555
10958
  await cp2(resolved.dir, destDir, { recursive: true });
10556
- meta = await readThemeMeta(path15.join(destDir, "theme.json"));
10959
+ meta = await readThemeMeta(destDir);
10557
10960
  assertThemeSheetsContained(meta);
10558
10961
  resolveDeclaredStyles(themeStyleList(meta), destDir, `Theme "${destId}"`);
10559
10962
  resolveDeclaredStyles(themeEngineStyleList(meta), destDir, `Theme "${destId}"`);
10560
10963
  info2 = themeInfo(destId, "project", meta);
10561
10964
  } else {
10562
10965
  const dir = themeDirFor(projectDir, target.id);
10563
- meta = await readThemeMeta(path15.join(dir, "theme.json"));
10966
+ meta = await readThemeMeta(dir);
10564
10967
  assertThemeSheetsContained(meta);
10565
10968
  const primary = themeStyleList(meta)[0];
10566
- if (!existsSync12(path15.join(dir, primary))) {
10969
+ if (!existsSync13(path16.join(dir, primary))) {
10567
10970
  throw new Error(`Theme "${target.id}" is not present in this project.`);
10568
10971
  }
10569
10972
  resolveDeclaredStyles(themeStyleList(meta), dir, `Theme "${target.id}"`);
@@ -10574,10 +10977,10 @@ async function applyTheme(projectDir, target) {
10574
10977
  return info2;
10575
10978
  }
10576
10979
  async function uniqueThemeId(projectDir, base) {
10577
- const root = path15.join(projectDir, THEMES_DIR);
10980
+ const root = path16.join(projectDir, THEMES_DIR);
10578
10981
  let id = themeSlug(base);
10579
10982
  let n = 2;
10580
- while (existsSync12(path15.join(root, id))) {
10983
+ while (existsSync13(path16.join(root, id))) {
10581
10984
  id = `${themeSlug(base)}-${n++}`;
10582
10985
  }
10583
10986
  return id;
@@ -10592,20 +10995,20 @@ async function importThemeFromFolder(projectDir, sourceDir) {
10592
10995
  if (!info2.isDirectory()) {
10593
10996
  throw new Error("The chosen path is not a folder.");
10594
10997
  }
10595
- const meta = await readThemeMeta(path15.join(sourceDir, "theme.json"));
10998
+ const meta = await readThemeMeta(sourceDir);
10596
10999
  assertThemeSheetsContained(meta);
10597
11000
  const primary = themeStyleList(meta)[0];
10598
- if (!existsSync12(path15.join(sourceDir, primary))) {
11001
+ if (!existsSync13(path16.join(sourceDir, primary))) {
10599
11002
  throw new Error(primary === "theme.css" ? "A theme folder must contain a theme.css file." : `A theme folder must contain its declared stylesheet: ${primary}`);
10600
11003
  }
10601
11004
  resolveDeclaredStyles(themeStyleList(meta), sourceDir, "The theme folder");
10602
11005
  resolveDeclaredStyles(themeEngineStyleList(meta), sourceDir, "The theme folder");
10603
- const base = meta.name || path15.basename(sourceDir);
11006
+ const base = meta.name || path16.basename(sourceDir);
10604
11007
  const id = await uniqueThemeId(projectDir, base);
10605
- const destDir = path15.join(projectDir, THEMES_DIR, id);
10606
- await mkdir7(path15.dirname(destDir), { recursive: true });
11008
+ const destDir = path16.join(projectDir, THEMES_DIR, id);
11009
+ await mkdir7(path16.dirname(destDir), { recursive: true });
10607
11010
  await cp2(sourceDir, destDir, { recursive: true });
10608
- return themeInfo(id, "project", await readThemeMeta(path15.join(destDir, "theme.json")));
11011
+ return themeInfo(id, "project", await readThemeMeta(destDir));
10609
11012
  }
10610
11013
  function looksLikeCssUrl(url) {
10611
11014
  try {
@@ -10652,7 +11055,7 @@ async function importThemeFromUrl(projectDir, url) {
10652
11055
  let baseName;
10653
11056
  if (looksLikeCssUrl(trimmed)) {
10654
11057
  css = await fetchText(trimmed);
10655
- baseName = path15.basename(new URL(trimmed, "https://x/").pathname, ".css") || "theme";
11058
+ baseName = path16.basename(new URL(trimmed, "https://x/").pathname, ".css") || "theme";
10656
11059
  } else {
10657
11060
  const base = trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
10658
11061
  css = await fetchText(`${base}theme.css`);
@@ -10662,41 +11065,41 @@ async function importThemeFromUrl(projectDir, url) {
10662
11065
  if (parsed && typeof parsed === "object")
10663
11066
  meta = parsed;
10664
11067
  } catch {}
10665
- baseName = meta.name || path15.basename(new URL(base, "https://x/").pathname.replace(/\/$/, "")) || "theme";
11068
+ baseName = meta.name || path16.basename(new URL(base, "https://x/").pathname.replace(/\/$/, "")) || "theme";
10666
11069
  }
10667
11070
  if (!css.trim()) {
10668
11071
  throw new Error(`Fetched theme CSS from ${trimmed} was empty.`);
10669
11072
  }
10670
11073
  assertLooksLikeCss(css, trimmed);
10671
11074
  const id = await uniqueThemeId(projectDir, meta.name || baseName);
10672
- const destDir = path15.join(projectDir, THEMES_DIR, id);
11075
+ const destDir = path16.join(projectDir, THEMES_DIR, id);
10673
11076
  await mkdir7(destDir, { recursive: true });
10674
- await writeFile7(path15.join(destDir, "theme.css"), css, "utf8");
11077
+ await writeFile7(path16.join(destDir, "theme.css"), css, "utf8");
10675
11078
  const finalMeta = {
10676
11079
  name: meta.name || prettify(id),
10677
11080
  author: meta.author,
10678
11081
  description: meta.description,
10679
11082
  preview: meta.preview ?? null
10680
11083
  };
10681
- await writeFile7(path15.join(destDir, "theme.json"), JSON.stringify(finalMeta, null, 2), "utf8");
11084
+ await writeFile7(path16.join(destDir, "theme.json"), JSON.stringify(finalMeta, null, 2), "utf8");
10682
11085
  return themeInfo(id, "project", finalMeta);
10683
11086
  }
10684
11087
  async function readThemeCss(projectDir, source) {
10685
11088
  if (source.kind === "builtin") {
10686
11089
  const resolved = await resolveBuiltInTheme(source.id);
10687
- return readFile26(resolved.cssPath, "utf8");
11090
+ return readFile27(resolved.cssPath, "utf8");
10688
11091
  }
10689
11092
  if (!projectDir)
10690
11093
  throw new Error("A project is required to read a project theme.");
10691
11094
  const dir = themeDirFor(projectDir, source.id);
10692
- const meta = await readThemeMeta(path15.join(dir, "theme.json"));
11095
+ const meta = await readThemeMeta(dir);
10693
11096
  const sheets = themeStyleList(meta);
10694
11097
  const parts = [];
10695
11098
  for (const rel of sheets) {
10696
- const sheetPath = path15.join(dir, rel);
10697
- if (!existsSync12(sheetPath))
11099
+ const sheetPath = path16.join(dir, rel);
11100
+ if (!existsSync13(sheetPath))
10698
11101
  continue;
10699
- parts.push(await readFile26(sheetPath, "utf8"));
11102
+ parts.push(await readFile27(sheetPath, "utf8"));
10700
11103
  }
10701
11104
  if (parts.length === 0) {
10702
11105
  throw new Error(`Theme "${source.id}" has no ${sheets[0]} in this project.`);
@@ -10707,7 +11110,7 @@ async function readThemeCss(projectDir, source) {
10707
11110
  }
10708
11111
  async function removeProjectTheme(projectDir, id) {
10709
11112
  const dir = themeDirFor(projectDir, id);
10710
- if (existsSync12(dir)) {
11113
+ if (existsSync13(dir)) {
10711
11114
  await rm3(dir, { recursive: true, force: true });
10712
11115
  }
10713
11116
  const { doc, file } = await loadManifestDoc(projectDir);
@@ -10745,7 +11148,7 @@ async function detectLegacyForkedTheme(projectDir, active) {
10745
11148
  return null;
10746
11149
  let bookCss;
10747
11150
  try {
10748
- bookCss = await readFile26(path15.join(projectDir, "styles", "book.css"), "utf8");
11151
+ bookCss = await readFile27(path16.join(projectDir, "styles", "book.css"), "utf8");
10749
11152
  } catch {
10750
11153
  return null;
10751
11154
  }
@@ -10844,7 +11247,7 @@ async function scaffoldProject(options) {
10844
11247
  throw new CreateProjectErrorImpl("invalid-name", `Could not derive a valid folder name from "${name}".`);
10845
11248
  }
10846
11249
  const parentDir = options.parentDir;
10847
- if (!parentDir || !path16.isAbsolute(parentDir)) {
11250
+ if (!parentDir || !path17.isAbsolute(parentDir)) {
10848
11251
  throw new CreateProjectErrorImpl("parent-not-writable", "The save location must be an absolute path.");
10849
11252
  }
10850
11253
  try {
@@ -10852,7 +11255,7 @@ async function scaffoldProject(options) {
10852
11255
  } catch {
10853
11256
  throw new CreateProjectErrorImpl("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
10854
11257
  }
10855
- const projectDir = path16.join(parentDir, folderName);
11258
+ const projectDir = path17.join(parentDir, folderName);
10856
11259
  let targetExists = true;
10857
11260
  try {
10858
11261
  await access(projectDir, FS.F_OK);
@@ -10874,17 +11277,17 @@ async function scaffoldProject(options) {
10874
11277
  for (const entry of entries) {
10875
11278
  if (entry.name === ".gutterpress-template.json")
10876
11279
  continue;
10877
- await cp3(path16.join(customTemplateDir, entry.name), path16.join(projectDir, entry.name), { recursive: true });
11280
+ await cp3(path17.join(customTemplateDir, entry.name), path17.join(projectDir, entry.name), { recursive: true });
10878
11281
  }
10879
- await mkdir8(path16.join(projectDir, "assets"), { recursive: true });
11282
+ await mkdir8(path17.join(projectDir, "assets"), { recursive: true });
10880
11283
  } else {
10881
11284
  const tplManifest = await getAssetPath(`templates/${template}/manifest.yaml`);
10882
11285
  const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
10883
- await mkdir8(path16.join(projectDir, "assets"), { recursive: true });
10884
- await copyFile(tplManifest, path16.join(projectDir, MANIFEST_FILENAMES[0]));
10885
- await copyFile(tplChapter, path16.join(projectDir, "chapter-01.md"));
10886
- await mkdir8(path16.join(projectDir, "styles"), { recursive: true });
10887
- await writeFile8(path16.join(projectDir, "styles", "book.css"), starterOverridesCss(starterThemeId), "utf8");
11286
+ await mkdir8(path17.join(projectDir, "assets"), { recursive: true });
11287
+ await copyFile(tplManifest, path17.join(projectDir, MANIFEST_FILENAMES[0]));
11288
+ await copyFile(tplChapter, path17.join(projectDir, "chapter-01.md"));
11289
+ await mkdir8(path17.join(projectDir, "styles"), { recursive: true });
11290
+ await writeFile8(path17.join(projectDir, "styles", "book.css"), starterOverridesCss(starterThemeId), "utf8");
10888
11291
  }
10889
11292
  } catch (e) {
10890
11293
  throw new CreateProjectErrorImpl("scaffold-io", `Could not create the project files: ${e instanceof Error ? e.message : String(e)}`);
@@ -10894,13 +11297,13 @@ async function scaffoldProject(options) {
10894
11297
  } catch (e) {
10895
11298
  throw new CreateProjectErrorImpl("scaffold-io", `Could not write .gitignore: ${e instanceof Error ? e.message : String(e)}`);
10896
11299
  }
10897
- const manifestPath = MANIFEST_FILENAMES.map((name2) => path16.join(projectDir, name2)).find(existsSync13) ?? path16.join(projectDir, MANIFEST_FILENAMES[0]);
11300
+ const manifestPath = MANIFEST_FILENAMES.map((name2) => path17.join(projectDir, name2)).find(existsSync14) ?? path17.join(projectDir, MANIFEST_FILENAMES[0]);
10898
11301
  const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
10899
11302
  const substitutions = {
10900
11303
  "{{TITLE}}": escapeYamlScalar(name),
10901
11304
  "{{AUTHOR}}": escapeYamlScalar(author)
10902
11305
  };
10903
- let openFile = path16.join(projectDir, "chapter-01.md");
11306
+ let openFile = path17.join(projectDir, "chapter-01.md");
10904
11307
  try {
10905
11308
  await fillTemplateFile(manifestPath, substitutions);
10906
11309
  if (!customTemplateDir) {
@@ -10924,7 +11327,7 @@ async function scaffoldProject(options) {
10924
11327
  }
10925
11328
  const firstSource = await firstSourceFile(manifestPath);
10926
11329
  if (firstSource)
10927
- openFile = path16.join(projectDir, firstSource);
11330
+ openFile = path17.join(projectDir, firstSource);
10928
11331
  await fillTemplateFile(openFile, { "{{TITLE}}": name }).catch(() => {});
10929
11332
  } catch (e) {
10930
11333
  throw new CreateProjectErrorImpl("scaffold-io", `Could not finalise the project files: ${e instanceof Error ? e.message : String(e)}`);
@@ -10946,7 +11349,7 @@ async function scaffoldProject(options) {
10946
11349
  versionHistory = "none";
10947
11350
  const { findEnclosingRepoDir } = await import("./project-source-ekcyp63q.js");
10948
11351
  const enclosingRepoDir = await findEnclosingRepoDir(projectDir);
10949
- versionHistoryError = enclosingRepoDir ? `This book was added to ${path16.basename(enclosingRepoDir)}'s shared version history; save a snapshot to record it.` : e instanceof Error ? e.message : String(e);
11352
+ versionHistoryError = enclosingRepoDir ? `This book was added to ${path17.basename(enclosingRepoDir)}'s shared version history; save a snapshot to record it.` : e instanceof Error ? e.message : String(e);
10950
11353
  }
10951
11354
  }
10952
11355
  const result = {
@@ -10966,7 +11369,7 @@ function prettifyFolderName(base) {
10966
11369
  }
10967
11370
  async function adoptFolder(options) {
10968
11371
  const dir = options.dir;
10969
- if (typeof dir !== "string" || !path16.isAbsolute(dir)) {
11372
+ if (typeof dir !== "string" || !path17.isAbsolute(dir)) {
10970
11373
  throw new CreateProjectErrorImpl("scaffold-io", "An absolute folder path is required.");
10971
11374
  }
10972
11375
  let st;
@@ -10978,11 +11381,11 @@ async function adoptFolder(options) {
10978
11381
  if (!st.isDirectory()) {
10979
11382
  throw new CreateProjectErrorImpl("scaffold-io", `Not a folder: ${dir}`);
10980
11383
  }
10981
- if (MANIFEST_FILENAMES.some((name) => existsSync13(path16.join(dir, name)))) {
11384
+ if (MANIFEST_FILENAMES.some((name) => existsSync14(path17.join(dir, name)))) {
10982
11385
  throw new CreateProjectErrorImpl("target-exists", "This folder is already a Gutterpress project.");
10983
11386
  }
10984
11387
  const template = options.template ?? "book";
10985
- const title = (options.title ?? "").trim() || prettifyFolderName(path16.basename(dir));
11388
+ const title = (options.title ?? "").trim() || prettifyFolderName(path17.basename(dir));
10986
11389
  const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
10987
11390
  let mdFiles;
10988
11391
  try {
@@ -10990,14 +11393,14 @@ async function adoptFolder(options) {
10990
11393
  mdFiles = entries.filter((e) => e.isFile() && /\.(md|markdown)$/i.test(e.name)).map((e) => e.name).sort((a, b) => a.localeCompare(b));
10991
11394
  if (mdFiles.length === 0) {
10992
11395
  const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
10993
- await copyFile(tplChapter, path16.join(dir, "chapter-01.md"));
10994
- await fillTemplateFile(path16.join(dir, "chapter-01.md"), { "{{TITLE}}": title }).catch(() => {});
11396
+ await copyFile(tplChapter, path17.join(dir, "chapter-01.md"));
11397
+ await fillTemplateFile(path17.join(dir, "chapter-01.md"), { "{{TITLE}}": title }).catch(() => {});
10995
11398
  mdFiles = ["chapter-01.md"];
10996
11399
  }
10997
11400
  const themeId = STARTER_THEME_FOR_TEMPLATE[template] ?? "clean-book";
10998
- await mkdir8(path16.join(dir, "styles"), { recursive: true });
10999
- const bookCssPath = path16.join(dir, "styles", "book.css");
11000
- const hadOwnStylesheet = existsSync13(bookCssPath);
11401
+ await mkdir8(path17.join(dir, "styles"), { recursive: true });
11402
+ const bookCssPath = path17.join(dir, "styles", "book.css");
11403
+ const hadOwnStylesheet = existsSync14(bookCssPath);
11001
11404
  if (!hadOwnStylesheet) {
11002
11405
  await writeFile8(bookCssPath, starterOverridesCss(themeId), "utf8");
11003
11406
  }
@@ -11015,8 +11418,8 @@ ${filesYaml}
11015
11418
  styles:
11016
11419
  - styles/book.css
11017
11420
  `;
11018
- await writeFile8(path16.join(dir, MANIFEST_FILENAMES[0]), manifest, "utf8");
11019
- await mkdir8(path16.join(dir, "assets"), { recursive: true });
11421
+ await writeFile8(path17.join(dir, MANIFEST_FILENAMES[0]), manifest, "utf8");
11422
+ await mkdir8(path17.join(dir, "assets"), { recursive: true });
11020
11423
  if (!hadOwnStylesheet) {
11021
11424
  await applyTheme(dir, { kind: "builtin", id: themeId });
11022
11425
  }
@@ -11046,8 +11449,8 @@ styles:
11046
11449
  }
11047
11450
  const result = {
11048
11451
  projectDir: dir,
11049
- manifestPath: path16.join(dir, MANIFEST_FILENAMES[0]),
11050
- openFile: path16.join(dir, mdFiles[0]),
11452
+ manifestPath: path17.join(dir, MANIFEST_FILENAMES[0]),
11453
+ openFile: path17.join(dir, mdFiles[0]),
11051
11454
  versionHistory
11052
11455
  };
11053
11456
  if (versionHistoryError !== undefined)
@@ -11055,10 +11458,10 @@ styles:
11055
11458
  return result;
11056
11459
  }
11057
11460
  async function ensureGitignoreHasDist(projectDir) {
11058
- const gitignorePath = path16.join(projectDir, ".gitignore");
11461
+ const gitignorePath = path17.join(projectDir, ".gitignore");
11059
11462
  let existing = null;
11060
11463
  try {
11061
- existing = await readFile27(gitignorePath, "utf8");
11464
+ existing = await readFile28(gitignorePath, "utf8");
11062
11465
  } catch {}
11063
11466
  if (existing === null) {
11064
11467
  await writeFile8(gitignorePath, `dist/
@@ -11078,7 +11481,7 @@ async function ensureGitignoreHasDist(projectDir) {
11078
11481
  async function firstSourceFile(manifestPath) {
11079
11482
  let text;
11080
11483
  try {
11081
- text = await readFile27(manifestPath, "utf8");
11484
+ text = await readFile28(manifestPath, "utf8");
11082
11485
  } catch {
11083
11486
  return;
11084
11487
  }
@@ -11090,17 +11493,161 @@ async function firstSourceFile(manifestPath) {
11090
11493
  return m ? m[1].trim() : undefined;
11091
11494
  }
11092
11495
  async function fillTemplateFile(filePath, substitutions) {
11093
- let text = await readFile27(filePath, "utf8");
11496
+ let text = await readFile28(filePath, "utf8");
11094
11497
  for (const [token, value] of Object.entries(substitutions)) {
11095
11498
  text = text.split(token).join(value);
11096
11499
  }
11097
11500
  await writeFile8(filePath, text, "utf8");
11098
11501
  }
11502
+ // src/lib/extension-scaffold.ts
11503
+ import { access as access2, mkdir as mkdir9, readFile as readFile29, writeFile as writeFile9 } from "node:fs/promises";
11504
+ import { constants as FS2 } from "node:fs";
11505
+ import path18 from "node:path";
11506
+ var EXTENSION_KINDS = ["plugin", "theme"];
11507
+ var RESERVED_PREFIX = "gp-";
11508
+ var TEMPLATE_FILES = {
11509
+ plugin: [
11510
+ ["gutterpress.json", "gutterpress.json"],
11511
+ ["package.json", "package.json"],
11512
+ ["README.md", "README.md"],
11513
+ ["plugin.js.tpl", "plugin.js"],
11514
+ ["styles/plugin.css", "styles/plugin.css"],
11515
+ ["snippets/term-box.md", "snippets/term-box.md"],
11516
+ ["test/fixture.md", "test/fixture.md"],
11517
+ ["test/expected.html", "test/expected.html"],
11518
+ ["test/plugin.test.js.tpl", "test/plugin.test.js"]
11519
+ ],
11520
+ theme: [
11521
+ ["gutterpress.json", "gutterpress.json"],
11522
+ ["README.md", "README.md"],
11523
+ ["components.yaml", "components.yaml"],
11524
+ ["snippets/callout.md", "snippets/callout.md"],
11525
+ ["styles/tokens.css", "styles/tokens.css"],
11526
+ ["styles/base.css", "styles/base.css"],
11527
+ ["styles/components.css", "styles/components.css"],
11528
+ ["styles/page-templates.css", "styles/page-templates.css"],
11529
+ ["styles/page-rules.css", "styles/page-rules.css"],
11530
+ ["styles/book.css", "styles/book.css"]
11531
+ ]
11532
+ };
11533
+ var OPEN_FILE = {
11534
+ plugin: "plugin.js",
11535
+ theme: "styles/tokens.css"
11536
+ };
11537
+ var DEFAULT_DESCRIPTION = {
11538
+ plugin: "A Gutterpress plugin.",
11539
+ theme: "A Gutterpress theme."
11540
+ };
11541
+ var DEFAULT_AUTHOR2 = "Anonymous";
11542
+
11543
+ class ExtensionScaffoldError extends Error {
11544
+ code;
11545
+ constructor(code, message) {
11546
+ super(message);
11547
+ this.name = "CreateProjectError";
11548
+ this.code = code;
11549
+ }
11550
+ }
11551
+ function resolveExtensionPrefix(slug, requested) {
11552
+ const raw = (requested ?? `${slug}-`).trim();
11553
+ const prefix = raw.endsWith("-") ? raw : `${raw}-`;
11554
+ if (!/^[a-z][a-z0-9-]*-$/.test(prefix)) {
11555
+ throw new ExtensionScaffoldError("invalid-name", `"${raw}" is not a usable prefix. Use lower-case letters, digits and hyphens, ` + `starting with a letter — for example "${slug}-" or "fn-".`);
11556
+ }
11557
+ if (prefix === RESERVED_PREFIX) {
11558
+ throw new ExtensionScaffoldError("invalid-name", `"${RESERVED_PREFIX}" is reserved for Gutterpress core's own classes and custom ` + `properties. Taking it would silently override them — choose your own prefix.`);
11559
+ }
11560
+ return prefix;
11561
+ }
11562
+ function identifierFor(slug) {
11563
+ const camel = slug.replace(/-([a-z0-9])/g, (_m, c) => c.toUpperCase());
11564
+ return /^[0-9]/.test(camel) ? `_${camel}` : camel;
11565
+ }
11566
+ function escapeJsonScalar(value) {
11567
+ return JSON.stringify(value).slice(1, -1);
11568
+ }
11569
+ async function scaffoldExtension(options) {
11570
+ const kind = options.kind;
11571
+ if (kind !== "plugin" && kind !== "theme") {
11572
+ throw new ExtensionScaffoldError("invalid-name", `Unknown extension kind "${String(kind)}". Choose one of: ${EXTENSION_KINDS.join(", ")}.`);
11573
+ }
11574
+ const name = (options.name ?? "").trim();
11575
+ if (!name) {
11576
+ throw new ExtensionScaffoldError("invalid-name", "A name is required.");
11577
+ }
11578
+ const slug = (options.folderName ?? slugify(name)).trim();
11579
+ if (!slug || slug !== slugify(slug)) {
11580
+ throw new ExtensionScaffoldError("invalid-name", `Could not derive a valid folder name from "${name}".`);
11581
+ }
11582
+ const prefix = resolveExtensionPrefix(slug, options.prefix);
11583
+ const parentDir = options.parentDir;
11584
+ if (!parentDir || !path18.isAbsolute(parentDir)) {
11585
+ throw new ExtensionScaffoldError("parent-not-writable", "The save location must be an absolute path.");
11586
+ }
11587
+ try {
11588
+ await access2(parentDir, FS2.W_OK);
11589
+ } catch {
11590
+ throw new ExtensionScaffoldError("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
11591
+ }
11592
+ const extensionDir = path18.join(parentDir, slug);
11593
+ let targetExists = true;
11594
+ try {
11595
+ await access2(extensionDir, FS2.F_OK);
11596
+ } catch {
11597
+ targetExists = false;
11598
+ }
11599
+ if (targetExists) {
11600
+ throw new ExtensionScaffoldError("target-exists", `A folder named "${slug}" already exists here. Choose a different name or location.`);
11601
+ }
11602
+ const author = (options.author ?? "").trim() || DEFAULT_AUTHOR2;
11603
+ const description = (options.description ?? "").trim() || DEFAULT_DESCRIPTION[kind];
11604
+ const plain = {
11605
+ "{{NAME}}": name,
11606
+ "{{SLUG}}": slug,
11607
+ "{{PREFIX}}": prefix,
11608
+ "{{AUTHOR}}": author,
11609
+ "{{DESCRIPTION}}": description,
11610
+ "{{IDENT}}": identifierFor(slug)
11611
+ };
11612
+ const jsonSafe = Object.fromEntries(Object.entries(plain).map(([token, value]) => [token, escapeJsonScalar(value)]));
11613
+ const entries = TEMPLATE_FILES[kind];
11614
+ const written = [];
11615
+ try {
11616
+ await mkdir9(extensionDir, { recursive: true });
11617
+ for (const [assetName, relDest] of entries) {
11618
+ const src = await getAssetPath(`extension-templates/${kind}/${assetName}`);
11619
+ const dest = path18.join(extensionDir, relDest);
11620
+ await mkdir9(path18.dirname(dest), { recursive: true });
11621
+ const text = await readFile29(src, "utf8");
11622
+ const table = relDest.endsWith(".json") ? jsonSafe : plain;
11623
+ await writeFile9(dest, substitute(text, table), "utf8");
11624
+ written.push(relDest);
11625
+ }
11626
+ } catch (e) {
11627
+ throw new ExtensionScaffoldError("scaffold-io", `Could not create the ${kind} files: ${e instanceof Error ? e.message : String(e)}`);
11628
+ }
11629
+ return {
11630
+ extensionDir,
11631
+ manifestPath: path18.join(extensionDir, "gutterpress.json"),
11632
+ kind,
11633
+ slug,
11634
+ prefix,
11635
+ openFile: path18.join(extensionDir, OPEN_FILE[kind]),
11636
+ files: written
11637
+ };
11638
+ }
11639
+ function substitute(text, table) {
11640
+ let out = text;
11641
+ for (const [token, value] of Object.entries(table)) {
11642
+ out = out.split(token).join(value);
11643
+ }
11644
+ return out;
11645
+ }
11099
11646
  // src/lib/project-templates.ts
11100
- import { cp as cp4, mkdir as mkdir9, readFile as readFile28, readdir as readdir8, stat as stat7, writeFile as writeFile9 } from "node:fs/promises";
11101
- import { constants as FS2, existsSync as existsSync14 } from "node:fs";
11102
- import { access as access2 } from "node:fs/promises";
11103
- import path17 from "node:path";
11647
+ import { cp as cp4, mkdir as mkdir10, readFile as readFile30, readdir as readdir8, stat as stat7, writeFile as writeFile10 } from "node:fs/promises";
11648
+ import { constants as FS3, existsSync as existsSync15 } from "node:fs";
11649
+ import { access as access3 } from "node:fs/promises";
11650
+ import path19 from "node:path";
11104
11651
  import { isMap, isScalar, isSeq as isSeq3, parseDocument as parseDocument2 } from "yaml";
11105
11652
  var BUILT_IN_TEMPLATE_IDS = [
11106
11653
  "book",
@@ -11109,7 +11656,7 @@ var BUILT_IN_TEMPLATE_IDS = [
11109
11656
  ];
11110
11657
  async function readTemplateChoices(manifestPath) {
11111
11658
  try {
11112
- const text = await readFile28(manifestPath, "utf8");
11659
+ const text = await readFile30(manifestPath, "utf8");
11113
11660
  const doc = parseDocument2(text);
11114
11661
  const out = {};
11115
11662
  const preset = doc.get("preset");
@@ -11184,7 +11731,7 @@ function rewriteEntry(node, newPath) {
11184
11731
  node.value = newPath;
11185
11732
  }
11186
11733
  function commonAncestorDir(absFiles) {
11187
- const dirSegs = absFiles.map((f) => path17.resolve(f).split(path17.sep).slice(0, -1));
11734
+ const dirSegs = absFiles.map((f) => path19.resolve(f).split(path19.sep).slice(0, -1));
11188
11735
  if (dirSegs.length === 0)
11189
11736
  return "";
11190
11737
  let common = dirSegs[0];
@@ -11194,22 +11741,22 @@ function commonAncestorDir(absFiles) {
11194
11741
  i++;
11195
11742
  common = common.slice(0, i);
11196
11743
  }
11197
- return common.join(path17.sep) || path17.sep;
11744
+ return common.join(path19.sep) || path19.sep;
11198
11745
  }
11199
11746
  function pickVendorDir(templateDir, base) {
11200
- const preferred = path17.basename(base) || VENDOR_DIR;
11201
- if (!existsSync14(path17.join(templateDir, preferred)))
11747
+ const preferred = path19.basename(base) || VENDOR_DIR;
11748
+ if (!existsSync15(path19.join(templateDir, preferred)))
11202
11749
  return preferred;
11203
11750
  for (let n = 1;; n++) {
11204
11751
  const candidate = `${preferred}-${n}`;
11205
- if (!existsSync14(path17.join(templateDir, candidate)))
11752
+ if (!existsSync15(path19.join(templateDir, candidate)))
11206
11753
  return candidate;
11207
11754
  }
11208
11755
  }
11209
11756
  var TEMPLATE_META_FILE = ".gutterpress-template.json";
11210
11757
  async function reconcileSharedRefs(templateDir, sourceProjectDir, mode) {
11211
11758
  const { doc, file } = await loadManifestDoc(templateDir);
11212
- const escapes = (p) => escapesProjectRoot(sourceProjectDir, path17.resolve(sourceProjectDir, p));
11759
+ const escapes = (p) => escapesProjectRoot(sourceProjectDir, path19.resolve(sourceProjectDir, p));
11213
11760
  const stylesSeq = doc.get("styles", true);
11214
11761
  const pluginsSeq = doc.get("plugins", true);
11215
11762
  const escapingStyles = collectEscaping(isSeq3(stylesSeq) ? stylesSeq.items : [], scalarString, escapes);
@@ -11225,16 +11772,16 @@ async function reconcileSharedRefs(templateDir, sourceProjectDir, mode) {
11225
11772
  return { vendoredRefs: [], excludedRefs };
11226
11773
  }
11227
11774
  const styleClosure = await collectStyleDependencies(sourceProjectDir, escapingStyles.map((s) => s.value));
11228
- const pluginFiles = escapingPlugins.map((p) => path17.resolve(sourceProjectDir, p.value));
11775
+ const pluginFiles = escapingPlugins.map((p) => path19.resolve(sourceProjectDir, p.value));
11229
11776
  const externalFiles = [...new Set([...styleClosure, ...pluginFiles])].filter((f) => escapesProjectRoot(sourceProjectDir, f));
11230
11777
  const base = commonAncestorDir(externalFiles);
11231
11778
  const vendorDir = pickVendorDir(templateDir, base);
11232
- await Promise.all(externalFiles.filter((abs) => existsSync14(abs)).map(async (abs) => {
11233
- const dest = path17.join(templateDir, vendorDir, path17.relative(base, abs));
11234
- await mkdir9(path17.dirname(dest), { recursive: true });
11779
+ await Promise.all(externalFiles.filter((abs) => existsSync15(abs)).map(async (abs) => {
11780
+ const dest = path19.join(templateDir, vendorDir, path19.relative(base, abs));
11781
+ await mkdir10(path19.dirname(dest), { recursive: true });
11235
11782
  await cp4(abs, dest);
11236
11783
  }));
11237
- const bookLocal = (value) => toPosixPath(path17.join(vendorDir, path17.relative(base, path17.resolve(sourceProjectDir, value))));
11784
+ const bookLocal = (value) => toPosixPath(path19.join(vendorDir, path19.relative(base, path19.resolve(sourceProjectDir, value))));
11238
11785
  const vendoredRefs = [];
11239
11786
  for (const { node, value } of [...escapingStyles, ...escapingPlugins]) {
11240
11787
  const rewritten = bookLocal(value);
@@ -11249,30 +11796,30 @@ async function saveProjectAsTemplate(options) {
11249
11796
  const id = slugify(name);
11250
11797
  if (!id)
11251
11798
  throw new Error(`Could not derive a template id from "${name}".`);
11252
- const dir = path17.join(templatesRoot, id);
11799
+ const dir = path19.join(templatesRoot, id);
11253
11800
  let exists = true;
11254
11801
  try {
11255
- await access2(dir, FS2.F_OK);
11802
+ await access3(dir, FS3.F_OK);
11256
11803
  } catch {
11257
11804
  exists = false;
11258
11805
  }
11259
11806
  if (exists) {
11260
11807
  throw new Error(`A template named "${name}" already exists. Choose a different name.`);
11261
11808
  }
11262
- await mkdir9(dir, { recursive: true });
11809
+ await mkdir10(dir, { recursive: true });
11263
11810
  const entries = await readdir8(projectDir, { withFileTypes: true });
11264
11811
  for (const entry of entries) {
11265
11812
  if (SKIP_ENTRIES.has(entry.name))
11266
11813
  continue;
11267
11814
  if (entry.name === TEMPLATE_META_FILE)
11268
11815
  continue;
11269
- await cp4(path17.join(projectDir, entry.name), path17.join(dir, entry.name), { recursive: true });
11816
+ await cp4(path19.join(projectDir, entry.name), path19.join(dir, entry.name), { recursive: true });
11270
11817
  }
11271
11818
  const manifestName = MANIFEST_FILENAMES.find((name2) => entries.some((entry) => entry.name === name2));
11272
11819
  const shared = manifestName ? await reconcileSharedRefs(dir, projectDir, options.sharedRefs ?? "vendor") : { vendoredRefs: [], excludedRefs: [] };
11273
11820
  if (manifestName)
11274
- await retokeniseManifest(path17.join(dir, manifestName));
11275
- await writeFile9(path17.join(dir, TEMPLATE_META_FILE), JSON.stringify({ label: name }, null, 2), "utf8");
11821
+ await retokeniseManifest(path19.join(dir, manifestName));
11822
+ await writeFile10(path19.join(dir, TEMPLATE_META_FILE), JSON.stringify({ label: name }, null, 2), "utf8");
11276
11823
  return {
11277
11824
  id,
11278
11825
  label: name,
@@ -11285,12 +11832,12 @@ async function saveProjectAsTemplate(options) {
11285
11832
  async function retokeniseManifest(manifestPath) {
11286
11833
  let text;
11287
11834
  try {
11288
- text = await readFile28(manifestPath, "utf8");
11835
+ text = await readFile30(manifestPath, "utf8");
11289
11836
  } catch {
11290
11837
  return;
11291
11838
  }
11292
11839
  text = text.replace(/^title:\s*.*$/m, 'title: "{{TITLE}}"').replace(/^(authors:[^\n]*\n[ \t]*-[ \t]*).*$/m, '$1"{{AUTHOR}}"');
11293
- await writeFile9(manifestPath, text, "utf8");
11840
+ await writeFile10(manifestPath, text, "utf8");
11294
11841
  }
11295
11842
  async function listCustomTemplates(templatesRoot) {
11296
11843
  let entries;
@@ -11303,10 +11850,10 @@ async function listCustomTemplates(templatesRoot) {
11303
11850
  for (const entry of entries) {
11304
11851
  if (!entry.isDirectory())
11305
11852
  continue;
11306
- const dir = path17.join(templatesRoot, entry.name);
11853
+ const dir = path19.join(templatesRoot, entry.name);
11307
11854
  let label = prettify(entry.name);
11308
11855
  try {
11309
- const meta = JSON.parse(await readFile28(path17.join(dir, TEMPLATE_META_FILE), "utf8"));
11856
+ const meta = JSON.parse(await readFile30(path19.join(dir, TEMPLATE_META_FILE), "utf8"));
11310
11857
  if (typeof meta.label === "string" && meta.label)
11311
11858
  label = meta.label;
11312
11859
  } catch {}
@@ -11316,7 +11863,7 @@ async function listCustomTemplates(templatesRoot) {
11316
11863
  description: "Your saved template.",
11317
11864
  kind: "custom",
11318
11865
  dir,
11319
- ...await readTemplateChoices(path17.join(dir, MANIFEST_FILENAMES[0]))
11866
+ ...await readTemplateChoices(path19.join(dir, MANIFEST_FILENAMES[0]))
11320
11867
  });
11321
11868
  }
11322
11869
  out.sort((a, b) => a.label.localeCompare(b.label));
@@ -11328,84 +11875,17 @@ async function importTemplateFromFolder(options) {
11328
11875
  if (!srcStat.isDirectory()) {
11329
11876
  throw new Error("The chosen path is not a folder.");
11330
11877
  }
11331
- const name = options.name?.trim() || prettify(path17.basename(sourceDir));
11878
+ const name = options.name?.trim() || prettify(path19.basename(sourceDir));
11332
11879
  return saveProjectAsTemplate({ projectDir: sourceDir, name, templatesRoot });
11333
11880
  }
11334
11881
  // src/lib/snippets.ts
11335
- import { readdir as readdir9, readFile as readFile29, writeFile as writeFile10, mkdir as mkdir10, rm as rm4 } from "node:fs/promises";
11336
- import path18 from "node:path";
11337
- var SNIPPETS_DIR = "snippets";
11338
- var PLACEHOLDER_RE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
11339
- function extractVariables(template) {
11340
- const seen = new Set;
11341
- const order = [];
11342
- for (const m of template.matchAll(PLACEHOLDER_RE)) {
11343
- const name = m[1];
11344
- if (!seen.has(name)) {
11345
- seen.add(name);
11346
- order.push(name);
11347
- }
11348
- }
11349
- return order;
11350
- }
11351
- function substituteVariables(template, values) {
11352
- return template.replace(PLACEHOLDER_RE, (_full, name) => Object.prototype.hasOwnProperty.call(values, name) ? values[name] : "");
11353
- }
11354
- function resolveSnippetPath(projectDir, fileName) {
11355
- const dir = path18.resolve(projectDir, SNIPPETS_DIR);
11356
- const full = path18.resolve(dir, fileName);
11357
- if (full !== path18.join(dir, path18.basename(fileName)) || path18.dirname(full) !== dir) {
11358
- throw new Error(`Unsafe snippet filename: ${fileName}`);
11359
- }
11360
- return full;
11361
- }
11362
- async function listSnippets(projectDir) {
11363
- const dir = path18.join(projectDir, SNIPPETS_DIR);
11364
- let names;
11365
- try {
11366
- names = await readdir9(dir);
11367
- } catch {
11368
- return [];
11369
- }
11370
- const entries = [];
11371
- for (const fileName of names) {
11372
- if (!fileName.toLowerCase().endsWith(".md"))
11373
- continue;
11374
- let body = "";
11375
- try {
11376
- body = await readFile29(path18.join(dir, fileName), "utf8");
11377
- } catch {
11378
- continue;
11379
- }
11380
- entries.push({
11381
- name: prettify(fileName.replace(/\.md$/i, "")),
11382
- fileName,
11383
- variables: extractVariables(body)
11384
- });
11385
- }
11386
- entries.sort((a, b) => a.name.localeCompare(b.name));
11387
- return entries;
11388
- }
11389
- async function readSnippet(projectDir, fileName) {
11390
- return readFile29(resolveSnippetPath(projectDir, fileName), "utf8");
11391
- }
11392
- async function saveSnippet(projectDir, name, body) {
11393
- const stem = slugify(name);
11394
- if (!stem)
11395
- throw new Error(`Could not derive a filename from "${name}".`);
11396
- const fileName = `${stem}.md`;
11397
- const dir = path18.join(projectDir, SNIPPETS_DIR);
11398
- await mkdir10(dir, { recursive: true });
11399
- await writeFile10(resolveSnippetPath(projectDir, fileName), body, "utf8");
11400
- return { name, fileName, variables: extractVariables(body) };
11401
- }
11402
- async function deleteSnippet(projectDir, fileName) {
11403
- await rm4(resolveSnippetPath(projectDir, fileName), { force: true });
11404
- }
11882
+ import { readdir as readdir9, readFile as readFile32, writeFile as writeFile11, mkdir as mkdir13, rm as rm6, stat as stat9 } from "node:fs/promises";
11883
+ import path22 from "node:path";
11884
+
11405
11885
  // src/lib/plugin-manager.ts
11406
11886
  import { randomUUID as randomUUID4 } from "node:crypto";
11407
- import { cp as cp5, lstat as lstat3, mkdir as mkdir12, rename as rename5, rm as rm6, stat as stat8 } from "node:fs/promises";
11408
- import path20 from "node:path";
11887
+ import { cp as cp5, lstat as lstat3, mkdir as mkdir12, rename as rename5, rm as rm5, stat as stat8 } from "node:fs/promises";
11888
+ import path21 from "node:path";
11409
11889
  import { isSeq as isSeq4, isMap as isMap2, isScalar as isScalar2, YAMLMap, Scalar as Scalar2 } from "yaml";
11410
11890
 
11411
11891
  // src/lib/npm-plugin-installer.ts
@@ -11416,12 +11896,12 @@ import {
11416
11896
  mkdir as mkdir11,
11417
11897
  mkdtemp as mkdtemp2,
11418
11898
  open as open2,
11419
- readFile as readFile30,
11899
+ readFile as readFile31,
11420
11900
  realpath as realpath2,
11421
11901
  rename as rename4,
11422
- rm as rm5
11902
+ rm as rm4
11423
11903
  } from "node:fs/promises";
11424
- import path19 from "node:path";
11904
+ import path20 from "node:path";
11425
11905
  import { Transform } from "node:stream";
11426
11906
  import { pipeline } from "node:stream/promises";
11427
11907
  import { createGunzip } from "node:zlib";
@@ -11466,8 +11946,8 @@ function mb(bytes) {
11466
11946
  return `${Math.ceil(bytes / 1024 / 1024)}MB`;
11467
11947
  }
11468
11948
  function isContained2(root, candidate) {
11469
- const relative2 = path19.relative(root, candidate);
11470
- return relative2 === "" || !relative2.startsWith("..") && !path19.isAbsolute(relative2);
11949
+ const relative2 = path20.relative(root, candidate);
11950
+ return relative2 === "" || !relative2.startsWith("..") && !path20.isAbsolute(relative2);
11471
11951
  }
11472
11952
  function mergeLimits(overrides) {
11473
11953
  const limits = { ...DEFAULT_LIMITS, ...overrides };
@@ -11774,13 +12254,13 @@ async function extractTarball(ctx, archivePath, packageDir, packageName) {
11774
12254
  throw rejected;
11775
12255
  }
11776
12256
  async function readInstalledManifest(packageDir, selected) {
11777
- const packageJsonPath = path19.join(packageDir, "package.json");
12257
+ const packageJsonPath = path20.join(packageDir, "package.json");
11778
12258
  let manifest = null;
11779
12259
  try {
11780
12260
  const info2 = await lstat2(packageJsonPath);
11781
12261
  if (!info2.isFile() || info2.isSymbolicLink())
11782
12262
  throw new Error("not a regular file");
11783
- manifest = object2(JSON.parse(await readFile30(packageJsonPath, "utf8")));
12263
+ manifest = object2(JSON.parse(await readFile31(packageJsonPath, "utf8")));
11784
12264
  } catch (error2) {
11785
12265
  throw new SecurityInstallError(`Downloaded ${selected.name}@${selected.version} has no valid package.json (${error2 instanceof Error ? error2.message : String(error2)}).`);
11786
12266
  }
@@ -11850,17 +12330,17 @@ async function installPackage(ctx, name, selector, parentPackagePath, ancestors,
11850
12330
  throw new SecurityInstallError(`Dependency graph exceeds ${ctx.limits.totalPackages} packages.`);
11851
12331
  }
11852
12332
  const relativePath = relativePackagePath(parentPackagePath, name);
11853
- const packageDir = path19.join(ctx.stageRoot, ...relativePath.split("/"));
11854
- const archivePath = path19.join(ctx.downloadsDir, `${ctx.archiveCounter++}.tgz`);
12333
+ const packageDir = path20.join(ctx.stageRoot, ...relativePath.split("/"));
12334
+ const archivePath = path20.join(ctx.downloadsDir, `${ctx.archiveCounter++}.tgz`);
11855
12335
  try {
11856
- await mkdir11(path19.dirname(packageDir), { recursive: true });
12336
+ await mkdir11(path20.dirname(packageDir), { recursive: true });
11857
12337
  await downloadTarball(ctx, selected, archivePath);
11858
12338
  await extractTarball(ctx, archivePath, packageDir, selected.name);
11859
12339
  const manifest = await readInstalledManifest(packageDir, selected);
11860
12340
  if (!platformAllowed(manifest.os, process.platform) || !platformAllowed(manifest.cpu, process.arch)) {
11861
12341
  const reason = `${name}@${selected.version} does not support ${process.platform}/${process.arch}`;
11862
12342
  if (optional) {
11863
- await rm5(packageDir, { recursive: true, force: true });
12343
+ await rm4(packageDir, { recursive: true, force: true });
11864
12344
  return { skipped: reason };
11865
12345
  }
11866
12346
  throw new PackageUnavailableError(reason);
@@ -11929,14 +12409,14 @@ async function installPackage(ctx, name, selector, parentPackagePath, ancestors,
11929
12409
  }
11930
12410
  return { path: relativePath };
11931
12411
  } catch (error2) {
11932
- await rm5(packageDir, { recursive: true, force: true });
12412
+ await rm4(packageDir, { recursive: true, force: true });
11933
12413
  if (optional && isOptionalAvailabilityError(error2)) {
11934
12414
  discardOptionalSubtree();
11935
12415
  return { skipped: error2 instanceof Error ? error2.message : String(error2) };
11936
12416
  }
11937
12417
  throw error2;
11938
12418
  } finally {
11939
- await rm5(archivePath, { force: true });
12419
+ await rm4(archivePath, { force: true });
11940
12420
  }
11941
12421
  }
11942
12422
  async function ensureDirectory(pathname) {
@@ -11952,9 +12432,9 @@ async function ensureDirectory(pathname) {
11952
12432
  }
11953
12433
  }
11954
12434
  async function prepareVendorParent(projectDir, name) {
11955
- const plugins = path19.join(projectDir, PLUGINS_DIR);
11956
- const npm = path19.join(plugins, VENDORED_NPM_DIR);
11957
- const packageParent = path19.dirname(vendoredNpmPluginRoot(projectDir, name, "0.0.0"));
12435
+ const plugins = path20.join(projectDir, PLUGINS_DIR);
12436
+ const npm = path20.join(plugins, VENDORED_NPM_DIR);
12437
+ const packageParent = path20.dirname(vendoredNpmPluginRoot(projectDir, name, "0.0.0"));
11958
12438
  await mkdir11(projectDir, { recursive: true });
11959
12439
  await ensureDirectory(plugins);
11960
12440
  await ensureDirectory(npm);
@@ -11982,14 +12462,14 @@ function publicReceipt(record) {
11982
12462
  };
11983
12463
  }
11984
12464
  async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
11985
- const requestedProjectDir = path19.resolve(projectDirInput);
12465
+ const requestedProjectDir = path20.resolve(projectDirInput);
11986
12466
  await mkdir11(requestedProjectDir, { recursive: true });
11987
12467
  const projectDir = await realpath2(requestedProjectDir);
11988
12468
  const requested = parseNpmPluginSpec(packageSpec);
11989
12469
  const npmRoot = await prepareVendorParent(projectDir, requested.name);
11990
- const container = await mkdtemp2(path19.join(npmRoot, ".install-"));
11991
- const stageRoot = path19.join(container, "root");
11992
- const downloadsDir = path19.join(container, "downloads");
12470
+ const container = await mkdtemp2(path20.join(npmRoot, ".install-"));
12471
+ const stageRoot = path20.join(container, "root");
12472
+ const downloadsDir = path20.join(container, "downloads");
11993
12473
  await Promise.all([mkdir11(stageRoot), mkdir11(downloadsDir)]);
11994
12474
  const ctx = {
11995
12475
  fetch: options.fetch ?? globalThis.fetch,
@@ -12035,7 +12515,7 @@ async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
12035
12515
  skipped: ctx.skipped,
12036
12516
  tree: { algorithm: "sha256", ...tree }
12037
12517
  };
12038
- const receiptFile = await open2(path19.join(stageRoot, VENDOR_RECEIPT_FILE), "wx");
12518
+ const receiptFile = await open2(path20.join(stageRoot, VENDOR_RECEIPT_FILE), "wx");
12039
12519
  try {
12040
12520
  await receiptFile.writeFile(`${JSON.stringify(receipt, null, 2)}
12041
12521
  `, "utf8");
@@ -12044,7 +12524,7 @@ async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
12044
12524
  await receiptFile.close();
12045
12525
  }
12046
12526
  finalRoot = vendoredNpmPluginRoot(projectDir, rootRecord.name, rootRecord.version);
12047
- const realFinalParent = await realpath2(path19.dirname(finalRoot));
12527
+ const realFinalParent = await realpath2(path20.dirname(finalRoot));
12048
12528
  if (!isContained2(projectDir, realFinalParent)) {
12049
12529
  throw new Error("Plugin install destination resolves outside the project.");
12050
12530
  }
@@ -12076,16 +12556,16 @@ async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
12076
12556
  warnings: [...ctx.warnings]
12077
12557
  };
12078
12558
  } finally {
12079
- await rm5(container, { recursive: true, force: true }).catch(() => {});
12559
+ await rm4(container, { recursive: true, force: true }).catch(() => {});
12080
12560
  }
12081
12561
  }
12082
12562
  async function finalizeNpmPluginInstall(installed) {
12083
12563
  if (installed.backupRoot) {
12084
- await rm5(installed.backupRoot, { recursive: true, force: true });
12564
+ await rm4(installed.backupRoot, { recursive: true, force: true });
12085
12565
  }
12086
12566
  }
12087
12567
  async function rollbackNpmPluginInstall(installed) {
12088
- await rm5(installed.installRoot, { recursive: true, force: true });
12568
+ await rm4(installed.installRoot, { recursive: true, force: true });
12089
12569
  if (installed.backupRoot)
12090
12570
  await rename4(installed.backupRoot, installed.installRoot);
12091
12571
  }
@@ -12093,7 +12573,7 @@ async function rollbackNpmPluginInstall(installed) {
12093
12573
  // src/lib/plugin-manager.ts
12094
12574
  var pluginMutationQueues = new Map;
12095
12575
  function withPluginMutationLock(projectDir, mutation) {
12096
- const resolved = path20.resolve(projectDir);
12576
+ const resolved = path21.resolve(projectDir);
12097
12577
  const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
12098
12578
  const previous = pluginMutationQueues.get(key) ?? Promise.resolve();
12099
12579
  const run2 = previous.then(mutation, mutation);
@@ -12213,7 +12693,7 @@ async function setPluginEnabledUnlocked(projectDir, ref, enabled) {
12213
12693
  const { doc, file } = await loadManifestDoc(projectDir);
12214
12694
  const seq = doc.get("plugins", true);
12215
12695
  if (!isSeq4(seq)) {
12216
- throw new Error(`No plugins are configured in ${path20.basename(file)}.`);
12696
+ throw new Error(`No plugins are configured in ${path21.basename(file)}.`);
12217
12697
  }
12218
12698
  const idx = indexOfRef(seq, ref);
12219
12699
  if (idx < 0) {
@@ -12331,12 +12811,12 @@ async function addLocalPluginUnlocked(projectDir, sourcePath) {
12331
12811
  } catch {
12332
12812
  throw new Error(`Plugin source not found: ${sourcePath}`);
12333
12813
  }
12334
- const base = path20.basename(sourcePath);
12335
- const destDir = path20.join(projectDir, PLUGINS_DIR);
12814
+ const base = path21.basename(sourcePath);
12815
+ const destDir = path21.join(projectDir, PLUGINS_DIR);
12336
12816
  await mkdir12(destDir, { recursive: true });
12337
- const dest = path20.join(destDir, base);
12338
- const staging = path20.join(destDir, `.import-${randomUUID4()}`);
12339
- const backup = path20.join(destDir, `.backup-${randomUUID4()}`);
12817
+ const dest = path21.join(destDir, base);
12818
+ const staging = path21.join(destDir, `.import-${randomUUID4()}`);
12819
+ const backup = path21.join(destDir, `.backup-${randomUUID4()}`);
12340
12820
  let hasBackup = false;
12341
12821
  try {
12342
12822
  if (info2.isDirectory()) {
@@ -12363,16 +12843,16 @@ async function addLocalPluginUnlocked(projectDir, sourcePath) {
12363
12843
  try {
12364
12844
  await appendPlugin(projectDir, relPath, new Scalar2(relPath));
12365
12845
  } catch (error2) {
12366
- await rm6(dest, { recursive: true, force: true });
12846
+ await rm5(dest, { recursive: true, force: true });
12367
12847
  if (hasBackup)
12368
12848
  await rename5(backup, dest);
12369
12849
  throw error2;
12370
12850
  }
12371
12851
  if (hasBackup)
12372
- await rm6(backup, { recursive: true, force: true }).catch(() => {});
12852
+ await rm5(backup, { recursive: true, force: true }).catch(() => {});
12373
12853
  return { ref: relPath, kind: "local", enabled: true, path: relPath };
12374
12854
  } finally {
12375
- await rm6(staging, { recursive: true, force: true }).catch(() => {});
12855
+ await rm5(staging, { recursive: true, force: true }).catch(() => {});
12376
12856
  }
12377
12857
  }
12378
12858
  async function validateProjectPlugins(projectDir) {
@@ -12410,11 +12890,158 @@ async function validateProjectPlugins(projectDir) {
12410
12890
  }
12411
12891
  return out;
12412
12892
  }
12893
+
12894
+ // src/lib/snippets.ts
12895
+ var SNIPPETS_DIR = "snippets";
12896
+ var PLACEHOLDER_RE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
12897
+ function extractVariables(template) {
12898
+ const seen = new Set;
12899
+ const order = [];
12900
+ for (const m of template.matchAll(PLACEHOLDER_RE)) {
12901
+ const name = m[1];
12902
+ if (!seen.has(name)) {
12903
+ seen.add(name);
12904
+ order.push(name);
12905
+ }
12906
+ }
12907
+ return order;
12908
+ }
12909
+ function substituteVariables(template, values) {
12910
+ return template.replace(PLACEHOLDER_RE, (_full, name) => Object.prototype.hasOwnProperty.call(values, name) ? values[name] : "");
12911
+ }
12912
+ function resolveSafeChildFile(dir, fileName) {
12913
+ const full = path22.resolve(dir, fileName);
12914
+ if (full !== path22.join(dir, path22.basename(fileName)) || path22.dirname(full) !== dir) {
12915
+ throw new Error(`Unsafe snippet filename: ${fileName}`);
12916
+ }
12917
+ return full;
12918
+ }
12919
+ function resolveSnippetPath(projectDir, fileName) {
12920
+ return resolveSafeChildFile(path22.resolve(projectDir, SNIPPETS_DIR), fileName);
12921
+ }
12922
+ async function scanSnippetFiles(dir) {
12923
+ let names;
12924
+ try {
12925
+ names = await readdir9(dir);
12926
+ } catch {
12927
+ return [];
12928
+ }
12929
+ const entries = [];
12930
+ for (const fileName of names) {
12931
+ if (!fileName.toLowerCase().endsWith(".md"))
12932
+ continue;
12933
+ let body = "";
12934
+ try {
12935
+ body = await readFile32(path22.join(dir, fileName), "utf8");
12936
+ } catch {
12937
+ continue;
12938
+ }
12939
+ entries.push({
12940
+ name: prettify(fileName.replace(/\.md$/i, "")),
12941
+ fileName,
12942
+ variables: extractVariables(body)
12943
+ });
12944
+ }
12945
+ entries.sort((a, b) => a.name.localeCompare(b.name));
12946
+ return entries;
12947
+ }
12948
+ async function listSnippets(projectDir) {
12949
+ const files = await scanSnippetFiles(path22.join(projectDir, SNIPPETS_DIR));
12950
+ return files.map((file) => ({ ...file, source: { kind: "project" } }));
12951
+ }
12952
+ async function readSnippet(projectDir, fileName) {
12953
+ return readFile32(resolveSnippetPath(projectDir, fileName), "utf8");
12954
+ }
12955
+ async function saveSnippet(projectDir, name, body) {
12956
+ const stem = slugify(name);
12957
+ if (!stem)
12958
+ throw new Error(`Could not derive a filename from "${name}".`);
12959
+ const fileName = `${stem}.md`;
12960
+ const dir = path22.join(projectDir, SNIPPETS_DIR);
12961
+ await mkdir13(dir, { recursive: true });
12962
+ await writeFile11(resolveSnippetPath(projectDir, fileName), body, "utf8");
12963
+ return { name, fileName, variables: extractVariables(body), source: { kind: "project" } };
12964
+ }
12965
+ async function deleteSnippet(projectDir, fileName) {
12966
+ await rm6(resolveSnippetPath(projectDir, fileName), { force: true });
12967
+ }
12968
+ async function listInstalledExtensions(projectDir) {
12969
+ const out = [];
12970
+ const activeTheme = await getActiveTheme(projectDir);
12971
+ const themeSnippets = activeTheme?.snippets?.trim();
12972
+ if (activeTheme && themeSnippets) {
12973
+ out.push({
12974
+ ref: activeTheme.id,
12975
+ kind: "theme",
12976
+ dir: path22.join(projectDir, THEMES_DIR, activeTheme.id),
12977
+ name: activeTheme.name,
12978
+ snippetsRel: themeSnippets
12979
+ });
12980
+ }
12981
+ for (const entry of await listProjectPlugins(projectDir)) {
12982
+ if (entry.kind !== "local" || !entry.enabled)
12983
+ continue;
12984
+ const dir = path22.resolve(projectDir, entry.ref);
12985
+ let isExtensionDir = false;
12986
+ try {
12987
+ isExtensionDir = (await stat9(dir)).isDirectory();
12988
+ } catch {
12989
+ continue;
12990
+ }
12991
+ if (!isExtensionDir)
12992
+ continue;
12993
+ const meta = await readExtensionMeta(dir);
12994
+ const snippetsRel = meta.snippets?.trim();
12995
+ if (!snippetsRel)
12996
+ continue;
12997
+ out.push({
12998
+ ref: entry.ref,
12999
+ kind: "plugin",
13000
+ dir,
13001
+ name: meta.name?.trim() || prettify(path22.basename(dir)),
13002
+ snippetsRel
13003
+ });
13004
+ }
13005
+ return out;
13006
+ }
13007
+ function extensionSnippetsDir(ext) {
13008
+ if (pathEscapesFolder(ext.snippetsRel))
13009
+ return null;
13010
+ return path22.join(ext.dir, ext.snippetsRel);
13011
+ }
13012
+ async function listMergedSnippets(projectDir) {
13013
+ const project = await listSnippets(projectDir);
13014
+ const projectFileNames = new Set(project.map((entry) => entry.fileName.toLowerCase()));
13015
+ const merged = [...project];
13016
+ const extensions = (await listInstalledExtensions(projectDir)).sort((a, b) => a.name.localeCompare(b.name));
13017
+ for (const ext of extensions) {
13018
+ const dir = extensionSnippetsDir(ext);
13019
+ if (!dir)
13020
+ continue;
13021
+ for (const file of await scanSnippetFiles(dir)) {
13022
+ if (projectFileNames.has(file.fileName.toLowerCase()))
13023
+ continue;
13024
+ merged.push({ ...file, source: { kind: ext.kind, ref: ext.ref, name: ext.name } });
13025
+ }
13026
+ }
13027
+ return merged;
13028
+ }
13029
+ async function readExtensionSnippet(projectDir, source, fileName) {
13030
+ const ext = (await listInstalledExtensions(projectDir)).find((candidate) => candidate.kind === source.kind && candidate.ref === source.ref);
13031
+ if (!ext) {
13032
+ throw new Error(`Extension "${source.ref}" is not installed or is no longer active.`);
13033
+ }
13034
+ const dir = extensionSnippetsDir(ext);
13035
+ if (!dir) {
13036
+ throw new Error(`Extension "${source.ref}" does not declare a usable snippets folder.`);
13037
+ }
13038
+ return readFile32(resolveSafeChildFile(dir, fileName), "utf8");
13039
+ }
12413
13040
  // src/lib/theme-import.ts
12414
- import { existsSync as existsSync15 } from "node:fs";
12415
- import { mkdir as mkdir13, mkdtemp as mkdtemp3, readFile as readFile31, rm as rm7, writeFile as writeFile11 } from "node:fs/promises";
13041
+ import { existsSync as existsSync16 } from "node:fs";
13042
+ import { mkdir as mkdir14, mkdtemp as mkdtemp3, readFile as readFile33, rm as rm7, writeFile as writeFile12 } from "node:fs/promises";
12416
13043
  import { tmpdir as tmpdir3 } from "node:os";
12417
- import path21 from "node:path";
13044
+ import path23 from "node:path";
12418
13045
  import { unzipSync } from "fflate";
12419
13046
  var MAX_THEME_ARCHIVE_BYTES = 25 * 1024 * 1024;
12420
13047
  var MAX_THEME_UNZIPPED_BYTES = 25 * 1024 * 1024;
@@ -12447,7 +13074,7 @@ function classifyThemeCssFindings(findings) {
12447
13074
  const warnings = reject ? [] : findings.filter((f) => f.rule !== ruleSyntax);
12448
13075
  return { reject, warnings };
12449
13076
  }
12450
- var KNOWN_THEME_FILES = new Set(["theme.css", "theme.json"]);
13077
+ var KNOWN_THEME_FILES = new Set(["theme.css", "theme.json", EXTENSION_MANIFEST_FILENAME]);
12451
13078
  var ALLOWED_ASSET_EXTS = new Set([
12452
13079
  ".woff",
12453
13080
  ".woff2",
@@ -12463,8 +13090,12 @@ var ALLOWED_ASSET_EXTS = new Set([
12463
13090
  ".avif",
12464
13091
  ".css"
12465
13092
  ]);
12466
- function unexpectedThemeFiles(relPaths) {
13093
+ function unexpectedThemeFiles(relPaths, declaredExtras = []) {
13094
+ const extras = declaredExtras.map((p) => p.replace(/\\/g, "/"));
13095
+ const isDeclared = (p) => extras.some((extra) => p === extra || p.startsWith(`${extra}/`));
12467
13096
  return relPaths.map((p) => p.replace(/\\/g, "/")).filter((p) => p.length > 0 && !p.endsWith("/")).filter((p) => {
13097
+ if (isDeclared(p))
13098
+ return false;
12468
13099
  const base = p.split("/").pop().toLowerCase();
12469
13100
  if (KNOWN_THEME_FILES.has(base))
12470
13101
  return false;
@@ -12476,20 +13107,12 @@ function unexpectedThemeFiles(relPaths) {
12476
13107
  function mb2(n) {
12477
13108
  return `${Math.round(n / (1024 * 1024))}MB`;
12478
13109
  }
12479
- async function readThemeJson(p) {
12480
- try {
12481
- const parsed = JSON.parse(await readFile31(p, "utf8"));
12482
- return parsed && typeof parsed === "object" ? parsed : {};
12483
- } catch {
12484
- return {};
12485
- }
12486
- }
12487
13110
  async function finalizeThemeImport(projectDir, sourceDir, relPaths) {
12488
- const cssPath = path21.join(sourceDir, "theme.css");
12489
- if (!existsSync15(cssPath)) {
13111
+ const cssPath = path23.join(sourceDir, "theme.css");
13112
+ if (!existsSync16(cssPath)) {
12490
13113
  throw new Error("A theme must contain a theme.css file.");
12491
13114
  }
12492
- const css = await readFile31(cssPath, "utf8");
13115
+ const css = await readFile33(cssPath, "utf8");
12493
13116
  const { reject, warnings: printFindings } = classifyThemeCssFindings(checkCss(css, "theme.css"));
12494
13117
  if (reject) {
12495
13118
  throw new Error(`theme.css could not be parsed — ${reject.message}`);
@@ -12498,29 +13121,29 @@ async function finalizeThemeImport(projectDir, sourceDir, relPaths) {
12498
13121
  for (const w of printFindings) {
12499
13122
  warnings.push({ code: "print-safety", message: w.message });
12500
13123
  }
12501
- const jsonPath = path21.join(sourceDir, "theme.json");
12502
- const jsonExists = existsSync15(jsonPath);
12503
- const meta = jsonExists ? await readThemeJson(jsonPath) : {};
12504
- if (!jsonExists) {
13124
+ const metaExists = existsSync16(path23.join(sourceDir, EXTENSION_MANIFEST_FILENAME)) || existsSync16(path23.join(sourceDir, "theme.json"));
13125
+ const meta = metaExists ? await readExtensionMeta(sourceDir) : {};
13126
+ if (!metaExists) {
12505
13127
  warnings.push({
12506
13128
  code: "no-theme-json",
12507
- message: "No theme.json found — the theme's name was taken from the file/folder name."
13129
+ message: "No gutterpress.json or theme.json found — the theme's name was taken from the file/folder name."
12508
13130
  });
12509
13131
  } else if (!meta.name || !meta.name.trim()) {
12510
13132
  warnings.push({
12511
13133
  code: "unnamed-theme",
12512
- message: `theme.json has no "name" — the theme's name was taken from the file/folder name.`
13134
+ message: `The metadata file has no "name" — the theme's name was taken from the file/folder name.`
12513
13135
  });
12514
13136
  }
12515
13137
  assertThemeSheetsContained(meta);
13138
+ const metaFilename = existsSync16(path23.join(sourceDir, EXTENSION_MANIFEST_FILENAME)) ? EXTENSION_MANIFEST_FILENAME : "theme.json";
12516
13139
  const extraSheets = [
12517
13140
  ...new Set([...themeStyleList(meta), ...themeEngineStyleList(meta)])
12518
13141
  ].filter((rel) => rel !== "theme.css");
12519
- const extraSheetPaths = resolveDeclaredStyles(extraSheets, sourceDir, "theme.json") ?? [];
13142
+ const extraSheetPaths = resolveDeclaredStyles(extraSheets, sourceDir, metaFilename) ?? [];
12520
13143
  for (let i = 0;i < extraSheets.length; i++) {
12521
13144
  const rel = extraSheets[i];
12522
13145
  const sheetPath = extraSheetPaths[i];
12523
- const sheetCss = await readFile31(sheetPath, "utf8");
13146
+ const sheetCss = await readFile33(sheetPath, "utf8");
12524
13147
  const { reject: sheetReject, warnings: sheetFindings } = classifyThemeCssFindings(checkCss(sheetCss, rel));
12525
13148
  if (sheetReject) {
12526
13149
  throw new Error(`${rel} could not be parsed — ${sheetReject.message}`);
@@ -12529,7 +13152,8 @@ async function finalizeThemeImport(projectDir, sourceDir, relPaths) {
12529
13152
  warnings.push({ code: "print-safety", message: `${rel}: ${w.message}` });
12530
13153
  }
12531
13154
  }
12532
- const extra = unexpectedThemeFiles(relPaths);
13155
+ const declaredExtras = [meta.markdown, meta.components, meta.snippets].filter((p) => typeof p === "string" && p.trim().length > 0);
13156
+ const extra = unexpectedThemeFiles(relPaths, declaredExtras);
12533
13157
  if (extra.length > 0) {
12534
13158
  const shown = extra.slice(0, 6).join(", ");
12535
13159
  warnings.push({
@@ -12581,7 +13205,7 @@ async function importThemeFromZip(projectDir, archive) {
12581
13205
  throw new Error("No theme.css found in the package. A theme needs a theme.css at the package root or inside a single folder.");
12582
13206
  }
12583
13207
  const prefix = root === "" ? "" : `${root}/`;
12584
- const tmp = await mkdtemp3(path21.join(tmpdir3(), "gutterpress-theme-"));
13208
+ const tmp = await mkdtemp3(path23.join(tmpdir3(), "gutterpress-theme-"));
12585
13209
  try {
12586
13210
  const relPaths = [];
12587
13211
  for (const [name, data] of Object.entries(files)) {
@@ -12593,13 +13217,13 @@ async function importThemeFromZip(projectDir, archive) {
12593
13217
  const rel = prefix ? norm.slice(prefix.length) : norm;
12594
13218
  if (!rel || rel.split("/").some((seg) => seg === ".."))
12595
13219
  continue;
12596
- const dest = path21.join(tmp, rel);
12597
- if (dest !== tmp && !dest.startsWith(tmp + path21.sep)) {
13220
+ const dest = path23.join(tmp, rel);
13221
+ if (dest !== tmp && !dest.startsWith(tmp + path23.sep)) {
12598
13222
  throw new Error(`The package contains an unsafe path and was rejected: ${name}`);
12599
13223
  }
12600
13224
  relPaths.push(rel);
12601
- await mkdir13(path21.dirname(dest), { recursive: true });
12602
- await writeFile11(dest, data);
13225
+ await mkdir14(path23.dirname(dest), { recursive: true });
13226
+ await writeFile12(dest, data);
12603
13227
  }
12604
13228
  return await finalizeThemeImport(projectDir, tmp, relPaths);
12605
13229
  } finally {
@@ -12612,10 +13236,10 @@ async function importThemeFromCssText(projectDir, css, name) {
12612
13236
  throw new Error(`The CSS could not be parsed — ${reject.message}`);
12613
13237
  }
12614
13238
  const displayName = name.trim() || "Imported theme";
12615
- const tmp = await mkdtemp3(path21.join(tmpdir3(), "gutterpress-theme-"));
13239
+ const tmp = await mkdtemp3(path23.join(tmpdir3(), "gutterpress-theme-"));
12616
13240
  try {
12617
- await writeFile11(path21.join(tmp, "theme.css"), css, "utf8");
12618
- await writeFile11(path21.join(tmp, "theme.json"), JSON.stringify({ name: displayName }, null, 2), "utf8");
13241
+ await writeFile12(path23.join(tmp, "theme.css"), css, "utf8");
13242
+ await writeFile12(path23.join(tmp, "theme.json"), JSON.stringify({ name: displayName }, null, 2), "utf8");
12619
13243
  const result = await finalizeThemeImport(projectDir, tmp, ["theme.css", "theme.json"]);
12620
13244
  result.warnings.unshift({
12621
13245
  code: "no-theme-json",
@@ -12627,14 +13251,14 @@ async function importThemeFromCssText(projectDir, css, name) {
12627
13251
  }
12628
13252
  }
12629
13253
  async function importThemeFromFile(projectDir, filePath) {
12630
- const ext = path21.extname(filePath).toLowerCase();
13254
+ const ext = path23.extname(filePath).toLowerCase();
12631
13255
  if (ext === ".zip") {
12632
- const buf = await readFile31(filePath);
13256
+ const buf = await readFile33(filePath);
12633
13257
  return importThemeFromZip(projectDir, new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
12634
13258
  }
12635
13259
  if (ext === ".css") {
12636
- const css = await readFile31(filePath, "utf8");
12637
- return importThemeFromCssText(projectDir, css, prettify(path21.basename(filePath, ".css")));
13260
+ const css = await readFile33(filePath, "utf8");
13261
+ return importThemeFromCssText(projectDir, css, prettify(path23.basename(filePath, ".css")));
12638
13262
  }
12639
13263
  throw new Error("Choose a .zip theme package or a .css stylesheet.");
12640
13264
  }
@@ -13337,8 +13961,8 @@ async function listGitHubBranches(credential, owner, repo, options = {}) {
13337
13961
  return branches;
13338
13962
  }
13339
13963
  // src/lib/remote-auth/clone.ts
13340
- import { mkdir as mkdir14, readdir as readdir10, rm as rm8 } from "node:fs/promises";
13341
- import path22 from "node:path";
13964
+ import { mkdir as mkdir15, readdir as readdir10, rm as rm8 } from "node:fs/promises";
13965
+ import path24 from "node:path";
13342
13966
  import git3 from "isomorphic-git";
13343
13967
 
13344
13968
  // src/lib/remote-auth/git-http.ts
@@ -13669,7 +14293,7 @@ function friendlyCloneError(e) {
13669
14293
  }
13670
14294
  async function cloneRepository(options) {
13671
14295
  const { dir, onProgress, tokenStore } = options;
13672
- if (!path22.isAbsolute(dir)) {
14296
+ if (!path24.isAbsolute(dir)) {
13673
14297
  throw new Error("cloneRepository requires an absolute destination path.");
13674
14298
  }
13675
14299
  let parsed;
@@ -13689,7 +14313,7 @@ async function cloneRepository(options) {
13689
14313
  return withRepoLock(dir, async () => {
13690
14314
  await assertCloneTarget(dir);
13691
14315
  const dirExistedBefore = gitFs.existsSync(dir);
13692
- await mkdir14(dir, { recursive: true });
14316
+ await mkdir15(dir, { recursive: true });
13693
14317
  try {
13694
14318
  await git3.clone({
13695
14319
  fs: gitFs,
@@ -13981,8 +14605,8 @@ async function diagnoseProjectRemote(projectDir, options = {}) {
13981
14605
  import git6 from "isomorphic-git";
13982
14606
 
13983
14607
  // src/lib/remote-auth/converge-merge.ts
13984
- import { mkdir as mkdir15, unlink as unlink3, writeFile as writeFile12 } from "node:fs/promises";
13985
- import path23 from "node:path";
14608
+ import { mkdir as mkdir16, unlink as unlink3, writeFile as writeFile13 } from "node:fs/promises";
14609
+ import path25 from "node:path";
13986
14610
  import git5 from "isomorphic-git";
13987
14611
  import diff3Merge from "diff3";
13988
14612
  function isMergeConflictError(e) {
@@ -14029,9 +14653,9 @@ async function readBlobOrNull(dir, cache2, oid, filepath) {
14029
14653
  }
14030
14654
  }
14031
14655
  async function writeTreeFile(dir, filepath, content) {
14032
- const abs = path23.join(dir, filepath);
14033
- await mkdir15(path23.dirname(abs), { recursive: true });
14034
- await writeFile12(abs, content);
14656
+ const abs = path25.join(dir, filepath);
14657
+ await mkdir16(path25.dirname(abs), { recursive: true });
14658
+ await writeFile13(abs, content);
14035
14659
  }
14036
14660
  async function bothAddedPaths(dir, cache2, ourTip, theirTip, allowUnrelated) {
14037
14661
  const bases = await git5.findMergeBase({ fs: gitFs, dir, cache: cache2, oids: [ourTip, theirTip] });
@@ -14127,7 +14751,7 @@ async function convergeMerge(params) {
14127
14751
  }
14128
14752
  for (const p of paths.deleteByTheirs) {
14129
14753
  const ourBlob = await readBlobOrNull(dir, cache2, ourTip, p);
14130
- await unlink3(path23.join(dir, p)).catch((e) => {
14754
+ await unlink3(path25.join(dir, p)).catch((e) => {
14131
14755
  if (e?.code !== "ENOENT")
14132
14756
  throw e;
14133
14757
  });
@@ -14368,8 +14992,8 @@ async function syncProject(options) {
14368
14992
  });
14369
14993
  }
14370
14994
  // src/lib/publish/run-publish.ts
14371
- import { readdir as readdir12, stat as stat11 } from "node:fs/promises";
14372
- import path27 from "node:path";
14995
+ import { readdir as readdir12, stat as stat12 } from "node:fs/promises";
14996
+ import path29 from "node:path";
14373
14997
 
14374
14998
  // src/lib/publish/command-runner.ts
14375
14999
  var CAPTURE_LIMIT = 64 * 1024;
@@ -14518,25 +15142,25 @@ ${tail}` : ""}`);
14518
15142
  };
14519
15143
 
14520
15144
  // src/lib/publish/providers/guided.ts
14521
- import { copyFile as copyFile2, mkdir as mkdir16, writeFile as writeFile13 } from "node:fs/promises";
14522
- import path24 from "node:path";
15145
+ import { copyFile as copyFile2, mkdir as mkdir17, writeFile as writeFile14 } from "node:fs/promises";
15146
+ import path26 from "node:path";
14523
15147
  async function stageGuidedPackage(req, providerId, listingLines) {
14524
- const packageDir = path24.join(path24.dirname(req.artifact.path), "publish", providerId);
14525
- await mkdir16(packageDir, { recursive: true });
14526
- await copyFile2(req.artifact.path, path24.join(packageDir, path24.basename(req.artifact.path)));
15148
+ const packageDir = path26.join(path26.dirname(req.artifact.path), "publish", providerId);
15149
+ await mkdir17(packageDir, { recursive: true });
15150
+ await copyFile2(req.artifact.path, path26.join(packageDir, path26.basename(req.artifact.path)));
14527
15151
  const { title, authors } = req.project;
14528
15152
  const sheet = [
14529
15153
  `# ${title || "Untitled"}`,
14530
15154
  "",
14531
15155
  `- **Authors:** ${authors.length ? authors.join(", ") : "(not set)"}`,
14532
- `- **File:** ${path24.basename(req.artifact.path)}`,
15156
+ `- **File:** ${path26.basename(req.artifact.path)}`,
14533
15157
  ...listingLines,
14534
15158
  "",
14535
15159
  "Copy these details into the platform's listing form when uploading.",
14536
15160
  ""
14537
15161
  ].join(`
14538
15162
  `);
14539
- await writeFile13(path24.join(packageDir, "LISTING.md"), sheet, "utf8");
15163
+ await writeFile14(path26.join(packageDir, "LISTING.md"), sheet, "utf8");
14540
15164
  return packageDir;
14541
15165
  }
14542
15166
 
@@ -14599,9 +15223,9 @@ var drivethrurpgProvider = {
14599
15223
  };
14600
15224
 
14601
15225
  // src/lib/publish/providers/gdrive.ts
14602
- import { mkdtemp as mkdtemp4, readFile as readFile32, readdir as readdir11, rm as rm9, stat as stat9, writeFile as writeFile14 } from "node:fs/promises";
15226
+ import { mkdtemp as mkdtemp4, readFile as readFile34, readdir as readdir11, rm as rm9, stat as stat10, writeFile as writeFile15 } from "node:fs/promises";
14603
15227
  import { tmpdir as tmpdir4 } from "node:os";
14604
- import path25 from "node:path";
15228
+ import path27 from "node:path";
14605
15229
  import { Zip, ZipDeflate } from "fflate";
14606
15230
 
14607
15231
  // src/lib/publish/google-drive.ts
@@ -14986,34 +15610,34 @@ function zipEntriesNonBlocking(entries, opts) {
14986
15610
  async function collectZipEntries(root, dir, out, onWarn) {
14987
15611
  const entries = await readdir11(dir, { withFileTypes: true });
14988
15612
  for (const entry of entries) {
14989
- const full = path25.join(dir, entry.name);
15613
+ const full = path27.join(dir, entry.name);
14990
15614
  if (entry.isDirectory()) {
14991
15615
  await collectZipEntries(root, full, out, onWarn);
14992
15616
  } else if (entry.isFile()) {
14993
- const rel = path25.relative(root, full).split(path25.sep).join("/");
14994
- out[rel] = await readFile32(full);
15617
+ const rel = path27.relative(root, full).split(path27.sep).join("/");
15618
+ out[rel] = await readFile34(full);
14995
15619
  } else if (entry.isSymbolicLink()) {
14996
- const resolved = await stat9(full).catch(() => null);
15620
+ const resolved = await stat10(full).catch(() => null);
14997
15621
  if (resolved?.isFile()) {
14998
- const rel = path25.relative(root, full).split(path25.sep).join("/");
14999
- out[rel] = await readFile32(full);
15622
+ const rel = path27.relative(root, full).split(path27.sep).join("/");
15623
+ out[rel] = await readFile34(full);
15000
15624
  } else if (resolved?.isDirectory()) {
15001
15625
  await collectZipEntries(root, full, out, onWarn);
15002
15626
  } else {
15003
- onWarn?.(`Skipped "${path25.relative(root, full)}" in the website export — it's a broken symlink.`);
15627
+ onWarn?.(`Skipped "${path27.relative(root, full)}" in the website export — it's a broken symlink.`);
15004
15628
  }
15005
15629
  } else {
15006
- onWarn?.(`Skipped "${path25.relative(root, full)}" in the website export — not a file, folder, or symlink.`);
15630
+ onWarn?.(`Skipped "${path27.relative(root, full)}" in the website export — not a file, folder, or symlink.`);
15007
15631
  }
15008
15632
  }
15009
15633
  }
15010
- async function zipHtmlExport(exportDir, title, onWarn, writeArchiveImpl = writeFile14) {
15634
+ async function zipHtmlExport(exportDir, title, onWarn, writeArchiveImpl = writeFile15) {
15011
15635
  const entries = {};
15012
15636
  await collectZipEntries(exportDir, exportDir, entries, onWarn);
15013
15637
  const fileName = `${bookSlug(title)}-website.zip`;
15014
- const tmpRoot = await mkdtemp4(path25.join(tmpdir4(), "gutterpress-gdrive-"));
15638
+ const tmpRoot = await mkdtemp4(path27.join(tmpdir4(), "gutterpress-gdrive-"));
15015
15639
  try {
15016
- const filePath = path25.join(tmpRoot, fileName);
15640
+ const filePath = path27.join(tmpRoot, fileName);
15017
15641
  const zipped = await zipEntriesNonBlocking(entries, { level: 6 });
15018
15642
  await writeArchiveImpl(filePath, zipped);
15019
15643
  return {
@@ -15061,12 +15685,12 @@ var gdriveProvider = {
15061
15685
  const isHtml = req.artifact.format === "html";
15062
15686
  const source = isHtml ? await zipHtmlExport(req.artifact.path, req.project.title, (msg) => req.deps.onProgress?.(msg)) : {
15063
15687
  filePath: req.artifact.path,
15064
- fileName: path25.basename(req.artifact.path),
15688
+ fileName: path27.basename(req.artifact.path),
15065
15689
  mimeType: "application/pdf",
15066
15690
  cleanup: async () => {}
15067
15691
  };
15068
15692
  try {
15069
- const artifactStat = await stat9(source.filePath);
15693
+ const artifactStat = await stat10(source.filePath);
15070
15694
  const about = await driveAbout(fetchImpl, accessToken);
15071
15695
  if (about.quota.limitBytes != null && about.quota.freeBytes != null) {
15072
15696
  if (artifactStat.size > about.quota.freeBytes) {
@@ -15111,8 +15735,8 @@ var gdriveProvider = {
15111
15735
  };
15112
15736
 
15113
15737
  // src/lib/publish/butler.ts
15114
- import { chmod as chmod2, mkdir as mkdir17, stat as stat10, writeFile as writeFile15 } from "node:fs/promises";
15115
- import path26 from "node:path";
15738
+ import { chmod as chmod2, mkdir as mkdir18, stat as stat11, writeFile as writeFile16 } from "node:fs/promises";
15739
+ import path28 from "node:path";
15116
15740
  import { unzipSync as unzipSync2 } from "fflate";
15117
15741
  var BUTLER_DOWNLOAD_TIMEOUT_MS = 300000;
15118
15742
  function butlerBrothChannel(platform2 = process.platform, arch2 = process.arch) {
@@ -15132,7 +15756,7 @@ function butlerBinaryName(platform2 = process.platform) {
15132
15756
  }
15133
15757
  async function fileExists2(p) {
15134
15758
  try {
15135
- return (await stat10(p)).isFile();
15759
+ return (await stat11(p)).isFile();
15136
15760
  } catch {
15137
15761
  return false;
15138
15762
  }
@@ -15149,8 +15773,8 @@ async function ensureButler(deps) {
15149
15773
  }
15150
15774
  if (await commandExists("butler", runCommand, deps.env))
15151
15775
  return "butler";
15152
- const cacheDir = path26.join(deps.configDir ?? defaultConfigDir(), "tools", "butler");
15153
- const cached = path26.join(cacheDir, butlerBinaryName());
15776
+ const cacheDir = path28.join(deps.configDir ?? defaultConfigDir(), "tools", "butler");
15777
+ const cached = path28.join(cacheDir, butlerBinaryName());
15154
15778
  if (await fileExists2(cached))
15155
15779
  return cached;
15156
15780
  const channel = butlerBrothChannel();
@@ -15172,14 +15796,14 @@ async function ensureButler(deps) {
15172
15796
  return new Uint8Array(await response.arrayBuffer());
15173
15797
  });
15174
15798
  const files = unzipSync2(archive);
15175
- await mkdir17(cacheDir, { recursive: true });
15799
+ await mkdir18(cacheDir, { recursive: true });
15176
15800
  let foundBinary = false;
15177
15801
  for (const [name, data] of Object.entries(files)) {
15178
15802
  if (name.endsWith("/"))
15179
15803
  continue;
15180
- const target = path26.join(cacheDir, path26.basename(name));
15181
- await writeFile15(target, data);
15182
- if (path26.basename(name) === butlerBinaryName()) {
15804
+ const target = path28.join(cacheDir, path28.basename(name));
15805
+ await writeFile16(target, data);
15806
+ if (path28.basename(name) === butlerBinaryName()) {
15183
15807
  await chmod2(target, 493).catch(() => {});
15184
15808
  foundBinary = true;
15185
15809
  }
@@ -15628,9 +16252,9 @@ async function resolvePublishRequest(options, deps) {
15628
16252
  const effectiveDeps = effectiveAccount ? { ...deps, credentialAccount: effectiveAccount } : deps;
15629
16253
  const outDir = resolveOutputDir(manifestDir, config.title);
15630
16254
  const effectiveFormat = resolvePublishFormat(provider.info, providerConfig);
15631
- const defaultArtifact = effectiveFormat === "pdf" ? path27.join(outDir, artifactName(config.title, "pdf")) : outDir;
16255
+ const defaultArtifact = effectiveFormat === "pdf" ? path29.join(outDir, artifactName(config.title, "pdf")) : outDir;
15632
16256
  const artifact = {
15633
- path: options.artifactPath ? path27.resolve(options.projectDir, options.artifactPath) : defaultArtifact,
16257
+ path: options.artifactPath ? path29.resolve(options.projectDir, options.artifactPath) : defaultArtifact,
15634
16258
  format: effectiveFormat
15635
16259
  };
15636
16260
  return {
@@ -15648,7 +16272,7 @@ var PDF_HINT = "Build the PDF first (gutterpress build, or export it from the ap
15648
16272
  var HTML_HINT = "Build the website export first (gutterpress build --format html).";
15649
16273
  async function artifactIssues(artifact) {
15650
16274
  try {
15651
- const s = await stat11(artifact.path);
16275
+ const s = await stat12(artifact.path);
15652
16276
  if (artifact.format === "pdf" && !s.isFile()) {
15653
16277
  return [
15654
16278
  {
@@ -15694,7 +16318,7 @@ async function artifactIssues(artifact) {
15694
16318
  var BUILD_FINGERPRINT_FILENAME = "build-fingerprint.json";
15695
16319
  async function htmlDirIssues(dir) {
15696
16320
  const issues = [];
15697
- const isThere = async (rel) => stat11(path27.join(dir, rel)).then(() => true, () => false);
16321
+ const isThere = async (rel) => stat12(path29.join(dir, rel)).then(() => true, () => false);
15698
16322
  if (!await isThere(BOOK_HTML)) {
15699
16323
  issues.push({
15700
16324
  severity: "error",
@@ -15813,17 +16437,17 @@ async function disconnectPublishCredential(key, deps, options = {}) {
15813
16437
  }
15814
16438
  }
15815
16439
  // src/lib/publish/selections.ts
15816
- import { chmod as chmod3, mkdir as mkdir18, readFile as readFile33, writeFile as writeFile16 } from "node:fs/promises";
15817
- import path28 from "node:path";
16440
+ import { chmod as chmod3, mkdir as mkdir19, readFile as readFile35, writeFile as writeFile17 } from "node:fs/promises";
16441
+ import path30 from "node:path";
15818
16442
  class PublishSelectionsStore {
15819
16443
  filePath;
15820
16444
  queue = Promise.resolve();
15821
16445
  constructor(filePath) {
15822
- this.filePath = filePath ?? path28.join(defaultConfigDir(), "publish-selections.json");
16446
+ this.filePath = filePath ?? path30.join(defaultConfigDir(), "publish-selections.json");
15823
16447
  }
15824
16448
  async read() {
15825
16449
  try {
15826
- const raw = await readFile33(this.filePath, "utf8");
16450
+ const raw = await readFile35(this.filePath, "utf8");
15827
16451
  const parsed = JSON.parse(raw);
15828
16452
  if (parsed && typeof parsed === "object") {
15829
16453
  return {
@@ -15836,8 +16460,8 @@ class PublishSelectionsStore {
15836
16460
  return { version: 1, global: {}, projects: {} };
15837
16461
  }
15838
16462
  async write(data) {
15839
- await mkdir18(path28.dirname(this.filePath), { recursive: true });
15840
- await writeFile16(this.filePath, JSON.stringify(data, null, 2), {
16463
+ await mkdir19(path30.dirname(this.filePath), { recursive: true });
16464
+ await writeFile17(this.filePath, JSON.stringify(data, null, 2), {
15841
16465
  encoding: "utf8",
15842
16466
  mode: 384
15843
16467
  });
@@ -15939,4 +16563,4 @@ async function setPublishProviderConfig(projectDir, providerId, values) {
15939
16563
  await writeManifestDoc(file, doc);
15940
16564
  return settingsFromDoc(doc);
15941
16565
  }
15942
- export { PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, hasProjectManifest, loadManifest, loadManifestWithPath, resolveConfig, SYNC_SNAPSHOT_MESSAGE, log, resolveActiveStyles, listProjectStyles, PLUGINS_DIR, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, checkCss, runLint, formatReport, getChecks, getCheckById, resolveCheckSelectors, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, defaultConfigDir, FileTokenStore, extractUrlCredential, getSystemDiagnostics, THEMES_DIR, BUILT_IN_THEME_IDS, listBuiltInThemes, resolveBuiltInTheme, listProjectThemes, getActiveTheme, getPreviousTheme, revertTheme, applyTheme, importThemeFromFolder, importThemeFromUrl, readThemeCss, removeProjectTheme, detectLegacyForkedTheme, 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, importThemeFromFile, setManifestFields, readManifestFields, setActiveStyles, AUTO_SNAPSHOT_MIN_MINUTES, AUTO_SNAPSHOT_MAX_MINUTES, AUTO_SNAPSHOT_DEFAULT_MINUTES, AUTO_SYNC_MIN_MINUTES, AUTO_SYNC_MAX_MINUTES, AUTO_SYNC_DEFAULT_MINUTES, AUTO_SYNC_PUSH_INTERVAL_MINUTES, autoSnapshotDelayMs, autoSyncDelayMs, isGitInternalPath, resolveGitHubClientId, GITHUB_HOST, GitHubAuthProvider, GDRIVE_HOST, resolveGoogleClientId, resolveGoogleClientSecret, GOOGLE_NOT_CONFIGURED_MESSAGE, requireGoogleClientCredentials, pkceChallengeFromVerifier, GoogleAuthProvider, revokeGoogleCredential, publishCredentialKey, publishConnectionStatus, listPublishAccounts, connectGoogleDrive, listGitHubRepositories, listRepoBooks, listGitHubBranches, sanitizeCloneFolderName, cloneRepository, isSshRemoteUrl, testRemoteAccess, knownForgeTokenUrl, connectGenericHost, parseRemoteOrigin, forgeKindForHost, diagnoseProjectRemote, syncProject, listPublishProviders, publishProviderFor, resolvePublishFormat, resolvePublishRequest, runPublish, connectPublishProvider, disconnectPublishCredential, PublishSelectionsStore, readPublishSettings, setPublishProviderConfig };
16566
+ export { PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, hasProjectManifest, loadManifest, loadManifestWithPath, resolveConfig, SYNC_SNAPSHOT_MESSAGE, log, resolveActiveStyles, listProjectStyles, PLUGINS_DIR, EXTENSION_MANIFEST_FILENAME, LEGACY_THEME_MANIFEST_FILENAME, readExtensionMeta, extensionStyleList, extensionEngineStyleList, pathEscapesFolder, assertExtensionContained, resolveExtension, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, checkCss, runLint, formatReport, getChecks, getCheckById, resolveCheckSelectors, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, defaultConfigDir, FileTokenStore, extractUrlCredential, getSystemDiagnostics, THEMES_DIR, BUILT_IN_THEME_IDS, listBuiltInThemes, resolveBuiltInTheme, listProjectThemes, getActiveTheme, getPreviousTheme, revertTheme, applyTheme, importThemeFromFolder, importThemeFromUrl, readThemeCss, removeProjectTheme, detectLegacyForkedTheme, slugifyProjectName, escapeYamlScalar, scaffoldProject, adoptFolder, EXTENSION_KINDS, RESERVED_PREFIX, resolveExtensionPrefix, scaffoldExtension, BUILT_IN_TEMPLATE_IDS, listBuiltInTemplates, saveProjectAsTemplate, listCustomTemplates, importTemplateFromFolder, RECOMMENDED_PLUGINS, listProjectPlugins, setPluginEnabled, addNpmPlugin, addLocalPlugin, validateProjectPlugins, SNIPPETS_DIR, extractVariables, substituteVariables, listSnippets, readSnippet, saveSnippet, deleteSnippet, listMergedSnippets, readExtensionSnippet, importThemeFromFile, setManifestFields, readManifestFields, setActiveStyles, AUTO_SNAPSHOT_MIN_MINUTES, AUTO_SNAPSHOT_MAX_MINUTES, AUTO_SNAPSHOT_DEFAULT_MINUTES, AUTO_SYNC_MIN_MINUTES, AUTO_SYNC_MAX_MINUTES, AUTO_SYNC_DEFAULT_MINUTES, AUTO_SYNC_PUSH_INTERVAL_MINUTES, autoSnapshotDelayMs, autoSyncDelayMs, isGitInternalPath, resolveGitHubClientId, GITHUB_HOST, GitHubAuthProvider, GDRIVE_HOST, resolveGoogleClientId, resolveGoogleClientSecret, GOOGLE_NOT_CONFIGURED_MESSAGE, requireGoogleClientCredentials, pkceChallengeFromVerifier, GoogleAuthProvider, revokeGoogleCredential, publishCredentialKey, publishConnectionStatus, listPublishAccounts, connectGoogleDrive, listGitHubRepositories, listRepoBooks, listGitHubBranches, sanitizeCloneFolderName, cloneRepository, isSshRemoteUrl, testRemoteAccess, knownForgeTokenUrl, connectGenericHost, parseRemoteOrigin, forgeKindForHost, diagnoseProjectRemote, syncProject, listPublishProviders, publishProviderFor, resolvePublishFormat, resolvePublishRequest, runPublish, connectPublishProvider, disconnectPublishCredential, PublishSelectionsStore, readPublishSettings, setPublishProviderConfig };