gutterpress 0.11.15-alpha.2 → 0.11.16-alpha.1

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 (32) hide show
  1. package/README.md +6 -0
  2. package/dist/{README-8b33qbjw.md → README-vsnsc1b3.md} +14 -0
  3. package/dist/api/index.d.ts +2 -2
  4. package/dist/api/index.js +4 -6
  5. package/dist/{build-4522yqmm.js → build-46bb205t.js} +4 -4
  6. package/dist/{cli-b496rxy0.js → cli-6tfc4ed6.js} +1 -1
  7. package/dist/{cli-mc3waxaw.js → cli-d55z7rtk.js} +3 -3
  8. package/dist/{cli-vdpnctkq.js → cli-p8xwsknv.js} +1 -1
  9. package/dist/{cli-7pw53gnm.js → cli-sq0awkbf.js} +161 -7
  10. package/dist/cli.js +12 -12
  11. package/dist/{doctor-fgvwe9j2.js → doctor-tjatz81j.js} +2 -2
  12. package/dist/{engine-h8zdtxhp.js → engine-nv2xddnk.js} +1 -1
  13. package/dist/{engine-s18wy689.js → engine-szx34415.js} +2 -2
  14. package/dist/{ext-v44phkxz.js → ext-ph0770mx.js} +4 -4
  15. package/dist/{index-ypxhxvwv.js → index-9ydc9q9q.js} +1 -1
  16. package/dist/{index-j77039xr.js → index-azdfgy3t.js} +245 -53
  17. package/dist/{index-w6tqhafk.js → index-eawjxgkn.js} +3 -3
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.js +5 -7
  20. package/dist/lib/markdown/assemble.d.ts +7 -0
  21. package/dist/lib/markdown/plugins.d.ts +1 -1
  22. package/dist/lib/markdown/renderer.d.ts +91 -0
  23. package/dist/lib/snippets.d.ts +34 -92
  24. package/dist/{new-7npm260n.js → new-daeh9p9p.js} +4 -4
  25. package/dist/{plugin.js-aszz0zgh.tpl → plugin.js-z492w01r.tpl} +44 -0
  26. package/dist/{plugin.test.js-nw7c84s6.tpl → plugin.test.js-6n8ct5e3.tpl} +35 -1
  27. package/dist/{preflight-y0jhsz8p.js → preflight-bfvw2qe5.js} +4 -4
  28. package/dist/{preview-xkp190pd.js → preview-00298rvf.js} +4 -4
  29. package/dist/{publish-czx47htf.js → publish-jn9t18mr.js} +4 -4
  30. package/dist/render.js +156 -2
  31. package/dist/{validate-x89z3zrq.js → validate-cm1m5bbf.js} +4 -4
  32. package/package.json +1 -1
@@ -4,7 +4,7 @@ import {
4
4
  MARGIN_BOX_IGNORED_PROPERTIES,
5
5
  getAssetPath,
6
6
  getAssetsDir
7
- } from "./index-w6tqhafk.js";
7
+ } from "./index-eawjxgkn.js";
8
8
  import {
9
9
  INSTALL_HINTS,
10
10
  execCapture,
@@ -855,6 +855,133 @@ function warn(env, line, type, message, marker) {
855
855
  env.layoutWarnings = [];
856
856
  env.layoutWarnings.push({ line, type, message, marker });
857
857
  }
858
+ var COMPONENT_SEVERITIES = ["error", "warning", "info"];
859
+ function blockEnd(tokens, i) {
860
+ if (tokens[i].nesting !== 1)
861
+ return i;
862
+ let depth = 0;
863
+ for (let j = i;j < tokens.length; j++) {
864
+ depth += tokens[j].nesting;
865
+ if (depth === 0)
866
+ return j;
867
+ }
868
+ return tokens.length - 1;
869
+ }
870
+ function inlineText(tokens, from, to) {
871
+ const parts = [];
872
+ for (let k = from;k <= to; k++)
873
+ if (tokens[k].type === "inline")
874
+ parts.push(tokens[k].content);
875
+ return parts.join(`
876
+ `);
877
+ }
878
+ function soleImage(inline) {
879
+ const parts = (inline && inline.children || []).filter((c) => c.type !== "softbreak" && !(c.type === "text" && !c.content.trim()));
880
+ return parts.length === 1 && parts[0].type === "image" ? parts[0] : null;
881
+ }
882
+ function toComponentBlocks(tokens, fallbackLine) {
883
+ const blocks = [];
884
+ for (let i = 0;i < tokens.length; i++) {
885
+ const tok = tokens[i];
886
+ const end = blockEnd(tokens, i);
887
+ const line = tok.map ? tok.map[0] + 1 : tok.meta && tok.meta.line || fallbackLine;
888
+ switch (tok.type) {
889
+ case "heading_open":
890
+ blocks.push({ type: "heading", level: Number(tok.tag.slice(1)), text: inlineText(tokens, i, end), line });
891
+ break;
892
+ case "paragraph_open": {
893
+ const image = soleImage(tokens[i + 1]);
894
+ blocks.push(image ? { type: "image", alt: image.content, src: image.attrGet("src") || "", line } : { type: "paragraph", text: inlineText(tokens, i, end), line });
895
+ break;
896
+ }
897
+ case "bullet_list_open":
898
+ case "ordered_list_open": {
899
+ const items = [];
900
+ for (let k = i + 1;k < end; k++) {
901
+ if (tokens[k].type !== "list_item_open" || tokens[k].level !== tok.level + 1)
902
+ continue;
903
+ const itemEnd = blockEnd(tokens, k);
904
+ const first = tokens.slice(k + 1, itemEnd).find((t) => t.type === "inline");
905
+ items.push(first ? first.content : "");
906
+ k = itemEnd;
907
+ }
908
+ blocks.push({ type: "list", ordered: tok.type === "ordered_list_open", items, line });
909
+ break;
910
+ }
911
+ case "blockquote_open":
912
+ blocks.push({ type: "quote", text: inlineText(tokens, i, end), line });
913
+ break;
914
+ case "fence":
915
+ case "code_block":
916
+ blocks.push({ type: "code", lang: (tok.info || "").trim().split(/\s+/)[0] || "", text: tok.content, line });
917
+ break;
918
+ case "table_open":
919
+ blocks.push({ type: "table", line });
920
+ break;
921
+ case "hr":
922
+ blocks.push({ type: "rule", line });
923
+ break;
924
+ case "html_block":
925
+ blocks.push({ type: "html", text: tok.content, line });
926
+ break;
927
+ case "layout_component_open":
928
+ blocks.push({ type: "component", name: tok.meta.component, line });
929
+ break;
930
+ default:
931
+ blocks.push({ type: tok.type.replace(/_open$/, ""), line });
932
+ }
933
+ i = end;
934
+ }
935
+ return blocks;
936
+ }
937
+ function runComponentValidate(env, decl, meta, tokens, lines) {
938
+ const name = meta.component;
939
+ const line = meta.line || 0;
940
+ const report = (at, type, message, severity) => {
941
+ if (!env.layoutWarnings)
942
+ env.layoutWarnings = [];
943
+ env.layoutWarnings.push(severity ? { line: at, type, message, severity } : { line: at, type, message });
944
+ };
945
+ const failed = (why) => report(line, "component_validate_failed", `@${name}: plugin "${decl.validateOwner}"'s validate() ${why}`);
946
+ let lastLine = line;
947
+ for (const t of tokens)
948
+ if (t.map && t.map[1] > lastLine)
949
+ lastLine = t.map[1];
950
+ const component = {
951
+ name,
952
+ variant: meta.variant || null,
953
+ attrs: { ...meta.attrs },
954
+ line,
955
+ text: lines.slice(line, lastLine).join(`
956
+ `),
957
+ blocks: toComponentBlocks(tokens, line)
958
+ };
959
+ let result;
960
+ try {
961
+ result = decl.validate(component);
962
+ } catch (error) {
963
+ failed(`threw: ${error instanceof Error ? error.message : String(error)}`);
964
+ return;
965
+ }
966
+ if (result === undefined || result === null)
967
+ return;
968
+ if (typeof result === "string")
969
+ result = [result];
970
+ if (!Array.isArray(result)) {
971
+ failed("must return a message, an array of problems, or nothing (and must not be async).");
972
+ return;
973
+ }
974
+ for (const entry of result) {
975
+ const problem = typeof entry === "string" ? { message: entry } : entry;
976
+ if (!problem || typeof problem.message !== "string" || !problem.message.trim()) {
977
+ failed("returned a problem without a message.");
978
+ continue;
979
+ }
980
+ const at = Number.isInteger(problem.line) && problem.line > 0 ? problem.line : line;
981
+ const severity = COMPONENT_SEVERITIES.includes(problem.severity) ? problem.severity : undefined;
982
+ report(at, "component_invalid", `@${name}: ${problem.message}`, severity);
983
+ }
984
+ }
858
985
  function proseEscapeHint(kind) {
859
986
  return ` If this line is prose that happens to begin with "@${kind}" — a wrapped sentence, for` + ` instance — escape it as \\@${kind} or write it as \`@${kind}\` in backticks, and it stays text.`;
860
987
  }
@@ -957,13 +1084,20 @@ function resolveContainerShape(name, pluginName, decl) {
957
1084
  }
958
1085
  autoCloseAtEof = decl.autoCloseAt.includes("eof");
959
1086
  }
960
- return { tag, classBase, variants, label, autoCloseAtEof };
1087
+ if (decl.validate !== undefined && typeof decl.validate !== "function") {
1088
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`validate\` that is not a function.`);
1089
+ }
1090
+ const check = decl.validate ? { validate: decl.validate, validateOwner: pluginName } : {};
1091
+ return { tag, classBase, variants, label, autoCloseAtEof, ...check };
961
1092
  }
962
1093
  function resolveMarkerDeclaration(name, rawDecl, rawRegistry, originOf) {
963
1094
  const pluginName = originOf.get(name);
964
1095
  if (typeof rawDecl !== "object" || rawDecl === null || Array.isArray(rawDecl)) {
965
1096
  throw new Error(`Plugin "${pluginName}"'s marker "@${name}" is not a plain object.`);
966
1097
  }
1098
+ if (rawDecl.snippet !== undefined && (typeof rawDecl.snippet !== "string" || !rawDecl.snippet.trim())) {
1099
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`snippet\` that is not a non-empty path string.`);
1100
+ }
967
1101
  if (rawDecl.deprecated !== undefined) {
968
1102
  if (typeof rawDecl.deprecated !== "string" || !rawDecl.deprecated) {
969
1103
  throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`deprecated\` value that is not a non-empty string.`);
@@ -1178,8 +1312,8 @@ function plugin(md, pluginOptions = {}) {
1178
1312
  }
1179
1313
  function openDeclaredMarker(meta, decl) {
1180
1314
  const t = new state.Token("layout_component_open", decl.tag, 1);
1181
- t.meta = { line: meta.__line };
1182
1315
  const variant = meta.name || decl.presetVariant || null;
1316
+ t.meta = { line: meta.__line, component: meta.kind, variant, attrs: meta.attrs || {}, labelled: false };
1183
1317
  const variantClass = variant && decl.variants && decl.variants[variant] || "";
1184
1318
  const baseClass = [decl.classBase, variantClass].filter(Boolean).join(" ");
1185
1319
  addClasses(t, baseClass, meta.attrs && meta.attrs.class ? meta.attrs.class : "");
@@ -1193,6 +1327,7 @@ function plugin(md, pluginOptions = {}) {
1193
1327
  labelToken.content = `<${decl.label.tag} class="${escapeAttr(decl.label.class)}">${escapeHtml(value)}</${decl.label.tag}>
1194
1328
  `;
1195
1329
  out.push(labelToken);
1330
+ t.meta.labelled = true;
1196
1331
  }
1197
1332
  }
1198
1333
  }
@@ -1454,6 +1589,25 @@ function plugin(md, pluginOptions = {}) {
1454
1589
  stack.closeAll();
1455
1590
  state.tokens = out;
1456
1591
  });
1592
+ if (declaredMarkers && [...declaredMarkers.values()].some((d) => d.validate)) {
1593
+ md.core.ruler.push("gp_component_validate", function(state) {
1594
+ let lines = null;
1595
+ const tokens = state.tokens;
1596
+ for (let i = 0;i < tokens.length; i++) {
1597
+ const tok = tokens[i];
1598
+ if (tok.type !== "layout_component_open")
1599
+ continue;
1600
+ const decl = declaredMarkers.get(tok.meta.component);
1601
+ if (!decl || !decl.validate)
1602
+ continue;
1603
+ if (!lines)
1604
+ lines = state.src.split(`
1605
+ `);
1606
+ const from = i + (tok.meta.labelled ? 2 : 1);
1607
+ runComponentValidate(state.env, decl, tok.meta, tokens.slice(from, blockEnd(tokens, i)), lines);
1608
+ }
1609
+ });
1610
+ }
1457
1611
  md.renderer.rules.layout_page_break = (tokens, idx) => {
1458
1612
  const cls = tokens[idx].attrGet("class") || "gp-page-break";
1459
1613
  const rangeAttr = tokens[idx].attrGet("data-source-range");
@@ -3860,7 +4014,7 @@ import git from "isomorphic-git";
3860
4014
  // package.json
3861
4015
  var package_default = {
3862
4016
  name: "gutterpress",
3863
- version: "0.11.15-alpha.2",
4017
+ version: "0.11.16-alpha.1",
3864
4018
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
3865
4019
  author: "itlackey",
3866
4020
  license: "MPL-2.0",
@@ -6890,7 +7044,7 @@ var check24 = {
6890
7044
  md.render(content, env);
6891
7045
  for (const w of env.layoutWarnings ?? []) {
6892
7046
  results.push(finding(check24.id, {
6893
- severity: "warning",
7047
+ severity: w.severity ?? "warning",
6894
7048
  message: w.message,
6895
7049
  file,
6896
7050
  ...w.line > 0 ? { line: w.line } : {},
@@ -8475,7 +8629,7 @@ class PdfOutput {
8475
8629
  const rawPdf = pdfxMode ? path8.join(stage, "raw.pdf") : path8.resolve(pdfFile);
8476
8630
  await fsp2.mkdir(path8.dirname(path8.resolve(pdfFile)), { recursive: true });
8477
8631
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
8478
- const { buildNativePdf } = await import("./engine-s18wy689.js");
8632
+ const { buildNativePdf } = await import("./engine-szx34415.js");
8479
8633
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
8480
8634
  title: config.title,
8481
8635
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
@@ -11882,6 +12036,7 @@ async function readExtensionCss(projectDir, use) {
11882
12036
 
11883
12037
  // src/lib/snippets.ts
11884
12038
  var SNIPPETS_DIR = "snippets";
12039
+ var LEVEL_RANK = { project: 0, extension: 1, core: 2 };
11885
12040
  var PLACEHOLDER_RE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
11886
12041
  function extractVariables(template) {
11887
12042
  const seen = new Set;
@@ -11898,16 +12053,14 @@ function extractVariables(template) {
11898
12053
  function substituteVariables(template, values) {
11899
12054
  return template.replace(PLACEHOLDER_RE, (_full, name) => Object.prototype.hasOwnProperty.call(values, name) ? values[name] : "");
11900
12055
  }
11901
- function resolveSafeChildFile(dir, fileName) {
12056
+ function resolveSnippetPath(projectDir, fileName) {
12057
+ const dir = path20.resolve(projectDir, SNIPPETS_DIR);
11902
12058
  const full = path20.resolve(dir, fileName);
11903
12059
  if (full !== path20.join(dir, path20.basename(fileName)) || path20.dirname(full) !== dir) {
11904
12060
  throw new Error(`Unsafe snippet filename: ${fileName}`);
11905
12061
  }
11906
12062
  return full;
11907
12063
  }
11908
- function resolveSnippetPath(projectDir, fileName) {
11909
- return resolveSafeChildFile(path20.resolve(projectDir, SNIPPETS_DIR), fileName);
11910
- }
11911
12064
  async function scanSnippetFiles(dir) {
11912
12065
  let names;
11913
12066
  try {
@@ -11928,7 +12081,8 @@ async function scanSnippetFiles(dir) {
11928
12081
  entries.push({
11929
12082
  name: prettify(fileName.replace(/\.md$/i, "")),
11930
12083
  fileName,
11931
- variables: extractVariables(body)
12084
+ variables: extractVariables(body),
12085
+ body
11932
12086
  });
11933
12087
  }
11934
12088
  entries.sort((a, b) => a.name.localeCompare(b.name));
@@ -11938,9 +12092,6 @@ async function listSnippets(projectDir) {
11938
12092
  const files = await scanSnippetFiles(path20.join(projectDir, SNIPPETS_DIR));
11939
12093
  return files.map((file) => ({ ...file, source: { kind: "project" } }));
11940
12094
  }
11941
- async function readSnippet(projectDir, fileName) {
11942
- return readFile30(resolveSnippetPath(projectDir, fileName), "utf8");
11943
- }
11944
12095
  async function saveSnippet(projectDir, name, body) {
11945
12096
  const stem = slugify(name);
11946
12097
  if (!stem)
@@ -11949,56 +12100,97 @@ async function saveSnippet(projectDir, name, body) {
11949
12100
  const dir = path20.join(projectDir, SNIPPETS_DIR);
11950
12101
  await mkdir11(dir, { recursive: true });
11951
12102
  await writeFile10(resolveSnippetPath(projectDir, fileName), body, "utf8");
11952
- return { name, fileName, variables: extractVariables(body), source: { kind: "project" } };
12103
+ return { name, fileName, variables: extractVariables(body), body, source: { kind: "project" } };
11953
12104
  }
11954
12105
  async function deleteSnippet(projectDir, fileName) {
11955
12106
  await rm4(resolveSnippetPath(projectDir, fileName), { force: true });
11956
12107
  }
11957
- async function listInstalledExtensions(projectDir) {
11958
- const out = [];
11959
- for (const entry of await listProjectExtensions(projectDir)) {
11960
- if (!entry.enabled || !entry.dir || !entry.carries.snippets)
11961
- continue;
11962
- const meta = await readExtensionMeta(entry.dir);
11963
- const snippetsRel = meta.snippets?.trim();
11964
- if (!snippetsRel)
11965
- continue;
11966
- out.push({ ref: entry.use, dir: entry.dir, name: entry.label, snippetsRel });
11967
- }
11968
- return out;
11969
- }
11970
- function extensionSnippetsDir(ext) {
11971
- if (pathEscapesFolder(ext.snippetsRel))
12108
+ async function declaredSnippetsFolder(entry) {
12109
+ if (!entry.dir || !entry.carries.snippets)
11972
12110
  return null;
11973
- return path20.join(ext.dir, ext.snippetsRel);
12111
+ const rel = (await readExtensionMeta(entry.dir)).snippets?.trim();
12112
+ return rel && !pathEscapesFolder(rel) ? rel : null;
11974
12113
  }
11975
- async function listMergedSnippets(projectDir) {
11976
- const project = await listSnippets(projectDir);
11977
- const projectFileNames = new Set(project.map((entry) => entry.fileName.toLowerCase()));
11978
- const merged = [...project];
11979
- const extensions = (await listInstalledExtensions(projectDir)).sort((a, b) => a.name.localeCompare(b.name));
11980
- for (const ext of extensions) {
11981
- const dir = extensionSnippetsDir(ext);
11982
- if (!dir)
12114
+ async function declaredMarkers(entry, projectDir) {
12115
+ if (!entry.carries.markdown)
12116
+ return;
12117
+ try {
12118
+ return (await loadPlugin(extensionConfigFor(entry), projectDir)).markers;
12119
+ } catch {
12120
+ return;
12121
+ }
12122
+ }
12123
+ async function collectLibrary(projectDir) {
12124
+ const groups = [];
12125
+ const components = [];
12126
+ for (const entry of await listProjectExtensions(projectDir)) {
12127
+ if (!entry.enabled)
11983
12128
  continue;
11984
- for (const file of await scanSnippetFiles(dir)) {
11985
- if (projectFileNames.has(file.fileName.toLowerCase()))
12129
+ const source = { kind: "extension", ref: entry.use, name: entry.label };
12130
+ const folder = await declaredSnippetsFolder(entry);
12131
+ const snippets2 = folder ? (await scanSnippetFiles(path20.join(entry.dir, folder))).map((file) => ({ ...file, source })) : [];
12132
+ for (const [name, decl] of Object.entries(await declaredMarkers(entry, projectDir) ?? {})) {
12133
+ if (decl.deprecated !== undefined)
12134
+ continue;
12135
+ components.push({ name, source });
12136
+ if (!entry.dir)
12137
+ continue;
12138
+ const own = typeof decl.snippet === "string" && decl.snippet.trim() ? decl.snippet.trim() : null;
12139
+ const rel = own ?? path20.join(folder ?? SNIPPETS_DIR, `${name}.md`);
12140
+ if (pathEscapesFolder(rel))
12141
+ continue;
12142
+ const listed = folder && path20.dirname(path20.join(entry.dir, rel)) === path20.join(entry.dir, folder) ? snippets2.find((s) => s.fileName === path20.basename(rel)) : undefined;
12143
+ if (listed) {
12144
+ listed.component = name;
12145
+ continue;
12146
+ }
12147
+ const body = await readFile30(path20.join(entry.dir, rel), "utf8").catch(() => null);
12148
+ if (body === null)
11986
12149
  continue;
11987
- merged.push({ ...file, source: { kind: "extension", ref: ext.ref, name: ext.name } });
12150
+ snippets2.push({
12151
+ name: prettify(path20.basename(rel).replace(/\.md$/i, "")),
12152
+ fileName: rel.split(path20.sep).join("/"),
12153
+ variables: extractVariables(body),
12154
+ body,
12155
+ source,
12156
+ component: name
12157
+ });
12158
+ }
12159
+ if (snippets2.length) {
12160
+ snippets2.sort((a, b) => a.name.localeCompare(b.name));
12161
+ groups.push({ label: entry.label, snippets: snippets2 });
11988
12162
  }
11989
12163
  }
11990
- return merged;
11991
- }
11992
- async function readExtensionSnippet(projectDir, source, fileName) {
11993
- const ext = (await listInstalledExtensions(projectDir)).find((candidate) => candidate.ref === source.ref);
11994
- if (!ext) {
11995
- throw new Error(`Extension "${source.ref}" is not installed or is no longer active.`);
12164
+ const book = await listSnippets(projectDir);
12165
+ const names = new Set(components.map((c) => c.name));
12166
+ for (const s of book) {
12167
+ const stem = s.fileName.replace(/\.md$/i, "").toLowerCase();
12168
+ if (names.has(stem))
12169
+ s.component = stem;
11996
12170
  }
11997
- const dir = extensionSnippetsDir(ext);
11998
- if (!dir) {
11999
- throw new Error(`Extension "${source.ref}" does not declare a usable snippets folder.`);
12171
+ groups.sort((a, b) => a.label.localeCompare(b.label));
12172
+ const snippets = [...book, ...groups.flatMap((g) => g.snippets)];
12173
+ for (const component of components) {
12174
+ let best;
12175
+ for (const s of snippets) {
12176
+ if (s.component !== component.name)
12177
+ continue;
12178
+ if (s.source.kind === "extension" && s.source.ref !== component.source.ref)
12179
+ continue;
12180
+ if (!best || LEVEL_RANK[s.source.kind] < LEVEL_RANK[best.source.kind])
12181
+ best = s;
12182
+ }
12183
+ if (best)
12184
+ component.snippet = best.body;
12000
12185
  }
12001
- return readFile30(resolveSafeChildFile(dir, fileName), "utf8");
12186
+ components.sort((a, b) => a.name.localeCompare(b.name));
12187
+ return { snippets, components };
12188
+ }
12189
+ async function listMergedSnippets(projectDir) {
12190
+ return (await collectLibrary(projectDir)).snippets;
12191
+ }
12192
+ async function listMarkerComponents(projectDir) {
12193
+ return (await collectLibrary(projectDir)).components;
12002
12194
  }
12003
12195
  // src/lib/extension-import.ts
12004
12196
  import { existsSync as existsSync15 } from "node:fs";
@@ -16036,4 +16228,4 @@ async function setPublishProviderConfig(projectDir, providerId, values) {
16036
16228
  await writeManifestDoc(file, doc);
16037
16229
  return settingsFromDoc(doc);
16038
16230
  }
16039
- export { 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, getChecks, getCheckById, resolveCheckSelectors, formatReport, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, checkCss, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, defaultConfigDir, FileTokenStore, extractUrlCredential, getSystemDiagnostics, slugifyProjectName, escapeYamlScalar, scaffoldProject, adoptFolder, ensureGitignoreHasDist, EXTENSION_KINDS, RESERVED_PREFIX, resolveExtensionPrefix, scaffoldExtension, BUILT_IN_TEMPLATE_IDS, listBuiltInTemplates, saveProjectAsTemplate, listCustomTemplates, importTemplateFromFolder, DEFAULT_NPM_REGISTRY, npmRegistryUrl, EXTENSIONS_DIR, RECOMMENDED_EXTENSIONS, BUILT_IN_STYLE_SET_IDS, sameExtension, listProjectExtensions, describeExtension, setExtensionEnabled, reorderExtensions, removeExtension, addExtension, checkExtensionUpdates, updateExtensions, listBuiltInStyleSets, addBuiltInStyleSet, validateProjectExtensions, readExtensionCss, 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, refreshRemoteCopies, REPAIR_SNAPSHOT_MESSAGE, repairOnlineBackup, scorchedEarth, listPublishProviders, publishProviderFor, resolvePublishFormat, resolvePublishRequest, runPublish, connectPublishProvider, disconnectPublishCredential, PublishSelectionsStore, readPublishSettings, setPublishProviderConfig };
16231
+ export { 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, getChecks, getCheckById, resolveCheckSelectors, formatReport, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, checkCss, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, defaultConfigDir, FileTokenStore, extractUrlCredential, getSystemDiagnostics, slugifyProjectName, escapeYamlScalar, scaffoldProject, adoptFolder, ensureGitignoreHasDist, EXTENSION_KINDS, RESERVED_PREFIX, resolveExtensionPrefix, scaffoldExtension, BUILT_IN_TEMPLATE_IDS, listBuiltInTemplates, saveProjectAsTemplate, listCustomTemplates, importTemplateFromFolder, DEFAULT_NPM_REGISTRY, npmRegistryUrl, EXTENSIONS_DIR, RECOMMENDED_EXTENSIONS, BUILT_IN_STYLE_SET_IDS, sameExtension, listProjectExtensions, describeExtension, setExtensionEnabled, reorderExtensions, removeExtension, addExtension, checkExtensionUpdates, updateExtensions, listBuiltInStyleSets, addBuiltInStyleSet, validateProjectExtensions, readExtensionCss, SNIPPETS_DIR, extractVariables, substituteVariables, listSnippets, saveSnippet, deleteSnippet, listMergedSnippets, listMarkerComponents, 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, refreshRemoteCopies, REPAIR_SNAPSHOT_MESSAGE, repairOnlineBackup, scorchedEarth, listPublishProviders, publishProviderFor, resolvePublishFormat, resolvePublishRequest, runPublish, connectPublishProvider, disconnectPublishCredential, PublishSelectionsStore, readPublishSettings, setPublishProviderConfig };
@@ -68,10 +68,10 @@ var chapter_01_default3 = "./chapter-01-6fn9pvd0.md";
68
68
  var package_default = "./package-9rv69pxm.json";
69
69
 
70
70
  // src/assets/extension-templates/plugin/README.md
71
- var README_default = "./README-8b33qbjw.md";
71
+ var README_default = "./README-vsnsc1b3.md";
72
72
 
73
73
  // src/assets/extension-templates/plugin/plugin.js.tpl
74
- var plugin_js_default = "./plugin.js-aszz0zgh.tpl";
74
+ var plugin_js_default = "./plugin.js-z492w01r.tpl";
75
75
 
76
76
  // src/assets/extension-templates/plugin/styles/plugin.css
77
77
  var plugin_default = "./plugin-spbcem4j.css";
@@ -86,7 +86,7 @@ var fixture_default = "./fixture-2qnyqgb5.md";
86
86
  var expected_default = "./expected-ycmacem8.html";
87
87
 
88
88
  // src/assets/extension-templates/plugin/test/plugin.test.js.tpl
89
- var plugin_test_js_default = "./plugin.test.js-nw7c84s6.tpl";
89
+ var plugin_test_js_default = "./plugin.test.js-6n8ct5e3.tpl";
90
90
 
91
91
  // src/assets/extension-templates/theme/package.json
92
92
  var package_default2 = "./package-aenwjew3.json";
package/dist/index.d.ts CHANGED
@@ -30,4 +30,4 @@ export { BUILD_DIAGNOSTIC_CODES } from "./engine/compiler/build.ts";
30
30
  export type { BuildDiagnostic, BuildDiagnosticCode } from "./engine/compiler/build.ts";
31
31
  export type { PrintSafeWarning } from "./lib/printsafe.ts";
32
32
  export { ruleRemoteUrls, ruleRiskyProps, ruleSyntax, } from "./lib/printsafe.ts";
33
- export type { GutterpressPlugin, GutterpressPluginExport, GutterpressPluginMetadata, } from "./lib/markdown/plugins.ts";
33
+ export type { GutterpressComponent, GutterpressComponentBlock, GutterpressComponentProblem, GutterpressPlugin, GutterpressPluginExport, GutterpressPluginMetadata, } from "./lib/markdown/plugins.ts";
package/dist/index.js CHANGED
@@ -77,6 +77,7 @@ import {
77
77
  listCustomTemplates,
78
78
  listGitHubBranches,
79
79
  listGitHubRepositories,
80
+ listMarkerComponents,
80
81
  listMergedSnippets,
81
82
  listProjectExtensions,
82
83
  listProjectStyles,
@@ -99,10 +100,8 @@ import {
99
100
  publishTargetFor,
100
101
  readExtensionCss,
101
102
  readExtensionMeta,
102
- readExtensionSnippet,
103
103
  readManifestFields,
104
104
  readPublishSettings,
105
- readSnippet,
106
105
  refreshRemoteCopies,
107
106
  removeExtension,
108
107
  reorderExtensions,
@@ -146,13 +145,13 @@ import {
146
145
  testRemoteAccess,
147
146
  updateExtensions,
148
147
  validateProjectExtensions
149
- } from "./index-j77039xr.js";
148
+ } from "./index-azdfgy3t.js";
150
149
  import {
151
150
  BUILD_DIAGNOSTIC_CODES
152
- } from "./index-ypxhxvwv.js";
151
+ } from "./index-9ydc9q9q.js";
153
152
  import {
154
153
  BuildError
155
- } from "./index-w6tqhafk.js";
154
+ } from "./index-eawjxgkn.js";
156
155
  import {
157
156
  DEFAULT_PRINT_OPTS,
158
157
  assertMilestone,
@@ -232,10 +231,8 @@ export {
232
231
  removeExtension,
233
232
  refreshRemoteCopies,
234
233
  readyProbeExpr,
235
- readSnippet,
236
234
  readPublishSettings,
237
235
  readManifestFields,
238
- readExtensionSnippet,
239
236
  readExtensionMeta,
240
237
  readExtensionCss,
241
238
  publishTargetFor,
@@ -259,6 +256,7 @@ export {
259
256
  listProjectStyles,
260
257
  listProjectExtensions,
261
258
  listMergedSnippets,
259
+ listMarkerComponents,
262
260
  listLocalBranches,
263
261
  listGitHubRepositories,
264
262
  listGitHubBranches,
@@ -24,6 +24,11 @@ export type ReadText = (relPath: string) => Promise<string>;
24
24
  * same type, via a second, unconditional check modeled on `unknown_gp_class`
25
25
  * rather than the core-only `scanForMistypedMarkers`.
26
26
  *
27
+ * A declared marker's opt-in `validate` adds two more: `component_invalid`
28
+ * (one problem the validator reported — the only warning that may carry a
29
+ * `severity`) and `component_validate_failed` (the validator threw or
30
+ * returned something unusable).
31
+ *
27
32
  * There is deliberately no `section_without_page`: a @section with no open
28
33
  * @page is valid authoring (17/17 false positives across two real books).
29
34
  */
@@ -32,6 +37,8 @@ export interface LayoutWarning {
32
37
  type: string;
33
38
  message: string;
34
39
  marker?: unknown;
40
+ /** Set only by a component's `validate`; absent means `"warning"`. */
41
+ severity?: "error" | "warning" | "info";
35
42
  }
36
43
  /**
37
44
  * The Chromium milestone that paints a `@page { background: url() }` image
@@ -1,6 +1,6 @@
1
1
  import type { ResolvedExtensionConfig } from "../../schema/manifest.types";
2
2
  import type { LoadedPlugin } from "./renderer";
3
- export type { GutterpressMarkerDeclaration, GutterpressMarkerLabel, GutterpressMarkerTable, GutterpressPlugin, GutterpressPluginMetadata, GutterpressPluginExport, LoadedPlugin, } from "./renderer";
3
+ export type { GutterpressComponent, GutterpressComponentBlock, GutterpressComponentProblem, GutterpressMarkerDeclaration, GutterpressMarkerLabel, GutterpressMarkerTable, GutterpressPlugin, GutterpressPluginMetadata, GutterpressPluginExport, LoadedPlugin, } from "./renderer";
4
4
  export { applyPlugins, collectPluginCss, collectPluginStylePaths, collectPluginStyleGroups } from "./renderer";
5
5
  export type { PluginStyleGroup } from "./renderer";
6
6
  import { type PluginStyleGroup } from "./renderer";
@@ -106,7 +106,98 @@ export interface GutterpressMarkerDeclaration {
106
106
  };
107
107
  /** This marker is retired: using it (or its `@end-` form) warns with this message and is otherwise a no-op. */
108
108
  deprecated?: string;
109
+ /**
110
+ * The example content the desktop editor inserts for this component, as a
111
+ * path relative to the extension's own folder (where its package.json
112
+ * lives). Defaults to `<snippets folder>/<marker name>.md`. Read by the
113
+ * host's component listing only — it never affects rendering.
114
+ */
115
+ snippet?: string;
116
+ /**
117
+ * Opt-in structure check, run every time the component is rendered
118
+ * (validate, Problems panel, build, preview). Receives a plain description
119
+ * of the component's content — see {@link GutterpressComponent} — and
120
+ * returns its problems. Must be synchronous; ignored on an alias (which
121
+ * uses its target's) and on a deprecated entry.
122
+ */
123
+ validate?: (component: GutterpressComponent) => GutterpressComponentProblems;
124
+ }
125
+ /** One top-level block inside a component, as its `validate` sees it. `line` is 1-based in the file. */
126
+ export type GutterpressComponentBlock = {
127
+ type: "heading";
128
+ level: number;
129
+ text: string;
130
+ line: number;
131
+ } | {
132
+ type: "paragraph";
133
+ text: string;
134
+ line: number;
135
+ } | {
136
+ type: "image";
137
+ alt: string;
138
+ src: string;
139
+ line: number;
140
+ } | {
141
+ type: "list";
142
+ ordered: boolean;
143
+ items: string[];
144
+ line: number;
145
+ } | {
146
+ type: "quote";
147
+ text: string;
148
+ line: number;
149
+ } | {
150
+ type: "code";
151
+ lang: string;
152
+ text: string;
153
+ line: number;
154
+ } | {
155
+ type: "table" | "rule";
156
+ line: number;
157
+ } | {
158
+ type: "html";
159
+ text: string;
160
+ line: number;
161
+ } | {
162
+ type: "component";
163
+ name: string;
164
+ line: number;
109
165
  }
166
+ /** Any other block (e.g. one another plugin adds), named after its markdown-it block type. */
167
+ | {
168
+ type: string;
169
+ line: number;
170
+ };
171
+ /**
172
+ * What a declared marker's `validate` receives: a fresh, plain copy of one
173
+ * use of the component (changing it has no effect). `text` fields are raw
174
+ * markdown — inline formatting is not parsed.
175
+ */
176
+ export interface GutterpressComponent {
177
+ /** The marker as the author typed it, e.g. `"term-box"`. */
178
+ name: string;
179
+ /** The bare word after the marker (`@term-box note` → `"note"`), or null. */
180
+ variant: string | null;
181
+ /** The marker's attributes, e.g. `{ label: "Read this first" }`. */
182
+ attrs: Record<string, string>;
183
+ /** The marker's own 1-based line. */
184
+ line: number;
185
+ /** The raw markdown inside the component, from the line after the marker through its last line of content. */
186
+ text: string;
187
+ /** The component's top-level content, in order. */
188
+ blocks: GutterpressComponentBlock[];
189
+ }
190
+ /**
191
+ * One problem from `validate`: a message, or a message with a 1-based `line`
192
+ * (default: the marker's line) and `severity` (default `"warning"`).
193
+ */
194
+ export type GutterpressComponentProblem = string | {
195
+ message: string;
196
+ line?: number;
197
+ severity?: "error" | "warning" | "info";
198
+ };
199
+ /** `validate`'s return value: nothing, one message, or a list of problems. */
200
+ export type GutterpressComponentProblems = GutterpressComponentProblem[] | string | void | null | undefined;
110
201
  /** A plugin's full declared marker table — see {@link GutterpressMarkerDeclaration}. */
111
202
  export type GutterpressMarkerTable = Record<string, GutterpressMarkerDeclaration>;
112
203
  /**