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.
- package/README.md +10 -3
- package/dist/README-aaqapam1.md +186 -0
- package/dist/README-mdq34cmb.md +155 -0
- package/dist/api/index.d.ts +6 -2
- package/dist/api/index.js +30 -2
- package/dist/{audit-19e2431j.js → audit-ckbmgzpk.js} +4 -4
- package/dist/base-ypqh9n7f.css +123 -0
- package/dist/book-ted3tzrp.css +58 -0
- package/dist/{build-gqqew2vr.js → build-sgpyjx5n.js} +4 -4
- package/dist/callout-hv56m16h.md +7 -0
- package/dist/{cli-s8q42x3r.js → cli-4vtgt2ng.js} +78 -2
- package/dist/{cli-pvk2s9d7.js → cli-pw6z15yv.js} +1 -1
- package/dist/{cli-h8ejbtyr.js → cli-v1ma3v2s.js} +1 -1
- package/dist/{cli-kzf64c2g.js → cli-wvw2msvn.js} +946 -401
- package/dist/cli.js +16 -16
- package/dist/components-0mmswvvm.yaml +51 -0
- package/dist/components-aam0fe2z.css +94 -0
- package/dist/{doctor-3ccz35yz.js → doctor-59dwzq4r.js} +2 -2
- package/dist/{engine-pb6h089e.js → engine-daad33qn.js} +1 -1
- package/dist/{engine-88mynx9a.js → engine-db8632w6.js} +2 -2
- package/dist/expected-ycmacem8.html +9 -0
- package/dist/fixture-2qnyqgb5.md +10 -0
- package/dist/gutterpress-m98e2w2d.json +8 -0
- package/dist/gutterpress-x2ycdsb8.json +17 -0
- package/dist/{index-837htyjy.js → index-9pbaznww.js} +78 -2
- package/dist/{index-r4ny4fyr.js → index-m00n6sz4.js} +1147 -523
- package/dist/{index-a0xpm188.js → index-rpt4vtby.js} +1 -1
- package/dist/index.js +31 -3
- package/dist/lib/build-runner.d.ts +52 -0
- package/dist/lib/extension-manifest.d.ts +164 -0
- package/dist/lib/extension-scaffold.d.ts +65 -0
- package/dist/lib/lint-runner.d.ts +12 -0
- package/dist/lib/markdown/assemble.d.ts +12 -0
- package/dist/lib/markdown/markers.d.ts +20 -0
- package/dist/lib/markdown/plugins.d.ts +1 -1
- package/dist/lib/markdown/renderer.d.ts +99 -0
- package/dist/lib/snippets.d.ts +121 -5
- package/dist/lib/theme-import.d.ts +8 -4
- package/dist/lib/theme-manager.d.ts +52 -39
- package/dist/lib/validation-exec.d.ts +12 -0
- package/dist/{lint-mpz7vfc8.js → lint-9xhz0bsw.js} +4 -4
- package/dist/{manifest.schema-kwnrwv8b.json → manifest.schema-1q35yxs9.json} +2 -2
- package/dist/{new-nbrpsw7t.js → new-5drcm5am.js} +111 -6
- package/dist/package-rm9d2tht.json +29 -0
- package/dist/page-rules-nsj3aw42.css +128 -0
- package/dist/page-templates-7md4ev7d.css +60 -0
- package/dist/plugin-m2w7g47v.css +114 -0
- package/dist/{plugin-gg5mk26h.js → plugin-sc6zfwre.js} +4 -4
- package/dist/plugin.js-aszz0zgh.tpl +172 -0
- package/dist/plugin.test.js-mv0ghv5n.tpl +232 -0
- package/dist/{preflight-jnmd4mg2.js → preflight-g31c4060.js} +4 -4
- package/dist/{preview-hsr9d12d.js → preview-w1pqpq2g.js} +4 -4
- package/dist/{publish-pcrrn6fd.js → publish-es72yhd8.js} +4 -4
- package/dist/render.js +275 -8
- package/dist/term-box-4y0w7y32.md +7 -0
- package/dist/{theme-6fa0yjzq.js → theme-msr4ctky.js} +4 -4
- package/dist/tokens-e4xk3x4v.css +86 -0
- package/dist/{validate-7n25fx0g.js → validate-tsgprng7.js} +4 -4
- 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;
|
package/dist/lib/snippets.d.ts
CHANGED
|
@@ -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
|
|
26
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
4
|
+
} from "./cli-wvw2msvn.js";
|
|
5
5
|
import {
|
|
6
6
|
log
|
|
7
|
-
} from "./cli-
|
|
7
|
+
} from "./cli-pw6z15yv.js";
|
|
8
8
|
import {
|
|
9
9
|
UsageError,
|
|
10
10
|
rejectExtraPositionals,
|
|
11
11
|
rejectUnknownFlags
|
|
12
|
-
} from "./cli-
|
|
13
|
-
import"./cli-
|
|
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-
|
|
10
|
+
} from "./cli-wvw2msvn.js";
|
|
9
11
|
import {
|
|
10
12
|
resolveGhostscript
|
|
11
|
-
} from "./cli-
|
|
13
|
+
} from "./cli-pw6z15yv.js";
|
|
12
14
|
import {
|
|
13
15
|
UsageError,
|
|
14
16
|
rejectExtraPositionals,
|
|
15
17
|
rejectUnknownFlags
|
|
16
|
-
} from "./cli-
|
|
17
|
-
import"./cli-
|
|
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: "
|
|
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
|
|
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)) {
|