gutterpress 0.10.7 → 0.10.8-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.
Files changed (59) hide show
  1. package/README.md +10 -3
  2. package/dist/README-aaqapam1.md +186 -0
  3. package/dist/README-mdq34cmb.md +155 -0
  4. package/dist/api/index.d.ts +6 -2
  5. package/dist/api/index.js +30 -2
  6. package/dist/{audit-19e2431j.js → audit-ckbmgzpk.js} +4 -4
  7. package/dist/base-ypqh9n7f.css +123 -0
  8. package/dist/book-ted3tzrp.css +58 -0
  9. package/dist/{build-gqqew2vr.js → build-sgpyjx5n.js} +4 -4
  10. package/dist/callout-hv56m16h.md +7 -0
  11. package/dist/{cli-s8q42x3r.js → cli-4vtgt2ng.js} +78 -2
  12. package/dist/{cli-pvk2s9d7.js → cli-pw6z15yv.js} +1 -1
  13. package/dist/{cli-h8ejbtyr.js → cli-v1ma3v2s.js} +1 -1
  14. package/dist/{cli-kzf64c2g.js → cli-wvw2msvn.js} +946 -401
  15. package/dist/cli.js +16 -16
  16. package/dist/components-0mmswvvm.yaml +51 -0
  17. package/dist/components-aam0fe2z.css +94 -0
  18. package/dist/{doctor-3ccz35yz.js → doctor-59dwzq4r.js} +2 -2
  19. package/dist/{engine-pb6h089e.js → engine-daad33qn.js} +1 -1
  20. package/dist/{engine-88mynx9a.js → engine-db8632w6.js} +2 -2
  21. package/dist/expected-ycmacem8.html +9 -0
  22. package/dist/fixture-2qnyqgb5.md +10 -0
  23. package/dist/gutterpress-m98e2w2d.json +8 -0
  24. package/dist/gutterpress-x2ycdsb8.json +17 -0
  25. package/dist/{index-837htyjy.js → index-9pbaznww.js} +78 -2
  26. package/dist/{index-r4ny4fyr.js → index-m00n6sz4.js} +1147 -523
  27. package/dist/{index-a0xpm188.js → index-rpt4vtby.js} +1 -1
  28. package/dist/index.js +31 -3
  29. package/dist/lib/build-runner.d.ts +52 -0
  30. package/dist/lib/extension-manifest.d.ts +164 -0
  31. package/dist/lib/extension-scaffold.d.ts +65 -0
  32. package/dist/lib/lint-runner.d.ts +12 -0
  33. package/dist/lib/markdown/assemble.d.ts +12 -0
  34. package/dist/lib/markdown/markers.d.ts +20 -0
  35. package/dist/lib/markdown/plugins.d.ts +1 -1
  36. package/dist/lib/markdown/renderer.d.ts +99 -0
  37. package/dist/lib/snippets.d.ts +121 -5
  38. package/dist/lib/theme-import.d.ts +8 -4
  39. package/dist/lib/theme-manager.d.ts +52 -39
  40. package/dist/lib/validation-exec.d.ts +12 -0
  41. package/dist/{lint-mpz7vfc8.js → lint-9xhz0bsw.js} +4 -4
  42. package/dist/{manifest.schema-kwnrwv8b.json → manifest.schema-1q35yxs9.json} +2 -2
  43. package/dist/{new-nbrpsw7t.js → new-5drcm5am.js} +111 -6
  44. package/dist/package-rm9d2tht.json +29 -0
  45. package/dist/page-rules-nsj3aw42.css +128 -0
  46. package/dist/page-templates-7md4ev7d.css +60 -0
  47. package/dist/plugin-m2w7g47v.css +114 -0
  48. package/dist/{plugin-gg5mk26h.js → plugin-sc6zfwre.js} +4 -4
  49. package/dist/plugin.js-aszz0zgh.tpl +172 -0
  50. package/dist/plugin.test.js-mv0ghv5n.tpl +232 -0
  51. package/dist/{preflight-jnmd4mg2.js → preflight-g31c4060.js} +4 -4
  52. package/dist/{preview-hsr9d12d.js → preview-w1pqpq2g.js} +4 -4
  53. package/dist/{publish-pcrrn6fd.js → publish-es72yhd8.js} +4 -4
  54. package/dist/render.js +275 -8
  55. package/dist/term-box-4y0w7y32.md +7 -0
  56. package/dist/{theme-6fa0yjzq.js → theme-msr4ctky.js} +4 -4
  57. package/dist/tokens-e4xk3x4v.css +86 -0
  58. package/dist/{validate-7n25fx0g.js → validate-tsgprng7.js} +4 -4
  59. package/package.json +1 -1
@@ -39,6 +39,76 @@ export interface GutterpressPluginMetadata {
39
39
  author?: string;
40
40
  keywords?: string[];
41
41
  }
42
+ /**
43
+ * A declared marker's structural-element label (#240) — a real child element
44
+ * injected as the container's first child, the same "structural element
45
+ * carrying the data as both text content and an attribute" shape
46
+ * `markers.js`'s own `.chapter-opener` uses for `@chapter`.
47
+ */
48
+ export interface GutterpressMarkerLabel {
49
+ /** Class on the injected label element. */
50
+ class: string;
51
+ /**
52
+ * Where the label text comes from. Only `"attr:<name>"` is supported today
53
+ * (the marker's own attribute of that name) — see `markers.js`'s
54
+ * `resolveContainerShape` for why the format leaves room to grow later
55
+ * instead of silently doing nothing for an unsupported value now.
56
+ */
57
+ from: string;
58
+ /** HTML tag for the injected label element. Defaults to `"div"`. */
59
+ tag?: string;
60
+ }
61
+ /**
62
+ * One entry in a plugin's declared `markers` table (#240 — "declarative
63
+ * container components in core"). Exactly one of three shapes:
64
+ *
65
+ * - a CONTAINER: `tag`/`class`/`variants`/`label`/`autoCloseAt`, any/all
66
+ * optional (a bare `{}` is a valid, if pointless, `<div>` wrapper);
67
+ * - an ALIAS: `alias` (another declared name) + optional `preset`;
68
+ * - a DEPRECATION: `deprecated` (a human-readable retirement message) —
69
+ * wins over every other field on the same entry, per
70
+ * `markers.js`'s `resolveMarkerDeclaration`.
71
+ *
72
+ * See `markers.js`'s header comment for a worked example and
73
+ * `buildDeclaredMarkerRegistry` for the full validation/resolution contract
74
+ * (collision rules, alias indirection, the auto-derived `@end-<name>`
75
+ * closer). This type exists so a plugin AUTHOR importing it type-only
76
+ * (CLAUDE.md §5) has a real shape to write against; core's own consumption
77
+ * of the data is plain, duck-typed JS, matching every other markdown-it
78
+ * plugin input.
79
+ */
80
+ export interface GutterpressMarkerDeclaration {
81
+ /** Wrapper element tag. Defaults to `"div"`. */
82
+ tag?: string;
83
+ /** Base class(es) on the wrapper, e.g. `"dc-alert"`. */
84
+ class?: string;
85
+ /**
86
+ * Extra class(es) keyed by the marker's own bare name/argument (its
87
+ * "variant" — `@callout warning` selects `variants.warning`), appended
88
+ * after `class`.
89
+ */
90
+ variants?: Record<string, string>;
91
+ label?: GutterpressMarkerLabel;
92
+ /**
93
+ * Boundaries at which an unclosed instance of this marker auto-closes
94
+ * WITHOUT a warning. `"eof"` is the only value implemented today — an
95
+ * unclosed container always auto-closes at end-of-document regardless
96
+ * (required for well-formed HTML across concatenated chapter files); this
97
+ * only controls whether that forced close is silent (declare it here) or
98
+ * warns (the default — most likely a forgotten `@end-<name>`).
99
+ */
100
+ autoCloseAt?: Array<"eof">;
101
+ /** This marker is sugar for another declared marker (must name a non-alias, non-deprecated entry). */
102
+ alias?: string;
103
+ /** With `alias`: defaults applied when the invocation line supplies none. */
104
+ preset?: {
105
+ variant?: string;
106
+ };
107
+ /** This marker is retired: using it (or its `@end-` form) warns with this message and is otherwise a no-op. */
108
+ deprecated?: string;
109
+ }
110
+ /** A plugin's full declared marker table — see {@link GutterpressMarkerDeclaration}. */
111
+ export type GutterpressMarkerTable = Record<string, GutterpressMarkerDeclaration>;
42
112
  /**
43
113
  * Full shape a plugin module may export. Only `default` is required.
44
114
  *
@@ -48,6 +118,7 @@ export interface GutterpressPluginMetadata {
48
118
  * export const metadata: GutterpressPluginMetadata = { name: 'my-plugin', version: '1.0.0' };
49
119
  * export const css = `.my-class { color: red; }`;
50
120
  * export const styles = ["./styles/components.css", "./styles/callouts.css"];
121
+ * export const markers: GutterpressMarkerTable = { callout: { tag: 'div', class: 'dc-alert' } };
51
122
  * ```
52
123
  */
53
124
  export interface GutterpressPluginExport {
@@ -66,6 +137,12 @@ export interface GutterpressPluginExport {
66
137
  * before the project's own stylesheets.
67
138
  */
68
139
  styles?: string[];
140
+ /**
141
+ * Declarative container components (#240) — data interpreted by CORE's
142
+ * marker parser (`markers.js`), the same relationship `css`/`styles`
143
+ * already have to the loader. See {@link GutterpressMarkerDeclaration}.
144
+ */
145
+ markers?: GutterpressMarkerTable;
69
146
  }
70
147
  /** Internal representation of a loaded plugin, ready for `md.use()`. */
71
148
  export interface LoadedPlugin {
@@ -80,6 +157,12 @@ export interface LoadedPlugin {
80
157
  * declared order. `undefined`/`[]` for a plugin that declares none.
81
158
  */
82
159
  styles?: string[];
160
+ /** #240 — the plugin's raw, as-authored `markers` export, unresolved (see
161
+ * {@link GutterpressPluginExport.markers}). `createMarkdownRenderer` merges
162
+ * every loaded plugin's table via `buildDeclaredMarkerRegistry` before
163
+ * `gutterpressMarkers` ever runs — this field is untouched by the loader,
164
+ * mirroring how `css` is carried through as an opaque string. */
165
+ markers?: GutterpressMarkerTable;
83
166
  options: Record<string, unknown>;
84
167
  }
85
168
  /**
@@ -135,6 +218,22 @@ export declare const BUILTIN_OPTIONAL_PLUGINS: Record<string, GutterpressPlugin>
135
218
  * primitive. A project using neither plugin still renders `> [!NOTE]` as a
136
219
  * literal blockquote, unchanged — this feature is opt-in, not a default.
137
220
  *
221
+ * #240 — before `gutterpressMarkers` is applied, every loaded plugin's
222
+ * declared `markers` table (if any) is merged into ONE registry via
223
+ * `buildDeclaredMarkerRegistry` (markers.js) — validating collisions against
224
+ * core's own reserved names and against each other, resolving alias/preset
225
+ * indirection — and handed to the marker plugin as
226
+ * `{ declaredMarkers }`. This is WHY the merge happens here rather than in
227
+ * the loader (`plugins.ts`): `createMarkdownRenderer` is the one place that
228
+ * already sees every loaded plugin together, and merging before
229
+ * `md.use(gutterpressMarkers, ...)` is what lets `@callout`/`@end-callout`
230
+ * be recognized by the SAME block-level grammar as `@section` from the very
231
+ * first parse, rather than needing a second pass. A plugin's own hand-
232
+ * written block rule (for authors who need more than the declarative table)
233
+ * is still registered later, in the customPlugins loop below, exactly as
234
+ * before #240 — declaring `markers` and writing a plain markdown-it plugin
235
+ * function are not mutually exclusive.
236
+ *
138
237
  * @param customPlugins - Optional array of custom plugins to load
139
238
  */
140
239
  export declare function createMarkdownRenderer(customPlugins?: LoadedPlugin[]): MarkdownIt;
@@ -1,13 +1,47 @@
1
1
  /** Folder (relative to the project root) snippets live in. */
2
2
  export declare const SNIPPETS_DIR = "snippets";
3
+ /**
4
+ * Where a merged-list entry came from (#242).
5
+ *
6
+ * `{ kind: "project" }` — the author's own snippet, from `<projectDir>/
7
+ * snippets/`. This is the ONLY provenance `saveSnippet`/`deleteSnippet` ever
8
+ * produce or touch, and therefore the ONLY provenance the picker may offer to
9
+ * edit or delete — a `source` this shape is the single flag the UI needs to
10
+ * gate those actions, so "can this be deleted" never drifts out of sync with
11
+ * "where did this come from" (one field, not two that could disagree).
12
+ *
13
+ * `{ kind: "plugin" | "theme", ref, name }` — a READ-ONLY snippet merged in
14
+ * from an installed, currently-ACTIVE extension (see
15
+ * {@link listInstalledExtensions} for exactly which extensions qualify).
16
+ * `name` is the extension's display name — the picker's group label, so an
17
+ * author always sees WHICH extension a snippet came from, never just "not
18
+ * mine". `ref` is the same stable identifier {@link listProjectPlugins} (a
19
+ * plugin's manifest `path`) or {@link getActiveTheme} (a theme's project id)
20
+ * already hand out; it is round-tripped back into {@link readExtensionSnippet}
21
+ * so that function can re-derive the extension's folder itself from a small,
22
+ * validated identifier instead of trusting a filesystem path a caller could
23
+ * construct.
24
+ */
25
+ export type SnippetSource = {
26
+ kind: "project";
27
+ } | {
28
+ kind: "plugin" | "theme";
29
+ ref: string;
30
+ name: string;
31
+ };
3
32
  /** One snippet's metadata for the picker (no body — read lazily). */
4
33
  export interface SnippetEntry {
5
34
  /** Display name (derived from the `.md` filename stem, prettified). */
6
35
  name: string;
7
- /** The on-disk filename, e.g. `callout.md`. Stable id for read/delete. */
36
+ /** The on-disk filename, e.g. `callout.md`. Stable id for read/delete
37
+ * WITHIN its own source — an extension entry's `fileName` is only ever
38
+ * resolved back to a file via {@link readExtensionSnippet} (which also
39
+ * needs `source`), never via the project-only {@link readSnippet}. */
8
40
  fileName: string;
9
41
  /** Distinct `{{variable}}` names parsed from the body, in first-seen order. */
10
42
  variables: string[];
43
+ /** Provenance (#242) — see {@link SnippetSource}. */
44
+ source: SnippetSource;
11
45
  }
12
46
  /**
13
47
  * Parse the distinct `{{variable}}` placeholder names from a template, in the
@@ -22,18 +56,100 @@ export declare function extractVariables(template: string): string[];
22
56
  */
23
57
  export declare function substituteVariables(template: string, values: Record<string, string>): string;
24
58
  /**
25
- * List the project's snippets (newest filesystem order is not guaranteed; sort
26
- * for the picker). Returns `[]` when the `snippets/` folder doesn't exist.
59
+ * List the project's OWN snippets only `<projectDir>/snippets/`, exactly as
60
+ * before #242. The picker itself now calls {@link listMergedSnippets} (which
61
+ * calls this as its first step); this stays exported and unchanged in
62
+ * behavior because it is independently useful (and independently tested) as
63
+ * "just the author's own snippets", with no extension-discovery cost paid by
64
+ * a caller that doesn't need it.
27
65
  */
28
66
  export declare function listSnippets(projectDir: string): Promise<SnippetEntry[]>;
29
- /** Read one snippet's raw body. Refuses path traversal. */
67
+ /** Read one snippet's raw body. Refuses path traversal. Project snippets
68
+ * only — see {@link readExtensionSnippet} for the merged-list counterpart
69
+ * that reads an extension-provided entry instead. */
30
70
  export declare function readSnippet(projectDir: string, fileName: string): Promise<string>;
31
71
  /**
32
72
  * Save a snippet body under `snippets/<slug(name)>.md`, creating the folder when
33
73
  * absent. Returns the stored entry (with its filename + parsed variables). The
34
74
  * returned `name` echoes the author-supplied name, while `fileName` is the
35
75
  * slugified storage name.
76
+ *
77
+ * #242: always writes to (and returns a `source` naming) the PROJECT's own
78
+ * folder — "Save selection as snippet" keeps writing to the project even
79
+ * when the picker is currently showing a merged list that includes
80
+ * extension-provided entries (the issue's suggested shape, point 4). There is
81
+ * no parameter that could redirect this into an extension's folder.
36
82
  */
37
83
  export declare function saveSnippet(projectDir: string, name: string, body: string): Promise<SnippetEntry>;
38
- /** Delete a snippet by filename. Refuses path traversal. */
84
+ /**
85
+ * Delete a snippet by filename. Refuses path traversal.
86
+ *
87
+ * #242: project snippets ONLY — `resolveSnippetPath` hard-scopes every path
88
+ * to `<projectDir>/snippets/`, so this function is structurally incapable of
89
+ * reaching into an installed extension's folder no matter what `fileName` a
90
+ * caller passes (there is no argument that names an extension at all). An
91
+ * extension's own snippet files are therefore never at risk from the
92
+ * picker's delete button — the safety is in the function signature, not in
93
+ * a check the picker has to remember to make.
94
+ */
39
95
  export declare function deleteSnippet(projectDir: string, fileName: string): Promise<void>;
96
+ /**
97
+ * The project's own snippets, merged with every installed-and-active
98
+ * extension's (#242) — this is "the snippet host" the picker actually calls;
99
+ * `listSnippets` above is now just its first ingredient.
100
+ *
101
+ * PRECEDENCE / collision (issue's suggested shape, point 2): when an
102
+ * extension snippet's FILENAME — the slugified identity `saveSnippet` itself
103
+ * derives a name into, so two different-cased spellings of the same name
104
+ * collide exactly as they would on a real re-save — matches a project
105
+ * snippet's, the project one wins outright and the extension's copy is
106
+ * dropped from this call's result. It is not renamed, not kept reachable
107
+ * under a second key, and nothing on disk is touched: the comparison and the
108
+ * drop happen freshly on every call, so the instant the author renames (or
109
+ * deletes) their colliding snippet, the extension's becomes visible again
110
+ * with no separate "restore" step. Rationale: the moment an author saves
111
+ * their own snippet under a name an extension already used, the natural
112
+ * reading is "I'm overriding this one for my project" — a picker entry that
113
+ * silently stays inserted from the extension forever after would contradict
114
+ * that, and a picker entry that just isn't there is a far smaller surprise
115
+ * than two identically-named rows the author has to guess between.
116
+ *
117
+ * This precedence rule is PROJECT-vs-EXTENSION only. Two different
118
+ * extensions that each happen to ship a same-named snippet are NOT
119
+ * deduplicated against each other — both survive, each under its own group
120
+ * header (see GROUPING below), because there is no ambiguity to resolve:
121
+ * unlike the project-vs-extension case, neither copy could be mistaken for
122
+ * "the author's own", so there is nothing here for one to silently win over.
123
+ *
124
+ * GROUPING: the result is ordered project-first (`listSnippets`'s own
125
+ * alphabetical order), then one contiguous run per extension — extensions
126
+ * alphabetical by display name, each run alphabetical by snippet name. The
127
+ * picker groups purely by noticing `source` change between consecutive
128
+ * entries; there is no separate grouped/tree shape to keep in sync with this
129
+ * flat list.
130
+ */
131
+ export declare function listMergedSnippets(projectDir: string): Promise<SnippetEntry[]>;
132
+ /**
133
+ * Read one extension-provided snippet's raw body (#242) — the read-only
134
+ * counterpart to `readSnippet` for entries `listMergedSnippets` tagged with
135
+ * an extension `source`.
136
+ *
137
+ * Deliberately NOT a raw-path read: `source` carries only the same small,
138
+ * stable `{ kind, ref }` pair `listMergedSnippets` already handed back (see
139
+ * {@link SnippetSource}), and this function re-runs the EXACT SAME discovery
140
+ * {@link listMergedSnippets} used ({@link listInstalledExtensions}) to find
141
+ * the matching extension's folder again, rather than trusting any path a
142
+ * caller could construct directly — the same defense-in-depth stance
143
+ * `resolveSnippetPath` already takes for the project's own snippets, now
144
+ * extended to a second, per-extension root instead of a single project one.
145
+ *
146
+ * Throws when `source` no longer resolves to an installed, active extension
147
+ * (it was disabled, uninstalled, or the theme was switched since the list
148
+ * was fetched — the picker's existing `error` display already handles a
149
+ * thrown read the same way a vanished project snippet would) or when
150
+ * `fileName` escapes that extension's snippets folder.
151
+ */
152
+ export declare function readExtensionSnippet(projectDir: string, source: {
153
+ kind: "plugin" | "theme";
154
+ ref: string;
155
+ }, fileName: string): Promise<string>;
@@ -40,11 +40,15 @@ export declare function classifyThemeCssFindings(findings: PrintSafeWarning[]):
40
40
  };
41
41
  /**
42
42
  * From a theme folder's relative file paths, return the ones that are neither
43
- * `theme.css`/`theme.json` nor a recognized bundled asset (font/image/css).
44
- * These trigger a non-fatal "unexpected extra files" warning the import
45
- * still copies the whole folder.
43
+ * `theme.css`/`theme.json`/`gutterpress.json`, a recognized bundled asset
44
+ * (font/image/css), nor one the metadata itself declared (#241
45
+ * `declaredExtras`: the extension's `markdown`/`components`/`snippets`
46
+ * entries, when present, passed as relative paths; a `snippets` folder
47
+ * suppresses everything under it, not just the exact entry). These trigger a
48
+ * non-fatal "unexpected extra files" warning — the import still copies the
49
+ * whole folder.
46
50
  */
47
- export declare function unexpectedThemeFiles(relPaths: string[]): string[];
51
+ export declare function unexpectedThemeFiles(relPaths: string[], declaredExtras?: string[]): string[];
48
52
  /**
49
53
  * Import a theme from a `.zip` package. Unzips in memory (fflate), rejects
50
54
  * unsafe paths and over-cap archives, locates the theme root (at the archive
@@ -1,44 +1,20 @@
1
+ import { type ExtensionMetadata, assertExtensionContained, extensionEngineStyleList } from "./extension-manifest.ts";
1
2
  /** Folder (relative to the project root) themes are copied into on apply/import. */
2
3
  export declare const THEMES_DIR = "themes";
3
4
  /** The built-in themes shipped as embedded assets (ids are folder names). */
4
5
  export declare const BUILT_IN_THEME_IDS: readonly ["clean-book", "zine", "technical-doc"];
5
6
  export type BuiltInThemeId = (typeof BUILT_IN_THEME_IDS)[number];
6
- /** Parsed `theme.json` metadata (every field optional in the file). */
7
- export interface ThemeMetadata {
8
- name?: string;
9
- author?: string;
10
- description?: string;
11
- /** Optional preview image path (relative to the theme folder). */
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;
41
- }
7
+ /**
8
+ * Parsed theme metadata (every field optional in the file) — #241: a theme is
9
+ * the degenerate "styles only" case of {@link ExtensionMetadata}, so this is
10
+ * now a plain alias rather than a separate type. A `theme.json` file only
11
+ * ever populates the fields this comment used to document in full
12
+ * (`name`/`author`/`description`/`preview`/`styles`/`engineStyles`/
13
+ * `tokensFile`, #239); the three newer fields (`markdown`/`components`/
14
+ * `snippets`) are only reachable via a `gutterpress.json` — see
15
+ * `extension-manifest.ts` for the complete, current field-by-field doc.
16
+ */
17
+ export type ThemeMetadata = ExtensionMetadata;
42
18
  /** Author-friendly metadata for one theme (built-in or project). */
43
19
  export interface ThemeInfo {
44
20
  /** Stable id (a built-in id, or a slug for imported/applied themes). */
@@ -70,6 +46,26 @@ export interface ThemeInfo {
70
46
  * fallback.
71
47
  */
72
48
  tokensFile: string;
49
+ /**
50
+ * #241 — declared-relative path to a markdown-it entry, when this theme's
51
+ * `gutterpress.json` names one (never possible via the legacy `theme.json`
52
+ * shape). Informational only: the theme verbs (list/apply/import/revert/
53
+ * remove) do not load or wire it — a full extension carrying both styles
54
+ * AND markdown installs via `gutterpress plugin add <folder>` instead (see
55
+ * `plugins.ts`'s `loadExtensionFromDir`), which resolves and applies this
56
+ * SAME field. Surfaced here so a theme card can at least show "this
57
+ * extension also has a plugin half" rather than staying silent about it.
58
+ */
59
+ markdown?: string;
60
+ /** #241 — declared-relative path to a component catalog file, informational
61
+ * only (same rationale as {@link markdown}); no catalog reader consumes
62
+ * this yet. */
63
+ components?: string;
64
+ /** #241 — declared-relative path to a snippets folder, informational only
65
+ * on THIS type (same rationale as {@link markdown} — the theme verbs
66
+ * don't load it). The active theme's copy of this field IS consumed,
67
+ * by `snippets.ts`'s `listMergedSnippets` (#242) via `getActiveTheme`. */
68
+ snippets?: string;
73
69
  }
74
70
  /** A built-in theme resolved to disk (extracted from the embedded assets). */
75
71
  export interface ResolvedTheme {
@@ -94,10 +90,17 @@ export type ApplyThemeTarget = {
94
90
  * satisfies, so nothing needs migrating. Exported for `theme-import.ts`,
95
91
  * which validates every one of these (existence + print-safety) at import
96
92
  * time, the same way it always validated the lone `theme.css`.
93
+ *
94
+ * #241: layers that theme-only default on top of `extension-manifest.ts`'s
95
+ * {@link extensionStyleList}, which does NOT apply it (a markdown-only
96
+ * extension has no reason to require a `theme.css` it never declared) — one
97
+ * shared "read the declared list" body, two defaulting rules.
97
98
  */
98
99
  export declare function themeStyleList(meta: ThemeMetadata): string[];
99
100
  /**
100
- * Every declared sheet must live INSIDE the theme folder. A theme is
101
+ * Every declared sheet (styles/engineStyles #241: now also `markdown`/
102
+ * `components`/`snippets`/`tokensFile`, since a `gutterpress.json`-formatted
103
+ * theme can declare them too) must live INSIDE the theme folder. A theme is
101
104
  * self-contained by contract (apply copies the whole folder), and an imported
102
105
  * package is untrusted input: a `../` or absolute entry would make apply read
103
106
  * a file from anywhere on disk into the book.
@@ -109,16 +112,26 @@ export declare function themeStyleList(meta: ThemeMetadata): string[];
109
112
  * `getPreviousTheme`, `readThemeCss`) also call: one hand-edited theme.json
110
113
  * must not take down listing every theme, the same reason `readThemeMeta`
111
114
  * returns `{}` for an unparseable file instead of throwing.
115
+ *
116
+ * #241 — a straight re-export of the extension-generic
117
+ * {@link assertExtensionContained}: a theme's containment rule is not a
118
+ * separate implementation, it IS the extension rule, applied to a metadata
119
+ * object that (for a `theme.json`-only folder) never has the three newer
120
+ * fields populated in the first place.
112
121
  */
113
- export declare function assertThemeSheetsContained(meta: ThemeMetadata): void;
122
+ export declare const assertThemeSheetsContained: typeof assertExtensionContained;
114
123
  /**
115
124
  * A theme's declared engine-conditional sheets, relative to its folder
116
125
  * (#239). `Array.isArray` guards a malformed `engineStyles.native` (authored
117
126
  * as something other than a list) — treated as "none declared" rather than a
118
127
  * hard crash on a JSON author's typo. Exported for `theme-import.ts`'s import
119
128
  * validation, same reuse rationale as {@link themeStyleList}.
129
+ *
130
+ * #241 — a straight alias: `engineStyles.native` never had a theme-specific
131
+ * default to layer on, so this is byte-identical to
132
+ * {@link extensionEngineStyleList}.
120
133
  */
121
- export declare function themeEngineStyleList(meta: ThemeMetadata): string[];
134
+ export declare const themeEngineStyleList: typeof extensionEngineStyleList;
122
135
  /** List the built-in themes (metadata read from the extracted embedded assets). */
123
136
  export declare function listBuiltInThemes(): Promise<ThemeInfo[]>;
124
137
  /** Resolve a built-in theme to its extracted `theme.css` + parsed metadata. */
@@ -14,6 +14,18 @@ export interface ValidationExecutionArgs {
14
14
  phase?: string;
15
15
  /** Publish-target ids (CSV) to validate against, overriding the manifest's `targets:` for this run. */
16
16
  target?: string;
17
+ /**
18
+ * Pre-loaded, absolute plugin `styles` file paths (#262) — supplied by the
19
+ * build pipeline's preValidate gate (`build-runner.ts`'s
20
+ * `runQualityGates`, via `loadBuildPlugins`) so this run does not load
21
+ * plugins itself when the build already has. `undefined` (the default —
22
+ * every standalone `validate`/`preflight`/`audit` invocation, and the
23
+ * desktop Problems panel) makes {@link executeValidation} load plugins
24
+ * itself, degrade-and-report, exactly like `gutterpress lint`
25
+ * (lint-runner.ts). An explicit `[]` is honored as-is, not treated as
26
+ * "unset".
27
+ */
28
+ pluginStylePaths?: string[];
17
29
  }
18
30
  export interface ValidationExecutionResult {
19
31
  config: ResolvedConfig;
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  MANIFEST_FILENAMES,
3
3
  runLint
4
- } from "./cli-kzf64c2g.js";
4
+ } from "./cli-wvw2msvn.js";
5
5
  import {
6
6
  log
7
- } from "./cli-pvk2s9d7.js";
7
+ } from "./cli-pw6z15yv.js";
8
8
  import {
9
9
  UsageError,
10
10
  rejectExtraPositionals,
11
11
  rejectUnknownFlags
12
- } from "./cli-h8ejbtyr.js";
13
- import"./cli-s8q42x3r.js";
12
+ } from "./cli-v1ma3v2s.js";
13
+ import"./cli-4vtgt2ng.js";
14
14
  import"./cli-c41yr7he.js";
15
15
  import {
16
16
  EXIT_CODES
@@ -88,8 +88,8 @@
88
88
  "path": {
89
89
  "type": "string",
90
90
  "minLength": 1,
91
- "description": "Path to a local plugin module, relative to the manifest directory.",
92
- "examples": ["plugins/dimm-city-plugin.js", "../shared/plugins/callouts.js"]
91
+ "description": "Path to a local plugin module OR an extension folder (a gutterpress.json package, optionally carrying styles/engineStyles alongside its markdown entry), relative to the manifest directory.",
92
+ "examples": ["plugins/dimm-city-plugin.js", "../shared/plugins/callouts.js", "./extensions/dc-components"]
93
93
  },
94
94
  "name": {
95
95
  "type": "string",
@@ -1,20 +1,22 @@
1
1
  import {
2
2
  BUILT_IN_TEMPLATE_IDS,
3
+ EXTENSION_KINDS,
3
4
  PRESETS,
4
5
  PRESET_IDS,
5
6
  TARGETS,
6
7
  TARGET_IDS,
8
+ scaffoldExtension,
7
9
  scaffoldProject
8
- } from "./cli-kzf64c2g.js";
10
+ } from "./cli-wvw2msvn.js";
9
11
  import {
10
12
  resolveGhostscript
11
- } from "./cli-pvk2s9d7.js";
13
+ } from "./cli-pw6z15yv.js";
12
14
  import {
13
15
  UsageError,
14
16
  rejectExtraPositionals,
15
17
  rejectUnknownFlags
16
- } from "./cli-h8ejbtyr.js";
17
- import"./cli-s8q42x3r.js";
18
+ } from "./cli-v1ma3v2s.js";
19
+ import"./cli-4vtgt2ng.js";
18
20
  import {
19
21
  isToolAvailable
20
22
  } from "./cli-c41yr7he.js";
@@ -31,9 +33,21 @@ import { resolve } from "node:path";
31
33
  var newArgs = {
32
34
  name: {
33
35
  type: "positional",
34
- description: "Project name (becomes the title and folder name)",
36
+ description: "Name (becomes the title/package name and the folder name)",
35
37
  required: true
36
38
  },
39
+ kind: {
40
+ type: "string",
41
+ description: `What to create: book (default), ${EXTENSION_KINDS.join(", ")}`
42
+ },
43
+ prefix: {
44
+ type: "string",
45
+ description: 'Class/custom-property prefix an extension claims (default: its slug, e.g. "field-notes-"); --kind plugin|theme only'
46
+ },
47
+ description: {
48
+ type: "string",
49
+ description: "One-line description recorded in the extension's metadata; --kind plugin|theme only"
50
+ },
37
51
  preset: {
38
52
  type: "string",
39
53
  description: `Vendor preset the book is designed for: ${PRESET_IDS.join(", ")} (required; custom also needs --page-width/--page-height)`
@@ -76,6 +90,41 @@ var newArgs = {
76
90
  default: true
77
91
  }
78
92
  };
93
+ function flagsPassed(rawArgs) {
94
+ const seen = new Set;
95
+ for (const token of rawArgs) {
96
+ if (token === "--")
97
+ break;
98
+ if (!token.startsWith("--") || token.length <= 2)
99
+ continue;
100
+ const equalsAt = token.indexOf("=");
101
+ let name = equalsAt === -1 ? token.slice(2) : token.slice(2, equalsAt);
102
+ name = name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
103
+ if (name.startsWith("no-"))
104
+ name = name.slice(3);
105
+ seen.add(name);
106
+ }
107
+ return seen;
108
+ }
109
+ var BOOK_ONLY_FLAGS = [
110
+ "preset",
111
+ "targets",
112
+ "template",
113
+ "page-width",
114
+ "page-height",
115
+ "page-tolerance",
116
+ "git"
117
+ ];
118
+ var EXTENSION_ONLY_FLAGS = ["prefix", "description"];
119
+ function rejectFlagsForKind(rawArgs, kind) {
120
+ const passed = flagsPassed(rawArgs);
121
+ const inapplicable = (kind === "book" ? EXTENSION_ONLY_FLAGS : BOOK_ONLY_FLAGS).filter((f) => passed.has(f));
122
+ if (inapplicable.length === 0)
123
+ return;
124
+ const list = inapplicable.map((f) => `--${f}`).join(", ");
125
+ console.error(kind === "book" ? `${list} ${inapplicable.length > 1 ? "are" : "is"} only meaningful for an extension. ` + `Add --kind ${EXTENSION_KINDS.join(" or --kind ")}, or drop ${inapplicable.length > 1 ? "them" : "it"}.` : `${list} ${inapplicable.length > 1 ? "describe a book, not" : "describes a book, not"} a ${kind}. ` + `A ${kind} has no trim size, publish target or starter template — drop ${inapplicable.length > 1 ? "them" : "it"}.`);
126
+ process.exit(EXIT_CODES.USAGE);
127
+ }
79
128
  function parsePoints(raw, flag) {
80
129
  if (typeof raw !== "string" || raw === "")
81
130
  return;
@@ -86,10 +135,55 @@ function parsePoints(raw, flag) {
86
135
  }
87
136
  return value;
88
137
  }
138
+ function optionalString(value) {
139
+ return typeof value === "string" && value ? value : undefined;
140
+ }
141
+ async function runExtensionScaffold(kind, name, parentDir, args) {
142
+ try {
143
+ const result = await scaffoldExtension({
144
+ name,
145
+ kind,
146
+ parentDir,
147
+ folderName: optionalString(args.folder),
148
+ prefix: optionalString(args.prefix),
149
+ author: optionalString(args.author),
150
+ description: optionalString(args.description)
151
+ });
152
+ console.log(`Created ${kind}: ${result.extensionDir}`);
153
+ console.log(` metadata: ${result.manifestPath}`);
154
+ console.log(` class prefix: ${result.prefix}`);
155
+ console.log(` files: ${result.files.length}`);
156
+ console.log(` start editing in: ${result.openFile}`);
157
+ console.log("");
158
+ if (kind === "plugin") {
159
+ console.log("Next: check it still works —");
160
+ console.log(` cd ${result.slug} && bun install && bun test`);
161
+ console.log("");
162
+ console.log(" then load it from a book's manifest.yaml:");
163
+ console.log(" plugins:");
164
+ console.log(` - path: plugins/${result.slug}`);
165
+ console.log("");
166
+ console.log(" (point `path` at the FOLDER, not plugin.js — that is what makes");
167
+ console.log(" Gutterpress read gutterpress.json and pick up the stylesheet too.)");
168
+ } else {
169
+ console.log("Next: install it into a book —");
170
+ console.log(` gutterpress theme import ${result.extensionDir} <book>`);
171
+ console.log(` gutterpress theme apply ${result.slug} <book>`);
172
+ console.log("");
173
+ console.log(" Each stylesheet opens with the OWNS / MUST NOT CONTAIN header that");
174
+ console.log(" says which rules belong in it. Start with styles/tokens.css.");
175
+ }
176
+ } catch (e) {
177
+ const err = e;
178
+ const code = err && typeof err.code === "string" ? err.code : "scaffold-io";
179
+ console.error(`Could not create ${kind}: ${err?.message ?? String(e)}`);
180
+ process.exit(code === "scaffold-io" ? EXIT_CODES.PIPELINE : EXIT_CODES.USAGE);
181
+ }
182
+ }
89
183
  var new_default = defineCommand({
90
184
  meta: {
91
185
  name: "new",
92
- description: "Create a new Gutterpress project from a starter template"
186
+ description: "Create a new Gutterpress book, plugin or theme from a starter template"
93
187
  },
94
188
  args: newArgs,
95
189
  async run({ args, rawArgs }) {
@@ -105,6 +199,17 @@ var new_default = defineCommand({
105
199
  }
106
200
  const name = String(args.name);
107
201
  const parentDir = resolve(typeof args.dir === "string" && args.dir ? args.dir : process.cwd());
202
+ const rawKind = typeof args.kind === "string" && args.kind ? args.kind : "book";
203
+ if (rawKind !== "book" && !EXTENSION_KINDS.includes(rawKind)) {
204
+ console.error(`Unknown kind "${rawKind}". Choose one of: book, ${EXTENSION_KINDS.join(", ")}.`);
205
+ process.exit(EXIT_CODES.USAGE);
206
+ }
207
+ const kind = rawKind;
208
+ rejectFlagsForKind(rawArgs, kind);
209
+ if (kind !== "book") {
210
+ await runExtensionScaffold(kind, name, parentDir, args);
211
+ return;
212
+ }
108
213
  let template;
109
214
  if (typeof args.template === "string" && args.template) {
110
215
  if (!BUILT_IN_TEMPLATE_IDS.includes(args.template)) {