gutterpress 0.11.16-alpha.5 → 0.11.16

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.
package/dist/index.js CHANGED
@@ -48,7 +48,7 @@ import {
48
48
  describeExtension,
49
49
  diagnoseProjectRemote,
50
50
  disconnectPublishCredential,
51
- ensureGitignoreHasDist,
51
+ ensureProjectGitignore,
52
52
  escapeYamlScalar,
53
53
  executeAndReport,
54
54
  executeValidation,
@@ -119,6 +119,8 @@ import {
119
119
  resolveGoogleClientSecret,
120
120
  resolvePublishFormat,
121
121
  resolvePublishRequest,
122
+ restoreFailureMessage,
123
+ restorePinnedExtensions,
122
124
  revokeGoogleCredential,
123
125
  ruleRemoteUrls,
124
126
  ruleRiskyProps,
@@ -146,7 +148,7 @@ import {
146
148
  testRemoteAccess,
147
149
  updateExtensions,
148
150
  validateProjectExtensions
149
- } from "./index-nk2mt7gp.js";
151
+ } from "./index-40achhs8.js";
150
152
  import {
151
153
  BUILD_DIAGNOSTIC_CODES
152
154
  } from "./index-qep93v0z.js";
@@ -212,6 +214,8 @@ export {
212
214
  ruleRemoteUrls,
213
215
  revokeGoogleCredential,
214
216
  restoreVersionWithBackup,
217
+ restorePinnedExtensions,
218
+ restoreFailureMessage,
215
219
  resolvePublishRequest,
216
220
  resolvePublishFormat,
217
221
  resolveLogger,
@@ -290,7 +294,7 @@ export {
290
294
  executeValidation,
291
295
  executeAndReport,
292
296
  escapeYamlScalar,
293
- ensureGitignoreHasDist,
297
+ ensureProjectGitignore,
294
298
  disconnectPublishCredential,
295
299
  diagnoseProjectRemote,
296
300
  detectProjectSource,
@@ -82,6 +82,8 @@ export interface BuiltInStyleSet {
82
82
  name: string;
83
83
  description: string;
84
84
  }
85
+ /** One key per book folder, however the path is spelled (Windows paths are case-insensitive). */
86
+ export declare function projectKey(projectDir: string): string;
85
87
  /**
86
88
  * Two specifiers name the same extension when they are equal, or when both
87
89
  * are npm specifiers for the same package (a re-install re-pins the version
@@ -146,6 +148,17 @@ export interface AddExtensionOptions extends NpmPluginInstallOptions {
146
148
  * Returns the described entry.
147
149
  */
148
150
  export declare function addExtension(projectDir: string, specifier: string, options?: AddExtensionOptions): Promise<ProjectExtensionEntry>;
151
+ /**
152
+ * Download one pinned npm extension again (`name@<exact version>`): the same
153
+ * verified transaction as {@link addExtension}, minus the manifest write — the
154
+ * pin is already there and is never changed. Returns the install's warnings.
155
+ */
156
+ export declare function restoreNpmExtension(projectDir: string, name: string, version: string, exportName: string | undefined, options?: NpmPluginInstallOptions & {
157
+ /** Every version of `name` an enabled manifest entry pins — never pruned. */
158
+ keepVersions?: readonly string[];
159
+ }): Promise<string[]>;
160
+ /** True when `plugins/npm/<name>/<version>/` is absent or has no loadable entry — what the loader calls "not installed" or "incomplete". */
161
+ export declare function vendoredCopyNeedsInstall(projectDir: string, name: string, version: string): Promise<boolean>;
149
162
  /** One pinned npm extension against npm's `latest` tag. */
150
163
  export interface ExtensionUpdateCheck {
151
164
  /** The specifier as written in the manifest (`name@current`). */
@@ -0,0 +1,74 @@
1
+ import type { NpmPluginInstallOptions } from "./npm-plugin-installer.ts";
2
+ export interface RestorePinnedOptions extends NpmPluginInstallOptions {
3
+ /** An explicit `--manifest` file; the book folder is then that file's folder. */
4
+ manifestPath?: string;
5
+ /**
6
+ * Progress for a UI: nothing is reported when every copy is present; else a
7
+ * `start` (the packages to download), one `package` event as each starts
8
+ * and ends, and an `end`. A caller that joins a run already in flight for
9
+ * the same book gets that run's result but no events.
10
+ */
11
+ onProgress?: (event: RestoreProgress) => void;
12
+ }
13
+ /** One step of a restore, for a progress indicator. `spec` is `name@version`. */
14
+ export type RestoreProgress = {
15
+ type: "start";
16
+ specs: string[];
17
+ } | {
18
+ type: "package";
19
+ spec: string;
20
+ index: number;
21
+ total: number;
22
+ state: "downloading" | "done" | "failed";
23
+ message?: string;
24
+ } | {
25
+ type: "end";
26
+ installed: string[];
27
+ failed: RestoreFailure[];
28
+ };
29
+ export interface RestoreFailure {
30
+ /** `name@version`, as pinned. */
31
+ use: string;
32
+ message: string;
33
+ }
34
+ export interface RestoreResult {
35
+ /** File name of the manifest the pins were read from (`manifest.yaml`), or null when the folder has none. */
36
+ manifestFile: string | null;
37
+ /** `name@version` of each package downloaded. */
38
+ installed: string[];
39
+ failed: RestoreFailure[];
40
+ warnings: string[];
41
+ /** npm could not be reached, so every package still missing was reported as failed without being tried. */
42
+ offline: boolean;
43
+ }
44
+ /**
45
+ * Download every pinned npm extension of the book whose copy is missing or
46
+ * incomplete. Never throws for a failed download — failures come back in
47
+ * `failed` so each caller can decide (the CLI fails fast, preview and the
48
+ * desktop degrade). An unreadable manifest restores nothing: whoever reads it
49
+ * next reports the real problem.
50
+ */
51
+ export declare function restorePinnedExtensions(projectDir: string, options?: RestorePinnedOptions): Promise<RestoreResult>;
52
+ /** What went wrong, which extension, and how to retry — one failure as a sentence. */
53
+ export declare function restoreFailureMessage(failure: RestoreFailure, manifestFile: string | null): string;
54
+ /**
55
+ * One sentence for a restore that could not reach npm: which extensions are
56
+ * missing, and that they have to be installed while online — after which the
57
+ * book works offline.
58
+ */
59
+ export declare function restoreOfflineMessage(failed: RestoreFailure[], manifestFile: string | null): string;
60
+ /**
61
+ * The restore step every command and host path that loads a book's plugins
62
+ * runs first: restore, print one line per downloaded package, and either fail
63
+ * fast (build, export, validate and the checks: a final artifact must never
64
+ * silently omit author-configured formatting) or, for the live preview, warn
65
+ * and carry on so one missing package cannot blank the whole preview.
66
+ *
67
+ * It is called from the shared "load the book's plugins" seams themselves
68
+ * (`loadBuildPlugins`, `executeValidation`), not from each command, so a new
69
+ * command that builds or checks cannot forget it. The loader stays offline.
70
+ */
71
+ export declare function restoreForCommand(dir: string, options: {
72
+ manifestPath?: string;
73
+ failFast: boolean;
74
+ }): Promise<void>;
@@ -133,7 +133,7 @@
133
133
  * carrying gp-left could start shifting; that is standards-tracking per
134
134
  * CLAUDE.md ("Chrome wins once it ships"), not a bug in the author's book.
135
135
  */
136
- export declare const GUTTERPRESS_CSS = "\n/* gp-* author image/block vocabulary. One vocabulary, gp-* only \u2014 the\n pre-vocabulary utility names (.center/.float-left/.float-right/\n .full-width/.full-bleed) were REMOVED when gp-* shipped; books rename\n the classes in their markdown (see the migration note). Source ORDER is\n the contract \u2014 see the doctrine comment above. */\n\n/* flow positions */\n.gp-left {\n float: left;\n margin: 0 var(--gp-gap, 1em) var(--gp-gap, 1em) 0;\n max-width: 50%;\n}\n.gp-right {\n float: right;\n margin: 0 0 var(--gp-gap, 1em) var(--gp-gap, 1em);\n max-width: 50%;\n}\n.gp-center {\n display: block;\n float: none;\n margin-left: auto;\n margin-right: auto;\n max-width: 100%;\n}\n.gp-full {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n@page gp-full-bleed { margin-left: 0; margin-right: 0; }\n.gp-bleed {\n display: block;\n float: none;\n break-before: page;\n page: gp-full-bleed;\n max-width: none;\n width: 100%;\n margin-left: 0;\n margin-right: 0;\n}\n\n/* sizes \u2014 AFTER the flow positions so max-width:100% lifts the floats' 50%\n cap at equal specificity */\n.gp-small { width: 25%; max-width: 100%; }\n.gp-medium { width: 50%; max-width: 100%; }\n.gp-large { width: 75%; max-width: 100%; }\n\n/* float clearance presets \u2014 consumed by var(--gp-gap) in the float rules\n above and by .gp-shape's shape-margin below; --gp-gap itself is\n author-settable CSS */\n.gp-tight { --gp-gap: 0.5em; }\n.gp-loose { --gp-gap: 2em; }\n\n/* column runs \u2014 plain CSS Multi-column, exposed as author vocabulary so\n \"put this in two columns\" does not require borrowing a styled container\n from the book's own component layer. That borrowing is what this exists\n to prevent: a book whose theme paints .section chrome by default gives\n every author who opens a section just to start a column run a panel they\n did not ask for, and the book then needs a reset rule to take it back.\n With a neutral primitive the author opts into columns and nothing else.\n\n Permanent vocabulary, not a shim: Chromium implements multicol natively\n and these rules are the standard properties verbatim, so there is no\n spec gap here to remove later. Deliberately minimal \u2014 column-fill is\n NOT set, because the correct value depends on whether the run fragments\n across pages (auto packs each page's columns; the CSS initial balance is\n right for a run that fits on one page) and only the author knows which.\n --gp-column-gap is author-settable. */\n.gp-columns-2 { columns: 2; column-gap: var(--gp-column-gap, 1.5em); }\n.gp-columns-3 { columns: 3; column-gap: var(--gp-column-gap, 1.5em); }\n\n/* the per-shape decisions the paragraph above deliberately leaves to the\n author, named instead of left as raw CSS every book was reinventing\n (CLAUDE.md \u00A70: \"behavior broadly useful to non-technical authors belongs\n in core\").\n Permanent vocabulary, standard properties verbatim \u2014 same rationale as\n the column/grid runs above. One name each, no aliases:\n .gp-columns-all column-span: all a heading or block that\n spans every column in the\n run it sits inside.\n .gp-columns-flow column-fill: auto a run that FRAGMENTS across\n pages \u2014 every page's\n columns fill instead of\n only the last one\n balancing (the dead-column\n collapse the build's\n engine.multicol.dead-column\n warning names this fix\n for).\n .gp-columns-balanced column-fill: balance a run that fits on ONE\n page (the CSS initial\n value \u2014 ragged columns\n would be wrong here). */\n.gp-columns-all { column-span: all; }\n.gp-columns-flow { column-fill: auto; }\n.gp-columns-balanced { column-fill: balance; }\n\n/* grid runs \u2014 the SLOTTED counterpart to the column runs above. Grid places\n each child into the next cell, across then down (deterministic slots: card\n layouts, stat blocks, image-plus-caption pairs); columns FLOW one text run\n down then across. Same neutral-primitive rationale as .gp-columns-*, and\n permanent vocabulary for the same reason: standard CSS Grid verbatim, no\n spec gap to remove later. MEASURED (Chromium 151, gp-grid evidence pack):\n grid rows fragment across sheets with EXACT print/viewer parity \u2014 2- and\n 3-col, unequal item heights, mid-row cuts, multi-sheet overflow,\n break-inside:avoid, gap geometry \u2014 so a grid taller than the page is safe,\n no fit-one-page constraint. Two things to know, not fix:\n - on a min-height page root (MARKER_CSS), default align-content\n stretches rows apart to fill the page \u2014 identically in both engines.\n Authors wanting packed rows set align-content: start.\n - a @page-break / @column-break marker DIRECTLY inside a grid container\n becomes a grid item and corrupts placement (the one measured parity\n break); markers.js diagnoses it (break_inside_grid).\n --gp-grid-gap is author-settable. */\n.gp-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gp-grid-gap, 1.5em); }\n.gp-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gp-grid-gap, 1.5em); }\n\n/* fragmentation controls \u2014 generic pagination utilities for any element,\n independent of the column/grid vocabulary above (a book reached for\n these just as often outside a multicol run: keeping a card whole,\n forcing a section to start a fresh page). Standard properties only \u2014\n this project is Chromium-only (CLAUDE.md), so there are no legacy\n page-break-* twins to also emit. One name each. */\n.gp-no-break { break-inside: avoid; }\n.gp-break-before { break-before: page; }\n\n/* shape wrap \u2014 text follows the image's alpha silhouette instead of its\n rectangular box. shape-outside only applies to floats, so this is inert\n without .gp-left/.gp-right (and under .gp-pin, which un-floats). The\n shape URL cannot be written in CSS (url() contexts can't read attr()),\n so the image renderer rule (images.ts) mirrors the src into an inline\n --gp-shape:url(...) custom property whenever it sees this class --\n authors only ever type the class. threshold 0.2 ignores near-transparent\n anti-aliasing halos; shape-margin shares the float-gap vocabulary. */\nimg.gp-shape {\n shape-outside: var(--gp-shape);\n shape-image-threshold: 0.2;\n shape-margin: var(--gp-gap, 1em);\n}\n\n/* pin \u2014 within the nearest positioned ancestor (.page/.spread, rule above).\n inset:0 and the explicit centers are load-bearing; see doctrine comment. */\n.gp-pin {\n position: absolute;\n inset: 0;\n align-self: center;\n justify-self: center;\n margin: 0;\n max-width: 100%;\n}\n\n/* pin edge modifiers \u2014 AFTER .gp-pin to beat its center defaults;\n justify-self is inert on in-flow floats, so gp-left/gp-right safely do\n double duty as flow float + pin edge */\n.gp-top { align-self: start; }\n.gp-bottom { align-self: end; }\n.gp-left { justify-self: start; }\n.gp-right { justify-self: end; }\n\n/* wrapper-margin neutralization (same pattern and rationale as the\n .gp-bleed paragraph-margin note in the doctrine comment; for pin, the\n emptied paragraph would otherwise leave a phantom margin gap in flow) */\n:where(p:has(> img.gp-bleed:only-child)) { margin: 0; }\n:where(p:has(> img.gp-pin:only-child)) { margin: 0; }\n\n/* depth \u2014 a named ladder for z-index, so books stop hand-tuning bare\n integers. A real book measured 21 z-index declarations using only four\n distinct values (-1, 0, 1, 2), each written literally at its use site.\n The custom properties are the author-settable surface (a book needing a\n deeper stack raises them once); the classes are the shorthand.\n\n NOT named \"layer\": CSS Paged Media 3 \u00A73.1 already defines \"page layers\"\n (page background, canvas, borders, contents, margin boxes) and those are\n parts of the PAGE BOX, not a z-ladder for content. Reusing the word for a\n different concept would collide with the spec vocabulary this project\n tracks. The pin EDGE modifiers already own .gp-top/.gp-bottom, so the\n ladder avoids those words too.\n\n .gp-behind is the one that earns its place: it puts a pinned image UNDER\n the page's text, which is otherwise impossible to express without a bare\n negative z-index. \"Above\" needs no class \u2014 an out-of-flow pin already\n paints above in-flow content.\n\n Two things silently defeat .gp-behind, neither visible at the use site:\n - a stacking context on the .page/.spread ancestor (z-index, isolation,\n opacity, filter, transform on it traps the negative layer inside).\n Core keeps .page/.spread at 'position: relative; z-index: auto'\n precisely so they are not stacking contexts.\n - a clipping ancestor (overflow other than visible) \u2014 but only where\n the art actually overhangs that ancestor's clip box on a clipped\n axis: the overhang is cut off, the same mechanism that clips a\n .gp-bleed plate back to the wrapper's width. Clipping never reorders\n layers \u2014 within-bounds art under a clipping .page prints whole and\n still behind (measured; see the build audit's comment in\n engine/compiler/build.ts), and a static wrapper's overflow never\n binds an abspos .gp-pin at all.\n The build-time engine.layer.trapped audit reports both against the live\n ancestor chain. printsafe/page-containment is only an early source hint for\n declarations written directly on .page/.spread. */\n:root {\n --gp-z-behind: -1;\n --gp-z-base: 0;\n --gp-z-raised: 1;\n --gp-z-front: 2;\n}\n.gp-behind { z-index: var(--gp-z-behind); }\n.gp-base { z-index: var(--gp-z-base); }\n.gp-raised { z-index: var(--gp-z-raised); }\n.gp-front { z-index: var(--gp-z-front); }\n\n/* GFM-style alert/callout boxes (#237) \u2014 the DOM gfm-alerts.ts emits from\n \"> [!TYPE]\" blockquote syntax when a project opts into the bundled\n gutterpress-gfm-alerts feature (BUILTIN_OPTIONAL_PLUGINS, renderer.ts).\n The plugin is optional and inert by default; these rules are equally\n inert wherever the classes never appear, exactly like every other\n utility above, so shipping them costs nothing to a book that has not\n turned the feature on.\n\n This is authored-COMPONENT vocabulary, not the @marker structural family\n (markers.js/MARKER_CSS) \u2014 CLAUDE.md \u00A76 splits the two core CSS blocks by\n ROLE, and an author-facing box an author invokes via inline syntax is the\n same role as every other gp-* utility here, not page/chapter/section DOM.\n\n Deliberately minimal (CLAUDE.md \"thin over capable\" / the issue's own\n \"a rule and a label\"): a left border plus a bold label, :where()'d to\n zero specificity so a theme's own (unlayered) rule for any of these\n classes wins outright regardless of selector weight \u2014 see the\n cascade-layers note atop this file. Five semantic accent colors (the same\n NOTE/TIP/IMPORTANT/WARNING/CAUTION hues GitHub's own alerts use \u2014 a\n long-established, non-proprietary convention, not DC branding) are\n exposed as :root custom properties, the same author-settable-surface\n pattern as the --gp-z-* ladder above: a book retints every alert of one\n type by overriding a single property, no specificity fight required.\n --gp-alert-color cascades from the type modifier down to .gp-alert-title\n through ordinary CSS inheritance (the title is always a DOM descendant of\n the box), so it is set in exactly five places, not ten. */\n:root {\n --gp-alert-note-color: #0969da;\n --gp-alert-tip-color: #1a7f37;\n --gp-alert-important-color: #8250df;\n --gp-alert-warning-color: #9a6700;\n --gp-alert-caution-color: #cf222e;\n}\n:where(.gp-alert) {\n margin: 1em 0;\n padding: 0.5em 1em;\n border-left: 0.25em solid var(--gp-alert-color, currentColor);\n}\n:where(.gp-alert-title) {\n margin: 0 0 0.5em;\n font-weight: bold;\n color: var(--gp-alert-color, inherit);\n}\n:where(.gp-alert-note) { --gp-alert-color: var(--gp-alert-note-color); }\n:where(.gp-alert-tip) { --gp-alert-color: var(--gp-alert-tip-color); }\n:where(.gp-alert-important) { --gp-alert-color: var(--gp-alert-important-color); }\n:where(.gp-alert-warning) { --gp-alert-color: var(--gp-alert-warning-color); }\n:where(.gp-alert-caution) { --gp-alert-color: var(--gp-alert-caution-color); }\n";
136
+ export declare const GUTTERPRESS_CSS = "\n/* gp-* author image/block vocabulary. One vocabulary, gp-* only \u2014 the\n pre-vocabulary utility names (.center/.float-left/.float-right/\n .full-width/.full-bleed) were REMOVED when gp-* shipped; books rename\n the classes in their markdown (see the migration note). Source ORDER is\n the contract \u2014 see the doctrine comment above. */\n\n/* flow positions */\n.gp-left {\n float: left;\n margin: 0 var(--gp-gap, 1em) var(--gp-gap, 1em) 0;\n max-width: 50%;\n}\n.gp-right {\n float: right;\n margin: 0 0 var(--gp-gap, 1em) var(--gp-gap, 1em);\n max-width: 50%;\n}\n.gp-center {\n display: block;\n float: none;\n margin-left: auto;\n margin-right: auto;\n max-width: 100%;\n}\n.gp-full {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n@page gp-full-bleed { margin-left: 0; margin-right: 0; }\n.gp-bleed {\n display: block;\n float: none;\n break-before: page;\n page: gp-full-bleed;\n max-width: none;\n width: 100%;\n margin-left: 0;\n margin-right: 0;\n}\n\n/* sizes \u2014 AFTER the flow positions so max-width:100% lifts the floats' 50%\n cap at equal specificity */\n.gp-small { width: 25%; max-width: 100%; }\n.gp-medium { width: 50%; max-width: 100%; }\n.gp-large { width: 75%; max-width: 100%; }\n\n/* float clearance presets \u2014 consumed by var(--gp-gap) in the float rules\n above and by .gp-shape's shape-margin below; --gp-gap itself is\n author-settable CSS */\n.gp-tight { --gp-gap: 0.5em; }\n.gp-loose { --gp-gap: 2em; }\n\n/* column runs \u2014 plain CSS Multi-column, exposed as author vocabulary so\n \"put this in two columns\" does not require borrowing a styled container\n from the book's own component layer. That borrowing is what this exists\n to prevent: a book whose theme paints .section chrome by default gives\n every author who opens a section just to start a column run a panel they\n did not ask for, and the book then needs a reset rule to take it back.\n With a neutral primitive the author opts into columns and nothing else.\n\n Permanent vocabulary, not a shim: Chromium implements multicol natively\n and these rules are the standard properties verbatim, so there is no\n spec gap here to remove later. Deliberately minimal \u2014 column-fill is\n NOT set, because the correct value depends on whether the run fragments\n across pages (auto packs each page's columns; the CSS initial balance is\n right for a run that fits on one page) and only the author knows which.\n --gp-column-gap is author-settable.\n .gp-columns is what the core @columns marker emits (markers.js); on its\n own it is two columns, and @columns 1 to 5 add the matching count, which\n wins because it comes later at the same specificity. */\n.gp-columns, .gp-columns-1, .gp-columns-2, .gp-columns-3, .gp-columns-4, .gp-columns-5 {\n column-gap: var(--gp-column-gap, 1.5em);\n}\n.gp-columns { columns: 2; }\n.gp-columns-1 { columns: 1; }\n.gp-columns-2 { columns: 2; }\n.gp-columns-3 { columns: 3; }\n.gp-columns-4 { columns: 4; }\n.gp-columns-5 { columns: 5; }\n\n/* the per-shape decisions the paragraph above deliberately leaves to the\n author, named instead of left as raw CSS every book was reinventing\n (CLAUDE.md \u00A70: \"behavior broadly useful to non-technical authors belongs\n in core\").\n Permanent vocabulary, standard properties verbatim \u2014 same rationale as\n the column/grid runs above. One name each, no aliases:\n .gp-columns-all column-span: all a heading or block that\n spans every column in the\n run it sits inside.\n .gp-columns-flow column-fill: auto a run that FRAGMENTS across\n pages \u2014 every page's\n columns fill instead of\n only the last one\n balancing (the dead-column\n collapse the build's\n engine.multicol.dead-column\n warning names this fix\n for).\n .gp-columns-balanced column-fill: balance a run that fits on ONE\n page (the CSS initial\n value \u2014 ragged columns\n would be wrong here). */\n.gp-columns-all { column-span: all; }\n.gp-columns-flow { column-fill: auto; }\n.gp-columns-balanced { column-fill: balance; }\n\n/* grid runs \u2014 the SLOTTED counterpart to the column runs above. Grid places\n each child into the next cell, across then down (deterministic slots: card\n layouts, stat blocks, image-plus-caption pairs); columns FLOW one text run\n down then across. Same neutral-primitive rationale as .gp-columns-*, and\n permanent vocabulary for the same reason: standard CSS Grid verbatim, no\n spec gap to remove later. MEASURED (Chromium 151, gp-grid evidence pack):\n grid rows fragment across sheets with EXACT print/viewer parity \u2014 2- and\n 3-col, unequal item heights, mid-row cuts, multi-sheet overflow,\n break-inside:avoid, gap geometry \u2014 so a grid taller than the page is safe,\n no fit-one-page constraint. Two things to know, not fix:\n - on a min-height page root (MARKER_CSS), default align-content\n stretches rows apart to fill the page \u2014 identically in both engines.\n Authors wanting packed rows set align-content: start.\n - a @page-break / @column-break marker DIRECTLY inside a grid container\n becomes a grid item and corrupts placement (the one measured parity\n break); markers.js diagnoses it (break_inside_grid).\n --gp-grid-gap is author-settable. */\n.gp-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gp-grid-gap, 1.5em); }\n.gp-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gp-grid-gap, 1.5em); }\n\n/* fragmentation controls \u2014 generic pagination utilities for any element,\n independent of the column/grid vocabulary above (a book reached for\n these just as often outside a multicol run: keeping a card whole,\n forcing a section to start a fresh page). Standard properties only \u2014\n this project is Chromium-only (CLAUDE.md), so there are no legacy\n page-break-* twins to also emit. One name each. */\n.gp-no-break { break-inside: avoid; }\n.gp-break-before { break-before: page; }\n\n/* shape wrap \u2014 text follows the image's alpha silhouette instead of its\n rectangular box. shape-outside only applies to floats, so this is inert\n without .gp-left/.gp-right (and under .gp-pin, which un-floats). The\n shape URL cannot be written in CSS (url() contexts can't read attr()),\n so the image renderer rule (images.ts) mirrors the src into an inline\n --gp-shape:url(...) custom property whenever it sees this class --\n authors only ever type the class. threshold 0.2 ignores near-transparent\n anti-aliasing halos; shape-margin shares the float-gap vocabulary. */\nimg.gp-shape {\n shape-outside: var(--gp-shape);\n shape-image-threshold: 0.2;\n shape-margin: var(--gp-gap, 1em);\n}\n\n/* pin \u2014 within the nearest positioned ancestor (.page/.spread, rule above).\n inset:0 and the explicit centers are load-bearing; see doctrine comment. */\n.gp-pin {\n position: absolute;\n inset: 0;\n align-self: center;\n justify-self: center;\n margin: 0;\n max-width: 100%;\n}\n\n/* pin edge modifiers \u2014 AFTER .gp-pin to beat its center defaults;\n justify-self is inert on in-flow floats, so gp-left/gp-right safely do\n double duty as flow float + pin edge */\n.gp-top { align-self: start; }\n.gp-bottom { align-self: end; }\n.gp-left { justify-self: start; }\n.gp-right { justify-self: end; }\n\n/* wrapper-margin neutralization (same pattern and rationale as the\n .gp-bleed paragraph-margin note in the doctrine comment; for pin, the\n emptied paragraph would otherwise leave a phantom margin gap in flow) */\n:where(p:has(> img.gp-bleed:only-child)) { margin: 0; }\n:where(p:has(> img.gp-pin:only-child)) { margin: 0; }\n\n/* depth \u2014 a named ladder for z-index, so books stop hand-tuning bare\n integers. A real book measured 21 z-index declarations using only four\n distinct values (-1, 0, 1, 2), each written literally at its use site.\n The custom properties are the author-settable surface (a book needing a\n deeper stack raises them once); the classes are the shorthand.\n\n NOT named \"layer\": CSS Paged Media 3 \u00A73.1 already defines \"page layers\"\n (page background, canvas, borders, contents, margin boxes) and those are\n parts of the PAGE BOX, not a z-ladder for content. Reusing the word for a\n different concept would collide with the spec vocabulary this project\n tracks. The pin EDGE modifiers already own .gp-top/.gp-bottom, so the\n ladder avoids those words too.\n\n .gp-behind is the one that earns its place: it puts a pinned image UNDER\n the page's text, which is otherwise impossible to express without a bare\n negative z-index. \"Above\" needs no class \u2014 an out-of-flow pin already\n paints above in-flow content.\n\n Two things silently defeat .gp-behind, neither visible at the use site:\n - a stacking context on the .page/.spread ancestor (z-index, isolation,\n opacity, filter, transform on it traps the negative layer inside).\n Core keeps .page/.spread at 'position: relative; z-index: auto'\n precisely so they are not stacking contexts.\n - a clipping ancestor (overflow other than visible) \u2014 but only where\n the art actually overhangs that ancestor's clip box on a clipped\n axis: the overhang is cut off, the same mechanism that clips a\n .gp-bleed plate back to the wrapper's width. Clipping never reorders\n layers \u2014 within-bounds art under a clipping .page prints whole and\n still behind (measured; see the build audit's comment in\n engine/compiler/build.ts), and a static wrapper's overflow never\n binds an abspos .gp-pin at all.\n The build-time engine.layer.trapped audit reports both against the live\n ancestor chain. printsafe/page-containment is only an early source hint for\n declarations written directly on .page/.spread. */\n:root {\n --gp-z-behind: -1;\n --gp-z-base: 0;\n --gp-z-raised: 1;\n --gp-z-front: 2;\n}\n.gp-behind { z-index: var(--gp-z-behind); }\n.gp-base { z-index: var(--gp-z-base); }\n.gp-raised { z-index: var(--gp-z-raised); }\n.gp-front { z-index: var(--gp-z-front); }\n\n/* GFM-style alert/callout boxes (#237) \u2014 the DOM gfm-alerts.ts emits from\n \"> [!TYPE]\" blockquote syntax when a project opts into the bundled\n gutterpress-gfm-alerts feature (BUILTIN_OPTIONAL_PLUGINS, renderer.ts).\n The plugin is optional and inert by default; these rules are equally\n inert wherever the classes never appear, exactly like every other\n utility above, so shipping them costs nothing to a book that has not\n turned the feature on.\n\n This is authored-COMPONENT vocabulary, not the @marker structural family\n (markers.js/MARKER_CSS) \u2014 CLAUDE.md \u00A76 splits the two core CSS blocks by\n ROLE, and an author-facing box an author invokes via inline syntax is the\n same role as every other gp-* utility here, not page/chapter/section DOM.\n\n Deliberately minimal (CLAUDE.md \"thin over capable\" / the issue's own\n \"a rule and a label\"): a left border plus a bold label, :where()'d to\n zero specificity so a theme's own (unlayered) rule for any of these\n classes wins outright regardless of selector weight \u2014 see the\n cascade-layers note atop this file. Five semantic accent colors (the same\n NOTE/TIP/IMPORTANT/WARNING/CAUTION hues GitHub's own alerts use \u2014 a\n long-established, non-proprietary convention, not DC branding) are\n exposed as :root custom properties, the same author-settable-surface\n pattern as the --gp-z-* ladder above: a book retints every alert of one\n type by overriding a single property, no specificity fight required.\n --gp-alert-color cascades from the type modifier down to .gp-alert-title\n through ordinary CSS inheritance (the title is always a DOM descendant of\n the box), so it is set in exactly five places, not ten. */\n:root {\n --gp-alert-note-color: #0969da;\n --gp-alert-tip-color: #1a7f37;\n --gp-alert-important-color: #8250df;\n --gp-alert-warning-color: #9a6700;\n --gp-alert-caution-color: #cf222e;\n}\n:where(.gp-alert) {\n margin: 1em 0;\n padding: 0.5em 1em;\n border-left: 0.25em solid var(--gp-alert-color, currentColor);\n}\n:where(.gp-alert-title) {\n margin: 0 0 0.5em;\n font-weight: bold;\n color: var(--gp-alert-color, inherit);\n}\n:where(.gp-alert-note) { --gp-alert-color: var(--gp-alert-note-color); }\n:where(.gp-alert-tip) { --gp-alert-color: var(--gp-alert-tip-color); }\n:where(.gp-alert-important) { --gp-alert-color: var(--gp-alert-important-color); }\n:where(.gp-alert-warning) { --gp-alert-color: var(--gp-alert-warning-color); }\n:where(.gp-alert-caution) { --gp-alert-color: var(--gp-alert-caution-color); }\n";
137
137
  /**
138
138
  * GP_CLASSES — every class an author may legitimately write with a `gp-`
139
139
  * prefix: the `.gp-*` selectors in `GUTTERPRESS_CSS` above, the two
@@ -22,8 +22,16 @@ export interface InstalledNpmPlugin {
22
22
  installRoot: string;
23
23
  /** Previous same-version directory held until the manifest commits. */
24
24
  backupRoot: string | null;
25
+ /** Folders THIS install created (`plugins/`, `plugins/npm/`, `plugins/npm/<name>/`), outermost first. */
26
+ createdDirs: string[];
25
27
  warnings: string[];
26
28
  }
29
+ /**
30
+ * Remove folders a failed install created — innermost first, and only while
31
+ * they are still empty, so a folder that gained anything else (a local plugin
32
+ * in `plugins/`, another package's copy) is never touched.
33
+ */
34
+ export declare function removeEmptyDirs(dirs: readonly string[]): Promise<void>;
27
35
  /**
28
36
  * Download and publish a fresh vendor tree for one npm plugin. Never reuses
29
37
  * bytes: a same-version reinstall downloads again and replaces the tree (the
@@ -0,0 +1,10 @@
1
+ /** Entries every book ignores, in the order they are appended. */
2
+ export declare const PROJECT_GITIGNORE_ENTRIES: readonly ["dist/", "plugins/npm/"];
3
+ /**
4
+ * Make sure `<projectDir>/.gitignore` ignores `dist/` and `plugins/npm/`,
5
+ * creating the file or appending what is missing. Returns the entries the
6
+ * author's own rules re-include (left alone) so a caller can warn.
7
+ */
8
+ export declare function ensureProjectGitignore(projectDir: string): Promise<{
9
+ negated: string[];
10
+ }>;
@@ -143,31 +143,11 @@ export interface AdoptFolderOptions {
143
143
  * Adopt an EXISTING folder as a gutterpress project, in place (no new subfolder).
144
144
  * Writes a `manifest.yaml` (using the folder's existing top-level `.md` files as
145
145
  * `source.files`, or scaffolding a `chapter-01.md` when there are none), copies
146
- * a starter `styles/book.css`, ensures `dist/` is gitignored (see
147
- * `ensureGitignoreHasDist`), and optionally initialises local version history.
146
+ * a starter `styles/book.css`, ensures `dist/` and `plugins/npm/` are gitignored
147
+ * (see `ensureProjectGitignore`), and optionally initialises local version history.
148
148
  *
149
149
  * NON-DESTRUCTIVE (global never-overwrite rule): refuses if the folder is
150
150
  * already a project, and never overwrites an existing `manifest.yaml`,
151
151
  * `styles/book.css`, `.gitignore`, or any markdown file.
152
152
  */
153
153
  export declare function adoptFolder(options: AdoptFolderOptions): Promise<CreateProjectResult>;
154
- /**
155
- * Ensure `<projectDir>/.gitignore` excludes the build output directory
156
- * (`dist/` — lib/output-paths.ts's `DIST_DIRNAME`).
157
- *
158
- * Confirmed bug this closes: no scaffolded project ever got a `.gitignore`,
159
- * and gutterpress's auto-snapshot feature is ON BY DEFAULT and auto-pushes.
160
- * Every `gutterpress build` writes a fresh, incompressible PDF (plus any copied
161
- * assets) into `dist/<title-slug>/`; without an ignore rule, the very next
162
- * snapshot commits and pushes it. That grows `.git` by a full PDF on every
163
- * build and, once a single artifact crosses GitHub's 100MB per-file limit,
164
- * the push is rejected outright — for a non-technical author with no way to
165
- * `git filter-repo` their way out.
166
- *
167
- * NEVER overwrites an existing `.gitignore` (global never-delete-user-data
168
- * rule): if the file already excludes `dist/` in some common spelling
169
- * (`dist`, `dist/`, `/dist/`, …) it is left untouched; otherwise the line is
170
- * APPENDED, not prepended or reformatted, so any content the author already
171
- * has stays exactly as they wrote it.
172
- */
173
- export declare function ensureGitignoreHasDist(projectDir: string): Promise<void>;
@@ -20,8 +20,9 @@ export interface ValidationExecutionArgs {
20
20
  * `runQualityGates`, via `loadBuildPlugins`) so this run does not load
21
21
  * plugins itself when the build already has. `undefined` (the default —
22
22
  * every standalone `validate`/`preflight` invocation, and the desktop
23
- * Problems panel) makes {@link executeValidation} load plugins itself,
24
- * degrade-and-report. An explicit `[]` is honored as-is, not treated as
23
+ * Problems panel) makes {@link executeValidation} restore the book's
24
+ * missing pinned extensions (failing the run if it cannot) and then load
25
+ * plugins itself, degrade-and-report. An explicit `[]` is honored as-is, not treated as
25
26
  * "unset".
26
27
  */
27
28
  pluginStylePaths?: string[];
@@ -7,15 +7,15 @@ import {
7
7
  TARGET_IDS,
8
8
  scaffoldExtension,
9
9
  scaffoldProject
10
- } from "./cli-4d4xegsw.js";
10
+ } from "./cli-ev175hx9.js";
11
11
  import {
12
12
  resolveGhostscript
13
- } from "./cli-6s4pkvbx.js";
13
+ } from "./cli-0prrt9e4.js";
14
14
  import {
15
15
  UsageError,
16
16
  rejectExtraPositionals,
17
17
  rejectUnknownFlags
18
- } from "./cli-cg03zvez.js";
18
+ } from "./cli-182j48xc.js";
19
19
  import"./cli-wqy8hnnh.js";
20
20
  import {
21
21
  EXIT_CODES
@@ -2,17 +2,18 @@ import {
2
2
  executeValidation,
3
3
  publishTargetFor,
4
4
  reportMissingTools
5
- } from "./cli-4d4xegsw.js";
5
+ } from "./cli-ev175hx9.js";
6
6
  import {
7
7
  log
8
- } from "./cli-6s4pkvbx.js";
8
+ } from "./cli-0prrt9e4.js";
9
9
  import {
10
10
  UsageError,
11
11
  rejectExtraPositionals,
12
12
  rejectUnknownFlags
13
- } from "./cli-cg03zvez.js";
13
+ } from "./cli-182j48xc.js";
14
14
  import"./cli-wqy8hnnh.js";
15
15
  import {
16
+ BuildError,
16
17
  EXIT_CODES
17
18
  } from "./cli-46ycxe6r.js";
18
19
  import"./cli-kkzka31p.js";
@@ -190,7 +191,7 @@ var preflight_default = defineCommand({
190
191
  });
191
192
  } catch (error) {
192
193
  log.error(error instanceof Error ? error.message : String(error));
193
- process.exit(error instanceof UsageError ? error.exitCode : EXIT_CODES.USAGE);
194
+ process.exit(error instanceof UsageError || error instanceof BuildError ? error.exitCode : EXIT_CODES.USAGE);
194
195
  }
195
196
  if (!execution)
196
197
  return;
@@ -3,13 +3,14 @@ import {
3
3
  } from "./cli-75qkn0k5.js";
4
4
  import {
5
5
  openPath,
6
+ restoreForCommand,
6
7
  runBuild,
7
8
  splitOutPath,
8
9
  startPreviewServer
9
- } from "./cli-4d4xegsw.js";
10
+ } from "./cli-ev175hx9.js";
10
11
  import {
11
12
  log
12
- } from "./cli-6s4pkvbx.js";
13
+ } from "./cli-0prrt9e4.js";
13
14
  import {
14
15
  UsageError,
15
16
  parseFormat,
@@ -17,7 +18,7 @@ import {
17
18
  rejectExtraPositionals,
18
19
  rejectUnknownFlags,
19
20
  resolvePort
20
- } from "./cli-cg03zvez.js";
21
+ } from "./cli-182j48xc.js";
21
22
  import"./cli-wqy8hnnh.js";
22
23
  import {
23
24
  BuildError
@@ -52,6 +53,7 @@ var preview_default = defineCommand({
52
53
  if (typeof args.manifest === "string") {
53
54
  throw new UsageError("--manifest is only supported by preview --format pdf or pdfx; live HTML preview discovers the book manifest from its input directory.");
54
55
  }
56
+ await restoreForCommand(inputPath ?? process.cwd(), { failFast: false });
55
57
  await startPreviewServer({
56
58
  input: inputPath,
57
59
  port: resolvePort(args.port),
@@ -68,6 +70,7 @@ var preview_default = defineCommand({
68
70
  }
69
71
  const pdfxFlavor = parsePdfxFlavor(args["pdfx-flavor"], format);
70
72
  const { outDir, pdfFileOverride } = splitOutPath(typeof args.out === "string" ? args.out : undefined, format);
73
+ const manifestPath = typeof args.manifest === "string" ? args.manifest : undefined;
71
74
  const result = await runBuild({
72
75
  inputDir: inputPath,
73
76
  format,
@@ -75,7 +78,7 @@ var preview_default = defineCommand({
75
78
  pdfFileOverride,
76
79
  pdfxFlavor,
77
80
  iccPath: typeof args.icc === "string" ? args.icc : undefined,
78
- manifestPath: typeof args.manifest === "string" ? args.manifest : undefined,
81
+ manifestPath,
79
82
  stripAnnotations: typeof args["strip-annotations"] === "boolean" ? args["strip-annotations"] : undefined,
80
83
  skipLint: !!args["skip-lint"],
81
84
  skipPreValidate: !!args["skip-pre-validate"],
@@ -11,16 +11,16 @@ import {
11
11
  publishProviderFor,
12
12
  resolvePublishFormat,
13
13
  runPublish
14
- } from "./cli-4d4xegsw.js";
14
+ } from "./cli-ev175hx9.js";
15
15
  import {
16
16
  FileTokenStore,
17
17
  log
18
- } from "./cli-6s4pkvbx.js";
18
+ } from "./cli-0prrt9e4.js";
19
19
  import {
20
20
  UsageError,
21
21
  rejectExtraPositionals,
22
22
  rejectUnknownFlags
23
- } from "./cli-cg03zvez.js";
23
+ } from "./cli-182j48xc.js";
24
24
  import"./cli-wqy8hnnh.js";
25
25
  import {
26
26
  EXIT_CODES
@@ -55,7 +55,8 @@ function stdinLooksPiped() {
55
55
  }
56
56
  function emitResult(result, json) {
57
57
  if (json) {
58
- console.log(JSON.stringify(result, null, 2));
58
+ process.stdout.write(JSON.stringify(result, null, 2) + `
59
+ `);
59
60
  return;
60
61
  }
61
62
  for (const issue of result.issues) {
@@ -173,7 +174,8 @@ var publish_default = defineCommand({
173
174
  };
174
175
  }));
175
176
  if (json) {
176
- console.log(JSON.stringify(rows, null, 2));
177
+ process.stdout.write(JSON.stringify(rows, null, 2) + `
178
+ `);
177
179
  } else {
178
180
  for (const r of rows) {
179
181
  const status = r.kind === "guided" ? "no key needed" : r.connected ? "connected" : "not connected";
package/dist/render.js CHANGED
@@ -457,16 +457,23 @@ function resolveMarkerDeclaration(name, rawDecl, rawRegistry, originOf) {
457
457
  }
458
458
  return { name, baseKind: name, ...resolveContainerShape(name, pluginName, rawDecl) };
459
459
  }
460
+ var CORE_MARKER_OWNER = "Gutterpress";
461
+ var CORE_DECLARED_MARKERS = {
462
+ columns: {
463
+ class: "gp-columns",
464
+ variants: { 1: "gp-columns-1", 2: "gp-columns-2", 3: "gp-columns-3", 4: "gp-columns-4", 5: "gp-columns-5" }
465
+ }
466
+ };
460
467
  function buildDeclaredMarkerRegistry(sources) {
461
468
  const rawRegistry = new Map;
462
469
  const originOf = new Map;
463
- for (const { pluginName, markers } of sources) {
470
+ for (const { pluginName, markers } of [{ pluginName: CORE_MARKER_OWNER, markers: CORE_DECLARED_MARKERS }, ...sources]) {
464
471
  if (!markers)
465
472
  continue;
466
473
  for (const [name, rawDecl] of Object.entries(markers)) {
467
474
  validateDeclaredMarkerName(name, pluginName);
468
- if (KNOWN_KINDS.includes(name)) {
469
- throw new Error(`Plugin "${pluginName}" declares marker "@${name}", which is a core Gutterpress marker name ` + `(${KNOWN_KINDS.map((k) => `@${k}`).join(", ")}). Core marker names cannot be shadowed — ` + `rename the plugin's marker.`);
475
+ if (KNOWN_KINDS.includes(name) || pluginName !== CORE_MARKER_OWNER && name in CORE_DECLARED_MARKERS) {
476
+ 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.`);
470
477
  }
471
478
  if (rawRegistry.has(name)) {
472
479
  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.`);
@@ -531,6 +538,9 @@ function scanForUnknownDeclaredMarkers(state, declaredMarkers) {
531
538
  }
532
539
  }
533
540
  }
541
+ function variantClassFor(decl, word) {
542
+ return word && decl.variants && Object.hasOwn(decl.variants, word) ? decl.variants[word] : "";
543
+ }
534
544
  function warnUnknownVariant(env, meta, decl) {
535
545
  const word = meta.name;
536
546
  if (!word)
@@ -546,7 +556,7 @@ function plugin(md, pluginOptions = {}) {
546
556
  preferPagesInSpreads: false,
547
557
  ...pluginOptions
548
558
  };
549
- const declaredMarkers = options.declaredMarkers instanceof Map && options.declaredMarkers.size > 0 ? options.declaredMarkers : null;
559
+ const declaredMarkers = options.declaredMarkers instanceof Map && options.declaredMarkers.size > 0 ? options.declaredMarkers : buildDeclaredMarkerRegistry([]);
550
560
  const declaredWords = declaredKindWords(declaredMarkers);
551
561
  function markerBlock(state, startLine, endLine, silent) {
552
562
  const pos = state.bMarks[startLine] + state.tShift[startLine];
@@ -659,7 +669,7 @@ function plugin(md, pluginOptions = {}) {
659
669
  attrs: meta.attrs || {},
660
670
  labelled: false
661
671
  };
662
- const variantClass = variant && decl.variants && decl.variants[variant] || "";
672
+ const variantClass = variantClassFor(decl, variant);
663
673
  const baseClass = [decl.classBase, variantClass].filter(Boolean).join(" ");
664
674
  addClasses(t, baseClass, meta.attrs && meta.attrs.class ? meta.attrs.class : "");
665
675
  attachDataAttrs(t, decl.baseKind, variant, meta.attrs || {});
@@ -770,7 +780,7 @@ function plugin(md, pluginOptions = {}) {
770
780
  const attrs = meta.attrs || {};
771
781
  const variant = meta.name || decl.presetVariant || null;
772
782
  warnUnknownVariant(state.env, meta, decl);
773
- const variantClass = variant && decl.variants && decl.variants[variant] || "";
783
+ const variantClass = variantClassFor(decl, variant);
774
784
  const cls = [decl.classBase, variantClass, attrs.class].filter(Boolean).join(" ");
775
785
  return {
776
786
  kind: "section",
@@ -1105,9 +1115,19 @@ var GUTTERPRESS_CSS = `
1105
1115
  NOT set, because the correct value depends on whether the run fragments
1106
1116
  across pages (auto packs each page's columns; the CSS initial balance is
1107
1117
  right for a run that fits on one page) and only the author knows which.
1108
- --gp-column-gap is author-settable. */
1109
- .gp-columns-2 { columns: 2; column-gap: var(--gp-column-gap, 1.5em); }
1110
- .gp-columns-3 { columns: 3; column-gap: var(--gp-column-gap, 1.5em); }
1118
+ --gp-column-gap is author-settable.
1119
+ .gp-columns is what the core @columns marker emits (markers.js); on its
1120
+ own it is two columns, and @columns 1 to 5 add the matching count, which
1121
+ wins because it comes later at the same specificity. */
1122
+ .gp-columns, .gp-columns-1, .gp-columns-2, .gp-columns-3, .gp-columns-4, .gp-columns-5 {
1123
+ column-gap: var(--gp-column-gap, 1.5em);
1124
+ }
1125
+ .gp-columns { columns: 2; }
1126
+ .gp-columns-1 { columns: 1; }
1127
+ .gp-columns-2 { columns: 2; }
1128
+ .gp-columns-3 { columns: 3; }
1129
+ .gp-columns-4 { columns: 4; }
1130
+ .gp-columns-5 { columns: 5; }
1111
1131
 
1112
1132
  /* the per-shape decisions the paragraph above deliberately leaves to the
1113
1133
  author, named instead of left as raw CSS every book was reinventing
@@ -1308,8 +1328,12 @@ var GP_CLASSES = new Set([
1308
1328
  "gp-large",
1309
1329
  "gp-tight",
1310
1330
  "gp-loose",
1331
+ "gp-columns",
1332
+ "gp-columns-1",
1311
1333
  "gp-columns-2",
1312
1334
  "gp-columns-3",
1335
+ "gp-columns-4",
1336
+ "gp-columns-5",
1313
1337
  "gp-columns-all",
1314
1338
  "gp-columns-flow",
1315
1339
  "gp-columns-balanced",
@@ -1,16 +1,17 @@
1
1
  import {
2
2
  executeAndReport
3
- } from "./cli-4d4xegsw.js";
3
+ } from "./cli-ev175hx9.js";
4
4
  import {
5
5
  log
6
- } from "./cli-6s4pkvbx.js";
6
+ } from "./cli-0prrt9e4.js";
7
7
  import {
8
8
  UsageError,
9
9
  rejectExtraPositionals,
10
10
  rejectUnknownFlags
11
- } from "./cli-cg03zvez.js";
11
+ } from "./cli-182j48xc.js";
12
12
  import"./cli-wqy8hnnh.js";
13
13
  import {
14
+ BuildError,
14
15
  EXIT_CODES
15
16
  } from "./cli-46ycxe6r.js";
16
17
  import"./cli-kkzka31p.js";
@@ -96,7 +97,7 @@ var validate_default = defineCommand({
96
97
  }, format);
97
98
  } catch (error) {
98
99
  log.error(error instanceof Error ? error.message : String(error));
99
- process.exit(error instanceof UsageError ? error.exitCode : EXIT_CODES.USAGE);
100
+ process.exit(error instanceof UsageError || error instanceof BuildError ? error.exitCode : EXIT_CODES.USAGE);
100
101
  }
101
102
  if (!result.ok) {
102
103
  process.exit(EXIT_CODES.FINDINGS);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gutterpress",
3
- "version": "0.11.16-alpha.5",
3
+ "version": "0.11.16",
4
4
  "description": "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
5
5
  "author": "itlackey",
6
6
  "license": "MPL-2.0",
@@ -66,6 +66,7 @@
66
66
  "fflate": "0.8.3",
67
67
  "glob": "^13.0.0",
68
68
  "htmlhint": "^1.9.2",
69
+ "ignore": "^5.3.2",
69
70
  "isomorphic-git": "^1.38.4",
70
71
  "markdown-it": "^14.3.2",
71
72
  "markdown-it-abbr": "^2.0.0",