gutterpress 0.11.18-alpha.3 → 0.11.18-alpha.5

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 (29) hide show
  1. package/dist/api/index.js +2 -2
  2. package/dist/{build-5n7wpqg3.js → build-hc743bws.js} +4 -4
  3. package/dist/{cli-12aq78aq.js → cli-bx24fj78.js} +1 -1
  4. package/dist/{cli-zrs92eef.js → cli-dm3pa0cw.js} +2 -2
  5. package/dist/{cli-72r4c2k4.js → cli-dxs69beq.js} +415 -44
  6. package/dist/{cli-z835nehj.js → cli-q90qmcvp.js} +1 -1
  7. package/dist/cli.js +12 -12
  8. package/dist/{doctor-e3zkvkkh.js → doctor-vq5xetqk.js} +2 -2
  9. package/dist/{engine-17gjf7t0.js → engine-vvtkew4q.js} +1 -1
  10. package/dist/{engine-68avsbe2.js → engine-wjz5yhmp.js} +2 -2
  11. package/dist/{ext-rcf6hfhq.js → ext-zspsqk4y.js} +4 -4
  12. package/dist/{index-q88g67zg.js → index-89zg3jtk.js} +1 -1
  13. package/dist/{index-b81jc99n.js → index-pb4fj8m2.js} +2 -2
  14. package/dist/{index-rpc74pqb.js → index-r2ry6j3x.js} +416 -45
  15. package/dist/index.js +3 -3
  16. package/dist/lib/markdown/block-names.d.ts +82 -0
  17. package/dist/lib/markdown/renderer.d.ts +3 -2
  18. package/dist/lib/markdown/table-align.d.ts +32 -0
  19. package/dist/lib/printsafe.d.ts +2 -0
  20. package/dist/lib/unknown-classes.d.ts +12 -0
  21. package/dist/{manifest.schema-8m82f37q.json → manifest.schema-nc30dyfs.json} +1 -1
  22. package/dist/{new-6pm52vh7.js → new-zrvm9vpe.js} +4 -4
  23. package/dist/{preflight-d84a2ab9.js → preflight-h003cdh9.js} +4 -4
  24. package/dist/{preview-9s4ktkbk.js → preview-xd02xc2h.js} +4 -4
  25. package/dist/{publish-wcrtgxgk.js → publish-1yzay4z4.js} +4 -4
  26. package/dist/render.js +199 -28
  27. package/dist/{theme-j2bagrfx.css → theme-5729pyq1.css} +4 -1
  28. package/dist/{validate-q9c00m8g.js → validate-gmma4d2t.js} +4 -4
  29. package/package.json +1 -1
@@ -9,7 +9,7 @@ import {
9
9
  getAssetsDir,
10
10
  lastPageMode,
11
11
  parseSize
12
- } from "./index-b81jc99n.js";
12
+ } from "./index-pb4fj8m2.js";
13
13
  import {
14
14
  INSTALL_HINTS,
15
15
  execCapture,
@@ -786,10 +786,11 @@ function parseMarkerLine(line, declaredWords) {
786
786
  const kind = head.slice(1);
787
787
  if (!KNOWN_KINDS.includes(kind) && !(declaredWords && declaredWords.has(kind)))
788
788
  return null;
789
- if (kind === "page-break" || kind === "column-break" || kind === "continue" || kind.startsWith("end-")) {
789
+ if (kind === "continue" || kind.startsWith("end-")) {
790
790
  return { kind, name: null, attrs: {} };
791
791
  }
792
- 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);
793
794
  const hasExplicitAttrsOrShorthand = body.some((token) => token.includes("=") || token.startsWith(".") || token.startsWith("#"));
794
795
  const bareTokens = body.filter(isBareToken);
795
796
  let name = null;
@@ -857,6 +858,8 @@ function parseMarkerLine(line, declaredWords) {
857
858
  }
858
859
  if (classes.length)
859
860
  attrs.class = classes.join(" ");
861
+ if (isBreak)
862
+ return { kind, name: null, attrs };
860
863
  const marker = { kind, name, attrs };
861
864
  if (hasAmbiguousBareToken)
862
865
  marker.__ambiguousBareToken = true;
@@ -1039,6 +1042,11 @@ function attachDataAttrs(token, kind, name, attrs) {
1039
1042
  token.attrSet(k.startsWith("data-") ? k : `data-${k}`, v);
1040
1043
  }
1041
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
+ }
1042
1050
  var DECLARED_NAME_RE = /^[a-z][a-z0-9-]*$/;
1043
1051
  function validateDeclaredMarkerName(name, pluginName) {
1044
1052
  if (!DECLARED_NAME_RE.test(name)) {
@@ -1595,7 +1603,11 @@ function plugin(md, pluginOptions = {}) {
1595
1603
  attachDataAttrs(t, component.kind, component.variant, meta.attrs || {});
1596
1604
  else
1597
1605
  attachDataAttrs(t, "section", meta.name, meta.attrs || {});
1598
- 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
+ };
1599
1611
  stack.open({
1600
1612
  kind: "section",
1601
1613
  classes: meta.attrs && meta.attrs.class || "",
@@ -1693,12 +1705,12 @@ function plugin(md, pluginOptions = {}) {
1693
1705
  drainDeclaredFrames(keep);
1694
1706
  const contMeta = {
1695
1707
  name: section.meta.name,
1696
- attrs: { ...section.meta.attrs || {} }
1708
+ attrs: { ...section.meta.attrs || {} },
1709
+ __continued: true
1697
1710
  };
1698
- if (section.meta.component) {
1711
+ delete contMeta.attrs.id;
1712
+ if (section.meta.component)
1699
1713
  contMeta.__component = section.meta.component;
1700
- contMeta.__continued = true;
1701
- }
1702
1714
  contMeta.__line = line;
1703
1715
  const cls = (contMeta.attrs.class || "").split(/\s+/).filter(Boolean);
1704
1716
  if (!cls.includes("gp-continued"))
@@ -1711,16 +1723,14 @@ function plugin(md, pluginOptions = {}) {
1711
1723
  if (kind === "page-break") {
1712
1724
  const t = new state.Token("layout_page_break", "div", 0);
1713
1725
  t.meta = { line };
1714
- t.attrSet("class", "gp-page-break");
1715
- t.attrSet("aria-hidden", "true");
1726
+ attachBreak(t, "gp-page-break", meta.attrs);
1716
1727
  out.push(t);
1717
1728
  continue;
1718
1729
  }
1719
1730
  if (kind === "column-break") {
1720
1731
  const t = new state.Token("layout_column_break", "div", 0);
1721
1732
  t.meta = { line };
1722
- t.attrSet("class", "gp-column-break");
1723
- t.attrSet("aria-hidden", "true");
1733
+ attachBreak(t, "gp-column-break", meta.attrs);
1724
1734
  out.push(t);
1725
1735
  continue;
1726
1736
  }
@@ -1784,24 +1794,10 @@ function plugin(md, pluginOptions = {}) {
1784
1794
  }
1785
1795
  });
1786
1796
  }
1787
- md.renderer.rules.layout_page_break = (tokens, idx) => {
1788
- const cls = tokens[idx].attrGet("class") || "gp-page-break";
1789
- const rangeAttr = tokens[idx].attrGet("data-source-range");
1790
- const rangeHtml = rangeAttr ? ` data-source-range="${escapeAttr(rangeAttr)}"` : "";
1791
- const chapterAttr = tokens[idx].attrGet("data-chapter-src");
1792
- const chapterHtml = chapterAttr ? ` data-chapter-src="${escapeAttr(chapterAttr)}"` : "";
1793
- 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>
1794
1798
  `;
1795
- };
1796
- md.renderer.rules.layout_column_break = (tokens, idx) => {
1797
- const cls = tokens[idx].attrGet("class") || "gp-column-break";
1798
- const rangeAttr = tokens[idx].attrGet("data-source-range");
1799
- const rangeHtml = rangeAttr ? ` data-source-range="${escapeAttr(rangeAttr)}"` : "";
1800
- const chapterAttr = tokens[idx].attrGet("data-chapter-src");
1801
- const chapterHtml = chapterAttr ? ` data-chapter-src="${escapeAttr(chapterAttr)}"` : "";
1802
- return `<div class="${escapeAttr(cls)}" aria-hidden="true"${rangeHtml}${chapterHtml}></div>
1803
- `;
1804
- };
1799
+ md.renderer.rules.layout_page_break = renderBreak;
1800
+ md.renderer.rules.layout_column_break = renderBreak;
1805
1801
  md.renderer.rules.layout_marker = () => "";
1806
1802
  }
1807
1803
  var NO_MARGIN_BOXES = [
@@ -2153,6 +2149,27 @@ var GP_CLASSES = new Set([
2153
2149
  "gp-flush"
2154
2150
  ]);
2155
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
+
2156
2173
  // src/lib/markdown/renderer.ts
2157
2174
  import MarkdownIt from "markdown-it";
2158
2175
 
@@ -2321,6 +2338,156 @@ function gpPinScope(md) {
2321
2338
  });
2322
2339
  }
2323
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
+
2324
2491
  // src/lib/markdown/renderer.ts
2325
2492
  import markdownItSourceMap from "markdown-it-source-map";
2326
2493
  import markdownItDeflist from "markdown-it-deflist";
@@ -2487,6 +2654,10 @@ function registerImageRule(md) {
2487
2654
  if (srcIdx >= 0 && token.attrs) {
2488
2655
  const src = token.attrs[srcIdx][1];
2489
2656
  if (src) {
2657
+ const explicit = takeExplicitName(token);
2658
+ const named = explicit || nameFromSrc(src);
2659
+ if (named)
2660
+ token.attrSet("data-name", named);
2490
2661
  const bucket = env ?? {};
2491
2662
  (bucket.imageRefs ??= []).push(src);
2492
2663
  const cls = token.attrGet("class");
@@ -2862,6 +3033,8 @@ function createMarkdownRenderer(customPlugins) {
2862
3033
  const declaredMarkers = buildDeclaredMarkerRegistry(declaredMarkerSources);
2863
3034
  md.use(plugin, { declaredMarkers });
2864
3035
  md.use(gpPinScope);
3036
+ md.use(gpBlockNames);
3037
+ md.use(gpTableAlign);
2865
3038
  registerImageRule(md);
2866
3039
  if (customPlugins && customPlugins.length > 0) {
2867
3040
  applyPlugins(md, customPlugins);
@@ -2960,6 +3133,8 @@ ${GUTTERPRESS_CSS.trim()}
2960
3133
  }`,
2961
3134
  pluginCss ? `/* extension css */
2962
3135
  ${pluginCss.trim()}` : null,
3136
+ `/* column alignment */
3137
+ ${TABLE_ALIGN_CSS.trim()}`,
2963
3138
  projectCss ? `/* project css */
2964
3139
  ${projectCss.trim()}` : null
2965
3140
  ].filter(Boolean).join(`
@@ -4712,15 +4887,6 @@ async function rollbackNpmPluginInstall(installed) {
4712
4887
  // src/lib/extension-manager.ts
4713
4888
  import { gt as semverGt, rcompare as semverRcompare } from "semver";
4714
4889
 
4715
- // src/lib/slug.ts
4716
- function slugify(name, fallback = "") {
4717
- return name.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || fallback;
4718
- }
4719
- function prettify(slug) {
4720
- const spaced = slug.replace(/[-_]+/g, " ").trim();
4721
- return spaced.charAt(0).toUpperCase() + spaced.slice(1);
4722
- }
4723
-
4724
4890
  // src/lib/project-gitignore.ts
4725
4891
  import { readFile as readFile7, writeFile as writeFile2 } from "node:fs/promises";
4726
4892
  import path6 from "node:path";
@@ -5750,7 +5916,7 @@ import git from "isomorphic-git";
5750
5916
  // package.json
5751
5917
  var package_default = {
5752
5918
  name: "gutterpress",
5753
- version: "0.11.18-alpha.3",
5919
+ version: "0.11.18-alpha.5",
5754
5920
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
5755
5921
  author: "itlackey",
5756
5922
  license: "MPL-2.0",
@@ -7620,6 +7786,8 @@ var ruleRemoteUrls = "printsafe/no-remote-urls";
7620
7786
  var ruleRiskyProps = "printsafe/no-risky-print-effects";
7621
7787
  var ruleSyntax = "printsafe/syntax-error";
7622
7788
  var rulePageContainment = "printsafe/page-containment";
7789
+ var rulePreviewCannotFollow = "printsafe/preview-cannot-follow";
7790
+ var ruleEngineHyphenation = "printsafe/engine-hyphenation";
7623
7791
  function extractUrls(value) {
7624
7792
  const urls = [];
7625
7793
  const re = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
@@ -7765,6 +7933,66 @@ function clipsDescendants(decl) {
7765
7933
  const v = decl.value.trim().toLowerCase();
7766
7934
  return v !== "" && !isInertValue(p, v);
7767
7935
  }
7936
+ var PREVIEW_AT_RULES = new Set(["media", "supports", "layer", "scope", "container", "document"]);
7937
+ var FORCED_BREAK_VALUE = /^(page|left|right|recto|verso)$/i;
7938
+ var LEGACY_FORCED_VALUE = /^(always|left|right)$/i;
7939
+ function previewBlindSpot(decl) {
7940
+ const prop = decl.prop.toLowerCase();
7941
+ const value = decl.value.trim();
7942
+ const written = `${decl.prop}: ${decl.value}`;
7943
+ const isBreak = prop === "break-before" || prop === "break-after";
7944
+ const isLegacy = prop === "page-break-before" || prop === "page-break-after";
7945
+ if (!isBreak && !isLegacy && prop !== "page")
7946
+ return null;
7947
+ const rules = [];
7948
+ for (let p = decl.parent;p; p = p.parent) {
7949
+ if (p.type === "rule")
7950
+ rules.push(p);
7951
+ else if (p.type === "atrule") {
7952
+ const name = p.name.toLowerCase();
7953
+ if (!PREVIEW_AT_RULES.has(name))
7954
+ return null;
7955
+ }
7956
+ }
7957
+ if (rules.length === 0)
7958
+ return null;
7959
+ const consequence = "the PDF honours it but the live preview does not show it, so the two page counts drift apart";
7960
+ if (isLegacy) {
7961
+ if (!LEGACY_FORCED_VALUE.test(value))
7962
+ return null;
7963
+ const modern = prop.replace("page-break-", "break-");
7964
+ const siblings = decl.parent.nodes ?? [];
7965
+ if (siblings.some((n) => n.type === "decl" && n.prop.toLowerCase() === modern))
7966
+ return null;
7967
+ const keyword = /^always$/i.test(value) ? "page" : value.toLowerCase();
7968
+ return `"${written}" is the legacy spelling, and the live preview reads only ${modern}: ${consequence}. Write "${modern}: ${keyword}".`;
7969
+ }
7970
+ const viaVar = /\bvar\(/i.test(value);
7971
+ if (isBreak && !viaVar && !FORCED_BREAK_VALUE.test(value))
7972
+ return null;
7973
+ if (prop === "page" && !viaVar && (value === "" || /^auto$/i.test(value)))
7974
+ return null;
7975
+ const what = prop === "page" ? "page assignment" : "forced break";
7976
+ if (viaVar) {
7977
+ 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.`;
7978
+ }
7979
+ const parent = decl.parent;
7980
+ if (rules.length >= 2 || parent.type === "atrule") {
7981
+ const outer = rules[rules.length - 1].selector;
7982
+ 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.`;
7983
+ }
7984
+ let prev = decl.prev();
7985
+ while (prev && prev.type === "comment")
7986
+ prev = prev.prev();
7987
+ if (prev && (prev.type === "rule" || prev.type === "atrule" && prev.nodes)) {
7988
+ 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.`;
7989
+ }
7990
+ return null;
7991
+ }
7992
+ function isAutoHyphenation(decl) {
7993
+ const prop = decl.prop.toLowerCase();
7994
+ return (prop === "hyphens" || prop === "-webkit-hyphens") && decl.value.trim().toLowerCase() === "auto";
7995
+ }
7768
7996
  function nodeLoc(node) {
7769
7997
  return {
7770
7998
  line: node.source?.start?.line ?? 1,
@@ -7877,6 +8105,22 @@ function checkCssRaw(css, from) {
7877
8105
  });
7878
8106
  }
7879
8107
  });
8108
+ root.walkDecls((decl) => {
8109
+ const message = previewBlindSpot(decl);
8110
+ if (message) {
8111
+ warnings.push({ rule: rulePreviewCannotFollow, severity: "warning", message, ...nodeLoc(decl) });
8112
+ }
8113
+ });
8114
+ root.walkDecls((decl) => {
8115
+ if (!isAutoHyphenation(decl))
8116
+ return;
8117
+ warnings.push({
8118
+ rule: ruleEngineHyphenation,
8119
+ severity: "warning",
8120
+ message: `"${decl.prop}: auto" depends on the browser's hyphenation dictionaries, so the desktop app and the CLI (and different operating systems) can break lines and pages differently. Use "hyphens: manual" with soft hyphens (&shy;) for identical pages everywhere.`,
8121
+ ...nodeLoc(decl)
8122
+ });
8123
+ });
7880
8124
  root.walkRules((rule) => {
7881
8125
  if (rule.parent?.type === "atrule") {
7882
8126
  const at = rule.parent.name.toLowerCase();
@@ -8688,10 +8932,117 @@ registerCheck(check25);
8688
8932
 
8689
8933
  // src/checks/source/layout-markers.ts
8690
8934
  import { readFile as readFile22 } from "node:fs/promises";
8935
+
8936
+ // src/lib/unknown-classes.ts
8937
+ import postcss4 from "postcss";
8938
+ var EMITTED = new Set([
8939
+ "chapter",
8940
+ "spread",
8941
+ "page",
8942
+ "section",
8943
+ "footnotes",
8944
+ "footnotes-sep",
8945
+ "footnote-ref",
8946
+ "footnote-item",
8947
+ "footnote-backref"
8948
+ ]);
8949
+ function classesInTokens(tokens) {
8950
+ const uses = new Map;
8951
+ const note = (token, line) => {
8952
+ const cls = token.attrs?.find(([key]) => key === "class")?.[1];
8953
+ if (!cls)
8954
+ return;
8955
+ for (const c of cls.split(/\s+/)) {
8956
+ if (!c || c.startsWith("gp-") || EMITTED.has(c) || /^chapter-\d+$/.test(c))
8957
+ continue;
8958
+ const seen = uses.get(c);
8959
+ if (seen)
8960
+ seen.count++;
8961
+ else
8962
+ uses.set(c, { line, count: 1 });
8963
+ }
8964
+ };
8965
+ for (const token of tokens) {
8966
+ const meta = token.meta;
8967
+ const line = token.map ? token.map[0] + 1 : typeof meta?.line === "number" ? meta.line : 0;
8968
+ note(token, line);
8969
+ for (const child of token.children ?? [])
8970
+ note(child, line);
8971
+ }
8972
+ return uses;
8973
+ }
8974
+ var CLASS_RE = /\.((?:[\w -￿-]|\\.)+)/g;
8975
+ function addSelectorClasses(selector, into) {
8976
+ const bare = selector.replace(/\[(?:[^\]"']|"[^"]*"|'[^']*')*\]/g, "").replace(/"[^"]*"|'[^']*'/g, "");
8977
+ for (const m of bare.matchAll(CLASS_RE)) {
8978
+ const name = m[1].replace(/\\(.)/g, "$1");
8979
+ if (!/^\d/.test(name))
8980
+ into.add(name);
8981
+ }
8982
+ }
8983
+ function addCssClasses(css, into) {
8984
+ try {
8985
+ postcss4.parse(css).walk((node) => {
8986
+ if (node.type === "rule")
8987
+ addSelectorClasses(node.selector, into);
8988
+ else if (node.type === "atrule") {
8989
+ const at = node;
8990
+ if (/^(scope|container)$/i.test(at.name))
8991
+ addSelectorClasses(at.params, into);
8992
+ }
8993
+ });
8994
+ } catch {
8995
+ addSelectorClasses(css.replace(/\/\*[\s\S]*?\*\//g, ""), into);
8996
+ }
8997
+ }
8998
+ function editDistance3(a, b) {
8999
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
9000
+ for (let i = 1;i <= a.length; i++) {
9001
+ const cur = [i];
9002
+ for (let j = 1;j <= b.length; j++) {
9003
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
9004
+ }
9005
+ prev = cur;
9006
+ }
9007
+ return prev[b.length];
9008
+ }
9009
+ function nearestKnownClass(cls, known) {
9010
+ const limit = cls.length >= 6 ? 2 : 1;
9011
+ let best = null;
9012
+ let bestDistance = limit + 1;
9013
+ for (const candidate of known) {
9014
+ if (Math.abs(candidate.length - cls.length) > limit)
9015
+ continue;
9016
+ const d = editDistance3(cls, candidate);
9017
+ if (d > 0 && d < bestDistance) {
9018
+ best = candidate;
9019
+ bestDistance = d;
9020
+ }
9021
+ }
9022
+ return best;
9023
+ }
9024
+
9025
+ // src/checks/source/layout-markers.ts
9026
+ async function collectKnownClasses(ctx, plugins) {
9027
+ const names = new Set;
9028
+ addCssClasses(MARKER_CSS, names);
9029
+ addCssClasses(GUTTERPRESS_CSS, names);
9030
+ for (const p of plugins)
9031
+ if (p.css)
9032
+ addCssClasses(p.css, names);
9033
+ const roots = [...ctx.cssFiles ?? [], ...plugins.flatMap((p) => p.styles ?? [])];
9034
+ const sheets = (await collectStyleDependencies(ctx.inputDir, roots)).filter((f) => /\.css$/i.test(f));
9035
+ for (const sheet of sheets) {
9036
+ try {
9037
+ addCssClasses(await readFile22(sheet, "utf8"), names);
9038
+ } catch {}
9039
+ }
9040
+ return names;
9041
+ }
8691
9042
  var check26 = {
8692
9043
  id: "source.markdown.layout-markers",
8693
9044
  name: "Layout Markers",
8694
- description: "Reports @page/@section/@chapter marker arguments Gutterpress could not understand, plus any unknown gp-* class",
9045
+ description: "Reports @page/@section/@chapter marker arguments Gutterpress could not understand, plus any unknown gp-* class and any other class no stylesheet mentions",
8695
9046
  category: "source",
8696
9047
  phase: "pre-build",
8697
9048
  async run(ctx) {
@@ -8703,11 +9054,31 @@ var check26 = {
8703
9054
  results.push(inspectionFailed(check26.id, `Plugin "${ref}" could not be loaded, so markers it defines were not checked: ${error2.message}`));
8704
9055
  });
8705
9056
  const md = createMarkdownRenderer(plugins);
9057
+ let known;
9058
+ const knownClasses = () => known ??= collectKnownClasses(ctx, plugins);
8706
9059
  for (const file of files) {
8707
9060
  try {
8708
9061
  const content = await readFile22(file, "utf8");
8709
9062
  const env = {};
8710
- md.render(content, env);
9063
+ const tokens = md.parse(content, env);
9064
+ md.renderer.render(tokens, md.options, env);
9065
+ const uses = classesInTokens(tokens);
9066
+ if (uses.size > 0) {
9067
+ const names = await knownClasses();
9068
+ const warnedLines = new Set((env.layoutWarnings ?? []).map((w) => w.line));
9069
+ for (const [cls, use] of uses) {
9070
+ if (names.has(cls) || warnedLines.has(use.line))
9071
+ continue;
9072
+ const guess = nearestKnownClass(cls, names);
9073
+ results.push(finding(check26.id, {
9074
+ severity: "warning",
9075
+ 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.` : ""),
9076
+ file,
9077
+ ...use.line > 0 ? { line: use.line } : {},
9078
+ code: "unknown_class"
9079
+ }));
9080
+ }
9081
+ }
8711
9082
  for (const w of env.layoutWarnings ?? []) {
8712
9083
  results.push(finding(check26.id, {
8713
9084
  severity: w.severity ?? "warning",
@@ -10306,7 +10677,7 @@ class PdfOutput {
10306
10677
  const rawPdf = pdfxMode ? path13.join(stage, "raw.pdf") : path13.resolve(pdfFile);
10307
10678
  await fsp2.mkdir(path13.dirname(path13.resolve(pdfFile)), { recursive: true });
10308
10679
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
10309
- const { buildNativePdf } = await import("./engine-68avsbe2.js");
10680
+ const { buildNativePdf } = await import("./engine-wjz5yhmp.js");
10310
10681
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
10311
10682
  title: config.title,
10312
10683
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
@@ -11782,7 +12153,7 @@ import path20 from "node:path";
11782
12153
  import { existsSync as existsSync13 } from "node:fs";
11783
12154
  import { mkdir as mkdir8, readFile as readFile29, writeFile as writeFile8 } from "node:fs/promises";
11784
12155
  import path19 from "node:path";
11785
- import postcss4 from "postcss";
12156
+ import postcss5 from "postcss";
11786
12157
  import { Scalar as Scalar2 } from "yaml";
11787
12158
  function isPositivePoints(value) {
11788
12159
  return typeof value === "number" && Number.isFinite(value) && value > 0;
@@ -11843,7 +12214,7 @@ function sizeDeclsOf(rule) {
11843
12214
  function readPageSizeFromCss(css) {
11844
12215
  let root;
11845
12216
  try {
11846
- root = postcss4.parse(css);
12217
+ root = postcss5.parse(css);
11847
12218
  } catch {
11848
12219
  return null;
11849
12220
  }
@@ -11852,7 +12223,7 @@ function readPageSizeFromCss(css) {
11852
12223
  return last ? parseSize(last.value) ?? null : null;
11853
12224
  }
11854
12225
  function setPageSizeInCss(css, page) {
11855
- const root = postcss4.parse(css);
12226
+ const root = postcss5.parse(css);
11856
12227
  const decls = plainPageRules(root).flatMap(sizeDeclsOf);
11857
12228
  const last = decls[decls.length - 1];
11858
12229
  const value = formatPageSize(page);
package/dist/index.js CHANGED
@@ -153,14 +153,14 @@ import {
153
153
  testRemoteAccess,
154
154
  updateExtensions,
155
155
  validateProjectExtensions
156
- } from "./index-rpc74pqb.js";
156
+ } from "./index-r2ry6j3x.js";
157
157
  import"./index-ht1bcqdd.js";
158
158
  import {
159
159
  BUILD_DIAGNOSTIC_CODES
160
- } from "./index-q88g67zg.js";
160
+ } from "./index-89zg3jtk.js";
161
161
  import {
162
162
  BuildError
163
- } from "./index-b81jc99n.js";
163
+ } from "./index-pb4fj8m2.js";
164
164
  import {
165
165
  DEFAULT_PRINT_OPTS,
166
166
  assertMilestone,