gutterpress 0.10.6 → 0.10.7-beta.1
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/README.md +71 -3
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.js +4 -2
- package/dist/{audit-0a4sp2w5.js → audit-82jr4qhw.js} +4 -4
- package/dist/{build-z53bbxtx.js → build-e53pjtez.js} +4 -4
- package/dist/checks/source/css-ownership.d.ts +43 -0
- package/dist/checks/source/index.d.ts +1 -0
- package/dist/{cli-he23vgjj.js → cli-08psmhr5.js} +15 -2
- package/dist/{cli-m99hj80k.js → cli-mk1s46mx.js} +1 -1
- package/dist/{cli-bwbs3qm4.js → cli-s8q42x3r.js} +1 -1
- package/dist/{cli-kwt5ftvc.js → cli-w6h5zegm.js} +4991 -3933
- package/dist/cli.js +70 -37
- package/dist/{doctor-d8vpv1pt.js → doctor-50j69ryq.js} +2 -2
- package/dist/{engine-4ctgbwj5.js → engine-88mynx9a.js} +2 -2
- package/dist/{engine-bhhcne06.js → engine-pb6h089e.js} +1 -1
- package/dist/{index-k5hcp8wj.js → index-837htyjy.js} +1 -1
- package/dist/{index-1rcfsx67.js → index-9zf8mtk7.js} +2586 -2073
- package/dist/{index-eyzv51dw.js → index-a0xpm188.js} +1 -1
- package/dist/index.js +5 -3
- package/dist/lib/cli-args.d.ts +9 -0
- package/dist/lib/manifest-doc.d.ts +8 -2
- package/dist/lib/markdown/assemble.d.ts +24 -7
- package/dist/lib/markdown/gfm-alerts.d.ts +7 -0
- package/dist/lib/markdown/gutterpress-css.d.ts +1 -1
- package/dist/lib/markdown/index.d.ts +16 -0
- package/dist/lib/markdown/plugins.d.ts +8 -12
- package/dist/lib/markdown/renderer.d.ts +48 -1
- package/dist/lib/style-declarations.d.ts +20 -0
- package/dist/lib/style-resolver.d.ts +2 -0
- package/dist/lib/theme-manager.d.ts +110 -9
- package/dist/{lint-nm57gn26.js → lint-zbymjr1z.js} +4 -4
- package/dist/{manifest.schema-hn6ac0ae.json → manifest.schema-kwnrwv8b.json} +11 -8
- package/dist/{new-sawc7zh4.js → new-78j4mx60.js} +4 -4
- package/dist/{plugin-p8yghztz.js → plugin-rahnkax3.js} +5 -11
- package/dist/{preflight-48kwf2pm.js → preflight-qdmqf3j6.js} +4 -4
- package/dist/{preview-10r4dk3d.js → preview-yekma94n.js} +4 -4
- package/dist/{publish-cd05sbdx.js → publish-tm2rqyme.js} +4 -4
- package/dist/render.js +155 -52
- package/dist/schema/manifest.types.d.ts +20 -7
- package/dist/theme-j7f6aegv.js +323 -0
- package/dist/{validate-70ta3br9.js → validate-3ztxavd0.js} +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
connectPublishProvider,
|
|
40
40
|
defaultConfigDir,
|
|
41
41
|
deleteSnippet,
|
|
42
|
+
detectLegacyForkedTheme,
|
|
42
43
|
diagnoseProjectRemote,
|
|
43
44
|
disconnectPublishCredential,
|
|
44
45
|
escapeYamlScalar,
|
|
@@ -124,15 +125,15 @@ import {
|
|
|
124
125
|
syncProject,
|
|
125
126
|
testRemoteAccess,
|
|
126
127
|
validateProjectPlugins
|
|
127
|
-
} from "./index-
|
|
128
|
+
} from "./index-9zf8mtk7.js";
|
|
128
129
|
import {
|
|
129
130
|
BUILD_DIAGNOSTIC_CODES
|
|
130
|
-
} from "./index-
|
|
131
|
+
} from "./index-a0xpm188.js";
|
|
131
132
|
import {
|
|
132
133
|
BuildError,
|
|
133
134
|
DEFAULT_PRINT_OPTS,
|
|
134
135
|
readyProbeExpr
|
|
135
|
-
} from "./index-
|
|
136
|
+
} from "./index-837htyjy.js";
|
|
136
137
|
import {
|
|
137
138
|
AUTO_SNAPSHOT_MESSAGE,
|
|
138
139
|
HISTORY_PAGE_LIMIT,
|
|
@@ -245,6 +246,7 @@ export {
|
|
|
245
246
|
disconnectPublishCredential,
|
|
246
247
|
diagnoseProjectRemote,
|
|
247
248
|
detectProjectSource,
|
|
249
|
+
detectLegacyForkedTheme,
|
|
248
250
|
deleteSnippet,
|
|
249
251
|
defaultConfigDir,
|
|
250
252
|
connectPublishProvider,
|
package/dist/lib/cli-args.d.ts
CHANGED
|
@@ -50,3 +50,12 @@ export declare function parsePdfxFlavor(raw: unknown, format: BuildFormat): Pdfx
|
|
|
50
50
|
export declare function parseEngine(raw: unknown): "paged" | "native" | undefined;
|
|
51
51
|
/** Parse `--port`, defaulting to {@link NETWORK.DEFAULT_PORT} (0 = OS-assigned). */
|
|
52
52
|
export declare function resolvePort(raw: unknown): number;
|
|
53
|
+
/**
|
|
54
|
+
* THE command-boundary handler for a {@link UsageError}: print the message and
|
|
55
|
+
* exit with the error's own code. Every subcommand-dispatching command
|
|
56
|
+
* (`plugin`, `theme`) catches its parse errors here, so the usage-error→exit
|
|
57
|
+
* mapping has one definition rather than one per command.
|
|
58
|
+
*/
|
|
59
|
+
export declare function exitForUsage(error: unknown): never;
|
|
60
|
+
/** Resolve a `--dir` argument to an absolute path, defaulting to the cwd. */
|
|
61
|
+
export declare function resolveProjectDir(dir: unknown): string;
|
|
@@ -9,8 +9,14 @@ export declare function loadManifestDoc(projectDir: string): Promise<{
|
|
|
9
9
|
}>;
|
|
10
10
|
/** Atomically write the doc back, creating the project dir if needed. */
|
|
11
11
|
export declare function writeManifestDoc(file: string, doc: Document.Parsed): Promise<void>;
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* The named sequence node, creating (and attaching) an empty one if missing.
|
|
14
|
+
* `key` is a single top-level key (the original, still-exact behavior) OR a
|
|
15
|
+
* path for a nested key (e.g. `["engineStyles", "native"]`, #239) — the
|
|
16
|
+
* `getIn`/`setIn` branch auto-vivifies any missing intermediate map, exactly
|
|
17
|
+
* like a hand-written `engineStyles: { native: [...] }` would parse.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ensureSeq(doc: Document.Parsed, key: string | readonly string[]): YAMLSeq;
|
|
14
20
|
/**
|
|
15
21
|
* Unwrap a yaml seq item (or `getIn`-style Pair) to its string value: `null`
|
|
16
22
|
* when the item isn't a string. Handles both a bare Scalar/Pair-shaped node
|
|
@@ -23,6 +23,16 @@ export interface LayoutWarning {
|
|
|
23
23
|
message: string;
|
|
24
24
|
marker?: unknown;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The Chromium milestone that paints a `@page { background: url() }` image
|
|
28
|
+
* with no other reference to it (spec gap #152). Measured 2026-09-03: Chrome
|
|
29
|
+
* for Testing 152.0.7977.54 and the CI runner's Chrome stable both paint it;
|
|
30
|
+
* Chrome 151.0.7922.75 dropped it (docs/known-limitations.md §3). The
|
|
31
|
+
* `preloadImages` shim below expires when the engine's floor
|
|
32
|
+
* (`REQUIRED_MILESTONE`, engine/shared/cdp.ts) reaches this — the canary in
|
|
33
|
+
* engine/compiler/page-background-chromium-bug.canary.test.ts enforces that.
|
|
34
|
+
*/
|
|
35
|
+
export declare const PAGE_BACKGROUND_FIXED_MILESTONE = 152;
|
|
26
36
|
export interface AssembleBookHtmlOptions {
|
|
27
37
|
/** Ordered list of project-root-relative `.md` files to concatenate. */
|
|
28
38
|
files: string[];
|
|
@@ -42,9 +52,10 @@ export interface AssembleBookHtmlOptions {
|
|
|
42
52
|
*/
|
|
43
53
|
projectCss?: string;
|
|
44
54
|
/**
|
|
45
|
-
* SHIM — spec gap #152
|
|
46
|
-
*
|
|
47
|
-
*
|
|
55
|
+
* SHIM — spec gap #152, fixed upstream in Chromium
|
|
56
|
+
* {@link PAGE_BACKGROUND_FIXED_MILESTONE}. Output-relative hrefs of the
|
|
57
|
+
* images the project's stylesheets staged (`inlineStyles`'s copy plan,
|
|
58
|
+
* verbatim), each emitted as one `<link rel="preload" as="image">`.
|
|
48
59
|
*
|
|
49
60
|
* Chromium reaches an `@page`-only `url()` lazily, during the print, and the
|
|
50
61
|
* print path CDP drives never waits for a pending resource — so the sheet
|
|
@@ -64,10 +75,16 @@ export interface AssembleBookHtmlOptions {
|
|
|
64
75
|
* either document order) — which is why `asset-inline.ts` content-addresses
|
|
65
76
|
* every CSS image so no element can name one.
|
|
66
77
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
78
|
+
* WHEN IT GOES: Chromium fixed the bug in milestone
|
|
79
|
+
* {@link PAGE_BACKGROUND_FIXED_MILESTONE} (measured 2026-09-03: Chrome 152
|
|
80
|
+
* paints the sole-referenced image, 151 dropped it). The preload changes
|
|
81
|
+
* nothing on a fixed Chromium, and it still protects every Chromium the
|
|
82
|
+
* engine accepts below that (`REQUIRED_MILESTONE` in `engine/shared/cdp.ts`,
|
|
83
|
+
* 148 — what Electron 42 ships), so it stays until the floor reaches the
|
|
84
|
+
* fix. The expiry canary,
|
|
85
|
+
* `engine/compiler/page-background-chromium-bug.canary.test.ts`, goes red
|
|
86
|
+
* the day the floor is raised that far — delete this option, the `.map()`
|
|
87
|
+
* that feeds it in `markdown/index.ts`, the constant, and the canary.
|
|
71
88
|
*
|
|
72
89
|
* The copy plan is the source, NOT a scan of the assembled CSS: `pluginCss`
|
|
73
90
|
* never passes through `inlineStyles`, so a `url()` inside it is never
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type MarkdownIt from "markdown-it";
|
|
2
|
+
/**
|
|
3
|
+
* The plugin export — a plain `(md) => void` markdown-it plugin (CLAUDE.md
|
|
4
|
+
* §5), registered by name from `BUILTIN_OPTIONAL_PLUGINS` when a project
|
|
5
|
+
* lists `gutterpress-gfm-alerts` in its manifest.
|
|
6
|
+
*/
|
|
7
|
+
export default function gfmAlerts(md: MarkdownIt): void;
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
* carrying gp-left could start shifting; that is standards-tracking per
|
|
137
137
|
* CLAUDE.md ("Chrome wins once it ships"), not a bug in the author's book.
|
|
138
138
|
*/
|
|
139
|
-
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 (2026-09-01 CSS architecture review, findings C1/C7 \u2014 CLAUDE.md \u00A70:\n \"behavior broadly useful to non-technical authors belongs 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";
|
|
139
|
+
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 (2026-09-01 CSS architecture review, findings C1/C7 \u2014 CLAUDE.md \u00A70:\n \"behavior broadly useful to non-technical authors belongs 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";
|
|
140
140
|
/**
|
|
141
141
|
* GP_CLASSES — every class an author may legitimately write with a `gp-`
|
|
142
142
|
* prefix: the `.gp-*` selectors in `GUTTERPRESS_CSS` above, the two
|
|
@@ -37,6 +37,20 @@ export declare function renderChapters(inputDir: string, opts?: {
|
|
|
37
37
|
files?: string[] | null;
|
|
38
38
|
plugins?: LoadedPlugin[];
|
|
39
39
|
pluginCss?: string;
|
|
40
|
+
/**
|
|
41
|
+
* #238 — absolute paths of plugin-declared `styles` files, in plugin load
|
|
42
|
+
* order (`loadPluginsWithCss`'s `pluginStylePaths`). Inlined through the
|
|
43
|
+
* SAME pipeline the project's own stylesheets get (`inlineStyles`: fonts/
|
|
44
|
+
* images embedded, local `@import` followed, print-safety lintable) and
|
|
45
|
+
* placed in the SAME cascade position the legacy `pluginCss` string
|
|
46
|
+
* already holds — after core, before the project's own stylesheets.
|
|
47
|
+
* Absolute paths pass through `inlineStyles` unchanged (`path.resolve` is
|
|
48
|
+
* a no-op on an already-absolute input), so a plugin's stylesheet
|
|
49
|
+
* location is independent of `inputDir`. Omitting this (existing
|
|
50
|
+
* string-only-`css` plugins, or none at all) is a zero-cost, byte-
|
|
51
|
+
* identical no-op — see the test asserting exactly that.
|
|
52
|
+
*/
|
|
53
|
+
pluginStylePaths?: string[];
|
|
40
54
|
/** Wrap each source file for incremental preview pagination. */
|
|
41
55
|
wrapChapters?: boolean;
|
|
42
56
|
/** Add source-file ids to source-mapped preview blocks without wrappers. */
|
|
@@ -70,6 +84,8 @@ export declare function renderChaptersToFile(inputDir: string, outDir: string, o
|
|
|
70
84
|
files?: string[] | null;
|
|
71
85
|
plugins?: LoadedPlugin[];
|
|
72
86
|
pluginCss?: string;
|
|
87
|
+
/** #238 — see {@link renderChapters}'s option of the same name. */
|
|
88
|
+
pluginStylePaths?: string[];
|
|
73
89
|
/** ARCH finding #4 — see {@link renderChapters}'s option of the same name. */
|
|
74
90
|
onChapterWarnings?: (file: string, warnings: LayoutWarning[]) => void;
|
|
75
91
|
/** See {@link renderChapters}'s options of the same names. */
|
|
@@ -1,23 +1,12 @@
|
|
|
1
1
|
import type { ResolvedPluginConfig } from "../../schema/manifest.types";
|
|
2
2
|
import type { LoadedPlugin } from "./renderer";
|
|
3
3
|
export type { GutterpressPlugin, GutterpressPluginMetadata, GutterpressPluginExport, LoadedPlugin, } from "./renderer";
|
|
4
|
-
export { applyPlugins, collectPluginCss } from "./renderer";
|
|
4
|
+
export { applyPlugins, collectPluginCss, collectPluginStylePaths } from "./renderer";
|
|
5
5
|
/** Test-only mutation seam between snapshot completion and snapshot verification. */
|
|
6
6
|
export declare function __setVendorSnapshotHookForTests(hook?: (sourceRoot: string, snapshotRoot: string) => void | Promise<void>): void;
|
|
7
7
|
export declare function clearVendoredPluginResolver(baseDir: string, packageName: string, version: string): void;
|
|
8
8
|
/** Test-only: reset the path-plugin cache between test cases. */
|
|
9
9
|
export declare function __resetPathPluginCacheForTests(): void;
|
|
10
|
-
/**
|
|
11
|
-
* Load a single plugin from a file path or npm package.
|
|
12
|
-
*
|
|
13
|
-
* Throws if the plugin cannot be resolved, imported, or doesn't export a
|
|
14
|
-
* valid plugin function. The error message identifies which manifest entry
|
|
15
|
-
* failed so users can find it.
|
|
16
|
-
*
|
|
17
|
-
* Path plugins always go through the mtime cache (see the call below): it is
|
|
18
|
-
* correct in both a one-shot CLI build and the long-lived Electron host that
|
|
19
|
-
* runs `runBuild` in-process, so no caller-selected cache mode is needed.
|
|
20
|
-
*/
|
|
21
10
|
export declare function loadPlugin(config: ResolvedPluginConfig, baseDir: string): Promise<LoadedPlugin>;
|
|
22
11
|
/**
|
|
23
12
|
* Load all plugins from the resolved configuration.
|
|
@@ -50,6 +39,13 @@ export interface LoadedPluginsWithCss {
|
|
|
50
39
|
* straight through without an `?? []` at every call site. */
|
|
51
40
|
plugins: LoadedPlugin[] | undefined;
|
|
52
41
|
pluginCss: string;
|
|
42
|
+
/**
|
|
43
|
+
* #238 — absolute paths of every plugin-declared `styles` file, flattened
|
|
44
|
+
* in plugin load order. `[]` when no loaded plugin declares any (including
|
|
45
|
+
* when `configs` was empty), so callers can pass this straight through to
|
|
46
|
+
* `renderChapters`'s `pluginStylePaths` without an `?? []` guard.
|
|
47
|
+
*/
|
|
48
|
+
pluginStylePaths: string[];
|
|
53
49
|
}
|
|
54
50
|
/**
|
|
55
51
|
* Shared "load plugins -> collect their CSS" preamble (ARCH finding #53).
|
|
@@ -47,6 +47,7 @@ export interface GutterpressPluginMetadata {
|
|
|
47
47
|
* export default plugin;
|
|
48
48
|
* export const metadata: GutterpressPluginMetadata = { name: 'my-plugin', version: '1.0.0' };
|
|
49
49
|
* export const css = `.my-class { color: red; }`;
|
|
50
|
+
* export const styles = ["./styles/components.css", "./styles/callouts.css"];
|
|
50
51
|
* ```
|
|
51
52
|
*/
|
|
52
53
|
export interface GutterpressPluginExport {
|
|
@@ -54,6 +55,17 @@ export interface GutterpressPluginExport {
|
|
|
54
55
|
metadata?: GutterpressPluginMetadata;
|
|
55
56
|
/** CSS injected into <head> after user stylesheets. Use sparingly — has equal cascade specificity. */
|
|
56
57
|
css?: string;
|
|
58
|
+
/**
|
|
59
|
+
* File-based plugin CSS (#238), as paths RELATIVE TO THIS MODULE — resolved
|
|
60
|
+
* by the node-coupled loader (`plugins.ts`), never by this pure module. Each
|
|
61
|
+
* file enters the SAME pipeline a manifest `styles:` entry does: asset-
|
|
62
|
+
* inlined (fonts/images embedded, local `@import` followed) and print-safety
|
|
63
|
+
* lintable — unlike `css`, which is an opaque string no other subsystem can
|
|
64
|
+
* see. Kept alongside `css` (not a replacement) for one-liners that don't
|
|
65
|
+
* warrant a separate file. Cascade position matches `css`'s: after core,
|
|
66
|
+
* before the project's own stylesheets.
|
|
67
|
+
*/
|
|
68
|
+
styles?: string[];
|
|
57
69
|
}
|
|
58
70
|
/** Internal representation of a loaded plugin, ready for `md.use()`. */
|
|
59
71
|
export interface LoadedPlugin {
|
|
@@ -61,6 +73,13 @@ export interface LoadedPlugin {
|
|
|
61
73
|
plugin: GutterpressPlugin;
|
|
62
74
|
metadata?: GutterpressPluginMetadata;
|
|
63
75
|
css?: string;
|
|
76
|
+
/**
|
|
77
|
+
* #238 — UNLIKE `GutterpressPluginExport.styles` (author-declared, relative
|
|
78
|
+
* to the plugin module), this is already resolved to ABSOLUTE filesystem
|
|
79
|
+
* paths by the loader (`plugins.ts`'s `loadPlugin`), in the plugin's own
|
|
80
|
+
* declared order. `undefined`/`[]` for a plugin that declares none.
|
|
81
|
+
*/
|
|
82
|
+
styles?: string[];
|
|
64
83
|
options: Record<string, unknown>;
|
|
65
84
|
}
|
|
66
85
|
/**
|
|
@@ -72,6 +91,14 @@ export interface LoadedPlugin {
|
|
|
72
91
|
* non-technical author gets the feature instantly instead of a "not installed"
|
|
73
92
|
* error. (attrs/footnote/deflist are NOT here — they are always-on defaults
|
|
74
93
|
* applied unconditionally below.)
|
|
94
|
+
*
|
|
95
|
+
* `gutterpress-gfm-alerts` (#237) is keyed differently from its four
|
|
96
|
+
* siblings: it is not a real npm package, it is Gutterpress's OWN code
|
|
97
|
+
* (gfm-alerts.ts) registered under a name that reads like one, matching the
|
|
98
|
+
* shape "keyed by npm name" is written for. There is nothing to install
|
|
99
|
+
* either way — the lookup below always wins before any npm resolution is
|
|
100
|
+
* attempted (`plugins.ts`'s `loadPlugin`) — so a real npm package never
|
|
101
|
+
* existing under this exact name costs nothing.
|
|
75
102
|
*/
|
|
76
103
|
export declare const BUILTIN_OPTIONAL_PLUGINS: Record<string, GutterpressPlugin>;
|
|
77
104
|
/**
|
|
@@ -97,7 +124,16 @@ export declare const BUILTIN_OPTIONAL_PLUGINS: Record<string, GutterpressPlugin>
|
|
|
97
124
|
*
|
|
98
125
|
* GFM-style `> [!NOTE]` alerts were also moved into the DC plugin on the
|
|
99
126
|
* same date because the emitted classes (dc-alert, dc-vibe-callout, etc.)
|
|
100
|
-
*
|
|
127
|
+
* were DC-branded — core should not leak DC identifiers. #237 (0.10.7)
|
|
128
|
+
* restored a core-owned, unbranded equivalent as an OPT-IN bundled feature —
|
|
129
|
+
* `gfm-alerts.ts`, registered below as `gutterpress-gfm-alerts` — emitting
|
|
130
|
+
* only the standard GitHub five (NOTE/TIP/IMPORTANT/WARNING/CAUTION) as
|
|
131
|
+
* neutral `gp-alert`/`gp-alert-<type>` structure (see that file's header).
|
|
132
|
+
* This does not re-converge with the DC plugin: DC's branded extra types
|
|
133
|
+
* (`[!DM]`/`[!VIBE]`/`[!ORIGIN]`, etc.) and its own class names stay exactly
|
|
134
|
+
* where they were moved to, layered on top of (or independent from) this
|
|
135
|
+
* primitive. A project using neither plugin still renders `> [!NOTE]` as a
|
|
136
|
+
* literal blockquote, unchanged — this feature is opt-in, not a default.
|
|
101
137
|
*
|
|
102
138
|
* @param customPlugins - Optional array of custom plugins to load
|
|
103
139
|
*/
|
|
@@ -114,3 +150,14 @@ export declare function applyPlugins(md: MarkdownIt, plugins: LoadedPlugin[]): v
|
|
|
114
150
|
* Collect CSS from all loaded plugins, concatenated in load order.
|
|
115
151
|
*/
|
|
116
152
|
export declare function collectPluginCss(plugins: LoadedPlugin[]): string;
|
|
153
|
+
/**
|
|
154
|
+
* Collect every plugin-declared stylesheet PATH (#238), flattened in plugin
|
|
155
|
+
* load order (a plugin's own files keep their declared order). These are
|
|
156
|
+
* already-resolved ABSOLUTE filesystem paths — `plugins.ts`'s loader resolves
|
|
157
|
+
* them before a `LoadedPlugin` exists, so this stays a pure list operation
|
|
158
|
+
* with no `node:*` needed, matching {@link collectPluginCss}. The caller feeds
|
|
159
|
+
* the result through the SAME asset-inline pipeline a manifest `styles:` list
|
|
160
|
+
* gets (see `lib/markdown/index.ts`'s `renderChapters`), which is where the
|
|
161
|
+
* files are actually read.
|
|
162
|
+
*/
|
|
163
|
+
export declare function collectPluginStylePaths(plugins: LoadedPlugin[]): string[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a declared stylesheet list to absolute, existence-checked paths, in
|
|
3
|
+
* declared order. `undefined`/`[]` returns `undefined` — a plugin/theme that
|
|
4
|
+
* declares no `styles` pays zero cost here, the exact contract #238
|
|
5
|
+
* established and #239 now shares. Any entry that doesn't resolve to a real
|
|
6
|
+
* file THROWS immediately: a final artifact must never silently drop an
|
|
7
|
+
* author-declared stylesheet from the cascade (CLAUDE.md §5's fail-fast
|
|
8
|
+
* doctrine) — this is the load-time check, matching a plugin's own
|
|
9
|
+
* missing-file error, not a render-time warning.
|
|
10
|
+
*
|
|
11
|
+
* `baseDir` is never resolved by this function — whether a plugin/theme even
|
|
12
|
+
* HAS a usable base directory (e.g. a plugin loaded via a bare npm specifier
|
|
13
|
+
* with no on-disk path) is a caller-specific question the caller answers
|
|
14
|
+
* before calling this; by the time `baseDir` reaches here it names a real
|
|
15
|
+
* directory to resolve `rawStyles` against.
|
|
16
|
+
*
|
|
17
|
+
* @param subject Names the declaring thing for the error message, e.g.
|
|
18
|
+
* `Plugin "my-plugin"` or `Theme "dc-design"`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveDeclaredStyles(rawStyles: string[] | undefined, baseDir: string, subject: string): string[] | undefined;
|
|
@@ -7,6 +7,8 @@ export interface ProjectStyle {
|
|
|
7
7
|
/** True when this stylesheet is in the manifest `styles:` list (the active set). */
|
|
8
8
|
active: boolean;
|
|
9
9
|
}
|
|
10
|
+
/** Project-relative, forward-slash display path for an absolute css path. */
|
|
11
|
+
export declare function relDisplay(projectDir: string, absPath: string): string;
|
|
10
12
|
/**
|
|
11
13
|
* THE canonical "which stylesheet does this project use?" resolver — consumed by
|
|
12
14
|
* BOTH the renderer (to `<link>` them) and the editor (to edit them), so they
|
|
@@ -10,6 +10,34 @@ export interface ThemeMetadata {
|
|
|
10
10
|
description?: string;
|
|
11
11
|
/** Optional preview image path (relative to the theme folder). */
|
|
12
12
|
preview?: string | null;
|
|
13
|
+
/**
|
|
14
|
+
* #239 — ordered stylesheets, relative to the theme folder. A theme is no
|
|
15
|
+
* longer capped at one file: this is what lets a real component library
|
|
16
|
+
* (tokens/base/components/templates/rules as separate layered sheets) be a
|
|
17
|
+
* theme. Absent/empty defaults to `["theme.css"]` — every theme published
|
|
18
|
+
* before this field existed keeps working, byte-for-byte, untouched. Globs
|
|
19
|
+
* are NOT supported, matching the manifest's own `styles:` list (§0 — one
|
|
20
|
+
* authoring convention, not two).
|
|
21
|
+
*/
|
|
22
|
+
styles?: string[];
|
|
23
|
+
/**
|
|
24
|
+
* #239 — engine-conditional sheets, relative to the theme folder. Applying
|
|
25
|
+
* the theme appends these to the manifest's `engineStyles.native` list
|
|
26
|
+
* (loaded last, so furniture wins — see manifest.ts's resolveWithPreset).
|
|
27
|
+
*/
|
|
28
|
+
engineStyles?: {
|
|
29
|
+
native?: string[];
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* #239 — which declared sheet (a path from `styles`, or `theme.css` when
|
|
33
|
+
* `styles` is absent) carries the author-facing `:root` token surface. Lets
|
|
34
|
+
* a rich multi-sheet theme tell the Design panel's guided token editor
|
|
35
|
+
* where to look instead of it guessing at the single active stylesheet.
|
|
36
|
+
* Purely advisory metadata — nothing in this module enforces it. Absent
|
|
37
|
+
* here means "no override"; {@link ThemeInfo.tokensFile} is where the
|
|
38
|
+
* default (the theme's primary/only sheet) gets filled in.
|
|
39
|
+
*/
|
|
40
|
+
tokensFile?: string;
|
|
13
41
|
}
|
|
14
42
|
/** Author-friendly metadata for one theme (built-in or project). */
|
|
15
43
|
export interface ThemeInfo {
|
|
@@ -25,6 +53,23 @@ export interface ThemeInfo {
|
|
|
25
53
|
kind: "builtin" | "project";
|
|
26
54
|
/** Optional preview image path relative to the theme folder. */
|
|
27
55
|
preview?: string | null;
|
|
56
|
+
/**
|
|
57
|
+
* #239 — the theme's declared stylesheets, relative to the theme folder, in
|
|
58
|
+
* cascade order. Always populated (defaults to `["theme.css"]` when the
|
|
59
|
+
* theme declares no `styles`), so a caller never needs to re-derive the
|
|
60
|
+
* default itself.
|
|
61
|
+
*/
|
|
62
|
+
styles: string[];
|
|
63
|
+
/**
|
|
64
|
+
* #239 — which declared sheet (relative to the theme folder) carries the
|
|
65
|
+
* `:root` token surface — see {@link ThemeMetadata.tokensFile}. ALWAYS
|
|
66
|
+
* populated: an explicit `theme.json` declaration wins, otherwise this
|
|
67
|
+
* defaults to the theme's primary sheet (`styles[0]`) — `"theme.css"` for
|
|
68
|
+
* every legacy single-sheet theme, exactly as `styles` itself defaults.
|
|
69
|
+
* A caller (the Design panel) never needs its own "guess the token sheet"
|
|
70
|
+
* fallback.
|
|
71
|
+
*/
|
|
72
|
+
tokensFile: string;
|
|
28
73
|
}
|
|
29
74
|
/** A built-in theme resolved to disk (extracted from the embedded assets). */
|
|
30
75
|
export interface ResolvedTheme {
|
|
@@ -42,19 +87,54 @@ export type ApplyThemeTarget = {
|
|
|
42
87
|
kind: "project";
|
|
43
88
|
id: string;
|
|
44
89
|
};
|
|
90
|
+
/**
|
|
91
|
+
* A theme's declared stylesheets, relative to its folder, in cascade order
|
|
92
|
+
* (#239). An absent OR empty `styles` defaults to `["theme.css"]` — the
|
|
93
|
+
* single-file format every theme published before this field existed already
|
|
94
|
+
* satisfies, so nothing needs migrating. Exported for `theme-import.ts`,
|
|
95
|
+
* which validates every one of these (existence + print-safety) at import
|
|
96
|
+
* time, the same way it always validated the lone `theme.css`.
|
|
97
|
+
*/
|
|
98
|
+
export declare function themeStyleList(meta: ThemeMetadata): string[];
|
|
99
|
+
/**
|
|
100
|
+
* Every declared sheet must live INSIDE the theme folder. A theme is
|
|
101
|
+
* self-contained by contract (apply copies the whole folder), and an imported
|
|
102
|
+
* package is untrusted input: a `../` or absolute entry would make apply read
|
|
103
|
+
* a file from anywhere on disk into the book.
|
|
104
|
+
*
|
|
105
|
+
* This is a WRITE-BOUNDARY guard — `applyTheme`, `importThemeFromFolder` and
|
|
106
|
+
* `theme-import.ts`'s package validation call it before copying anything or
|
|
107
|
+
* wiring a manifest. It deliberately does NOT live in {@link themeStyleList},
|
|
108
|
+
* which the read paths (`listProjectThemes`, `getActiveTheme`,
|
|
109
|
+
* `getPreviousTheme`, `readThemeCss`) also call: one hand-edited theme.json
|
|
110
|
+
* must not take down listing every theme, the same reason `readThemeMeta`
|
|
111
|
+
* returns `{}` for an unparseable file instead of throwing.
|
|
112
|
+
*/
|
|
113
|
+
export declare function assertThemeSheetsContained(meta: ThemeMetadata): void;
|
|
114
|
+
/**
|
|
115
|
+
* A theme's declared engine-conditional sheets, relative to its folder
|
|
116
|
+
* (#239). `Array.isArray` guards a malformed `engineStyles.native` (authored
|
|
117
|
+
* as something other than a list) — treated as "none declared" rather than a
|
|
118
|
+
* hard crash on a JSON author's typo. Exported for `theme-import.ts`'s import
|
|
119
|
+
* validation, same reuse rationale as {@link themeStyleList}.
|
|
120
|
+
*/
|
|
121
|
+
export declare function themeEngineStyleList(meta: ThemeMetadata): string[];
|
|
45
122
|
/** List the built-in themes (metadata read from the extracted embedded assets). */
|
|
46
123
|
export declare function listBuiltInThemes(): Promise<ThemeInfo[]>;
|
|
47
124
|
/** Resolve a built-in theme to its extracted `theme.css` + parsed metadata. */
|
|
48
125
|
export declare function resolveBuiltInTheme(id: string): Promise<ResolvedTheme>;
|
|
49
126
|
/**
|
|
50
127
|
* List the themes that live inside the project under `themes/<id>/` (each a
|
|
51
|
-
* folder
|
|
128
|
+
* folder whose PRIMARY declared sheet exists — `theme.css` by default, or
|
|
129
|
+
* `theme.json`'s `styles[0]` for a multi-sheet theme, #239). Returns `[]`
|
|
130
|
+
* when there is no `themes/` folder.
|
|
52
131
|
*/
|
|
53
132
|
export declare function listProjectThemes(projectDir: string): Promise<ThemeInfo[]>;
|
|
54
133
|
/**
|
|
55
|
-
* Read the project's currently active theme (the theme
|
|
56
|
-
* the manifest `styles:` list AND whose folder
|
|
57
|
-
* `null` when no theme is
|
|
134
|
+
* Read the project's currently active theme (the theme with an entry under
|
|
135
|
+
* `themes/<id>/…` in the manifest `styles:` list AND whose folder's PRIMARY
|
|
136
|
+
* declared sheet still exists on disk, #239). Returns `null` when no theme is
|
|
137
|
+
* applied.
|
|
58
138
|
*/
|
|
59
139
|
export declare function getActiveTheme(projectDir: string): Promise<ThemeInfo | null>;
|
|
60
140
|
/**
|
|
@@ -92,8 +172,10 @@ export declare function revertTheme(projectDir: string): Promise<ThemeInfo>;
|
|
|
92
172
|
export declare function applyTheme(projectDir: string, target: ApplyThemeTarget): Promise<ThemeInfo>;
|
|
93
173
|
/**
|
|
94
174
|
* Import a theme from a local folder by copying it into the project's
|
|
95
|
-
* `themes/<id>/`. The folder MUST contain
|
|
96
|
-
*
|
|
175
|
+
* `themes/<id>/`. The folder MUST contain its PRIMARY declared stylesheet —
|
|
176
|
+
* `theme.css` by default, or `theme.json`'s `styles[0]` for a multi-sheet
|
|
177
|
+
* theme (#239) that ships no `theme.css` at all. Metadata comes from its
|
|
178
|
+
* `theme.json` (synthesised from the folder name when absent). The imported
|
|
97
179
|
* theme becomes available to {@link applyTheme}; importing does NOT auto-apply.
|
|
98
180
|
*/
|
|
99
181
|
export declare function importThemeFromFolder(projectDir: string, sourceDir: string): Promise<ThemeInfo>;
|
|
@@ -114,8 +196,11 @@ export declare function importThemeFromFolder(projectDir: string, sourceDir: str
|
|
|
114
196
|
export declare function importThemeFromUrl(projectDir: string, url: string): Promise<ThemeInfo>;
|
|
115
197
|
/**
|
|
116
198
|
* Read a theme's CSS for previewing. Built-in themes read from the embedded
|
|
117
|
-
* assets; project themes read
|
|
118
|
-
*
|
|
199
|
+
* assets; project themes read and concatenate EVERY sheet `theme.json`
|
|
200
|
+
* declares (#239 — `theme.css` alone when it declares none), in cascade
|
|
201
|
+
* order. Used by the host to feed the renderer a sample-render thumbnail (the
|
|
202
|
+
* renderer never touches fs) — a read-only preview, so concatenation is fine
|
|
203
|
+
* even though it wouldn't be for an editable surface.
|
|
119
204
|
*/
|
|
120
205
|
export declare function readThemeCss(projectDir: string | null, source: {
|
|
121
206
|
kind: "builtin" | "project";
|
|
@@ -123,6 +208,22 @@ export declare function readThemeCss(projectDir: string | null, source: {
|
|
|
123
208
|
}): Promise<string>;
|
|
124
209
|
/**
|
|
125
210
|
* Remove an imported/applied project theme folder. If it was the active theme,
|
|
126
|
-
* its
|
|
211
|
+
* its ENTIRE block is dropped too — every `styles:` entry AND every
|
|
212
|
+
* `engineStyles.native` entry under `themes/<id>/…` (#239), not just a single
|
|
213
|
+
* `theme.css` href. Never touches built-in (embedded) themes.
|
|
127
214
|
*/
|
|
128
215
|
export declare function removeProjectTheme(projectDir: string, id: string): Promise<void>;
|
|
216
|
+
/**
|
|
217
|
+
* #236 follow-through: detect a project whose `styles/book.css` is
|
|
218
|
+
* byte-identical to a built-in theme's `theme.css` while NO theme is tracked
|
|
219
|
+
* as active. That shape is exactly what a pre-0.10.7 `gutterpress new` (or
|
|
220
|
+
* "set up as a book") produced — a real, working stylesheet that is simply
|
|
221
|
+
* invisible to this command and to the desktop's Theme panel, and which will
|
|
222
|
+
* keep loading AFTER (so silently override) whatever theme is applied next.
|
|
223
|
+
* Read-only: this never modifies the project — it only surfaces a note so the
|
|
224
|
+
* author can decide whether to run `theme apply` themselves.
|
|
225
|
+
*/
|
|
226
|
+
export declare function detectLegacyForkedTheme(projectDir: string, active: ThemeInfo | null): Promise<{
|
|
227
|
+
id: string;
|
|
228
|
+
name: string;
|
|
229
|
+
} | null>;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MANIFEST_FILENAMES,
|
|
3
3
|
runLint
|
|
4
|
-
} from "./cli-
|
|
4
|
+
} from "./cli-w6h5zegm.js";
|
|
5
5
|
import {
|
|
6
6
|
log
|
|
7
|
-
} from "./cli-
|
|
7
|
+
} from "./cli-mk1s46mx.js";
|
|
8
8
|
import {
|
|
9
9
|
UsageError,
|
|
10
10
|
rejectExtraPositionals,
|
|
11
11
|
rejectUnknownFlags
|
|
12
|
-
} from "./cli-
|
|
13
|
-
import"./cli-
|
|
12
|
+
} from "./cli-08psmhr5.js";
|
|
13
|
+
import"./cli-s8q42x3r.js";
|
|
14
14
|
import"./cli-c41yr7he.js";
|
|
15
15
|
import {
|
|
16
16
|
EXIT_CODES
|
|
@@ -38,13 +38,8 @@
|
|
|
38
38
|
},
|
|
39
39
|
"engineStyles": {
|
|
40
40
|
"type": "object",
|
|
41
|
-
"description": "Engine-conditional stylesheets, appended AFTER `styles`. `.native` is the only list
|
|
41
|
+
"description": "Engine-conditional stylesheets, appended AFTER `styles`. `.native` is the only list — the native engine is the only engine, so there is nothing else to condition on. A manifest from the dual-engine era may still carry `.paged`; it keeps loading (with a one-line warning) but is no longer part of this schema.",
|
|
42
42
|
"properties": {
|
|
43
|
-
"paged": {
|
|
44
|
-
"type": "array",
|
|
45
|
-
"description": "Accepted for backward-compatible manifest parsing only; entries here are ignored with a warning.",
|
|
46
|
-
"items": { "type": "string" }
|
|
47
|
-
},
|
|
48
43
|
"native": { "type": "array", "items": { "type": "string" } }
|
|
49
44
|
},
|
|
50
45
|
"additionalProperties": false,
|
|
@@ -58,7 +53,7 @@
|
|
|
58
53
|
},
|
|
59
54
|
"styles": {
|
|
60
55
|
"type": "array",
|
|
61
|
-
"description": "Project-relative CSS files to <link> into the rendered book, applied in order. If omitted, Gutterpress discovers one: styles/book.css, else css/print.css, css/index.css, css/style.css, css/main.css
|
|
56
|
+
"description": "Project-relative CSS files to <link> into the rendered book, applied in order. If omitted, Gutterpress discovers one: styles/book.css (what `gutterpress new` scaffolds), else four legacy names kept only for pre-existing projects — css/print.css, css/index.css, css/style.css, css/main.css — else the first discovered .css, else none. New projects should set this explicitly or use styles/book.css.",
|
|
62
57
|
"items": {
|
|
63
58
|
"type": "string",
|
|
64
59
|
"minLength": 1,
|
|
@@ -115,7 +110,7 @@
|
|
|
115
110
|
},
|
|
116
111
|
"priority": {
|
|
117
112
|
"type": "integer",
|
|
118
|
-
"description": "Load order.
|
|
113
|
+
"description": "Advanced, rarely needed. Load order: higher loads first. If one of your plugins needs to see tokens another one of your plugins produces, give it the LOWER number of the two, so it loads (and runs) after.",
|
|
119
114
|
"default": 100,
|
|
120
115
|
"examples": [100, 200]
|
|
121
116
|
},
|
|
@@ -322,6 +317,14 @@
|
|
|
322
317
|
{ "type": "boolean", "const": false }
|
|
323
318
|
]
|
|
324
319
|
},
|
|
320
|
+
"cssOwnership": {
|
|
321
|
+
"description": "Path to a CSS ownership contract (default discovery: .gutterpress/css-contract.yaml), or false to disable the source.css-ownership check. Optional — absent means no contract, so the check reports nothing.",
|
|
322
|
+
"default": null,
|
|
323
|
+
"oneOf": [
|
|
324
|
+
{ "type": "string", "minLength": 1 },
|
|
325
|
+
{ "type": "boolean", "const": false }
|
|
326
|
+
]
|
|
327
|
+
},
|
|
325
328
|
"allowedCallouts": {
|
|
326
329
|
"type": "array",
|
|
327
330
|
"deprecated": true,
|