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
@@ -13,21 +13,22 @@ import {
13
13
  stampCreator,
14
14
  stripAnnotations,
15
15
  warn
16
- } from "./cli-pvk2s9d7.js";
16
+ } from "./cli-vxf151ea.js";
17
17
  import {
18
18
  DEBOUNCE,
19
19
  UsageError,
20
20
  resolvePort
21
- } from "./cli-h8ejbtyr.js";
21
+ } from "./cli-vcv5ewqj.js";
22
22
  import {
23
23
  MARGIN_BOX_IGNORED_PROPERTIES,
24
24
  RENDER_TIMEOUT_MS,
25
25
  REQUIRED_MILESTONE,
26
26
  closeBrowser,
27
27
  getAssetPath,
28
+ getAssetsDir,
28
29
  getBrowser,
29
30
  prewarmBrowser
30
- } from "./cli-s8q42x3r.js";
31
+ } from "./cli-4vtgt2ng.js";
31
32
  import {
32
33
  INSTALL_HINTS,
33
34
  execCapture,
@@ -1078,7 +1079,7 @@ async function verifyVendoredPlugin(projectDir, expectedName, expectedVersion) {
1078
1079
 
1079
1080
  // src/lib/lint-runner.ts
1080
1081
  import { resolve as resolve4 } from "node:path";
1081
- import { readFile as readFile4 } from "node:fs/promises";
1082
+ import { readFile as readFile5 } from "node:fs/promises";
1082
1083
 
1083
1084
  // src/lib/printsafe.ts
1084
1085
  import postcss from "postcss";
@@ -1379,8 +1380,8 @@ async function resolveActiveStyles(projectDir, manifestStyles) {
1379
1380
 
1380
1381
  // src/lib/markdown/plugins.ts
1381
1382
  import { createHash as createHash2 } from "node:crypto";
1382
- import { existsSync as existsSync4, lstatSync, realpathSync, rmSync, statSync as statSync2, unlinkSync } from "node:fs";
1383
- import { cp, link, mkdir, mkdtemp, readFile as readFile3, rm, unlink, writeFile } from "node:fs/promises";
1383
+ import { existsSync as existsSync5, lstatSync, realpathSync, rmSync, statSync as statSync2, unlinkSync } from "node:fs";
1384
+ import { cp, link, mkdir, mkdtemp, readFile as readFile4, rm, unlink, writeFile } from "node:fs/promises";
1384
1385
  import { tmpdir } from "node:os";
1385
1386
  import { resolve as resolve3, join, dirname as dirname2, basename, extname, relative, isAbsolute } from "node:path";
1386
1387
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -1404,6 +1405,70 @@ function resolveDeclaredStyles(rawStyles, baseDir, subject) {
1404
1405
  });
1405
1406
  }
1406
1407
 
1408
+ // src/lib/extension-manifest.ts
1409
+ import { existsSync as existsSync4 } from "node:fs";
1410
+ import { readFile as readFile3 } from "node:fs/promises";
1411
+ import path3 from "node:path";
1412
+ var EXTENSION_MANIFEST_FILENAME = "gutterpress.json";
1413
+ var LEGACY_THEME_MANIFEST_FILENAME = "theme.json";
1414
+ async function readJsonMetaFile(jsonPath) {
1415
+ try {
1416
+ const parsed = JSON.parse(await readFile3(jsonPath, "utf8"));
1417
+ return parsed && typeof parsed === "object" ? parsed : {};
1418
+ } catch {
1419
+ return {};
1420
+ }
1421
+ }
1422
+ async function readExtensionMeta(dir) {
1423
+ const extensionPath = path3.join(dir, EXTENSION_MANIFEST_FILENAME);
1424
+ if (existsSync4(extensionPath)) {
1425
+ return readJsonMetaFile(extensionPath);
1426
+ }
1427
+ return readJsonMetaFile(path3.join(dir, LEGACY_THEME_MANIFEST_FILENAME));
1428
+ }
1429
+ function declaredList(value) {
1430
+ return Array.isArray(value) ? value.filter((s) => typeof s === "string" && s.trim().length > 0) : [];
1431
+ }
1432
+ function extensionStyleList(meta) {
1433
+ return declaredList(meta.styles);
1434
+ }
1435
+ function extensionEngineStyleList(meta) {
1436
+ return declaredList(meta.engineStyles?.native);
1437
+ }
1438
+ function pathEscapesFolder(rel) {
1439
+ return path3.isAbsolute(rel) || rel.split(/[\\/]/).includes("..");
1440
+ }
1441
+ function assertExtensionContained(meta) {
1442
+ const declared = [
1443
+ ...extensionStyleList(meta),
1444
+ ...extensionEngineStyleList(meta),
1445
+ ...meta.markdown ? [meta.markdown] : [],
1446
+ ...meta.components ? [meta.components] : [],
1447
+ ...meta.snippets ? [meta.snippets] : [],
1448
+ ...meta.tokensFile ? [meta.tokensFile] : []
1449
+ ];
1450
+ for (const rel of declared) {
1451
+ if (pathEscapesFolder(rel)) {
1452
+ throw new Error(`Extension metadata declares "${rel}" outside its own folder; an extension must be self-contained.`);
1453
+ }
1454
+ }
1455
+ }
1456
+ function resolveExtension(dir, meta, subject) {
1457
+ const styles = resolveDeclaredStyles(extensionStyleList(meta), dir, subject);
1458
+ const engineStyles = resolveDeclaredStyles(extensionEngineStyleList(meta), dir, subject);
1459
+ const [markdown] = resolveDeclaredStyles(meta.markdown ? [meta.markdown] : undefined, dir, subject) ?? [];
1460
+ const [components] = resolveDeclaredStyles(meta.components ? [meta.components] : undefined, dir, subject) ?? [];
1461
+ const [snippets] = resolveDeclaredStyles(meta.snippets ? [meta.snippets] : undefined, dir, subject) ?? [];
1462
+ return {
1463
+ ...markdown ? { markdown } : {},
1464
+ ...styles ? { styles } : {},
1465
+ ...engineStyles ? { engineStyles } : {},
1466
+ ...meta.tokensFile?.trim() ? { tokensFile: meta.tokensFile.trim() } : {},
1467
+ ...components ? { components } : {},
1468
+ ...snippets ? { snippets } : {}
1469
+ };
1470
+ }
1471
+
1407
1472
  // src/lib/markdown/renderer.ts
1408
1473
  import MarkdownIt from "markdown-it";
1409
1474
  import markdownItAttrs from "markdown-it-attrs";
@@ -1461,7 +1526,7 @@ function escapeHtml(s) {
1461
1526
  function escapeAttr(s) {
1462
1527
  return String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1463
1528
  }
1464
- function parseMarkerLine(line) {
1529
+ function parseMarkerLine(line, declaredWords) {
1465
1530
  const trimmed = line.trim();
1466
1531
  if (!trimmed.startsWith("@"))
1467
1532
  return null;
@@ -1495,9 +1560,9 @@ function parseMarkerLine(line) {
1495
1560
  tokens[i] = unwrapAttrBraces(tokens[i]);
1496
1561
  const head = tokens[0];
1497
1562
  const kind = head.slice(1);
1498
- if (!KNOWN_KINDS.includes(kind))
1563
+ if (!KNOWN_KINDS.includes(kind) && !(declaredWords && declaredWords.has(kind)))
1499
1564
  return null;
1500
- if (kind === "page-break" || kind === "column-break" || kind === "end-section" || kind === "continue") {
1565
+ if (kind === "page-break" || kind === "column-break" || kind === "continue" || kind.startsWith("end-")) {
1501
1566
  return { kind, name: null, attrs: {} };
1502
1567
  }
1503
1568
  const body = tokens.slice(1);
@@ -1600,12 +1665,14 @@ function attachDataAttrs(token, kind, name, attrs) {
1600
1665
  if (name) {
1601
1666
  if (kind === "chapter")
1602
1667
  token.attrSet("data-chapter-label", name);
1603
- if (kind === "spread")
1668
+ else if (kind === "spread")
1604
1669
  token.attrSet("data-spread", name);
1605
- if (kind === "page")
1670
+ else if (kind === "page")
1606
1671
  token.attrSet("data-page", name);
1607
- if (kind === "section")
1672
+ else if (kind === "section")
1608
1673
  token.attrSet("data-section", name);
1674
+ else
1675
+ token.attrSet(`data-${kind}`, name);
1609
1676
  }
1610
1677
  if (attrs.template)
1611
1678
  token.attrSet("data-template", attrs.template);
@@ -1621,16 +1688,193 @@ function attachDataAttrs(token, kind, name, attrs) {
1621
1688
  token.attrSet(`data-${k}`, v);
1622
1689
  }
1623
1690
  }
1691
+ var DECLARED_NAME_RE = /^[a-z][a-z0-9-]*$/;
1692
+ function validateDeclaredMarkerName(name, pluginName) {
1693
+ if (!DECLARED_NAME_RE.test(name)) {
1694
+ 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").`);
1695
+ }
1696
+ if (name.startsWith("end-")) {
1697
+ 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.`);
1698
+ }
1699
+ }
1700
+ var TAG_NAME_RE = /^[a-z][a-z0-9-]*$/;
1701
+ function resolveContainerShape(name, pluginName, decl) {
1702
+ const tag = decl.tag !== undefined ? decl.tag : "div";
1703
+ if (typeof tag !== "string" || !TAG_NAME_RE.test(tag)) {
1704
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has an invalid \`tag\` (${JSON.stringify(decl.tag)}) — ` + `must be a lower-case HTML tag name.`);
1705
+ }
1706
+ const classBase = decl.class !== undefined ? decl.class : "";
1707
+ if (typeof classBase !== "string") {
1708
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`class\` that is not a string.`);
1709
+ }
1710
+ let variants;
1711
+ if (decl.variants !== undefined) {
1712
+ if (typeof decl.variants !== "object" || decl.variants === null || Array.isArray(decl.variants)) {
1713
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has \`variants\` that is not a plain object.`);
1714
+ }
1715
+ variants = {};
1716
+ for (const [variantName, variantClass] of Object.entries(decl.variants)) {
1717
+ if (typeof variantClass !== "string") {
1718
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" variant "${variantName}" is not a string.`);
1719
+ }
1720
+ variants[variantName] = variantClass;
1721
+ }
1722
+ }
1723
+ let label;
1724
+ if (decl.label !== undefined) {
1725
+ if (typeof decl.label !== "object" || decl.label === null || Array.isArray(decl.label)) {
1726
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`label\` that is not an object.`);
1727
+ }
1728
+ if (typeof decl.label.class !== "string" || !decl.label.class) {
1729
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`label.class\` that is not a non-empty string.`);
1730
+ }
1731
+ const match = typeof decl.label.from === "string" ? /^attr:([A-Za-z_][A-Za-z0-9_-]*)$/.exec(decl.label.from) : null;
1732
+ if (!match) {
1733
+ 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.`);
1734
+ }
1735
+ const labelTag = decl.label.tag !== undefined ? decl.label.tag : "div";
1736
+ if (typeof labelTag !== "string" || !TAG_NAME_RE.test(labelTag)) {
1737
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has an invalid \`label.tag\` (${JSON.stringify(decl.label.tag)}).`);
1738
+ }
1739
+ label = { class: decl.label.class, attr: match[1], tag: labelTag };
1740
+ }
1741
+ let autoCloseAtEof = false;
1742
+ if (decl.autoCloseAt !== undefined) {
1743
+ if (!Array.isArray(decl.autoCloseAt)) {
1744
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has an \`autoCloseAt\` that is not an array.`);
1745
+ }
1746
+ for (const value of decl.autoCloseAt) {
1747
+ if (value !== "eof") {
1748
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" declares an unsupported \`autoCloseAt\` value ` + `${JSON.stringify(value)} — only "eof" is currently supported.`);
1749
+ }
1750
+ }
1751
+ autoCloseAtEof = decl.autoCloseAt.includes("eof");
1752
+ }
1753
+ return { tag, classBase, variants, label, autoCloseAtEof };
1754
+ }
1755
+ function resolveMarkerDeclaration(name, rawDecl, rawRegistry, originOf) {
1756
+ const pluginName = originOf.get(name);
1757
+ if (typeof rawDecl !== "object" || rawDecl === null || Array.isArray(rawDecl)) {
1758
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" is not a plain object.`);
1759
+ }
1760
+ if (rawDecl.deprecated !== undefined) {
1761
+ if (typeof rawDecl.deprecated !== "string" || !rawDecl.deprecated) {
1762
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`deprecated\` value that is not a non-empty string.`);
1763
+ }
1764
+ return { name, deprecated: rawDecl.deprecated };
1765
+ }
1766
+ if (rawDecl.alias !== undefined) {
1767
+ if (typeof rawDecl.alias !== "string" || !rawDecl.alias) {
1768
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has an \`alias\` value that is not a non-empty string.`);
1769
+ }
1770
+ const targetRaw = rawRegistry.get(rawDecl.alias);
1771
+ if (!targetRaw) {
1772
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" aliases unknown marker "@${rawDecl.alias}" — no ` + `loaded plugin declares it.`);
1773
+ }
1774
+ if (targetRaw.deprecated !== undefined) {
1775
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" aliases "@${rawDecl.alias}", which is deprecated: ` + `${targetRaw.deprecated}`);
1776
+ }
1777
+ if (targetRaw.alias !== undefined) {
1778
+ 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.`);
1779
+ }
1780
+ const preset = rawDecl.preset !== undefined ? rawDecl.preset : {};
1781
+ if (typeof preset !== "object" || preset === null || Array.isArray(preset)) {
1782
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`preset\` that is not a plain object.`);
1783
+ }
1784
+ if (preset.variant !== undefined && typeof preset.variant !== "string") {
1785
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`preset.variant\` that is not a string.`);
1786
+ }
1787
+ const container = resolveContainerShape(rawDecl.alias, originOf.get(rawDecl.alias), targetRaw);
1788
+ return { name, baseKind: rawDecl.alias, presetVariant: preset.variant, ...container };
1789
+ }
1790
+ return { name, baseKind: name, ...resolveContainerShape(name, pluginName, rawDecl) };
1791
+ }
1792
+ function buildDeclaredMarkerRegistry(sources) {
1793
+ const rawRegistry = new Map;
1794
+ const originOf = new Map;
1795
+ for (const { pluginName, markers } of sources) {
1796
+ if (!markers)
1797
+ continue;
1798
+ for (const [name, rawDecl] of Object.entries(markers)) {
1799
+ validateDeclaredMarkerName(name, pluginName);
1800
+ if (KNOWN_KINDS.includes(name)) {
1801
+ 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.`);
1802
+ }
1803
+ if (rawRegistry.has(name)) {
1804
+ 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.`);
1805
+ }
1806
+ rawRegistry.set(name, rawDecl);
1807
+ originOf.set(name, pluginName);
1808
+ }
1809
+ }
1810
+ const resolved = new Map;
1811
+ for (const [name, rawDecl] of rawRegistry) {
1812
+ resolved.set(name, resolveMarkerDeclaration(name, rawDecl, rawRegistry, originOf));
1813
+ }
1814
+ return resolved;
1815
+ }
1816
+ function declaredKindWords(declaredMarkers) {
1817
+ if (!declaredMarkers)
1818
+ return null;
1819
+ const words = new Set;
1820
+ for (const name of declaredMarkers.keys()) {
1821
+ words.add(name);
1822
+ words.add(`end-${name}`);
1823
+ }
1824
+ return words;
1825
+ }
1826
+ function nearestDeclaredMarkerName(word, declaredMarkers) {
1827
+ const w = word.toLowerCase();
1828
+ const threshold = Math.max(2, Math.floor(w.length / 3));
1829
+ let best = null;
1830
+ let bestDistance = Infinity;
1831
+ for (const known of declaredMarkers.keys()) {
1832
+ const d = editDistance(w, known);
1833
+ if (d < bestDistance) {
1834
+ bestDistance = d;
1835
+ best = known;
1836
+ }
1837
+ }
1838
+ return best !== null && bestDistance > 0 && bestDistance <= threshold ? best : null;
1839
+ }
1840
+ function scanForUnknownDeclaredMarkers(state, declaredMarkers) {
1841
+ if (!declaredMarkers)
1842
+ return;
1843
+ for (let tokenIndex = 0;tokenIndex < state.tokens.length; tokenIndex++) {
1844
+ const tok = state.tokens[tokenIndex];
1845
+ if (tok.type !== "inline" || !tok.map)
1846
+ continue;
1847
+ const opener = state.tokens[tokenIndex - 1];
1848
+ if (!opener || opener.type !== "paragraph_open")
1849
+ continue;
1850
+ const lines = tok.content.split(`
1851
+ `);
1852
+ for (let i = 0;i < lines.length; i++) {
1853
+ const m = /^@([A-Za-z][A-Za-z0-9-]*)(?=\s|$)/.exec(lines[i].trim());
1854
+ if (!m)
1855
+ continue;
1856
+ const word = m[1];
1857
+ if (KNOWN_KINDS.includes(word) || declaredMarkers.has(word))
1858
+ continue;
1859
+ const suggestion = nearestDeclaredMarkerName(word, declaredMarkers);
1860
+ if (!suggestion)
1861
+ continue;
1862
+ warn2(state.env, tok.map[0] + i + 1, "unknown_marker", `Unknown marker "@${word}". Did you mean "@${suggestion}"?`, null);
1863
+ }
1864
+ }
1865
+ }
1624
1866
  function plugin(md, pluginOptions = {}) {
1625
1867
  const options = {
1626
1868
  preferPagesInSpreads: false,
1627
1869
  ...pluginOptions
1628
1870
  };
1871
+ const declaredMarkers = options.declaredMarkers instanceof Map && options.declaredMarkers.size > 0 ? options.declaredMarkers : null;
1872
+ const declaredWords = declaredKindWords(declaredMarkers);
1629
1873
  function markerBlock(state, startLine, endLine, silent) {
1630
1874
  const pos = state.bMarks[startLine] + state.tShift[startLine];
1631
1875
  const max = state.eMarks[startLine];
1632
1876
  const line = state.src.slice(pos, max);
1633
- const parsed = parseMarkerLine(line);
1877
+ const parsed = parseMarkerLine(line, declaredWords);
1634
1878
  if (!parsed)
1635
1879
  return false;
1636
1880
  if (silent)
@@ -1687,10 +1931,64 @@ function plugin(md, pluginOptions = {}) {
1687
1931
  }
1688
1932
  }
1689
1933
  md.core.ruler.after("block", "layout_transform", function(state) {
1934
+ scanForUnknownDeclaredMarkers(state, declaredMarkers);
1690
1935
  if (!state.env.__layoutMarkersUsed)
1691
1936
  return;
1692
1937
  scanForMistypedMarkers(state);
1693
1938
  const out = [];
1939
+ const declaredFrames = [];
1940
+ function declaredFrameIndex(kind) {
1941
+ for (let i = declaredFrames.length - 1;i >= 0; i--) {
1942
+ if (declaredFrames[i].kind === kind)
1943
+ return i;
1944
+ }
1945
+ return -1;
1946
+ }
1947
+ function closeDeclaredFrame(kind) {
1948
+ const at = declaredFrameIndex(kind);
1949
+ if (at === -1)
1950
+ return false;
1951
+ while (declaredFrames.length > at) {
1952
+ const frame = declaredFrames.pop();
1953
+ out.push(new state.Token("layout_component_close", frame.tag, -1));
1954
+ }
1955
+ return true;
1956
+ }
1957
+ function drainDeclaredFrames() {
1958
+ while (declaredFrames.length) {
1959
+ const frame = declaredFrames.pop();
1960
+ out.push(new state.Token("layout_component_close", frame.tag, -1));
1961
+ }
1962
+ }
1963
+ function drainDeclaredFramesAtEof() {
1964
+ while (declaredFrames.length) {
1965
+ const frame = declaredFrames.pop();
1966
+ if (!frame.decl.autoCloseAtEof) {
1967
+ warn2(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);
1968
+ }
1969
+ out.push(new state.Token("layout_component_close", frame.tag, -1));
1970
+ }
1971
+ }
1972
+ function openDeclaredMarker(meta, decl) {
1973
+ const t = new state.Token("layout_component_open", decl.tag, 1);
1974
+ t.meta = { line: meta.__line };
1975
+ const variant = meta.name || decl.presetVariant || null;
1976
+ const variantClass = variant && decl.variants && decl.variants[variant] || "";
1977
+ const baseClass = [decl.classBase, variantClass].filter(Boolean).join(" ");
1978
+ addClasses(t, baseClass, meta.attrs && meta.attrs.class ? meta.attrs.class : "");
1979
+ attachDataAttrs(t, decl.baseKind, variant, meta.attrs || {});
1980
+ out.push(t);
1981
+ declaredFrames.push({ kind: decl.baseKind, tag: decl.tag, decl, line: meta.__line });
1982
+ if (decl.label) {
1983
+ const value = meta.attrs && meta.attrs[decl.label.attr];
1984
+ if (value) {
1985
+ const labelToken = new state.Token("html_block", "", 0);
1986
+ labelToken.content = `<${decl.label.tag} class="${escapeAttr(decl.label.class)}">${escapeHtml(value)}</${decl.label.tag}>
1987
+ `;
1988
+ out.push(labelToken);
1989
+ }
1990
+ }
1991
+ }
1694
1992
  const stack = {
1695
1993
  frames: [],
1696
1994
  get(kind) {
@@ -1840,6 +2138,7 @@ function plugin(md, pluginOptions = {}) {
1840
2138
  const kind = meta.kind;
1841
2139
  const line = meta.__line || 0;
1842
2140
  if (kind === "chapter") {
2141
+ drainDeclaredFrames();
1843
2142
  stack.close("chapter");
1844
2143
  openChapter(meta);
1845
2144
  continue;
@@ -1848,16 +2147,19 @@ function plugin(md, pluginOptions = {}) {
1848
2147
  if (stack.has("spread")) {
1849
2148
  warn2(state.env, line, "nested_spread", "@spread encountered while another spread is open; closing the previous spread automatically.", meta);
1850
2149
  }
2150
+ drainDeclaredFrames();
1851
2151
  stack.close("spread");
1852
2152
  openSpread(meta);
1853
2153
  continue;
1854
2154
  }
1855
2155
  if (kind === "page") {
2156
+ drainDeclaredFrames();
1856
2157
  stack.close("page");
1857
2158
  openPage(meta);
1858
2159
  continue;
1859
2160
  }
1860
2161
  if (kind === "section") {
2162
+ drainDeclaredFrames();
1861
2163
  warnIfEmptyDecoratedSection("section", line);
1862
2164
  stack.close("section");
1863
2165
  const spread = stack.get("spread");
@@ -1873,6 +2175,7 @@ function plugin(md, pluginOptions = {}) {
1873
2175
  warn2(state.env, line, "continue_without_section", "@continue used without an open @section; ignoring marker.", meta);
1874
2176
  continue;
1875
2177
  }
2178
+ drainDeclaredFrames();
1876
2179
  const contMeta = {
1877
2180
  name: section.meta.name,
1878
2181
  attrs: { ...section.meta.attrs || {} }
@@ -1905,15 +2208,42 @@ function plugin(md, pluginOptions = {}) {
1905
2208
  continue;
1906
2209
  }
1907
2210
  if (kind === "end-section") {
2211
+ drainDeclaredFrames();
1908
2212
  warnIfEmptyDecoratedSection("end-section", line);
1909
2213
  stack.close("section");
1910
2214
  continue;
1911
2215
  }
2216
+ if (declaredMarkers) {
2217
+ const openDecl = declaredMarkers.get(kind);
2218
+ if (openDecl) {
2219
+ if (openDecl.deprecated) {
2220
+ warn2(state.env, line, "deprecated_marker", `@${kind} is deprecated: ${openDecl.deprecated}`, meta);
2221
+ continue;
2222
+ }
2223
+ closeDeclaredFrame(openDecl.baseKind);
2224
+ openDeclaredMarker(meta, openDecl);
2225
+ continue;
2226
+ }
2227
+ if (kind.startsWith("end-")) {
2228
+ const closeDecl = declaredMarkers.get(kind.slice(4));
2229
+ if (closeDecl) {
2230
+ if (closeDecl.deprecated) {
2231
+ warn2(state.env, line, "deprecated_marker", `@${kind} is deprecated: ${closeDecl.deprecated}`, meta);
2232
+ continue;
2233
+ }
2234
+ if (!closeDeclaredFrame(closeDecl.baseKind)) {
2235
+ warn2(state.env, line, "declared_marker_close_without_open", `@${kind} used without an open @${closeDecl.baseKind}; ignoring marker.`, meta);
2236
+ }
2237
+ continue;
2238
+ }
2239
+ }
2240
+ }
1912
2241
  }
1913
2242
  const eofSpread = stack.get("spread");
1914
2243
  if (eofSpread && !eofSpread.sawAnyPage) {
1915
2244
  warn2(state.env, 0, "spread_eof_close", "An open @spread reached end-of-document; closing it automatically.", null);
1916
2245
  }
2246
+ drainDeclaredFramesAtEof();
1917
2247
  stack.closeAll();
1918
2248
  state.tokens = out;
1919
2249
  });
@@ -2903,7 +3233,9 @@ function createMarkdownRenderer(customPlugins) {
2903
3233
  md.use(unwrapPlugin(markdownItFootnote));
2904
3234
  md.use(unwrapPlugin(markdownItDeflist));
2905
3235
  md.use(unwrapPlugin(markdownItSourceMap));
2906
- md.use(plugin);
3236
+ const declaredMarkerSources = (customPlugins ?? []).filter((p) => !!p.markers).map((p) => ({ pluginName: p.name, markers: p.markers }));
3237
+ const declaredMarkers = buildDeclaredMarkerRegistry(declaredMarkerSources);
3238
+ md.use(plugin, { declaredMarkers });
2907
3239
  md.use(gpPinScope);
2908
3240
  registerImageRule(md);
2909
3241
  if (customPlugins && customPlugins.length > 0) {
@@ -3226,7 +3558,7 @@ async function rewriteReachableModuleImports(entryPath, tree) {
3226
3558
  const pkg = packageOwningFile(tree, absolute);
3227
3559
  if (!pkg)
3228
3560
  throw new Error(`Reachable plugin module is outside the verified package tree: ${absolute}`);
3229
- const source = await readFile3(absolute, "utf8");
3561
+ const source = await readFile4(absolute, "utf8");
3230
3562
  const [imports] = parseModuleImports(source, absolute);
3231
3563
  const replacements = [];
3232
3564
  const targets = new Set;
@@ -3457,12 +3789,22 @@ function validateStylesExport(value, pluginRef) {
3457
3789
  }
3458
3790
  return value.length > 0 ? value : undefined;
3459
3791
  }
3792
+ function validateMarkersExport(value, pluginRef) {
3793
+ if (value === undefined)
3794
+ return;
3795
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
3796
+ throw new Error(`Plugin "${pluginRef}" exports \`markers\` that is not a plain object.`);
3797
+ }
3798
+ const table = value;
3799
+ return Object.keys(table).length > 0 ? table : undefined;
3800
+ }
3460
3801
  function extractPluginExports(pluginModule, pluginRef, exportName) {
3461
3802
  const mod = pluginModule !== null && (typeof pluginModule === "object" || typeof pluginModule === "function") ? pluginModule : {};
3462
3803
  let plugin2;
3463
3804
  let metadata = mod.metadata;
3464
3805
  let css = mod.css;
3465
3806
  let styles = validateStylesExport(mod.styles, pluginRef);
3807
+ let markers = validateMarkersExport(mod.markers, pluginRef);
3466
3808
  if (exportName && typeof mod[exportName] === "function") {
3467
3809
  plugin2 = mod[exportName];
3468
3810
  } else if (exportName) {
@@ -3478,11 +3820,12 @@ function extractPluginExports(pluginModule, pluginRef, exportName) {
3478
3820
  metadata = inner.metadata ?? metadata;
3479
3821
  css = inner.css ?? css;
3480
3822
  styles = validateStylesExport(inner.styles, pluginRef) ?? styles;
3823
+ markers = validateMarkersExport(inner.markers, pluginRef) ?? markers;
3481
3824
  }
3482
3825
  if (typeof plugin2 !== "function") {
3483
3826
  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) { ... }\`.`);
3484
3827
  }
3485
- return { plugin: plugin2, metadata, css, styles };
3828
+ return { plugin: plugin2, metadata, css, styles, markers };
3486
3829
  }
3487
3830
  var pathPluginCache = new Map;
3488
3831
  var liveShadowPaths = new Set;
@@ -3553,6 +3896,48 @@ function resolvePluginStyles(rawStyles, moduleDir, pluginRef) {
3553
3896
  }
3554
3897
  return resolveDeclaredStyles(rawStyles, moduleDir, `Plugin "${pluginRef}"`);
3555
3898
  }
3899
+ function noopPlugin() {}
3900
+ function extensionMetadata(meta) {
3901
+ if (!meta.name && !meta.description && !meta.author)
3902
+ return;
3903
+ return {
3904
+ ...meta.name ? { name: meta.name } : {},
3905
+ ...meta.description ? { description: meta.description } : {},
3906
+ ...meta.author ? { author: meta.author } : {}
3907
+ };
3908
+ }
3909
+ async function loadExtensionFromDir(extensionDir, config, pluginRef) {
3910
+ const meta = await readExtensionMeta(extensionDir);
3911
+ assertExtensionContained(meta);
3912
+ const resolved = resolveExtension(extensionDir, meta, `Plugin "${pluginRef}"`);
3913
+ const extensionStyles = [...resolved.styles ?? [], ...resolved.engineStyles ?? []];
3914
+ const name = config.name ?? meta.name ?? pluginRef;
3915
+ if (!resolved.markdown && extensionStyles.length === 0) {
3916
+ 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.");
3917
+ }
3918
+ if (!resolved.markdown) {
3919
+ return {
3920
+ name,
3921
+ plugin: noopPlugin,
3922
+ ...extensionMetadata(meta) ? { metadata: extensionMetadata(meta) } : {},
3923
+ ...extensionStyles.length > 0 ? { styles: extensionStyles } : {},
3924
+ options: config.options
3925
+ };
3926
+ }
3927
+ const pluginModule = await loadCachedPathPluginModule(resolved.markdown);
3928
+ const { plugin: plugin2, metadata, css, styles: rawStyles, markers } = extractPluginExports(pluginModule, pluginRef, config.export);
3929
+ const ownStyles = resolvePluginStyles(rawStyles, dirname2(resolved.markdown), pluginRef);
3930
+ const styles = [...extensionStyles, ...ownStyles ?? []];
3931
+ return {
3932
+ name,
3933
+ plugin: plugin2,
3934
+ metadata: metadata ?? extensionMetadata(meta),
3935
+ css,
3936
+ ...styles.length > 0 ? { styles } : {},
3937
+ ...markers ? { markers } : {},
3938
+ options: config.options
3939
+ };
3940
+ }
3556
3941
  async function loadPlugin(config, baseDir) {
3557
3942
  const pluginRef = config.path ?? config.name ?? "(unspecified)";
3558
3943
  let pluginModule;
@@ -3568,10 +3953,21 @@ async function loadPlugin(config, baseDir) {
3568
3953
  options: config.options
3569
3954
  };
3570
3955
  }
3956
+ if (config.path) {
3957
+ const candidatePath = resolve3(baseDir, config.path);
3958
+ if (existsSync5(candidatePath) && statSync2(candidatePath).isDirectory()) {
3959
+ try {
3960
+ return await loadExtensionFromDir(candidatePath, config, pluginRef);
3961
+ } catch (error2) {
3962
+ const errorMsg = error2 instanceof Error ? error2.message : String(error2);
3963
+ throw new Error(`Failed to load plugin "${pluginRef}": ${errorMsg}`);
3964
+ }
3965
+ }
3966
+ }
3571
3967
  try {
3572
3968
  if (config.path) {
3573
3969
  const pluginPath = resolve3(baseDir, config.path);
3574
- if (!existsSync4(pluginPath)) {
3970
+ if (!existsSync5(pluginPath)) {
3575
3971
  throw new Error(`Plugin file not found: ${pluginPath} (resolved from manifest entry path="${config.path}")`);
3576
3972
  }
3577
3973
  pluginModule = await loadCachedPathPluginModule(pluginPath);
@@ -3587,7 +3983,7 @@ async function loadPlugin(config, baseDir) {
3587
3983
  const errorMsg = error2 instanceof Error ? error2.message : String(error2);
3588
3984
  throw new Error(`Failed to load plugin "${pluginRef}": ${errorMsg}`);
3589
3985
  }
3590
- const { plugin: plugin2, metadata, css, styles: rawStyles } = extractPluginExports(pluginModule, pluginRef, config.export);
3986
+ const { plugin: plugin2, metadata, css, styles: rawStyles, markers } = extractPluginExports(pluginModule, pluginRef, config.export);
3591
3987
  const styles = resolvePluginStyles(rawStyles, moduleDir, pluginRef);
3592
3988
  return {
3593
3989
  name: pluginName,
@@ -3595,6 +3991,7 @@ async function loadPlugin(config, baseDir) {
3595
3991
  metadata,
3596
3992
  css,
3597
3993
  styles,
3994
+ markers,
3598
3995
  options: config.options
3599
3996
  };
3600
3997
  }
@@ -3638,7 +4035,10 @@ async function runLint(opts = {}) {
3638
4035
  } else {
3639
4036
  const relStyles = await resolveActiveStyles(manifestDir, resolved.styles);
3640
4037
  const projectFiles = relStyles.map((rel) => resolve4(manifestDir, rel));
3641
- const { pluginStylePaths } = await loadPluginsWithCss(resolved.plugins, manifestDir, (ref, err) => log.warn(`Skipping plugin "${ref}" for lint — ${err.message}`));
4038
+ let pluginStylePaths = opts.pluginStylePaths;
4039
+ if (pluginStylePaths === undefined) {
4040
+ ({ pluginStylePaths } = await loadPluginsWithCss(resolved.plugins, manifestDir, (ref, err) => log.warn(`Skipping plugin "${ref}" for lint — ${err.message}`)));
4041
+ }
3642
4042
  files = [...projectFiles, ...pluginStylePaths].filter((f) => !f.endsWith(".min.css"));
3643
4043
  }
3644
4044
  if (files.length === 0) {
@@ -3652,7 +4052,7 @@ async function runLint(opts = {}) {
3652
4052
  for (const file of files) {
3653
4053
  let css;
3654
4054
  try {
3655
- css = await readFile4(file, "utf8");
4055
+ css = await readFile5(file, "utf8");
3656
4056
  } catch (err) {
3657
4057
  log.error(` ${file}`);
3658
4058
  log.error(` cannot read stylesheet: ${err instanceof Error ? err.message : String(err)}`);
@@ -3791,7 +4191,7 @@ function inspectionFailed(checkId, message, extra = {}) {
3791
4191
  }
3792
4192
 
3793
4193
  // src/lib/pdf-inspect.ts
3794
- import { stat, readFile as readFile5 } from "node:fs/promises";
4194
+ import { stat, readFile as readFile6 } from "node:fs/promises";
3795
4195
  import { getDocumentProxy } from "unpdf";
3796
4196
  import * as pdfjs from "unpdf/pdfjs";
3797
4197
  function getOps(mod) {
@@ -3827,17 +4227,17 @@ function evictLru() {
3827
4227
  destroyEntryAfterGrace(evicted);
3828
4228
  }
3829
4229
  }
3830
- async function loadPdf(path3) {
4230
+ async function loadPdf(path4) {
3831
4231
  let st;
3832
4232
  try {
3833
- st = await stat(path3);
4233
+ st = await stat(path4);
3834
4234
  } catch {
3835
4235
  return null;
3836
4236
  }
3837
- const hit = docCache.get(path3);
4237
+ const hit = docCache.get(path4);
3838
4238
  if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) {
3839
- docCache.delete(path3);
3840
- docCache.set(path3, hit);
4239
+ docCache.delete(path4);
4240
+ docCache.set(path4, hit);
3841
4241
  try {
3842
4242
  return await hit.doc;
3843
4243
  } catch {
@@ -3846,15 +4246,15 @@ async function loadPdf(path3) {
3846
4246
  }
3847
4247
  if (hit)
3848
4248
  destroyEntry(hit);
3849
- const docPromise = readFile5(path3).then((buf) => getDocumentProxy(new Uint8Array(buf)));
3850
- docCache.delete(path3);
3851
- docCache.set(path3, { mtimeMs: st.mtimeMs, size: st.size, doc: docPromise });
4249
+ const docPromise = readFile6(path4).then((buf) => getDocumentProxy(new Uint8Array(buf)));
4250
+ docCache.delete(path4);
4251
+ docCache.set(path4, { mtimeMs: st.mtimeMs, size: st.size, doc: docPromise });
3852
4252
  evictLru();
3853
4253
  try {
3854
4254
  return await docPromise;
3855
4255
  } catch {
3856
- if (docCache.get(path3)?.doc === docPromise)
3857
- docCache.delete(path3);
4256
+ if (docCache.get(path4)?.doc === docPromise)
4257
+ docCache.delete(path4);
3858
4258
  return null;
3859
4259
  }
3860
4260
  }
@@ -4305,9 +4705,9 @@ var check3 = {
4305
4705
  registerCheck(check3);
4306
4706
 
4307
4707
  // src/lib/pdf-parse.ts
4308
- import { readFile as readFile6 } from "node:fs/promises";
4708
+ import { readFile as readFile7 } from "node:fs/promises";
4309
4709
  async function readPdfBytes(pdfPath) {
4310
- return readFile6(pdfPath, "latin1");
4710
+ return readFile7(pdfPath, "latin1");
4311
4711
  }
4312
4712
  function parseInkCov(out) {
4313
4713
  const lines = out.split(/\r?\n/).filter((l) => l.includes("CMYK"));
@@ -4878,21 +5278,21 @@ var check15 = {
4878
5278
  registerCheck(check15);
4879
5279
 
4880
5280
  // src/checks/source/markdownlint.ts
4881
- import { readFile as readFile7 } from "node:fs/promises";
5281
+ import { readFile as readFile8 } from "node:fs/promises";
4882
5282
  import { lint } from "markdownlint/sync";
4883
5283
  import { parse as parseYaml2 } from "yaml";
4884
5284
 
4885
5285
  // src/checks/source/config-file.ts
4886
- import { existsSync as existsSync5 } from "node:fs";
5286
+ import { existsSync as existsSync6 } from "node:fs";
4887
5287
  import { resolve as resolve5 } from "node:path";
4888
5288
  function findConfigFile(inputDir, names, explicit) {
4889
5289
  if (explicit) {
4890
5290
  const p = resolve5(inputDir, explicit);
4891
- return existsSync5(p) ? p : null;
5291
+ return existsSync6(p) ? p : null;
4892
5292
  }
4893
5293
  for (const name of names) {
4894
5294
  const p = resolve5(inputDir, name);
4895
- if (existsSync5(p))
5295
+ if (existsSync6(p))
4896
5296
  return p;
4897
5297
  }
4898
5298
  return null;
@@ -4911,7 +5311,7 @@ function stripJsonComments(s) {
4911
5311
  return s.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
4912
5312
  }
4913
5313
  async function loadConfig(configPath) {
4914
- const raw = await readFile7(configPath, "utf8");
5314
+ const raw = await readFile8(configPath, "utf8");
4915
5315
  let parsed;
4916
5316
  if (configPath.endsWith(".json")) {
4917
5317
  parsed = JSON.parse(raw);
@@ -4970,8 +5370,8 @@ var check16 = {
4970
5370
  registerCheck(check16);
4971
5371
 
4972
5372
  // src/checks/source/htmlhint.ts
4973
- import { existsSync as existsSync6 } from "node:fs";
4974
- import { readFile as readFile8 } from "node:fs/promises";
5373
+ import { existsSync as existsSync7 } from "node:fs";
5374
+ import { readFile as readFile9 } from "node:fs/promises";
4975
5375
  import htmlhint from "htmlhint";
4976
5376
  var { HTMLHint } = htmlhint;
4977
5377
  var CONFIG_NAMES2 = [".htmlhintrc"];
@@ -4985,14 +5385,14 @@ var check17 = {
4985
5385
  const sourceConfig = ctx.config.validate.source;
4986
5386
  if (sourceConfig.htmlhint === false)
4987
5387
  return [];
4988
- if (!ctx.htmlPath || !existsSync6(ctx.htmlPath))
5388
+ if (!ctx.htmlPath || !existsSync7(ctx.htmlPath))
4989
5389
  return [];
4990
5390
  const configPath = typeof sourceConfig.htmlhint === "string" ? sourceConfig.htmlhint : null;
4991
5391
  const resolvedConfig = findConfigFile(ctx.inputDir, CONFIG_NAMES2, configPath);
4992
5392
  if (!resolvedConfig && !configPath)
4993
5393
  return [];
4994
- const ruleset = resolvedConfig ? JSON.parse(await readFile8(resolvedConfig, "utf8")) : HTMLHint.defaultRuleset;
4995
- const html = await readFile8(ctx.htmlPath, "utf8");
5394
+ const ruleset = resolvedConfig ? JSON.parse(await readFile9(resolvedConfig, "utf8")) : HTMLHint.defaultRuleset;
5395
+ const html = await readFile9(ctx.htmlPath, "utf8");
4996
5396
  const messages = HTMLHint.verify(html, ruleset);
4997
5397
  return messages.map((m) => ({
4998
5398
  checkId: check17.id,
@@ -5007,7 +5407,7 @@ var check17 = {
5007
5407
  registerCheck(check17);
5008
5408
 
5009
5409
  // src/checks/source/stylelint.ts
5010
- import { readFile as readFile9 } from "node:fs/promises";
5410
+ import { readFile as readFile10 } from "node:fs/promises";
5011
5411
  var check18 = {
5012
5412
  id: "source.stylelint",
5013
5413
  name: "Print-safety CSS",
@@ -5025,7 +5425,7 @@ var check18 = {
5025
5425
  for (const file of files) {
5026
5426
  let css;
5027
5427
  try {
5028
- css = await readFile9(file, "utf8");
5428
+ css = await readFile10(file, "utf8");
5029
5429
  } catch {
5030
5430
  continue;
5031
5431
  }
@@ -5046,12 +5446,12 @@ var check18 = {
5046
5446
  registerCheck(check18);
5047
5447
 
5048
5448
  // src/checks/source/css-ownership.ts
5049
- import { readFile as readFile10 } from "node:fs/promises";
5449
+ import { readFile as readFile11 } from "node:fs/promises";
5050
5450
  import postcss2 from "postcss";
5051
5451
  import { parse as parseYaml3 } from "yaml";
5052
5452
  var CONFIG_NAMES3 = [".gutterpress/css-contract.yaml", ".gutterpress/css-contract.yml"];
5053
5453
  async function loadContract(configPath) {
5054
- const raw = await readFile10(configPath, "utf8");
5454
+ const raw = await readFile11(configPath, "utf8");
5055
5455
  const parsed = parseYaml3(raw);
5056
5456
  if (parsed && typeof parsed === "object")
5057
5457
  return parsed;
@@ -5202,7 +5602,7 @@ var check19 = {
5202
5602
  continue;
5203
5603
  let css;
5204
5604
  try {
5205
- css = await readFile10(absFile, "utf8");
5605
+ css = await readFile11(absFile, "utf8");
5206
5606
  } catch {
5207
5607
  continue;
5208
5608
  }
@@ -5244,14 +5644,14 @@ var check19 = {
5244
5644
  registerCheck(check19);
5245
5645
 
5246
5646
  // src/checks/source/local-refs.ts
5247
- import { existsSync as existsSync7 } from "node:fs";
5248
- import { readFile as readFile12 } from "node:fs/promises";
5647
+ import { existsSync as existsSync8 } from "node:fs";
5648
+ import { readFile as readFile13 } from "node:fs/promises";
5249
5649
  import { dirname as dirname3, resolve as resolve6 } from "node:path";
5250
5650
 
5251
5651
  // src/lib/asset-inline.ts
5252
5652
  import { createHash as createHash3 } from "node:crypto";
5253
- import { readFile as readFile11 } from "node:fs/promises";
5254
- import path3 from "node:path";
5653
+ import { readFile as readFile12 } from "node:fs/promises";
5654
+ import path4 from "node:path";
5255
5655
  import postcss3 from "postcss";
5256
5656
  var FONT_EXTS = new Set([".woff2", ".woff", ".ttf", ".otf"]);
5257
5657
  var HASHED_ASSET_DIR = "assets";
@@ -5292,19 +5692,19 @@ function decodeRef(ref) {
5292
5692
  }
5293
5693
  }
5294
5694
  function toPosix(p) {
5295
- return p.split(path3.sep).join("/");
5695
+ return p.split(path4.sep).join("/");
5296
5696
  }
5297
5697
  function escapesProjectRoot(projectDir, absPath) {
5298
- const rel = path3.relative(projectDir, absPath);
5299
- return rel === ".." || rel.startsWith(`..${path3.sep}`) || path3.isAbsolute(rel);
5698
+ const rel = path4.relative(projectDir, absPath);
5699
+ return rel === ".." || rel.startsWith(`..${path4.sep}`) || path4.isAbsolute(rel);
5300
5700
  }
5301
5701
  function proseImageRefError(ref, projectDir) {
5302
5702
  const cleaned = stripUrlSuffix(decodeRef(ref));
5303
- if (/^file:/i.test(cleaned) || path3.isAbsolute(cleaned)) {
5703
+ if (/^file:/i.test(cleaned) || path4.isAbsolute(cleaned)) {
5304
5704
  return `Image reference must be relative to the project: ${ref}
5305
5705
  ` + ` Copy the file into your project folder and reference it from there.`;
5306
5706
  }
5307
- if (escapesProjectRoot(projectDir, path3.resolve(projectDir, cleaned))) {
5707
+ if (escapesProjectRoot(projectDir, path4.resolve(projectDir, cleaned))) {
5308
5708
  return `Image reference points outside the project: ${ref}
5309
5709
  ` + ` Copy the file into your project folder and reference it from there.`;
5310
5710
  }
@@ -5315,7 +5715,7 @@ function mimeFor(ext) {
5315
5715
  }
5316
5716
  async function readOrThrow(absPath, kind, referencedFrom) {
5317
5717
  try {
5318
- return await readFile11(absPath);
5718
+ return await readFile12(absPath);
5319
5719
  } catch {
5320
5720
  throw new BuildError(`Missing ${kind}: ${absPath}
5321
5721
  referenced from ${referencedFrom}
@@ -5374,12 +5774,12 @@ function importTarget(atRule) {
5374
5774
  return target;
5375
5775
  }
5376
5776
  async function inlineOne(cssPath, copies, warnings, seen) {
5377
- const abs = path3.resolve(cssPath);
5777
+ const abs = path4.resolve(cssPath);
5378
5778
  if (seen.has(abs))
5379
5779
  return "";
5380
5780
  seen.add(abs);
5381
5781
  const source = await readOrThrow(abs, "stylesheet", "the project manifest");
5382
- const cssDir = path3.dirname(abs);
5782
+ const cssDir = path4.dirname(abs);
5383
5783
  let root;
5384
5784
  try {
5385
5785
  root = postcss3.parse(source.toString("utf-8"), { from: abs });
@@ -5401,8 +5801,8 @@ async function inlineOne(cssPath, copies, warnings, seen) {
5401
5801
  return null;
5402
5802
  }
5403
5803
  const relPath = stripUrlSuffix(decodeRef(raw));
5404
- const absAsset = path3.resolve(cssDir, relPath);
5405
- const ext = path3.extname(absAsset).toLowerCase();
5804
+ const absAsset = path4.resolve(cssDir, relPath);
5805
+ const ext = path4.extname(absAsset).toLowerCase();
5406
5806
  if (FONT_EXTS.has(ext)) {
5407
5807
  const bytes2 = await readOrThrow(absAsset, "font file", abs);
5408
5808
  return dataUri(bytes2, ext);
@@ -5420,7 +5820,7 @@ async function inlineOne(cssPath, copies, warnings, seen) {
5420
5820
  imports.push({ node, target });
5421
5821
  });
5422
5822
  for (const { node, target } of imports) {
5423
- const importedAbs = path3.resolve(cssDir, stripUrlSuffix(decodeRef(target)));
5823
+ const importedAbs = path4.resolve(cssDir, stripUrlSuffix(decodeRef(target)));
5424
5824
  const inlined = await inlineOne(importedAbs, copies, warnings, seen);
5425
5825
  node.replaceWith(wrapImportConditions(node, postcss3.parse(inlined, { from: importedAbs })));
5426
5826
  }
@@ -5432,10 +5832,10 @@ async function inlineStyles(projectDir, stylePaths) {
5432
5832
  const seen = new Set;
5433
5833
  const parts = [];
5434
5834
  for (const rel of stylePaths) {
5435
- const abs = path3.resolve(projectDir, rel);
5835
+ const abs = path4.resolve(projectDir, rel);
5436
5836
  const css = await inlineOne(abs, copies, warnings, seen);
5437
5837
  if (css.trim().length > 0) {
5438
- parts.push(`/* ${toPosix(path3.relative(projectDir, abs))} */
5838
+ parts.push(`/* ${toPosix(path4.relative(projectDir, abs))} */
5439
5839
  ${css.trim()}`);
5440
5840
  }
5441
5841
  }
@@ -5451,14 +5851,14 @@ async function collectStyleDependencies(projectDir, stylePaths) {
5451
5851
  const found = new Set;
5452
5852
  const visited = new Set;
5453
5853
  async function walk(cssPath) {
5454
- const abs = path3.resolve(cssPath);
5854
+ const abs = path4.resolve(cssPath);
5455
5855
  if (visited.has(abs))
5456
5856
  return;
5457
5857
  visited.add(abs);
5458
5858
  found.add(abs);
5459
5859
  let source;
5460
5860
  try {
5461
- source = await readFile11(abs, "utf-8");
5861
+ source = await readFile12(abs, "utf-8");
5462
5862
  } catch {
5463
5863
  return;
5464
5864
  }
@@ -5468,14 +5868,14 @@ async function collectStyleDependencies(projectDir, stylePaths) {
5468
5868
  } catch {
5469
5869
  return;
5470
5870
  }
5471
- const cssDir = path3.dirname(abs);
5871
+ const cssDir = path4.dirname(abs);
5472
5872
  root.walkDecls((decl) => {
5473
5873
  if (!decl.value.includes("url("))
5474
5874
  return;
5475
5875
  for (const raw of extractCssUrls(decl.value)) {
5476
5876
  if (!raw || isNonFilesystemRef(raw))
5477
5877
  continue;
5478
- found.add(path3.resolve(cssDir, stripUrlSuffix(decodeRef(raw))));
5878
+ found.add(path4.resolve(cssDir, stripUrlSuffix(decodeRef(raw))));
5479
5879
  }
5480
5880
  });
5481
5881
  const imports = [];
@@ -5485,11 +5885,11 @@ async function collectStyleDependencies(projectDir, stylePaths) {
5485
5885
  imports.push(target);
5486
5886
  });
5487
5887
  for (const target of imports) {
5488
- await walk(path3.resolve(cssDir, stripUrlSuffix(decodeRef(target))));
5888
+ await walk(path4.resolve(cssDir, stripUrlSuffix(decodeRef(target))));
5489
5889
  }
5490
5890
  }
5491
5891
  for (const rel of stylePaths) {
5492
- await walk(path3.resolve(projectDir, rel));
5892
+ await walk(path4.resolve(projectDir, rel));
5493
5893
  }
5494
5894
  return [...found];
5495
5895
  }
@@ -5506,8 +5906,8 @@ async function planImageCopies(projectDir, refs) {
5506
5906
  continue;
5507
5907
  }
5508
5908
  const cleaned = stripUrlSuffix(decodeRef(ref));
5509
- const abs = path3.resolve(projectDir, cleaned);
5510
- const rel = path3.relative(projectDir, abs);
5909
+ const abs = path4.resolve(projectDir, cleaned);
5910
+ const rel = path4.relative(projectDir, abs);
5511
5911
  const dest = toPosix(rel);
5512
5912
  copies.set(dest, { from: abs, to: dest });
5513
5913
  destinations.set(ref, dest);
@@ -5528,18 +5928,18 @@ async function inlineShapeUrls(html, baseDir) {
5528
5928
  if (!raw || isNonFilesystemRef(raw))
5529
5929
  continue;
5530
5930
  const cleaned = stripUrlSuffix(decodeRef(raw));
5531
- if (path3.isAbsolute(cleaned))
5931
+ if (path4.isAbsolute(cleaned))
5532
5932
  continue;
5533
- const abs = path3.resolve(baseDir, cleaned);
5933
+ const abs = path4.resolve(baseDir, cleaned);
5534
5934
  if (escapesProjectRoot(baseDir, abs))
5535
5935
  continue;
5536
5936
  let bytes;
5537
5937
  try {
5538
- bytes = await readFile11(abs);
5938
+ bytes = await readFile12(abs);
5539
5939
  } catch {
5540
5940
  continue;
5541
5941
  }
5542
- const mime = mimeFor(path3.extname(abs).toLowerCase());
5942
+ const mime = mimeFor(path4.extname(abs).toLowerCase());
5543
5943
  const dataUri2 = `data:${mime};base64,${bytes.toString("base64")}`;
5544
5944
  replacements.set(m[0], `--gp-shape:url(&quot;${dataUri2}&quot;)`);
5545
5945
  }
@@ -5629,7 +6029,7 @@ var check20 = {
5629
6029
  const collectRenderedLocalRefs = createRenderedLocalRefCollector(plugins);
5630
6030
  for (const file of files) {
5631
6031
  try {
5632
- const content = await readFile12(file, "utf8");
6032
+ const content = await readFile13(file, "utf8");
5633
6033
  for (const { ref, kind, line } of collectRenderedLocalRefs(content)) {
5634
6034
  if (isNonFilesystemRef(ref))
5635
6035
  continue;
@@ -5671,15 +6071,15 @@ function localRefExists(ref, kind, sourceFile, projectRoot) {
5671
6071
  const rootRelative = resolve6(projectRoot, cleaned);
5672
6072
  switch (kind) {
5673
6073
  case "image":
5674
- return existsSync7(rootRelative);
6074
+ return existsSync8(rootRelative);
5675
6075
  case "link":
5676
- return existsSync7(fileRelative);
6076
+ return existsSync8(fileRelative);
5677
6077
  }
5678
6078
  }
5679
6079
  registerCheck(check20);
5680
6080
 
5681
6081
  // src/checks/source/accessibility-alt-text.ts
5682
- import { readFile as readFile13 } from "node:fs/promises";
6082
+ import { readFile as readFile14 } from "node:fs/promises";
5683
6083
  var check21 = {
5684
6084
  id: "source.accessibility.alt-text",
5685
6085
  name: "Image Alt Text",
@@ -5693,7 +6093,7 @@ var check21 = {
5693
6093
  const results = [];
5694
6094
  for (const file of files) {
5695
6095
  try {
5696
- const content = await readFile13(file, "utf8");
6096
+ const content = await readFile14(file, "utf8");
5697
6097
  const lines = content.split(`
5698
6098
  `);
5699
6099
  let inFence = false;
@@ -5737,7 +6137,7 @@ function extractImageAlts(line) {
5737
6137
  registerCheck(check21);
5738
6138
 
5739
6139
  // src/checks/source/accessibility-heading-order.ts
5740
- import { readFile as readFile14 } from "node:fs/promises";
6140
+ import { readFile as readFile15 } from "node:fs/promises";
5741
6141
  var check22 = {
5742
6142
  id: "source.accessibility.heading-order",
5743
6143
  name: "Heading Order",
@@ -5751,7 +6151,7 @@ var check22 = {
5751
6151
  const results = [];
5752
6152
  for (const file of files) {
5753
6153
  try {
5754
- const content = await readFile14(file, "utf8");
6154
+ const content = await readFile15(file, "utf8");
5755
6155
  const lines = content.split(`
5756
6156
  `);
5757
6157
  let inFence = false;
@@ -5787,7 +6187,7 @@ var check22 = {
5787
6187
  registerCheck(check22);
5788
6188
 
5789
6189
  // src/checks/source/layout-markers.ts
5790
- import { readFile as readFile15 } from "node:fs/promises";
6190
+ import { readFile as readFile16 } from "node:fs/promises";
5791
6191
  var check23 = {
5792
6192
  id: "source.markdown.layout-markers",
5793
6193
  name: "Layout Markers",
@@ -5805,7 +6205,7 @@ var check23 = {
5805
6205
  const md = createMarkdownRenderer(plugins);
5806
6206
  for (const file of files) {
5807
6207
  try {
5808
- const content = await readFile15(file, "utf8");
6208
+ const content = await readFile16(file, "utf8");
5809
6209
  const env = {};
5810
6210
  md.render(content, env);
5811
6211
  for (const w of env.layoutWarnings ?? []) {
@@ -5827,8 +6227,8 @@ var check23 = {
5827
6227
  registerCheck(check23);
5828
6228
 
5829
6229
  // src/checks/source/merge-markers.ts
5830
- import { readFile as readFile16 } from "node:fs/promises";
5831
- import path4 from "node:path";
6230
+ import { readFile as readFile17 } from "node:fs/promises";
6231
+ import path5 from "node:path";
5832
6232
 
5833
6233
  // src/checks/asset/extensions.ts
5834
6234
  var RASTER_INSPECTABLE_EXTS = [
@@ -5869,7 +6269,7 @@ var check24 = {
5869
6269
  for (const file of textFiles) {
5870
6270
  let content;
5871
6271
  try {
5872
- content = await readFile16(file, "utf8");
6272
+ content = await readFile17(file, "utf8");
5873
6273
  } catch {
5874
6274
  results.push(inspectionFailed(check24.id, `Could not read source file: ${file}`, { file }));
5875
6275
  continue;
@@ -5912,7 +6312,7 @@ var check24 = {
5912
6312
  ignore: [...ASSET_SCAN_IGNORE_GLOBS]
5913
6313
  });
5914
6314
  for (const sibling of siblings.sort()) {
5915
- const rel = path4.relative(ctx.inputDir, sibling).split(path4.sep).join("/");
6315
+ const rel = path5.relative(ctx.inputDir, sibling).split(path5.sep).join("/");
5916
6316
  results.push(finding(check24.id, {
5917
6317
  severity: "warning",
5918
6318
  code: "kept-both-versions",
@@ -5932,27 +6332,27 @@ registerCheck(check24);
5932
6332
  import { stat as stat3 } from "node:fs/promises";
5933
6333
 
5934
6334
  // src/lib/image-inspect.ts
5935
- import { stat as stat2, readFile as readFile17 } from "node:fs/promises";
6335
+ import { stat as stat2, readFile as readFile18 } from "node:fs/promises";
5936
6336
  var DEFAULT_DPI = 72;
5937
6337
  var cache = new Map;
5938
- async function inspectImage(path5) {
6338
+ async function inspectImage(path6) {
5939
6339
  let st;
5940
6340
  try {
5941
- st = await stat2(path5);
6341
+ st = await stat2(path6);
5942
6342
  } catch {
5943
6343
  return null;
5944
6344
  }
5945
- const hit = cache.get(path5);
6345
+ const hit = cache.get(path6);
5946
6346
  if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size)
5947
6347
  return hit.info;
5948
6348
  let info2 = null;
5949
6349
  try {
5950
- const buf = await readFile17(path5);
6350
+ const buf = await readFile18(path6);
5951
6351
  info2 = parseImage(buf);
5952
6352
  } catch {
5953
6353
  info2 = null;
5954
6354
  }
5955
- cache.set(path5, { mtimeMs: st.mtimeMs, size: st.size, info: info2 });
6355
+ cache.set(path6, { mtimeMs: st.mtimeMs, size: st.size, info: info2 });
5956
6356
  return info2;
5957
6357
  }
5958
6358
  function parseImage(b) {
@@ -6406,7 +6806,7 @@ var check30 = {
6406
6806
  registerCheck(check30);
6407
6807
 
6408
6808
  // src/checks/asset/font-license.ts
6409
- import { existsSync as existsSync8 } from "node:fs";
6809
+ import { existsSync as existsSync9 } from "node:fs";
6410
6810
  import { resolve as resolve7 } from "node:path";
6411
6811
  var LICENSE_NAMES = [
6412
6812
  "LICENSE",
@@ -6442,7 +6842,7 @@ var check31 = {
6442
6842
  }
6443
6843
  const results = [];
6444
6844
  for (const fontDir of fontDirs) {
6445
- const hasLicense = LICENSE_NAMES.some((name) => existsSync8(resolve7(fontDir, name)));
6845
+ const hasLicense = LICENSE_NAMES.some((name) => existsSync9(resolve7(fontDir, name)));
6446
6846
  if (!hasLicense) {
6447
6847
  results.push({
6448
6848
  checkId: check31.id,
@@ -6507,7 +6907,7 @@ var check32 = {
6507
6907
  registerCheck(check32);
6508
6908
 
6509
6909
  // src/checks/heuristic/section-density.ts
6510
- import { readFile as readFile18 } from "node:fs/promises";
6910
+ import { readFile as readFile19 } from "node:fs/promises";
6511
6911
  var check33 = {
6512
6912
  id: "heuristic.chunking.section-density",
6513
6913
  name: "Section Density",
@@ -6524,7 +6924,7 @@ var check33 = {
6524
6924
  const results = [];
6525
6925
  for (const file of files) {
6526
6926
  try {
6527
- const content = await readFile18(file, "utf8");
6927
+ const content = await readFile19(file, "utf8");
6528
6928
  const lines = content.split(`
6529
6929
  `);
6530
6930
  let currentSectionStart = 0;
@@ -6689,11 +7089,11 @@ function reportMissingTools(result) {
6689
7089
  }
6690
7090
 
6691
7091
  // src/lib/validation-exec.ts
6692
- import { existsSync as existsSync9 } from "node:fs";
7092
+ import { existsSync as existsSync10 } from "node:fs";
6693
7093
  import { dirname as dirname4, join as join3, resolve as resolve8 } from "node:path";
6694
7094
 
6695
7095
  // src/lib/output-paths.ts
6696
- import path5 from "node:path";
7096
+ import path6 from "node:path";
6697
7097
 
6698
7098
  // src/lib/slug.ts
6699
7099
  function slugify(name, fallback = "") {
@@ -6711,7 +7111,7 @@ function bookSlug(title) {
6711
7111
  return slugify(title ?? "", "book");
6712
7112
  }
6713
7113
  function resolveOutputDir(manifestDir, title) {
6714
- return path5.resolve(manifestDir, DIST_DIRNAME, bookSlug(title));
7114
+ return path6.resolve(manifestDir, DIST_DIRNAME, bookSlug(title));
6715
7115
  }
6716
7116
  function artifactName(title, format) {
6717
7117
  return `${bookSlug(title)}-${format}.pdf`;
@@ -6721,7 +7121,7 @@ function artifactName(title, format) {
6721
7121
  var BOOK_HTML_FILENAME = "book.html";
6722
7122
 
6723
7123
  // src/lib/markdown/index.ts
6724
- import { readdir as readdir3, readFile as readFile19, writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
7124
+ import { readdir as readdir3, readFile as readFile20, writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
6725
7125
 
6726
7126
  // src/lib/remote-auth/sync-messages.ts
6727
7127
  function isOnlineSibling(filepath) {
@@ -6849,7 +7249,7 @@ async function renderChapters(inputDir, opts = {}) {
6849
7249
  }
6850
7250
  return assembleBookHtml({
6851
7251
  files,
6852
- readText: (relPath) => readFile19(join2(inputDir, canonicalChapterId(relPath)), "utf-8"),
7252
+ readText: (relPath) => readFile20(join2(inputDir, canonicalChapterId(relPath)), "utf-8"),
6853
7253
  projectCss: inlined.css,
6854
7254
  preloadImages: [...pluginInlined.copies.map((c) => c.to), ...inlined.copies.map((c) => c.to)],
6855
7255
  title: opts.title,
@@ -7214,7 +7614,7 @@ async function executeValidation(args) {
7214
7614
  const targetIds = resolveTargets(parseCsv(typeof args.target === "string" ? args.target : undefined), config.targets);
7215
7615
  const pdfPath = typeof args.pdf === "string" ? resolve8(args.pdf) : undefined;
7216
7616
  const inputDir = typeof args.input === "string" ? resolve8(args.input) : undefined;
7217
- if (pdfPath && !existsSync9(pdfPath)) {
7617
+ if (pdfPath && !existsSync10(pdfPath)) {
7218
7618
  throw new Error(`File not found: ${pdfPath}`);
7219
7619
  }
7220
7620
  const categories = resolveCategoryArg(typeof args.category === "string" ? args.category : undefined);
@@ -7245,10 +7645,15 @@ async function executeValidation(args) {
7245
7645
  const relMarkdown = await resolveActiveMarkdownFiles(manifestDir, config.source.files);
7246
7646
  markdownFiles = relMarkdown.map((f) => join3(manifestDir, canonicalChapterId(f)));
7247
7647
  const relStyles = await resolveActiveStyles(manifestDir, config.styles);
7248
- cssFiles = relStyles.map((rel) => resolve8(manifestDir, rel)).filter((f) => !f.endsWith(".min.css"));
7648
+ const projectCssFiles = relStyles.map((rel) => resolve8(manifestDir, rel));
7649
+ let pluginStylePaths = args.pluginStylePaths;
7650
+ if (pluginStylePaths === undefined) {
7651
+ ({ pluginStylePaths } = await loadPluginsWithCss(config.plugins, manifestDir, (ref, err) => log.warn(`Skipping plugin "${ref}" for validation — ${err.message}`)));
7652
+ }
7653
+ cssFiles = [...projectCssFiles, ...pluginStylePaths].filter((f) => !f.endsWith(".min.css"));
7249
7654
  assetDirs = [inputDir, ...await sharedAssetDirs(manifestDir, relStyles, inputDir)];
7250
7655
  const possibleHtml = join3(outDir, BOOK_HTML_FILENAME);
7251
- if (existsSync9(possibleHtml)) {
7656
+ if (existsSync10(possibleHtml)) {
7252
7657
  htmlPath = possibleHtml;
7253
7658
  }
7254
7659
  }
@@ -7341,7 +7746,7 @@ function buildPdfSummaryLines(results) {
7341
7746
  }
7342
7747
 
7343
7748
  // src/lib/build-runner.ts
7344
- import path8 from "node:path";
7749
+ import path9 from "node:path";
7345
7750
  import fs2 from "node:fs";
7346
7751
  import fsp2 from "node:fs/promises";
7347
7752
  import os2 from "node:os";
@@ -7349,9 +7754,9 @@ import { randomBytes } from "node:crypto";
7349
7754
 
7350
7755
  // src/lib/build-fingerprint.ts
7351
7756
  import * as fs from "node:fs";
7352
- import { mkdir as mkdir3, readFile as readFile20, writeFile as writeFile3 } from "node:fs/promises";
7757
+ import { mkdir as mkdir3, readFile as readFile21, writeFile as writeFile3 } from "node:fs/promises";
7353
7758
  import { createHash as createHash4 } from "node:crypto";
7354
- import path6 from "node:path";
7759
+ import path7 from "node:path";
7355
7760
  import git from "isomorphic-git";
7356
7761
  var FINGERPRINT_FILENAME = "build-fingerprint.json";
7357
7762
  var VERSION_TIMEOUT_MS = 4000;
@@ -7400,7 +7805,7 @@ async function getGitRevision(sourceDir) {
7400
7805
  const candidateDirs = [sourceDir, process.cwd()].filter((v) => Boolean(v));
7401
7806
  const seen = new Set;
7402
7807
  for (const dir of candidateDirs) {
7403
- const abs = path6.resolve(dir);
7808
+ const abs = path7.resolve(dir);
7404
7809
  if (seen.has(abs))
7405
7810
  continue;
7406
7811
  seen.add(abs);
@@ -7436,8 +7841,8 @@ async function getEngineBundleHash() {
7436
7841
  getAssetPath("engine/gutterpress-agent.js")
7437
7842
  ]);
7438
7843
  const [viewerBytes, agentBytes] = await Promise.all([
7439
- readFile20(viewerPath),
7440
- readFile20(agentPath)
7844
+ readFile21(viewerPath),
7845
+ readFile21(agentPath)
7441
7846
  ]);
7442
7847
  const hash = createHash4("sha256");
7443
7848
  hash.update(viewerBytes);
@@ -7485,8 +7890,8 @@ function sanitizeArgs(args) {
7485
7890
  return out;
7486
7891
  }
7487
7892
  async function writeBuildFingerprint(input) {
7488
- const outputDir = path6.resolve(input.outputDir);
7489
- const outPath = path6.join(outputDir, FINGERPRINT_FILENAME);
7893
+ const outputDir = path7.resolve(input.outputDir);
7894
+ const outPath = path7.join(outputDir, FINGERPRINT_FILENAME);
7490
7895
  const [tools, sourceRevision] = await Promise.all([
7491
7896
  getToolVersions(),
7492
7897
  getGitRevision(input.sourceDir)
@@ -7502,7 +7907,7 @@ async function writeBuildFingerprint(input) {
7502
7907
  iccPath: input.pdfx.iccPath,
7503
7908
  stripAnnotations: input.pdfx.stripAnnotations
7504
7909
  },
7505
- outputDir: input.recordedOutputDir ? path6.resolve(input.recordedOutputDir) : outputDir
7910
+ outputDir: input.recordedOutputDir ? path7.resolve(input.recordedOutputDir) : outputDir
7506
7911
  },
7507
7912
  sourceRevision,
7508
7913
  tools
@@ -7578,7 +7983,7 @@ function computeGates(format, opts, config) {
7578
7983
  }
7579
7984
 
7580
7985
  // src/lib/build-staging.ts
7581
- import path7 from "node:path";
7986
+ import path8 from "node:path";
7582
7987
  import os from "node:os";
7583
7988
  import fsp from "node:fs/promises";
7584
7989
 
@@ -7761,8 +8166,8 @@ function placeholderPng(width = 640, height = 480, cell = 32) {
7761
8166
 
7762
8167
  // src/lib/build-staging.ts
7763
8168
  async function shipViewerHtml(htmlFile, outDir) {
7764
- await fsp.mkdir(path7.join(outDir, "engine"), { recursive: true });
7765
- await fsp.copyFile(await getAssetPath("engine/gutterpress-viewer.js"), path7.join(outDir, "engine/gutterpress-viewer.js"));
8169
+ await fsp.mkdir(path8.join(outDir, "engine"), { recursive: true });
8170
+ await fsp.copyFile(await getAssetPath("engine/gutterpress-viewer.js"), path8.join(outDir, "engine/gutterpress-viewer.js"));
7766
8171
  const tag = ` <script src="engine/gutterpress-viewer.js"></script>
7767
8172
  `;
7768
8173
  const html = await fsp.readFile(htmlFile, "utf-8");
@@ -7791,18 +8196,18 @@ async function stageBookAssets(options) {
7791
8196
  return { missing: [...missingPlaceholders.keys()].sort() };
7792
8197
  }
7793
8198
  async function copyReferencedAssets(copies, outDir) {
7794
- const dirs = new Set(copies.map((c) => path7.dirname(path7.resolve(outDir, c.to))));
8199
+ const dirs = new Set(copies.map((c) => path8.dirname(path8.resolve(outDir, c.to))));
7795
8200
  await Promise.all([...dirs].map((d) => fsp.mkdir(d, { recursive: true })));
7796
8201
  const missing = new Map;
7797
8202
  await Promise.all(copies.map(async (c) => {
7798
- const dest = path7.resolve(outDir, c.to);
8203
+ const dest = path8.resolve(outDir, c.to);
7799
8204
  try {
7800
8205
  await fsp.copyFile(c.from, dest);
7801
8206
  } catch (err) {
7802
8207
  if (err?.code === "ENOENT") {
7803
8208
  const placeholder = placeholderOutputPath(c.to);
7804
- const placeholderDest = path7.resolve(outDir, placeholder);
7805
- await fsp.mkdir(path7.dirname(placeholderDest), { recursive: true });
8209
+ const placeholderDest = path8.resolve(outDir, placeholder);
8210
+ await fsp.mkdir(path8.dirname(placeholderDest), { recursive: true });
7806
8211
  await fsp.writeFile(placeholderDest, placeholderPng());
7807
8212
  missing.set(c.to, placeholder);
7808
8213
  return;
@@ -7813,7 +8218,7 @@ async function copyReferencedAssets(copies, outDir) {
7813
8218
  return missing;
7814
8219
  }
7815
8220
  async function createStageRoot() {
7816
- return fsp.mkdtemp(path7.join(os.tmpdir(), "gutterpress-stage-"));
8221
+ return fsp.mkdtemp(path8.join(os.tmpdir(), "gutterpress-stage-"));
7817
8222
  }
7818
8223
 
7819
8224
  // src/lib/build-runner.ts
@@ -7821,18 +8226,18 @@ function splitOutPath(outArg, format) {
7821
8226
  if (typeof outArg !== "string" || outArg.length === 0) {
7822
8227
  return { outDir: undefined, pdfFileOverride: null };
7823
8228
  }
7824
- const resolved = path8.resolve(outArg);
8229
+ const resolved = path9.resolve(outArg);
7825
8230
  if ((format === "pdf" || format === "pdfx") && resolved.toLowerCase().endsWith(".pdf")) {
7826
- return { outDir: path8.dirname(resolved), pdfFileOverride: resolved };
8231
+ return { outDir: path9.dirname(resolved), pdfFileOverride: resolved };
7827
8232
  }
7828
8233
  return { outDir: resolved, pdfFileOverride: null };
7829
8234
  }
7830
8235
  function layoutWarningKey(file, line, message) {
7831
- return `${path8.resolve(file)}\x00${line ?? 0}\x00${message}`;
8236
+ return `${path9.resolve(file)}\x00${line ?? 0}\x00${message}`;
7832
8237
  }
7833
8238
  async function resolveBuildContext(opts) {
7834
8239
  const { format } = opts;
7835
- const inputDir = path8.resolve(opts.inputDir);
8240
+ const inputDir = path9.resolve(opts.inputDir);
7836
8241
  const { manifest, manifestDir, manifestPath } = await loadManifestWithPath(opts.manifestPath ?? inputDir, { explicit: opts.manifestPath !== undefined });
7837
8242
  if (manifestPath === null) {
7838
8243
  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>`.");
@@ -7847,10 +8252,10 @@ async function resolveBuildContext(opts) {
7847
8252
  pdfx: pdfxConfigOverride,
7848
8253
  engine: opts.engine
7849
8254
  }, manifest);
7850
- 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) };
7851
- const outDir = target.kind === "file" ? path8.dirname(target.file) : target.dir;
8255
+ 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) };
8256
+ const outDir = target.kind === "file" ? path9.dirname(target.file) : target.dir;
7852
8257
  const gates = computeGates(format, opts, config);
7853
- 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")}`);
8258
+ 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")}`);
7854
8259
  return {
7855
8260
  opts,
7856
8261
  format,
@@ -7862,15 +8267,32 @@ async function resolveBuildContext(opts) {
7862
8267
  manifestDir,
7863
8268
  config,
7864
8269
  gates,
7865
- prevalidatedLayoutWarningKeys: new Set
8270
+ prevalidatedLayoutWarningKeys: new Set,
8271
+ plugins: null
7866
8272
  };
7867
8273
  }
8274
+ async function loadBuildPlugins(ctx) {
8275
+ if (ctx.plugins)
8276
+ return ctx.plugins;
8277
+ const { config, renderDir } = ctx;
8278
+ if (config.plugins.length > 0) {
8279
+ log.info(`Loading ${config.plugins.length} plugin(s)...`);
8280
+ }
8281
+ const loaded = await loadPluginsWithCss(config.plugins, renderDir);
8282
+ if (loaded.plugins && loaded.plugins.length > 0) {
8283
+ log.success(`Loaded ${loaded.plugins.length} plugin(s)`);
8284
+ }
8285
+ ctx.plugins = loaded;
8286
+ return loaded;
8287
+ }
7868
8288
  async function runQualityGates(ctx) {
7869
8289
  const { gates, opts, renderDir } = ctx;
8290
+ const pluginStylePaths = gates.lint || gates.preValidate ? (await loadBuildPlugins(ctx)).pluginStylePaths : undefined;
7870
8291
  if (gates.lint) {
7871
8292
  log.info("Lint: CSS print-safety");
7872
8293
  const lintResult = await runLint({
7873
- manifest: opts.manifestPath ?? renderDir
8294
+ manifest: opts.manifestPath ?? renderDir,
8295
+ pluginStylePaths
7874
8296
  });
7875
8297
  if (!lintResult.ok) {
7876
8298
  throw new BuildError("CSS lint failed", 2);
@@ -7881,7 +8303,8 @@ async function runQualityGates(ctx) {
7881
8303
  const result = await executeAndReport({
7882
8304
  input: renderDir,
7883
8305
  phase: "pre-build",
7884
- manifest: opts.manifestPath
8306
+ manifest: opts.manifestPath,
8307
+ pluginStylePaths
7885
8308
  }, "text");
7886
8309
  for (const finding2 of result.execution.report.results) {
7887
8310
  if (finding2.checkId === "source.markdown.layout-markers" && finding2.file && finding2.code !== "inspect-failed") {
@@ -7900,13 +8323,7 @@ async function renderBook(ctx) {
7900
8323
  } else {
7901
8324
  log.info("Using all .md files in alphabetical order");
7902
8325
  }
7903
- if (config.plugins.length > 0) {
7904
- log.info(`Loading ${config.plugins.length} plugin(s)...`);
7905
- }
7906
- const { plugins, pluginCss, pluginStylePaths } = await loadPluginsWithCss(config.plugins, renderDir);
7907
- if (plugins && plugins.length > 0) {
7908
- log.success(`Loaded ${plugins.length} plugin(s)`);
7909
- }
8326
+ const { plugins, pluginCss, pluginStylePaths } = await loadBuildPlugins(ctx);
7910
8327
  const imageRefs = [];
7911
8328
  const cssAssets = [];
7912
8329
  const htmlFile = await renderChaptersToFile(renderDir, workDir, {
@@ -7918,7 +8335,7 @@ async function renderBook(ctx) {
7918
8335
  pluginStylePaths,
7919
8336
  onChapterWarnings: (file, warnings) => {
7920
8337
  for (const w of warnings) {
7921
- const key = layoutWarningKey(path8.resolve(renderDir, file), w.line, w.message);
8338
+ const key = layoutWarningKey(path9.resolve(renderDir, file), w.line, w.message);
7922
8339
  if (!ctx.prevalidatedLayoutWarningKeys.has(key)) {
7923
8340
  log.warn(` ${file}, line ${w.line}: ${w.message}`);
7924
8341
  }
@@ -7956,9 +8373,9 @@ async function renderBook(ctx) {
7956
8373
  return htmlFile;
7957
8374
  }
7958
8375
  async function resolveIccProfile(icc, manifestDir, explicitIccPath) {
7959
- const iccCandidates = path8.isAbsolute(icc) ? [icc] : [path8.resolve(manifestDir, icc), path8.resolve(icc)];
8376
+ const iccCandidates = path9.isAbsolute(icc) ? [icc] : [path9.resolve(manifestDir, icc), path9.resolve(icc)];
7960
8377
  let effectiveIccPath = iccCandidates.find((candidate) => fs2.existsSync(candidate)) ?? null;
7961
- if (!effectiveIccPath && !explicitIccPath && path8.basename(icc) === "CGATS21_CRPC1.icc") {
8378
+ if (!effectiveIccPath && !explicitIccPath && path9.basename(icc) === "CGATS21_CRPC1.icc") {
7962
8379
  effectiveIccPath = await getAssetPath("profiles/CGATS21_CRPC1.icc");
7963
8380
  }
7964
8381
  if (!effectiveIccPath) {
@@ -7974,13 +8391,13 @@ async function finalizeBuild(ctx, fingerprint, wroteMessage, paths, artifactName
7974
8391
  });
7975
8392
  await publishBuild(ctx, artifactName2);
7976
8393
  const delivered = ctx.target.kind !== "file";
7977
- const fingerprintPath = delivered ? path8.join(ctx.outDir, path8.basename(workFingerprint)) : null;
8394
+ const fingerprintPath = delivered ? path9.join(ctx.outDir, path9.basename(workFingerprint)) : null;
7978
8395
  log.success(wroteMessage);
7979
8396
  if (fingerprintPath)
7980
8397
  log.info(`Fingerprint: ${fingerprintPath}`);
7981
8398
  return {
7982
8399
  outDir: ctx.outDir,
7983
- htmlPath: delivered ? path8.join(ctx.outDir, path8.relative(ctx.workDir, paths.htmlPath)) : null,
8400
+ htmlPath: delivered ? path9.join(ctx.outDir, path9.relative(ctx.workDir, paths.htmlPath)) : null,
7984
8401
  pdfPath: paths.pdfPath,
7985
8402
  fingerprintPath,
7986
8403
  diagnostics
@@ -7991,8 +8408,8 @@ async function publishBuild(ctx, artifactName2) {
7991
8408
  if (target.kind === "file") {
7992
8409
  if (!artifactName2)
7993
8410
  throw new BuildError("No artifact to write for this format", 1);
7994
- await fsp2.mkdir(path8.dirname(target.file), { recursive: true });
7995
- await fsp2.copyFile(path8.join(workDir, artifactName2), target.file);
8411
+ await fsp2.mkdir(path9.dirname(target.file), { recursive: true });
8412
+ await fsp2.copyFile(path9.join(workDir, artifactName2), target.file);
7996
8413
  return;
7997
8414
  }
7998
8415
  if (target.kind === "directory") {
@@ -8001,12 +8418,12 @@ async function publishBuild(ctx, artifactName2) {
8001
8418
  return;
8002
8419
  }
8003
8420
  for (const name of await listPdfs(target.dir)) {
8004
- if (!fs2.existsSync(path8.join(workDir, name))) {
8005
- await fsp2.rename(path8.join(target.dir, name), path8.join(workDir, name));
8421
+ if (!fs2.existsSync(path9.join(workDir, name))) {
8422
+ await fsp2.rename(path9.join(target.dir, name), path9.join(workDir, name));
8006
8423
  }
8007
8424
  }
8008
8425
  await fsp2.rm(target.dir, { recursive: true, force: true });
8009
- await fsp2.mkdir(path8.dirname(target.dir), { recursive: true });
8426
+ await fsp2.mkdir(path9.dirname(target.dir), { recursive: true });
8010
8427
  await fsp2.rename(workDir, target.dir);
8011
8428
  }
8012
8429
  async function listPdfs(dir) {
@@ -8022,7 +8439,7 @@ class HtmlOutput {
8022
8439
  const { workDir, outDir, inputDir, config, opts } = ctx;
8023
8440
  log.info("Shipping self-contained HTML + viewer bundle (native engine)");
8024
8441
  await shipViewerHtml(htmlFile, workDir);
8025
- 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>
8442
+ 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>
8026
8443
  `, "utf-8");
8027
8444
  return finalizeBuild(ctx, {
8028
8445
  command: "build",
@@ -8035,7 +8452,7 @@ class HtmlOutput {
8035
8452
  iccPath: null,
8036
8453
  stripAnnotations: null
8037
8454
  }
8038
- }, `Wrote: ${path8.join(outDir, BOOK_HTML)}`, { htmlPath: htmlFile, pdfPath: null });
8455
+ }, `Wrote: ${path9.join(outDir, BOOK_HTML)}`, { htmlPath: htmlFile, pdfPath: null });
8039
8456
  }
8040
8457
  }
8041
8458
 
@@ -8044,14 +8461,14 @@ class PdfOutput {
8044
8461
  const { workDir, outDir, inputDir, config, opts, format, manifestDir, gates } = ctx;
8045
8462
  const pdfxMode = format === "pdfx" ? opts.pdfxFlavor ?? config.pdfx.flavor : undefined;
8046
8463
  const pdfName = artifactName(config.title, pdfxMode ? "pdfx" : "pdf");
8047
- const pdfFile = path8.join(workDir, pdfName);
8048
- const reportedPdf = ctx.target.kind === "file" ? ctx.target.file : path8.join(outDir, pdfName);
8464
+ const pdfFile = path9.join(workDir, pdfName);
8465
+ const reportedPdf = ctx.target.kind === "file" ? ctx.target.file : path9.join(outDir, pdfName);
8049
8466
  const stage = await createStageRoot();
8050
8467
  try {
8051
- const rawPdf = pdfxMode ? path8.join(stage, "raw.pdf") : path8.resolve(pdfFile);
8052
- await fsp2.mkdir(path8.dirname(path8.resolve(pdfFile)), { recursive: true });
8468
+ const rawPdf = pdfxMode ? path9.join(stage, "raw.pdf") : path9.resolve(pdfFile);
8469
+ await fsp2.mkdir(path9.dirname(path9.resolve(pdfFile)), { recursive: true });
8053
8470
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
8054
- const { buildNativePdf } = await import("./engine-pb6h089e.js");
8471
+ const { buildNativePdf } = await import("./engine-daad33qn.js");
8055
8472
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
8056
8473
  title: config.title,
8057
8474
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
@@ -8082,7 +8499,7 @@ class PdfOutput {
8082
8499
  await stripAnnotations(rawPdf, stage);
8083
8500
  }
8084
8501
  log.info(`Converting to CMYK PDF/X (${pdfxMode})`);
8085
- await convertToPdfxCmyk(rawPdf, path8.resolve(pdfFile), {
8502
+ await convertToPdfxCmyk(rawPdf, path9.resolve(pdfFile), {
8086
8503
  iccPath: effectiveIccPath,
8087
8504
  pdfx: pdfxMode,
8088
8505
  title: config.title,
@@ -8171,10 +8588,10 @@ function openPath(filePath) {
8171
8588
  }
8172
8589
 
8173
8590
  // src/preview/lifecycle.ts
8174
- import path10 from "path";
8591
+ import path11 from "path";
8175
8592
  import { tmpdir as tmpdir2 } from "os";
8176
8593
  import { randomBytes as randomBytes2 } from "crypto";
8177
- import { readdir as readdir4, readFile as readFile21, writeFile as writeFile4 } from "node:fs/promises";
8594
+ import { readdir as readdir4, readFile as readFile22, writeFile as writeFile4 } from "node:fs/promises";
8178
8595
 
8179
8596
  // src/utils/file-utils.ts
8180
8597
  import { promises as fs3 } from "fs";
@@ -8196,7 +8613,7 @@ async function mkdir4(dirPath) {
8196
8613
  // src/preview/file-watcher.ts
8197
8614
  import { watch } from "chokidar";
8198
8615
  import fsp3 from "node:fs/promises";
8199
- import path9 from "path";
8616
+ import path10 from "path";
8200
8617
  var EMPTY_BOOK_HTML = `<!doctype html>
8201
8618
  <html lang="en">
8202
8619
  <head>
@@ -8250,7 +8667,7 @@ function injectPreviewScripts(html, pageIsolateChapters) {
8250
8667
  async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets) {
8251
8668
  if (!inputPath) {
8252
8669
  cssAssets.clear();
8253
- await fsp3.writeFile(path9.join(tempDir, BOOK_HTML_FILENAME), EMPTY_BOOK_HTML, "utf-8");
8670
+ await fsp3.writeFile(path10.join(tempDir, BOOK_HTML_FILENAME), EMPTY_BOOK_HTML, "utf-8");
8254
8671
  return;
8255
8672
  }
8256
8673
  const nextAssets = new Map;
@@ -8265,7 +8682,7 @@ async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets) {
8265
8682
  cssAssets.clear();
8266
8683
  for (const [to, from] of nextAssets)
8267
8684
  cssAssets.set(to, from);
8268
- await fsp3.writeFile(path9.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html, false), "utf-8");
8685
+ await fsp3.writeFile(path10.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html, false), "utf-8");
8269
8686
  }
8270
8687
  async function renderChapterPreviewHtml(inputPath, file, config) {
8271
8688
  const html = await renderPreviewBook(inputPath, config, {
@@ -8277,12 +8694,12 @@ async function renderChapterPreviewHtml(inputPath, file, config) {
8277
8694
  function describeChanges(changes, inputResolved) {
8278
8695
  const files = [];
8279
8696
  for (const [changedPath, event] of changes) {
8280
- const relative2 = path9.relative(inputResolved, path9.resolve(changedPath));
8281
- if (relative2 === "" || relative2 === ".." || relative2.startsWith(`..${path9.sep}`) || path9.isAbsolute(relative2))
8697
+ const relative2 = path10.relative(inputResolved, path10.resolve(changedPath));
8698
+ if (relative2 === "" || relative2 === ".." || relative2.startsWith(`..${path10.sep}`) || path10.isAbsolute(relative2))
8282
8699
  continue;
8283
8700
  files.push({
8284
8701
  relativePath: canonicalChapterId(relative2),
8285
- ext: path9.extname(changedPath).toLowerCase(),
8702
+ ext: path10.extname(changedPath).toLowerCase(),
8286
8703
  event
8287
8704
  });
8288
8705
  }
@@ -8334,23 +8751,23 @@ function isIgnoredWatchPath(candidatePath, projectRoot) {
8334
8751
  return isDotPathUnderRoot(candidatePath, projectRoot) || isGeneratedProjectPath(relative2);
8335
8752
  }
8336
8753
  function isExternalWatchCandidate(candidatePath, targets) {
8337
- const candidate = path9.resolve(candidatePath);
8754
+ const candidate = path10.resolve(candidatePath);
8338
8755
  for (const rawTarget of targets) {
8339
- const target = path9.resolve(rawTarget);
8340
- const rel = path9.relative(candidate, target);
8341
- if (rel === "" || rel !== ".." && !rel.startsWith(`..${path9.sep}`) && !path9.isAbsolute(rel))
8756
+ const target = path10.resolve(rawTarget);
8757
+ const rel = path10.relative(candidate, target);
8758
+ if (rel === "" || rel !== ".." && !rel.startsWith(`..${path10.sep}`) && !path10.isAbsolute(rel))
8342
8759
  return true;
8343
8760
  }
8344
8761
  return false;
8345
8762
  }
8346
8763
  async function nearestExistingDirectory(start) {
8347
- let current = path9.resolve(start);
8764
+ let current = path10.resolve(start);
8348
8765
  while (true) {
8349
8766
  try {
8350
8767
  if ((await fsp3.stat(current)).isDirectory())
8351
8768
  return current;
8352
8769
  } catch {}
8353
- const parent = path9.dirname(current);
8770
+ const parent = path10.dirname(current);
8354
8771
  if (parent === current)
8355
8772
  return current;
8356
8773
  current = parent;
@@ -8359,12 +8776,12 @@ async function nearestExistingDirectory(start) {
8359
8776
  async function externalWatchRoots(targets) {
8360
8777
  const roots = new Set;
8361
8778
  for (const target of targets) {
8362
- roots.add(await nearestExistingDirectory(path9.dirname(path9.resolve(target))));
8779
+ roots.add(await nearestExistingDirectory(path10.dirname(path10.resolve(target))));
8363
8780
  }
8364
8781
  return [...roots];
8365
8782
  }
8366
8783
  async function externalWatchTargets(projectDir, config) {
8367
- const root = path9.resolve(projectDir);
8784
+ const root = path10.resolve(projectDir);
8368
8785
  let canonicalRoot = root;
8369
8786
  try {
8370
8787
  canonicalRoot = await fsp3.realpath(root);
@@ -8372,21 +8789,21 @@ async function externalWatchTargets(projectDir, config) {
8372
8789
  const styles = await resolveActiveStyles(root, config.styles);
8373
8790
  const candidates = [
8374
8791
  ...await collectStyleDependencies(root, styles),
8375
- ...(config.plugins ?? []).map((p) => p.path).filter((p) => !!p).map((p) => path9.resolve(root, p))
8792
+ ...(config.plugins ?? []).map((p) => p.path).filter((p) => !!p).map((p) => path10.resolve(root, p))
8376
8793
  ];
8377
8794
  const external = new Set;
8378
8795
  for (const abs of candidates) {
8379
- const authoredPath = path9.resolve(abs);
8380
- const authoredRel = path9.relative(root, authoredPath);
8381
- if (authoredRel !== "" && (authoredRel === ".." || authoredRel.startsWith(`..${path9.sep}`) || path9.isAbsolute(authoredRel))) {
8796
+ const authoredPath = path10.resolve(abs);
8797
+ const authoredRel = path10.relative(root, authoredPath);
8798
+ if (authoredRel !== "" && (authoredRel === ".." || authoredRel.startsWith(`..${path10.sep}`) || path10.isAbsolute(authoredRel))) {
8382
8799
  external.add(authoredPath);
8383
8800
  }
8384
8801
  let canonicalPath = authoredPath;
8385
8802
  try {
8386
8803
  canonicalPath = await fsp3.realpath(canonicalPath);
8387
8804
  } catch {}
8388
- const rel = path9.relative(canonicalRoot, canonicalPath);
8389
- if (rel === "" || rel !== ".." && !rel.startsWith(`..${path9.sep}`) && !path9.isAbsolute(rel))
8805
+ const rel = path10.relative(canonicalRoot, canonicalPath);
8806
+ if (rel === "" || rel !== ".." && !rel.startsWith(`..${path10.sep}`) && !path10.isAbsolute(rel))
8390
8807
  continue;
8391
8808
  external.add(canonicalPath);
8392
8809
  }
@@ -8413,7 +8830,7 @@ async function addAndAwaitWatch(watcher, roots) {
8413
8830
  }
8414
8831
  }
8415
8832
  function createFileWatcher(state) {
8416
- const inputResolved = path9.resolve(state.currentInputPath);
8833
+ const inputResolved = path10.resolve(state.currentInputPath);
8417
8834
  const watcher = watch(inputResolved, {
8418
8835
  persistent: true,
8419
8836
  ignoreInitial: true,
@@ -8453,7 +8870,7 @@ function createFileWatcher(state) {
8453
8870
  async function recordChange(event, filePath) {
8454
8871
  if (closed)
8455
8872
  return;
8456
- const target = path9.resolve(filePath);
8873
+ const target = path10.resolve(filePath);
8457
8874
  const settled = settledWrites.get(target);
8458
8875
  if (settled && event !== "unlink" && Date.now() <= settled.expiresAt) {
8459
8876
  const diskContent = await fsp3.readFile(target, "utf8").catch(() => null);
@@ -8474,7 +8891,7 @@ function createFileWatcher(state) {
8474
8891
  recordChange(event, filePath);
8475
8892
  });
8476
8893
  externalWatcher.on("all", (event, filePath) => {
8477
- const target = path9.resolve(filePath);
8894
+ const target = path10.resolve(filePath);
8478
8895
  if (!desiredExternals.has(target))
8479
8896
  return;
8480
8897
  if (event === "add" && suppressInitialExternalAdds.delete(target))
@@ -8482,8 +8899,8 @@ function createFileWatcher(state) {
8482
8899
  recordChange(event, target);
8483
8900
  });
8484
8901
  function isWatchedSource(target) {
8485
- const rel = path9.relative(inputResolved, target);
8486
- const inProject = rel === "" || rel !== ".." && !rel.startsWith(`..${path9.sep}`) && !path9.isAbsolute(rel);
8902
+ const rel = path10.relative(inputResolved, target);
8903
+ const inProject = rel === "" || rel !== ".." && !rel.startsWith(`..${path10.sep}`) && !path10.isAbsolute(rel);
8487
8904
  if (inProject)
8488
8905
  return !isIgnoredWatchPath(target, inputResolved);
8489
8906
  return desiredExternals.has(target);
@@ -8508,7 +8925,7 @@ function createFileWatcher(state) {
8508
8925
  state.notifySettledWrite = (filePath, writtenContent) => {
8509
8926
  if (closed)
8510
8927
  return;
8511
- const target = path9.resolve(filePath);
8928
+ const target = path10.resolve(filePath);
8512
8929
  if (isWatchedSource(target)) {
8513
8930
  acceptSettledWrite(target, writtenContent);
8514
8931
  return;
@@ -8529,7 +8946,7 @@ function createFileWatcher(state) {
8529
8946
  return;
8530
8947
  let nextTargets;
8531
8948
  try {
8532
- nextTargets = new Set((await externalWatchTargets(inputResolved, state.config)).map((target) => path9.resolve(target)));
8949
+ nextTargets = new Set((await externalWatchTargets(inputResolved, state.config)).map((target) => path10.resolve(target)));
8533
8950
  } catch (err) {
8534
8951
  debug(`Could not resolve external watch targets: ${err}`);
8535
8952
  return;
@@ -8661,7 +9078,7 @@ async function stopFileWatcher(state) {
8661
9078
  }
8662
9079
 
8663
9080
  // src/preview/lifecycle.ts
8664
- var TEMP_DIR_BASE = path10.join(tmpdir2(), "gutterpress-preview");
9081
+ var TEMP_DIR_BASE = path11.join(tmpdir2(), "gutterpress-preview");
8665
9082
  var PID_FILE_NAME = ".gutterpress.pid";
8666
9083
  function isProcessAlive(pid) {
8667
9084
  if (!Number.isFinite(pid) || pid <= 0)
@@ -8681,10 +9098,10 @@ async function cleanupOrphanTempDirs() {
8681
9098
  return;
8682
9099
  }
8683
9100
  for (const entry of entries) {
8684
- const dir = path10.join(TEMP_DIR_BASE, entry);
8685
- const pidFile = path10.join(dir, PID_FILE_NAME);
9101
+ const dir = path11.join(TEMP_DIR_BASE, entry);
9102
+ const pidFile = path11.join(dir, PID_FILE_NAME);
8686
9103
  try {
8687
- const raw = await readFile21(pidFile, "utf8");
9104
+ const raw = await readFile22(pidFile, "utf8");
8688
9105
  const pid = parseInt(raw.trim(), 10);
8689
9106
  if (!isProcessAlive(pid)) {
8690
9107
  debug(`Removing orphan temp dir ${dir} (pid ${pid} not alive)`);
@@ -8696,10 +9113,10 @@ async function cleanupOrphanTempDirs() {
8696
9113
  async function initializePreviewDirectories() {
8697
9114
  await cleanupOrphanTempDirs();
8698
9115
  const tempDirSuffix = randomBytes2(8).toString("hex");
8699
- const tempDir = path10.join(TEMP_DIR_BASE, tempDirSuffix);
9116
+ const tempDir = path11.join(TEMP_DIR_BASE, tempDirSuffix);
8700
9117
  await mkdir4(tempDir);
8701
9118
  debug(`Created temporary directory: ${tempDir}`);
8702
- await writeFile4(path10.join(tempDir, PID_FILE_NAME), `${process.pid}
9119
+ await writeFile4(path11.join(tempDir, PID_FILE_NAME), `${process.pid}
8703
9120
  `, "utf8");
8704
9121
  return tempDir;
8705
9122
  }
@@ -8790,12 +9207,12 @@ function createServerState(inputPath, tempDir, config, options) {
8790
9207
  import http from "node:http";
8791
9208
  import net from "node:net";
8792
9209
  import { createHash as createHash6, randomUUID } from "node:crypto";
8793
- import { readFile as readFile22, stat as stat4 } from "node:fs/promises";
8794
- import path12 from "path";
9210
+ import { readFile as readFile23, stat as stat4 } from "node:fs/promises";
9211
+ import path13 from "path";
8795
9212
  import { WebSocket, WebSocketServer } from "ws";
8796
9213
 
8797
9214
  // src/lib/static-serve.ts
8798
- import path11 from "node:path";
9215
+ import path12 from "node:path";
8799
9216
  var STATIC_MIME = {
8800
9217
  ".html": "text/html; charset=utf-8",
8801
9218
  ".htm": "text/html; charset=utf-8",
@@ -8817,9 +9234,9 @@ var STATIC_MIME = {
8817
9234
  ".otf": "font/otf"
8818
9235
  };
8819
9236
  function resolveWithinRoot(relPath, root) {
8820
- const resolvedRoot = path11.resolve(root);
8821
- const candidate = path11.resolve(resolvedRoot, "." + (relPath.startsWith("/") ? relPath : "/" + relPath));
8822
- if (candidate !== resolvedRoot && !candidate.startsWith(resolvedRoot + path11.sep)) {
9237
+ const resolvedRoot = path12.resolve(root);
9238
+ const candidate = path12.resolve(resolvedRoot, "." + (relPath.startsWith("/") ? relPath : "/" + relPath));
9239
+ if (candidate !== resolvedRoot && !candidate.startsWith(resolvedRoot + path12.sep)) {
8823
9240
  return null;
8824
9241
  }
8825
9242
  return candidate;
@@ -9072,7 +9489,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
9072
9489
  try {
9073
9490
  const s = await stat4(filePath);
9074
9491
  if (s.isDirectory())
9075
- filePath = path12.join(filePath, "index.html");
9492
+ filePath = path13.join(filePath, "index.html");
9076
9493
  } catch {
9077
9494
  res.writeHead(404);
9078
9495
  res.end("Not Found");
@@ -9080,7 +9497,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
9080
9497
  }
9081
9498
  let data;
9082
9499
  try {
9083
- data = await readFile22(filePath);
9500
+ data = await readFile23(filePath);
9084
9501
  } catch {
9085
9502
  res.writeHead(404);
9086
9503
  res.end("Not Found");
@@ -9093,7 +9510,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
9093
9510
  res.end(withHmr);
9094
9511
  return;
9095
9512
  }
9096
- const ct = STATIC_MIME[path12.extname(filePath).toLowerCase()] ?? "application/octet-stream";
9513
+ const ct = STATIC_MIME[path13.extname(filePath).toLowerCase()] ?? "application/octet-stream";
9097
9514
  res.writeHead(200, { "Content-Type": ct, "Cache-Control": cacheControl, ...extraHeaders });
9098
9515
  res.end(data);
9099
9516
  }
@@ -9103,7 +9520,7 @@ var EMBEDDED_CACHE_CONTROL = "public, no-cache";
9103
9520
  var EMBEDDED_ETAG = `"gutterpress-${PACKAGE_VERSION}"`;
9104
9521
  var PROJECT_ASSET_CACHE_CONTROL = "private, no-cache";
9105
9522
  async function serveRevalidatedStatic(req, absPath, res, validators) {
9106
- const extension = path12.extname(absPath).toLowerCase();
9523
+ const extension = path13.extname(absPath).toLowerCase();
9107
9524
  if (extension === ".html" || extension === ".htm") {
9108
9525
  await serveStatic(absPath, res);
9109
9526
  return;
@@ -9130,7 +9547,7 @@ async function serveRevalidatedStatic(req, absPath, res, validators) {
9130
9547
  let data;
9131
9548
  if (!validator || validator.stamp !== stamp) {
9132
9549
  try {
9133
- data = await readFile22(absPath);
9550
+ data = await readFile23(absPath);
9134
9551
  } catch {
9135
9552
  await serveStatic(absPath, res);
9136
9553
  return;
@@ -9269,7 +9686,7 @@ async function createPreviewServer(state, port) {
9269
9686
  const file = rawFile ? canonicalChapterId(rawFile) : "";
9270
9687
  const revision = Number(url.searchParams.get("revision"));
9271
9688
  const configuredFiles = state.config.source?.files;
9272
- 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)) {
9689
+ 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)) {
9273
9690
  res.writeHead(400);
9274
9691
  res.end("Bad Request");
9275
9692
  return;
@@ -9469,9 +9886,9 @@ async function startPreviewServer(options) {
9469
9886
  }
9470
9887
 
9471
9888
  // src/lib/theme-manager.ts
9472
- import { cp as cp2, mkdir as mkdir6, readFile as readFile24, readdir as readdir5, stat as stat5, writeFile as writeFile5, rm as rm3 } from "node:fs/promises";
9473
- import { existsSync as existsSync11 } from "node:fs";
9474
- import path14 from "node:path";
9889
+ import { cp as cp2, mkdir as mkdir6, readFile as readFile25, readdir as readdir5, stat as stat5, writeFile as writeFile5, rm as rm3 } from "node:fs/promises";
9890
+ import { existsSync as existsSync12 } from "node:fs";
9891
+ import path15 from "node:path";
9475
9892
  import { isSeq as isSeq2, Scalar } from "yaml";
9476
9893
 
9477
9894
  // src/lib/fetch-timeout.ts
@@ -9508,20 +9925,20 @@ async function withFetchTimeout(options, run) {
9508
9925
  }
9509
9926
 
9510
9927
  // src/lib/manifest-doc.ts
9511
- import { existsSync as existsSync10 } from "node:fs";
9928
+ import { existsSync as existsSync11 } from "node:fs";
9512
9929
  import { randomUUID as randomUUID2 } from "node:crypto";
9513
- import { mkdir as mkdir5, open, readFile as readFile23, rename, rm as rm2 } from "node:fs/promises";
9514
- import path13 from "node:path";
9930
+ import { mkdir as mkdir5, open, readFile as readFile24, rename, rm as rm2 } from "node:fs/promises";
9931
+ import path14 from "node:path";
9515
9932
  import { parseDocument, isSeq, YAMLSeq } from "yaml";
9516
9933
  function resolveManifestPath(projectDir) {
9517
- const existing = MANIFEST_FILENAMES.find((name) => existsSync10(path13.join(projectDir, name)));
9518
- return path13.join(projectDir, existing ?? MANIFEST_FILENAMES[0]);
9934
+ const existing = MANIFEST_FILENAMES.find((name) => existsSync11(path14.join(projectDir, name)));
9935
+ return path14.join(projectDir, existing ?? MANIFEST_FILENAMES[0]);
9519
9936
  }
9520
9937
  async function loadManifestDoc(projectDir) {
9521
9938
  const file = resolveManifestPath(projectDir);
9522
9939
  let text = "";
9523
9940
  try {
9524
- text = await readFile23(file, "utf8");
9941
+ text = await readFile24(file, "utf8");
9525
9942
  } catch (error2) {
9526
9943
  if (error2.code !== "ENOENT")
9527
9944
  throw error2;
@@ -9529,9 +9946,9 @@ async function loadManifestDoc(projectDir) {
9529
9946
  return { doc: parseDocument(text), file };
9530
9947
  }
9531
9948
  async function writeManifestDoc(file, doc) {
9532
- const dir = path13.dirname(file);
9949
+ const dir = path14.dirname(file);
9533
9950
  await mkdir5(dir, { recursive: true });
9534
- const temporary = path13.join(dir, `.${path13.basename(file)}.tmp-${process.pid}-${randomUUID2()}`);
9951
+ const temporary = path14.join(dir, `.${path14.basename(file)}.tmp-${process.pid}-${randomUUID2()}`);
9535
9952
  try {
9536
9953
  const handle = await open(temporary, "wx", 438);
9537
9954
  try {
@@ -9597,29 +10014,13 @@ var BUILT_IN_FALLBACK_META = {
9597
10014
  description: "Clean sans-serif manual with clear hierarchy and code styling."
9598
10015
  }
9599
10016
  };
9600
- async function readThemeMeta(jsonPath) {
9601
- try {
9602
- const text = await readFile24(jsonPath, "utf8");
9603
- const parsed = JSON.parse(text);
9604
- return parsed && typeof parsed === "object" ? parsed : {};
9605
- } catch {
9606
- return {};
9607
- }
9608
- }
10017
+ var readThemeMeta = readExtensionMeta;
9609
10018
  function themeStyleList(meta) {
9610
- const declared = Array.isArray(meta.styles) ? meta.styles.filter((s) => typeof s === "string" && s.trim().length > 0) : [];
10019
+ const declared = extensionStyleList(meta);
9611
10020
  return declared.length > 0 ? declared : ["theme.css"];
9612
10021
  }
9613
- function assertThemeSheetsContained(meta) {
9614
- for (const rel of [...themeStyleList(meta), ...themeEngineStyleList(meta)]) {
9615
- if (path14.isAbsolute(rel) || rel.split(/[\\/]/).includes("..")) {
9616
- throw new Error(`theme.json declares stylesheet "${rel}" outside the theme folder; a theme must be self-contained.`);
9617
- }
9618
- }
9619
- }
9620
- function themeEngineStyleList(meta) {
9621
- return Array.isArray(meta.engineStyles?.native) ? meta.engineStyles.native.filter((s) => typeof s === "string" && s.trim().length > 0) : [];
9622
- }
10022
+ var assertThemeSheetsContained = assertExtensionContained;
10023
+ var themeEngineStyleList = extensionEngineStyleList;
9623
10024
  function themeInfo(id, kind, meta) {
9624
10025
  const styles = themeStyleList(meta);
9625
10026
  return {
@@ -9630,7 +10031,10 @@ function themeInfo(id, kind, meta) {
9630
10031
  kind,
9631
10032
  preview: meta.preview ?? null,
9632
10033
  styles,
9633
- tokensFile: meta.tokensFile?.trim() || styles[0]
10034
+ tokensFile: meta.tokensFile?.trim() || styles[0],
10035
+ markdown: meta.markdown?.trim() || undefined,
10036
+ components: meta.components?.trim() || undefined,
10037
+ snippets: meta.snippets?.trim() || undefined
9634
10038
  };
9635
10039
  }
9636
10040
  function themeSlug(name) {
@@ -9640,14 +10044,14 @@ function themeDirFor(projectDir, id) {
9640
10044
  if (typeof id !== "string" || id.length === 0 || themeSlug(id) !== id) {
9641
10045
  throw new Error(`Invalid theme id "${id}".`);
9642
10046
  }
9643
- return path14.join(projectDir, THEMES_DIR, id);
10047
+ return path15.join(projectDir, THEMES_DIR, id);
9644
10048
  }
9645
10049
  async function listBuiltInThemes() {
9646
10050
  const out = [];
9647
10051
  for (const id of BUILT_IN_THEME_IDS) {
9648
10052
  let meta = {};
9649
10053
  try {
9650
- meta = await readThemeMeta(await getAssetPath(`${THEMES_DIR}/${id}/theme.json`));
10054
+ meta = await readThemeMeta(path15.join(await getAssetsDir(), THEMES_DIR, id));
9651
10055
  } catch {
9652
10056
  meta = {};
9653
10057
  }
@@ -9665,20 +10069,19 @@ async function resolveBuiltInTheme(id) {
9665
10069
  throw new Error(`Unknown built-in theme: "${id}".`);
9666
10070
  }
9667
10071
  const cssPath = await getAssetPath(`${THEMES_DIR}/${id}/theme.css`);
9668
- const jsonPath = await getAssetPath(`${THEMES_DIR}/${id}/theme.json`);
9669
- if (!existsSync11(cssPath)) {
10072
+ if (!existsSync12(cssPath)) {
9670
10073
  throw new Error(`Built-in theme "${id}" is missing its theme.css.`);
9671
10074
  }
9672
- const meta = await readThemeMeta(jsonPath);
10075
+ const meta = await readThemeMeta(path15.join(await getAssetsDir(), THEMES_DIR, id));
9673
10076
  if (!meta.name)
9674
10077
  meta.name = BUILT_IN_FALLBACK_META[id]?.name;
9675
10078
  if (!meta.description) {
9676
10079
  meta.description = BUILT_IN_FALLBACK_META[id]?.description;
9677
10080
  }
9678
- return { info: themeInfo(id, "builtin", meta), cssPath, dir: path14.dirname(cssPath) };
10081
+ return { info: themeInfo(id, "builtin", meta), cssPath, dir: path15.dirname(cssPath) };
9679
10082
  }
9680
10083
  async function listProjectThemes(projectDir) {
9681
- const root = path14.join(projectDir, THEMES_DIR);
10084
+ const root = path15.join(projectDir, THEMES_DIR);
9682
10085
  let entries;
9683
10086
  try {
9684
10087
  entries = await readdir5(root, { withFileTypes: true });
@@ -9689,10 +10092,10 @@ async function listProjectThemes(projectDir) {
9689
10092
  for (const entry of entries) {
9690
10093
  if (!entry.isDirectory())
9691
10094
  continue;
9692
- const dir = path14.join(root, entry.name);
9693
- const meta = await readThemeMeta(path14.join(dir, "theme.json"));
10095
+ const dir = path15.join(root, entry.name);
10096
+ const meta = await readThemeMeta(dir);
9694
10097
  const primary = themeStyleList(meta)[0];
9695
- if (!existsSync11(path14.join(dir, primary)))
10098
+ if (!existsSync12(path15.join(dir, primary)))
9696
10099
  continue;
9697
10100
  out.push(themeInfo(entry.name, "project", meta));
9698
10101
  }
@@ -9743,10 +10146,10 @@ async function getActiveTheme(projectDir) {
9743
10146
  if (!m)
9744
10147
  continue;
9745
10148
  const id = m[1];
9746
- const dir = path14.join(projectDir, THEMES_DIR, id);
9747
- const meta = await readThemeMeta(path14.join(dir, "theme.json"));
10149
+ const dir = path15.join(projectDir, THEMES_DIR, id);
10150
+ const meta = await readThemeMeta(dir);
9748
10151
  const primary = themeStyleList(meta)[0];
9749
- if (!existsSync11(path14.join(dir, primary)))
10152
+ if (!existsSync12(path15.join(dir, primary)))
9750
10153
  continue;
9751
10154
  return themeInfo(id, "project", meta);
9752
10155
  }
@@ -9784,9 +10187,9 @@ async function getPreviousTheme(projectDir) {
9784
10187
  if (!id)
9785
10188
  return null;
9786
10189
  const dir = themeDirFor(projectDir, id);
9787
- const meta = await readThemeMeta(path14.join(dir, "theme.json"));
10190
+ const meta = await readThemeMeta(dir);
9788
10191
  const primary = themeStyleList(meta)[0];
9789
- if (!existsSync11(path14.join(dir, primary)))
10192
+ if (!existsSync12(path15.join(dir, primary)))
9790
10193
  return null;
9791
10194
  const active = await getActiveTheme(projectDir);
9792
10195
  if (active && active.id === id)
@@ -9806,23 +10209,23 @@ async function applyTheme(projectDir, target) {
9806
10209
  if (target.kind === "builtin") {
9807
10210
  const resolved = await resolveBuiltInTheme(target.id);
9808
10211
  let destId = target.id;
9809
- if (existsSync11(path14.join(projectDir, THEMES_DIR, destId, "theme.css"))) {
10212
+ if (existsSync12(path15.join(projectDir, THEMES_DIR, destId, "theme.css"))) {
9810
10213
  destId = await uniqueThemeId(projectDir, target.id);
9811
10214
  }
9812
- const destDir = path14.join(projectDir, THEMES_DIR, destId);
10215
+ const destDir = path15.join(projectDir, THEMES_DIR, destId);
9813
10216
  await mkdir6(destDir, { recursive: true });
9814
10217
  await cp2(resolved.dir, destDir, { recursive: true });
9815
- meta = await readThemeMeta(path14.join(destDir, "theme.json"));
10218
+ meta = await readThemeMeta(destDir);
9816
10219
  assertThemeSheetsContained(meta);
9817
10220
  resolveDeclaredStyles(themeStyleList(meta), destDir, `Theme "${destId}"`);
9818
10221
  resolveDeclaredStyles(themeEngineStyleList(meta), destDir, `Theme "${destId}"`);
9819
10222
  info2 = themeInfo(destId, "project", meta);
9820
10223
  } else {
9821
10224
  const dir = themeDirFor(projectDir, target.id);
9822
- meta = await readThemeMeta(path14.join(dir, "theme.json"));
10225
+ meta = await readThemeMeta(dir);
9823
10226
  assertThemeSheetsContained(meta);
9824
10227
  const primary = themeStyleList(meta)[0];
9825
- if (!existsSync11(path14.join(dir, primary))) {
10228
+ if (!existsSync12(path15.join(dir, primary))) {
9826
10229
  throw new Error(`Theme "${target.id}" is not present in this project.`);
9827
10230
  }
9828
10231
  resolveDeclaredStyles(themeStyleList(meta), dir, `Theme "${target.id}"`);
@@ -9833,10 +10236,10 @@ async function applyTheme(projectDir, target) {
9833
10236
  return info2;
9834
10237
  }
9835
10238
  async function uniqueThemeId(projectDir, base) {
9836
- const root = path14.join(projectDir, THEMES_DIR);
10239
+ const root = path15.join(projectDir, THEMES_DIR);
9837
10240
  let id = themeSlug(base);
9838
10241
  let n = 2;
9839
- while (existsSync11(path14.join(root, id))) {
10242
+ while (existsSync12(path15.join(root, id))) {
9840
10243
  id = `${themeSlug(base)}-${n++}`;
9841
10244
  }
9842
10245
  return id;
@@ -9851,20 +10254,20 @@ async function importThemeFromFolder(projectDir, sourceDir) {
9851
10254
  if (!info2.isDirectory()) {
9852
10255
  throw new Error("The chosen path is not a folder.");
9853
10256
  }
9854
- const meta = await readThemeMeta(path14.join(sourceDir, "theme.json"));
10257
+ const meta = await readThemeMeta(sourceDir);
9855
10258
  assertThemeSheetsContained(meta);
9856
10259
  const primary = themeStyleList(meta)[0];
9857
- if (!existsSync11(path14.join(sourceDir, primary))) {
10260
+ if (!existsSync12(path15.join(sourceDir, primary))) {
9858
10261
  throw new Error(primary === "theme.css" ? "A theme folder must contain a theme.css file." : `A theme folder must contain its declared stylesheet: ${primary}`);
9859
10262
  }
9860
10263
  resolveDeclaredStyles(themeStyleList(meta), sourceDir, "The theme folder");
9861
10264
  resolveDeclaredStyles(themeEngineStyleList(meta), sourceDir, "The theme folder");
9862
- const base = meta.name || path14.basename(sourceDir);
10265
+ const base = meta.name || path15.basename(sourceDir);
9863
10266
  const id = await uniqueThemeId(projectDir, base);
9864
- const destDir = path14.join(projectDir, THEMES_DIR, id);
9865
- await mkdir6(path14.dirname(destDir), { recursive: true });
10267
+ const destDir = path15.join(projectDir, THEMES_DIR, id);
10268
+ await mkdir6(path15.dirname(destDir), { recursive: true });
9866
10269
  await cp2(sourceDir, destDir, { recursive: true });
9867
- return themeInfo(id, "project", await readThemeMeta(path14.join(destDir, "theme.json")));
10270
+ return themeInfo(id, "project", await readThemeMeta(destDir));
9868
10271
  }
9869
10272
  function looksLikeCssUrl(url) {
9870
10273
  try {
@@ -9911,7 +10314,7 @@ async function importThemeFromUrl(projectDir, url) {
9911
10314
  let baseName;
9912
10315
  if (looksLikeCssUrl(trimmed)) {
9913
10316
  css = await fetchText(trimmed);
9914
- baseName = path14.basename(new URL(trimmed, "https://x/").pathname, ".css") || "theme";
10317
+ baseName = path15.basename(new URL(trimmed, "https://x/").pathname, ".css") || "theme";
9915
10318
  } else {
9916
10319
  const base = trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
9917
10320
  css = await fetchText(`${base}theme.css`);
@@ -9921,41 +10324,41 @@ async function importThemeFromUrl(projectDir, url) {
9921
10324
  if (parsed && typeof parsed === "object")
9922
10325
  meta = parsed;
9923
10326
  } catch {}
9924
- baseName = meta.name || path14.basename(new URL(base, "https://x/").pathname.replace(/\/$/, "")) || "theme";
10327
+ baseName = meta.name || path15.basename(new URL(base, "https://x/").pathname.replace(/\/$/, "")) || "theme";
9925
10328
  }
9926
10329
  if (!css.trim()) {
9927
10330
  throw new Error(`Fetched theme CSS from ${trimmed} was empty.`);
9928
10331
  }
9929
10332
  assertLooksLikeCss(css, trimmed);
9930
10333
  const id = await uniqueThemeId(projectDir, meta.name || baseName);
9931
- const destDir = path14.join(projectDir, THEMES_DIR, id);
10334
+ const destDir = path15.join(projectDir, THEMES_DIR, id);
9932
10335
  await mkdir6(destDir, { recursive: true });
9933
- await writeFile5(path14.join(destDir, "theme.css"), css, "utf8");
10336
+ await writeFile5(path15.join(destDir, "theme.css"), css, "utf8");
9934
10337
  const finalMeta = {
9935
10338
  name: meta.name || prettify(id),
9936
10339
  author: meta.author,
9937
10340
  description: meta.description,
9938
10341
  preview: meta.preview ?? null
9939
10342
  };
9940
- await writeFile5(path14.join(destDir, "theme.json"), JSON.stringify(finalMeta, null, 2), "utf8");
10343
+ await writeFile5(path15.join(destDir, "theme.json"), JSON.stringify(finalMeta, null, 2), "utf8");
9941
10344
  return themeInfo(id, "project", finalMeta);
9942
10345
  }
9943
10346
  async function readThemeCss(projectDir, source) {
9944
10347
  if (source.kind === "builtin") {
9945
10348
  const resolved = await resolveBuiltInTheme(source.id);
9946
- return readFile24(resolved.cssPath, "utf8");
10349
+ return readFile25(resolved.cssPath, "utf8");
9947
10350
  }
9948
10351
  if (!projectDir)
9949
10352
  throw new Error("A project is required to read a project theme.");
9950
10353
  const dir = themeDirFor(projectDir, source.id);
9951
- const meta = await readThemeMeta(path14.join(dir, "theme.json"));
10354
+ const meta = await readThemeMeta(dir);
9952
10355
  const sheets = themeStyleList(meta);
9953
10356
  const parts = [];
9954
10357
  for (const rel of sheets) {
9955
- const sheetPath = path14.join(dir, rel);
9956
- if (!existsSync11(sheetPath))
10358
+ const sheetPath = path15.join(dir, rel);
10359
+ if (!existsSync12(sheetPath))
9957
10360
  continue;
9958
- parts.push(await readFile24(sheetPath, "utf8"));
10361
+ parts.push(await readFile25(sheetPath, "utf8"));
9959
10362
  }
9960
10363
  if (parts.length === 0) {
9961
10364
  throw new Error(`Theme "${source.id}" has no ${sheets[0]} in this project.`);
@@ -9966,7 +10369,7 @@ async function readThemeCss(projectDir, source) {
9966
10369
  }
9967
10370
  async function removeProjectTheme(projectDir, id) {
9968
10371
  const dir = themeDirFor(projectDir, id);
9969
- if (existsSync11(dir)) {
10372
+ if (existsSync12(dir)) {
9970
10373
  await rm3(dir, { recursive: true, force: true });
9971
10374
  }
9972
10375
  const { doc, file } = await loadManifestDoc(projectDir);
@@ -10004,7 +10407,7 @@ async function detectLegacyForkedTheme(projectDir, active) {
10004
10407
  return null;
10005
10408
  let bookCss;
10006
10409
  try {
10007
- bookCss = await readFile24(path14.join(projectDir, "styles", "book.css"), "utf8");
10410
+ bookCss = await readFile25(path15.join(projectDir, "styles", "book.css"), "utf8");
10008
10411
  } catch {
10009
10412
  return null;
10010
10413
  }
@@ -10024,9 +10427,9 @@ async function detectLegacyForkedTheme(projectDir, active) {
10024
10427
  }
10025
10428
 
10026
10429
  // src/lib/project-scaffold.ts
10027
- import { access, copyFile, cp as cp3, mkdir as mkdir7, readdir as readdir6, readFile as readFile25, stat as stat6, writeFile as writeFile6 } from "node:fs/promises";
10028
- import { constants as FS, existsSync as existsSync12 } from "node:fs";
10029
- import path15 from "node:path";
10430
+ import { access, copyFile, cp as cp3, mkdir as mkdir7, readdir as readdir6, readFile as readFile26, stat as stat6, writeFile as writeFile6 } from "node:fs/promises";
10431
+ import { constants as FS, existsSync as existsSync13 } from "node:fs";
10432
+ import path16 from "node:path";
10030
10433
  var STARTER_THEME_FOR_TEMPLATE = {
10031
10434
  book: "clean-book",
10032
10435
  zine: "zine",
@@ -10106,7 +10509,7 @@ async function scaffoldProject(options) {
10106
10509
  throw new CreateProjectErrorImpl("invalid-name", `Could not derive a valid folder name from "${name}".`);
10107
10510
  }
10108
10511
  const parentDir = options.parentDir;
10109
- if (!parentDir || !path15.isAbsolute(parentDir)) {
10512
+ if (!parentDir || !path16.isAbsolute(parentDir)) {
10110
10513
  throw new CreateProjectErrorImpl("parent-not-writable", "The save location must be an absolute path.");
10111
10514
  }
10112
10515
  try {
@@ -10114,7 +10517,7 @@ async function scaffoldProject(options) {
10114
10517
  } catch {
10115
10518
  throw new CreateProjectErrorImpl("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
10116
10519
  }
10117
- const projectDir = path15.join(parentDir, folderName);
10520
+ const projectDir = path16.join(parentDir, folderName);
10118
10521
  let targetExists = true;
10119
10522
  try {
10120
10523
  await access(projectDir, FS.F_OK);
@@ -10136,17 +10539,17 @@ async function scaffoldProject(options) {
10136
10539
  for (const entry of entries) {
10137
10540
  if (entry.name === ".gutterpress-template.json")
10138
10541
  continue;
10139
- await cp3(path15.join(customTemplateDir, entry.name), path15.join(projectDir, entry.name), { recursive: true });
10542
+ await cp3(path16.join(customTemplateDir, entry.name), path16.join(projectDir, entry.name), { recursive: true });
10140
10543
  }
10141
- await mkdir7(path15.join(projectDir, "assets"), { recursive: true });
10544
+ await mkdir7(path16.join(projectDir, "assets"), { recursive: true });
10142
10545
  } else {
10143
10546
  const tplManifest = await getAssetPath(`templates/${template}/manifest.yaml`);
10144
10547
  const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
10145
- await mkdir7(path15.join(projectDir, "assets"), { recursive: true });
10146
- await copyFile(tplManifest, path15.join(projectDir, MANIFEST_FILENAMES[0]));
10147
- await copyFile(tplChapter, path15.join(projectDir, "chapter-01.md"));
10148
- await mkdir7(path15.join(projectDir, "styles"), { recursive: true });
10149
- await writeFile6(path15.join(projectDir, "styles", "book.css"), starterOverridesCss(starterThemeId), "utf8");
10548
+ await mkdir7(path16.join(projectDir, "assets"), { recursive: true });
10549
+ await copyFile(tplManifest, path16.join(projectDir, MANIFEST_FILENAMES[0]));
10550
+ await copyFile(tplChapter, path16.join(projectDir, "chapter-01.md"));
10551
+ await mkdir7(path16.join(projectDir, "styles"), { recursive: true });
10552
+ await writeFile6(path16.join(projectDir, "styles", "book.css"), starterOverridesCss(starterThemeId), "utf8");
10150
10553
  }
10151
10554
  } catch (e) {
10152
10555
  throw new CreateProjectErrorImpl("scaffold-io", `Could not create the project files: ${e instanceof Error ? e.message : String(e)}`);
@@ -10156,13 +10559,13 @@ async function scaffoldProject(options) {
10156
10559
  } catch (e) {
10157
10560
  throw new CreateProjectErrorImpl("scaffold-io", `Could not write .gitignore: ${e instanceof Error ? e.message : String(e)}`);
10158
10561
  }
10159
- const manifestPath = MANIFEST_FILENAMES.map((name2) => path15.join(projectDir, name2)).find(existsSync12) ?? path15.join(projectDir, MANIFEST_FILENAMES[0]);
10562
+ const manifestPath = MANIFEST_FILENAMES.map((name2) => path16.join(projectDir, name2)).find(existsSync13) ?? path16.join(projectDir, MANIFEST_FILENAMES[0]);
10160
10563
  const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
10161
10564
  const substitutions = {
10162
10565
  "{{TITLE}}": escapeYamlScalar(name),
10163
10566
  "{{AUTHOR}}": escapeYamlScalar(author)
10164
10567
  };
10165
- let openFile = path15.join(projectDir, "chapter-01.md");
10568
+ let openFile = path16.join(projectDir, "chapter-01.md");
10166
10569
  try {
10167
10570
  await fillTemplateFile(manifestPath, substitutions);
10168
10571
  if (!customTemplateDir) {
@@ -10186,7 +10589,7 @@ async function scaffoldProject(options) {
10186
10589
  }
10187
10590
  const firstSource = await firstSourceFile(manifestPath);
10188
10591
  if (firstSource)
10189
- openFile = path15.join(projectDir, firstSource);
10592
+ openFile = path16.join(projectDir, firstSource);
10190
10593
  await fillTemplateFile(openFile, { "{{TITLE}}": name }).catch(() => {});
10191
10594
  } catch (e) {
10192
10595
  throw new CreateProjectErrorImpl("scaffold-io", `Could not finalise the project files: ${e instanceof Error ? e.message : String(e)}`);
@@ -10208,7 +10611,7 @@ async function scaffoldProject(options) {
10208
10611
  versionHistory = "none";
10209
10612
  const { findEnclosingRepoDir } = await import("./project-source-ekcyp63q.js");
10210
10613
  const enclosingRepoDir = await findEnclosingRepoDir(projectDir);
10211
- versionHistoryError = enclosingRepoDir ? `This book was added to ${path15.basename(enclosingRepoDir)}'s shared version history; save a snapshot to record it.` : e instanceof Error ? e.message : String(e);
10614
+ 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);
10212
10615
  }
10213
10616
  }
10214
10617
  const result = {
@@ -10223,10 +10626,10 @@ async function scaffoldProject(options) {
10223
10626
  return result;
10224
10627
  }
10225
10628
  async function ensureGitignoreHasDist(projectDir) {
10226
- const gitignorePath = path15.join(projectDir, ".gitignore");
10629
+ const gitignorePath = path16.join(projectDir, ".gitignore");
10227
10630
  let existing = null;
10228
10631
  try {
10229
- existing = await readFile25(gitignorePath, "utf8");
10632
+ existing = await readFile26(gitignorePath, "utf8");
10230
10633
  } catch {}
10231
10634
  if (existing === null) {
10232
10635
  await writeFile6(gitignorePath, `dist/
@@ -10246,7 +10649,7 @@ async function ensureGitignoreHasDist(projectDir) {
10246
10649
  async function firstSourceFile(manifestPath) {
10247
10650
  let text;
10248
10651
  try {
10249
- text = await readFile25(manifestPath, "utf8");
10652
+ text = await readFile26(manifestPath, "utf8");
10250
10653
  } catch {
10251
10654
  return;
10252
10655
  }
@@ -10258,13 +10661,158 @@ async function firstSourceFile(manifestPath) {
10258
10661
  return m ? m[1].trim() : undefined;
10259
10662
  }
10260
10663
  async function fillTemplateFile(filePath, substitutions) {
10261
- let text = await readFile25(filePath, "utf8");
10664
+ let text = await readFile26(filePath, "utf8");
10262
10665
  for (const [token, value] of Object.entries(substitutions)) {
10263
10666
  text = text.split(token).join(value);
10264
10667
  }
10265
10668
  await writeFile6(filePath, text, "utf8");
10266
10669
  }
10267
10670
 
10671
+ // src/lib/extension-scaffold.ts
10672
+ import { access as access2, mkdir as mkdir8, readFile as readFile27, writeFile as writeFile7 } from "node:fs/promises";
10673
+ import { constants as FS2 } from "node:fs";
10674
+ import path17 from "node:path";
10675
+ var EXTENSION_KINDS = ["plugin", "theme"];
10676
+ var RESERVED_PREFIX = "gp-";
10677
+ var TEMPLATE_FILES = {
10678
+ plugin: [
10679
+ ["gutterpress.json", "gutterpress.json"],
10680
+ ["package.json", "package.json"],
10681
+ ["README.md", "README.md"],
10682
+ ["plugin.js.tpl", "plugin.js"],
10683
+ ["styles/plugin.css", "styles/plugin.css"],
10684
+ ["snippets/term-box.md", "snippets/term-box.md"],
10685
+ ["test/fixture.md", "test/fixture.md"],
10686
+ ["test/expected.html", "test/expected.html"],
10687
+ ["test/plugin.test.js.tpl", "test/plugin.test.js"]
10688
+ ],
10689
+ theme: [
10690
+ ["gutterpress.json", "gutterpress.json"],
10691
+ ["README.md", "README.md"],
10692
+ ["components.yaml", "components.yaml"],
10693
+ ["snippets/callout.md", "snippets/callout.md"],
10694
+ ["styles/tokens.css", "styles/tokens.css"],
10695
+ ["styles/base.css", "styles/base.css"],
10696
+ ["styles/components.css", "styles/components.css"],
10697
+ ["styles/page-templates.css", "styles/page-templates.css"],
10698
+ ["styles/page-rules.css", "styles/page-rules.css"],
10699
+ ["styles/book.css", "styles/book.css"]
10700
+ ]
10701
+ };
10702
+ var OPEN_FILE = {
10703
+ plugin: "plugin.js",
10704
+ theme: "styles/tokens.css"
10705
+ };
10706
+ var DEFAULT_DESCRIPTION = {
10707
+ plugin: "A Gutterpress plugin.",
10708
+ theme: "A Gutterpress theme."
10709
+ };
10710
+ var DEFAULT_AUTHOR2 = "Anonymous";
10711
+
10712
+ class ExtensionScaffoldError extends Error {
10713
+ code;
10714
+ constructor(code, message) {
10715
+ super(message);
10716
+ this.name = "CreateProjectError";
10717
+ this.code = code;
10718
+ }
10719
+ }
10720
+ function resolveExtensionPrefix(slug, requested) {
10721
+ const raw = (requested ?? `${slug}-`).trim();
10722
+ const prefix = raw.endsWith("-") ? raw : `${raw}-`;
10723
+ if (!/^[a-z][a-z0-9-]*-$/.test(prefix)) {
10724
+ 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-".`);
10725
+ }
10726
+ if (prefix === RESERVED_PREFIX) {
10727
+ 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.`);
10728
+ }
10729
+ return prefix;
10730
+ }
10731
+ function identifierFor(slug) {
10732
+ const camel = slug.replace(/-([a-z0-9])/g, (_m, c) => c.toUpperCase());
10733
+ return /^[0-9]/.test(camel) ? `_${camel}` : camel;
10734
+ }
10735
+ function escapeJsonScalar(value) {
10736
+ return JSON.stringify(value).slice(1, -1);
10737
+ }
10738
+ async function scaffoldExtension(options) {
10739
+ const kind = options.kind;
10740
+ if (kind !== "plugin" && kind !== "theme") {
10741
+ throw new ExtensionScaffoldError("invalid-name", `Unknown extension kind "${String(kind)}". Choose one of: ${EXTENSION_KINDS.join(", ")}.`);
10742
+ }
10743
+ const name = (options.name ?? "").trim();
10744
+ if (!name) {
10745
+ throw new ExtensionScaffoldError("invalid-name", "A name is required.");
10746
+ }
10747
+ const slug = (options.folderName ?? slugify(name)).trim();
10748
+ if (!slug || slug !== slugify(slug)) {
10749
+ throw new ExtensionScaffoldError("invalid-name", `Could not derive a valid folder name from "${name}".`);
10750
+ }
10751
+ const prefix = resolveExtensionPrefix(slug, options.prefix);
10752
+ const parentDir = options.parentDir;
10753
+ if (!parentDir || !path17.isAbsolute(parentDir)) {
10754
+ throw new ExtensionScaffoldError("parent-not-writable", "The save location must be an absolute path.");
10755
+ }
10756
+ try {
10757
+ await access2(parentDir, FS2.W_OK);
10758
+ } catch {
10759
+ throw new ExtensionScaffoldError("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
10760
+ }
10761
+ const extensionDir = path17.join(parentDir, slug);
10762
+ let targetExists = true;
10763
+ try {
10764
+ await access2(extensionDir, FS2.F_OK);
10765
+ } catch {
10766
+ targetExists = false;
10767
+ }
10768
+ if (targetExists) {
10769
+ throw new ExtensionScaffoldError("target-exists", `A folder named "${slug}" already exists here. Choose a different name or location.`);
10770
+ }
10771
+ const author = (options.author ?? "").trim() || DEFAULT_AUTHOR2;
10772
+ const description = (options.description ?? "").trim() || DEFAULT_DESCRIPTION[kind];
10773
+ const plain = {
10774
+ "{{NAME}}": name,
10775
+ "{{SLUG}}": slug,
10776
+ "{{PREFIX}}": prefix,
10777
+ "{{AUTHOR}}": author,
10778
+ "{{DESCRIPTION}}": description,
10779
+ "{{IDENT}}": identifierFor(slug)
10780
+ };
10781
+ const jsonSafe = Object.fromEntries(Object.entries(plain).map(([token, value]) => [token, escapeJsonScalar(value)]));
10782
+ const entries = TEMPLATE_FILES[kind];
10783
+ const written = [];
10784
+ try {
10785
+ await mkdir8(extensionDir, { recursive: true });
10786
+ for (const [assetName, relDest] of entries) {
10787
+ const src = await getAssetPath(`extension-templates/${kind}/${assetName}`);
10788
+ const dest = path17.join(extensionDir, relDest);
10789
+ await mkdir8(path17.dirname(dest), { recursive: true });
10790
+ const text = await readFile27(src, "utf8");
10791
+ const table = relDest.endsWith(".json") ? jsonSafe : plain;
10792
+ await writeFile7(dest, substitute(text, table), "utf8");
10793
+ written.push(relDest);
10794
+ }
10795
+ } catch (e) {
10796
+ throw new ExtensionScaffoldError("scaffold-io", `Could not create the ${kind} files: ${e instanceof Error ? e.message : String(e)}`);
10797
+ }
10798
+ return {
10799
+ extensionDir,
10800
+ manifestPath: path17.join(extensionDir, "gutterpress.json"),
10801
+ kind,
10802
+ slug,
10803
+ prefix,
10804
+ openFile: path17.join(extensionDir, OPEN_FILE[kind]),
10805
+ files: written
10806
+ };
10807
+ }
10808
+ function substitute(text, table) {
10809
+ let out = text;
10810
+ for (const [token, value] of Object.entries(table)) {
10811
+ out = out.split(token).join(value);
10812
+ }
10813
+ return out;
10814
+ }
10815
+
10268
10816
  // src/lib/project-templates.ts
10269
10817
  import { isMap, isScalar, isSeq as isSeq3, parseDocument as parseDocument2 } from "yaml";
10270
10818
  var BUILT_IN_TEMPLATE_IDS = [
@@ -10283,7 +10831,7 @@ var SKIP_ENTRIES = new Set([
10283
10831
  ]);
10284
10832
 
10285
10833
  // src/lib/plugin-manager.ts
10286
- import path17 from "node:path";
10834
+ import path19 from "node:path";
10287
10835
  import { isSeq as isSeq4, isMap as isMap2, isScalar as isScalar2, YAMLMap, Scalar as Scalar2 } from "yaml";
10288
10836
 
10289
10837
  // src/lib/npm-plugin-installer.ts
@@ -10291,15 +10839,15 @@ import { createHash as createHash7, randomUUID as randomUUID3, timingSafeEqual }
10291
10839
  import { createReadStream as createReadStream2 } from "node:fs";
10292
10840
  import {
10293
10841
  lstat as lstat2,
10294
- mkdir as mkdir8,
10842
+ mkdir as mkdir9,
10295
10843
  mkdtemp as mkdtemp2,
10296
10844
  open as open2,
10297
- readFile as readFile26,
10845
+ readFile as readFile28,
10298
10846
  realpath as realpath2,
10299
10847
  rename as rename2,
10300
10848
  rm as rm4
10301
10849
  } from "node:fs/promises";
10302
- import path16 from "node:path";
10850
+ import path18 from "node:path";
10303
10851
  import { Transform } from "node:stream";
10304
10852
  import { pipeline } from "node:stream/promises";
10305
10853
  import { createGunzip } from "node:zlib";
@@ -10344,8 +10892,8 @@ function mb(bytes) {
10344
10892
  return `${Math.ceil(bytes / 1024 / 1024)}MB`;
10345
10893
  }
10346
10894
  function isContained2(root, candidate) {
10347
- const relative2 = path16.relative(root, candidate);
10348
- return relative2 === "" || !relative2.startsWith("..") && !path16.isAbsolute(relative2);
10895
+ const relative2 = path18.relative(root, candidate);
10896
+ return relative2 === "" || !relative2.startsWith("..") && !path18.isAbsolute(relative2);
10349
10897
  }
10350
10898
  function mergeLimits(overrides) {
10351
10899
  const limits = { ...DEFAULT_LIMITS, ...overrides };
@@ -10579,7 +11127,7 @@ async function extractTarball(ctx, archivePath, packageDir, packageName) {
10579
11127
  "OldGnuLongPath",
10580
11128
  "OldExtendedHeader"
10581
11129
  ]);
10582
- await mkdir8(packageDir, { recursive: true });
11130
+ await mkdir9(packageDir, { recursive: true });
10583
11131
  const unpack = extract({
10584
11132
  cwd: packageDir,
10585
11133
  strip: 1,
@@ -10652,13 +11200,13 @@ async function extractTarball(ctx, archivePath, packageDir, packageName) {
10652
11200
  throw rejected;
10653
11201
  }
10654
11202
  async function readInstalledManifest(packageDir, selected) {
10655
- const packageJsonPath = path16.join(packageDir, "package.json");
11203
+ const packageJsonPath = path18.join(packageDir, "package.json");
10656
11204
  let manifest = null;
10657
11205
  try {
10658
11206
  const info2 = await lstat2(packageJsonPath);
10659
11207
  if (!info2.isFile() || info2.isSymbolicLink())
10660
11208
  throw new Error("not a regular file");
10661
- manifest = object2(JSON.parse(await readFile26(packageJsonPath, "utf8")));
11209
+ manifest = object2(JSON.parse(await readFile28(packageJsonPath, "utf8")));
10662
11210
  } catch (error2) {
10663
11211
  throw new SecurityInstallError(`Downloaded ${selected.name}@${selected.version} has no valid package.json (${error2 instanceof Error ? error2.message : String(error2)}).`);
10664
11212
  }
@@ -10728,10 +11276,10 @@ async function installPackage(ctx, name, selector, parentPackagePath, ancestors,
10728
11276
  throw new SecurityInstallError(`Dependency graph exceeds ${ctx.limits.totalPackages} packages.`);
10729
11277
  }
10730
11278
  const relativePath = relativePackagePath(parentPackagePath, name);
10731
- const packageDir = path16.join(ctx.stageRoot, ...relativePath.split("/"));
10732
- const archivePath = path16.join(ctx.downloadsDir, `${ctx.archiveCounter++}.tgz`);
11279
+ const packageDir = path18.join(ctx.stageRoot, ...relativePath.split("/"));
11280
+ const archivePath = path18.join(ctx.downloadsDir, `${ctx.archiveCounter++}.tgz`);
10733
11281
  try {
10734
- await mkdir8(path16.dirname(packageDir), { recursive: true });
11282
+ await mkdir9(path18.dirname(packageDir), { recursive: true });
10735
11283
  await downloadTarball(ctx, selected, archivePath);
10736
11284
  await extractTarball(ctx, archivePath, packageDir, selected.name);
10737
11285
  const manifest = await readInstalledManifest(packageDir, selected);
@@ -10826,14 +11374,14 @@ async function ensureDirectory(pathname) {
10826
11374
  } catch (error2) {
10827
11375
  if (error2.code !== "ENOENT")
10828
11376
  throw error2;
10829
- await mkdir8(pathname);
11377
+ await mkdir9(pathname);
10830
11378
  }
10831
11379
  }
10832
11380
  async function prepareVendorParent(projectDir, name) {
10833
- const plugins = path16.join(projectDir, PLUGINS_DIR);
10834
- const npm = path16.join(plugins, VENDORED_NPM_DIR);
10835
- const packageParent = path16.dirname(vendoredNpmPluginRoot(projectDir, name, "0.0.0"));
10836
- await mkdir8(projectDir, { recursive: true });
11381
+ const plugins = path18.join(projectDir, PLUGINS_DIR);
11382
+ const npm = path18.join(plugins, VENDORED_NPM_DIR);
11383
+ const packageParent = path18.dirname(vendoredNpmPluginRoot(projectDir, name, "0.0.0"));
11384
+ await mkdir9(projectDir, { recursive: true });
10837
11385
  await ensureDirectory(plugins);
10838
11386
  await ensureDirectory(npm);
10839
11387
  await ensureDirectory(packageParent);
@@ -10860,15 +11408,15 @@ function publicReceipt(record) {
10860
11408
  };
10861
11409
  }
10862
11410
  async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
10863
- const requestedProjectDir = path16.resolve(projectDirInput);
10864
- await mkdir8(requestedProjectDir, { recursive: true });
11411
+ const requestedProjectDir = path18.resolve(projectDirInput);
11412
+ await mkdir9(requestedProjectDir, { recursive: true });
10865
11413
  const projectDir = await realpath2(requestedProjectDir);
10866
11414
  const requested = parseNpmPluginSpec(packageSpec);
10867
11415
  const npmRoot = await prepareVendorParent(projectDir, requested.name);
10868
- const container = await mkdtemp2(path16.join(npmRoot, ".install-"));
10869
- const stageRoot = path16.join(container, "root");
10870
- const downloadsDir = path16.join(container, "downloads");
10871
- await Promise.all([mkdir8(stageRoot), mkdir8(downloadsDir)]);
11416
+ const container = await mkdtemp2(path18.join(npmRoot, ".install-"));
11417
+ const stageRoot = path18.join(container, "root");
11418
+ const downloadsDir = path18.join(container, "downloads");
11419
+ await Promise.all([mkdir9(stageRoot), mkdir9(downloadsDir)]);
10872
11420
  const ctx = {
10873
11421
  fetch: options.fetch ?? globalThis.fetch,
10874
11422
  signal: options.signal,
@@ -10913,7 +11461,7 @@ async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
10913
11461
  skipped: ctx.skipped,
10914
11462
  tree: { algorithm: "sha256", ...tree }
10915
11463
  };
10916
- const receiptFile = await open2(path16.join(stageRoot, VENDOR_RECEIPT_FILE), "wx");
11464
+ const receiptFile = await open2(path18.join(stageRoot, VENDOR_RECEIPT_FILE), "wx");
10917
11465
  try {
10918
11466
  await receiptFile.writeFile(`${JSON.stringify(receipt, null, 2)}
10919
11467
  `, "utf8");
@@ -10922,7 +11470,7 @@ async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
10922
11470
  await receiptFile.close();
10923
11471
  }
10924
11472
  finalRoot = vendoredNpmPluginRoot(projectDir, rootRecord.name, rootRecord.version);
10925
- const realFinalParent = await realpath2(path16.dirname(finalRoot));
11473
+ const realFinalParent = await realpath2(path18.dirname(finalRoot));
10926
11474
  if (!isContained2(projectDir, realFinalParent)) {
10927
11475
  throw new Error("Plugin install destination resolves outside the project.");
10928
11476
  }
@@ -10971,7 +11519,7 @@ async function rollbackNpmPluginInstall(installed) {
10971
11519
  // src/lib/plugin-manager.ts
10972
11520
  var pluginMutationQueues = new Map;
10973
11521
  function withPluginMutationLock(projectDir, mutation) {
10974
- const resolved = path17.resolve(projectDir);
11522
+ const resolved = path19.resolve(projectDir);
10975
11523
  const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
10976
11524
  const previous = pluginMutationQueues.get(key) ?? Promise.resolve();
10977
11525
  const run = previous.then(mutation, mutation);
@@ -11171,10 +11719,10 @@ async function addNpmPluginUnlocked(projectDir, packageSpec, options) {
11171
11719
  }
11172
11720
 
11173
11721
  // src/lib/theme-import.ts
11174
- import { existsSync as existsSync13 } from "node:fs";
11175
- import { mkdir as mkdir9, mkdtemp as mkdtemp3, readFile as readFile27, rm as rm5, writeFile as writeFile7 } from "node:fs/promises";
11722
+ import { existsSync as existsSync14 } from "node:fs";
11723
+ import { mkdir as mkdir10, mkdtemp as mkdtemp3, readFile as readFile29, rm as rm5, writeFile as writeFile8 } from "node:fs/promises";
11176
11724
  import { tmpdir as tmpdir3 } from "node:os";
11177
- import path18 from "node:path";
11725
+ import path20 from "node:path";
11178
11726
  import { unzipSync } from "fflate";
11179
11727
  var MAX_THEME_ARCHIVE_BYTES = 25 * 1024 * 1024;
11180
11728
  var MAX_THEME_UNZIPPED_BYTES = 25 * 1024 * 1024;
@@ -11207,7 +11755,7 @@ function classifyThemeCssFindings(findings) {
11207
11755
  const warnings = reject ? [] : findings.filter((f) => f.rule !== ruleSyntax);
11208
11756
  return { reject, warnings };
11209
11757
  }
11210
- var KNOWN_THEME_FILES = new Set(["theme.css", "theme.json"]);
11758
+ var KNOWN_THEME_FILES = new Set(["theme.css", "theme.json", EXTENSION_MANIFEST_FILENAME]);
11211
11759
  var ALLOWED_ASSET_EXTS = new Set([
11212
11760
  ".woff",
11213
11761
  ".woff2",
@@ -11223,8 +11771,12 @@ var ALLOWED_ASSET_EXTS = new Set([
11223
11771
  ".avif",
11224
11772
  ".css"
11225
11773
  ]);
11226
- function unexpectedThemeFiles(relPaths) {
11774
+ function unexpectedThemeFiles(relPaths, declaredExtras = []) {
11775
+ const extras = declaredExtras.map((p) => p.replace(/\\/g, "/"));
11776
+ const isDeclared = (p) => extras.some((extra) => p === extra || p.startsWith(`${extra}/`));
11227
11777
  return relPaths.map((p) => p.replace(/\\/g, "/")).filter((p) => p.length > 0 && !p.endsWith("/")).filter((p) => {
11778
+ if (isDeclared(p))
11779
+ return false;
11228
11780
  const base = p.split("/").pop().toLowerCase();
11229
11781
  if (KNOWN_THEME_FILES.has(base))
11230
11782
  return false;
@@ -11236,20 +11788,12 @@ function unexpectedThemeFiles(relPaths) {
11236
11788
  function mb2(n) {
11237
11789
  return `${Math.round(n / (1024 * 1024))}MB`;
11238
11790
  }
11239
- async function readThemeJson(p) {
11240
- try {
11241
- const parsed = JSON.parse(await readFile27(p, "utf8"));
11242
- return parsed && typeof parsed === "object" ? parsed : {};
11243
- } catch {
11244
- return {};
11245
- }
11246
- }
11247
11791
  async function finalizeThemeImport(projectDir, sourceDir, relPaths) {
11248
- const cssPath = path18.join(sourceDir, "theme.css");
11249
- if (!existsSync13(cssPath)) {
11792
+ const cssPath = path20.join(sourceDir, "theme.css");
11793
+ if (!existsSync14(cssPath)) {
11250
11794
  throw new Error("A theme must contain a theme.css file.");
11251
11795
  }
11252
- const css = await readFile27(cssPath, "utf8");
11796
+ const css = await readFile29(cssPath, "utf8");
11253
11797
  const { reject, warnings: printFindings } = classifyThemeCssFindings(checkCss(css, "theme.css"));
11254
11798
  if (reject) {
11255
11799
  throw new Error(`theme.css could not be parsed — ${reject.message}`);
@@ -11258,29 +11802,29 @@ async function finalizeThemeImport(projectDir, sourceDir, relPaths) {
11258
11802
  for (const w of printFindings) {
11259
11803
  warnings.push({ code: "print-safety", message: w.message });
11260
11804
  }
11261
- const jsonPath = path18.join(sourceDir, "theme.json");
11262
- const jsonExists = existsSync13(jsonPath);
11263
- const meta = jsonExists ? await readThemeJson(jsonPath) : {};
11264
- if (!jsonExists) {
11805
+ const metaExists = existsSync14(path20.join(sourceDir, EXTENSION_MANIFEST_FILENAME)) || existsSync14(path20.join(sourceDir, "theme.json"));
11806
+ const meta = metaExists ? await readExtensionMeta(sourceDir) : {};
11807
+ if (!metaExists) {
11265
11808
  warnings.push({
11266
11809
  code: "no-theme-json",
11267
- message: "No theme.json found — the theme's name was taken from the file/folder name."
11810
+ message: "No gutterpress.json or theme.json found — the theme's name was taken from the file/folder name."
11268
11811
  });
11269
11812
  } else if (!meta.name || !meta.name.trim()) {
11270
11813
  warnings.push({
11271
11814
  code: "unnamed-theme",
11272
- message: `theme.json has no "name" — the theme's name was taken from the file/folder name.`
11815
+ message: `The metadata file has no "name" — the theme's name was taken from the file/folder name.`
11273
11816
  });
11274
11817
  }
11275
11818
  assertThemeSheetsContained(meta);
11819
+ const metaFilename = existsSync14(path20.join(sourceDir, EXTENSION_MANIFEST_FILENAME)) ? EXTENSION_MANIFEST_FILENAME : "theme.json";
11276
11820
  const extraSheets = [
11277
11821
  ...new Set([...themeStyleList(meta), ...themeEngineStyleList(meta)])
11278
11822
  ].filter((rel) => rel !== "theme.css");
11279
- const extraSheetPaths = resolveDeclaredStyles(extraSheets, sourceDir, "theme.json") ?? [];
11823
+ const extraSheetPaths = resolveDeclaredStyles(extraSheets, sourceDir, metaFilename) ?? [];
11280
11824
  for (let i = 0;i < extraSheets.length; i++) {
11281
11825
  const rel = extraSheets[i];
11282
11826
  const sheetPath = extraSheetPaths[i];
11283
- const sheetCss = await readFile27(sheetPath, "utf8");
11827
+ const sheetCss = await readFile29(sheetPath, "utf8");
11284
11828
  const { reject: sheetReject, warnings: sheetFindings } = classifyThemeCssFindings(checkCss(sheetCss, rel));
11285
11829
  if (sheetReject) {
11286
11830
  throw new Error(`${rel} could not be parsed — ${sheetReject.message}`);
@@ -11289,7 +11833,8 @@ async function finalizeThemeImport(projectDir, sourceDir, relPaths) {
11289
11833
  warnings.push({ code: "print-safety", message: `${rel}: ${w.message}` });
11290
11834
  }
11291
11835
  }
11292
- const extra = unexpectedThemeFiles(relPaths);
11836
+ const declaredExtras = [meta.markdown, meta.components, meta.snippets].filter((p) => typeof p === "string" && p.trim().length > 0);
11837
+ const extra = unexpectedThemeFiles(relPaths, declaredExtras);
11293
11838
  if (extra.length > 0) {
11294
11839
  const shown = extra.slice(0, 6).join(", ");
11295
11840
  warnings.push({
@@ -11341,7 +11886,7 @@ async function importThemeFromZip(projectDir, archive) {
11341
11886
  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.");
11342
11887
  }
11343
11888
  const prefix = root === "" ? "" : `${root}/`;
11344
- const tmp = await mkdtemp3(path18.join(tmpdir3(), "gutterpress-theme-"));
11889
+ const tmp = await mkdtemp3(path20.join(tmpdir3(), "gutterpress-theme-"));
11345
11890
  try {
11346
11891
  const relPaths = [];
11347
11892
  for (const [name, data] of Object.entries(files)) {
@@ -11353,13 +11898,13 @@ async function importThemeFromZip(projectDir, archive) {
11353
11898
  const rel = prefix ? norm.slice(prefix.length) : norm;
11354
11899
  if (!rel || rel.split("/").some((seg) => seg === ".."))
11355
11900
  continue;
11356
- const dest = path18.join(tmp, rel);
11357
- if (dest !== tmp && !dest.startsWith(tmp + path18.sep)) {
11901
+ const dest = path20.join(tmp, rel);
11902
+ if (dest !== tmp && !dest.startsWith(tmp + path20.sep)) {
11358
11903
  throw new Error(`The package contains an unsafe path and was rejected: ${name}`);
11359
11904
  }
11360
11905
  relPaths.push(rel);
11361
- await mkdir9(path18.dirname(dest), { recursive: true });
11362
- await writeFile7(dest, data);
11906
+ await mkdir10(path20.dirname(dest), { recursive: true });
11907
+ await writeFile8(dest, data);
11363
11908
  }
11364
11909
  return await finalizeThemeImport(projectDir, tmp, relPaths);
11365
11910
  } finally {
@@ -11372,10 +11917,10 @@ async function importThemeFromCssText(projectDir, css, name) {
11372
11917
  throw new Error(`The CSS could not be parsed — ${reject.message}`);
11373
11918
  }
11374
11919
  const displayName = name.trim() || "Imported theme";
11375
- const tmp = await mkdtemp3(path18.join(tmpdir3(), "gutterpress-theme-"));
11920
+ const tmp = await mkdtemp3(path20.join(tmpdir3(), "gutterpress-theme-"));
11376
11921
  try {
11377
- await writeFile7(path18.join(tmp, "theme.css"), css, "utf8");
11378
- await writeFile7(path18.join(tmp, "theme.json"), JSON.stringify({ name: displayName }, null, 2), "utf8");
11922
+ await writeFile8(path20.join(tmp, "theme.css"), css, "utf8");
11923
+ await writeFile8(path20.join(tmp, "theme.json"), JSON.stringify({ name: displayName }, null, 2), "utf8");
11379
11924
  const result = await finalizeThemeImport(projectDir, tmp, ["theme.css", "theme.json"]);
11380
11925
  result.warnings.unshift({
11381
11926
  code: "no-theme-json",
@@ -11387,14 +11932,14 @@ async function importThemeFromCssText(projectDir, css, name) {
11387
11932
  }
11388
11933
  }
11389
11934
  async function importThemeFromFile(projectDir, filePath) {
11390
- const ext = path18.extname(filePath).toLowerCase();
11935
+ const ext = path20.extname(filePath).toLowerCase();
11391
11936
  if (ext === ".zip") {
11392
- const buf = await readFile27(filePath);
11937
+ const buf = await readFile29(filePath);
11393
11938
  return importThemeFromZip(projectDir, new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
11394
11939
  }
11395
11940
  if (ext === ".css") {
11396
- const css = await readFile27(filePath, "utf8");
11397
- return importThemeFromCssText(projectDir, css, prettify(path18.basename(filePath, ".css")));
11941
+ const css = await readFile29(filePath, "utf8");
11942
+ return importThemeFromCssText(projectDir, css, prettify(path20.basename(filePath, ".css")));
11398
11943
  }
11399
11944
  throw new Error("Choose a .zip theme package or a .css stylesheet.");
11400
11945
  }
@@ -11903,25 +12448,25 @@ ${tail}` : ""}`);
11903
12448
  };
11904
12449
 
11905
12450
  // src/lib/publish/providers/guided.ts
11906
- import { copyFile as copyFile2, mkdir as mkdir10, writeFile as writeFile8 } from "node:fs/promises";
11907
- import path19 from "node:path";
12451
+ import { copyFile as copyFile2, mkdir as mkdir11, writeFile as writeFile9 } from "node:fs/promises";
12452
+ import path21 from "node:path";
11908
12453
  async function stageGuidedPackage(req, providerId, listingLines) {
11909
- const packageDir = path19.join(path19.dirname(req.artifact.path), "publish", providerId);
11910
- await mkdir10(packageDir, { recursive: true });
11911
- await copyFile2(req.artifact.path, path19.join(packageDir, path19.basename(req.artifact.path)));
12454
+ const packageDir = path21.join(path21.dirname(req.artifact.path), "publish", providerId);
12455
+ await mkdir11(packageDir, { recursive: true });
12456
+ await copyFile2(req.artifact.path, path21.join(packageDir, path21.basename(req.artifact.path)));
11912
12457
  const { title, authors } = req.project;
11913
12458
  const sheet = [
11914
12459
  `# ${title || "Untitled"}`,
11915
12460
  "",
11916
12461
  `- **Authors:** ${authors.length ? authors.join(", ") : "(not set)"}`,
11917
- `- **File:** ${path19.basename(req.artifact.path)}`,
12462
+ `- **File:** ${path21.basename(req.artifact.path)}`,
11918
12463
  ...listingLines,
11919
12464
  "",
11920
12465
  "Copy these details into the platform's listing form when uploading.",
11921
12466
  ""
11922
12467
  ].join(`
11923
12468
  `);
11924
- await writeFile8(path19.join(packageDir, "LISTING.md"), sheet, "utf8");
12469
+ await writeFile9(path21.join(packageDir, "LISTING.md"), sheet, "utf8");
11925
12470
  return packageDir;
11926
12471
  }
11927
12472
 
@@ -11984,9 +12529,9 @@ var drivethrurpgProvider = {
11984
12529
  };
11985
12530
 
11986
12531
  // src/lib/publish/providers/gdrive.ts
11987
- import { mkdtemp as mkdtemp4, readFile as readFile28, readdir as readdir7, rm as rm6, stat as stat7, writeFile as writeFile9 } from "node:fs/promises";
12532
+ import { mkdtemp as mkdtemp4, readFile as readFile30, readdir as readdir7, rm as rm6, stat as stat7, writeFile as writeFile10 } from "node:fs/promises";
11988
12533
  import { tmpdir as tmpdir4 } from "node:os";
11989
- import path20 from "node:path";
12534
+ import path22 from "node:path";
11990
12535
  import { Zip, ZipDeflate } from "fflate";
11991
12536
 
11992
12537
  // src/lib/publish/google-drive.ts
@@ -12371,34 +12916,34 @@ function zipEntriesNonBlocking(entries, opts) {
12371
12916
  async function collectZipEntries(root, dir, out, onWarn) {
12372
12917
  const entries = await readdir7(dir, { withFileTypes: true });
12373
12918
  for (const entry of entries) {
12374
- const full = path20.join(dir, entry.name);
12919
+ const full = path22.join(dir, entry.name);
12375
12920
  if (entry.isDirectory()) {
12376
12921
  await collectZipEntries(root, full, out, onWarn);
12377
12922
  } else if (entry.isFile()) {
12378
- const rel = path20.relative(root, full).split(path20.sep).join("/");
12379
- out[rel] = await readFile28(full);
12923
+ const rel = path22.relative(root, full).split(path22.sep).join("/");
12924
+ out[rel] = await readFile30(full);
12380
12925
  } else if (entry.isSymbolicLink()) {
12381
12926
  const resolved = await stat7(full).catch(() => null);
12382
12927
  if (resolved?.isFile()) {
12383
- const rel = path20.relative(root, full).split(path20.sep).join("/");
12384
- out[rel] = await readFile28(full);
12928
+ const rel = path22.relative(root, full).split(path22.sep).join("/");
12929
+ out[rel] = await readFile30(full);
12385
12930
  } else if (resolved?.isDirectory()) {
12386
12931
  await collectZipEntries(root, full, out, onWarn);
12387
12932
  } else {
12388
- onWarn?.(`Skipped "${path20.relative(root, full)}" in the website export — it's a broken symlink.`);
12933
+ onWarn?.(`Skipped "${path22.relative(root, full)}" in the website export — it's a broken symlink.`);
12389
12934
  }
12390
12935
  } else {
12391
- onWarn?.(`Skipped "${path20.relative(root, full)}" in the website export — not a file, folder, or symlink.`);
12936
+ onWarn?.(`Skipped "${path22.relative(root, full)}" in the website export — not a file, folder, or symlink.`);
12392
12937
  }
12393
12938
  }
12394
12939
  }
12395
- async function zipHtmlExport(exportDir, title, onWarn, writeArchiveImpl = writeFile9) {
12940
+ async function zipHtmlExport(exportDir, title, onWarn, writeArchiveImpl = writeFile10) {
12396
12941
  const entries = {};
12397
12942
  await collectZipEntries(exportDir, exportDir, entries, onWarn);
12398
12943
  const fileName = `${bookSlug(title)}-website.zip`;
12399
- const tmpRoot = await mkdtemp4(path20.join(tmpdir4(), "gutterpress-gdrive-"));
12944
+ const tmpRoot = await mkdtemp4(path22.join(tmpdir4(), "gutterpress-gdrive-"));
12400
12945
  try {
12401
- const filePath = path20.join(tmpRoot, fileName);
12946
+ const filePath = path22.join(tmpRoot, fileName);
12402
12947
  const zipped = await zipEntriesNonBlocking(entries, { level: 6 });
12403
12948
  await writeArchiveImpl(filePath, zipped);
12404
12949
  return {
@@ -12446,7 +12991,7 @@ var gdriveProvider = {
12446
12991
  const isHtml = req.artifact.format === "html";
12447
12992
  const source = isHtml ? await zipHtmlExport(req.artifact.path, req.project.title, (msg) => req.deps.onProgress?.(msg)) : {
12448
12993
  filePath: req.artifact.path,
12449
- fileName: path20.basename(req.artifact.path),
12994
+ fileName: path22.basename(req.artifact.path),
12450
12995
  mimeType: "application/pdf",
12451
12996
  cleanup: async () => {}
12452
12997
  };
@@ -12496,8 +13041,8 @@ var gdriveProvider = {
12496
13041
  };
12497
13042
 
12498
13043
  // src/lib/publish/butler.ts
12499
- import { chmod, mkdir as mkdir11, stat as stat8, writeFile as writeFile10 } from "node:fs/promises";
12500
- import path21 from "node:path";
13044
+ import { chmod, mkdir as mkdir12, stat as stat8, writeFile as writeFile11 } from "node:fs/promises";
13045
+ import path23 from "node:path";
12501
13046
  import { unzipSync as unzipSync2 } from "fflate";
12502
13047
  var BUTLER_DOWNLOAD_TIMEOUT_MS = 300000;
12503
13048
  function butlerBrothChannel(platform = process.platform, arch = process.arch) {
@@ -12534,8 +13079,8 @@ async function ensureButler(deps) {
12534
13079
  }
12535
13080
  if (await commandExists("butler", runCommand, deps.env))
12536
13081
  return "butler";
12537
- const cacheDir = path21.join(deps.configDir ?? defaultConfigDir(), "tools", "butler");
12538
- const cached = path21.join(cacheDir, butlerBinaryName());
13082
+ const cacheDir = path23.join(deps.configDir ?? defaultConfigDir(), "tools", "butler");
13083
+ const cached = path23.join(cacheDir, butlerBinaryName());
12539
13084
  if (await fileExists2(cached))
12540
13085
  return cached;
12541
13086
  const channel = butlerBrothChannel();
@@ -12557,14 +13102,14 @@ async function ensureButler(deps) {
12557
13102
  return new Uint8Array(await response.arrayBuffer());
12558
13103
  });
12559
13104
  const files = unzipSync2(archive);
12560
- await mkdir11(cacheDir, { recursive: true });
13105
+ await mkdir12(cacheDir, { recursive: true });
12561
13106
  let foundBinary = false;
12562
13107
  for (const [name, data] of Object.entries(files)) {
12563
13108
  if (name.endsWith("/"))
12564
13109
  continue;
12565
- const target = path21.join(cacheDir, path21.basename(name));
12566
- await writeFile10(target, data);
12567
- if (path21.basename(name) === butlerBinaryName()) {
13110
+ const target = path23.join(cacheDir, path23.basename(name));
13111
+ await writeFile11(target, data);
13112
+ if (path23.basename(name) === butlerBinaryName()) {
12568
13113
  await chmod(target, 493).catch(() => {});
12569
13114
  foundBinary = true;
12570
13115
  }
@@ -12994,7 +13539,7 @@ function publishProviderFor(id) {
12994
13539
 
12995
13540
  // src/lib/publish/run-publish.ts
12996
13541
  import { readdir as readdir8, stat as stat9 } from "node:fs/promises";
12997
- import path22 from "node:path";
13542
+ import path24 from "node:path";
12998
13543
  function resolvePublishFormat(info8, providerConfig) {
12999
13544
  if (info8.formats && info8.formats.length > 0) {
13000
13545
  const configured = typeof providerConfig.format === "string" ? providerConfig.format.trim() : "";
@@ -13015,9 +13560,9 @@ async function resolvePublishRequest(options, deps) {
13015
13560
  const effectiveDeps = effectiveAccount ? { ...deps, credentialAccount: effectiveAccount } : deps;
13016
13561
  const outDir = resolveOutputDir(manifestDir, config.title);
13017
13562
  const effectiveFormat = resolvePublishFormat(provider.info, providerConfig);
13018
- const defaultArtifact = effectiveFormat === "pdf" ? path22.join(outDir, artifactName(config.title, "pdf")) : outDir;
13563
+ const defaultArtifact = effectiveFormat === "pdf" ? path24.join(outDir, artifactName(config.title, "pdf")) : outDir;
13019
13564
  const artifact = {
13020
- path: options.artifactPath ? path22.resolve(options.projectDir, options.artifactPath) : defaultArtifact,
13565
+ path: options.artifactPath ? path24.resolve(options.projectDir, options.artifactPath) : defaultArtifact,
13021
13566
  format: effectiveFormat
13022
13567
  };
13023
13568
  return {
@@ -13081,7 +13626,7 @@ async function artifactIssues(artifact) {
13081
13626
  var BUILD_FINGERPRINT_FILENAME = "build-fingerprint.json";
13082
13627
  async function htmlDirIssues(dir) {
13083
13628
  const issues = [];
13084
- const isThere = async (rel) => stat9(path22.join(dir, rel)).then(() => true, () => false);
13629
+ const isThere = async (rel) => stat9(path24.join(dir, rel)).then(() => true, () => false);
13085
13630
  if (!await isThere(BOOK_HTML)) {
13086
13631
  issues.push({
13087
13632
  severity: "error",
@@ -13313,17 +13858,17 @@ import git6 from "isomorphic-git";
13313
13858
  import git5 from "isomorphic-git";
13314
13859
  import diff3Merge from "diff3";
13315
13860
  // src/lib/publish/selections.ts
13316
- import { chmod as chmod2, mkdir as mkdir12, readFile as readFile29, writeFile as writeFile11 } from "node:fs/promises";
13317
- import path23 from "node:path";
13861
+ import { chmod as chmod2, mkdir as mkdir13, readFile as readFile31, writeFile as writeFile12 } from "node:fs/promises";
13862
+ import path25 from "node:path";
13318
13863
  class PublishSelectionsStore {
13319
13864
  filePath;
13320
13865
  queue = Promise.resolve();
13321
13866
  constructor(filePath) {
13322
- this.filePath = filePath ?? path23.join(defaultConfigDir(), "publish-selections.json");
13867
+ this.filePath = filePath ?? path25.join(defaultConfigDir(), "publish-selections.json");
13323
13868
  }
13324
13869
  async read() {
13325
13870
  try {
13326
- const raw = await readFile29(this.filePath, "utf8");
13871
+ const raw = await readFile31(this.filePath, "utf8");
13327
13872
  const parsed = JSON.parse(raw);
13328
13873
  if (parsed && typeof parsed === "object") {
13329
13874
  return {
@@ -13336,8 +13881,8 @@ class PublishSelectionsStore {
13336
13881
  return { version: 1, global: {}, projects: {} };
13337
13882
  }
13338
13883
  async write(data) {
13339
- await mkdir12(path23.dirname(this.filePath), { recursive: true });
13340
- await writeFile11(this.filePath, JSON.stringify(data, null, 2), {
13884
+ await mkdir13(path25.dirname(this.filePath), { recursive: true });
13885
+ await writeFile12(this.filePath, JSON.stringify(data, null, 2), {
13341
13886
  encoding: "utf8",
13342
13887
  mode: 384
13343
13888
  });
@@ -13402,4 +13947,4 @@ function normalizeProjectKey(projectDir) {
13402
13947
  }
13403
13948
  // src/lib/publish/manifest-publish.ts
13404
13949
  import { isMap as isMap4 } from "yaml";
13405
- export { PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, loadManifestWithPath, parseNpmPluginSpec, runLint, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, BUILT_IN_THEME_IDS, listBuiltInThemes, listProjectThemes, getActiveTheme, getPreviousTheme, revertTheme, applyTheme, importThemeFromFolder, importThemeFromUrl, removeProjectTheme, detectLegacyForkedTheme, scaffoldProject, BUILT_IN_TEMPLATE_IDS, addNpmPlugin, importThemeFromFile, publishCredentialKey, publishConnectionStatus, listPublishAccounts, connectGoogleDrive, listPublishProviders, publishProviderFor, resolvePublishFormat, runPublish, connectPublishProvider, disconnectPublishCredential };
13950
+ export { PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, loadManifestWithPath, parseNpmPluginSpec, runLint, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, BUILT_IN_THEME_IDS, listBuiltInThemes, listProjectThemes, getActiveTheme, getPreviousTheme, revertTheme, applyTheme, importThemeFromFolder, importThemeFromUrl, removeProjectTheme, detectLegacyForkedTheme, scaffoldProject, EXTENSION_KINDS, scaffoldExtension, BUILT_IN_TEMPLATE_IDS, addNpmPlugin, importThemeFromFile, publishCredentialKey, publishConnectionStatus, listPublishAccounts, connectGoogleDrive, listPublishProviders, publishProviderFor, resolvePublishFormat, runPublish, connectPublishProvider, disconnectPublishCredential };