gutterpress 0.11.17-alpha.5 → 0.11.17-alpha.7

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 (31) hide show
  1. package/dist/api/index.d.ts +2 -2
  2. package/dist/api/index.js +4 -2
  3. package/dist/{build-jr9mqwgr.js → build-nbwgzcgv.js} +4 -4
  4. package/dist/{cli-2xpwjanr.js → cli-2yvrfr58.js} +1 -1
  5. package/dist/{cli-82tvm0sg.js → cli-v3xmpvs8.js} +191 -118
  6. package/dist/{cli-5sg6hcqa.js → cli-vsgj1n9s.js} +1 -1
  7. package/dist/{cli-c3enmb58.js → cli-ycbg7mxk.js} +4 -4
  8. package/dist/cli.js +12 -12
  9. package/dist/{components-dhgv2sm7.yaml → components-zzqdpt3m.yaml} +1 -1
  10. package/dist/{doctor-zxk3pamr.js → doctor-dmagdmyw.js} +2 -2
  11. package/dist/{engine-eactk9nn.js → engine-5d1rpxxq.js} +2 -2
  12. package/dist/{engine-63r7pxsy.js → engine-yf2w9hj7.js} +1 -1
  13. package/dist/{ext-4vapkh0p.js → ext-twnqxfba.js} +4 -4
  14. package/dist/{gutterpress-viewer-1n793gwd.js → gutterpress-viewer-ez7k4ha7.js} +19 -1
  15. package/dist/{index-mb8mzwye.js → index-09h3n36d.js} +2 -2
  16. package/dist/{index-5ez30g3a.js → index-k0k4syyd.js} +215 -120
  17. package/dist/{index-5gp6mkmf.js → index-vmcfahe2.js} +3 -3
  18. package/dist/index.js +5 -3
  19. package/dist/lib/markdown/assemble.d.ts +3 -1
  20. package/dist/lib/markdown/gutterpress-css.d.ts +1 -1
  21. package/dist/lib/markdown/markers.d.ts +27 -154
  22. package/dist/lib/markdown/renderer.d.ts +10 -1
  23. package/dist/lib/snippets.d.ts +23 -0
  24. package/dist/{new-hzs75cmd.js → new-mvtamd8q.js} +4 -4
  25. package/dist/{page-templates-7md4ev7d.css → page-templates-tx9ryt19.css} +4 -11
  26. package/dist/{preflight-4k8dtsfs.js → preflight-et7fnz7v.js} +4 -4
  27. package/dist/{preview-whg4rwg6.js → preview-xzvwzsyz.js} +4 -4
  28. package/dist/{publish-4vtd7qve.js → publish-2x100s48.js} +4 -4
  29. package/dist/render.js +187 -114
  30. package/dist/{validate-c32mm2bh.js → validate-nrpa6kah.js} +4 -4
  31. package/package.json +1 -1
@@ -32,8 +32,8 @@ export { TARGETS, TARGET_IDS, publishTargetFor } from "../lib/targets.ts";
32
32
  export type { PublishTarget, TargetOverlay } from "../lib/targets.ts";
33
33
  export { listBuiltInTemplates, listCustomTemplates, saveProjectAsTemplate, importTemplateFromFolder, BUILT_IN_TEMPLATE_IDS, } from "../lib/project-templates.ts";
34
34
  export type { TemplateInfo, SaveProjectAsTemplateOptions, } from "../lib/project-templates.ts";
35
- export { extractVariables, substituteVariables, listSnippets, saveSnippet, deleteSnippet, listMergedSnippets, listMarkerComponents, SNIPPETS_DIR, } from "../lib/snippets.ts";
36
- export type { MarkerComponent, SnippetEntry, SnippetSource } from "../lib/snippets.ts";
35
+ export { extractVariables, substituteVariables, listSnippets, saveSnippet, deleteSnippet, listMergedSnippets, listMarkerComponents, listLocationVariants, SNIPPETS_DIR, } from "../lib/snippets.ts";
36
+ export type { LocationVariant, MarkerComponent, SnippetEntry, SnippetSource } from "../lib/snippets.ts";
37
37
  export { listProjectExtensions, describeExtension, addExtension, checkExtensionUpdates, listNpmVersions, updateExtensions, removeExtension, setExtensionEnabled, reorderExtensions, validateProjectExtensions, readExtensionCss, listBuiltInStyleSets, addBuiltInStyleSet, sameExtension, RECOMMENDED_EXTENSIONS, BUILT_IN_STYLE_SET_IDS, EXTENSIONS_DIR, PLUGINS_DIR, } from "../lib/extension-manager.ts";
38
38
  export type { ProjectExtensionEntry, ExtensionValidationResult, RecommendedExtension, ExtensionSourceKind, BuiltInStyleSet, BuiltInStyleSetId, AddExtensionOptions, ExtensionUpdateCheck, ExtensionUpdateResult, CheckExtensionUpdatesOptions, UpdateExtensionsOptions, } from "../lib/extension-manager.ts";
39
39
  export { BUNDLED_EXTENSIONS, isBundledExtension, isPathSpecifier, parseExtensionSpecifier, } from "../lib/extension-specifier.ts";
package/dist/api/index.js CHANGED
@@ -70,6 +70,7 @@ import {
70
70
  listCustomTemplates,
71
71
  listGitHubBranches,
72
72
  listGitHubRepositories,
73
+ listLocationVariants,
73
74
  listMarkerComponents,
74
75
  listMergedSnippets,
75
76
  listNpmVersions,
@@ -135,11 +136,11 @@ import {
135
136
  testRemoteAccess,
136
137
  updateExtensions,
137
138
  validateProjectExtensions
138
- } from "../index-5ez30g3a.js";
139
+ } from "../index-k0k4syyd.js";
139
140
  import"../index-ht1bcqdd.js";
140
141
  import {
141
142
  BuildError
142
- } from "../index-5gp6mkmf.js";
143
+ } from "../index-vmcfahe2.js";
143
144
  import {
144
145
  DEFAULT_PRINT_OPTS,
145
146
  assertMilestone,
@@ -242,6 +243,7 @@ export {
242
243
  listNpmVersions,
243
244
  listMergedSnippets,
244
245
  listMarkerComponents,
246
+ listLocationVariants,
245
247
  listLocalBranches,
246
248
  listGitHubRepositories,
247
249
  listGitHubBranches,
@@ -1,19 +1,19 @@
1
1
  import {
2
2
  runBuild,
3
3
  splitOutPath
4
- } from "./cli-82tvm0sg.js";
4
+ } from "./cli-v3xmpvs8.js";
5
5
  import {
6
6
  log
7
- } from "./cli-5sg6hcqa.js";
7
+ } from "./cli-vsgj1n9s.js";
8
8
  import {
9
9
  UsageError,
10
10
  parseFormat,
11
11
  parsePdfxFlavor,
12
12
  rejectExtraPositionals,
13
13
  rejectUnknownFlags
14
- } from "./cli-2xpwjanr.js";
14
+ } from "./cli-2yvrfr58.js";
15
15
  import"./cli-ht1bcqdd.js";
16
- import"./cli-c3enmb58.js";
16
+ import"./cli-ycbg7mxk.js";
17
17
  import {
18
18
  BuildError
19
19
  } from "./cli-46ycxe6r.js";
@@ -4,7 +4,7 @@ import {
4
4
  // package.json
5
5
  var package_default = {
6
6
  name: "gutterpress",
7
- version: "0.11.17-alpha.5",
7
+ version: "0.11.17-alpha.7",
8
8
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
9
9
  author: "itlackey",
10
10
  license: "MPL-2.0",
@@ -13,12 +13,12 @@ import {
13
13
  stampCreator,
14
14
  stripAnnotations,
15
15
  warn
16
- } from "./cli-5sg6hcqa.js";
16
+ } from "./cli-vsgj1n9s.js";
17
17
  import {
18
18
  DEBOUNCE,
19
19
  UsageError,
20
20
  resolvePort
21
- } from "./cli-2xpwjanr.js";
21
+ } from "./cli-2yvrfr58.js";
22
22
  import {
23
23
  blackTextToGray
24
24
  } from "./cli-ht1bcqdd.js";
@@ -27,7 +27,7 @@ import {
27
27
  getAssetPath,
28
28
  getAssetsDir,
29
29
  lastPageMode
30
- } from "./cli-c3enmb58.js";
30
+ } from "./cli-ycbg7mxk.js";
31
31
  import {
32
32
  BuildError,
33
33
  EXIT_CODES
@@ -1169,6 +1169,16 @@ function resolveContainerShape(name, pluginName, decl) {
1169
1169
  ...check
1170
1170
  };
1171
1171
  }
1172
+ function presetVariantOf(name, pluginName, rawDecl) {
1173
+ const preset = rawDecl.preset !== undefined ? rawDecl.preset : {};
1174
+ if (typeof preset !== "object" || preset === null || Array.isArray(preset)) {
1175
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`preset\` that is not a plain object.`);
1176
+ }
1177
+ if (preset.variant !== undefined && typeof preset.variant !== "string") {
1178
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`preset.variant\` that is not a string.`);
1179
+ }
1180
+ return preset.variant;
1181
+ }
1172
1182
  function resolveMarkerDeclaration(name, rawDecl, rawRegistry, originOf) {
1173
1183
  const pluginName = originOf.get(name);
1174
1184
  if (typeof rawDecl !== "object" || rawDecl === null || Array.isArray(rawDecl)) {
@@ -1197,35 +1207,70 @@ function resolveMarkerDeclaration(name, rawDecl, rawRegistry, originOf) {
1197
1207
  if (targetRaw.alias !== undefined) {
1198
1208
  throw new Error(`Plugin "${pluginName}"'s marker "@${name}" aliases "@${rawDecl.alias}", which is itself an alias. ` + `Chained aliases are not supported — alias the base marker directly.`);
1199
1209
  }
1200
- const preset = rawDecl.preset !== undefined ? rawDecl.preset : {};
1201
- if (typeof preset !== "object" || preset === null || Array.isArray(preset)) {
1202
- throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`preset\` that is not a plain object.`);
1203
- }
1204
- if (preset.variant !== undefined && typeof preset.variant !== "string") {
1205
- throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`preset.variant\` that is not a string.`);
1206
- }
1207
1210
  const container = resolveContainerShape(rawDecl.alias, originOf.get(rawDecl.alias), targetRaw);
1208
- return { name, baseKind: rawDecl.alias, presetVariant: preset.variant, ...container };
1211
+ return { name, baseKind: rawDecl.alias, presetVariant: presetVariantOf(name, pluginName, rawDecl), ...container };
1209
1212
  }
1210
- return { name, baseKind: name, ...resolveContainerShape(name, pluginName, rawDecl) };
1213
+ const presetVariant = presetVariantOf(name, pluginName, rawDecl);
1214
+ return {
1215
+ name,
1216
+ baseKind: name,
1217
+ ...presetVariant !== undefined ? { presetVariant } : {},
1218
+ ...resolveContainerShape(name, pluginName, rawDecl)
1219
+ };
1211
1220
  }
1212
1221
  var CORE_MARKER_OWNER = "Gutterpress";
1213
1222
  var CORE_DECLARED_MARKERS = {
1214
1223
  columns: {
1215
1224
  class: "gp-columns",
1225
+ preset: { variant: "2" },
1216
1226
  variants: { 1: "gp-columns-1", 2: "gp-columns-2", 3: "gp-columns-3", 4: "gp-columns-4", 5: "gp-columns-5" }
1217
- }
1227
+ },
1228
+ page: { variants: { blank: "gp-page-blank", "full-bleed": "gp-page-full-bleed" } }
1218
1229
  };
1230
+ var LOCATION_KINDS = ["page", "chapter", "section"];
1231
+ var LOCATION_KEYS = ["variants", "label", "snippet"];
1232
+ function isLocationDeclaration(name, rawDecl) {
1233
+ return LOCATION_KINDS.includes(name) && rawDecl !== null && typeof rawDecl === "object" && !Array.isArray(rawDecl) && Object.hasOwn(rawDecl, "variants") && Object.keys(rawDecl).every((k) => LOCATION_KEYS.includes(k));
1234
+ }
1235
+ function addLocationVariants(kind, rawDecl, pluginName, into) {
1236
+ const { variants, label, snippet } = rawDecl;
1237
+ const where = `Plugin "${pluginName}" declares @${kind} variants`;
1238
+ if (variants === null || typeof variants !== "object" || Array.isArray(variants)) {
1239
+ throw new Error(`${where} that are not a plain object of { word: 'class' }.`);
1240
+ }
1241
+ if (label !== undefined && typeof label !== "string")
1242
+ throw new Error(`${where} with a \`label\` that is not a string.`);
1243
+ if (snippet !== undefined && typeof snippet !== "string")
1244
+ throw new Error(`${where} with a \`snippet\` that is not a string.`);
1245
+ for (const [word, cls] of Object.entries(variants)) {
1246
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(word)) {
1247
+ throw new Error(`${where} with an invalid word "${word}" — use letters, digits, hyphens and underscores.`);
1248
+ }
1249
+ if (typeof cls !== "string" || !cls.trim()) {
1250
+ throw new Error(`${where}: variant "${word}" needs a non-empty class string.`);
1251
+ }
1252
+ const taken = into[kind].get(word);
1253
+ if (taken) {
1254
+ throw new Error(`@${kind} variant "${word}" is declared by both plugin "${taken.owner}" and plugin "${pluginName}". ` + `Variant words must be unique across every loaded plugin — rename one of them.`);
1255
+ }
1256
+ into[kind].set(word, { class: cls.trim(), owner: pluginName, ...label ? { label } : {}, ...snippet ? { snippet } : {} });
1257
+ }
1258
+ }
1219
1259
  function buildDeclaredMarkerRegistry(sources) {
1220
1260
  const rawRegistry = new Map;
1221
1261
  const originOf = new Map;
1262
+ const locationVariants = { page: new Map, chapter: new Map, section: new Map };
1222
1263
  for (const { pluginName, markers } of [{ pluginName: CORE_MARKER_OWNER, markers: CORE_DECLARED_MARKERS }, ...sources]) {
1223
1264
  if (!markers)
1224
1265
  continue;
1225
1266
  for (const [name, rawDecl] of Object.entries(markers)) {
1226
1267
  validateDeclaredMarkerName(name, pluginName);
1268
+ if (isLocationDeclaration(name, rawDecl)) {
1269
+ addLocationVariants(name, rawDecl, pluginName, locationVariants);
1270
+ continue;
1271
+ }
1227
1272
  if (KNOWN_KINDS.includes(name) || pluginName !== CORE_MARKER_OWNER && name in CORE_DECLARED_MARKERS) {
1228
- throw new Error(`Plugin "${pluginName}" declares marker "@${name}", which is a core Gutterpress marker name ` + `(${[...KNOWN_KINDS, ...Object.keys(CORE_DECLARED_MARKERS)].map((k) => `@${k}`).join(", ")}). ` + `Core marker names cannot be shadowed — ` + `rename the plugin's marker.`);
1273
+ throw new Error(`Plugin "${pluginName}" declares marker "@${name}", which is a core Gutterpress marker name ` + `(${[...KNOWN_KINDS, ...Object.keys(CORE_DECLARED_MARKERS).filter((k) => !KNOWN_KINDS.includes(k))].map((k) => `@${k}`).join(", ")}). ` + `Core marker names cannot be shadowed — ` + `rename the plugin's marker.`);
1229
1274
  }
1230
1275
  if (rawRegistry.has(name)) {
1231
1276
  throw new Error(`Marker "@${name}" is declared by both plugin "${originOf.get(name)}" and plugin "${pluginName}". ` + `Declared marker names must be unique across every loaded plugin — rename one of them.`);
@@ -1238,6 +1283,7 @@ function buildDeclaredMarkerRegistry(sources) {
1238
1283
  for (const [name, rawDecl] of rawRegistry) {
1239
1284
  resolved.set(name, resolveMarkerDeclaration(name, rawDecl, rawRegistry, originOf));
1240
1285
  }
1286
+ resolved.locationVariants = locationVariants;
1241
1287
  return resolved;
1242
1288
  }
1243
1289
  function declaredKindWords(declaredMarkers) {
@@ -1293,6 +1339,9 @@ function scanForUnknownDeclaredMarkers(state, declaredMarkers) {
1293
1339
  function variantClassFor(decl, word) {
1294
1340
  return word && decl.variants && Object.hasOwn(decl.variants, word) ? decl.variants[word] : "";
1295
1341
  }
1342
+ function nearVariant(word, variants) {
1343
+ return variants.find((v) => v.toLowerCase() === word.toLowerCase()) || (word.length >= 3 ? nearestDeclaredMarkerName(word, variants) : null);
1344
+ }
1296
1345
  function warnUnknownVariant(env, meta, decl) {
1297
1346
  const word = meta.name;
1298
1347
  if (!word)
@@ -1300,9 +1349,16 @@ function warnUnknownVariant(env, meta, decl) {
1300
1349
  const variants = Object.keys(decl.variants || {});
1301
1350
  if (variants.includes(word))
1302
1351
  return;
1303
- const near = variants.find((v) => v.toLowerCase() === word.toLowerCase()) || (word.length >= 3 ? nearestDeclaredMarkerName(word, variants) : null);
1352
+ const near = nearVariant(word, variants);
1304
1353
  warn2(env, meta.__line, "unknown_variant", `"${word}" is not a variant of @${meta.kind}, so it adds no styling. ` + (variants.length ? `Variants: ${variants.join(", ")}.` : `@${meta.kind} has no variants.`) + (near ? ` Did you mean "${near}"?` : ""), meta);
1305
1354
  }
1355
+ function warnNearMissVariant(env, meta, kind, variants) {
1356
+ const words = [...variants.keys()];
1357
+ const near = nearVariant(meta.name, words);
1358
+ if (!near)
1359
+ return;
1360
+ warn2(env, meta.__line, "unknown_variant", `"${meta.name}" is not a variant of @${kind}, so it is only a ${kind} name and adds no styling. ` + `Variants: ${words.join(", ")}. Did you mean "${near}"?`, meta);
1361
+ }
1306
1362
  function plugin(md, pluginOptions = {}) {
1307
1363
  const options = {
1308
1364
  preferPagesInSpreads: false,
@@ -1310,6 +1366,7 @@ function plugin(md, pluginOptions = {}) {
1310
1366
  };
1311
1367
  const declaredMarkers = options.declaredMarkers instanceof Map && options.declaredMarkers.size > 0 ? options.declaredMarkers : buildDeclaredMarkerRegistry([]);
1312
1368
  const declaredWords = declaredKindWords(declaredMarkers);
1369
+ const locationVariants = declaredMarkers.locationVariants || buildDeclaredMarkerRegistry([]).locationVariants;
1313
1370
  function markerBlock(state, startLine, endLine, silent) {
1314
1371
  const pos = state.bMarks[startLine] + state.tShift[startLine];
1315
1372
  const max = state.eMarks[startLine];
@@ -1388,20 +1445,35 @@ function plugin(md, pluginOptions = {}) {
1388
1445
  const at = declaredFrameIndex(kind);
1389
1446
  if (at === -1)
1390
1447
  return false;
1391
- while (declaredFrames.length > at) {
1392
- const frame = declaredFrames.pop();
1393
- out.push(new state.Token("layout_component_close", frame.tag, -1));
1394
- }
1448
+ drainDeclaredFrames(at);
1395
1449
  return true;
1396
1450
  }
1397
- function drainDeclaredFrames() {
1398
- while (declaredFrames.length) {
1451
+ function sectionAbove(length) {
1452
+ const section = stack.get("section");
1453
+ return !!section && section.inColumns && declaredFrameIndex("columns") === length - 1;
1454
+ }
1455
+ function sectionHostDepth() {
1456
+ const section = stack.get("section");
1457
+ if (section && !section.inColumns)
1458
+ return 0;
1459
+ return declaredFrameIndex("columns") + 1;
1460
+ }
1461
+ function drainDeclaredFrames(keep = 0) {
1462
+ while (declaredFrames.length > keep) {
1463
+ if (sectionAbove(declaredFrames.length)) {
1464
+ stack.close("section");
1465
+ continue;
1466
+ }
1399
1467
  const frame = declaredFrames.pop();
1400
1468
  out.push(new state.Token("layout_component_close", frame.tag, -1));
1401
1469
  }
1402
1470
  }
1403
1471
  function drainDeclaredFramesAtEof() {
1404
1472
  while (declaredFrames.length) {
1473
+ if (sectionAbove(declaredFrames.length)) {
1474
+ stack.close("section");
1475
+ continue;
1476
+ }
1405
1477
  const frame = declaredFrames.pop();
1406
1478
  if (!frame.decl.autoCloseAtEof) {
1407
1479
  warn2(state.env, frame.line || 0, "declared_marker_eof_close", `An open @${frame.kind} reached end-of-document; closing it automatically. Add ` + `@end-${frame.kind} to close it explicitly, or declare autoCloseAt: ["eof"] on this ` + `marker if running to end-of-document is expected.`, null);
@@ -1478,14 +1550,16 @@ function plugin(md, pluginOptions = {}) {
1478
1550
  function openChapter(meta) {
1479
1551
  const t = new state.Token("layout_chapter_open", "div", 1);
1480
1552
  t.meta = { line: meta.__line };
1481
- const classes = meta.attrs?.class || "";
1553
+ const variant = locationVariants.chapter.get(meta.name);
1554
+ const label = variant ? "" : meta.name;
1555
+ const classes = [variant?.class, meta.attrs?.class].filter(Boolean).join(" ");
1482
1556
  addClasses(t, "chapter", classes);
1483
- attachDataAttrs(t, "chapter", meta.name, meta.attrs || {});
1557
+ attachDataAttrs(t, "chapter", label, meta.attrs || {});
1484
1558
  const explicit = (classes.match(/(?:^|\s)(chapter-\d+)(?=\s|$)/) || [])[1] || "";
1485
1559
  const fromAttr = meta.attrs?.ch ? `chapter-${meta.attrs.ch}` : "";
1486
1560
  stack.open({
1487
1561
  kind: "chapter",
1488
- label: meta.name || "",
1562
+ label: label || "",
1489
1563
  counterClass: explicit || fromAttr,
1490
1564
  openerEmitted: false
1491
1565
  }, t);
@@ -1509,7 +1583,11 @@ function plugin(md, pluginOptions = {}) {
1509
1583
  const explicit = meta.attrs && meta.attrs.class ? meta.attrs.class : "";
1510
1584
  const counterClass = chapter ? chapter.counterClass : "";
1511
1585
  const tokens = explicit ? explicit.split(/\s+/) : [];
1512
- const merged = counterClass && !tokens.includes(counterClass) ? explicit ? `${explicit} ${counterClass}` : counterClass : explicit;
1586
+ const withCounter = counterClass && !tokens.includes(counterClass) ? explicit ? `${explicit} ${counterClass}` : counterClass : explicit;
1587
+ const variant = locationVariants.page.get(meta.name);
1588
+ if (!variant && meta.name)
1589
+ warnNearMissVariant(state.env, meta, "page", locationVariants.page);
1590
+ const merged = [variant?.class, withCounter].filter(Boolean).join(" ");
1513
1591
  addClasses(t, "page", merged);
1514
1592
  attachDataAttrs(t, "page", meta.name, meta.attrs || {});
1515
1593
  const label = chapter ? chapter.label : "";
@@ -1545,10 +1623,11 @@ function plugin(md, pluginOptions = {}) {
1545
1623
  __component: { component: meta.kind, kind: decl.baseKind, variant, attrs }
1546
1624
  };
1547
1625
  }
1548
- function openSection(meta) {
1626
+ function openSection(meta, inColumns = false) {
1549
1627
  const t = new state.Token("layout_section_open", "div", 1);
1550
1628
  const component = meta.__component || null;
1551
- addClasses(t, "section", meta.attrs && meta.attrs.class ? meta.attrs.class : "");
1629
+ const variant = component ? null : locationVariants.section.get(meta.name);
1630
+ addClasses(t, "section", [variant?.class, meta.attrs && meta.attrs.class].filter(Boolean).join(" "));
1552
1631
  if (component)
1553
1632
  attachDataAttrs(t, component.kind, component.variant, meta.attrs || {});
1554
1633
  else
@@ -1558,25 +1637,11 @@ function plugin(md, pluginOptions = {}) {
1558
1637
  kind: "section",
1559
1638
  classes: meta.attrs && meta.attrs.class || "",
1560
1639
  sawContent: false,
1640
+ ...inColumns ? { inColumns: true } : {},
1561
1641
  meta: { name: meta.name || null, attrs: { ...meta.attrs || {} }, component },
1562
1642
  openToken: t
1563
1643
  }, t);
1564
1644
  }
1565
- function breakHost() {
1566
- return stack.get("section") || stack.get("page") || stack.get("spread");
1567
- }
1568
- function gridClassOf(frame) {
1569
- if (!frame || !frame.classes)
1570
- return null;
1571
- return frame.classes.split(/\s+/).find((c) => c.startsWith("gp-grid-")) || null;
1572
- }
1573
- function warnBreakInsideGrid(kind, line, meta) {
1574
- const host = breakHost();
1575
- const gridClass = gridClassOf(host);
1576
- if (!gridClass)
1577
- return;
1578
- warn2(state.env, line, "break_inside_grid", `@${kind} inside a grid container: the enclosing @${host.kind} carries .${gridClass}, so this break's <div> becomes a grid ITEM — it takes a cell of its own, corrupts the grid's placement, and print and the live preview then disagree about which page the content after it lands on. Move the break outside the grid @${host.kind}, or remove it — grid rows already flow and fragment across pages on their own.`, meta);
1579
- }
1580
1645
  function warnIfEmptyDecoratedSection(closingKind, closingLine) {
1581
1646
  const sec = stack.get("section");
1582
1647
  if (!sec || sec.sawContent)
@@ -1641,14 +1706,18 @@ function plugin(md, pluginOptions = {}) {
1641
1706
  continue;
1642
1707
  }
1643
1708
  if (kind === "section") {
1644
- drainDeclaredFrames();
1709
+ const keep = sectionHostDepth();
1710
+ drainDeclaredFrames(keep);
1645
1711
  warnIfEmptyDecoratedSection("section", line);
1646
1712
  stack.close("section");
1647
1713
  const spread = stack.get("spread");
1648
1714
  if (spread && spread.noPagesYet) {
1649
1715
  warn2(state.env, line, "spread_without_pages", "@section inside a spread without an explicit @page. Allowed (adhoc spread), but explicit @page markers give stronger control.", meta);
1650
1716
  }
1651
- openSection(meta);
1717
+ if (meta.name && !locationVariants.section.has(meta.name)) {
1718
+ warnNearMissVariant(state.env, meta, "section", locationVariants.section);
1719
+ }
1720
+ openSection(meta, keep > 0);
1652
1721
  continue;
1653
1722
  }
1654
1723
  if (kind === "continue") {
@@ -1657,7 +1726,8 @@ function plugin(md, pluginOptions = {}) {
1657
1726
  warn2(state.env, line, "continue_without_section", "@continue used without an open @section; ignoring marker.", meta);
1658
1727
  continue;
1659
1728
  }
1660
- drainDeclaredFrames();
1729
+ const keep = sectionHostDepth();
1730
+ drainDeclaredFrames(keep);
1661
1731
  const contMeta = {
1662
1732
  name: section.meta.name,
1663
1733
  attrs: { ...section.meta.attrs || {} }
@@ -1672,11 +1742,10 @@ function plugin(md, pluginOptions = {}) {
1672
1742
  cls.push("gp-continued");
1673
1743
  contMeta.attrs.class = cls.join(" ");
1674
1744
  stack.close("section");
1675
- openSection(contMeta);
1745
+ openSection(contMeta, keep > 0);
1676
1746
  continue;
1677
1747
  }
1678
1748
  if (kind === "page-break") {
1679
- warnBreakInsideGrid("page-break", line, meta);
1680
1749
  const t = new state.Token("layout_page_break", "div", 0);
1681
1750
  t.meta = { line };
1682
1751
  t.attrSet("class", "gp-page-break");
@@ -1685,7 +1754,6 @@ function plugin(md, pluginOptions = {}) {
1685
1754
  continue;
1686
1755
  }
1687
1756
  if (kind === "column-break") {
1688
- warnBreakInsideGrid("column-break", line, meta);
1689
1757
  const t = new state.Token("layout_column_break", "div", 0);
1690
1758
  t.meta = { line };
1691
1759
  t.attrSet("class", "gp-column-break");
@@ -1694,7 +1762,7 @@ function plugin(md, pluginOptions = {}) {
1694
1762
  continue;
1695
1763
  }
1696
1764
  if (kind === "end-section") {
1697
- drainDeclaredFrames();
1765
+ drainDeclaredFrames(sectionHostDepth());
1698
1766
  warnIfEmptyDecoratedSection("end-section", line);
1699
1767
  stack.close("section");
1700
1768
  continue;
@@ -1773,6 +1841,25 @@ function plugin(md, pluginOptions = {}) {
1773
1841
  };
1774
1842
  md.renderer.rules.layout_marker = () => "";
1775
1843
  }
1844
+ var NO_MARGIN_BOXES = [
1845
+ "top-left-corner",
1846
+ "top-left",
1847
+ "top-center",
1848
+ "top-right",
1849
+ "top-right-corner",
1850
+ "bottom-left-corner",
1851
+ "bottom-left",
1852
+ "bottom-center",
1853
+ "bottom-right",
1854
+ "bottom-right-corner",
1855
+ "left-top",
1856
+ "left-middle",
1857
+ "left-bottom",
1858
+ "right-top",
1859
+ "right-middle",
1860
+ "right-bottom"
1861
+ ].map((box) => ` @${box} { content: none; }`).join(`
1862
+ `);
1776
1863
  var MARKER_CSS = `
1777
1864
  /* The UA default of 8px body margin is a screen affordance with no meaning
1778
1865
  in paged media, and engines disagree about it: a polyfill that treats the
@@ -1792,11 +1879,48 @@ body { margin: 0; }
1792
1879
  .gp-column-break { break-after: column; height: 0; font-size: 0; line-height: 0; visibility: hidden; }
1793
1880
 
1794
1881
  :where(h1,h2,h3,h4,h5,h6) { break-after: avoid; }
1882
+ /* A lead-in paragraph ("**ROLL A DIE!**" above a roll table, "You will need:"
1883
+ above a list) stays with the table or list it introduces, the way a heading
1884
+ stays with what follows it -- otherwise the label can end a column or page
1885
+ with its table starting on the next (#370). Standard break-after; the
1886
+ :has() selector is Chromium's own. :where() keeps specificity at zero so an
1887
+ author rule such as p { break-after: auto } wins. Deliberately table/ul/ol only,
1888
+ also when a @section wraps it (the second rule): the lead-in then keeps with
1889
+ the section that opens with the table or list. */
1890
+ :where(p):has(+ :where(table, ul, ol)) { break-after: avoid; }
1891
+ :where(p):has(+ :where(.section) > :where(table, ul, ol):first-child) { break-after: avoid; }
1795
1892
  :where(img, svg, video) { max-width: 100%; }
1796
1893
  :where(p > img:only-child, figure > img) { width: fit-content; max-width: 100%; height: auto; vertical-align: bottom; }
1797
1894
  :where(p) > :where(img:not([class])) { max-height: calc(var(--gp-content-h) - 4px); object-fit: contain; }
1798
1895
  :where(figure) { break-inside: avoid; }
1799
- :where(.section, figure) > :where(:first-child) { break-before: avoid; }
1896
+
1897
+ /* Core page variants — \`@page blank\` and \`@page full-bleed\` (CORE_DECLARED_MARKERS).
1898
+ Each is the \`.page\` the marker already emits plus one class; the class
1899
+ assigns the page to a NAMED page, and the named \`@page\` block below is
1900
+ plain CSS Paged Media. Authors never write these names.
1901
+
1902
+ SPEC GAP FILLED: none. Named pages (\`@page <name>\`, \`page:\`) and margin
1903
+ box \`content: none\` are standard and implemented in Chromium; this is
1904
+ author vocabulary that expands to them (permanent, like @columns), not a
1905
+ shim, so nothing here is deleted when Chrome catches up. Both pages are
1906
+ named \`gp-\`, which the shrink-to-fit audit (engine/compiler/build.ts)
1907
+ leaves out of its per-page limits, so they cannot loosen it for other pages.
1908
+
1909
+ The folio still COUNTS on these pages (only what is drawn is cleared), and
1910
+ a book that assigns \`page:\` to every \`.page\` in its own unlayered CSS
1911
+ overrides the assignment, as any unlayered rule overrides this layer. */
1912
+ @page gp-page-blank {
1913
+ ${NO_MARGIN_BOXES}
1914
+ }
1915
+ .page.gp-page-blank { page: gp-page-blank; }
1916
+
1917
+ /* full-bleed: zero page margin, so the content box IS the sheet. The margin
1918
+ boxes are cleared too — with zero margins they would land on the trim line. */
1919
+ @page gp-page-full-bleed {
1920
+ margin: 0;
1921
+ ${NO_MARGIN_BOXES}
1922
+ }
1923
+ .page.gp-page-full-bleed { page: gp-page-full-bleed; }
1800
1924
 
1801
1925
  `;
1802
1926
 
@@ -1856,26 +1980,24 @@ var GUTTERPRESS_CSS = `
1856
1980
  .gp-tight { --gp-gap: 0.5em; }
1857
1981
  .gp-loose { --gp-gap: 2em; }
1858
1982
 
1859
- /* column runs — plain CSS Multi-column, exposed as author vocabulary so
1860
- "put this in two columns" does not require borrowing a styled container
1861
- from the book's own component layer. That borrowing is what this exists
1862
- to prevent: a book whose theme paints .section chrome by default gives
1863
- every author who opens a section just to start a column run a panel they
1864
- did not ask for, and the book then needs a reset rule to take it back.
1865
- With a neutral primitive the author opts into columns and nothing else.
1983
+ /* column runs — plain CSS Multi-column, emitted by the core @columns marker
1984
+ (markers.js): @columns [1-5] ... @end-columns is the ONE way to lay
1985
+ content out in columns, and @column-break moves to the next column.
1986
+ The classes below are the CSS that marker emits, not author vocabulary.
1866
1987
 
1867
1988
  Permanent vocabulary, not a shim: Chromium implements multicol natively
1868
1989
  and these rules are the standard properties verbatim, so there is no
1869
1990
  spec gap here to remove later. Deliberately minimal — column-fill is
1870
- NOT set, because the correct value depends on whether the run fragments
1871
- across pages (auto packs each page's columns; the CSS initial balance is
1872
- right for a run that fits on one page) and only the author knows which.
1873
- --gp-column-gap is author-settable.
1874
- .gp-columns is what the core @columns marker emits (markers.js); on its
1875
- own it is two columns, and @columns 1 to 5 add the matching count, which
1876
- wins because it comes later at the same specificity. */
1877
- .gp-columns, .gp-columns-1, .gp-columns-2, .gp-columns-3, .gp-columns-4, .gp-columns-5 {
1991
+ NOT set (the CSS initial balance applies).
1992
+ Themes style a run through two tokens instead of writing column rules:
1993
+ --gp-column-gap default 1.5em
1994
+ --gp-column-rule default none (any column-rule shorthand value)
1995
+ Both hooks sit at zero specificity (:where), so any author rule wins.
1996
+ .gp-columns is two columns on its own; a count class (bare @columns
1997
+ emits gp-columns-2) wins because it comes later at the same specificity. */
1998
+ :where(.gp-columns, .gp-columns-1, .gp-columns-2, .gp-columns-3, .gp-columns-4, .gp-columns-5) {
1878
1999
  column-gap: var(--gp-column-gap, 1.5em);
2000
+ column-rule: var(--gp-column-rule, none);
1879
2001
  }
1880
2002
  .gp-columns { columns: 2; }
1881
2003
  .gp-columns-1 { columns: 1; }
@@ -1884,54 +2006,8 @@ var GUTTERPRESS_CSS = `
1884
2006
  .gp-columns-4 { columns: 4; }
1885
2007
  .gp-columns-5 { columns: 5; }
1886
2008
 
1887
- /* the per-shape decisions the paragraph above deliberately leaves to the
1888
- author, named instead of left as raw CSS every book was reinventing
1889
- (CLAUDE.md §0: "behavior broadly useful to non-technical authors belongs
1890
- in core").
1891
- Permanent vocabulary, standard properties verbatim — same rationale as
1892
- the column/grid runs above. One name each, no aliases:
1893
- .gp-columns-all column-span: all a heading or block that
1894
- spans every column in the
1895
- run it sits inside.
1896
- .gp-columns-flow column-fill: auto a run that FRAGMENTS across
1897
- pages — every page's
1898
- columns fill instead of
1899
- only the last one
1900
- balancing (the dead-column
1901
- collapse the build's
1902
- engine.multicol.dead-column
1903
- warning names this fix
1904
- for).
1905
- .gp-columns-balanced column-fill: balance a run that fits on ONE
1906
- page (the CSS initial
1907
- value — ragged columns
1908
- would be wrong here). */
1909
- .gp-columns-all { column-span: all; }
1910
- .gp-columns-flow { column-fill: auto; }
1911
- .gp-columns-balanced { column-fill: balance; }
1912
-
1913
- /* grid runs — the SLOTTED counterpart to the column runs above. Grid places
1914
- each child into the next cell, across then down (deterministic slots: card
1915
- layouts, stat blocks, image-plus-caption pairs); columns FLOW one text run
1916
- down then across. Same neutral-primitive rationale as .gp-columns-*, and
1917
- permanent vocabulary for the same reason: standard CSS Grid verbatim, no
1918
- spec gap to remove later. MEASURED (Chromium 151, gp-grid evidence pack):
1919
- grid rows fragment across sheets with EXACT print/viewer parity — 2- and
1920
- 3-col, unequal item heights, mid-row cuts, multi-sheet overflow,
1921
- break-inside:avoid, gap geometry — so a grid taller than the page is safe,
1922
- no fit-one-page constraint. Two things to know, not fix:
1923
- - on a min-height page root (MARKER_CSS), default align-content
1924
- stretches rows apart to fill the page — identically in both engines.
1925
- Authors wanting packed rows set align-content: start.
1926
- - a @page-break / @column-break marker DIRECTLY inside a grid container
1927
- becomes a grid item and corrupts placement (the one measured parity
1928
- break); markers.js diagnoses it (break_inside_grid).
1929
- --gp-grid-gap is author-settable. */
1930
- .gp-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gp-grid-gap, 1.5em); }
1931
- .gp-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gp-grid-gap, 1.5em); }
1932
-
1933
2009
  /* fragmentation controls — generic pagination utilities for any element,
1934
- independent of the column/grid vocabulary above (a book reached for
2010
+ independent of the column vocabulary above (a book reached for
1935
2011
  these just as often outside a multicol run: keeping a card whole,
1936
2012
  forcing a section to start a fresh page). Standard properties only —
1937
2013
  this project is Chromium-only (CLAUDE.md), so there are no legacy
@@ -2089,13 +2165,10 @@ var GP_CLASSES = new Set([
2089
2165
  "gp-columns-3",
2090
2166
  "gp-columns-4",
2091
2167
  "gp-columns-5",
2092
- "gp-columns-all",
2093
- "gp-columns-flow",
2094
- "gp-columns-balanced",
2168
+ "gp-page-blank",
2169
+ "gp-page-full-bleed",
2095
2170
  "gp-no-break",
2096
2171
  "gp-break-before",
2097
- "gp-grid-2",
2098
- "gp-grid-3",
2099
2172
  "gp-shape",
2100
2173
  "gp-pin",
2101
2174
  "gp-top",
@@ -9650,7 +9723,7 @@ class PdfOutput {
9650
9723
  const rawPdf = pdfxMode ? path13.join(stage, "raw.pdf") : path13.resolve(pdfFile);
9651
9724
  await fsp2.mkdir(path13.dirname(path13.resolve(pdfFile)), { recursive: true });
9652
9725
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
9653
- const { buildNativePdf } = await import("./engine-63r7pxsy.js");
9726
+ const { buildNativePdf } = await import("./engine-yf2w9hj7.js");
9654
9727
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
9655
9728
  title: config.title,
9656
9729
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  package_default
3
- } from "./cli-2xpwjanr.js";
3
+ } from "./cli-2yvrfr58.js";
4
4
  import {
5
5
  execCapture,
6
6
  findTool,
@@ -1131,7 +1131,7 @@ var preview_shell_default = "./preview-shell-xwn84kqr.js";
1131
1131
  var CGATS21_CRPC1_default = "./CGATS21_CRPC1-g0e3k7kr.icc";
1132
1132
 
1133
1133
  // src/assets/engine/gutterpress-viewer.js
1134
- var gutterpress_viewer_default = "./gutterpress-viewer-1n793gwd.js";
1134
+ var gutterpress_viewer_default = "./gutterpress-viewer-ez7k4ha7.js";
1135
1135
 
1136
1136
  // src/assets/engine/gutterpress-agent.js
1137
1137
  var gutterpress_agent_default = "./gutterpress-agent-cazqstr1.js";
@@ -1185,7 +1185,7 @@ var package_default2 = "./package-aenwjew3.json";
1185
1185
  var README_default2 = "./README-3nj919rm.md";
1186
1186
 
1187
1187
  // src/assets/extension-templates/theme/components.yaml
1188
- var components_default = "./components-dhgv2sm7.yaml";
1188
+ var components_default = "./components-zzqdpt3m.yaml";
1189
1189
 
1190
1190
  // src/assets/extension-templates/theme/snippets/callout.md
1191
1191
  var callout_default = "./callout-hv56m16h.md";
@@ -1200,7 +1200,7 @@ var base_default = "./base-ypqh9n7f.css";
1200
1200
  var components_default2 = "./components-aam0fe2z.css";
1201
1201
 
1202
1202
  // src/assets/extension-templates/theme/styles/page-templates.css
1203
- var page_templates_default = "./page-templates-7md4ev7d.css";
1203
+ var page_templates_default = "./page-templates-tx9ryt19.css";
1204
1204
 
1205
1205
  // src/assets/extension-templates/theme/styles/page-rules.css
1206
1206
  var page_rules_default = "./page-rules-nsj3aw42.css";
@@ -2226,7 +2226,7 @@ ${mapCss}`;
2226
2226
  if (pageBackgrounds.length)
2227
2227
  log(`audit: ${pageBackgrounds.length} unreferenced @page background image(s)`);
2228
2228
  for (const d of multicol)
2229
- diagnose("engine.multicol.dead-column", `${d} runs over more than one page in columns, and only the last page's columns get balanced — earlier pages are left with an empty column. Add .gp-columns-flow to ${d} (column-fill: auto) so every page's columns fill.`);
2229
+ diagnose("engine.multicol.dead-column", `${d} runs over more than one page in columns, and only the last page's columns get balanced — earlier pages are left with an empty column. End the @columns run where the page ends and open a new @columns for the next page, or set column-fill: auto on ${d} in your stylesheet so every page's columns fill.`);
2230
2230
  if (multicol.length)
2231
2231
  log(`audit: ${multicol.length} fragmenting multicol warning(s)`);
2232
2232
  }