gutterpress 0.11.17-alpha.6 → 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.
@@ -1184,18 +1184,53 @@ var CORE_DECLARED_MARKERS = {
1184
1184
  class: "gp-columns",
1185
1185
  preset: { variant: "2" },
1186
1186
  variants: { 1: "gp-columns-1", 2: "gp-columns-2", 3: "gp-columns-3", 4: "gp-columns-4", 5: "gp-columns-5" }
1187
- }
1187
+ },
1188
+ page: { variants: { blank: "gp-page-blank", "full-bleed": "gp-page-full-bleed" } }
1188
1189
  };
1190
+ var LOCATION_KINDS = ["page", "chapter", "section"];
1191
+ var LOCATION_KEYS = ["variants", "label", "snippet"];
1192
+ function isLocationDeclaration(name, rawDecl) {
1193
+ 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));
1194
+ }
1195
+ function addLocationVariants(kind, rawDecl, pluginName, into) {
1196
+ const { variants, label, snippet } = rawDecl;
1197
+ const where = `Plugin "${pluginName}" declares @${kind} variants`;
1198
+ if (variants === null || typeof variants !== "object" || Array.isArray(variants)) {
1199
+ throw new Error(`${where} that are not a plain object of { word: 'class' }.`);
1200
+ }
1201
+ if (label !== undefined && typeof label !== "string")
1202
+ throw new Error(`${where} with a \`label\` that is not a string.`);
1203
+ if (snippet !== undefined && typeof snippet !== "string")
1204
+ throw new Error(`${where} with a \`snippet\` that is not a string.`);
1205
+ for (const [word, cls] of Object.entries(variants)) {
1206
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(word)) {
1207
+ throw new Error(`${where} with an invalid word "${word}" — use letters, digits, hyphens and underscores.`);
1208
+ }
1209
+ if (typeof cls !== "string" || !cls.trim()) {
1210
+ throw new Error(`${where}: variant "${word}" needs a non-empty class string.`);
1211
+ }
1212
+ const taken = into[kind].get(word);
1213
+ if (taken) {
1214
+ 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.`);
1215
+ }
1216
+ into[kind].set(word, { class: cls.trim(), owner: pluginName, ...label ? { label } : {}, ...snippet ? { snippet } : {} });
1217
+ }
1218
+ }
1189
1219
  function buildDeclaredMarkerRegistry(sources) {
1190
1220
  const rawRegistry = new Map;
1191
1221
  const originOf = new Map;
1222
+ const locationVariants = { page: new Map, chapter: new Map, section: new Map };
1192
1223
  for (const { pluginName, markers } of [{ pluginName: CORE_MARKER_OWNER, markers: CORE_DECLARED_MARKERS }, ...sources]) {
1193
1224
  if (!markers)
1194
1225
  continue;
1195
1226
  for (const [name, rawDecl] of Object.entries(markers)) {
1196
1227
  validateDeclaredMarkerName(name, pluginName);
1228
+ if (isLocationDeclaration(name, rawDecl)) {
1229
+ addLocationVariants(name, rawDecl, pluginName, locationVariants);
1230
+ continue;
1231
+ }
1197
1232
  if (KNOWN_KINDS.includes(name) || pluginName !== CORE_MARKER_OWNER && name in CORE_DECLARED_MARKERS) {
1198
- 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.`);
1233
+ 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.`);
1199
1234
  }
1200
1235
  if (rawRegistry.has(name)) {
1201
1236
  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.`);
@@ -1208,6 +1243,7 @@ function buildDeclaredMarkerRegistry(sources) {
1208
1243
  for (const [name, rawDecl] of rawRegistry) {
1209
1244
  resolved.set(name, resolveMarkerDeclaration(name, rawDecl, rawRegistry, originOf));
1210
1245
  }
1246
+ resolved.locationVariants = locationVariants;
1211
1247
  return resolved;
1212
1248
  }
1213
1249
  function declaredKindWords(declaredMarkers) {
@@ -1263,6 +1299,9 @@ function scanForUnknownDeclaredMarkers(state, declaredMarkers) {
1263
1299
  function variantClassFor(decl, word) {
1264
1300
  return word && decl.variants && Object.hasOwn(decl.variants, word) ? decl.variants[word] : "";
1265
1301
  }
1302
+ function nearVariant(word, variants) {
1303
+ return variants.find((v) => v.toLowerCase() === word.toLowerCase()) || (word.length >= 3 ? nearestDeclaredMarkerName(word, variants) : null);
1304
+ }
1266
1305
  function warnUnknownVariant(env, meta, decl) {
1267
1306
  const word = meta.name;
1268
1307
  if (!word)
@@ -1270,9 +1309,16 @@ function warnUnknownVariant(env, meta, decl) {
1270
1309
  const variants = Object.keys(decl.variants || {});
1271
1310
  if (variants.includes(word))
1272
1311
  return;
1273
- const near = variants.find((v) => v.toLowerCase() === word.toLowerCase()) || (word.length >= 3 ? nearestDeclaredMarkerName(word, variants) : null);
1312
+ const near = nearVariant(word, variants);
1274
1313
  warn(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);
1275
1314
  }
1315
+ function warnNearMissVariant(env, meta, kind, variants) {
1316
+ const words = [...variants.keys()];
1317
+ const near = nearVariant(meta.name, words);
1318
+ if (!near)
1319
+ return;
1320
+ warn(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);
1321
+ }
1276
1322
  function plugin(md, pluginOptions = {}) {
1277
1323
  const options = {
1278
1324
  preferPagesInSpreads: false,
@@ -1280,6 +1326,7 @@ function plugin(md, pluginOptions = {}) {
1280
1326
  };
1281
1327
  const declaredMarkers = options.declaredMarkers instanceof Map && options.declaredMarkers.size > 0 ? options.declaredMarkers : buildDeclaredMarkerRegistry([]);
1282
1328
  const declaredWords = declaredKindWords(declaredMarkers);
1329
+ const locationVariants = declaredMarkers.locationVariants || buildDeclaredMarkerRegistry([]).locationVariants;
1283
1330
  function markerBlock(state, startLine, endLine, silent) {
1284
1331
  const pos = state.bMarks[startLine] + state.tShift[startLine];
1285
1332
  const max = state.eMarks[startLine];
@@ -1463,14 +1510,16 @@ function plugin(md, pluginOptions = {}) {
1463
1510
  function openChapter(meta) {
1464
1511
  const t = new state.Token("layout_chapter_open", "div", 1);
1465
1512
  t.meta = { line: meta.__line };
1466
- const classes = meta.attrs?.class || "";
1513
+ const variant = locationVariants.chapter.get(meta.name);
1514
+ const label = variant ? "" : meta.name;
1515
+ const classes = [variant?.class, meta.attrs?.class].filter(Boolean).join(" ");
1467
1516
  addClasses(t, "chapter", classes);
1468
- attachDataAttrs(t, "chapter", meta.name, meta.attrs || {});
1517
+ attachDataAttrs(t, "chapter", label, meta.attrs || {});
1469
1518
  const explicit = (classes.match(/(?:^|\s)(chapter-\d+)(?=\s|$)/) || [])[1] || "";
1470
1519
  const fromAttr = meta.attrs?.ch ? `chapter-${meta.attrs.ch}` : "";
1471
1520
  stack.open({
1472
1521
  kind: "chapter",
1473
- label: meta.name || "",
1522
+ label: label || "",
1474
1523
  counterClass: explicit || fromAttr,
1475
1524
  openerEmitted: false
1476
1525
  }, t);
@@ -1494,7 +1543,11 @@ function plugin(md, pluginOptions = {}) {
1494
1543
  const explicit = meta.attrs && meta.attrs.class ? meta.attrs.class : "";
1495
1544
  const counterClass = chapter ? chapter.counterClass : "";
1496
1545
  const tokens = explicit ? explicit.split(/\s+/) : [];
1497
- const merged = counterClass && !tokens.includes(counterClass) ? explicit ? `${explicit} ${counterClass}` : counterClass : explicit;
1546
+ const withCounter = counterClass && !tokens.includes(counterClass) ? explicit ? `${explicit} ${counterClass}` : counterClass : explicit;
1547
+ const variant = locationVariants.page.get(meta.name);
1548
+ if (!variant && meta.name)
1549
+ warnNearMissVariant(state.env, meta, "page", locationVariants.page);
1550
+ const merged = [variant?.class, withCounter].filter(Boolean).join(" ");
1498
1551
  addClasses(t, "page", merged);
1499
1552
  attachDataAttrs(t, "page", meta.name, meta.attrs || {});
1500
1553
  const label = chapter ? chapter.label : "";
@@ -1533,7 +1586,8 @@ function plugin(md, pluginOptions = {}) {
1533
1586
  function openSection(meta, inColumns = false) {
1534
1587
  const t = new state.Token("layout_section_open", "div", 1);
1535
1588
  const component = meta.__component || null;
1536
- addClasses(t, "section", meta.attrs && meta.attrs.class ? meta.attrs.class : "");
1589
+ const variant = component ? null : locationVariants.section.get(meta.name);
1590
+ addClasses(t, "section", [variant?.class, meta.attrs && meta.attrs.class].filter(Boolean).join(" "));
1537
1591
  if (component)
1538
1592
  attachDataAttrs(t, component.kind, component.variant, meta.attrs || {});
1539
1593
  else
@@ -1620,6 +1674,9 @@ function plugin(md, pluginOptions = {}) {
1620
1674
  if (spread && spread.noPagesYet) {
1621
1675
  warn(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);
1622
1676
  }
1677
+ if (meta.name && !locationVariants.section.has(meta.name)) {
1678
+ warnNearMissVariant(state.env, meta, "section", locationVariants.section);
1679
+ }
1623
1680
  openSection(meta, keep > 0);
1624
1681
  continue;
1625
1682
  }
@@ -1744,6 +1801,25 @@ function plugin(md, pluginOptions = {}) {
1744
1801
  };
1745
1802
  md.renderer.rules.layout_marker = () => "";
1746
1803
  }
1804
+ var NO_MARGIN_BOXES = [
1805
+ "top-left-corner",
1806
+ "top-left",
1807
+ "top-center",
1808
+ "top-right",
1809
+ "top-right-corner",
1810
+ "bottom-left-corner",
1811
+ "bottom-left",
1812
+ "bottom-center",
1813
+ "bottom-right",
1814
+ "bottom-right-corner",
1815
+ "left-top",
1816
+ "left-middle",
1817
+ "left-bottom",
1818
+ "right-top",
1819
+ "right-middle",
1820
+ "right-bottom"
1821
+ ].map((box) => ` @${box} { content: none; }`).join(`
1822
+ `);
1747
1823
  var MARKER_CSS = `
1748
1824
  /* The UA default of 8px body margin is a screen affordance with no meaning
1749
1825
  in paged media, and engines disagree about it: a polyfill that treats the
@@ -1778,6 +1854,34 @@ body { margin: 0; }
1778
1854
  :where(p) > :where(img:not([class])) { max-height: calc(var(--gp-content-h) - 4px); object-fit: contain; }
1779
1855
  :where(figure) { break-inside: avoid; }
1780
1856
 
1857
+ /* Core page variants — \`@page blank\` and \`@page full-bleed\` (CORE_DECLARED_MARKERS).
1858
+ Each is the \`.page\` the marker already emits plus one class; the class
1859
+ assigns the page to a NAMED page, and the named \`@page\` block below is
1860
+ plain CSS Paged Media. Authors never write these names.
1861
+
1862
+ SPEC GAP FILLED: none. Named pages (\`@page <name>\`, \`page:\`) and margin
1863
+ box \`content: none\` are standard and implemented in Chromium; this is
1864
+ author vocabulary that expands to them (permanent, like @columns), not a
1865
+ shim, so nothing here is deleted when Chrome catches up. Both pages are
1866
+ named \`gp-\`, which the shrink-to-fit audit (engine/compiler/build.ts)
1867
+ leaves out of its per-page limits, so they cannot loosen it for other pages.
1868
+
1869
+ The folio still COUNTS on these pages (only what is drawn is cleared), and
1870
+ a book that assigns \`page:\` to every \`.page\` in its own unlayered CSS
1871
+ overrides the assignment, as any unlayered rule overrides this layer. */
1872
+ @page gp-page-blank {
1873
+ ${NO_MARGIN_BOXES}
1874
+ }
1875
+ .page.gp-page-blank { page: gp-page-blank; }
1876
+
1877
+ /* full-bleed: zero page margin, so the content box IS the sheet. The margin
1878
+ boxes are cleared too — with zero margins they would land on the trim line. */
1879
+ @page gp-page-full-bleed {
1880
+ margin: 0;
1881
+ ${NO_MARGIN_BOXES}
1882
+ }
1883
+ .page.gp-page-full-bleed { page: gp-page-full-bleed; }
1884
+
1781
1885
  `;
1782
1886
 
1783
1887
  // src/lib/markdown/gutterpress-css.ts
@@ -2021,6 +2125,8 @@ var GP_CLASSES = new Set([
2021
2125
  "gp-columns-3",
2022
2126
  "gp-columns-4",
2023
2127
  "gp-columns-5",
2128
+ "gp-page-blank",
2129
+ "gp-page-full-bleed",
2024
2130
  "gp-no-break",
2025
2131
  "gp-break-before",
2026
2132
  "gp-shape",
@@ -5641,7 +5747,7 @@ import git from "isomorphic-git";
5641
5747
  // package.json
5642
5748
  var package_default = {
5643
5749
  name: "gutterpress",
5644
- version: "0.11.17-alpha.6",
5750
+ version: "0.11.17-alpha.7",
5645
5751
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
5646
5752
  author: "itlackey",
5647
5753
  license: "MPL-2.0",
@@ -12594,8 +12700,11 @@ async function collectLibrary(projectDir) {
12594
12700
  loaded.push({ entry, source, folder, snippets: snippets2, markers: await declaredMarkers(entry, projectDir) });
12595
12701
  }
12596
12702
  let registry;
12703
+ let variantRegistry = buildDeclaredMarkerRegistry([]).locationVariants;
12597
12704
  try {
12598
- registry = buildDeclaredMarkerRegistry(loaded.flatMap((l) => l.markers ? [{ pluginName: l.entry.label, markers: l.markers }] : []));
12705
+ const built = buildDeclaredMarkerRegistry(loaded.flatMap((l) => l.markers ? [{ pluginName: l.entry.label, markers: l.markers }] : []));
12706
+ registry = built;
12707
+ variantRegistry = built.locationVariants;
12599
12708
  } catch {
12600
12709
  registry = undefined;
12601
12710
  }
@@ -12661,7 +12770,23 @@ async function collectLibrary(projectDir) {
12661
12770
  component.snippet = best.body;
12662
12771
  }
12663
12772
  components.sort((a, b) => a.name.localeCompare(b.name));
12664
- return { snippets, components };
12773
+ const sourceOf = new Map(loaded.map((l) => [l.entry.label, l.source]));
12774
+ const locationVariants = [];
12775
+ for (const kind of ["page", "chapter", "section"]) {
12776
+ for (const [word, v] of variantRegistry[kind]) {
12777
+ const source = sourceOf.get(v.owner) ?? { kind: "core" };
12778
+ locationVariants.push({
12779
+ kind,
12780
+ word,
12781
+ class: v.class,
12782
+ source,
12783
+ ...v.label ? { label: v.label } : {},
12784
+ ...v.snippet ? { snippet: v.snippet } : {}
12785
+ });
12786
+ }
12787
+ }
12788
+ locationVariants.sort((a, b) => a.kind.localeCompare(b.kind) || a.word.localeCompare(b.word));
12789
+ return { snippets, components, locationVariants };
12665
12790
  }
12666
12791
  async function listMergedSnippets(projectDir) {
12667
12792
  return (await collectLibrary(projectDir)).snippets;
@@ -12669,6 +12794,9 @@ async function listMergedSnippets(projectDir) {
12669
12794
  async function listMarkerComponents(projectDir) {
12670
12795
  return (await collectLibrary(projectDir)).components;
12671
12796
  }
12797
+ async function listLocationVariants(projectDir) {
12798
+ return (await collectLibrary(projectDir)).locationVariants;
12799
+ }
12672
12800
  // src/lib/extension-import.ts
12673
12801
  import { existsSync as existsSync16 } from "node:fs";
12674
12802
  import { cp as cp4, mkdir as mkdir13, mkdtemp as mkdtemp2, readFile as readFile34, rm as rm5, writeFile as writeFile13 } from "node:fs/promises";
@@ -16871,4 +16999,4 @@ async function setPublishProviderConfig(projectDir, providerId, values) {
16871
16999
  await writeManifestDoc(file, doc);
16872
17000
  return settingsFromDoc(doc);
16873
17001
  }
16874
- 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, 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 };
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 };
package/dist/index.js CHANGED
@@ -78,6 +78,7 @@ import {
78
78
  listCustomTemplates,
79
79
  listGitHubBranches,
80
80
  listGitHubRepositories,
81
+ listLocationVariants,
81
82
  listMarkerComponents,
82
83
  listMergedSnippets,
83
84
  listNpmVersions,
@@ -152,7 +153,7 @@ import {
152
153
  testRemoteAccess,
153
154
  updateExtensions,
154
155
  validateProjectExtensions
155
- } from "./index-2vd5b3vy.js";
156
+ } from "./index-k0k4syyd.js";
156
157
  import"./index-ht1bcqdd.js";
157
158
  import {
158
159
  BUILD_DIAGNOSTIC_CODES
@@ -271,6 +272,7 @@ export {
271
272
  listNpmVersions,
272
273
  listMergedSnippets,
273
274
  listMarkerComponents,
275
+ listLocationVariants,
274
276
  listLocalBranches,
275
277
  listGitHubRepositories,
276
278
  listGitHubBranches,
@@ -31,7 +31,9 @@ export type ReadText = (relPath: string) => Promise<string>;
31
31
  * returned something unusable).
32
32
  *
33
33
  * `unknown_variant` is emitted for a declared marker (container or
34
- * `section: true`) whose bare word is not one of its declared `variants`.
34
+ * `section: true`) whose bare word is not one of its declared `variants`, and
35
+ * for an `@page` / `@section` word that is a near-miss of a declared page /
36
+ * section variant (any other word there is a free name and stays silent).
35
37
  *
36
38
  * There is deliberately no `section_without_page`: a @section with no open
37
39
  * @page is valid authoring (17/17 false positives across two real books).
@@ -12,172 +12,37 @@
12
12
  * One entry per LOADED plugin that declared a non-empty `markers` export,
13
13
  * in plugin load order. A plugin with no `markers` export is simply
14
14
  * omitted by the caller — this function never sees it.
15
- * @returns {Map<string, object>} empty when `sources` is empty.
15
+ * @typedef {{ class: string, owner: string, label?: string, snippet?: string }} LocationVariant
16
+ * @returns {Map<string, object> & { locationVariants: { page: Map<string, LocationVariant>, chapter: Map<string, LocationVariant>, section: Map<string, LocationVariant> } }}
17
+ * the container markers (core's `@columns` always), plus the merged
18
+ * `page`/`chapter`/`section` variants as `locationVariants.<kind>: Map<word, { class,
19
+ * owner, label?, snippet? }>` (core's own page variants always).
16
20
  */
17
21
  export function buildDeclaredMarkerRegistry(sources: {
18
22
  pluginName: string;
19
23
  markers: Record<string, unknown>;
20
- }[]): Map<string, object>;
24
+ }[]): Map<string, object> & {
25
+ locationVariants: {
26
+ page: Map<string, LocationVariant>;
27
+ chapter: Map<string, LocationVariant>;
28
+ section: Map<string, LocationVariant>;
29
+ };
30
+ };
21
31
  export default function plugin(md: any, pluginOptions?: {}): void;
32
+ export const MARKER_CSS: string;
22
33
  /**
23
- * The minimal CSS the DOM this module emits requires. Author utility
24
- * vocabulary lives in gutterpress-css.ts — see the ownership note above.
25
- *
26
- * The real contract (#227; a prior version of this paragraph said the
27
- * opposite and was stale — 2026-09-01 CSS architecture review, C4):
28
- * consumers inject this FIRST, wrapped in `@layer gp.marker` (assemble.ts
29
- * declares `@layer gp.marker, gp.vocab;` before both core blocks). Author
30
- * CSS — plugin CSS, every project stylesheet — stays UNLAYERED, and per
31
- * the CSS Cascade Layers
32
- * spec unlayered CSS always wins over layered CSS regardless of selector
33
- * specificity. That is what makes "author wins" true now, not injection
34
- * order. The `:where()` wrapping on the break/orphan/sizing rules below is
35
- * a NARROWER, still-necessary guarantee that survives the layer: it keeps
36
- * this block from out-specificity-ing whatever ELSE an author writes
37
- * (their own plain `.section { break-inside: ... }`, say) — a concern
38
- * `@layer` doesn't touch, since two unlayered rules still settle by
39
- * ordinary specificity between themselves.
40
- *
41
- * `.page`/`.spread` are given `position: relative` so they are the containing
42
- * block for any abspos descendant: a mispinned `bottom: 0` now fails LOCALLY
43
- * on its own page instead of resolving against the document canvas and
44
- * painting on the last page of the book.
45
- *
46
- * `min-height: var(--gp-content-h)` is the other half of that contract: a
47
- * page root that only shrink-wraps its text is a containing block whose
48
- * `bottom` edge is the end of the PROSE, so `.gp-pin .gp-bottom` (and every
49
- * hand-written `position: absolute; bottom: 0`) lands under the last
50
- * paragraph instead of at the page foot. Nothing in a continuous document
51
- * stretches a page root to the page area on its own, so both renderers
52
- * publish the page CONTENT height as `--gp-content-h` for the page context
53
- * the element is in
54
- * (the viewer on each `.gp-strip`; the compiler on `:root` plus every
55
- * `page:` assignment selector), and custom properties inherit, so this one
56
- * rule reaches a page root at any wrapper depth. Undefined var (plain
57
- * markdown-it use, no engine) falls back to `1px`, which the cushion below
58
- * takes back to zero — the rule is inert outside the pipeline, leaving the
59
- * page root shrink-wrapped exactly as it is today.
60
- *
61
- * It is `min-height`, not `height`: a `.page` whose content runs past one
62
- * sheet must still fragment normally. The whole rule is `:where()`, so an
63
- * author who wants any of it back sets their own value at any specificity.
64
- *
65
- * `box-sizing` and `display` are what make a page-sized min-height safe, and
66
- * both were found by MEASURING a real book (docs/fixtures/css-authoring-spike)
67
- * print one extra sheet without them:
68
- *
69
- * - `border-box`, because a page root with padding (that fixture's cover
70
- * has `padding-top: 2.5in`) would otherwise be one content box PLUS its
71
- * padding tall, which is one sheet plus 2.5in — the overflow lands on a
72
- * spurious blank sheet.
73
- * - `flow-root`, because a child margin collapsing THROUGH the root's edge
74
- * survives the forced break and eats into the new sheet: that fixture's
75
- * last page opens with an `<h2>` whose 22.4px margin-top pushed a
76
- * 719px-tall root down 22.4px on a 720px sheet, spilling it. A page root
77
- * is a page; containing its children's margins is what a page does. The
78
- * viewer already forces `flow-root` on displaced page roots for exactly
79
- * this reason (`stabilizeFullHeightPageRoots`, fragment.ts) — declaring
80
- * it once in core means both renderers start from the same box. Zero
81
- * specificity, so an authored `display: flex`/`grid` page root still
82
- * wins, which is the case that function's comment warns about.
83
- *
84
- * The `- 1px` is the fragmentation cushion, and it is load-bearing. A box
85
- * whose bottom edge lands EXACTLY on the fragmentainer's edge costs an empty
86
- * fragment: MEASURED (Chromium 148, 384x480px sheet, 24px margins, one
87
- * short page root) `min-height: 432px` prints 2 sheets with the second
88
- * blank while `431px` prints 1 — and the viewer's multicol strip does the
89
- * same, which is how it first showed up (an 8pp preview of a 7pp book on
90
- * the gp-image-positioning fixture). One px at 96dpi is 0.75pt: it moves a
91
- * pinned foot up by less than a point and absorbs sub-pixel rounding in the
92
- * published value. Do not "clean it up" — the sheet it costs is silent.
93
- *
94
- * The break/orphan rules below (`break-after` on headings, image sizing)
95
- * are all `:where()` so they carry zero specificity —
96
- * author CSS at any specificity wins outright, reusing this same
97
- * after-author injection point.
98
- *
99
- * There is deliberately NO `.section > :first-child { break-before: avoid }`
100
- * "glue". A first child's break-before propagates to its parent's start
101
- * edge, so it made the break BEFORE every section `avoid`: a section after
102
- * other content could not move whole to the next page, and Chromium split it
103
- * instead, ignoring its own `break-inside: avoid`. Measured with and without
104
- * (marker-css-section-break.test.ts): it never prevented a stranded top edge
105
- * or heading. Its one useful side effect, a lead-in paragraph travelling with
106
- * a following @section that wraps a table, is the explicit rule below.
107
- *
108
- * `vertical-align: bottom` on a lone image is a print-correctness rule, not
109
- * cosmetics. An `<img>` in a `<p>` is inline-level, so its line box adds
110
- * half-leading/descender space UNDER the image: an image sized to exactly the
111
- * page content box (a book capping art at `page-height - margins`, or art that
112
- * naturally fills the page) produces a paragraph a few px TALLER than the box
113
- * it was sized to fit. MEASURED (Chromium 148, field guide chapter 1, 10in
114
- * content box): a 956px image made a 963.59px paragraph — a 3.6px overflow
115
- * that pushed the enclosing named-page wrapper's bottom edge onto the NEXT
116
- * sheet, so Chromium named that sheet after the PREVIOUS page name and the new
117
- * template's running head and folio silently vanished. `vertical-align:
118
- * bottom` collapses the line box onto the image, keeping the image inline (so
119
- * `text-align: center` still centers it — `display: block` would not).
120
- *
121
- * #231 — two engine-generic print fixes adopted from a real book's own
122
- * engine sheet, because both are written entirely in terms of core's own
123
- * published contract and fix a failure every book with big art or a
124
- * `<figure>` hits, not a DC-brand-specific one:
125
- *
126
- * - A bare markdown placard (`![Alt](art.jpg)`, no class) taller than the
127
- * page content box is monolithic replaced content, and the fragmenter
128
- * SLICES it mid-image across the page break instead of moving it whole.
129
- * Capping a bare image to `--gp-content-h` (core's own published page
130
- * CONTENT height for the page context it is in — see the min-height
131
- * rule above) with `object-fit: contain` letterboxes it onto one page
132
- * instead. Scoped to `:not([class])` so it never touches a `.gp-full`,
133
- * `.gp-bleed`, or any other explicitly sized/classed image — an author
134
- * who already sized their own art has already made the call this rule
135
- * exists to make for the ones who haven't. `--gp-content-h` is
136
- * published by BOTH renderers (the compiler on `:root` plus every
137
- * `page:` assignment selector; the viewer on each `.gp-strip`), so this
138
- * rule cannot itself split preview from print.
139
- *
140
- * MEASURED on the field guide (295pp, adopted verbatim from its own
141
- * native-furniture.css §9, which carried this exact rule for months):
142
- * chapter-01's rabbit placard paints 717.0pt tall on the default page
143
- * both before and after adoption (core's cap and the book's copy agree
144
- * bit-for-bit — the book's copy is now a harmless duplicate of core's,
145
- * not a competing rule). Chapter-03's full-sheet plate (an `@page` with
146
- * zero margins, so the content box IS the sheet) paints 621.0 x 804.0pt
147
- * inside its 621 x 810pt sheet both before and after — the OLD hand-
148
- * computed version of this rule (`calc(var(--page-height) - 0.5in -
149
- * 0.75in - 4px)`, hard-coding the DEFAULT page's margins) over-capped
150
- * this named page's art to 553.5 x 717.0pt, a 1.3in band of bare wall
151
- * along its foot; reading the cap off `--gp-content-h` instead fixed
152
- * that BEFORE core ever adopted the rule, so adoption itself changes
153
- * nothing further. Total page count unchanged at 295pp — no
154
- * shrink-to-fit side effect from moving the rule into core.
155
- *
156
- * - `figure { break-inside: avoid }` keeps in-flow art (anything an
157
- * author or a plugin wraps in a real `<figure>`) from being sliced
158
- * across a page break the same way a bare placard would be — the
159
- * `:where(p > img:only-child, figure > img)` sizing rule above already
160
- * assumes the figure survives as one piece; without this it did not.
161
- *
162
- * Design-for-deletion note (CLAUDE.md): neither rule is a Chromium-gap shim
163
- * with a removal trigger — both are permanent, standards-verbatim defaults
164
- * in the same category as the min-height/box-sizing/vertical-align rules
165
- * above (real print-fragmentation behavior, not a spec feature Chrome has
166
- * yet to ship). They stay `:where()` so an author's own sizing or
167
- * break-inside rule, at ANY specificity, still wins outright.
168
- *
169
- * The companion "spanner + unbreakable box" glue pattern (`column-span: all`
170
- * followed by a `break-inside: avoid` box) was measured for this same adoption pass and DELIBERATELY NOT
171
- * adopted: a `:where()`-zero-specificity override can never win against
172
- * whatever real-specificity rule gave the box its `break-inside: avoid` in
173
- * the first place (a synthetic 2-column fixture with the candidate
174
- * `:where(.gp-columns-all) + :where(*) { break-inside: auto }` produced
175
- * BYTE-IDENTICAL output to the same fixture without it, across every
176
- * geometry swept), and the field guide's own native-furniture.css §13 shows
177
- * a context-blind version of the same idea is actively wrong for at least
178
- * one real, shipped case (a short, non-fragmenting instance that needed its
179
- * `avoid` restored after a general fragmenting-context rule took it away).
180
- * Core ships no spanner class: a spanning heading is the end of one @columns run and the start of the next.
181
- *
34
+ * Merge every loaded plugin's `markers` export into ONE flat, resolved
35
+ * registry — `Map<name, ResolvedDecl>` — validating collisions at LOAD TIME
36
+ * (#240 / P2): a declared name that shadows a core reserved name, or that
37
+ * two different plugins both declare, throws immediately, naming both
38
+ * sides, instead of the pre-#240 silent-skip footgun (core claims its
39
+ * `@`-names during block parsing, before any plugin runs, so a colliding
40
+ * plugin marker used to never run and never warn — see the header comment
41
+ * on `KNOWN_KINDS`).
182
42
  */
183
- export const MARKER_CSS: "\n/* The UA default of 8px body margin is a screen affordance with no meaning\n in paged media, and engines disagree about it: a polyfill that treats the\n page div as the page box drops it, native print keeps it. Left in place it\n insets EVERY page's content by 8px per side, and -- measured, 300dpi,\n 6x4in sheet -- it is what stops a full-width block from reaching the\n paper: it lands at 0.080..5.917in of a 6in sheet instead of\n 0.000..6.000in, because width:100% resolves against the BODY content box,\n not the page's. Zeroing it here (first in the cascade) makes the two\n agree. Authors who want a body margin still just declare one. */\nbody { margin: 0; }\n\n.gp-page-break { break-before: page; }\n.page { break-before: page; }\n.spread { break-before: page; }\n:where(.page, .spread) { position: relative; display: flow-root; box-sizing: border-box; min-height: calc(var(--gp-content-h, 1px) - 1px); }\n.gp-column-break { break-after: column; height: 0; font-size: 0; line-height: 0; visibility: hidden; }\n\n:where(h1,h2,h3,h4,h5,h6) { break-after: avoid; }\n/* A lead-in paragraph (\"**ROLL A DIE!**\" above a roll table, \"You will need:\"\n above a list) stays with the table or list it introduces, the way a heading\n stays with what follows it -- otherwise the label can end a column or page\n with its table starting on the next (#370). Standard break-after; the\n :has() selector is Chromium's own. :where() keeps specificity at zero so an\n author rule such as p { break-after: auto } wins. Deliberately table/ul/ol only,\n also when a @section wraps it (the second rule): the lead-in then keeps with\n the section that opens with the table or list. */\n:where(p):has(+ :where(table, ul, ol)) { break-after: avoid; }\n:where(p):has(+ :where(.section) > :where(table, ul, ol):first-child) { break-after: avoid; }\n:where(img, svg, video) { max-width: 100%; }\n:where(p > img:only-child, figure > img) { width: fit-content; max-width: 100%; height: auto; vertical-align: bottom; }\n:where(p) > :where(img:not([class])) { max-height: calc(var(--gp-content-h) - 4px); object-fit: contain; }\n:where(figure) { break-inside: avoid; }\n\n";
43
+ export type LocationVariant = {
44
+ class: string;
45
+ owner: string;
46
+ label?: string;
47
+ snippet?: string;
48
+ };
@@ -241,7 +241,16 @@ export type GutterpressComponentProblem = string | {
241
241
  };
242
242
  /** `validate`'s return value: nothing, one message, or a list of problems. */
243
243
  export type GutterpressComponentProblems = GutterpressComponentProblem[] | string | void | null | undefined;
244
- /** A plugin's full declared marker table — see {@link GutterpressMarkerDeclaration}. */
244
+ /**
245
+ * A plugin's full declared marker table — see {@link GutterpressMarkerDeclaration}.
246
+ *
247
+ * The reserved kinds `page`, `chapter` and `section` may also appear, in a
248
+ * variants-only form: `{ variants: { 'chapter-start': 'dc-page-chapter-start' },
249
+ * label?, snippet? }`. `@page chapter-start` then adds that class to the
250
+ * `.page` (likewise `@chapter`, `@section`), so book CSS styles by location:
251
+ * `.page.dc-page-chapter-start .section { … }`. Any other key on those names
252
+ * is a load error.
253
+ */
245
254
  export type GutterpressMarkerTable = Record<string, GutterpressMarkerDeclaration>;
246
255
  /**
247
256
  * Full shape a plugin module may export. Only `default` is required.
@@ -93,8 +93,31 @@ export interface MarkerComponent {
93
93
  /** The winning example snippet's body (book, then extension, then core). */
94
94
  snippet?: string;
95
95
  }
96
+ /**
97
+ * One `page` / `chapter` / `section` variant the book can use (`@page blank`,
98
+ * `@section card`): the bare word, the class it adds, and who declares it.
99
+ * Core's own (`blank`, `full-bleed`) have `source: { kind: "core" }`. `label`
100
+ * and `snippet` are the declaring plugin's group values for that kind, passed
101
+ * through as declared (`snippet` is a path relative to the extension, unread).
102
+ * Data only — what the renderer does with a variant is `markers.js`'s.
103
+ */
104
+ export interface LocationVariant {
105
+ kind: "page" | "chapter" | "section";
106
+ /** The word after the marker: `@page <word>`. */
107
+ word: string;
108
+ /** The class it adds to the emitted element. */
109
+ class: string;
110
+ source: Extract<SnippetSource, {
111
+ kind: "extension" | "core";
112
+ }>;
113
+ label?: string;
114
+ snippet?: string;
115
+ }
96
116
  /** Every snippet the project can insert, from every level — see {@link collectLibrary}. */
97
117
  export declare function listMergedSnippets(projectDir: string): Promise<SnippetEntry[]>;
98
118
  /** Every component the project's enabled extensions declare, each with its
99
119
  * winning example snippet (book > extension > core) — see {@link collectLibrary}. */
100
120
  export declare function listMarkerComponents(projectDir: string): Promise<MarkerComponent[]>;
121
+ /** Every `page` / `chapter` / `section` variant the project can use — core's
122
+ * own plus each enabled extension's — see {@link LocationVariant}. */
123
+ export declare function listLocationVariants(projectDir: string): Promise<LocationVariant[]>;
@@ -7,15 +7,15 @@ import {
7
7
  TARGET_IDS,
8
8
  scaffoldExtension,
9
9
  scaffoldProject
10
- } from "./cli-an3pxzzk.js";
10
+ } from "./cli-v3xmpvs8.js";
11
11
  import {
12
12
  resolveGhostscript
13
- } from "./cli-0wtnw98y.js";
13
+ } from "./cli-vsgj1n9s.js";
14
14
  import {
15
15
  UsageError,
16
16
  rejectExtraPositionals,
17
17
  rejectUnknownFlags
18
- } from "./cli-jjbeqx94.js";
18
+ } from "./cli-2yvrfr58.js";
19
19
  import"./cli-ht1bcqdd.js";
20
20
  import"./cli-ycbg7mxk.js";
21
21
  import {
@@ -2,15 +2,15 @@ import {
2
2
  executeValidation,
3
3
  publishTargetFor,
4
4
  reportMissingTools
5
- } from "./cli-an3pxzzk.js";
5
+ } from "./cli-v3xmpvs8.js";
6
6
  import {
7
7
  log
8
- } from "./cli-0wtnw98y.js";
8
+ } from "./cli-vsgj1n9s.js";
9
9
  import {
10
10
  UsageError,
11
11
  rejectExtraPositionals,
12
12
  rejectUnknownFlags
13
- } from "./cli-jjbeqx94.js";
13
+ } from "./cli-2yvrfr58.js";
14
14
  import"./cli-ht1bcqdd.js";
15
15
  import"./cli-ycbg7mxk.js";
16
16
  import {
@@ -7,10 +7,10 @@ import {
7
7
  runBuild,
8
8
  splitOutPath,
9
9
  startPreviewServer
10
- } from "./cli-an3pxzzk.js";
10
+ } from "./cli-v3xmpvs8.js";
11
11
  import {
12
12
  log
13
- } from "./cli-0wtnw98y.js";
13
+ } from "./cli-vsgj1n9s.js";
14
14
  import {
15
15
  UsageError,
16
16
  parseFormat,
@@ -18,7 +18,7 @@ import {
18
18
  rejectExtraPositionals,
19
19
  rejectUnknownFlags,
20
20
  resolvePort
21
- } from "./cli-jjbeqx94.js";
21
+ } from "./cli-2yvrfr58.js";
22
22
  import"./cli-ht1bcqdd.js";
23
23
  import"./cli-ycbg7mxk.js";
24
24
  import {