gutterpress 0.11.17 → 0.11.18-alpha.10

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 (38) hide show
  1. package/dist/api/index.d.ts +1 -1
  2. package/dist/api/index.js +4 -2
  3. package/dist/{build-gxy185pv.js → build-2tgs5j4d.js} +4 -4
  4. package/dist/{cli-zfgvssqz.js → cli-d0cq87s5.js} +1 -1
  5. package/dist/{cli-a4qtctrw.js → cli-gf1ywzeg.js} +411 -50
  6. package/dist/{cli-ycbg7mxk.js → cli-ptez7zcc.js} +53 -5
  7. package/dist/{cli-8nw6d915.js → cli-ytck518d.js} +1 -1
  8. package/dist/cli.js +12 -12
  9. package/dist/{doctor-fygmzevd.js → doctor-y4yg51zy.js} +2 -2
  10. package/dist/engine/compiler/build.d.ts +1 -1
  11. package/dist/{engine-yf2w9hj7.js → engine-wtnakejg.js} +1 -1
  12. package/dist/{engine-5d1rpxxq.js → engine-zy6fzs5a.js} +2 -2
  13. package/dist/{ext-c48s56jm.js → ext-t2sye4as.js} +4 -4
  14. package/dist/{gutterpress-viewer-ez7k4ha7.js → gutterpress-viewer-k4mpqjh0.js} +406 -14
  15. package/dist/{index-3k3fyrec.js → index-cf1kanp0.js} +454 -52
  16. package/dist/{index-vmcfahe2.js → index-jt552wtf.js} +5 -5
  17. package/dist/{index-09h3n36d.js → index-n8vs6pss.js} +51 -2
  18. package/dist/index.js +5 -3
  19. package/dist/lib/markdown/block-names.d.ts +82 -0
  20. package/dist/lib/markdown/renderer.d.ts +3 -2
  21. package/dist/lib/markdown/table-align.d.ts +32 -0
  22. package/dist/lib/presets.d.ts +11 -0
  23. package/dist/lib/printsafe.d.ts +1 -0
  24. package/dist/lib/remote-auth/sync.d.ts +21 -0
  25. package/dist/lib/unknown-classes.d.ts +12 -0
  26. package/dist/{manifest.schema-gh0jjve9.json → manifest.schema-5pzhscqh.json} +7 -1
  27. package/dist/{new-a9m2jwyy.js → new-pbd5zrds.js} +4 -4
  28. package/dist/{preflight-w3tnbtb8.js → preflight-vnkb8tj7.js} +4 -4
  29. package/dist/preview/file-watcher.d.ts +4 -1
  30. package/dist/{preview-bridge-8jhd1wpb.js → preview-bridge-evecsh4w.js} +5 -1
  31. package/dist/{preview-7z8tt0b4.js → preview-g1hp0jt8.js} +4 -4
  32. package/dist/{preview-interface-c44qk17f.js → preview-interface-2n1d9pbf.js} +77 -1
  33. package/dist/{preview-shell-xwn84kqr.js → preview-shell-hyp7g158.js} +9 -0
  34. package/dist/{publish-bbftdxh2.js → publish-vkbmf3tj.js} +4 -4
  35. package/dist/render.js +199 -28
  36. package/dist/schema/manifest.types.d.ts +11 -0
  37. package/dist/{validate-x1ha1gqj.js → validate-q1xj2qxn.js} +4 -4
  38. package/package.json +1 -1
@@ -9,7 +9,7 @@ import {
9
9
  getAssetsDir,
10
10
  lastPageMode,
11
11
  parseSize
12
- } from "./index-vmcfahe2.js";
12
+ } from "./index-jt552wtf.js";
13
13
  import {
14
14
  INSTALL_HINTS,
15
15
  execCapture,
@@ -155,7 +155,8 @@ var DTRPG_PRESET = {
155
155
  page: {
156
156
  width: 621,
157
157
  height: 810,
158
- tolerance: 0.5
158
+ tolerance: 0.5,
159
+ bleed: 9
159
160
  },
160
161
  ink: {
161
162
  maxTac: 240,
@@ -231,7 +232,8 @@ var BOOK_PRESET = {
231
232
  page: {
232
233
  width: 432,
233
234
  height: 648,
234
- tolerance: 0.5
235
+ tolerance: 0.5,
236
+ bleed: undefined
235
237
  },
236
238
  ink: {
237
239
  maxTac: 400,
@@ -584,7 +586,8 @@ function resolveCustomPage(c, m) {
584
586
  return {
585
587
  width,
586
588
  height,
587
- tolerance: c.page?.tolerance ?? m.page?.tolerance ?? 0.5
589
+ tolerance: c.page?.tolerance ?? m.page?.tolerance ?? 0.5,
590
+ bleed: c.page?.bleed ?? m.page?.bleed
588
591
  };
589
592
  }
590
593
  function resolveWithPreset(cliOverrides, manifest, preset) {
@@ -783,10 +786,11 @@ function parseMarkerLine(line, declaredWords) {
783
786
  const kind = head.slice(1);
784
787
  if (!KNOWN_KINDS.includes(kind) && !(declaredWords && declaredWords.has(kind)))
785
788
  return null;
786
- if (kind === "page-break" || kind === "column-break" || kind === "continue" || kind.startsWith("end-")) {
789
+ if (kind === "continue" || kind.startsWith("end-")) {
787
790
  return { kind, name: null, attrs: {} };
788
791
  }
789
- const body = tokens.slice(1);
792
+ const isBreak = kind === "page-break" || kind === "column-break";
793
+ const body = isBreak ? tokens.slice(1).filter((token) => !isBareToken(token)) : tokens.slice(1);
790
794
  const hasExplicitAttrsOrShorthand = body.some((token) => token.includes("=") || token.startsWith(".") || token.startsWith("#"));
791
795
  const bareTokens = body.filter(isBareToken);
792
796
  let name = null;
@@ -854,6 +858,8 @@ function parseMarkerLine(line, declaredWords) {
854
858
  }
855
859
  if (classes.length)
856
860
  attrs.class = classes.join(" ");
861
+ if (isBreak)
862
+ return { kind, name: null, attrs };
857
863
  const marker = { kind, name, attrs };
858
864
  if (hasAmbiguousBareToken)
859
865
  marker.__ambiguousBareToken = true;
@@ -1036,6 +1042,11 @@ function attachDataAttrs(token, kind, name, attrs) {
1036
1042
  token.attrSet(k.startsWith("data-") ? k : `data-${k}`, v);
1037
1043
  }
1038
1044
  }
1045
+ function attachBreak(token, baseClass, attrs = {}) {
1046
+ addClasses(token, baseClass, attrs.class);
1047
+ token.attrSet("aria-hidden", "true");
1048
+ attachDataAttrs(token, "break", null, attrs);
1049
+ }
1039
1050
  var DECLARED_NAME_RE = /^[a-z][a-z0-9-]*$/;
1040
1051
  function validateDeclaredMarkerName(name, pluginName) {
1041
1052
  if (!DECLARED_NAME_RE.test(name)) {
@@ -1592,7 +1603,11 @@ function plugin(md, pluginOptions = {}) {
1592
1603
  attachDataAttrs(t, component.kind, component.variant, meta.attrs || {});
1593
1604
  else
1594
1605
  attachDataAttrs(t, "section", meta.name, meta.attrs || {});
1595
- t.meta = component ? { line: meta.__line, ...component, ...meta.__continued ? { continued: true } : {} } : { line: meta.__line };
1606
+ t.meta = {
1607
+ line: meta.__line,
1608
+ ...component || {},
1609
+ ...meta.__continued ? { continued: true } : {}
1610
+ };
1596
1611
  stack.open({
1597
1612
  kind: "section",
1598
1613
  classes: meta.attrs && meta.attrs.class || "",
@@ -1690,12 +1705,12 @@ function plugin(md, pluginOptions = {}) {
1690
1705
  drainDeclaredFrames(keep);
1691
1706
  const contMeta = {
1692
1707
  name: section.meta.name,
1693
- attrs: { ...section.meta.attrs || {} }
1708
+ attrs: { ...section.meta.attrs || {} },
1709
+ __continued: true
1694
1710
  };
1695
- if (section.meta.component) {
1711
+ delete contMeta.attrs.id;
1712
+ if (section.meta.component)
1696
1713
  contMeta.__component = section.meta.component;
1697
- contMeta.__continued = true;
1698
- }
1699
1714
  contMeta.__line = line;
1700
1715
  const cls = (contMeta.attrs.class || "").split(/\s+/).filter(Boolean);
1701
1716
  if (!cls.includes("gp-continued"))
@@ -1708,16 +1723,14 @@ function plugin(md, pluginOptions = {}) {
1708
1723
  if (kind === "page-break") {
1709
1724
  const t = new state.Token("layout_page_break", "div", 0);
1710
1725
  t.meta = { line };
1711
- t.attrSet("class", "gp-page-break");
1712
- t.attrSet("aria-hidden", "true");
1726
+ attachBreak(t, "gp-page-break", meta.attrs);
1713
1727
  out.push(t);
1714
1728
  continue;
1715
1729
  }
1716
1730
  if (kind === "column-break") {
1717
1731
  const t = new state.Token("layout_column_break", "div", 0);
1718
1732
  t.meta = { line };
1719
- t.attrSet("class", "gp-column-break");
1720
- t.attrSet("aria-hidden", "true");
1733
+ attachBreak(t, "gp-column-break", meta.attrs);
1721
1734
  out.push(t);
1722
1735
  continue;
1723
1736
  }
@@ -1781,24 +1794,10 @@ function plugin(md, pluginOptions = {}) {
1781
1794
  }
1782
1795
  });
1783
1796
  }
1784
- md.renderer.rules.layout_page_break = (tokens, idx) => {
1785
- const cls = tokens[idx].attrGet("class") || "gp-page-break";
1786
- const rangeAttr = tokens[idx].attrGet("data-source-range");
1787
- const rangeHtml = rangeAttr ? ` data-source-range="${escapeAttr(rangeAttr)}"` : "";
1788
- const chapterAttr = tokens[idx].attrGet("data-chapter-src");
1789
- const chapterHtml = chapterAttr ? ` data-chapter-src="${escapeAttr(chapterAttr)}"` : "";
1790
- return `<div class="${escapeAttr(cls)}" aria-hidden="true"${rangeHtml}${chapterHtml}></div>
1797
+ const renderBreak = (tokens, idx, options2, env, self) => `<div${self.renderAttrs(tokens[idx])}></div>
1791
1798
  `;
1792
- };
1793
- md.renderer.rules.layout_column_break = (tokens, idx) => {
1794
- const cls = tokens[idx].attrGet("class") || "gp-column-break";
1795
- const rangeAttr = tokens[idx].attrGet("data-source-range");
1796
- const rangeHtml = rangeAttr ? ` data-source-range="${escapeAttr(rangeAttr)}"` : "";
1797
- const chapterAttr = tokens[idx].attrGet("data-chapter-src");
1798
- const chapterHtml = chapterAttr ? ` data-chapter-src="${escapeAttr(chapterAttr)}"` : "";
1799
- return `<div class="${escapeAttr(cls)}" aria-hidden="true"${rangeHtml}${chapterHtml}></div>
1800
- `;
1801
- };
1799
+ md.renderer.rules.layout_page_break = renderBreak;
1800
+ md.renderer.rules.layout_column_break = renderBreak;
1802
1801
  md.renderer.rules.layout_marker = () => "";
1803
1802
  }
1804
1803
  var NO_MARGIN_BOXES = [
@@ -2150,6 +2149,27 @@ var GP_CLASSES = new Set([
2150
2149
  "gp-flush"
2151
2150
  ]);
2152
2151
 
2152
+ // src/lib/markdown/table-align.ts
2153
+ var ALIGN = /^text-align:(left|center|right)$/;
2154
+ function gpTableAlign(md) {
2155
+ md.core.ruler.after("block", "gp_table_align", (state) => {
2156
+ for (const token of state.tokens) {
2157
+ if (token.type !== "th_open" && token.type !== "td_open")
2158
+ continue;
2159
+ const match = ALIGN.exec(token.attrGet("style") ?? "");
2160
+ if (!match)
2161
+ continue;
2162
+ token.attrs = (token.attrs ?? []).filter(([key]) => key !== "style");
2163
+ token.attrSet("data-align", match[1]);
2164
+ }
2165
+ });
2166
+ }
2167
+ var TABLE_ALIGN_CSS = `
2168
+ th[data-align="left"], td[data-align="left"] { text-align: left; }
2169
+ th[data-align="center"], td[data-align="center"] { text-align: center; }
2170
+ th[data-align="right"], td[data-align="right"] { text-align: right; }
2171
+ `;
2172
+
2153
2173
  // src/lib/markdown/renderer.ts
2154
2174
  import MarkdownIt from "markdown-it";
2155
2175
 
@@ -2318,6 +2338,156 @@ function gpPinScope(md) {
2318
2338
  });
2319
2339
  }
2320
2340
 
2341
+ // src/lib/slug.ts
2342
+ function slugify(name, fallback = "") {
2343
+ return name.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || fallback;
2344
+ }
2345
+ function prettify(slug) {
2346
+ const spaced = slug.replace(/[-_]+/g, " ").trim();
2347
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
2348
+ }
2349
+
2350
+ // src/lib/markdown/block-names.ts
2351
+ var BLOCK_OPEN = /^layout_(chapter|spread|page|section|component)_open$/;
2352
+ var BLOCK_CLOSE = /^layout_(chapter|spread|page|section|component)_close$/;
2353
+ var FOLDED = {
2354
+ ß: "ss",
2355
+ æ: "ae",
2356
+ œ: "oe",
2357
+ ø: "o",
2358
+ đ: "d",
2359
+ ð: "d",
2360
+ þ: "th",
2361
+ ł: "l",
2362
+ ı: "i"
2363
+ };
2364
+ function nameSlug(text) {
2365
+ return slugify(text.replace(/['’]/g, "").replace(/[ßæœøđðþłı]/gi, (c) => FOLDED[c.toLowerCase()] ?? c));
2366
+ }
2367
+ function nameFromSrc(src) {
2368
+ if (/^data:/i.test(src))
2369
+ return "";
2370
+ const path = src.split(/[?#]/, 1)[0] ?? "";
2371
+ const base = path.split(/[\\/]/).pop() ?? "";
2372
+ let decoded = base;
2373
+ try {
2374
+ decoded = decodeURIComponent(base);
2375
+ } catch {}
2376
+ return nameSlug(decoded.replace(/\.[^.]*$/, ""));
2377
+ }
2378
+ function takeExplicitName(token) {
2379
+ const data = token.attrGet("data-name");
2380
+ const plain = token.attrGet("name");
2381
+ if (plain !== null && token.attrs)
2382
+ token.attrs = token.attrs.filter(([k]) => k !== "name");
2383
+ if (data)
2384
+ return data;
2385
+ if (plain) {
2386
+ token.attrSet("data-name", plain);
2387
+ return plain;
2388
+ }
2389
+ return "";
2390
+ }
2391
+ function inlineText2(inline) {
2392
+ if (!inline || inline.type !== "inline")
2393
+ return "";
2394
+ const parts = [];
2395
+ for (const child of inline.children ?? []) {
2396
+ if (child.type === "text" || child.type === "text_special" || child.type === "code_inline" || child.type === "image") {
2397
+ parts.push(child.content);
2398
+ } else if (child.type === "softbreak" || child.type === "hardbreak") {
2399
+ parts.push(" ");
2400
+ }
2401
+ }
2402
+ return parts.join("");
2403
+ }
2404
+ function claim(used, slug) {
2405
+ let name = slug;
2406
+ for (let n = 2;used.has(name); n++)
2407
+ name = `${slug}-${n}`;
2408
+ used.add(name);
2409
+ return name;
2410
+ }
2411
+ function nameBlocksAndHeadings(tokens) {
2412
+ const blocks = [];
2413
+ const open = [];
2414
+ const headings = [];
2415
+ const headingSlugs = new Map;
2416
+ for (let i = 0;i < tokens.length; i++) {
2417
+ const tok = tokens[i];
2418
+ const opened = BLOCK_OPEN.exec(tok.type);
2419
+ if (opened) {
2420
+ const block = { token: tok, type: opened[1], heading: "" };
2421
+ blocks.push(block);
2422
+ open.push(block);
2423
+ continue;
2424
+ }
2425
+ const closed = BLOCK_CLOSE.exec(tok.type);
2426
+ if (closed) {
2427
+ for (let j = open.length - 1;j >= 0; j--) {
2428
+ if (open[j].type === closed[1]) {
2429
+ open.splice(j, 1);
2430
+ break;
2431
+ }
2432
+ }
2433
+ continue;
2434
+ }
2435
+ if (tok.type === "heading_open") {
2436
+ const slug = nameSlug(inlineText2(tokens[i + 1]));
2437
+ headings.push(tok);
2438
+ headingSlugs.set(tok, slug);
2439
+ const owner = open[open.length - 1];
2440
+ if (owner && !owner.heading)
2441
+ owner.heading = slug;
2442
+ }
2443
+ }
2444
+ const usedBlocks = new Set;
2445
+ const usedHeadings = new Set;
2446
+ const unnamedBlocks = new Set;
2447
+ for (const block of blocks) {
2448
+ const explicit = block.token.attrGet("data-name");
2449
+ if (explicit)
2450
+ usedBlocks.add(explicit);
2451
+ else
2452
+ unnamedBlocks.add(block);
2453
+ }
2454
+ const unnamedHeadings = [];
2455
+ for (const heading of headings) {
2456
+ const explicit = takeExplicitName(heading);
2457
+ if (explicit)
2458
+ usedHeadings.add(explicit);
2459
+ else
2460
+ unnamedHeadings.push(heading);
2461
+ }
2462
+ let previousSection = null;
2463
+ for (const block of blocks) {
2464
+ const { token } = block;
2465
+ if (block.type === "section") {
2466
+ if (token.meta?.continued && previousSection) {
2467
+ const inherited = previousSection.token.attrGet("data-name");
2468
+ if (inherited)
2469
+ token.attrSet("data-name", inherited);
2470
+ previousSection = block;
2471
+ continue;
2472
+ }
2473
+ previousSection = block;
2474
+ }
2475
+ if (!unnamedBlocks.has(block))
2476
+ continue;
2477
+ const slug = nameSlug(token.attrGet("data-label") ?? "") || block.heading;
2478
+ if (slug)
2479
+ token.attrSet("data-name", claim(usedBlocks, slug));
2480
+ }
2481
+ for (const heading of unnamedHeadings) {
2482
+ const slug = headingSlugs.get(heading);
2483
+ if (slug)
2484
+ heading.attrSet("data-name", claim(usedHeadings, slug));
2485
+ }
2486
+ }
2487
+ function gpBlockNames(md) {
2488
+ md.core.ruler.push("gp_block_names", (state) => nameBlocksAndHeadings(state.tokens));
2489
+ }
2490
+
2321
2491
  // src/lib/markdown/renderer.ts
2322
2492
  import markdownItSourceMap from "markdown-it-source-map";
2323
2493
  import markdownItDeflist from "markdown-it-deflist";
@@ -2484,6 +2654,10 @@ function registerImageRule(md) {
2484
2654
  if (srcIdx >= 0 && token.attrs) {
2485
2655
  const src = token.attrs[srcIdx][1];
2486
2656
  if (src) {
2657
+ const explicit = takeExplicitName(token);
2658
+ const named = explicit || nameFromSrc(src);
2659
+ if (named)
2660
+ token.attrSet("data-name", named);
2487
2661
  const bucket = env ?? {};
2488
2662
  (bucket.imageRefs ??= []).push(src);
2489
2663
  const cls = token.attrGet("class");
@@ -2859,6 +3033,8 @@ function createMarkdownRenderer(customPlugins) {
2859
3033
  const declaredMarkers = buildDeclaredMarkerRegistry(declaredMarkerSources);
2860
3034
  md.use(plugin, { declaredMarkers });
2861
3035
  md.use(gpPinScope);
3036
+ md.use(gpBlockNames);
3037
+ md.use(gpTableAlign);
2862
3038
  registerImageRule(md);
2863
3039
  if (customPlugins && customPlugins.length > 0) {
2864
3040
  applyPlugins(md, customPlugins);
@@ -2957,6 +3133,8 @@ ${GUTTERPRESS_CSS.trim()}
2957
3133
  }`,
2958
3134
  pluginCss ? `/* extension css */
2959
3135
  ${pluginCss.trim()}` : null,
3136
+ `/* column alignment */
3137
+ ${TABLE_ALIGN_CSS.trim()}`,
2960
3138
  projectCss ? `/* project css */
2961
3139
  ${projectCss.trim()}` : null
2962
3140
  ].filter(Boolean).join(`
@@ -4709,15 +4887,6 @@ async function rollbackNpmPluginInstall(installed) {
4709
4887
  // src/lib/extension-manager.ts
4710
4888
  import { gt as semverGt, rcompare as semverRcompare } from "semver";
4711
4889
 
4712
- // src/lib/slug.ts
4713
- function slugify(name, fallback = "") {
4714
- return name.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || fallback;
4715
- }
4716
- function prettify(slug) {
4717
- const spaced = slug.replace(/[-_]+/g, " ").trim();
4718
- return spaced.charAt(0).toUpperCase() + spaced.slice(1);
4719
- }
4720
-
4721
4890
  // src/lib/project-gitignore.ts
4722
4891
  import { readFile as readFile7, writeFile as writeFile2 } from "node:fs/promises";
4723
4892
  import path6 from "node:path";
@@ -5747,7 +5916,7 @@ import git from "isomorphic-git";
5747
5916
  // package.json
5748
5917
  var package_default = {
5749
5918
  name: "gutterpress",
5750
- version: "0.11.17",
5919
+ version: "0.11.18-alpha.10",
5751
5920
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
5752
5921
  author: "itlackey",
5753
5922
  license: "MPL-2.0",
@@ -7617,6 +7786,7 @@ var ruleRemoteUrls = "printsafe/no-remote-urls";
7617
7786
  var ruleRiskyProps = "printsafe/no-risky-print-effects";
7618
7787
  var ruleSyntax = "printsafe/syntax-error";
7619
7788
  var rulePageContainment = "printsafe/page-containment";
7789
+ var rulePreviewCannotFollow = "printsafe/preview-cannot-follow";
7620
7790
  function extractUrls(value) {
7621
7791
  const urls = [];
7622
7792
  const re = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
@@ -7762,6 +7932,62 @@ function clipsDescendants(decl) {
7762
7932
  const v = decl.value.trim().toLowerCase();
7763
7933
  return v !== "" && !isInertValue(p, v);
7764
7934
  }
7935
+ var PREVIEW_AT_RULES = new Set(["media", "supports", "layer", "scope", "container", "document"]);
7936
+ var FORCED_BREAK_VALUE = /^(page|left|right|recto|verso)$/i;
7937
+ var LEGACY_FORCED_VALUE = /^(always|left|right)$/i;
7938
+ function previewBlindSpot(decl) {
7939
+ const prop = decl.prop.toLowerCase();
7940
+ const value = decl.value.trim();
7941
+ const written = `${decl.prop}: ${decl.value}`;
7942
+ const isBreak = prop === "break-before" || prop === "break-after";
7943
+ const isLegacy = prop === "page-break-before" || prop === "page-break-after";
7944
+ if (!isBreak && !isLegacy && prop !== "page")
7945
+ return null;
7946
+ const rules = [];
7947
+ for (let p = decl.parent;p; p = p.parent) {
7948
+ if (p.type === "rule")
7949
+ rules.push(p);
7950
+ else if (p.type === "atrule") {
7951
+ const name = p.name.toLowerCase();
7952
+ if (!PREVIEW_AT_RULES.has(name))
7953
+ return null;
7954
+ }
7955
+ }
7956
+ if (rules.length === 0)
7957
+ return null;
7958
+ const consequence = "the PDF honours it but the live preview does not show it, so the two page counts drift apart";
7959
+ if (isLegacy) {
7960
+ if (!LEGACY_FORCED_VALUE.test(value))
7961
+ return null;
7962
+ const modern = prop.replace("page-break-", "break-");
7963
+ const siblings = decl.parent.nodes ?? [];
7964
+ if (siblings.some((n) => n.type === "decl" && n.prop.toLowerCase() === modern))
7965
+ return null;
7966
+ const keyword = /^always$/i.test(value) ? "page" : value.toLowerCase();
7967
+ return `"${written}" is the legacy spelling, and the live preview reads only ${modern}: ${consequence}. Write "${modern}: ${keyword}".`;
7968
+ }
7969
+ const viaVar = /\bvar\(/i.test(value);
7970
+ if (isBreak && !viaVar && !FORCED_BREAK_VALUE.test(value))
7971
+ return null;
7972
+ if (prop === "page" && !viaVar && (value === "" || /^auto$/i.test(value)))
7973
+ return null;
7974
+ const what = prop === "page" ? "page assignment" : "forced break";
7975
+ if (viaVar) {
7976
+ return `The ${what} "${written}" uses var(): the live preview reads it only as a literal ${prop === "page" ? "page name" : "keyword (page, left, right, recto or verso)"}, so ${consequence}. Write the literal value.`;
7977
+ }
7978
+ const parent = decl.parent;
7979
+ if (rules.length >= 2 || parent.type === "atrule") {
7980
+ const outer = rules[rules.length - 1].selector;
7981
+ return `The ${what} "${written}" is inside a nested rule in "${outer}": the live preview reads only top-level rules and rules inside @media, @supports, @layer, @scope, @container and @document, so ${consequence}. Move it to a flat rule of its own.`;
7982
+ }
7983
+ let prev = decl.prev();
7984
+ while (prev && prev.type === "comment")
7985
+ prev = prev.prev();
7986
+ if (prev && (prev.type === "rule" || prev.type === "atrule" && prev.nodes)) {
7987
+ return `The ${what} "${written}" comes right after a nested rule in "${rules[0].selector}": the live preview's CSS reader drops the first declaration after a nested block, so ${consequence}. Move the declaration above the nested rule.`;
7988
+ }
7989
+ return null;
7990
+ }
7765
7991
  function nodeLoc(node) {
7766
7992
  return {
7767
7993
  line: node.source?.start?.line ?? 1,
@@ -7874,6 +8100,12 @@ function checkCssRaw(css, from) {
7874
8100
  });
7875
8101
  }
7876
8102
  });
8103
+ root.walkDecls((decl) => {
8104
+ const message = previewBlindSpot(decl);
8105
+ if (message) {
8106
+ warnings.push({ rule: rulePreviewCannotFollow, severity: "warning", message, ...nodeLoc(decl) });
8107
+ }
8108
+ });
7877
8109
  root.walkRules((rule) => {
7878
8110
  if (rule.parent?.type === "atrule") {
7879
8111
  const at = rule.parent.name.toLowerCase();
@@ -8685,10 +8917,117 @@ registerCheck(check25);
8685
8917
 
8686
8918
  // src/checks/source/layout-markers.ts
8687
8919
  import { readFile as readFile22 } from "node:fs/promises";
8920
+
8921
+ // src/lib/unknown-classes.ts
8922
+ import postcss4 from "postcss";
8923
+ var EMITTED = new Set([
8924
+ "chapter",
8925
+ "spread",
8926
+ "page",
8927
+ "section",
8928
+ "footnotes",
8929
+ "footnotes-sep",
8930
+ "footnote-ref",
8931
+ "footnote-item",
8932
+ "footnote-backref"
8933
+ ]);
8934
+ function classesInTokens(tokens) {
8935
+ const uses = new Map;
8936
+ const note = (token, line) => {
8937
+ const cls = token.attrs?.find(([key]) => key === "class")?.[1];
8938
+ if (!cls)
8939
+ return;
8940
+ for (const c of cls.split(/\s+/)) {
8941
+ if (!c || c.startsWith("gp-") || EMITTED.has(c) || /^chapter-\d+$/.test(c))
8942
+ continue;
8943
+ const seen = uses.get(c);
8944
+ if (seen)
8945
+ seen.count++;
8946
+ else
8947
+ uses.set(c, { line, count: 1 });
8948
+ }
8949
+ };
8950
+ for (const token of tokens) {
8951
+ const meta = token.meta;
8952
+ const line = token.map ? token.map[0] + 1 : typeof meta?.line === "number" ? meta.line : 0;
8953
+ note(token, line);
8954
+ for (const child of token.children ?? [])
8955
+ note(child, line);
8956
+ }
8957
+ return uses;
8958
+ }
8959
+ var CLASS_RE = /\.((?:[\w -￿-]|\\.)+)/g;
8960
+ function addSelectorClasses(selector, into) {
8961
+ const bare = selector.replace(/\[(?:[^\]"']|"[^"]*"|'[^']*')*\]/g, "").replace(/"[^"]*"|'[^']*'/g, "");
8962
+ for (const m of bare.matchAll(CLASS_RE)) {
8963
+ const name = m[1].replace(/\\(.)/g, "$1");
8964
+ if (!/^\d/.test(name))
8965
+ into.add(name);
8966
+ }
8967
+ }
8968
+ function addCssClasses(css, into) {
8969
+ try {
8970
+ postcss4.parse(css).walk((node) => {
8971
+ if (node.type === "rule")
8972
+ addSelectorClasses(node.selector, into);
8973
+ else if (node.type === "atrule") {
8974
+ const at = node;
8975
+ if (/^(scope|container)$/i.test(at.name))
8976
+ addSelectorClasses(at.params, into);
8977
+ }
8978
+ });
8979
+ } catch {
8980
+ addSelectorClasses(css.replace(/\/\*[\s\S]*?\*\//g, ""), into);
8981
+ }
8982
+ }
8983
+ function editDistance3(a, b) {
8984
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
8985
+ for (let i = 1;i <= a.length; i++) {
8986
+ const cur = [i];
8987
+ for (let j = 1;j <= b.length; j++) {
8988
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
8989
+ }
8990
+ prev = cur;
8991
+ }
8992
+ return prev[b.length];
8993
+ }
8994
+ function nearestKnownClass(cls, known) {
8995
+ const limit = cls.length >= 6 ? 2 : 1;
8996
+ let best = null;
8997
+ let bestDistance = limit + 1;
8998
+ for (const candidate of known) {
8999
+ if (Math.abs(candidate.length - cls.length) > limit)
9000
+ continue;
9001
+ const d = editDistance3(cls, candidate);
9002
+ if (d > 0 && d < bestDistance) {
9003
+ best = candidate;
9004
+ bestDistance = d;
9005
+ }
9006
+ }
9007
+ return best;
9008
+ }
9009
+
9010
+ // src/checks/source/layout-markers.ts
9011
+ async function collectKnownClasses(ctx, plugins) {
9012
+ const names = new Set;
9013
+ addCssClasses(MARKER_CSS, names);
9014
+ addCssClasses(GUTTERPRESS_CSS, names);
9015
+ for (const p of plugins)
9016
+ if (p.css)
9017
+ addCssClasses(p.css, names);
9018
+ const roots = [...ctx.cssFiles ?? [], ...plugins.flatMap((p) => p.styles ?? [])];
9019
+ const sheets = (await collectStyleDependencies(ctx.inputDir, roots)).filter((f) => /\.css$/i.test(f));
9020
+ for (const sheet of sheets) {
9021
+ try {
9022
+ addCssClasses(await readFile22(sheet, "utf8"), names);
9023
+ } catch {}
9024
+ }
9025
+ return names;
9026
+ }
8688
9027
  var check26 = {
8689
9028
  id: "source.markdown.layout-markers",
8690
9029
  name: "Layout Markers",
8691
- description: "Reports @page/@section/@chapter marker arguments Gutterpress could not understand, plus any unknown gp-* class",
9030
+ description: "Reports @page/@section/@chapter marker arguments Gutterpress could not understand, plus any unknown gp-* class and any other class no stylesheet mentions",
8692
9031
  category: "source",
8693
9032
  phase: "pre-build",
8694
9033
  async run(ctx) {
@@ -8700,11 +9039,31 @@ var check26 = {
8700
9039
  results.push(inspectionFailed(check26.id, `Plugin "${ref}" could not be loaded, so markers it defines were not checked: ${error2.message}`));
8701
9040
  });
8702
9041
  const md = createMarkdownRenderer(plugins);
9042
+ let known;
9043
+ const knownClasses = () => known ??= collectKnownClasses(ctx, plugins);
8703
9044
  for (const file of files) {
8704
9045
  try {
8705
9046
  const content = await readFile22(file, "utf8");
8706
9047
  const env = {};
8707
- md.render(content, env);
9048
+ const tokens = md.parse(content, env);
9049
+ md.renderer.render(tokens, md.options, env);
9050
+ const uses = classesInTokens(tokens);
9051
+ if (uses.size > 0) {
9052
+ const names = await knownClasses();
9053
+ const warnedLines = new Set((env.layoutWarnings ?? []).map((w) => w.line));
9054
+ for (const [cls, use] of uses) {
9055
+ if (names.has(cls) || warnedLines.has(use.line))
9056
+ continue;
9057
+ const guess = nearestKnownClass(cls, names);
9058
+ results.push(finding(check26.id, {
9059
+ severity: "warning",
9060
+ message: `Class ".${cls}" is not in any stylesheet (Gutterpress's own, the plugins', or this book's), so it styles nothing.` + (guess ? ` Did you mean ".${guess}"?` : "") + (use.count > 1 ? ` Used ${use.count} times in this file; the first is reported.` : ""),
9061
+ file,
9062
+ ...use.line > 0 ? { line: use.line } : {},
9063
+ code: "unknown_class"
9064
+ }));
9065
+ }
9066
+ }
8708
9067
  for (const w of env.layoutWarnings ?? []) {
8709
9068
  results.push(finding(check26.id, {
8710
9069
  severity: w.severity ?? "warning",
@@ -10303,7 +10662,7 @@ class PdfOutput {
10303
10662
  const rawPdf = pdfxMode ? path13.join(stage, "raw.pdf") : path13.resolve(pdfFile);
10304
10663
  await fsp2.mkdir(path13.dirname(path13.resolve(pdfFile)), { recursive: true });
10305
10664
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
10306
- const { buildNativePdf } = await import("./engine-5d1rpxxq.js");
10665
+ const { buildNativePdf } = await import("./engine-zy6fzs5a.js");
10307
10666
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
10308
10667
  title: config.title,
10309
10668
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
@@ -10525,8 +10884,10 @@ async function renderPreviewBook(inputPath, config, opts) {
10525
10884
  }
10526
10885
  });
10527
10886
  }
10528
- function injectPreviewScripts(html) {
10529
- const scripts = ` <script src="/engine/gutterpress-viewer.js"></script>
10887
+ function injectPreviewScripts(html, insetBleedPt = 0) {
10888
+ const meta = insetBleedPt > 0 ? ` <meta name="gp-inset-bleed" content="${insetBleedPt}">
10889
+ ` : "";
10890
+ const scripts = meta + ` <script src="/engine/gutterpress-viewer.js"></script>
10530
10891
  ` + `<script src="/preview/scripts/preview-interface.js"></script>
10531
10892
  ` + `<script src="/preview/scripts/preview-bridge.js"></script>
10532
10893
  `;
@@ -10560,7 +10921,7 @@ async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets, image
10560
10921
  for (const ref of nextImageRefs)
10561
10922
  imageRefs.add(ref);
10562
10923
  }
10563
- await fsp3.writeFile(path15.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html), "utf-8");
10924
+ await fsp3.writeFile(path15.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html, config.page?.bleed), "utf-8");
10564
10925
  }
10565
10926
  function isDotPathUnderRoot(candidatePath, root) {
10566
10927
  const normalizedRoot = root.replace(/\\/g, "/").replace(/\/+$/, "");
@@ -11777,7 +12138,7 @@ import path20 from "node:path";
11777
12138
  import { existsSync as existsSync13 } from "node:fs";
11778
12139
  import { mkdir as mkdir8, readFile as readFile29, writeFile as writeFile8 } from "node:fs/promises";
11779
12140
  import path19 from "node:path";
11780
- import postcss4 from "postcss";
12141
+ import postcss5 from "postcss";
11781
12142
  import { Scalar as Scalar2 } from "yaml";
11782
12143
  function isPositivePoints(value) {
11783
12144
  return typeof value === "number" && Number.isFinite(value) && value > 0;
@@ -11838,7 +12199,7 @@ function sizeDeclsOf(rule) {
11838
12199
  function readPageSizeFromCss(css) {
11839
12200
  let root;
11840
12201
  try {
11841
- root = postcss4.parse(css);
12202
+ root = postcss5.parse(css);
11842
12203
  } catch {
11843
12204
  return null;
11844
12205
  }
@@ -11847,7 +12208,7 @@ function readPageSizeFromCss(css) {
11847
12208
  return last ? parseSize(last.value) ?? null : null;
11848
12209
  }
11849
12210
  function setPageSizeInCss(css, page) {
11850
- const root = postcss4.parse(css);
12211
+ const root = postcss5.parse(css);
11851
12212
  const decls = plainPageRules(root).flatMap(sizeDeclsOf);
11852
12213
  const last = decls[decls.length - 1];
11853
12214
  const value = formatPageSize(page);
@@ -14722,6 +15083,47 @@ async function refreshRemoteCopies(options) {
14722
15083
  return { refreshed: false, reason: "offline" };
14723
15084
  }
14724
15085
  }
15086
+ async function checkRemoteUpdates(options) {
15087
+ const http3 = options.httpClient ?? defaultGitHttp;
15088
+ try {
15089
+ const dir = await repoDirFor(options.projectDir);
15090
+ const transport = await resolveTransport(dir, {
15091
+ ...options.credential ? { credential: options.credential } : {},
15092
+ ...options.tokenStore ? { tokenStore: options.tokenStore } : {}
15093
+ });
15094
+ const branch = await currentBranchOrThrow(dir);
15095
+ const refs = await git4.listServerRefs({
15096
+ http: http3,
15097
+ url: transport.url,
15098
+ prefix: `refs/heads/${branch}`,
15099
+ ...onAuthFor(transport.credential),
15100
+ ...transport.credential ? { onAuthFailure: () => ({ cancel: true }) } : {}
15101
+ });
15102
+ const remoteTip = refs.find((r) => r.ref === `refs/heads/${branch}`)?.oid;
15103
+ if (!remoteTip)
15104
+ return { available: false };
15105
+ const known = async (ref) => {
15106
+ try {
15107
+ return await git4.resolveRef({ fs: gitFs, dir, ref });
15108
+ } catch {
15109
+ return null;
15110
+ }
15111
+ };
15112
+ const tracking = await known(`refs/remotes/${transport.remote}/${branch}`);
15113
+ const local = await known(branch);
15114
+ return { available: remoteTip !== tracking && remoteTip !== local };
15115
+ } catch (e) {
15116
+ const message = e?.message ?? "";
15117
+ if (message === MSG_NO_REMOTE || message === MSG_SSH_REMOTE) {
15118
+ return { available: false, reason: "no-remote" };
15119
+ }
15120
+ const status = e?.data?.statusCode;
15121
+ if (status === 401 || status === 403 || status === 404) {
15122
+ return { available: false, reason: "auth" };
15123
+ }
15124
+ return { available: false, reason: "offline" };
15125
+ }
15126
+ }
14725
15127
 
14726
15128
  // src/lib/remote-auth/online-backup.ts
14727
15129
  var API_BASE3 = "https://api.github.com";
@@ -16999,4 +17401,4 @@ async function setPublishProviderConfig(projectDir, providerId, values) {
16999
17401
  await writeManifestDoc(file, doc);
17000
17402
  return settingsFromDoc(doc);
17001
17403
  }
17002
- 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, DEFAULT_NPM_REGISTRY, npmRegistryUrl, ensureProjectGitignore, EXTENSIONS_DIR, RECOMMENDED_EXTENSIONS, BUILT_IN_STYLE_SET_IDS, sameExtension, listProjectExtensions, describeExtension, setExtensionEnabled, reorderExtensions, removeExtension, addExtension, listNpmVersions, checkExtensionUpdates, updateExtensions, listBuiltInStyleSets, addBuiltInStyleSet, validateProjectExtensions, readExtensionCss, restorePinnedExtensions, restoreFailureMessage, getChecks, getCheckById, resolveCheckSelectors, formatReport, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, checkCss, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, defaultConfigDir, FileTokenStore, extractUrlCredential, getSystemDiagnostics, readPageSetup, setPageSetup, slugifyProjectName, escapeYamlScalar, scaffoldProject, adoptFolder, EXTENSION_KINDS, RESERVED_PREFIX, resolveExtensionPrefix, scaffoldExtension, BUILT_IN_TEMPLATE_IDS, listBuiltInTemplates, saveProjectAsTemplate, listCustomTemplates, importTemplateFromFolder, SNIPPETS_DIR, extractVariables, substituteVariables, listSnippets, saveSnippet, deleteSnippet, listMergedSnippets, listMarkerComponents, listLocationVariants, 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, syncProject, refreshRemoteCopies, isValidRepositoryName, setUpOnlineBackup, sanitizeCloneFolderName, cloneRepository, isSshRemoteUrl, testRemoteAccess, knownForgeTokenUrl, connectGenericHost, parseRemoteOrigin, forgeKindForHost, diagnoseProjectRemote, REPAIR_SNAPSHOT_MESSAGE, repairOnlineBackup, scorchedEarth, listPublishProviders, publishProviderFor, resolvePublishFormat, resolvePublishRequest, runPublish, connectPublishProvider, disconnectPublishCredential, PublishSelectionsStore, readPublishSettings, setPublishProviderConfig };
17404
+ 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, DEFAULT_NPM_REGISTRY, npmRegistryUrl, ensureProjectGitignore, EXTENSIONS_DIR, RECOMMENDED_EXTENSIONS, BUILT_IN_STYLE_SET_IDS, sameExtension, listProjectExtensions, describeExtension, setExtensionEnabled, reorderExtensions, removeExtension, addExtension, listNpmVersions, checkExtensionUpdates, updateExtensions, listBuiltInStyleSets, addBuiltInStyleSet, validateProjectExtensions, readExtensionCss, restorePinnedExtensions, restoreFailureMessage, getChecks, getCheckById, resolveCheckSelectors, formatReport, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, checkCss, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, defaultConfigDir, FileTokenStore, extractUrlCredential, getSystemDiagnostics, readPageSetup, setPageSetup, slugifyProjectName, escapeYamlScalar, scaffoldProject, adoptFolder, EXTENSION_KINDS, RESERVED_PREFIX, resolveExtensionPrefix, scaffoldExtension, BUILT_IN_TEMPLATE_IDS, listBuiltInTemplates, saveProjectAsTemplate, listCustomTemplates, importTemplateFromFolder, SNIPPETS_DIR, extractVariables, substituteVariables, listSnippets, saveSnippet, deleteSnippet, listMergedSnippets, listMarkerComponents, listLocationVariants, 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, syncProject, refreshRemoteCopies, checkRemoteUpdates, isValidRepositoryName, setUpOnlineBackup, sanitizeCloneFolderName, cloneRepository, isSshRemoteUrl, testRemoteAccess, knownForgeTokenUrl, connectGenericHost, parseRemoteOrigin, forgeKindForHost, diagnoseProjectRemote, REPAIR_SNAPSHOT_MESSAGE, repairOnlineBackup, scorchedEarth, listPublishProviders, publishProviderFor, resolvePublishFormat, resolvePublishRequest, runPublish, connectPublishProvider, disconnectPublishCredential, PublishSelectionsStore, readPublishSettings, setPublishProviderConfig };