gutterpress 0.10.9 → 0.10.10-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +66 -5
  2. package/dist/{README-jntqyj1z.md → README-3zk4tgnj.md} +4 -4
  3. package/dist/{README-6c7fda1s.md → README-7x5akcns.md} +5 -4
  4. package/dist/api/index.d.ts +7 -4
  5. package/dist/api/index.js +22 -12
  6. package/dist/{audit-fq7s1gsz.js → audit-wcmbgh39.js} +5 -5
  7. package/dist/{build-dyat3fqd.js → build-c1w6kqpv.js} +6 -6
  8. package/dist/{cli-ypfvnhag.js → cli-2rswy18t.js} +1 -1
  9. package/dist/{cli-w692g6dx.js → cli-4mtf9ajw.js} +127 -1
  10. package/dist/{cli-bzxqtvrk.js → cli-75qkn0k5.js} +1 -1
  11. package/dist/{cli-bx950tb4.js → cli-k8f9eb12.js} +22 -26
  12. package/dist/{cli-8pg731p5.js → cli-n0vgey6x.js} +4914 -4707
  13. package/dist/{cli-rh05byck.js → cli-scqp638v.js} +1 -1
  14. package/dist/cli.js +16 -16
  15. package/dist/{components-0mmswvvm.yaml → components-dhgv2sm7.yaml} +2 -2
  16. package/dist/{doctor-yr85n080.js → doctor-drv36btj.js} +2 -2
  17. package/dist/{engine-d46wgg99.js → engine-pt2acjb5.js} +2 -2
  18. package/dist/{engine-19mshh07.js → engine-qtmvaqt7.js} +1 -1
  19. package/dist/{ext-jrg8c48s.js → ext-xqx26xvy.js} +52 -8
  20. package/dist/{index-mey5vraz.js → index-1he93sjd.js} +918 -796
  21. package/dist/{index-zwcv1njy.js → index-5dgxeevv.js} +22 -26
  22. package/dist/{index-dhgzkhs7.js → index-a0e2253t.js} +1 -1
  23. package/dist/{index-dtrftayr.js → index-y01v2fe0.js} +127 -1
  24. package/dist/index.js +102 -15
  25. package/dist/lib/build-error.d.ts +6 -5
  26. package/dist/lib/build-preflight.d.ts +11 -1
  27. package/dist/lib/build-runner.d.ts +49 -16
  28. package/dist/lib/extension-import.d.ts +4 -4
  29. package/dist/lib/extension-manager.d.ts +3 -3
  30. package/dist/lib/extension-manifest.d.ts +91 -87
  31. package/dist/lib/extension-scaffold.d.ts +1 -1
  32. package/dist/lib/extension-search.d.ts +46 -0
  33. package/dist/lib/host-policy.d.ts +10 -12
  34. package/dist/lib/lint-runner.d.ts +16 -9
  35. package/dist/lib/npm-registry.d.ts +22 -0
  36. package/dist/lib/source-provider.d.ts +71 -0
  37. package/dist/lib/validation-exec.d.ts +11 -0
  38. package/dist/{lint-enasqbqx.js → lint-ejzmx14j.js} +5 -5
  39. package/dist/{new-r9c99wsn.js → new-g3q3h4s5.js} +6 -6
  40. package/dist/package-97gm31sg.json +10 -0
  41. package/dist/{package-rm9d2tht.json → package-9rv69pxm.json} +4 -1
  42. package/dist/package-aenwjew3.json +28 -0
  43. package/dist/package-gag6ykwn.json +10 -0
  44. package/dist/package-nrhkwf2m.json +10 -0
  45. package/dist/{plugin.test.js-npnfhyca.tpl → plugin.test.js-k2r87k04.tpl} +9 -8
  46. package/dist/{preflight-shk7vqbs.js → preflight-w5z2vfhx.js} +5 -5
  47. package/dist/{preview-52918mxk.js → preview-9s22dq77.js} +6 -6
  48. package/dist/{publish-8s60vhx7.js → publish-mt22ewpk.js} +5 -5
  49. package/dist/{source-provider-2j5x2hyw.js → source-provider-51g6mt6y.js} +7 -1
  50. package/dist/{source-provider-kn41jmbs.js → source-provider-jkppq9fc.js} +7 -1
  51. package/dist/{tokens-e4xk3x4v.css → tokens-2sracx1w.css} +1 -1
  52. package/dist/{validate-awccx32r.js → validate-kkn8rmy5.js} +5 -5
  53. package/package.json +1 -1
  54. package/dist/gutterpress-m98e2w2d.json +0 -8
  55. package/dist/gutterpress-x2ycdsb8.json +0 -17
  56. package/dist/theme-etj4gxck.json +0 -9
  57. package/dist/theme-s2rg7xe7.json +0 -9
  58. package/dist/theme-vktedy8q.json +0 -9
@@ -18,7 +18,7 @@ import {
18
18
  resolveChromiumExecutable,
19
19
  run,
20
20
  spawnCapture
21
- } from "./index-zwcv1njy.js";
21
+ } from "./index-5dgxeevv.js";
22
22
  import {
23
23
  gitFs,
24
24
  gitScopeFor,
@@ -29,7 +29,7 @@ import {
29
29
  snapshotStagingMarkerPath,
30
30
  snapshotWorkingTreeUnlocked,
31
31
  withRepoLock
32
- } from "./index-dtrftayr.js";
32
+ } from "./index-y01v2fe0.js";
33
33
  import {
34
34
  detectProjectSource,
35
35
  syncRemoteFor
@@ -58,6 +58,27 @@ import { lstat, readFile, readdir, realpath } from "node:fs/promises";
58
58
  import path from "node:path";
59
59
  import { exports as resolveExports } from "resolve.exports";
60
60
  import { satisfies, validRange } from "semver";
61
+
62
+ // src/lib/npm-registry.ts
63
+ var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
64
+ var ENV_OVERRIDE = "GUTTERPRESS_NPM_REGISTRY";
65
+ function npmRegistryUrl() {
66
+ const configured = process.env[ENV_OVERRIDE]?.trim();
67
+ if (!configured)
68
+ return DEFAULT_NPM_REGISTRY;
69
+ let scheme;
70
+ try {
71
+ scheme = new URL(configured).protocol;
72
+ } catch {
73
+ throw new Error(`Invalid ${ENV_OVERRIDE} URL "${configured}".`);
74
+ }
75
+ if (scheme !== "http:" && scheme !== "https:") {
76
+ throw new Error(`${ENV_OVERRIDE} must be an http(s) URL — got "${scheme}".`);
77
+ }
78
+ return configured.replace(/\/+$/, "");
79
+ }
80
+
81
+ // src/lib/plugin-vendor.ts
61
82
  var PLUGINS_DIR = "plugins";
62
83
  var VENDORED_NPM_DIR = "npm";
63
84
  var VENDOR_RECEIPT_FILE = ".gutterpress-install.json";
@@ -529,7 +550,7 @@ async function verifyVendoredPlugin(projectDir, expectedName, expectedVersion) {
529
550
  throw new Error(`Package receipt is missing provenance: ${pkg.name}@${pkg.version}`);
530
551
  }
531
552
  const tarball = new URL(pkg.tarball);
532
- if (tarball.origin !== "https://registry.npmjs.org" || tarball.username || tarball.password) {
553
+ if (tarball.origin !== new URL(npmRegistryUrl()).origin || tarball.username || tarball.password) {
533
554
  throw new Error(`Package receipt has an invalid tarball URL: ${pkg.name}@${pkg.version}`);
534
555
  }
535
556
  const packageDir = path.resolve(installRoot, ...relative.split("/"));
@@ -3637,7 +3658,7 @@ async function renderChaptersToFile(inputDir, outDir, opts = {}) {
3637
3658
 
3638
3659
  // src/lib/markdown/plugins.ts
3639
3660
  import { createHash as createHash3 } from "node:crypto";
3640
- import { existsSync as existsSync5, lstatSync, realpathSync, rmSync, statSync as statSync2, unlinkSync } from "node:fs";
3661
+ import { existsSync as existsSync4, lstatSync, realpathSync, rmSync, statSync as statSync2, unlinkSync } from "node:fs";
3641
3662
  import { cp, link, mkdir as mkdir2, mkdtemp, readFile as readFile6, rm, unlink, writeFile as writeFile2 } from "node:fs/promises";
3642
3663
  import { tmpdir } from "node:os";
3643
3664
  import { resolve as resolve3, join as join2, dirname as dirname2, basename, extname, relative, isAbsolute } from "node:path";
@@ -3663,25 +3684,46 @@ function resolveDeclaredStyles(rawStyles, baseDir, subject) {
3663
3684
  }
3664
3685
 
3665
3686
  // src/lib/extension-manifest.ts
3666
- import { existsSync as existsSync4 } from "node:fs";
3667
3687
  import { readFile as readFile5 } from "node:fs/promises";
3668
3688
  import path4 from "node:path";
3669
- var EXTENSION_MANIFEST_FILENAME = "gutterpress.json";
3670
- var LEGACY_THEME_MANIFEST_FILENAME = "theme.json";
3671
- async function readJsonMetaFile(jsonPath) {
3672
- try {
3673
- const parsed = JSON.parse(await readFile5(jsonPath, "utf8"));
3674
- return parsed && typeof parsed === "object" ? parsed : {};
3675
- } catch {
3689
+ var EXTENSION_MANIFEST_FILENAME = "package.json";
3690
+ var GUTTERPRESS_KEY = "gutterpress";
3691
+ function object2(value) {
3692
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
3693
+ }
3694
+ function str(value) {
3695
+ return typeof value === "string" ? value : undefined;
3696
+ }
3697
+ function authorName(value) {
3698
+ return str(value) ?? str(object2(value)?.name);
3699
+ }
3700
+ function metaFromPackageJson(raw) {
3701
+ const pkg = object2(raw);
3702
+ if (!pkg)
3676
3703
  return {};
3677
- }
3704
+ const gp = object2(pkg[GUTTERPRESS_KEY]) ?? {};
3705
+ const keywords = Array.isArray(pkg.keywords) ? pkg.keywords.filter((k) => typeof k === "string") : undefined;
3706
+ return {
3707
+ ...str(pkg.name) ? { name: str(pkg.name) } : {},
3708
+ ...authorName(pkg.author) ? { author: authorName(pkg.author) } : {},
3709
+ ...str(pkg.description) ? { description: str(pkg.description) } : {},
3710
+ ...keywords ? { keywords } : {},
3711
+ ...str(pkg.main) ? { main: str(pkg.main) } : {},
3712
+ ..."preview" in gp ? { preview: str(gp.preview) ?? null } : {},
3713
+ ...Array.isArray(gp.styles) ? { styles: declaredList(gp.styles) } : {},
3714
+ ...str(gp.tokensFile) ? { tokensFile: str(gp.tokensFile) } : {},
3715
+ ...str(gp.markdown) ? { markdown: str(gp.markdown) } : {},
3716
+ ...str(gp.components) ? { components: str(gp.components) } : {},
3717
+ ...str(gp.snippets) ? { snippets: str(gp.snippets) } : {},
3718
+ ...gp.engineStyles !== undefined ? { engineStyles: gp.engineStyles } : {}
3719
+ };
3678
3720
  }
3679
3721
  async function readExtensionMeta(dir) {
3680
- const extensionPath = path4.join(dir, EXTENSION_MANIFEST_FILENAME);
3681
- if (existsSync4(extensionPath)) {
3682
- return readJsonMetaFile(extensionPath);
3722
+ try {
3723
+ return metaFromPackageJson(JSON.parse(await readFile5(path4.join(dir, EXTENSION_MANIFEST_FILENAME), "utf8")));
3724
+ } catch {
3725
+ return {};
3683
3726
  }
3684
- return readJsonMetaFile(path4.join(dir, LEGACY_THEME_MANIFEST_FILENAME));
3685
3727
  }
3686
3728
  function declaredList(value) {
3687
3729
  return Array.isArray(value) ? value.filter((s) => typeof s === "string" && s.trim().length > 0) : [];
@@ -3689,16 +3731,13 @@ function declaredList(value) {
3689
3731
  function extensionStyleList(meta) {
3690
3732
  return declaredList(meta.styles);
3691
3733
  }
3692
- function extensionStyleListWithDefault(meta, dir) {
3693
- const declared = extensionStyleList(meta);
3694
- if (declared.length > 0)
3695
- return declared;
3696
- return existsSync4(path4.join(dir, "theme.css")) ? ["theme.css"] : [];
3734
+ function extensionEntry(meta) {
3735
+ return meta.markdown?.trim() || meta.main?.trim() || undefined;
3697
3736
  }
3698
- function extensionCarries(meta, dir) {
3737
+ function extensionCarries(meta) {
3699
3738
  return {
3700
- markdown: !!meta.markdown?.trim(),
3701
- styles: extensionStyleListWithDefault(meta, dir).length > 0,
3739
+ markdown: !!extensionEntry(meta),
3740
+ styles: extensionStyleList(meta).length > 0,
3702
3741
  snippets: !!meta.snippets?.trim(),
3703
3742
  components: !!meta.components?.trim()
3704
3743
  };
@@ -3710,9 +3749,10 @@ function assertExtensionContained(meta) {
3710
3749
  if (meta.engineStyles !== undefined) {
3711
3750
  throw new Error("The extension's metadata declares `engineStyles`, which was removed — " + "move its entries to the end of `styles`.");
3712
3751
  }
3752
+ const entry = extensionEntry(meta);
3713
3753
  const declared = [
3714
3754
  ...extensionStyleList(meta),
3715
- ...meta.markdown ? [meta.markdown] : [],
3755
+ ...entry ? [entry] : [],
3716
3756
  ...meta.components ? [meta.components] : [],
3717
3757
  ...meta.snippets ? [meta.snippets] : [],
3718
3758
  ...meta.tokensFile ? [meta.tokensFile] : []
@@ -3724,8 +3764,9 @@ function assertExtensionContained(meta) {
3724
3764
  }
3725
3765
  }
3726
3766
  function resolveExtension(dir, meta, subject) {
3727
- const styles = resolveDeclaredStyles(extensionStyleListWithDefault(meta, dir), dir, subject);
3728
- const [markdown] = resolveDeclaredStyles(meta.markdown ? [meta.markdown] : undefined, dir, subject) ?? [];
3767
+ const entry = extensionEntry(meta);
3768
+ const styles = resolveDeclaredStyles(extensionStyleList(meta), dir, subject);
3769
+ const [markdown] = resolveDeclaredStyles(entry ? [entry] : undefined, dir, subject) ?? [];
3729
3770
  const [components] = resolveDeclaredStyles(meta.components ? [meta.components] : undefined, dir, subject) ?? [];
3730
3771
  const [snippets] = resolveDeclaredStyles(meta.snippets ? [meta.snippets] : undefined, dir, subject) ?? [];
3731
3772
  return {
@@ -3973,9 +4014,9 @@ function isReceiptBoundRequireCall(node) {
3973
4014
  return true;
3974
4015
  if (callee.type !== "MemberExpression")
3975
4016
  return false;
3976
- const object2 = callee.object;
4017
+ const object3 = callee.object;
3977
4018
  const property = memberName(callee);
3978
- return Boolean(object2 && (object2.type === "Identifier" && object2.name === "module" && property === "require" || object2.type === "Identifier" && object2.name === "require" && property === "resolve"));
4019
+ return Boolean(object3 && (object3.type === "Identifier" && object3.name === "module" && property === "require" || object3.type === "Identifier" && object3.name === "require" && property === "resolve"));
3979
4020
  }
3980
4021
  function rewriteCommonJsRequires(source, absolute, pkg, tree) {
3981
4022
  const ast = parseJavaScript(source, {
@@ -4391,6 +4432,22 @@ function extensionMetadata(meta) {
4391
4432
  ...meta.author ? { author: meta.author } : {}
4392
4433
  };
4393
4434
  }
4435
+ function staleManifestHint(dir) {
4436
+ const stale = ["gutterpress.json", "theme.json"].find((name) => existsSync4(join2(dir, name)));
4437
+ if (!stale)
4438
+ return "";
4439
+ return `
4440
+
4441
+ This folder still has a ${stale}. Gutterpress no longer reads it — ` + `describe the extension in package.json instead:
4442
+ {
4443
+ "name": "my-extension",
4444
+ "description": "What it does",
4445
+ "author": "You",
4446
+ "main": "plugin.js",
4447
+ "gutterpress": { "styles": ["theme.css"] }
4448
+ }
4449
+ (\`main\` only if the package carries a markdown-it plugin; \`gutterpress.styles\` only if it carries stylesheets.)`;
4450
+ }
4394
4451
  async function loadExtensionFromDir(extensionDir, config, pluginRef) {
4395
4452
  const meta = await readExtensionMeta(extensionDir);
4396
4453
  assertExtensionContained(meta);
@@ -4398,7 +4455,7 @@ async function loadExtensionFromDir(extensionDir, config, pluginRef) {
4398
4455
  const extensionStyles = resolved.styles ?? [];
4399
4456
  const name = config.name ?? meta.name ?? pluginRef;
4400
4457
  if (!resolved.markdown && extensionStyles.length === 0) {
4401
- throw new Error(`Extension folder "${pluginRef}" declares neither \`markdown\` nor \`styles\` ` + "(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.");
4458
+ throw new Error(`Extension folder "${pluginRef}" declares no markdown-it plugin (\`main\`) and no ` + "`gutterpress.styles` in its package.json — there is nothing to load. Point " + "`path` at a JS file directly for a plain plugin, or describe the package in package.json." + staleManifestHint(extensionDir));
4402
4459
  }
4403
4460
  if (!resolved.markdown) {
4404
4461
  return {
@@ -4442,7 +4499,7 @@ async function loadPlugin(config, baseDir) {
4442
4499
  }
4443
4500
  if (config.path) {
4444
4501
  const candidatePath = resolve3(baseDir, config.path);
4445
- if (existsSync5(candidatePath) && statSync2(candidatePath).isDirectory()) {
4502
+ if (existsSync4(candidatePath) && statSync2(candidatePath).isDirectory()) {
4446
4503
  try {
4447
4504
  return await loadExtensionFromDir(candidatePath, config, pluginRef);
4448
4505
  } catch (error2) {
@@ -4454,7 +4511,7 @@ async function loadPlugin(config, baseDir) {
4454
4511
  try {
4455
4512
  if (config.path) {
4456
4513
  const pluginPath = resolve3(baseDir, config.path);
4457
- if (!existsSync5(pluginPath)) {
4514
+ if (!existsSync4(pluginPath)) {
4458
4515
  throw new Error(`Plugin file not found: ${pluginPath} (resolved from manifest entry path="${config.path}")`);
4459
4516
  }
4460
4517
  pluginModule = await loadCachedPathPluginModule(pluginPath);
@@ -4465,12 +4522,12 @@ async function loadPlugin(config, baseDir) {
4465
4522
  pluginModule = loaded.module;
4466
4523
  moduleDir = loaded.moduleDir;
4467
4524
  pluginName = config.name;
4468
- if (loaded.packageDir && (existsSync5(join2(loaded.packageDir, EXTENSION_MANIFEST_FILENAME)) || existsSync5(join2(loaded.packageDir, LEGACY_THEME_MANIFEST_FILENAME)))) {
4525
+ if (loaded.packageDir) {
4469
4526
  const meta = await readExtensionMeta(loaded.packageDir);
4470
4527
  assertExtensionContained(meta);
4471
- const resolved = resolveExtension(loaded.packageDir, meta, `Plugin "${pluginRef}"`);
4528
+ const resolved = resolveExtension(loaded.packageDir, { ...meta, main: undefined }, `Plugin "${pluginRef}"`);
4472
4529
  if (resolved.markdown && loaded.entryPath && resolve3(resolved.markdown) !== resolve3(loaded.entryPath)) {
4473
- throw new Error(`its gutterpress.json declares markdown "${meta.markdown}" but the package entry is "${relative(loaded.packageDir, loaded.entryPath)}" — an npm extension's markdown-it ` + "plugin is its package entry, so declare that file or drop the field.");
4530
+ throw new Error(`its package.json declares gutterpress.markdown "${meta.markdown}" but the package entry is "${relative(loaded.packageDir, loaded.entryPath)}" — an npm extension's ` + "markdown-it plugin is its package entry, so declare that file or drop the field.");
4474
4531
  }
4475
4532
  packageMeta = meta;
4476
4533
  packageStyles = resolved.styles ?? [];
@@ -4546,7 +4603,7 @@ function artifactName(title, format) {
4546
4603
  }
4547
4604
 
4548
4605
  // src/lib/ghostscript.ts
4549
- import { existsSync as existsSync6 } from "node:fs";
4606
+ import { existsSync as existsSync5 } from "node:fs";
4550
4607
  import { readFile as readFile7, writeFile as writeFile3, unlink as unlink2, rename, readdir as readdir4 } from "node:fs/promises";
4551
4608
  import { join as join3, basename as basename2, resolve as resolve4 } from "node:path";
4552
4609
  var WINDOWS_GHOSTSCRIPT_NAMES = ["gswin64c", "gswin32c", "gs"];
@@ -4581,7 +4638,7 @@ async function findWindowsGhostscriptInstall(env) {
4581
4638
  for (const version of versions) {
4582
4639
  for (const executable of ["gswin64c.exe", "gswin32c.exe"]) {
4583
4640
  const candidate = join3(installRoot, version.name, "bin", executable);
4584
- if (existsSync6(candidate))
4641
+ if (existsSync5(candidate))
4585
4642
  return candidate;
4586
4643
  }
4587
4644
  }
@@ -4590,7 +4647,7 @@ async function findWindowsGhostscriptInstall(env) {
4590
4647
  }
4591
4648
  async function resolveGhostscript(targetPlatform = process.platform, env = process.env) {
4592
4649
  const override = envValue(env, "GHOSTSCRIPT_PATH");
4593
- if (override && existsSync6(override))
4650
+ if (override && existsSync5(override))
4594
4651
  return override;
4595
4652
  const names = targetPlatform === "win32" ? WINDOWS_GHOSTSCRIPT_NAMES : POSIX_GHOSTSCRIPT_NAMES;
4596
4653
  for (const name of names) {
@@ -4782,7 +4839,7 @@ import git from "isomorphic-git";
4782
4839
  // package.json
4783
4840
  var package_default = {
4784
4841
  name: "gutterpress",
4785
- version: "0.10.9",
4842
+ version: "0.10.10-alpha.2",
4786
4843
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
4787
4844
  author: "itlackey",
4788
4845
  license: "MPL-2.0",
@@ -5051,491 +5108,163 @@ async function writeBuildFingerprint(input) {
5051
5108
  return outPath;
5052
5109
  }
5053
5110
 
5054
- // src/lib/lint-runner.ts
5055
- import { resolve as resolve5 } from "node:path";
5056
- import { readFile as readFile9 } from "node:fs/promises";
5111
+ // src/lib/validation-exec.ts
5112
+ import { existsSync as existsSync10 } from "node:fs";
5113
+ import { dirname as dirname4, join as join4, resolve as resolve8 } from "node:path";
5057
5114
 
5058
- // src/lib/printsafe.ts
5059
- import postcss2 from "postcss";
5060
- var ruleRemoteUrls = "printsafe/no-remote-urls";
5061
- var ruleRiskyProps = "printsafe/no-risky-print-effects";
5062
- var ruleSyntax = "printsafe/syntax-error";
5063
- var rulePageContainment = "printsafe/page-containment";
5064
- function extractUrls(value) {
5065
- const urls = [];
5066
- const re = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
5067
- let m;
5068
- while (m = re.exec(value))
5069
- urls.push(m[2] ?? "");
5070
- return urls;
5115
+ // src/checks/formatter.ts
5116
+ function formatReport(report, format = "text") {
5117
+ if (format === "json") {
5118
+ formatJson(report);
5119
+ } else {
5120
+ formatText(report);
5121
+ }
5071
5122
  }
5072
- function isRemoteUrl(url) {
5073
- const lower = url.trim().toLowerCase();
5074
- return lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("//");
5123
+ function formatText(report) {
5124
+ for (const r of report.infos) {
5125
+ const loc = targetPrefix(r.target) + formatLocation(r.file, r.line, r.column);
5126
+ log.info(`${loc}${r.message}`);
5127
+ if (r.detail)
5128
+ log.info(` ${r.detail}`);
5129
+ }
5130
+ for (const r of report.warnings) {
5131
+ const loc = targetPrefix(r.target) + formatLocation(r.file, r.line, r.column);
5132
+ log.warn(`${loc}${r.message}`);
5133
+ if (r.detail)
5134
+ log.warn(` ${r.detail}`);
5135
+ }
5136
+ for (const r of report.errors) {
5137
+ const loc = targetPrefix(r.target) + formatLocation(r.file, r.line, r.column);
5138
+ log.error(`${loc}${r.message}`);
5139
+ if (r.detail)
5140
+ log.error(` ${r.detail}`);
5141
+ }
5142
+ const { summary } = report;
5143
+ if (summary.errors > 0) {
5144
+ log.error(`VALIDATION FAILED (${summary.errors} error${summary.errors > 1 ? "s" : ""})`);
5145
+ } else if (summary.warnings > 0) {
5146
+ log.warn("VALIDATION PASSED (with warnings)");
5147
+ } else {
5148
+ log.success("VALIDATION PASSED");
5149
+ }
5075
5150
  }
5076
- var riskyProperties = new Set([
5077
- "filter",
5078
- "backdrop-filter",
5079
- "mix-blend-mode",
5080
- "background-blend-mode",
5081
- "animation",
5082
- "animation-name",
5083
- "transition",
5084
- "will-change",
5085
- "clip-path"
5086
- ]);
5087
- var marginBoxAtRuleNames = new Set([
5088
- "top-left-corner",
5089
- "top-left",
5090
- "top-center",
5091
- "top-right",
5092
- "top-right-corner",
5093
- "bottom-left-corner",
5094
- "bottom-left",
5095
- "bottom-center",
5096
- "bottom-right",
5097
- "bottom-right-corner",
5098
- "left-top",
5099
- "left-middle",
5100
- "left-bottom",
5101
- "right-top",
5102
- "right-middle",
5103
- "right-bottom"
5104
- ]);
5105
- var marginBoxIgnoredProperties = MARGIN_BOX_IGNORED_PROPERTIES;
5106
- function isInPageBox(decl) {
5107
- const page = decl.parent;
5108
- return !!page && page.type === "atrule" && page.name.toLowerCase() === "page";
5151
+ function targetPrefix(target) {
5152
+ return target ? `[${target}] ` : "";
5109
5153
  }
5110
- function isDroppedPageGradient(decl) {
5111
- const prop = decl.prop.toLowerCase();
5112
- if (prop !== "background" && prop !== "background-image")
5113
- return false;
5114
- if (!/(^|[\s,(])[\w-]*gradient\(/i.test(decl.value))
5115
- return false;
5116
- return isInPageBox(decl);
5154
+ function formatJson(report) {
5155
+ console.log(JSON.stringify({
5156
+ results: report.results,
5157
+ summary: report.summary,
5158
+ passed: report.passed
5159
+ }, null, 2));
5117
5160
  }
5118
- function isInPageMarginBox(decl) {
5119
- const box = decl.parent;
5120
- if (!box || box.type !== "atrule")
5121
- return false;
5122
- if (!marginBoxAtRuleNames.has(box.name.toLowerCase()))
5123
- return false;
5124
- const page = box.parent;
5125
- return !!page && page.type === "atrule" && page.name.toLowerCase() === "page";
5161
+ function formatLocation(file, line, column) {
5162
+ if (!file)
5163
+ return "";
5164
+ let loc = `${file}`;
5165
+ if (line != null) {
5166
+ loc += `:${line}`;
5167
+ if (column != null)
5168
+ loc += `:${column}`;
5169
+ }
5170
+ return `${loc}: `;
5126
5171
  }
5127
- function isUnpairedFitContentWidth(decl) {
5128
- const prop = decl.prop.toLowerCase();
5129
- if (prop !== "width" && prop !== "inline-size")
5130
- return false;
5131
- if (!/\bfit-content\b/i.test(decl.value))
5132
- return false;
5133
- if (!isInPageMarginBox(decl))
5172
+
5173
+ // src/checks/registry.ts
5174
+ var checks = new Map;
5175
+ function registerCheck(check) {
5176
+ checks.set(check.id, check);
5177
+ return () => {
5178
+ if (checks.get(check.id) === check)
5179
+ checks.delete(check.id);
5180
+ };
5181
+ }
5182
+ function getChecks(filter) {
5183
+ let result = Array.from(checks.values());
5184
+ if (filter?.category) {
5185
+ const cats = Array.isArray(filter.category) ? filter.category : [filter.category];
5186
+ result = result.filter((c) => cats.includes(c.category));
5187
+ }
5188
+ if (filter?.phase) {
5189
+ result = result.filter((c) => c.phase === filter.phase);
5190
+ }
5191
+ if (filter?.ids) {
5192
+ const idSet = new Set(filter.ids);
5193
+ result = result.filter((c) => idSet.has(c.id));
5194
+ }
5195
+ return result;
5196
+ }
5197
+ function getCheckById(id) {
5198
+ return checks.get(id);
5199
+ }
5200
+ function getKnownCategories() {
5201
+ const categories = new Set;
5202
+ for (const check of checks.values()) {
5203
+ categories.add(check.category);
5204
+ }
5205
+ return Array.from(categories);
5206
+ }
5207
+ function resolveCheckSelectors(selectors) {
5208
+ const allChecks = Array.from(checks.values());
5209
+ const resolved = [];
5210
+ const unmatched = [];
5211
+ const seen = new Set;
5212
+ for (const rawSelector of selectors) {
5213
+ const selector = rawSelector.trim();
5214
+ if (!selector)
5215
+ continue;
5216
+ const matcher = selector.includes("*") ? selectorToRegex(selector) : undefined;
5217
+ let matchedAny = false;
5218
+ for (const check of allChecks) {
5219
+ const matched = matcher ? matcher.test(check.id) : check.id === selector;
5220
+ if (!matched)
5221
+ continue;
5222
+ matchedAny = true;
5223
+ if (seen.has(check.id))
5224
+ continue;
5225
+ seen.add(check.id);
5226
+ resolved.push(check.id);
5227
+ }
5228
+ if (!matchedAny)
5229
+ unmatched.push(selector);
5230
+ }
5231
+ return { resolved, unmatched };
5232
+ }
5233
+ function selectorToRegex(selector) {
5234
+ const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
5235
+ return new RegExp(`^${escaped}$`);
5236
+ }
5237
+
5238
+ // src/checks/policy.ts
5239
+ var INSPECT_FAILED_CODE = "inspect-failed";
5240
+ function isCheckEnabled(check, config) {
5241
+ const entry = config.validate.checks[check.id];
5242
+ if (entry === false)
5134
5243
  return false;
5135
- const siblings = decl.parent.nodes ?? [];
5136
- const decls = siblings.filter((n) => n.type === "decl");
5137
- if (decls.some((d) => ["height", "block-size"].includes(d.prop.toLowerCase())))
5244
+ if (typeof entry === "object" && entry.enabled === false)
5138
5245
  return false;
5139
- if (decls.some((d) => /gradient\(/i.test(d.value)))
5246
+ if (check.enabledWhen && !check.enabledWhen(config))
5140
5247
  return false;
5141
5248
  return true;
5142
5249
  }
5143
- function targetsPageWrapper(selector) {
5144
- if (!/\.(page|spread)(?![\w-])/.test(selector))
5145
- return false;
5146
- return !/\bgp-(bleed|pin)\b/.test(selector);
5147
- }
5148
- var cssWideResetKeywords = new Set(["initial", "unset", "revert", "revert-layer"]);
5149
- var inertValues = {
5150
- "z-index": new Set(["auto"]),
5151
- opacity: new Set(["1", "100%"]),
5152
- "mix-blend-mode": new Set(["normal"]),
5153
- "background-blend-mode": new Set(["normal"]),
5154
- filter: new Set(["none"]),
5155
- "backdrop-filter": new Set(["none"]),
5156
- transform: new Set(["none"]),
5157
- rotate: new Set(["none"]),
5158
- translate: new Set(["none"]),
5159
- scale: new Set(["none"]),
5160
- perspective: new Set(["none"]),
5161
- "box-shadow": new Set(["none"]),
5162
- outline: new Set(["none"]),
5163
- "outline-style": new Set(["none"]),
5164
- "will-change": new Set(["auto", "none"]),
5165
- "clip-path": new Set(["none"]),
5166
- transition: new Set(["none"]),
5167
- animation: new Set(["none"]),
5168
- "animation-name": new Set(["none"]),
5169
- overflow: new Set(["visible"]),
5170
- "overflow-x": new Set(["visible"]),
5171
- "overflow-y": new Set(["visible"])
5172
- };
5173
- function isInertValue(prop, value) {
5174
- const v = value.trim().toLowerCase();
5175
- return cssWideResetKeywords.has(v) || (inertValues[prop]?.has(v) ?? false);
5176
- }
5177
- function createsStackingContext(decl) {
5178
- const p = decl.prop.toLowerCase();
5179
- const v = decl.value.trim().toLowerCase();
5180
- if (isInertValue(p, v))
5181
- return false;
5182
- switch (p) {
5183
- case "isolation":
5184
- return v === "isolate";
5185
- case "contain":
5186
- return /\b(paint|layout|strict|content)\b/.test(v);
5187
- case "z-index":
5188
- case "opacity":
5189
- case "mix-blend-mode":
5190
- case "filter":
5191
- case "backdrop-filter":
5192
- case "transform":
5193
- case "perspective":
5194
- case "will-change":
5195
- return true;
5196
- default:
5197
- return false;
5198
- }
5199
- }
5200
- function clipsDescendants(decl) {
5201
- const p = decl.prop.toLowerCase();
5202
- if (p !== "overflow" && p !== "overflow-x" && p !== "overflow-y")
5203
- return false;
5204
- const v = decl.value.trim().toLowerCase();
5205
- return v !== "" && !isInertValue(p, v);
5206
- }
5207
- function nodeLoc(node) {
5208
- return {
5209
- line: node.source?.start?.line ?? 1,
5210
- column: node.source?.start?.column ?? 1
5211
- };
5212
- }
5213
- function checkCss(css, from) {
5214
- let root;
5215
- try {
5216
- root = postcss2.parse(css, from ? { from } : undefined);
5217
- } catch (err) {
5218
- const e = err;
5219
- return [
5220
- {
5221
- rule: ruleSyntax,
5222
- severity: "error",
5223
- message: `CSS syntax error: ${e.reason ?? e.message ?? "unparseable"}`,
5224
- line: e.line ?? 1,
5225
- column: e.column ?? 1
5226
- }
5227
- ];
5228
- }
5229
- const warnings = [];
5230
- const reportRemoteUrls = (value, node) => {
5231
- for (const url of extractUrls(value)) {
5232
- if (isRemoteUrl(url)) {
5233
- warnings.push({
5234
- rule: ruleRemoteUrls,
5235
- severity: "error",
5236
- message: `Remote URL is not allowed in print CSS: ${url}`,
5237
- ...nodeLoc(node)
5238
- });
5239
- }
5240
- }
5241
- };
5242
- root.walkDecls((decl) => reportRemoteUrls(decl.value || "", decl));
5243
- root.walkAtRules((at) => reportRemoteUrls(at.params || "", at));
5244
- root.walkDecls((decl) => {
5245
- const prop = decl.prop.toLowerCase();
5246
- if (isInertValue(prop, decl.value))
5247
- return;
5248
- if (marginBoxIgnoredProperties.has(prop) && isInPageMarginBox(decl)) {
5249
- warnings.push({
5250
- rule: ruleRiskyProps,
5251
- severity: "warning",
5252
- message: `Property "${decl.prop}" is not supported in Chromium @page margin boxes and is silently ignored — the chrome renders square/unshadowed. Anything that would paint outside the box (box-shadow, outline, filter) or make it a stacking context (transform, opacity, mix-blend-mode) is dropped; text-shadow, border, border-radius, padding and background gradients all work inside the box.`,
5253
- ...nodeLoc(decl)
5254
- });
5255
- } else if (prop === "filter") {
5256
- warnings.push({
5257
- rule: ruleRiskyProps,
5258
- severity: "warning",
5259
- message: `Property is high-risk for print/PDF: 'filter' rasterizes its subtree to a 300 DPI bitmap in the printed PDF (text becomes unselectable, unsearchable, and inaccessible), and it dominates build time (~90% measured; 57.0s -> 6.2s over 60pp when scoped — see ENGINE.md §10). Scope it to the smallest possible selector. It also means the render-parity gate cannot see a text-only change inside it (docs/render-parity-gate.md, "Known blind spot").`,
5260
- ...nodeLoc(decl)
5261
- });
5262
- } else if (riskyProperties.has(prop)) {
5263
- warnings.push({
5264
- rule: ruleRiskyProps,
5265
- severity: "warning",
5266
- message: `Property is high-risk for print/PDF (can force rasterization): ${decl.prop}`,
5267
- ...nodeLoc(decl)
5268
- });
5269
- } else if (isDroppedPageGradient(decl)) {
5270
- warnings.push({
5271
- rule: ruleRiskyProps,
5272
- severity: "warning",
5273
- message: `"${decl.prop}: ${decl.value}" on @page paints nothing in print — Chromium drops a gradient in the @page box's own background, with no error and a valid-looking PDF. A solid colour there paints the whole sheet; the same gradient also works on "html" (it covers the content area, not the sheet) or in a margin box.`,
5274
- ...nodeLoc(decl)
5275
- });
5276
- } else if (isUnpairedFitContentWidth(decl)) {
5277
- warnings.push({
5278
- rule: ruleRiskyProps,
5279
- severity: "warning",
5280
- message: `"${decl.prop}: fit-content" in a @page margin box collapses only the inline axis — the box stays stretched to the full margin band, so a chip/pill renders as a tall rectangle around its text. Add "height: fit-content" (align-self / vertical-align do nothing here).`,
5281
- ...nodeLoc(decl)
5282
- });
5283
- }
5284
- });
5285
- root.walkRules((rule) => {
5286
- if (rule.parent?.type === "atrule") {
5287
- const at = rule.parent.name.toLowerCase();
5288
- if (at === "page" || marginBoxAtRuleNames.has(at))
5289
- return;
5290
- }
5291
- if (!targetsPageWrapper(rule.selector))
5292
- return;
5293
- rule.walkDecls((decl) => {
5294
- if (clipsDescendants(decl)) {
5295
- warnings.push({
5296
- rule: rulePageContainment,
5297
- severity: "warning",
5298
- message: `"${decl.prop}: ${decl.value}" on "${rule.selector}" clips out-of-flow descendants: a .gp-bleed plate is cut back to this box's width instead of reaching the sheet edge, and a .gp-behind image is cut the same way. Scope it with :has() (e.g. "${rule.selector}:not(:has(.gp-bleed, .gp-pin))") or drop it. This is an early source-only check; the build-time engine.layer.trapped audit is authoritative for the live ancestor chain.`,
5299
- ...nodeLoc(decl)
5300
- });
5301
- } else if (createsStackingContext(decl)) {
5302
- warnings.push({
5303
- rule: rulePageContainment,
5304
- severity: "warning",
5305
- message: `"${decl.prop}: ${decl.value}" on "${rule.selector}" makes the page box a stacking context, so a .gp-behind image is trapped inside it and paints ABOVE the page's text instead of under it. Core keeps .page/.spread at "position: relative; z-index: auto" for this reason. This is an early source-only check; the build-time engine.layer.trapped audit is authoritative for the live ancestor chain.`,
5306
- ...nodeLoc(decl)
5307
- });
5308
- }
5309
- });
5310
- });
5311
- return warnings;
5312
- }
5313
-
5314
- // src/lib/lint-runner.ts
5315
- async function runLint(opts = {}) {
5316
- const { glob } = await import("glob");
5317
- const { manifest, manifestDir } = await loadManifestWithPath(opts.manifest, {
5318
- explicit: opts.manifest !== undefined
5319
- });
5320
- const resolved = resolveConfig({}, manifest);
5321
- let files;
5322
- if (opts.files) {
5323
- files = await glob([opts.files], { nodir: true, ignore: ["**/*.min.css"] });
5324
- } else {
5325
- const relStyles = await resolveActiveStyles(manifestDir, resolved.styles);
5326
- const projectFiles = relStyles.map((rel) => resolve5(manifestDir, rel));
5327
- let pluginStylePaths = opts.pluginStylePaths;
5328
- if (pluginStylePaths === undefined) {
5329
- ({ pluginStylePaths } = await loadPluginsWithCss(resolved.extensions, manifestDir, (ref, err) => log.warn(`Skipping plugin "${ref}" for lint — ${err.message}`)));
5330
- }
5331
- files = [...projectFiles, ...pluginStylePaths].filter((f) => !f.endsWith(".min.css"));
5332
- }
5333
- if (files.length === 0) {
5334
- log.warn("No CSS files found to lint");
5335
- return { ok: true, riskyCount: 0, filesLinted: 0 };
5336
- }
5337
- log.info(`Linting ${files.length} CSS file(s)`);
5338
- let errorCount = 0;
5339
- let riskyCount = 0;
5340
- let linted = 0;
5341
- for (const file of files) {
5342
- let css;
5343
- try {
5344
- css = await readFile9(file, "utf8");
5345
- } catch (err) {
5346
- log.error(` ${file}`);
5347
- log.error(` cannot read stylesheet: ${err instanceof Error ? err.message : String(err)}`);
5348
- errorCount++;
5349
- continue;
5350
- }
5351
- linted++;
5352
- const warnings = checkCss(css, file);
5353
- const errors = warnings.filter((w) => w.severity === "error");
5354
- const risky = warnings.filter((w) => w.rule === ruleRiskyProps);
5355
- riskyCount += risky.length;
5356
- if (errors.length > 0) {
5357
- log.error(` ${file}`);
5358
- for (const w of errors) {
5359
- log.error(` ${w.line}:${w.column} ${w.message} (${w.rule})`);
5360
- }
5361
- errorCount += errors.length;
5362
- }
5363
- if (risky.length > 0) {
5364
- log.warn(` ${file}`);
5365
- for (const w of risky) {
5366
- log.warn(` ${w.line}:${w.column} ${w.message} (${w.rule})`);
5367
- }
5368
- }
5369
- }
5370
- if (errorCount > 0) {
5371
- log.error("CSS lint errors found");
5372
- return { ok: false, riskyCount, filesLinted: linted };
5373
- }
5374
- if (riskyCount > 0) {
5375
- log.warn(`${riskyCount} risky print properties found (may cause rasterization)`);
5376
- } else {
5377
- log.success("CSS lint passed");
5378
- }
5379
- return { ok: true, riskyCount, filesLinted: linted };
5380
- }
5381
-
5382
- // src/lib/validation-exec.ts
5383
- import { existsSync as existsSync11 } from "node:fs";
5384
- import { dirname as dirname4, join as join4, resolve as resolve9 } from "node:path";
5385
-
5386
- // src/checks/formatter.ts
5387
- function formatReport(report, format = "text") {
5388
- if (format === "json") {
5389
- formatJson(report);
5390
- } else {
5391
- formatText(report);
5392
- }
5393
- }
5394
- function formatText(report) {
5395
- for (const r of report.infos) {
5396
- const loc = targetPrefix(r.target) + formatLocation(r.file, r.line, r.column);
5397
- log.info(`${loc}${r.message}`);
5398
- if (r.detail)
5399
- log.info(` ${r.detail}`);
5400
- }
5401
- for (const r of report.warnings) {
5402
- const loc = targetPrefix(r.target) + formatLocation(r.file, r.line, r.column);
5403
- log.warn(`${loc}${r.message}`);
5404
- if (r.detail)
5405
- log.warn(` ${r.detail}`);
5406
- }
5407
- for (const r of report.errors) {
5408
- const loc = targetPrefix(r.target) + formatLocation(r.file, r.line, r.column);
5409
- log.error(`${loc}${r.message}`);
5410
- if (r.detail)
5411
- log.error(` ${r.detail}`);
5412
- }
5413
- const { summary } = report;
5414
- if (summary.errors > 0) {
5415
- log.error(`VALIDATION FAILED (${summary.errors} error${summary.errors > 1 ? "s" : ""})`);
5416
- } else if (summary.warnings > 0) {
5417
- log.warn("VALIDATION PASSED (with warnings)");
5418
- } else {
5419
- log.success("VALIDATION PASSED");
5420
- }
5421
- }
5422
- function targetPrefix(target) {
5423
- return target ? `[${target}] ` : "";
5424
- }
5425
- function formatJson(report) {
5426
- console.log(JSON.stringify({
5427
- results: report.results,
5428
- summary: report.summary,
5429
- passed: report.passed
5430
- }, null, 2));
5431
- }
5432
- function formatLocation(file, line, column) {
5433
- if (!file)
5434
- return "";
5435
- let loc = `${file}`;
5436
- if (line != null) {
5437
- loc += `:${line}`;
5438
- if (column != null)
5439
- loc += `:${column}`;
5440
- }
5441
- return `${loc}: `;
5442
- }
5443
-
5444
- // src/checks/registry.ts
5445
- var checks = new Map;
5446
- function registerCheck(check) {
5447
- checks.set(check.id, check);
5448
- return () => {
5449
- if (checks.get(check.id) === check)
5450
- checks.delete(check.id);
5451
- };
5452
- }
5453
- function getChecks(filter) {
5454
- let result = Array.from(checks.values());
5455
- if (filter?.category) {
5456
- const cats = Array.isArray(filter.category) ? filter.category : [filter.category];
5457
- result = result.filter((c) => cats.includes(c.category));
5458
- }
5459
- if (filter?.phase) {
5460
- result = result.filter((c) => c.phase === filter.phase);
5461
- }
5462
- if (filter?.ids) {
5463
- const idSet = new Set(filter.ids);
5464
- result = result.filter((c) => idSet.has(c.id));
5465
- }
5466
- return result;
5467
- }
5468
- function getCheckById(id) {
5469
- return checks.get(id);
5470
- }
5471
- function getKnownCategories() {
5472
- const categories = new Set;
5473
- for (const check of checks.values()) {
5474
- categories.add(check.category);
5475
- }
5476
- return Array.from(categories);
5477
- }
5478
- function resolveCheckSelectors(selectors) {
5479
- const allChecks = Array.from(checks.values());
5480
- const resolved = [];
5481
- const unmatched = [];
5482
- const seen = new Set;
5483
- for (const rawSelector of selectors) {
5484
- const selector = rawSelector.trim();
5485
- if (!selector)
5486
- continue;
5487
- const matcher = selector.includes("*") ? selectorToRegex(selector) : undefined;
5488
- let matchedAny = false;
5489
- for (const check of allChecks) {
5490
- const matched = matcher ? matcher.test(check.id) : check.id === selector;
5491
- if (!matched)
5492
- continue;
5493
- matchedAny = true;
5494
- if (seen.has(check.id))
5495
- continue;
5496
- seen.add(check.id);
5497
- resolved.push(check.id);
5498
- }
5499
- if (!matchedAny)
5500
- unmatched.push(selector);
5501
- }
5502
- return { resolved, unmatched };
5503
- }
5504
- function selectorToRegex(selector) {
5505
- const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
5506
- return new RegExp(`^${escaped}$`);
5507
- }
5508
-
5509
- // src/checks/policy.ts
5510
- var INSPECT_FAILED_CODE = "inspect-failed";
5511
- function isCheckEnabled(check, config) {
5512
- const entry = config.validate.checks[check.id];
5513
- if (entry === false)
5514
- return false;
5515
- if (typeof entry === "object" && entry.enabled === false)
5516
- return false;
5517
- if (check.enabledWhen && !check.enabledWhen(config))
5518
- return false;
5519
- return true;
5520
- }
5521
- function selectChecks(opts, config) {
5522
- const unmatched = [];
5523
- let checks2;
5524
- if (opts.only && opts.only.length > 0) {
5525
- const resolved = resolveCheckSelectors(opts.only);
5526
- checks2 = getChecks({ ids: resolved.resolved });
5527
- unmatched.push(...resolved.unmatched);
5528
- } else {
5529
- checks2 = getChecks({ category: opts.category, phase: opts.phase });
5530
- }
5531
- if (opts.skip && opts.skip.length > 0) {
5532
- const resolved = resolveCheckSelectors(opts.skip);
5533
- const skipSet = new Set(resolved.resolved);
5534
- checks2 = checks2.filter((c) => !skipSet.has(c.id));
5535
- unmatched.push(...resolved.unmatched);
5536
- }
5537
- checks2 = checks2.filter((c) => isCheckEnabled(c, config));
5538
- return { checks: checks2, unmatched };
5250
+ function selectChecks(opts, config) {
5251
+ const unmatched = [];
5252
+ let checks2;
5253
+ if (opts.only && opts.only.length > 0) {
5254
+ const resolved = resolveCheckSelectors(opts.only);
5255
+ checks2 = getChecks({ ids: resolved.resolved });
5256
+ unmatched.push(...resolved.unmatched);
5257
+ } else {
5258
+ checks2 = getChecks({ category: opts.category, phase: opts.phase });
5259
+ }
5260
+ if (opts.skip && opts.skip.length > 0) {
5261
+ const resolved = resolveCheckSelectors(opts.skip);
5262
+ const skipSet = new Set(resolved.resolved);
5263
+ checks2 = checks2.filter((c) => !skipSet.has(c.id));
5264
+ unmatched.push(...resolved.unmatched);
5265
+ }
5266
+ checks2 = checks2.filter((c) => isCheckEnabled(c, config));
5267
+ return { checks: checks2, unmatched };
5539
5268
  }
5540
5269
  function finding(checkId, init) {
5541
5270
  return { checkId, ...init };
@@ -5551,7 +5280,7 @@ function inspectionFailed(checkId, message, extra = {}) {
5551
5280
  }
5552
5281
 
5553
5282
  // src/lib/pdf-inspect.ts
5554
- import { stat, readFile as readFile10 } from "node:fs/promises";
5283
+ import { stat, readFile as readFile9 } from "node:fs/promises";
5555
5284
  import { getDocumentProxy } from "unpdf";
5556
5285
  import * as pdfjs from "unpdf/pdfjs";
5557
5286
  function getOps(mod) {
@@ -5606,7 +5335,7 @@ async function loadPdf(path7) {
5606
5335
  }
5607
5336
  if (hit)
5608
5337
  destroyEntry(hit);
5609
- const docPromise = readFile10(path7).then((buf) => getDocumentProxy(new Uint8Array(buf)));
5338
+ const docPromise = readFile9(path7).then((buf) => getDocumentProxy(new Uint8Array(buf)));
5610
5339
  docCache.delete(path7);
5611
5340
  docCache.set(path7, { mtimeMs: st.mtimeMs, size: st.size, doc: docPromise });
5612
5341
  evictLru();
@@ -5804,12 +5533,12 @@ function getOpPass(doc) {
5804
5533
  return p;
5805
5534
  }
5806
5535
  function getImagePixelSize(page, name, timeoutMs = 2000) {
5807
- return new Promise((resolve6) => {
5536
+ return new Promise((resolve5) => {
5808
5537
  let done = false;
5809
5538
  const finish = (v) => {
5810
5539
  if (!done) {
5811
5540
  done = true;
5812
- resolve6(v);
5541
+ resolve5(v);
5813
5542
  }
5814
5543
  };
5815
5544
  const t = setTimeout(() => finish(null), timeoutMs);
@@ -6065,9 +5794,9 @@ var check3 = {
6065
5794
  registerCheck(check3);
6066
5795
 
6067
5796
  // src/lib/pdf-parse.ts
6068
- import { readFile as readFile11 } from "node:fs/promises";
5797
+ import { readFile as readFile10 } from "node:fs/promises";
6069
5798
  async function readPdfBytes(pdfPath) {
6070
- return readFile11(pdfPath, "latin1");
5799
+ return readFile10(pdfPath, "latin1");
6071
5800
  }
6072
5801
  function parseInkCov(out) {
6073
5802
  const lines = out.split(/\r?\n/).filter((l) => l.includes("CMYK"));
@@ -6638,21 +6367,21 @@ var check15 = {
6638
6367
  registerCheck(check15);
6639
6368
 
6640
6369
  // src/checks/source/markdownlint.ts
6641
- import { readFile as readFile12 } from "node:fs/promises";
6370
+ import { readFile as readFile11 } from "node:fs/promises";
6642
6371
  import { lint } from "markdownlint/sync";
6643
6372
  import { parse as parseYaml2 } from "yaml";
6644
6373
 
6645
6374
  // src/checks/source/config-file.ts
6646
- import { existsSync as existsSync7 } from "node:fs";
6647
- import { resolve as resolve6 } from "node:path";
6375
+ import { existsSync as existsSync6 } from "node:fs";
6376
+ import { resolve as resolve5 } from "node:path";
6648
6377
  function findConfigFile(inputDir, names, explicit) {
6649
6378
  if (explicit) {
6650
- const p = resolve6(inputDir, explicit);
6651
- return existsSync7(p) ? p : null;
6379
+ const p = resolve5(inputDir, explicit);
6380
+ return existsSync6(p) ? p : null;
6652
6381
  }
6653
6382
  for (const name of names) {
6654
- const p = resolve6(inputDir, name);
6655
- if (existsSync7(p))
6383
+ const p = resolve5(inputDir, name);
6384
+ if (existsSync6(p))
6656
6385
  return p;
6657
6386
  }
6658
6387
  return null;
@@ -6671,7 +6400,7 @@ function stripJsonComments(s) {
6671
6400
  return s.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
6672
6401
  }
6673
6402
  async function loadConfig(configPath) {
6674
- const raw = await readFile12(configPath, "utf8");
6403
+ const raw = await readFile11(configPath, "utf8");
6675
6404
  let parsed;
6676
6405
  if (configPath.endsWith(".json")) {
6677
6406
  parsed = JSON.parse(raw);
@@ -6680,94 +6409,352 @@ async function loadConfig(configPath) {
6680
6409
  } else {
6681
6410
  parsed = parseYaml2(raw);
6682
6411
  }
6683
- if (parsed && typeof parsed === "object") {
6684
- const obj = parsed;
6685
- if (obj.config && typeof obj.config === "object") {
6686
- return obj.config;
6412
+ if (parsed && typeof parsed === "object") {
6413
+ const obj = parsed;
6414
+ if (obj.config && typeof obj.config === "object") {
6415
+ return obj.config;
6416
+ }
6417
+ return obj;
6418
+ }
6419
+ return {};
6420
+ }
6421
+ var check16 = {
6422
+ id: "source.markdownlint",
6423
+ name: "Markdownlint",
6424
+ description: "Runs markdownlint with project config to validate Markdown files",
6425
+ category: "source",
6426
+ phase: "pre-build",
6427
+ async run(ctx) {
6428
+ const sourceConfig = ctx.config.validate.source;
6429
+ if (sourceConfig.markdownlint === false)
6430
+ return [];
6431
+ const files = ctx.markdownFiles;
6432
+ if (!files || files.length === 0)
6433
+ return [];
6434
+ const configPath = typeof sourceConfig.markdownlint === "string" ? sourceConfig.markdownlint : null;
6435
+ const resolvedConfig = findConfigFile(ctx.inputDir, CONFIG_NAMES, configPath);
6436
+ if (!resolvedConfig && !configPath)
6437
+ return [];
6438
+ const config = resolvedConfig ? await loadConfig(resolvedConfig) : { default: true };
6439
+ const results = lint({ files, config });
6440
+ const out = [];
6441
+ for (const [file, violations] of Object.entries(results)) {
6442
+ if (!Array.isArray(violations))
6443
+ continue;
6444
+ for (const v of violations) {
6445
+ out.push({
6446
+ checkId: check16.id,
6447
+ severity: "warning",
6448
+ message: `${v.ruleDescription} (${v.ruleNames.join("/")})`,
6449
+ file,
6450
+ line: v.lineNumber,
6451
+ column: v.errorRange?.[0],
6452
+ code: v.ruleNames[0]
6453
+ });
6454
+ }
6455
+ }
6456
+ return out;
6457
+ }
6458
+ };
6459
+ registerCheck(check16);
6460
+
6461
+ // src/checks/source/htmlhint.ts
6462
+ import { existsSync as existsSync7 } from "node:fs";
6463
+ import { readFile as readFile12 } from "node:fs/promises";
6464
+ import htmlhint from "htmlhint";
6465
+ var { HTMLHint } = htmlhint;
6466
+ var CONFIG_NAMES2 = [".htmlhintrc"];
6467
+ var check17 = {
6468
+ id: "source.htmlhint",
6469
+ name: "HTMLHint",
6470
+ description: "Runs htmlhint with project config to validate generated HTML",
6471
+ category: "source",
6472
+ phase: "pre-build",
6473
+ async run(ctx) {
6474
+ const sourceConfig = ctx.config.validate.source;
6475
+ if (sourceConfig.htmlhint === false)
6476
+ return [];
6477
+ if (!ctx.htmlPath || !existsSync7(ctx.htmlPath))
6478
+ return [];
6479
+ const configPath = typeof sourceConfig.htmlhint === "string" ? sourceConfig.htmlhint : null;
6480
+ const resolvedConfig = findConfigFile(ctx.inputDir, CONFIG_NAMES2, configPath);
6481
+ if (!resolvedConfig && !configPath)
6482
+ return [];
6483
+ const ruleset = resolvedConfig ? JSON.parse(await readFile12(resolvedConfig, "utf8")) : HTMLHint.defaultRuleset;
6484
+ const html = await readFile12(ctx.htmlPath, "utf8");
6485
+ const messages = HTMLHint.verify(html, ruleset);
6486
+ return messages.map((m) => ({
6487
+ checkId: check17.id,
6488
+ severity: m.type === "error" ? "error" : "warning",
6489
+ message: `${m.rule.id}: ${m.message}`,
6490
+ file: ctx.htmlPath,
6491
+ line: m.line,
6492
+ column: m.col
6493
+ }));
6494
+ }
6495
+ };
6496
+ registerCheck(check17);
6497
+
6498
+ // src/checks/source/stylelint.ts
6499
+ import { readFile as readFile13 } from "node:fs/promises";
6500
+
6501
+ // src/lib/printsafe.ts
6502
+ import postcss2 from "postcss";
6503
+ var ruleRemoteUrls = "printsafe/no-remote-urls";
6504
+ var ruleRiskyProps = "printsafe/no-risky-print-effects";
6505
+ var ruleSyntax = "printsafe/syntax-error";
6506
+ var rulePageContainment = "printsafe/page-containment";
6507
+ function extractUrls(value) {
6508
+ const urls = [];
6509
+ const re = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
6510
+ let m;
6511
+ while (m = re.exec(value))
6512
+ urls.push(m[2] ?? "");
6513
+ return urls;
6514
+ }
6515
+ function isRemoteUrl(url) {
6516
+ const lower = url.trim().toLowerCase();
6517
+ return lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("//");
6518
+ }
6519
+ var riskyProperties = new Set([
6520
+ "filter",
6521
+ "backdrop-filter",
6522
+ "mix-blend-mode",
6523
+ "background-blend-mode",
6524
+ "animation",
6525
+ "animation-name",
6526
+ "transition",
6527
+ "will-change",
6528
+ "clip-path"
6529
+ ]);
6530
+ var marginBoxAtRuleNames = new Set([
6531
+ "top-left-corner",
6532
+ "top-left",
6533
+ "top-center",
6534
+ "top-right",
6535
+ "top-right-corner",
6536
+ "bottom-left-corner",
6537
+ "bottom-left",
6538
+ "bottom-center",
6539
+ "bottom-right",
6540
+ "bottom-right-corner",
6541
+ "left-top",
6542
+ "left-middle",
6543
+ "left-bottom",
6544
+ "right-top",
6545
+ "right-middle",
6546
+ "right-bottom"
6547
+ ]);
6548
+ var marginBoxIgnoredProperties = MARGIN_BOX_IGNORED_PROPERTIES;
6549
+ function isInPageBox(decl) {
6550
+ const page = decl.parent;
6551
+ return !!page && page.type === "atrule" && page.name.toLowerCase() === "page";
6552
+ }
6553
+ function isDroppedPageGradient(decl) {
6554
+ const prop = decl.prop.toLowerCase();
6555
+ if (prop !== "background" && prop !== "background-image")
6556
+ return false;
6557
+ if (!/(^|[\s,(])[\w-]*gradient\(/i.test(decl.value))
6558
+ return false;
6559
+ return isInPageBox(decl);
6560
+ }
6561
+ function isInPageMarginBox(decl) {
6562
+ const box = decl.parent;
6563
+ if (!box || box.type !== "atrule")
6564
+ return false;
6565
+ if (!marginBoxAtRuleNames.has(box.name.toLowerCase()))
6566
+ return false;
6567
+ const page = box.parent;
6568
+ return !!page && page.type === "atrule" && page.name.toLowerCase() === "page";
6569
+ }
6570
+ function isUnpairedFitContentWidth(decl) {
6571
+ const prop = decl.prop.toLowerCase();
6572
+ if (prop !== "width" && prop !== "inline-size")
6573
+ return false;
6574
+ if (!/\bfit-content\b/i.test(decl.value))
6575
+ return false;
6576
+ if (!isInPageMarginBox(decl))
6577
+ return false;
6578
+ const siblings = decl.parent.nodes ?? [];
6579
+ const decls = siblings.filter((n) => n.type === "decl");
6580
+ if (decls.some((d) => ["height", "block-size"].includes(d.prop.toLowerCase())))
6581
+ return false;
6582
+ if (decls.some((d) => /gradient\(/i.test(d.value)))
6583
+ return false;
6584
+ return true;
6585
+ }
6586
+ function targetsPageWrapper(selector) {
6587
+ if (!/\.(page|spread)(?![\w-])/.test(selector))
6588
+ return false;
6589
+ return !/\bgp-(bleed|pin)\b/.test(selector);
6590
+ }
6591
+ var cssWideResetKeywords = new Set(["initial", "unset", "revert", "revert-layer"]);
6592
+ var inertValues = {
6593
+ "z-index": new Set(["auto"]),
6594
+ opacity: new Set(["1", "100%"]),
6595
+ "mix-blend-mode": new Set(["normal"]),
6596
+ "background-blend-mode": new Set(["normal"]),
6597
+ filter: new Set(["none"]),
6598
+ "backdrop-filter": new Set(["none"]),
6599
+ transform: new Set(["none"]),
6600
+ rotate: new Set(["none"]),
6601
+ translate: new Set(["none"]),
6602
+ scale: new Set(["none"]),
6603
+ perspective: new Set(["none"]),
6604
+ "box-shadow": new Set(["none"]),
6605
+ outline: new Set(["none"]),
6606
+ "outline-style": new Set(["none"]),
6607
+ "will-change": new Set(["auto", "none"]),
6608
+ "clip-path": new Set(["none"]),
6609
+ transition: new Set(["none"]),
6610
+ animation: new Set(["none"]),
6611
+ "animation-name": new Set(["none"]),
6612
+ overflow: new Set(["visible"]),
6613
+ "overflow-x": new Set(["visible"]),
6614
+ "overflow-y": new Set(["visible"])
6615
+ };
6616
+ function isInertValue(prop, value) {
6617
+ const v = value.trim().toLowerCase();
6618
+ return cssWideResetKeywords.has(v) || (inertValues[prop]?.has(v) ?? false);
6619
+ }
6620
+ function createsStackingContext(decl) {
6621
+ const p = decl.prop.toLowerCase();
6622
+ const v = decl.value.trim().toLowerCase();
6623
+ if (isInertValue(p, v))
6624
+ return false;
6625
+ switch (p) {
6626
+ case "isolation":
6627
+ return v === "isolate";
6628
+ case "contain":
6629
+ return /\b(paint|layout|strict|content)\b/.test(v);
6630
+ case "z-index":
6631
+ case "opacity":
6632
+ case "mix-blend-mode":
6633
+ case "filter":
6634
+ case "backdrop-filter":
6635
+ case "transform":
6636
+ case "perspective":
6637
+ case "will-change":
6638
+ return true;
6639
+ default:
6640
+ return false;
6641
+ }
6642
+ }
6643
+ function clipsDescendants(decl) {
6644
+ const p = decl.prop.toLowerCase();
6645
+ if (p !== "overflow" && p !== "overflow-x" && p !== "overflow-y")
6646
+ return false;
6647
+ const v = decl.value.trim().toLowerCase();
6648
+ return v !== "" && !isInertValue(p, v);
6649
+ }
6650
+ function nodeLoc(node) {
6651
+ return {
6652
+ line: node.source?.start?.line ?? 1,
6653
+ column: node.source?.start?.column ?? 1
6654
+ };
6655
+ }
6656
+ function checkCss(css, from) {
6657
+ let root;
6658
+ try {
6659
+ root = postcss2.parse(css, from ? { from } : undefined);
6660
+ } catch (err) {
6661
+ const e = err;
6662
+ return [
6663
+ {
6664
+ rule: ruleSyntax,
6665
+ severity: "error",
6666
+ message: `CSS syntax error: ${e.reason ?? e.message ?? "unparseable"}`,
6667
+ line: e.line ?? 1,
6668
+ column: e.column ?? 1
6669
+ }
6670
+ ];
6671
+ }
6672
+ const warnings = [];
6673
+ const reportRemoteUrls = (value, node) => {
6674
+ for (const url of extractUrls(value)) {
6675
+ if (isRemoteUrl(url)) {
6676
+ warnings.push({
6677
+ rule: ruleRemoteUrls,
6678
+ severity: "error",
6679
+ message: `Remote URL is not allowed in print CSS: ${url}`,
6680
+ ...nodeLoc(node)
6681
+ });
6682
+ }
6687
6683
  }
6688
- return obj;
6689
- }
6690
- return {};
6691
- }
6692
- var check16 = {
6693
- id: "source.markdownlint",
6694
- name: "Markdownlint",
6695
- description: "Runs markdownlint with project config to validate Markdown files",
6696
- category: "source",
6697
- phase: "pre-build",
6698
- async run(ctx) {
6699
- const sourceConfig = ctx.config.validate.source;
6700
- if (sourceConfig.markdownlint === false)
6701
- return [];
6702
- const files = ctx.markdownFiles;
6703
- if (!files || files.length === 0)
6704
- return [];
6705
- const configPath = typeof sourceConfig.markdownlint === "string" ? sourceConfig.markdownlint : null;
6706
- const resolvedConfig = findConfigFile(ctx.inputDir, CONFIG_NAMES, configPath);
6707
- if (!resolvedConfig && !configPath)
6708
- return [];
6709
- const config = resolvedConfig ? await loadConfig(resolvedConfig) : { default: true };
6710
- const results = lint({ files, config });
6711
- const out = [];
6712
- for (const [file, violations] of Object.entries(results)) {
6713
- if (!Array.isArray(violations))
6714
- continue;
6715
- for (const v of violations) {
6716
- out.push({
6717
- checkId: check16.id,
6684
+ };
6685
+ root.walkDecls((decl) => reportRemoteUrls(decl.value || "", decl));
6686
+ root.walkAtRules((at) => reportRemoteUrls(at.params || "", at));
6687
+ root.walkDecls((decl) => {
6688
+ const prop = decl.prop.toLowerCase();
6689
+ if (isInertValue(prop, decl.value))
6690
+ return;
6691
+ if (marginBoxIgnoredProperties.has(prop) && isInPageMarginBox(decl)) {
6692
+ warnings.push({
6693
+ rule: ruleRiskyProps,
6694
+ severity: "warning",
6695
+ message: `Property "${decl.prop}" is not supported in Chromium @page margin boxes and is silently ignored — the chrome renders square/unshadowed. Anything that would paint outside the box (box-shadow, outline, filter) or make it a stacking context (transform, opacity, mix-blend-mode) is dropped; text-shadow, border, border-radius, padding and background gradients all work inside the box.`,
6696
+ ...nodeLoc(decl)
6697
+ });
6698
+ } else if (prop === "filter") {
6699
+ warnings.push({
6700
+ rule: ruleRiskyProps,
6701
+ severity: "warning",
6702
+ message: `Property is high-risk for print/PDF: 'filter' rasterizes its subtree to a 300 DPI bitmap in the printed PDF (text becomes unselectable, unsearchable, and inaccessible), and it dominates build time (~90% measured; 57.0s -> 6.2s over 60pp when scoped — see ENGINE.md §10). Scope it to the smallest possible selector. It also means the render-parity gate cannot see a text-only change inside it (docs/render-parity-gate.md, "Known blind spot").`,
6703
+ ...nodeLoc(decl)
6704
+ });
6705
+ } else if (riskyProperties.has(prop)) {
6706
+ warnings.push({
6707
+ rule: ruleRiskyProps,
6708
+ severity: "warning",
6709
+ message: `Property is high-risk for print/PDF (can force rasterization): ${decl.prop}`,
6710
+ ...nodeLoc(decl)
6711
+ });
6712
+ } else if (isDroppedPageGradient(decl)) {
6713
+ warnings.push({
6714
+ rule: ruleRiskyProps,
6715
+ severity: "warning",
6716
+ message: `"${decl.prop}: ${decl.value}" on @page paints nothing in print — Chromium drops a gradient in the @page box's own background, with no error and a valid-looking PDF. A solid colour there paints the whole sheet; the same gradient also works on "html" (it covers the content area, not the sheet) or in a margin box.`,
6717
+ ...nodeLoc(decl)
6718
+ });
6719
+ } else if (isUnpairedFitContentWidth(decl)) {
6720
+ warnings.push({
6721
+ rule: ruleRiskyProps,
6722
+ severity: "warning",
6723
+ message: `"${decl.prop}: fit-content" in a @page margin box collapses only the inline axis — the box stays stretched to the full margin band, so a chip/pill renders as a tall rectangle around its text. Add "height: fit-content" (align-self / vertical-align do nothing here).`,
6724
+ ...nodeLoc(decl)
6725
+ });
6726
+ }
6727
+ });
6728
+ root.walkRules((rule) => {
6729
+ if (rule.parent?.type === "atrule") {
6730
+ const at = rule.parent.name.toLowerCase();
6731
+ if (at === "page" || marginBoxAtRuleNames.has(at))
6732
+ return;
6733
+ }
6734
+ if (!targetsPageWrapper(rule.selector))
6735
+ return;
6736
+ rule.walkDecls((decl) => {
6737
+ if (clipsDescendants(decl)) {
6738
+ warnings.push({
6739
+ rule: rulePageContainment,
6718
6740
  severity: "warning",
6719
- message: `${v.ruleDescription} (${v.ruleNames.join("/")})`,
6720
- file,
6721
- line: v.lineNumber,
6722
- column: v.errorRange?.[0],
6723
- code: v.ruleNames[0]
6741
+ message: `"${decl.prop}: ${decl.value}" on "${rule.selector}" clips out-of-flow descendants: a .gp-bleed plate is cut back to this box's width instead of reaching the sheet edge, and a .gp-behind image is cut the same way. Scope it with :has() (e.g. "${rule.selector}:not(:has(.gp-bleed, .gp-pin))") or drop it. This is an early source-only check; the build-time engine.layer.trapped audit is authoritative for the live ancestor chain.`,
6742
+ ...nodeLoc(decl)
6743
+ });
6744
+ } else if (createsStackingContext(decl)) {
6745
+ warnings.push({
6746
+ rule: rulePageContainment,
6747
+ severity: "warning",
6748
+ message: `"${decl.prop}: ${decl.value}" on "${rule.selector}" makes the page box a stacking context, so a .gp-behind image is trapped inside it and paints ABOVE the page's text instead of under it. Core keeps .page/.spread at "position: relative; z-index: auto" for this reason. This is an early source-only check; the build-time engine.layer.trapped audit is authoritative for the live ancestor chain.`,
6749
+ ...nodeLoc(decl)
6724
6750
  });
6725
6751
  }
6726
- }
6727
- return out;
6728
- }
6729
- };
6730
- registerCheck(check16);
6731
-
6732
- // src/checks/source/htmlhint.ts
6733
- import { existsSync as existsSync8 } from "node:fs";
6734
- import { readFile as readFile13 } from "node:fs/promises";
6735
- import htmlhint from "htmlhint";
6736
- var { HTMLHint } = htmlhint;
6737
- var CONFIG_NAMES2 = [".htmlhintrc"];
6738
- var check17 = {
6739
- id: "source.htmlhint",
6740
- name: "HTMLHint",
6741
- description: "Runs htmlhint with project config to validate generated HTML",
6742
- category: "source",
6743
- phase: "pre-build",
6744
- async run(ctx) {
6745
- const sourceConfig = ctx.config.validate.source;
6746
- if (sourceConfig.htmlhint === false)
6747
- return [];
6748
- if (!ctx.htmlPath || !existsSync8(ctx.htmlPath))
6749
- return [];
6750
- const configPath = typeof sourceConfig.htmlhint === "string" ? sourceConfig.htmlhint : null;
6751
- const resolvedConfig = findConfigFile(ctx.inputDir, CONFIG_NAMES2, configPath);
6752
- if (!resolvedConfig && !configPath)
6753
- return [];
6754
- const ruleset = resolvedConfig ? JSON.parse(await readFile13(resolvedConfig, "utf8")) : HTMLHint.defaultRuleset;
6755
- const html = await readFile13(ctx.htmlPath, "utf8");
6756
- const messages = HTMLHint.verify(html, ruleset);
6757
- return messages.map((m) => ({
6758
- checkId: check17.id,
6759
- severity: m.type === "error" ? "error" : "warning",
6760
- message: `${m.rule.id}: ${m.message}`,
6761
- file: ctx.htmlPath,
6762
- line: m.line,
6763
- column: m.col
6764
- }));
6765
- }
6766
- };
6767
- registerCheck(check17);
6752
+ });
6753
+ });
6754
+ return warnings;
6755
+ }
6768
6756
 
6769
6757
  // src/checks/source/stylelint.ts
6770
- import { readFile as readFile14 } from "node:fs/promises";
6771
6758
  var check18 = {
6772
6759
  id: "source.stylelint",
6773
6760
  name: "Print-safety CSS",
@@ -6785,8 +6772,14 @@ var check18 = {
6785
6772
  for (const file of files) {
6786
6773
  let css;
6787
6774
  try {
6788
- css = await readFile14(file, "utf8");
6789
- } catch {
6775
+ css = await readFile13(file, "utf8");
6776
+ } catch (err) {
6777
+ results.push({
6778
+ checkId: check18.id,
6779
+ severity: "error",
6780
+ message: `Cannot read stylesheet: ${err instanceof Error ? err.message : String(err)}`,
6781
+ file
6782
+ });
6790
6783
  continue;
6791
6784
  }
6792
6785
  for (const w of checkCss(css, file)) {
@@ -6806,12 +6799,12 @@ var check18 = {
6806
6799
  registerCheck(check18);
6807
6800
 
6808
6801
  // src/checks/source/css-ownership.ts
6809
- import { readFile as readFile15 } from "node:fs/promises";
6802
+ import { readFile as readFile14 } from "node:fs/promises";
6810
6803
  import postcss3 from "postcss";
6811
6804
  import { parse as parseYaml3 } from "yaml";
6812
6805
  var CONFIG_NAMES3 = [".gutterpress/css-contract.yaml", ".gutterpress/css-contract.yml"];
6813
6806
  async function loadContract(configPath) {
6814
- const raw = await readFile15(configPath, "utf8");
6807
+ const raw = await readFile14(configPath, "utf8");
6815
6808
  const parsed = parseYaml3(raw);
6816
6809
  if (parsed && typeof parsed === "object")
6817
6810
  return parsed;
@@ -6962,7 +6955,7 @@ var check19 = {
6962
6955
  continue;
6963
6956
  let css;
6964
6957
  try {
6965
- css = await readFile15(absFile, "utf8");
6958
+ css = await readFile14(absFile, "utf8");
6966
6959
  } catch {
6967
6960
  continue;
6968
6961
  }
@@ -7004,11 +6997,11 @@ var check19 = {
7004
6997
  registerCheck(check19);
7005
6998
 
7006
6999
  // src/checks/source/local-refs.ts
7007
- import { existsSync as existsSync9 } from "node:fs";
7008
- import { dirname as dirname3, resolve as resolve7 } from "node:path";
7000
+ import { existsSync as existsSync8 } from "node:fs";
7001
+ import { dirname as dirname3, resolve as resolve6 } from "node:path";
7009
7002
 
7010
7003
  // src/checks/source/local-ref-parser.ts
7011
- import { readFile as readFile16 } from "node:fs/promises";
7004
+ import { readFile as readFile15 } from "node:fs/promises";
7012
7005
  function newlineOffsets(content) {
7013
7006
  const offsets = [];
7014
7007
  for (let i = 0;i < content.length; i++)
@@ -7077,7 +7070,7 @@ async function* renderedLocalRefs(ctx, checkId, results) {
7077
7070
  for (const file of files) {
7078
7071
  let refs;
7079
7072
  try {
7080
- refs = collectRenderedLocalRefs(await readFile16(file, "utf8"));
7073
+ refs = collectRenderedLocalRefs(await readFile15(file, "utf8"));
7081
7074
  } catch {
7082
7075
  results.push(inspectionFailed(checkId, `Could not read source file: ${file}`, { file }));
7083
7076
  continue;
@@ -7127,13 +7120,13 @@ function filesystemRef(ref) {
7127
7120
  }
7128
7121
  function localRefExists(ref, kind, sourceFile, projectRoot) {
7129
7122
  const cleaned = filesystemRef(ref);
7130
- const fileRelative = resolve7(dirname3(sourceFile), cleaned);
7131
- const rootRelative = resolve7(projectRoot, cleaned);
7123
+ const fileRelative = resolve6(dirname3(sourceFile), cleaned);
7124
+ const rootRelative = resolve6(projectRoot, cleaned);
7132
7125
  switch (kind) {
7133
7126
  case "image":
7134
- return existsSync9(rootRelative);
7127
+ return existsSync8(rootRelative);
7135
7128
  case "link":
7136
- return existsSync9(fileRelative);
7129
+ return existsSync8(fileRelative);
7137
7130
  }
7138
7131
  }
7139
7132
  registerCheck(check20);
@@ -7428,7 +7421,7 @@ var check21 = {
7428
7421
  registerCheck(check21);
7429
7422
 
7430
7423
  // src/checks/source/accessibility-alt-text.ts
7431
- import { readFile as readFile17 } from "node:fs/promises";
7424
+ import { readFile as readFile16 } from "node:fs/promises";
7432
7425
  var check22 = {
7433
7426
  id: "source.accessibility.alt-text",
7434
7427
  name: "Image Alt Text",
@@ -7442,7 +7435,7 @@ var check22 = {
7442
7435
  const results = [];
7443
7436
  for (const file of files) {
7444
7437
  try {
7445
- const content = await readFile17(file, "utf8");
7438
+ const content = await readFile16(file, "utf8");
7446
7439
  const lines = content.split(`
7447
7440
  `);
7448
7441
  let inFence = false;
@@ -7486,7 +7479,7 @@ function extractImageAlts(line) {
7486
7479
  registerCheck(check22);
7487
7480
 
7488
7481
  // src/checks/source/accessibility-heading-order.ts
7489
- import { readFile as readFile18 } from "node:fs/promises";
7482
+ import { readFile as readFile17 } from "node:fs/promises";
7490
7483
  var check23 = {
7491
7484
  id: "source.accessibility.heading-order",
7492
7485
  name: "Heading Order",
@@ -7500,7 +7493,7 @@ var check23 = {
7500
7493
  const results = [];
7501
7494
  for (const file of files) {
7502
7495
  try {
7503
- const content = await readFile18(file, "utf8");
7496
+ const content = await readFile17(file, "utf8");
7504
7497
  const lines = content.split(`
7505
7498
  `);
7506
7499
  let inFence = false;
@@ -7536,7 +7529,7 @@ var check23 = {
7536
7529
  registerCheck(check23);
7537
7530
 
7538
7531
  // src/checks/source/layout-markers.ts
7539
- import { readFile as readFile19 } from "node:fs/promises";
7532
+ import { readFile as readFile18 } from "node:fs/promises";
7540
7533
  var check24 = {
7541
7534
  id: "source.markdown.layout-markers",
7542
7535
  name: "Layout Markers",
@@ -7554,7 +7547,7 @@ var check24 = {
7554
7547
  const md = createMarkdownRenderer(plugins);
7555
7548
  for (const file of files) {
7556
7549
  try {
7557
- const content = await readFile19(file, "utf8");
7550
+ const content = await readFile18(file, "utf8");
7558
7551
  const env = {};
7559
7552
  md.render(content, env);
7560
7553
  for (const w of env.layoutWarnings ?? []) {
@@ -7576,7 +7569,7 @@ var check24 = {
7576
7569
  registerCheck(check24);
7577
7570
 
7578
7571
  // src/checks/source/merge-markers.ts
7579
- import { readFile as readFile20 } from "node:fs/promises";
7572
+ import { readFile as readFile19 } from "node:fs/promises";
7580
7573
  import path8 from "node:path";
7581
7574
 
7582
7575
  // src/checks/asset/extensions.ts
@@ -7618,7 +7611,7 @@ var check25 = {
7618
7611
  for (const file of textFiles) {
7619
7612
  let content;
7620
7613
  try {
7621
- content = await readFile20(file, "utf8");
7614
+ content = await readFile19(file, "utf8");
7622
7615
  } catch {
7623
7616
  results.push(inspectionFailed(check25.id, `Could not read source file: ${file}`, { file }));
7624
7617
  continue;
@@ -7681,7 +7674,7 @@ registerCheck(check25);
7681
7674
  import { stat as stat3 } from "node:fs/promises";
7682
7675
 
7683
7676
  // src/lib/image-inspect.ts
7684
- import { stat as stat2, readFile as readFile21 } from "node:fs/promises";
7677
+ import { stat as stat2, readFile as readFile20 } from "node:fs/promises";
7685
7678
  var DEFAULT_DPI = 72;
7686
7679
  var cache = new Map;
7687
7680
  async function inspectImage(path9) {
@@ -7696,7 +7689,7 @@ async function inspectImage(path9) {
7696
7689
  return hit.info;
7697
7690
  let info2 = null;
7698
7691
  try {
7699
- const buf = await readFile21(path9);
7692
+ const buf = await readFile20(path9);
7700
7693
  info2 = parseImage(buf);
7701
7694
  } catch {
7702
7695
  info2 = null;
@@ -8155,8 +8148,8 @@ var check31 = {
8155
8148
  registerCheck(check31);
8156
8149
 
8157
8150
  // src/checks/asset/font-license.ts
8158
- import { existsSync as existsSync10 } from "node:fs";
8159
- import { resolve as resolve8 } from "node:path";
8151
+ import { existsSync as existsSync9 } from "node:fs";
8152
+ import { resolve as resolve7 } from "node:path";
8160
8153
  var LICENSE_NAMES = [
8161
8154
  "LICENSE",
8162
8155
  "LICENSE.txt",
@@ -8186,12 +8179,12 @@ var check32 = {
8186
8179
  ignore: [...ASSET_SCAN_IGNORE_GLOBS]
8187
8180
  });
8188
8181
  for (const m of matches) {
8189
- fontDirs.add(resolve8(m, ".."));
8182
+ fontDirs.add(resolve7(m, ".."));
8190
8183
  }
8191
8184
  }
8192
8185
  const results = [];
8193
8186
  for (const fontDir of fontDirs) {
8194
- const hasLicense = LICENSE_NAMES.some((name) => existsSync10(resolve8(fontDir, name)));
8187
+ const hasLicense = LICENSE_NAMES.some((name) => existsSync9(resolve7(fontDir, name)));
8195
8188
  if (!hasLicense) {
8196
8189
  results.push({
8197
8190
  checkId: check32.id,
@@ -8256,7 +8249,7 @@ var check33 = {
8256
8249
  registerCheck(check33);
8257
8250
 
8258
8251
  // src/checks/heuristic/section-density.ts
8259
- import { readFile as readFile22 } from "node:fs/promises";
8252
+ import { readFile as readFile21 } from "node:fs/promises";
8260
8253
  var check34 = {
8261
8254
  id: "heuristic.chunking.section-density",
8262
8255
  name: "Section Density",
@@ -8273,7 +8266,7 @@ var check34 = {
8273
8266
  const results = [];
8274
8267
  for (const file of files) {
8275
8268
  try {
8276
- const content = await readFile22(file, "utf8");
8269
+ const content = await readFile21(file, "utf8");
8277
8270
  const lines = content.split(`
8278
8271
  `);
8279
8272
  let currentSectionStart = 0;
@@ -8542,13 +8535,13 @@ async function sharedAssetDirs(manifestDir, relStyles, inputDir) {
8542
8535
  } catch {
8543
8536
  return [];
8544
8537
  }
8545
- const projectRoot = resolve9(inputDir);
8538
+ const projectRoot = resolve8(inputDir);
8546
8539
  const dirs = new Set;
8547
8540
  for (const file of closure) {
8548
8541
  const ext = file.slice(file.lastIndexOf(".")).toLowerCase();
8549
8542
  if (!SHIPPED_ASSET_EXTS.has(ext))
8550
8543
  continue;
8551
- const dir = dirname4(resolve9(file));
8544
+ const dir = dirname4(resolve8(file));
8552
8545
  if (escapesProjectRoot(projectRoot, dir))
8553
8546
  dirs.add(dir);
8554
8547
  }
@@ -8709,11 +8702,11 @@ function planRuns(opts) {
8709
8702
  async function executeValidation(args) {
8710
8703
  const manifestPath = typeof args.manifest === "string" ? args.manifest : undefined;
8711
8704
  const { manifest, manifestDir } = await loadManifestWithPath(manifestPath ?? args.input ?? undefined, { explicit: manifestPath !== undefined });
8712
- const config = resolveConfig({}, manifest);
8705
+ const config = resolveConfig(args.skipStylelint ? { validate: { source: { stylelint: false } } } : {}, manifest);
8713
8706
  const targetIds = resolveTargets(parseCsv(typeof args.target === "string" ? args.target : undefined), config.targets);
8714
- const pdfPath = typeof args.pdf === "string" ? resolve9(args.pdf) : undefined;
8715
- const inputDir = typeof args.input === "string" ? resolve9(args.input) : undefined;
8716
- if (pdfPath && !existsSync11(pdfPath)) {
8707
+ const pdfPath = typeof args.pdf === "string" ? resolve8(args.pdf) : undefined;
8708
+ const inputDir = typeof args.input === "string" ? resolve8(args.input) : undefined;
8709
+ if (pdfPath && !existsSync10(pdfPath)) {
8717
8710
  throw new Error(`File not found: ${pdfPath}`);
8718
8711
  }
8719
8712
  const categories = resolveCategoryArg(typeof args.category === "string" ? args.category : undefined);
@@ -8744,7 +8737,7 @@ async function executeValidation(args) {
8744
8737
  const relMarkdown = await resolveActiveMarkdownFiles(manifestDir, config.source.files);
8745
8738
  markdownFiles = relMarkdown.map((f) => join4(manifestDir, canonicalChapterId(f)));
8746
8739
  const relStyles = await resolveActiveStyles(manifestDir, config.styles);
8747
- const projectCssFiles = relStyles.map((rel) => resolve9(manifestDir, rel));
8740
+ const projectCssFiles = relStyles.map((rel) => resolve8(manifestDir, rel));
8748
8741
  let pluginStylePaths = args.pluginStylePaths;
8749
8742
  if (pluginStylePaths === undefined) {
8750
8743
  ({ pluginStylePaths } = await loadPluginsWithCss(config.extensions, manifestDir, (ref, err) => log.warn(`Skipping plugin "${ref}" for validation — ${err.message}`)));
@@ -8752,7 +8745,7 @@ async function executeValidation(args) {
8752
8745
  cssFiles = [...projectCssFiles, ...pluginStylePaths].filter((f) => !f.endsWith(".min.css"));
8753
8746
  assetDirs = [inputDir, ...await sharedAssetDirs(manifestDir, relStyles, inputDir)];
8754
8747
  const possibleHtml = join4(outDir, BOOK_HTML_FILENAME);
8755
- if (existsSync11(possibleHtml)) {
8748
+ if (existsSync10(possibleHtml)) {
8756
8749
  htmlPath = possibleHtml;
8757
8750
  }
8758
8751
  }
@@ -8901,12 +8894,12 @@ function computeGates(format, opts, config) {
8901
8894
  if (opts.skipLint || opts.skipPreValidate || opts.skipPostValidate) {
8902
8895
  log.info("Validation/lint flags ignored for --format html (no validation phases apply)");
8903
8896
  }
8904
- return { lint: false, preValidate: false, postValidate: false };
8897
+ return { skipStylelint: false, preValidate: false, postValidate: false };
8905
8898
  }
8906
- const lint2 = !opts.skipLint && config.lint.enabled !== false;
8899
+ const skipStylelint = !!opts.skipLint || config.lint.enabled === false;
8907
8900
  const preValidate = !opts.skipPreValidate && config.validate.enabled !== false;
8908
8901
  const postValidate = format === "pdfx" && !opts.skipPostValidate && config.validate.enabled !== false;
8909
- return { lint: lint2, preValidate, postValidate };
8902
+ return { skipStylelint, preValidate, postValidate };
8910
8903
  }
8911
8904
 
8912
8905
  // src/lib/build-runner.ts
@@ -8974,24 +8967,15 @@ async function loadBuildPlugins(ctx) {
8974
8967
  }
8975
8968
  async function runQualityGates(ctx) {
8976
8969
  const { gates, opts, renderDir } = ctx;
8977
- const pluginStylePaths = gates.lint || gates.preValidate ? (await loadBuildPlugins(ctx)).pluginStylePaths : undefined;
8978
- if (gates.lint) {
8979
- log.info("Lint: CSS print-safety");
8980
- const lintResult = await runLint({
8981
- manifest: opts.manifestPath ?? renderDir,
8982
- pluginStylePaths
8983
- });
8984
- if (!lintResult.ok) {
8985
- throw new BuildError("CSS lint failed", 2);
8986
- }
8987
- }
8970
+ const pluginStylePaths = gates.preValidate ? (await loadBuildPlugins(ctx)).pluginStylePaths : undefined;
8988
8971
  if (gates.preValidate) {
8989
8972
  log.info("Pre-build validation");
8990
8973
  const result = await executeAndReport({
8991
8974
  input: renderDir,
8992
8975
  phase: "pre-build",
8993
8976
  manifest: opts.manifestPath,
8994
- pluginStylePaths
8977
+ pluginStylePaths,
8978
+ skipStylelint: gates.skipStylelint
8995
8979
  }, "text");
8996
8980
  for (const finding2 of result.execution.report.results) {
8997
8981
  if (finding2.checkId === "source.markdown.layout-markers" && finding2.file && finding2.code !== "inspect-failed") {
@@ -9078,7 +9062,7 @@ async function finalizeBuild(ctx, fingerprint, wroteMessage, paths, artifactName
9078
9062
  recordedOutputDir: fingerprint.outputDir
9079
9063
  });
9080
9064
  await publishBuild(ctx, artifactName2);
9081
- const delivered = ctx.target.kind !== "file";
9065
+ const delivered = ctx.target.kind === "project" || ctx.target.kind === "directory" && ctx.format === "html";
9082
9066
  const fingerprintPath = delivered ? path9.join(ctx.outDir, path9.basename(workFingerprint)) : null;
9083
9067
  log.success(wroteMessage);
9084
9068
  if (fingerprintPath)
@@ -9091,6 +9075,16 @@ async function finalizeBuild(ctx, fingerprint, wroteMessage, paths, artifactName
9091
9075
  diagnostics
9092
9076
  };
9093
9077
  }
9078
+ async function publishToDirectory(workDir, targetDir, format, artifactName2) {
9079
+ await fsp2.mkdir(targetDir, { recursive: true });
9080
+ if (format !== "html") {
9081
+ if (!artifactName2)
9082
+ throw new BuildError("No artifact to write for this format", 1);
9083
+ await fsp2.copyFile(path9.join(workDir, artifactName2), path9.join(targetDir, artifactName2));
9084
+ return;
9085
+ }
9086
+ await fsp2.cp(workDir, targetDir, { recursive: true, force: true });
9087
+ }
9094
9088
  async function publishBuild(ctx, artifactName2) {
9095
9089
  const { workDir, target } = ctx;
9096
9090
  if (target.kind === "file") {
@@ -9101,8 +9095,7 @@ async function publishBuild(ctx, artifactName2) {
9101
9095
  return;
9102
9096
  }
9103
9097
  if (target.kind === "directory") {
9104
- await fsp2.mkdir(target.dir, { recursive: true });
9105
- await fsp2.cp(workDir, target.dir, { recursive: true, force: true });
9098
+ await publishToDirectory(workDir, target.dir, ctx.format, artifactName2);
9106
9099
  return;
9107
9100
  }
9108
9101
  for (const name of await listPdfs(target.dir)) {
@@ -9156,7 +9149,7 @@ class PdfOutput {
9156
9149
  const rawPdf = pdfxMode ? path9.join(stage, "raw.pdf") : path9.resolve(pdfFile);
9157
9150
  await fsp2.mkdir(path9.dirname(path9.resolve(pdfFile)), { recursive: true });
9158
9151
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
9159
- const { buildNativePdf } = await import("./engine-d46wgg99.js");
9152
+ const { buildNativePdf } = await import("./engine-pt2acjb5.js");
9160
9153
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
9161
9154
  title: config.title,
9162
9155
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
@@ -9250,7 +9243,7 @@ async function runBuild(opts) {
9250
9243
  import path11 from "path";
9251
9244
  import { tmpdir as tmpdir2 } from "os";
9252
9245
  import { randomBytes as randomBytes2 } from "crypto";
9253
- import { readdir as readdir5, readFile as readFile23, writeFile as writeFile5 } from "node:fs/promises";
9246
+ import { readdir as readdir5, readFile as readFile22, writeFile as writeFile5 } from "node:fs/promises";
9254
9247
 
9255
9248
  // src/utils/file-utils.ts
9256
9249
  import { promises as fs3 } from "fs";
@@ -9655,8 +9648,8 @@ function createFileWatcher(state) {
9655
9648
  immediatePending = false;
9656
9649
  state.isRebuilding = true;
9657
9650
  let resolveInFlight;
9658
- const inFlight = new Promise((resolve10) => {
9659
- resolveInFlight = resolve10;
9651
+ const inFlight = new Promise((resolve9) => {
9652
+ resolveInFlight = resolve9;
9660
9653
  });
9661
9654
  state.rebuildPromise = inFlight;
9662
9655
  try {
@@ -9760,7 +9753,7 @@ async function cleanupOrphanTempDirs() {
9760
9753
  const dir = path11.join(TEMP_DIR_BASE, entry);
9761
9754
  const pidFile = path11.join(dir, PID_FILE_NAME);
9762
9755
  try {
9763
- const raw = await readFile23(pidFile, "utf8");
9756
+ const raw = await readFile22(pidFile, "utf8");
9764
9757
  const pid = parseInt(raw.trim(), 10);
9765
9758
  if (!isProcessAlive(pid)) {
9766
9759
  debug(`Removing orphan temp dir ${dir} (pid ${pid} not alive)`);
@@ -9805,10 +9798,10 @@ async function restartPreview(newInputPath, state) {
9805
9798
  }
9806
9799
  async function withTimeout(p, ms, label) {
9807
9800
  let timer;
9808
- const timeout = new Promise((resolve10) => {
9801
+ const timeout = new Promise((resolve9) => {
9809
9802
  timer = setTimeout(() => {
9810
9803
  debug(`shutdownServer: ${label} exceeded ${ms}ms — continuing`);
9811
- resolve10(undefined);
9804
+ resolve9(undefined);
9812
9805
  }, ms);
9813
9806
  });
9814
9807
  try {
@@ -9866,7 +9859,7 @@ function createServerState(inputPath, tempDir, config, options) {
9866
9859
  import http from "node:http";
9867
9860
  import net from "node:net";
9868
9861
  import { createHash as createHash6, randomUUID } from "node:crypto";
9869
- import { readFile as readFile24, stat as stat4 } from "node:fs/promises";
9862
+ import { readFile as readFile23, stat as stat4 } from "node:fs/promises";
9870
9863
  import path13 from "path";
9871
9864
  import { WebSocket, WebSocketServer } from "ws";
9872
9865
 
@@ -9887,13 +9880,13 @@ function buildOpenPathSpawnSpec(filePath, platform = process.platform) {
9887
9880
  }
9888
9881
  }
9889
9882
  function openPath(filePath) {
9890
- return new Promise((resolve10, reject) => {
9883
+ return new Promise((resolve9, reject) => {
9891
9884
  const { cmd, args, options } = buildOpenPathSpawnSpec(filePath);
9892
9885
  const child = spawn(cmd, args, options);
9893
9886
  child.once("error", reject);
9894
9887
  child.once("spawn", () => {
9895
9888
  child.unref();
9896
- resolve10();
9889
+ resolve9();
9897
9890
  });
9898
9891
  });
9899
9892
  }
@@ -10124,7 +10117,7 @@ iframe{position:absolute;inset:0;width:100%;height:100%;border:0;display:block}<
10124
10117
  </body></html>`;
10125
10118
  }
10126
10119
  function probePort(port, host) {
10127
- return new Promise((resolve10) => {
10120
+ return new Promise((resolve9) => {
10128
10121
  const srv = net.createServer();
10129
10122
  srv.unref();
10130
10123
  let settled = false;
@@ -10132,7 +10125,7 @@ function probePort(port, host) {
10132
10125
  if (settled)
10133
10126
  return;
10134
10127
  settled = true;
10135
- resolve10(result);
10128
+ resolve9(result);
10136
10129
  };
10137
10130
  srv.once("error", (error2) => finish({ available: false, error: error2 }));
10138
10131
  try {
@@ -10184,7 +10177,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
10184
10177
  }
10185
10178
  let data;
10186
10179
  try {
10187
- data = await readFile24(filePath);
10180
+ data = await readFile23(filePath);
10188
10181
  } catch {
10189
10182
  res.writeHead(404);
10190
10183
  res.end("Not Found");
@@ -10234,7 +10227,7 @@ async function serveRevalidatedStatic(req, absPath, res, validators) {
10234
10227
  let data;
10235
10228
  if (!validator || validator.stamp !== stamp) {
10236
10229
  try {
10237
- data = await readFile24(absPath);
10230
+ data = await readFile23(absPath);
10238
10231
  } catch {
10239
10232
  await serveStatic(absPath, res);
10240
10233
  return;
@@ -10457,7 +10450,7 @@ async function createPreviewServer(state, port) {
10457
10450
  socket.destroy();
10458
10451
  }
10459
10452
  });
10460
- await new Promise((resolve10) => server.listen(port, state.options.host, resolve10));
10453
+ await new Promise((resolve9) => server.listen(port, state.options.host, resolve9));
10461
10454
  const boundPort = server.address().port;
10462
10455
  const serverUrl = `http://localhost:${boundPort}`;
10463
10456
  info(`Preview server running at ${serverUrl}`);
@@ -10498,11 +10491,11 @@ async function createPreviewServer(state, port) {
10498
10491
  }
10499
10492
  clients.clear();
10500
10493
  wss.close();
10501
- await new Promise((resolve10) => {
10494
+ await new Promise((resolve9) => {
10502
10495
  if (typeof server.closeAllConnections === "function") {
10503
10496
  server.closeAllConnections();
10504
10497
  }
10505
- server.close(() => resolve10());
10498
+ server.close(() => resolve9());
10506
10499
  });
10507
10500
  },
10508
10501
  broadcastReload() {
@@ -10575,7 +10568,7 @@ async function startPreviewServer(options) {
10575
10568
  import { platform, arch, release } from "node:os";
10576
10569
 
10577
10570
  // src/lib/remote-auth/token-store.ts
10578
- import { chmod, mkdir as mkdir5, readFile as readFile25, rename as rename2, writeFile as writeFile6 } from "node:fs/promises";
10571
+ import { chmod, mkdir as mkdir5, readFile as readFile24, rename as rename2, writeFile as writeFile6 } from "node:fs/promises";
10579
10572
  import os3 from "node:os";
10580
10573
  import path14 from "node:path";
10581
10574
  function credentialHostKey(hostOrUrl) {
@@ -10619,7 +10612,7 @@ class FileTokenStore {
10619
10612
  async read() {
10620
10613
  let raw;
10621
10614
  try {
10622
- raw = await readFile25(this.filePath, "utf8");
10615
+ raw = await readFile24(this.filePath, "utf8");
10623
10616
  } catch (e) {
10624
10617
  if (e?.code === "ENOENT") {
10625
10618
  return { version: 1, credentials: {} };
@@ -10819,25 +10812,25 @@ async function getSystemDiagnostics() {
10819
10812
  };
10820
10813
  }
10821
10814
  // src/lib/project-scaffold.ts
10822
- import { access, copyFile, cp as cp3, mkdir as mkdir9, readdir as readdir6, readFile as readFile29, stat as stat6, writeFile as writeFile7 } from "node:fs/promises";
10823
- import { constants as FS, existsSync as existsSync14 } from "node:fs";
10815
+ import { access, copyFile, cp as cp3, mkdir as mkdir9, readdir as readdir6, readFile as readFile28, stat as stat6, writeFile as writeFile7 } from "node:fs/promises";
10816
+ import { constants as FS, existsSync as existsSync13 } from "node:fs";
10824
10817
  import path18 from "node:path";
10825
10818
 
10826
10819
  // src/lib/manifest-doc.ts
10827
- import { existsSync as existsSync12 } from "node:fs";
10820
+ import { existsSync as existsSync11 } from "node:fs";
10828
10821
  import { randomUUID as randomUUID2 } from "node:crypto";
10829
- import { mkdir as mkdir6, open, readFile as readFile26, rename as rename3, rm as rm2 } from "node:fs/promises";
10822
+ import { mkdir as mkdir6, open, readFile as readFile25, rename as rename3, rm as rm2 } from "node:fs/promises";
10830
10823
  import path15 from "node:path";
10831
10824
  import { parseDocument, isSeq, YAMLSeq } from "yaml";
10832
10825
  function resolveManifestPath(projectDir) {
10833
- const existing = MANIFEST_FILENAMES.find((name) => existsSync12(path15.join(projectDir, name)));
10826
+ const existing = MANIFEST_FILENAMES.find((name) => existsSync11(path15.join(projectDir, name)));
10834
10827
  return path15.join(projectDir, existing ?? MANIFEST_FILENAMES[0]);
10835
10828
  }
10836
10829
  async function loadManifestDoc(projectDir) {
10837
10830
  const file = resolveManifestPath(projectDir);
10838
10831
  let text = "";
10839
10832
  try {
10840
- text = await readFile26(file, "utf8");
10833
+ text = await readFile25(file, "utf8");
10841
10834
  } catch (error2) {
10842
10835
  if (error2.code !== "ENOENT")
10843
10836
  throw error2;
@@ -10884,8 +10877,8 @@ function scalarString(item) {
10884
10877
  }
10885
10878
 
10886
10879
  // src/lib/extension-manager.ts
10887
- import { cp as cp2, mkdir as mkdir8, readFile as readFile28, rm as rm4, stat as stat5 } from "node:fs/promises";
10888
- import { existsSync as existsSync13 } from "node:fs";
10880
+ import { cp as cp2, mkdir as mkdir8, readFile as readFile27, rm as rm4, stat as stat5 } from "node:fs/promises";
10881
+ import { existsSync as existsSync12 } from "node:fs";
10889
10882
  import path17 from "node:path";
10890
10883
  import { isSeq as isSeq2, isMap, isScalar, YAMLMap, Scalar } from "yaml";
10891
10884
 
@@ -10897,7 +10890,7 @@ import {
10897
10890
  mkdir as mkdir7,
10898
10891
  mkdtemp as mkdtemp2,
10899
10892
  open as open2,
10900
- readFile as readFile27,
10893
+ readFile as readFile26,
10901
10894
  realpath as realpath2,
10902
10895
  rename as rename4,
10903
10896
  rm as rm3
@@ -10942,7 +10935,6 @@ async function withFetchTimeout(options, run2) {
10942
10935
  }
10943
10936
 
10944
10937
  // src/lib/npm-plugin-installer.ts
10945
- var NPM_REGISTRY = "https://registry.npmjs.org";
10946
10938
  var DEFAULT_LIMITS = {
10947
10939
  metadataBytes: 15 * 1024 * 1024,
10948
10940
  packageTarballBytes: 50 * 1024 * 1024,
@@ -10964,11 +10956,11 @@ class PackageUnavailableError extends Error {
10964
10956
  function isOptionalAvailabilityError(error2) {
10965
10957
  return error2 instanceof PackageUnavailableError || error2 instanceof FriendlyHttpError || error2 instanceof FetchUnavailableError;
10966
10958
  }
10967
- function object2(value) {
10959
+ function object3(value) {
10968
10960
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
10969
10961
  }
10970
10962
  function stringMap2(value) {
10971
- const map = object2(value);
10963
+ const map = object3(value);
10972
10964
  if (!map)
10973
10965
  return {};
10974
10966
  const result = {};
@@ -11038,7 +11030,7 @@ async function fetchPackument(ctx, name) {
11038
11030
  timeoutMessage: `Looking up ${name} on npm timed out. Check your connection and try again.`,
11039
11031
  offlineMessage: (cause) => `Looking up ${name} on npm failed (${cause instanceof Error ? cause.message : String(cause)}).`
11040
11032
  }, async (signal) => {
11041
- const response = await ctx.fetch(`${NPM_REGISTRY}/${encodeURIComponent(name)}`, {
11033
+ const response = await ctx.fetch(`${npmRegistryUrl()}/${encodeURIComponent(name)}`, {
11042
11034
  signal,
11043
11035
  redirect: "error",
11044
11036
  headers: { accept: "application/vnd.npm.install-v1+json" }
@@ -11061,7 +11053,7 @@ async function fetchPackument(ctx, name) {
11061
11053
  }
11062
11054
  let metadata = null;
11063
11055
  try {
11064
- metadata = object2(JSON.parse(new TextDecoder().decode(bytes)));
11056
+ metadata = object3(JSON.parse(new TextDecoder().decode(bytes)));
11065
11057
  } catch {}
11066
11058
  if (!metadata || metadata.name !== name) {
11067
11059
  throw new FriendlyHttpError(`npm returned invalid metadata for "${name}".`);
@@ -11117,15 +11109,15 @@ async function selectPackage(ctx, name, selector) {
11117
11109
  throw new PackageUnavailableError(`Dependency selector "${selector}" for ${name} is not a registry semver range or dist-tag.`);
11118
11110
  }
11119
11111
  const metadata = await fetchPackument(ctx, name);
11120
- const versions = object2(metadata.versions);
11121
- const tags = object2(metadata["dist-tags"]);
11112
+ const versions = object3(metadata.versions);
11113
+ const tags = object3(metadata["dist-tags"]);
11122
11114
  if (!versions || !tags)
11123
11115
  throw new PackageUnavailableError(`npm metadata for "${name}" has no versions.`);
11124
11116
  let version;
11125
11117
  if (!selector) {
11126
11118
  if (typeof tags.latest === "string")
11127
11119
  version = tags.latest;
11128
- } else if (object2(versions[selector])) {
11120
+ } else if (object3(versions[selector])) {
11129
11121
  version = selector;
11130
11122
  } else if (typeof tags[selector] === "string") {
11131
11123
  version = tags[selector];
@@ -11135,11 +11127,11 @@ async function selectPackage(ctx, name, selector) {
11135
11127
  if (!version || !isExactNpmVersion(version)) {
11136
11128
  throw new PackageUnavailableError(`No exact npm version of "${name}" satisfies "${selector ?? "latest"}".`);
11137
11129
  }
11138
- const selected = object2(versions[version]);
11130
+ const selected = object3(versions[version]);
11139
11131
  if (!selected || selected.name !== name || selected.version !== version) {
11140
11132
  throw new SecurityInstallError(`npm metadata identity failed for "${name}@${version}".`);
11141
11133
  }
11142
- const dist = object2(selected.dist);
11134
+ const dist = object3(selected.dist);
11143
11135
  if (!dist || typeof dist.tarball !== "string") {
11144
11136
  throw new PackageUnavailableError(`npm metadata for "${name}@${version}" has no tarball.`);
11145
11137
  }
@@ -11149,7 +11141,7 @@ async function selectPackage(ctx, name, selector) {
11149
11141
  } catch {
11150
11142
  throw new SecurityInstallError(`npm returned an invalid tarball URL for "${name}@${version}".`);
11151
11143
  }
11152
- if (tarball.origin !== NPM_REGISTRY || tarball.protocol !== "https:" || tarball.username || tarball.password) {
11144
+ if (tarball.origin !== new URL(npmRegistryUrl()).origin || tarball.username || tarball.password) {
11153
11145
  throw new SecurityInstallError(`npm returned an unexpected tarball host for "${name}@${version}".`);
11154
11146
  }
11155
11147
  return { name, version, tarball: tarball.href, integrity: parseIntegrity(dist) };
@@ -11296,7 +11288,7 @@ async function readInstalledManifest(packageDir, selected) {
11296
11288
  const info2 = await lstat2(packageJsonPath);
11297
11289
  if (!info2.isFile() || info2.isSymbolicLink())
11298
11290
  throw new Error("not a regular file");
11299
- manifest = object2(JSON.parse(await readFile27(packageJsonPath, "utf8")));
11291
+ manifest = object3(JSON.parse(await readFile26(packageJsonPath, "utf8")));
11300
11292
  } catch (error2) {
11301
11293
  throw new SecurityInstallError(`Downloaded ${selected.name}@${selected.version} has no valid package.json (${error2 instanceof Error ? error2.message : String(error2)}).`);
11302
11294
  }
@@ -11408,14 +11400,14 @@ async function installPackage(ctx, name, selector, parentPackagePath, ancestors,
11408
11400
  for (const optionalName of Object.keys(optionalDependencies))
11409
11401
  delete dependencies[optionalName];
11410
11402
  const peerDependencies = stringMap2(manifest.peerDependencies);
11411
- const peerMeta = object2(manifest.peerDependenciesMeta) ?? {};
11403
+ const peerMeta = object3(manifest.peerDependenciesMeta) ?? {};
11412
11404
  for (const existing of [...Object.keys(dependencies), ...Object.keys(optionalDependencies)]) {
11413
11405
  delete peerDependencies[existing];
11414
11406
  }
11415
11407
  const nextAncestors = [...ancestors, record];
11416
11408
  const requiredEntries = [
11417
11409
  ...Object.entries(dependencies).map(([dependency, range]) => ({ dependency, range, kind: "required" })),
11418
- ...Object.entries(peerDependencies).filter(([dependency]) => object2(peerMeta[dependency])?.optional !== true).map(([dependency, range]) => ({ dependency, range, kind: "peer" }))
11410
+ ...Object.entries(peerDependencies).filter(([dependency]) => object3(peerMeta[dependency])?.optional !== true).map(([dependency, range]) => ({ dependency, range, kind: "peer" }))
11419
11411
  ].sort((a, b) => a.dependency.localeCompare(b.dependency));
11420
11412
  for (const { dependency, range } of requiredEntries) {
11421
11413
  const child = await installPackage(ctx, dependency, range, relativePath, nextAncestors, depth + 1, false);
@@ -11433,7 +11425,7 @@ async function installPackage(ctx, name, selector, parentPackagePath, ancestors,
11433
11425
  }
11434
11426
  }
11435
11427
  for (const [dependency, range] of Object.entries(peerDependencies)) {
11436
- if (object2(peerMeta[dependency])?.optional === true) {
11428
+ if (object3(peerMeta[dependency])?.optional === true) {
11437
11429
  ctx.skipped.push({
11438
11430
  from: relativePath,
11439
11431
  name: dependency,
@@ -11714,21 +11706,18 @@ var NO_CARRIES = {
11714
11706
  snippets: false,
11715
11707
  components: false
11716
11708
  };
11717
- function hasMetadataFile(dir) {
11718
- return existsSync13(path17.join(dir, EXTENSION_MANIFEST_FILENAME)) || existsSync13(path17.join(dir, LEGACY_THEME_MANIFEST_FILENAME));
11719
- }
11720
- async function describeFolder(dir, base, fallbackLabel) {
11709
+ async function describeFolder(dir, base, fallbackLabel, labelFrom = prettify) {
11721
11710
  const meta = await readExtensionMeta(dir);
11722
- const styles = extensionStyleListWithDefault(meta, dir);
11711
+ const styles = extensionStyleList(meta);
11723
11712
  return {
11724
11713
  ...base,
11725
- label: meta.name?.trim() || fallbackLabel,
11714
+ label: meta.name?.trim() ? labelFrom(meta.name.trim()) : fallbackLabel,
11726
11715
  ...meta.description?.trim() ? { description: meta.description.trim() } : {},
11727
11716
  ...meta.author?.trim() ? { author: meta.author.trim() } : {},
11728
11717
  ...meta.preview !== undefined ? { preview: meta.preview } : {},
11729
11718
  ...meta.tokensFile?.trim() ? { tokensFile: meta.tokensFile.trim() } : {},
11730
11719
  ...styles.length > 0 ? { styles } : {},
11731
- carries: extensionCarries(meta, dir),
11720
+ carries: extensionCarries(meta),
11732
11721
  dir
11733
11722
  };
11734
11723
  }
@@ -11801,10 +11790,14 @@ async function describeEntry(projectDir, raw) {
11801
11790
  return withWarnings({ ...base, label: parsed.name, carries: { ...NO_CARRIES, markdown: true } });
11802
11791
  }
11803
11792
  const pkgDir = vendoredNpmPluginPackageDir(installRoot, parsed.name);
11804
- if (!hasMetadataFile(pkgDir)) {
11793
+ if (!existsSync12(path17.join(pkgDir, EXTENSION_MANIFEST_FILENAME))) {
11805
11794
  return withWarnings({ ...base, label: parsed.name, carries: { ...NO_CARRIES, markdown: true } });
11806
11795
  }
11807
- return withWarnings(await describeFolder(pkgDir, base, parsed.name));
11796
+ const described = await describeFolder(pkgDir, base, parsed.name, (name) => name);
11797
+ return withWarnings({
11798
+ ...described,
11799
+ carries: { ...described.carries, markdown: true }
11800
+ });
11808
11801
  }
11809
11802
  async function listProjectExtensions(projectDir) {
11810
11803
  const { doc } = await loadManifestDoc(projectDir);
@@ -12005,7 +11998,7 @@ async function listBuiltInStyleSets() {
12005
11998
  const meta = await readExtensionMeta(path17.join(assets, "themes", id));
12006
11999
  out.push({
12007
12000
  id,
12008
- name: meta.name?.trim() || prettify(id),
12001
+ name: prettify(meta.name?.trim() || id),
12009
12002
  description: meta.description?.trim() || ""
12010
12003
  });
12011
12004
  }
@@ -12014,7 +12007,7 @@ async function listBuiltInStyleSets() {
12014
12007
  async function uniqueExtensionId(projectDir, base) {
12015
12008
  const slug = slugify(base) || "extension";
12016
12009
  let candidate = slug;
12017
- for (let n = 2;existsSync13(path17.join(projectDir, EXTENSIONS_DIR, candidate)); n++) {
12010
+ for (let n = 2;existsSync12(path17.join(projectDir, EXTENSIONS_DIR, candidate)); n++) {
12018
12011
  candidate = `${slug}-${n}`;
12019
12012
  }
12020
12013
  return candidate;
@@ -12025,7 +12018,7 @@ async function addBuiltInStyleSet(projectDir, id) {
12025
12018
  }
12026
12019
  return withMutationLock(projectDir, async () => {
12027
12020
  const dest = path17.join(projectDir, EXTENSIONS_DIR, id);
12028
- if (!existsSync13(dest)) {
12021
+ if (!existsSync12(dest)) {
12029
12022
  const src = path17.join(await getAssetsDir(), "themes", id);
12030
12023
  await mkdir8(path17.dirname(dest), { recursive: true });
12031
12024
  await cp2(src, dest, { recursive: true });
@@ -12067,7 +12060,7 @@ async function readExtensionCss(projectDir, use) {
12067
12060
  throw new Error(`Extension "${use}" declares no stylesheets.`);
12068
12061
  const parts = [];
12069
12062
  for (const sheet of sheets)
12070
- parts.push(await readFile28(sheet, "utf8"));
12063
+ parts.push(await readFile27(sheet, "utf8"));
12071
12064
  return parts.join(`
12072
12065
 
12073
12066
  `);
@@ -12204,7 +12197,7 @@ async function scaffoldProject(options) {
12204
12197
  } catch (e) {
12205
12198
  throw new CreateProjectErrorImpl("scaffold-io", `Could not write .gitignore: ${e instanceof Error ? e.message : String(e)}`);
12206
12199
  }
12207
- const manifestPath = MANIFEST_FILENAMES.map((name2) => path18.join(projectDir, name2)).find(existsSync14) ?? path18.join(projectDir, MANIFEST_FILENAMES[0]);
12200
+ const manifestPath = MANIFEST_FILENAMES.map((name2) => path18.join(projectDir, name2)).find(existsSync13) ?? path18.join(projectDir, MANIFEST_FILENAMES[0]);
12208
12201
  const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
12209
12202
  const substitutions = {
12210
12203
  "{{TITLE}}": escapeYamlScalar(name),
@@ -12244,7 +12237,7 @@ async function scaffoldProject(options) {
12244
12237
  let versionHistoryError;
12245
12238
  if (requested === "local-git") {
12246
12239
  try {
12247
- const { providerFor } = await import("./source-provider-kn41jmbs.js");
12240
+ const { providerFor } = await import("./source-provider-jkppq9fc.js");
12248
12241
  const provider = providerFor({ type: "local-folder", path: projectDir });
12249
12242
  await provider.initVersionHistory({
12250
12243
  projectDir,
@@ -12288,7 +12281,7 @@ async function adoptFolder(options) {
12288
12281
  if (!st.isDirectory()) {
12289
12282
  throw new CreateProjectErrorImpl("scaffold-io", `Not a folder: ${dir}`);
12290
12283
  }
12291
- if (MANIFEST_FILENAMES.some((name) => existsSync14(path18.join(dir, name)))) {
12284
+ if (MANIFEST_FILENAMES.some((name) => existsSync13(path18.join(dir, name)))) {
12292
12285
  throw new CreateProjectErrorImpl("target-exists", "This folder is already a Gutterpress project.");
12293
12286
  }
12294
12287
  const template = options.template ?? "book";
@@ -12307,7 +12300,7 @@ async function adoptFolder(options) {
12307
12300
  const themeId = STARTER_THEME_FOR_TEMPLATE[template] ?? "clean-book";
12308
12301
  await mkdir9(path18.join(dir, "styles"), { recursive: true });
12309
12302
  const bookCssPath = path18.join(dir, "styles", "book.css");
12310
- const hadOwnStylesheet = existsSync14(bookCssPath);
12303
+ const hadOwnStylesheet = existsSync13(bookCssPath);
12311
12304
  if (!hadOwnStylesheet) {
12312
12305
  await writeFile7(bookCssPath, starterOverridesCss(themeId), "utf8");
12313
12306
  }
@@ -12341,7 +12334,7 @@ styles:
12341
12334
  let versionHistoryError;
12342
12335
  if (requested === "local-git") {
12343
12336
  try {
12344
- const { providerFor } = await import("./source-provider-kn41jmbs.js");
12337
+ const { providerFor } = await import("./source-provider-jkppq9fc.js");
12345
12338
  const provider = providerFor({ type: "local-folder", path: dir });
12346
12339
  await provider.initVersionHistory({
12347
12340
  projectDir: dir,
@@ -12368,7 +12361,7 @@ async function ensureGitignoreHasDist(projectDir) {
12368
12361
  const gitignorePath = path18.join(projectDir, ".gitignore");
12369
12362
  let existing = null;
12370
12363
  try {
12371
- existing = await readFile29(gitignorePath, "utf8");
12364
+ existing = await readFile28(gitignorePath, "utf8");
12372
12365
  } catch {}
12373
12366
  if (existing === null) {
12374
12367
  await writeFile7(gitignorePath, `dist/
@@ -12388,7 +12381,7 @@ async function ensureGitignoreHasDist(projectDir) {
12388
12381
  async function firstSourceFile(manifestPath) {
12389
12382
  let text;
12390
12383
  try {
12391
- text = await readFile29(manifestPath, "utf8");
12384
+ text = await readFile28(manifestPath, "utf8");
12392
12385
  } catch {
12393
12386
  return;
12394
12387
  }
@@ -12400,21 +12393,20 @@ async function firstSourceFile(manifestPath) {
12400
12393
  return m ? m[1].trim() : undefined;
12401
12394
  }
12402
12395
  async function fillTemplateFile(filePath, substitutions) {
12403
- let text = await readFile29(filePath, "utf8");
12396
+ let text = await readFile28(filePath, "utf8");
12404
12397
  for (const [token, value] of Object.entries(substitutions)) {
12405
12398
  text = text.split(token).join(value);
12406
12399
  }
12407
12400
  await writeFile7(filePath, text, "utf8");
12408
12401
  }
12409
12402
  // src/lib/extension-scaffold.ts
12410
- import { access as access2, mkdir as mkdir10, readFile as readFile30, writeFile as writeFile8 } from "node:fs/promises";
12403
+ import { access as access2, mkdir as mkdir10, readFile as readFile29, writeFile as writeFile8 } from "node:fs/promises";
12411
12404
  import { constants as FS2 } from "node:fs";
12412
12405
  import path19 from "node:path";
12413
12406
  var EXTENSION_KINDS = ["plugin", "theme"];
12414
12407
  var RESERVED_PREFIX = "gp-";
12415
12408
  var TEMPLATE_FILES = {
12416
12409
  plugin: [
12417
- ["gutterpress.json", "gutterpress.json"],
12418
12410
  ["package.json", "package.json"],
12419
12411
  ["README.md", "README.md"],
12420
12412
  ["plugin.js.tpl", "plugin.js"],
@@ -12425,7 +12417,7 @@ var TEMPLATE_FILES = {
12425
12417
  ["test/plugin.test.js.tpl", "test/plugin.test.js"]
12426
12418
  ],
12427
12419
  theme: [
12428
- ["gutterpress.json", "gutterpress.json"],
12420
+ ["package.json", "package.json"],
12429
12421
  ["README.md", "README.md"],
12430
12422
  ["components.yaml", "components.yaml"],
12431
12423
  ["snippets/callout.md", "snippets/callout.md"],
@@ -12525,7 +12517,7 @@ async function scaffoldExtension(options) {
12525
12517
  const src = await getAssetPath(`extension-templates/${kind}/${assetName}`);
12526
12518
  const dest = path19.join(extensionDir, relDest);
12527
12519
  await mkdir10(path19.dirname(dest), { recursive: true });
12528
- const text = await readFile30(src, "utf8");
12520
+ const text = await readFile29(src, "utf8");
12529
12521
  const table = relDest.endsWith(".json") ? jsonSafe : plain;
12530
12522
  await writeFile8(dest, substitute(text, table), "utf8");
12531
12523
  written.push(relDest);
@@ -12535,7 +12527,7 @@ async function scaffoldExtension(options) {
12535
12527
  }
12536
12528
  return {
12537
12529
  extensionDir,
12538
- manifestPath: path19.join(extensionDir, "gutterpress.json"),
12530
+ manifestPath: path19.join(extensionDir, "package.json"),
12539
12531
  kind,
12540
12532
  slug,
12541
12533
  prefix,
@@ -12551,8 +12543,8 @@ function substitute(text, table) {
12551
12543
  return out;
12552
12544
  }
12553
12545
  // src/lib/project-templates.ts
12554
- import { cp as cp4, mkdir as mkdir11, readFile as readFile31, readdir as readdir7, stat as stat7, writeFile as writeFile9 } from "node:fs/promises";
12555
- import { constants as FS3, existsSync as existsSync15 } from "node:fs";
12546
+ import { cp as cp4, mkdir as mkdir11, readFile as readFile30, readdir as readdir7, stat as stat7, writeFile as writeFile9 } from "node:fs/promises";
12547
+ import { constants as FS3, existsSync as existsSync14 } from "node:fs";
12556
12548
  import { access as access3 } from "node:fs/promises";
12557
12549
  import path20 from "node:path";
12558
12550
  import { isMap as isMap2, isScalar as isScalar2, isSeq as isSeq3, parseDocument as parseDocument2 } from "yaml";
@@ -12563,7 +12555,7 @@ var BUILT_IN_TEMPLATE_IDS = [
12563
12555
  ];
12564
12556
  async function readTemplateChoices(manifestPath) {
12565
12557
  try {
12566
- const text = await readFile31(manifestPath, "utf8");
12558
+ const text = await readFile30(manifestPath, "utf8");
12567
12559
  const doc = parseDocument2(text);
12568
12560
  const out = {};
12569
12561
  const preset = doc.get("preset");
@@ -12653,11 +12645,11 @@ function commonAncestorDir(absFiles) {
12653
12645
  }
12654
12646
  function pickVendorDir(templateDir, base) {
12655
12647
  const preferred = path20.basename(base) || VENDOR_DIR;
12656
- if (!existsSync15(path20.join(templateDir, preferred)))
12648
+ if (!existsSync14(path20.join(templateDir, preferred)))
12657
12649
  return preferred;
12658
12650
  for (let n = 1;; n++) {
12659
12651
  const candidate = `${preferred}-${n}`;
12660
- if (!existsSync15(path20.join(templateDir, candidate)))
12652
+ if (!existsSync14(path20.join(templateDir, candidate)))
12661
12653
  return candidate;
12662
12654
  }
12663
12655
  }
@@ -12684,7 +12676,7 @@ async function reconcileSharedRefs(templateDir, sourceProjectDir, mode) {
12684
12676
  const externalFiles = [...new Set([...styleClosure, ...extensionFiles])].filter((f) => escapesProjectRoot(sourceProjectDir, f));
12685
12677
  const base = commonAncestorDir(externalFiles);
12686
12678
  const vendorDir = pickVendorDir(templateDir, base);
12687
- await Promise.all(externalFiles.filter((abs) => existsSync15(abs)).map(async (abs) => {
12679
+ await Promise.all(externalFiles.filter((abs) => existsSync14(abs)).map(async (abs) => {
12688
12680
  const dest = path20.join(templateDir, vendorDir, path20.relative(base, abs));
12689
12681
  await mkdir11(path20.dirname(dest), { recursive: true });
12690
12682
  await cp4(abs, dest, { recursive: true });
@@ -12745,7 +12737,7 @@ async function saveProjectAsTemplate(options) {
12745
12737
  async function retokeniseManifest(manifestPath) {
12746
12738
  let text;
12747
12739
  try {
12748
- text = await readFile31(manifestPath, "utf8");
12740
+ text = await readFile30(manifestPath, "utf8");
12749
12741
  } catch {
12750
12742
  return;
12751
12743
  }
@@ -12766,7 +12758,7 @@ async function listCustomTemplates(templatesRoot) {
12766
12758
  const dir = path20.join(templatesRoot, entry.name);
12767
12759
  let label = prettify(entry.name);
12768
12760
  try {
12769
- const meta = JSON.parse(await readFile31(path20.join(dir, TEMPLATE_META_FILE), "utf8"));
12761
+ const meta = JSON.parse(await readFile30(path20.join(dir, TEMPLATE_META_FILE), "utf8"));
12770
12762
  if (typeof meta.label === "string" && meta.label)
12771
12763
  label = meta.label;
12772
12764
  } catch {}
@@ -12792,7 +12784,7 @@ async function importTemplateFromFolder(options) {
12792
12784
  return saveProjectAsTemplate({ projectDir: sourceDir, name, templatesRoot });
12793
12785
  }
12794
12786
  // src/lib/snippets.ts
12795
- import { readdir as readdir8, readFile as readFile32, writeFile as writeFile10, mkdir as mkdir12, rm as rm5 } from "node:fs/promises";
12787
+ import { readdir as readdir8, readFile as readFile31, writeFile as writeFile10, mkdir as mkdir12, rm as rm5 } from "node:fs/promises";
12796
12788
  import path21 from "node:path";
12797
12789
  var SNIPPETS_DIR = "snippets";
12798
12790
  var PLACEHOLDER_RE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
@@ -12834,7 +12826,7 @@ async function scanSnippetFiles(dir) {
12834
12826
  continue;
12835
12827
  let body = "";
12836
12828
  try {
12837
- body = await readFile32(path21.join(dir, fileName), "utf8");
12829
+ body = await readFile31(path21.join(dir, fileName), "utf8");
12838
12830
  } catch {
12839
12831
  continue;
12840
12832
  }
@@ -12852,7 +12844,7 @@ async function listSnippets(projectDir) {
12852
12844
  return files.map((file) => ({ ...file, source: { kind: "project" } }));
12853
12845
  }
12854
12846
  async function readSnippet(projectDir, fileName) {
12855
- return readFile32(resolveSnippetPath(projectDir, fileName), "utf8");
12847
+ return readFile31(resolveSnippetPath(projectDir, fileName), "utf8");
12856
12848
  }
12857
12849
  async function saveSnippet(projectDir, name, body) {
12858
12850
  const stem = slugify(name);
@@ -12911,11 +12903,11 @@ async function readExtensionSnippet(projectDir, source, fileName) {
12911
12903
  if (!dir) {
12912
12904
  throw new Error(`Extension "${source.ref}" does not declare a usable snippets folder.`);
12913
12905
  }
12914
- return readFile32(resolveSafeChildFile(dir, fileName), "utf8");
12906
+ return readFile31(resolveSafeChildFile(dir, fileName), "utf8");
12915
12907
  }
12916
12908
  // src/lib/extension-import.ts
12917
- import { existsSync as existsSync16 } from "node:fs";
12918
- import { cp as cp5, mkdir as mkdir13, mkdtemp as mkdtemp3, readFile as readFile33, rm as rm6, writeFile as writeFile11 } from "node:fs/promises";
12909
+ import { existsSync as existsSync15 } from "node:fs";
12910
+ import { cp as cp5, mkdir as mkdir13, mkdtemp as mkdtemp3, readFile as readFile32, rm as rm6, writeFile as writeFile11 } from "node:fs/promises";
12919
12911
  import { tmpdir as tmpdir3 } from "node:os";
12920
12912
  import path22 from "node:path";
12921
12913
  import { unzipSync } from "fflate";
@@ -12950,7 +12942,7 @@ function classifyThemeCssFindings(findings) {
12950
12942
  const warnings = reject ? [] : findings.filter((f) => f.rule !== ruleSyntax);
12951
12943
  return { reject, warnings };
12952
12944
  }
12953
- var KNOWN_THEME_FILES = new Set(["theme.css", "theme.json", EXTENSION_MANIFEST_FILENAME]);
12945
+ var KNOWN_THEME_FILES = new Set(["theme.css", EXTENSION_MANIFEST_FILENAME]);
12954
12946
  var ALLOWED_ASSET_EXTS = new Set([
12955
12947
  ".woff",
12956
12948
  ".woff2",
@@ -12985,10 +12977,10 @@ function mb2(n) {
12985
12977
  }
12986
12978
  async function finalizeExtensionImport(projectDir, sourceDir, relPaths, fallbackName) {
12987
12979
  const cssPath = path22.join(sourceDir, "theme.css");
12988
- if (!existsSync16(cssPath)) {
12980
+ if (!existsSync15(cssPath)) {
12989
12981
  throw new Error("A theme must contain a theme.css file.");
12990
12982
  }
12991
- const css = await readFile33(cssPath, "utf8");
12983
+ const css = await readFile32(cssPath, "utf8");
12992
12984
  const { reject, warnings: printFindings } = classifyThemeCssFindings(checkCss(css, "theme.css"));
12993
12985
  if (reject) {
12994
12986
  throw new Error(`theme.css could not be parsed — ${reject.message}`);
@@ -12997,29 +12989,26 @@ async function finalizeExtensionImport(projectDir, sourceDir, relPaths, fallback
12997
12989
  for (const w of printFindings) {
12998
12990
  warnings.push({ code: "print-safety", message: w.message });
12999
12991
  }
13000
- const metaExists = existsSync16(path22.join(sourceDir, EXTENSION_MANIFEST_FILENAME)) || existsSync16(path22.join(sourceDir, "theme.json"));
12992
+ const metaExists = existsSync15(path22.join(sourceDir, EXTENSION_MANIFEST_FILENAME));
13001
12993
  const meta = metaExists ? await readExtensionMeta(sourceDir) : {};
13002
12994
  if (!metaExists) {
13003
12995
  warnings.push({
13004
12996
  code: "no-theme-json",
13005
- message: "No gutterpress.json or theme.json found — the theme's name was taken from the file/folder name."
12997
+ message: "No package.json found — one was written for you, and the theme's name was taken from the file/folder name."
13006
12998
  });
13007
12999
  } else if (!meta.name || !meta.name.trim()) {
13008
13000
  warnings.push({
13009
13001
  code: "unnamed-theme",
13010
- message: `The metadata file has no "name" — the theme's name was taken from the file/folder name.`
13002
+ message: `The package.json has no "name" — the theme's name was taken from the file/folder name.`
13011
13003
  });
13012
13004
  }
13013
13005
  assertExtensionContained(meta);
13014
- const metaFilename = existsSync16(path22.join(sourceDir, EXTENSION_MANIFEST_FILENAME)) ? EXTENSION_MANIFEST_FILENAME : "theme.json";
13015
- const extraSheets = [
13016
- ...new Set(extensionStyleListWithDefault(meta, sourceDir))
13017
- ].filter((rel) => rel !== "theme.css");
13018
- const extraSheetPaths = resolveDeclaredStyles(extraSheets, sourceDir, metaFilename) ?? [];
13006
+ const extraSheets = [...new Set(extensionStyleList(meta))].filter((rel) => rel !== "theme.css");
13007
+ const extraSheetPaths = resolveDeclaredStyles(extraSheets, sourceDir, EXTENSION_MANIFEST_FILENAME) ?? [];
13019
13008
  for (let i = 0;i < extraSheets.length; i++) {
13020
13009
  const rel = extraSheets[i];
13021
13010
  const sheetPath = extraSheetPaths[i];
13022
- const sheetCss = await readFile33(sheetPath, "utf8");
13011
+ const sheetCss = await readFile32(sheetPath, "utf8");
13023
13012
  const { reject: sheetReject, warnings: sheetFindings } = classifyThemeCssFindings(checkCss(sheetCss, rel));
13024
13013
  if (sheetReject) {
13025
13014
  throw new Error(`${rel} could not be parsed — ${sheetReject.message}`);
@@ -13041,9 +13030,30 @@ async function finalizeExtensionImport(projectDir, sourceDir, relPaths, fallback
13041
13030
  const dest = path22.join(projectDir, EXTENSIONS_DIR, id);
13042
13031
  await mkdir13(path22.dirname(dest), { recursive: true });
13043
13032
  await cp5(sourceDir, dest, { recursive: true });
13033
+ await completeLandedManifest(dest, id, meta, metaExists);
13044
13034
  const entry = await addExtension(projectDir, `./${EXTENSIONS_DIR}/${id}`);
13045
13035
  return { entry, warnings };
13046
13036
  }
13037
+ async function completeLandedManifest(dest, id, meta, metaExists) {
13038
+ if (metaExists && extensionStyleList(meta).length > 0)
13039
+ return;
13040
+ const file = path22.join(dest, EXTENSION_MANIFEST_FILENAME);
13041
+ let pkg = {};
13042
+ if (metaExists) {
13043
+ try {
13044
+ const parsed = JSON.parse(await readFile32(file, "utf8"));
13045
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
13046
+ pkg = parsed;
13047
+ }
13048
+ } catch {}
13049
+ }
13050
+ const gutterpress = pkg.gutterpress && typeof pkg.gutterpress === "object" && !Array.isArray(pkg.gutterpress) ? pkg.gutterpress : {};
13051
+ await writeFile11(file, JSON.stringify({
13052
+ ...pkg,
13053
+ name: typeof pkg.name === "string" && pkg.name.trim() ? pkg.name : id,
13054
+ gutterpress: { ...gutterpress, styles: ["theme.css"] }
13055
+ }, null, 2), "utf8");
13056
+ }
13047
13057
  async function importExtensionFromZip(projectDir, archive) {
13048
13058
  if (archive.length > MAX_THEME_ARCHIVE_BYTES) {
13049
13059
  throw new Error(`Extension package is too large (max ${mb2(MAX_THEME_ARCHIVE_BYTES)}).`);
@@ -13119,8 +13129,8 @@ async function importExtensionFromCssText(projectDir, css, name) {
13119
13129
  const tmp = await mkdtemp3(path22.join(tmpdir3(), "gutterpress-theme-"));
13120
13130
  try {
13121
13131
  await writeFile11(path22.join(tmp, "theme.css"), css, "utf8");
13122
- await writeFile11(path22.join(tmp, "theme.json"), JSON.stringify({ name: displayName }, null, 2), "utf8");
13123
- const result = await finalizeExtensionImport(projectDir, tmp, ["theme.css", "theme.json"], displayName);
13132
+ await writeFile11(path22.join(tmp, EXTENSION_MANIFEST_FILENAME), JSON.stringify({ name: slugify(displayName, "look"), gutterpress: { styles: ["theme.css"] } }, null, 2), "utf8");
13133
+ const result = await finalizeExtensionImport(projectDir, tmp, ["theme.css", EXTENSION_MANIFEST_FILENAME], displayName);
13124
13134
  result.warnings.unshift({
13125
13135
  code: "no-theme-json",
13126
13136
  message: "A single CSS file was imported — its name came from the file name."
@@ -13133,11 +13143,11 @@ async function importExtensionFromCssText(projectDir, css, name) {
13133
13143
  async function importExtensionFromFile(projectDir, filePath) {
13134
13144
  const ext = path22.extname(filePath).toLowerCase();
13135
13145
  if (ext === ".zip") {
13136
- const buf = await readFile33(filePath);
13146
+ const buf = await readFile32(filePath);
13137
13147
  return importExtensionFromZip(projectDir, new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
13138
13148
  }
13139
13149
  if (ext === ".css") {
13140
- const css = await readFile33(filePath, "utf8");
13150
+ const css = await readFile32(filePath, "utf8");
13141
13151
  return importExtensionFromCssText(projectDir, css, prettify(path22.basename(filePath, ".css")));
13142
13152
  }
13143
13153
  throw new Error("Choose a .zip extension package or a .css stylesheet.");
@@ -13191,7 +13201,7 @@ async function importExtensionFromUrl(projectDir, url) {
13191
13201
  const base = trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
13192
13202
  css = await fetchText(`${base}theme.css`);
13193
13203
  try {
13194
- const parsed = JSON.parse(await fetchText(`${base}theme.json`));
13204
+ const parsed = JSON.parse(await fetchText(`${base}${EXTENSION_MANIFEST_FILENAME}`));
13195
13205
  if (parsed && typeof parsed === "object")
13196
13206
  meta = parsed;
13197
13207
  } catch {}
@@ -13203,18 +13213,137 @@ async function importExtensionFromUrl(projectDir, url) {
13203
13213
  const tmp = await mkdtemp3(path22.join(tmpdir3(), "gutterpress-ext-"));
13204
13214
  try {
13205
13215
  await writeFile11(path22.join(tmp, "theme.css"), css, "utf8");
13216
+ const gp = meta.gutterpress && typeof meta.gutterpress === "object" && !Array.isArray(meta.gutterpress) ? meta.gutterpress : {};
13206
13217
  const finalMeta = {
13207
- name: typeof meta.name === "string" && meta.name || prettify(baseName),
13218
+ name: typeof meta.name === "string" && meta.name || slugify(baseName, "look"),
13208
13219
  ...typeof meta.author === "string" ? { author: meta.author } : {},
13209
13220
  ...typeof meta.description === "string" ? { description: meta.description } : {},
13210
- preview: typeof meta.preview === "string" ? meta.preview : null
13221
+ gutterpress: {
13222
+ ...gp,
13223
+ styles: ["theme.css"],
13224
+ preview: typeof gp.preview === "string" ? gp.preview : null
13225
+ }
13211
13226
  };
13212
- await writeFile11(path22.join(tmp, "theme.json"), JSON.stringify(finalMeta, null, 2), "utf8");
13213
- return await finalizeExtensionImport(projectDir, tmp, ["theme.css", "theme.json"], baseName);
13227
+ await writeFile11(path22.join(tmp, EXTENSION_MANIFEST_FILENAME), JSON.stringify(finalMeta, null, 2), "utf8");
13228
+ return await finalizeExtensionImport(projectDir, tmp, ["theme.css", EXTENSION_MANIFEST_FILENAME], baseName);
13214
13229
  } finally {
13215
13230
  await rm6(tmp, { recursive: true, force: true });
13216
13231
  }
13217
13232
  }
13233
+ // src/lib/extension-search.ts
13234
+ var FETCH_TIMEOUT_MS2 = 15000;
13235
+ var MAX_SEARCH_BYTES = 2 * 1024 * 1024;
13236
+ var SEARCH_KEYWORDS = ["gutterpress", "markdown-it-plugin"];
13237
+ var KIND_FOR_KEYWORD = {
13238
+ gutterpress: "gutterpress",
13239
+ "markdown-it-plugin": "markdown-it"
13240
+ };
13241
+ function invalid(reason) {
13242
+ throw new Error(`Malformed npm search response: ${reason}.`);
13243
+ }
13244
+ function object4(value) {
13245
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
13246
+ }
13247
+ function str2(value) {
13248
+ return typeof value === "string" && value.trim() ? value : undefined;
13249
+ }
13250
+ function parseSearchPage(text, keyword) {
13251
+ let parsed;
13252
+ try {
13253
+ parsed = JSON.parse(text);
13254
+ } catch {
13255
+ invalid("not valid JSON");
13256
+ }
13257
+ const root = object4(parsed);
13258
+ if (!root)
13259
+ invalid("root is not an object");
13260
+ if (!Array.isArray(root.objects))
13261
+ invalid('"objects" must be an array');
13262
+ if (typeof root.total !== "number")
13263
+ invalid('"total" must be a number');
13264
+ const packages = [];
13265
+ const scores = [];
13266
+ root.objects.forEach((raw, i) => {
13267
+ const entry = object4(raw);
13268
+ if (!entry)
13269
+ invalid(`objects[${i}] is not an object`);
13270
+ const pkg = object4(entry.package);
13271
+ if (!pkg)
13272
+ invalid(`objects[${i}].package is not an object`);
13273
+ const name = str2(pkg.name);
13274
+ const version = str2(pkg.version);
13275
+ if (!name)
13276
+ invalid(`objects[${i}].package.name must be a non-empty string`);
13277
+ if (!version)
13278
+ invalid(`objects[${i}].package.version must be a non-empty string`);
13279
+ const keywords = Array.isArray(pkg.keywords) ? pkg.keywords.filter((k) => typeof k === "string") : [];
13280
+ if (!keywords.includes(keyword))
13281
+ return;
13282
+ if (name === "markdown-it" || name.startsWith("@types/"))
13283
+ return;
13284
+ const links = object4(pkg.links) ?? {};
13285
+ packages.push({
13286
+ name,
13287
+ version,
13288
+ ...str2(pkg.description) ? { description: str2(pkg.description) } : {},
13289
+ kind: KIND_FOR_KEYWORD[keyword],
13290
+ keywords,
13291
+ ...str2(links.homepage) ? { homepage: str2(links.homepage) } : {},
13292
+ ...str2(links.npm) ? { npmUrl: str2(links.npm) } : {},
13293
+ ...str2(pkg.date) ? { date: str2(pkg.date) } : {}
13294
+ });
13295
+ scores.push(typeof entry.searchScore === "number" ? entry.searchScore : 0);
13296
+ });
13297
+ return { packages, scores, total: root.total };
13298
+ }
13299
+ async function searchNpmExtensions(query, options = {}) {
13300
+ const limit = options.limit ?? 20;
13301
+ const fetchImpl = options.fetch ?? globalThis.fetch;
13302
+ const registry = npmRegistryUrl();
13303
+ const trimmed = query.trim();
13304
+ const bodies = await withFetchTimeout({
13305
+ timeoutMs: FETCH_TIMEOUT_MS2,
13306
+ signal: options.signal,
13307
+ timeoutMessage: "Searching npm timed out. Check your connection and try again.",
13308
+ offlineMessage: "Couldn't reach the npm registry. Check your connection and try again."
13309
+ }, async (signal) => {
13310
+ const out = [];
13311
+ for (const keyword of SEARCH_KEYWORDS) {
13312
+ const text = [trimmed, `keywords:${keyword}`].filter(Boolean).join(" ");
13313
+ const url = `${registry}/-/v1/search?text=${encodeURIComponent(text)}&size=${limit}`;
13314
+ const res = await fetchImpl(url, { signal });
13315
+ if (!res.ok) {
13316
+ throw new FriendlyHttpError(`Failed to search npm (HTTP ${res.status}).`);
13317
+ }
13318
+ const body = await res.text();
13319
+ if (Buffer.byteLength(body, "utf8") > MAX_SEARCH_BYTES) {
13320
+ throw new FriendlyHttpError(`The npm search response is too large (limit ${MAX_SEARCH_BYTES} bytes).`);
13321
+ }
13322
+ out.push(body);
13323
+ }
13324
+ return out;
13325
+ });
13326
+ const pages = bodies.map((body, i) => parseSearchPage(body, SEARCH_KEYWORDS[i]));
13327
+ const byName = new Map;
13328
+ for (const page of pages) {
13329
+ page.packages.forEach((match, i) => {
13330
+ const existing = byName.get(match.name);
13331
+ if (!existing) {
13332
+ byName.set(match.name, { match, score: page.scores[i] ?? 0 });
13333
+ return;
13334
+ }
13335
+ if (match.kind === "gutterpress")
13336
+ existing.match = match;
13337
+ existing.score = Math.max(existing.score, page.scores[i] ?? 0);
13338
+ });
13339
+ }
13340
+ const matches = [...byName.values()].sort((a, b) => {
13341
+ if (a.match.kind !== b.match.kind)
13342
+ return a.match.kind === "gutterpress" ? -1 : 1;
13343
+ return b.score - a.score;
13344
+ }).slice(0, limit).map((entry) => entry.match);
13345
+ return { matches, total: pages.reduce((sum, page) => sum + page.total, 0) };
13346
+ }
13218
13347
  // src/lib/manifest-config.ts
13219
13348
  import { isSeq as isSeq4, isMap as isMap3, YAMLSeq as YAMLSeq3, Scalar as Scalar2 } from "yaml";
13220
13349
  function hasKey(obj, key) {
@@ -13306,8 +13435,6 @@ async function setActiveStyles(projectDir, paths) {
13306
13435
  return paths;
13307
13436
  }
13308
13437
  // src/lib/host-policy.ts
13309
- var AUTO_SNAPSHOT_MIN_MINUTES = 5;
13310
- var AUTO_SNAPSHOT_MAX_MINUTES = 24 * 60;
13311
13438
  var AUTO_SNAPSHOT_DEFAULT_MINUTES = 10;
13312
13439
  var AUTO_SYNC_MIN_MINUTES = 1;
13313
13440
  var AUTO_SYNC_MAX_MINUTES = 24 * 60;
@@ -13321,12 +13448,6 @@ function clampedDelayMs(enabled, minutes, bounds) {
13321
13448
  const clamped = Math.min(bounds.max, Math.max(bounds.min, resolved));
13322
13449
  return clamped * 60000;
13323
13450
  }
13324
- var AUTO_SNAPSHOT_BOUNDS = {
13325
- min: AUTO_SNAPSHOT_MIN_MINUTES,
13326
- max: AUTO_SNAPSHOT_MAX_MINUTES,
13327
- default: AUTO_SNAPSHOT_DEFAULT_MINUTES,
13328
- enabledDefault: true
13329
- };
13330
13451
  var AUTO_SYNC_BOUNDS = {
13331
13452
  min: AUTO_SYNC_MIN_MINUTES,
13332
13453
  max: AUTO_SYNC_MAX_MINUTES,
@@ -13334,7 +13455,8 @@ var AUTO_SYNC_BOUNDS = {
13334
13455
  enabledDefault: true
13335
13456
  };
13336
13457
  function autoSnapshotDelayMs(policy) {
13337
- return clampedDelayMs(policy?.autoSnapshot, policy?.autoSnapshotMinutes, AUTO_SNAPSHOT_BOUNDS);
13458
+ const isEnabled = policy?.autoSnapshot ?? true;
13459
+ return isEnabled ? AUTO_SNAPSHOT_DEFAULT_MINUTES * 60000 : null;
13338
13460
  }
13339
13461
  function autoSyncDelayMs(policy) {
13340
13462
  return clampedDelayMs(policy?.autoSync, policy?.autoSyncMinutes, AUTO_SYNC_BOUNDS);
@@ -13355,7 +13477,7 @@ var API_BASE = "https://api.github.com";
13355
13477
  var REQUEST_TIMEOUT_MS = 15000;
13356
13478
  var OFFLINE_MESSAGE = "Couldn't reach GitHub. Check your connection and try again.";
13357
13479
  function defaultSleep(ms) {
13358
- return new Promise((resolve10) => setTimeout(resolve10, ms));
13480
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
13359
13481
  }
13360
13482
  async function safeFetch(fetchImpl, url, init) {
13361
13483
  return withFetchTimeout({
@@ -13582,7 +13704,7 @@ function friendlyTokenError(status, body) {
13582
13704
  var SUCCESS_HTML = `<html><body style="font:16px system-ui;padding:3rem"><h2>You're connected</h2><p>Return to Gutterpress — you can close this tab.</p></body></html>`;
13583
13705
  var FAILURE_HTML = `<html><body style="font:16px system-ui;padding:3rem"><h2>Sign-in didn't complete</h2><p>Return to Gutterpress and try again.</p></body></html>`;
13584
13706
  function waitForCallback(server, expectedState, redirectUri, timeoutMs, signal) {
13585
- return new Promise((resolve10, reject) => {
13707
+ return new Promise((resolve9, reject) => {
13586
13708
  let settled = false;
13587
13709
  const cleanup = () => {
13588
13710
  clearTimeout(timer);
@@ -13596,7 +13718,7 @@ function waitForCallback(server, expectedState, redirectUri, timeoutMs, signal)
13596
13718
  return;
13597
13719
  settled = true;
13598
13720
  cleanup();
13599
- resolve10(value);
13721
+ resolve9(value);
13600
13722
  };
13601
13723
  const finishReject = (err) => {
13602
13724
  if (settled)
@@ -13668,14 +13790,14 @@ class GoogleAuthProvider {
13668
13790
  const state = b64url(crypto.randomBytes(16));
13669
13791
  const challenge = pkceChallengeFromVerifier(verifier);
13670
13792
  const server = http2.createServer();
13671
- await new Promise((resolve10, reject) => {
13793
+ await new Promise((resolve9, reject) => {
13672
13794
  const onListenError = (err) => {
13673
13795
  reject(new Error(`Couldn't start the local sign-in listener: ${err.message}`));
13674
13796
  };
13675
13797
  server.once("error", onListenError);
13676
13798
  server.listen(this.bindPort, "127.0.0.1", () => {
13677
13799
  server.removeListener("error", onListenError);
13678
- resolve10();
13800
+ resolve9();
13679
13801
  });
13680
13802
  });
13681
13803
  const address = server.address();
@@ -14357,8 +14479,8 @@ async function testRemoteAccess(options) {
14357
14479
  const credential = options.credential ?? urlCredential;
14358
14480
  const timeoutMs = options.timeoutMs ?? 15000;
14359
14481
  let timer;
14360
- const timeout = new Promise((resolve10) => {
14361
- timer = setTimeout(() => resolve10(failure("unreachable")), timeoutMs);
14482
+ const timeout = new Promise((resolve9) => {
14483
+ timer = setTimeout(() => resolve9(failure("unreachable")), timeoutMs);
14362
14484
  });
14363
14485
  const probe = (async () => {
14364
14486
  try {
@@ -14793,7 +14915,7 @@ var DEFAULT_SYNC_RETRY = {
14793
14915
  backoffMs: 150
14794
14916
  };
14795
14917
  function defaultSleep2(ms) {
14796
- return new Promise((resolve10) => setTimeout(resolve10, ms));
14918
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
14797
14919
  }
14798
14920
  async function syncProject(options) {
14799
14921
  const http3 = options.httpClient ?? defaultGitHttp;
@@ -15176,7 +15298,7 @@ var drivethrurpgProvider = {
15176
15298
  };
15177
15299
 
15178
15300
  // src/lib/publish/providers/gdrive.ts
15179
- import { mkdtemp as mkdtemp4, readFile as readFile34, readdir as readdir10, rm as rm8, stat as stat9, writeFile as writeFile14 } from "node:fs/promises";
15301
+ import { mkdtemp as mkdtemp4, readFile as readFile33, readdir as readdir10, rm as rm8, stat as stat9, writeFile as writeFile14 } from "node:fs/promises";
15180
15302
  import { tmpdir as tmpdir4 } from "node:os";
15181
15303
  import path26 from "node:path";
15182
15304
  import { Zip, ZipDeflate } from "fflate";
@@ -15325,7 +15447,7 @@ var RESUMABLE_CHUNK_SIZE = 8 * 1024 * 1024;
15325
15447
  var MAX_CHUNK_RETRIES = 3;
15326
15448
  var RETRY_BASE_DELAY_MS = 500;
15327
15449
  function defaultSleep3(ms) {
15328
- return new Promise((resolve10) => setTimeout(resolve10, ms));
15450
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
15329
15451
  }
15330
15452
  async function startResumableSession(fetchImpl, accessToken, opts) {
15331
15453
  const mimeType = opts.mimeType ?? "application/pdf";
@@ -15507,10 +15629,10 @@ function toDestination(folder) {
15507
15629
  }
15508
15630
  var ZIP_CHUNK_BYTES = 64 * 1024;
15509
15631
  function yieldToEventLoop() {
15510
- return new Promise((resolve10) => setImmediate(resolve10));
15632
+ return new Promise((resolve9) => setImmediate(resolve9));
15511
15633
  }
15512
15634
  function zipEntriesNonBlocking(entries, opts) {
15513
- return new Promise((resolve10, reject) => {
15635
+ return new Promise((resolve9, reject) => {
15514
15636
  const chunks = [];
15515
15637
  let total = 0;
15516
15638
  let settled = false;
@@ -15534,7 +15656,7 @@ function zipEntriesNonBlocking(entries, opts) {
15534
15656
  out.set(c, offset);
15535
15657
  offset += c.length;
15536
15658
  }
15537
- resolve10(out);
15659
+ resolve9(out);
15538
15660
  }
15539
15661
  });
15540
15662
  (async () => {
@@ -15568,12 +15690,12 @@ async function collectZipEntries(root, dir, out, onWarn) {
15568
15690
  await collectZipEntries(root, full, out, onWarn);
15569
15691
  } else if (entry.isFile()) {
15570
15692
  const rel = path26.relative(root, full).split(path26.sep).join("/");
15571
- out[rel] = await readFile34(full);
15693
+ out[rel] = await readFile33(full);
15572
15694
  } else if (entry.isSymbolicLink()) {
15573
15695
  const resolved = await stat9(full).catch(() => null);
15574
15696
  if (resolved?.isFile()) {
15575
15697
  const rel = path26.relative(root, full).split(path26.sep).join("/");
15576
- out[rel] = await readFile34(full);
15698
+ out[rel] = await readFile33(full);
15577
15699
  } else if (resolved?.isDirectory()) {
15578
15700
  await collectZipEntries(root, full, out, onWarn);
15579
15701
  } else {
@@ -16390,7 +16512,7 @@ async function disconnectPublishCredential(key, deps, options = {}) {
16390
16512
  }
16391
16513
  }
16392
16514
  // src/lib/publish/selections.ts
16393
- import { chmod as chmod3, mkdir as mkdir18, readFile as readFile35, writeFile as writeFile16 } from "node:fs/promises";
16515
+ import { chmod as chmod3, mkdir as mkdir18, readFile as readFile34, writeFile as writeFile16 } from "node:fs/promises";
16394
16516
  import path29 from "node:path";
16395
16517
  class PublishSelectionsStore {
16396
16518
  filePath;
@@ -16400,7 +16522,7 @@ class PublishSelectionsStore {
16400
16522
  }
16401
16523
  async read() {
16402
16524
  try {
16403
- const raw = await readFile35(this.filePath, "utf8");
16525
+ const raw = await readFile34(this.filePath, "utf8");
16404
16526
  const parsed = JSON.parse(raw);
16405
16527
  if (parsed && typeof parsed === "object") {
16406
16528
  return {
@@ -16516,4 +16638,4 @@ async function setPublishProviderConfig(projectDir, providerId, values) {
16516
16638
  await writeManifestDoc(file, doc);
16517
16639
  return settingsFromDoc(doc);
16518
16640
  }
16519
- export { PLUGINS_DIR, BUNDLED_EXTENSIONS, isBundledExtension, isPathSpecifier, parseExtensionSpecifier, PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, hasProjectManifest, loadManifest, loadManifestWithPath, resolveConfig, SYNC_SNAPSHOT_MESSAGE, log, resolveActiveStyles, listProjectStyles, EXTENSION_MANIFEST_FILENAME, LEGACY_THEME_MANIFEST_FILENAME, readExtensionMeta, extensionStyleList, extensionStyleListWithDefault, extensionCarries, pathEscapesFolder, assertExtensionContained, resolveExtension, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, checkCss, runLint, formatReport, getChecks, getCheckById, resolveCheckSelectors, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, defaultConfigDir, FileTokenStore, extractUrlCredential, getSystemDiagnostics, EXTENSIONS_DIR, RECOMMENDED_EXTENSIONS, BUILT_IN_STYLE_SET_IDS, sameExtension, listProjectExtensions, describeExtension, setExtensionEnabled, reorderExtensions, removeExtension, addExtension, listBuiltInStyleSets, addBuiltInStyleSet, validateProjectExtensions, readExtensionCss, slugifyProjectName, escapeYamlScalar, scaffoldProject, adoptFolder, EXTENSION_KINDS, RESERVED_PREFIX, resolveExtensionPrefix, scaffoldExtension, BUILT_IN_TEMPLATE_IDS, listBuiltInTemplates, saveProjectAsTemplate, listCustomTemplates, importTemplateFromFolder, SNIPPETS_DIR, extractVariables, substituteVariables, listSnippets, readSnippet, saveSnippet, deleteSnippet, listMergedSnippets, readExtensionSnippet, importExtensionFromFile, importExtensionFromUrl, setManifestFields, readManifestFields, setActiveStyles, AUTO_SNAPSHOT_MIN_MINUTES, AUTO_SNAPSHOT_MAX_MINUTES, AUTO_SNAPSHOT_DEFAULT_MINUTES, AUTO_SYNC_MIN_MINUTES, AUTO_SYNC_MAX_MINUTES, AUTO_SYNC_DEFAULT_MINUTES, AUTO_SYNC_PUSH_INTERVAL_MINUTES, autoSnapshotDelayMs, autoSyncDelayMs, isGitInternalPath, resolveGitHubClientId, GITHUB_HOST, GitHubAuthProvider, GDRIVE_HOST, resolveGoogleClientId, resolveGoogleClientSecret, GOOGLE_NOT_CONFIGURED_MESSAGE, requireGoogleClientCredentials, pkceChallengeFromVerifier, GoogleAuthProvider, revokeGoogleCredential, publishCredentialKey, publishConnectionStatus, listPublishAccounts, connectGoogleDrive, listGitHubRepositories, listRepoBooks, listGitHubBranches, sanitizeCloneFolderName, cloneRepository, isSshRemoteUrl, testRemoteAccess, knownForgeTokenUrl, connectGenericHost, parseRemoteOrigin, forgeKindForHost, diagnoseProjectRemote, syncProject, listPublishProviders, publishProviderFor, resolvePublishFormat, resolvePublishRequest, runPublish, connectPublishProvider, disconnectPublishCredential, PublishSelectionsStore, readPublishSettings, setPublishProviderConfig };
16641
+ export { DEFAULT_NPM_REGISTRY, npmRegistryUrl, PLUGINS_DIR, BUNDLED_EXTENSIONS, isBundledExtension, isPathSpecifier, parseExtensionSpecifier, PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, hasProjectManifest, loadManifest, loadManifestWithPath, resolveConfig, SYNC_SNAPSHOT_MESSAGE, log, resolveActiveStyles, listProjectStyles, EXTENSION_MANIFEST_FILENAME, readExtensionMeta, extensionStyleList, extensionEntry, extensionCarries, pathEscapesFolder, assertExtensionContained, resolveExtension, loadPluginsWithCss, formatReport, getChecks, getCheckById, resolveCheckSelectors, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, rulePageContainment, checkCss, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, defaultConfigDir, FileTokenStore, extractUrlCredential, getSystemDiagnostics, EXTENSIONS_DIR, RECOMMENDED_EXTENSIONS, BUILT_IN_STYLE_SET_IDS, sameExtension, listProjectExtensions, describeExtension, setExtensionEnabled, reorderExtensions, removeExtension, addExtension, listBuiltInStyleSets, addBuiltInStyleSet, validateProjectExtensions, readExtensionCss, slugifyProjectName, escapeYamlScalar, scaffoldProject, adoptFolder, EXTENSION_KINDS, RESERVED_PREFIX, resolveExtensionPrefix, scaffoldExtension, BUILT_IN_TEMPLATE_IDS, listBuiltInTemplates, saveProjectAsTemplate, listCustomTemplates, importTemplateFromFolder, SNIPPETS_DIR, extractVariables, substituteVariables, listSnippets, readSnippet, saveSnippet, deleteSnippet, listMergedSnippets, readExtensionSnippet, importExtensionFromFile, importExtensionFromUrl, MAX_SEARCH_BYTES, SEARCH_KEYWORDS, searchNpmExtensions, setManifestFields, readManifestFields, setActiveStyles, AUTO_SNAPSHOT_DEFAULT_MINUTES, AUTO_SYNC_MIN_MINUTES, AUTO_SYNC_MAX_MINUTES, AUTO_SYNC_DEFAULT_MINUTES, AUTO_SYNC_PUSH_INTERVAL_MINUTES, autoSnapshotDelayMs, autoSyncDelayMs, isGitInternalPath, resolveGitHubClientId, GITHUB_HOST, GitHubAuthProvider, GDRIVE_HOST, resolveGoogleClientId, resolveGoogleClientSecret, GOOGLE_NOT_CONFIGURED_MESSAGE, requireGoogleClientCredentials, pkceChallengeFromVerifier, GoogleAuthProvider, revokeGoogleCredential, publishCredentialKey, publishConnectionStatus, listPublishAccounts, connectGoogleDrive, listGitHubRepositories, listRepoBooks, listGitHubBranches, sanitizeCloneFolderName, cloneRepository, isSshRemoteUrl, testRemoteAccess, knownForgeTokenUrl, connectGenericHost, parseRemoteOrigin, forgeKindForHost, diagnoseProjectRemote, syncProject, listPublishProviders, publishProviderFor, resolvePublishFormat, resolvePublishRequest, runPublish, connectPublishProvider, disconnectPublishCredential, PublishSelectionsStore, readPublishSettings, setPublishProviderConfig };