gutterpress 0.11.15 → 0.11.16-alpha.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 +6 -0
- package/dist/{README-8b33qbjw.md → README-vsnsc1b3.md} +14 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +4 -6
- package/dist/{build-cbvapvgv.js → build-46bb205t.js} +4 -4
- package/dist/{cli-bhs0ssm2.js → cli-6tfc4ed6.js} +1 -1
- package/dist/{cli-mc3waxaw.js → cli-d55z7rtk.js} +3 -3
- package/dist/{cli-ar54jmc5.js → cli-p8xwsknv.js} +1 -1
- package/dist/{cli-s99rpea3.js → cli-sq0awkbf.js} +161 -7
- package/dist/cli.js +12 -12
- package/dist/{doctor-avq8vjva.js → doctor-tjatz81j.js} +2 -2
- package/dist/{engine-h8zdtxhp.js → engine-nv2xddnk.js} +1 -1
- package/dist/{engine-s18wy689.js → engine-szx34415.js} +2 -2
- package/dist/{ext-9tjaavrx.js → ext-ph0770mx.js} +4 -4
- package/dist/{index-ypxhxvwv.js → index-9ydc9q9q.js} +1 -1
- package/dist/{index-wj2dxga6.js → index-azdfgy3t.js} +245 -53
- package/dist/{index-w6tqhafk.js → index-eawjxgkn.js} +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -7
- package/dist/lib/markdown/assemble.d.ts +7 -0
- package/dist/lib/markdown/plugins.d.ts +1 -1
- package/dist/lib/markdown/renderer.d.ts +91 -0
- package/dist/lib/snippets.d.ts +34 -92
- package/dist/{new-cmmshb37.js → new-daeh9p9p.js} +4 -4
- package/dist/{plugin.js-aszz0zgh.tpl → plugin.js-z492w01r.tpl} +44 -0
- package/dist/{plugin.test.js-nw7c84s6.tpl → plugin.test.js-6n8ct5e3.tpl} +35 -1
- package/dist/{preflight-93xdp9jk.js → preflight-bfvw2qe5.js} +4 -4
- package/dist/{preview-dhjma9fn.js → preview-00298rvf.js} +4 -4
- package/dist/{publish-a28ajgqt.js → publish-jn9t18mr.js} +4 -4
- package/dist/render.js +156 -2
- package/dist/{validate-any9f0df.js → validate-cm1m5bbf.js} +4 -4
- package/package.json +1 -1
package/dist/lib/snippets.d.ts
CHANGED
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
/** Folder (relative to the project root) snippets live in. */
|
|
2
2
|
export declare const SNIPPETS_DIR = "snippets";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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: "extension", ref, name }` — a READ-ONLY snippet merged in from an
|
|
14
|
-
* installed, ENABLED extension (see {@link listInstalledExtensions}). `name`
|
|
15
|
-
* is the extension's display name — the picker's group label, so an author
|
|
16
|
-
* always sees WHICH extension a snippet came from, never just "not mine".
|
|
17
|
-
* `ref` is the extension's manifest specifier (`ProjectExtensionEntry.use`);
|
|
18
|
-
* it is round-tripped back into {@link readExtensionSnippet} so that function
|
|
19
|
-
* can re-derive the extension's folder itself from a small, validated
|
|
20
|
-
* identifier instead of trusting a filesystem path a caller could construct.
|
|
4
|
+
* Which level a snippet comes from. `project` is the book's own (the only
|
|
5
|
+
* level `saveSnippet`/`deleteSnippet` touch, so the only one the picker offers
|
|
6
|
+
* to delete); `extension` is read-only, from an enabled extension — `name` is
|
|
7
|
+
* its display name, `ref` its manifest specifier; `core` is reserved for
|
|
8
|
+
* snippets Gutterpress itself will ship.
|
|
21
9
|
*/
|
|
22
10
|
export type SnippetSource = {
|
|
23
11
|
kind: "project";
|
|
@@ -25,20 +13,24 @@ export type SnippetSource = {
|
|
|
25
13
|
kind: "extension";
|
|
26
14
|
ref: string;
|
|
27
15
|
name: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "core";
|
|
28
18
|
};
|
|
29
|
-
/** One snippet
|
|
19
|
+
/** One snippet, as the picker lists it. */
|
|
30
20
|
export interface SnippetEntry {
|
|
31
|
-
/** Display name (derived from the
|
|
21
|
+
/** Display name (derived from the filename stem, prettified). */
|
|
32
22
|
name: string;
|
|
33
|
-
/** The
|
|
34
|
-
*
|
|
35
|
-
* resolved back to a file via {@link readExtensionSnippet} (which also
|
|
36
|
-
* needs `source`), never via the project-only {@link readSnippet}. */
|
|
23
|
+
/** The file, relative to its level's snippets folder (an extension
|
|
24
|
+
* component's explicit `snippet:` path is relative to the extension). */
|
|
37
25
|
fileName: string;
|
|
38
26
|
/** Distinct `{{variable}}` names parsed from the body, in first-seen order. */
|
|
39
27
|
variables: string[];
|
|
40
|
-
/**
|
|
28
|
+
/** The snippet's text. */
|
|
29
|
+
body: string;
|
|
30
|
+
/** Which level it comes from — see {@link SnippetSource}. */
|
|
41
31
|
source: SnippetSource;
|
|
32
|
+
/** Set when this is the example snippet for that component (marker name). */
|
|
33
|
+
component?: string;
|
|
42
34
|
}
|
|
43
35
|
/**
|
|
44
36
|
* Parse the distinct `{{variable}}` placeholder names from a template, in the
|
|
@@ -52,19 +44,8 @@ export declare function extractVariables(template: string): string[];
|
|
|
52
44
|
* Pure.
|
|
53
45
|
*/
|
|
54
46
|
export declare function substituteVariables(template: string, values: Record<string, string>): string;
|
|
55
|
-
/**
|
|
56
|
-
* List the project's OWN snippets only — `<projectDir>/snippets/`, exactly as
|
|
57
|
-
* before #242. The picker itself now calls {@link listMergedSnippets} (which
|
|
58
|
-
* calls this as its first step); this stays exported and unchanged in
|
|
59
|
-
* behavior because it is independently useful (and independently tested) as
|
|
60
|
-
* "just the author's own snippets", with no extension-discovery cost paid by
|
|
61
|
-
* a caller that doesn't need it.
|
|
62
|
-
*/
|
|
47
|
+
/** The book's own snippets only — `<projectDir>/snippets/`. */
|
|
63
48
|
export declare function listSnippets(projectDir: string): Promise<SnippetEntry[]>;
|
|
64
|
-
/** Read one snippet's raw body. Refuses path traversal. Project snippets
|
|
65
|
-
* only — see {@link readExtensionSnippet} for the merged-list counterpart
|
|
66
|
-
* that reads an extension-provided entry instead. */
|
|
67
|
-
export declare function readSnippet(projectDir: string, fileName: string): Promise<string>;
|
|
68
49
|
/**
|
|
69
50
|
* Save a snippet body under `snippets/<slug(name)>.md`, creating the folder when
|
|
70
51
|
* absent. Returns the stored entry (with its filename + parsed variables). The
|
|
@@ -91,61 +72,22 @@ export declare function saveSnippet(projectDir: string, name: string, body: stri
|
|
|
91
72
|
*/
|
|
92
73
|
export declare function deleteSnippet(projectDir: string, fileName: string): Promise<void>;
|
|
93
74
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* `
|
|
97
|
-
*
|
|
98
|
-
* PRECEDENCE / collision (issue's suggested shape, point 2): when an
|
|
99
|
-
* extension snippet's FILENAME — the slugified identity `saveSnippet` itself
|
|
100
|
-
* derives a name into, so two different-cased spellings of the same name
|
|
101
|
-
* collide exactly as they would on a real re-save — matches a project
|
|
102
|
-
* snippet's, the project one wins outright and the extension's copy is
|
|
103
|
-
* dropped from this call's result. It is not renamed, not kept reachable
|
|
104
|
-
* under a second key, and nothing on disk is touched: the comparison and the
|
|
105
|
-
* drop happen freshly on every call, so the instant the author renames (or
|
|
106
|
-
* deletes) their colliding snippet, the extension's becomes visible again
|
|
107
|
-
* with no separate "restore" step. Rationale: the moment an author saves
|
|
108
|
-
* their own snippet under a name an extension already used, the natural
|
|
109
|
-
* reading is "I'm overriding this one for my project" — a picker entry that
|
|
110
|
-
* silently stays inserted from the extension forever after would contradict
|
|
111
|
-
* that, and a picker entry that just isn't there is a far smaller surprise
|
|
112
|
-
* than two identically-named rows the author has to guess between.
|
|
113
|
-
*
|
|
114
|
-
* This precedence rule is PROJECT-vs-EXTENSION only. Two different
|
|
115
|
-
* extensions that each happen to ship a same-named snippet are NOT
|
|
116
|
-
* deduplicated against each other — both survive, each under its own group
|
|
117
|
-
* header (see GROUPING below), because there is no ambiguity to resolve:
|
|
118
|
-
* unlike the project-vs-extension case, neither copy could be mistaken for
|
|
119
|
-
* "the author's own", so there is nothing here for one to silently win over.
|
|
120
|
-
*
|
|
121
|
-
* GROUPING: the result is ordered project-first (`listSnippets`'s own
|
|
122
|
-
* alphabetical order), then one contiguous run per extension — extensions
|
|
123
|
-
* alphabetical by display name, each run alphabetical by snippet name. The
|
|
124
|
-
* picker groups purely by noticing `source` change between consecutive
|
|
125
|
-
* entries; there is no separate grouped/tree shape to keep in sync with this
|
|
126
|
-
* flat list.
|
|
75
|
+
* One plugin-declared marker (a "component", `export const markers`) the
|
|
76
|
+
* project can use, with the example snippet the editor inserts for it.
|
|
77
|
+
* Unrelated to the `gutterpress.components` catalog file, which nothing reads.
|
|
127
78
|
*/
|
|
79
|
+
export interface MarkerComponent {
|
|
80
|
+
/** The marker name, without `@` — e.g. `term-box`. */
|
|
81
|
+
name: string;
|
|
82
|
+
/** The extension that declares it. */
|
|
83
|
+
source: Extract<SnippetSource, {
|
|
84
|
+
kind: "extension";
|
|
85
|
+
}>;
|
|
86
|
+
/** The winning example snippet's body (book, then extension, then core). */
|
|
87
|
+
snippet?: string;
|
|
88
|
+
}
|
|
89
|
+
/** Every snippet the project can insert, from every level — see {@link collectLibrary}. */
|
|
128
90
|
export declare function listMergedSnippets(projectDir: string): Promise<SnippetEntry[]>;
|
|
129
|
-
/**
|
|
130
|
-
*
|
|
131
|
-
|
|
132
|
-
* an extension `source`.
|
|
133
|
-
*
|
|
134
|
-
* Deliberately NOT a raw-path read: `source` carries only the same small,
|
|
135
|
-
* stable `{ kind, ref }` pair `listMergedSnippets` already handed back (see
|
|
136
|
-
* {@link SnippetSource}), and this function re-runs the EXACT SAME discovery
|
|
137
|
-
* {@link listMergedSnippets} used ({@link listInstalledExtensions}) to find
|
|
138
|
-
* the matching extension's folder again, rather than trusting any path a
|
|
139
|
-
* caller could construct directly — the same defense-in-depth stance
|
|
140
|
-
* `resolveSnippetPath` already takes for the project's own snippets, now
|
|
141
|
-
* extended to a second, per-extension root instead of a single project one.
|
|
142
|
-
*
|
|
143
|
-
* Throws when `source` no longer resolves to an installed, active extension
|
|
144
|
-
* (it was disabled or removed since the list was fetched — the picker's existing `error` display already handles a
|
|
145
|
-
* thrown read the same way a vanished project snippet would) or when
|
|
146
|
-
* `fileName` escapes that extension's snippets folder.
|
|
147
|
-
*/
|
|
148
|
-
export declare function readExtensionSnippet(projectDir: string, source: {
|
|
149
|
-
kind: "extension";
|
|
150
|
-
ref: string;
|
|
151
|
-
}, fileName: string): Promise<string>;
|
|
91
|
+
/** Every component the project's enabled extensions declare, each with its
|
|
92
|
+
* winning example snippet (book > extension > core) — see {@link collectLibrary}. */
|
|
93
|
+
export declare function listMarkerComponents(projectDir: string): Promise<MarkerComponent[]>;
|
|
@@ -7,16 +7,16 @@ import {
|
|
|
7
7
|
TARGET_IDS,
|
|
8
8
|
scaffoldExtension,
|
|
9
9
|
scaffoldProject
|
|
10
|
-
} from "./cli-
|
|
10
|
+
} from "./cli-sq0awkbf.js";
|
|
11
11
|
import {
|
|
12
12
|
resolveGhostscript
|
|
13
|
-
} from "./cli-
|
|
13
|
+
} from "./cli-6tfc4ed6.js";
|
|
14
14
|
import {
|
|
15
15
|
UsageError,
|
|
16
16
|
rejectExtraPositionals,
|
|
17
17
|
rejectUnknownFlags
|
|
18
|
-
} from "./cli-
|
|
19
|
-
import"./cli-
|
|
18
|
+
} from "./cli-p8xwsknv.js";
|
|
19
|
+
import"./cli-d55z7rtk.js";
|
|
20
20
|
import {
|
|
21
21
|
EXIT_CODES
|
|
22
22
|
} from "./cli-46ycxe6r.js";
|
|
@@ -67,6 +67,34 @@ const PREFIX = "{{PREFIX}}";
|
|
|
67
67
|
* variants extra classes keyed by the marker's bare word
|
|
68
68
|
* label a label element built from one of the marker's attributes
|
|
69
69
|
* autoCloseAt ["eof"] closes an unclosed container at end of file
|
|
70
|
+
* snippet example content the editor inserts for this component, as a
|
|
71
|
+
* path inside this folder (default: snippets/<name>.md — so
|
|
72
|
+
* `snippets/term-box.md` is term-box's example already)
|
|
73
|
+
* validate a function that checks what an author put inside the
|
|
74
|
+
* component and returns problems — see below
|
|
75
|
+
*
|
|
76
|
+
* VALIDATE — opt-in structure checks
|
|
77
|
+
*
|
|
78
|
+
* Gutterpress calls `validate(component)` for every use of the component
|
|
79
|
+
* whenever it checks a book (the desktop Problems panel, `gutterpress
|
|
80
|
+
* validate`, builds). `component` is a plain object — no markdown-it, no
|
|
81
|
+
* Gutterpress internals:
|
|
82
|
+
*
|
|
83
|
+
* component.name "term-box"
|
|
84
|
+
* component.variant the bare word after the marker ("note"), or null
|
|
85
|
+
* component.attrs the marker's attributes, e.g. { label: "…" }
|
|
86
|
+
* component.line the marker's line in the file
|
|
87
|
+
* component.text the raw markdown inside the component
|
|
88
|
+
* component.blocks its content, in order, each with `type` and `line`:
|
|
89
|
+
* heading {level, text} · paragraph {text} ·
|
|
90
|
+
* image {alt, src} · list {ordered, items} ·
|
|
91
|
+
* quote {text} · code {lang, text} · table · rule ·
|
|
92
|
+
* html {text} · component {name}
|
|
93
|
+
*
|
|
94
|
+
* Return nothing when it is fine, or a list of problems. A problem is a
|
|
95
|
+
* message string, or `{ message, line, severity }` to point at a line or
|
|
96
|
+
* choose "error" / "warning" (the default) / "info". The function must be
|
|
97
|
+
* synchronous; if it throws, the author sees that as a problem too.
|
|
70
98
|
*
|
|
71
99
|
* Names are validated when the book loads: lower-case letters, digits and
|
|
72
100
|
* hyphens; they may not start with `end-`, and they may not shadow a core
|
|
@@ -88,6 +116,22 @@ export const markers = {
|
|
|
88
116
|
from: "attr:label",
|
|
89
117
|
},
|
|
90
118
|
autoCloseAt: ["eof"],
|
|
119
|
+
// snippet: "examples/term-box.md", // only needed to override snippets/term-box.md
|
|
120
|
+
validate(box) {
|
|
121
|
+
const problems = [];
|
|
122
|
+
if (!box.attrs.label) {
|
|
123
|
+
problems.push('Give the term box a label: @term-box note label="Your term"');
|
|
124
|
+
}
|
|
125
|
+
if (!box.blocks.some((block) => block.type === "paragraph")) {
|
|
126
|
+
problems.push("Add a paragraph explaining the term.");
|
|
127
|
+
}
|
|
128
|
+
for (const block of box.blocks) {
|
|
129
|
+
if (block.type === "heading") {
|
|
130
|
+
problems.push({ message: "Use the label instead of a heading.", line: block.line });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return problems;
|
|
134
|
+
},
|
|
91
135
|
},
|
|
92
136
|
};
|
|
93
137
|
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
* package cannot import core (see README.md, "Why you cannot import
|
|
31
31
|
* gutterpress"). So the table's CONTRACT is checked here, and the rendered
|
|
32
32
|
* container is checked by running `gutterpress preview` on a real book.
|
|
33
|
+
* A component's `validate` needs no core at all — it takes a plain object —
|
|
34
|
+
* so it is tested directly, with hand-written components.
|
|
33
35
|
*/
|
|
34
36
|
import { describe, expect, test } from "bun:test";
|
|
35
37
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -212,6 +214,37 @@ describe("conventions", () => {
|
|
|
212
214
|
}
|
|
213
215
|
});
|
|
214
216
|
|
|
217
|
+
test("each component's snippet opens and closes its own marker", () => {
|
|
218
|
+
// The editor inserts this file when an author picks the component, so it
|
|
219
|
+
// is the example of the structure the component expects.
|
|
220
|
+
for (const [name, decl] of Object.entries(markers)) {
|
|
221
|
+
if (decl.deprecated !== undefined || decl.alias !== undefined) continue;
|
|
222
|
+
const rel = decl.snippet ?? `${pkg.gutterpress?.snippets ?? "snippets"}/${name}.md`;
|
|
223
|
+
if (!existsSync(path.join(root, rel))) continue;
|
|
224
|
+
const snippet = read(rel);
|
|
225
|
+
expect(snippet.trimStart().startsWith(`@${name}`)).toBe(true);
|
|
226
|
+
expect(snippet).toContain(`@end-${name}`);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("term-box's validate accepts a well-formed box and explains what a bad one is missing", () => {
|
|
231
|
+
// A component exactly as Gutterpress hands it to `validate`.
|
|
232
|
+
const good = {
|
|
233
|
+
name: "term-box",
|
|
234
|
+
variant: "note",
|
|
235
|
+
attrs: { label: "Gutter" },
|
|
236
|
+
line: 1,
|
|
237
|
+
text: "The space between two facing pages.",
|
|
238
|
+
blocks: [{ type: "paragraph", text: "The space between two facing pages.", line: 2 }],
|
|
239
|
+
};
|
|
240
|
+
expect(markers["term-box"].validate(good)).toEqual([]);
|
|
241
|
+
|
|
242
|
+
const bad = { ...good, attrs: {}, blocks: [{ type: "heading", level: 3, text: "Gutter", line: 2 }] };
|
|
243
|
+
const problems = markers["term-box"].validate(bad);
|
|
244
|
+
expect(problems).toHaveLength(3);
|
|
245
|
+
expect(problems).toContainEqual({ message: "Use the label instead of a heading.", line: 2 });
|
|
246
|
+
});
|
|
247
|
+
|
|
215
248
|
test("every public custom property carries the prefix too", () => {
|
|
216
249
|
// `--x` declared at :root is global. An unprefixed one would collide with
|
|
217
250
|
// the book's own tokens exactly as an unprefixed class would.
|
|
@@ -226,6 +259,7 @@ describe("conventions", () => {
|
|
|
226
259
|
// Gutterpress wraps this extension's CSS in `@layer ext.<name>` itself,
|
|
227
260
|
// in `extensions:` list order — see the header comment in
|
|
228
261
|
// styles/plugin.css. A layer declared here would only nest inside it.
|
|
229
|
-
|
|
262
|
+
// Rules only: that header comment itself mentions `@layer`.
|
|
263
|
+
expect(cssRules).not.toMatch(/@layer/);
|
|
230
264
|
});
|
|
231
265
|
});
|
|
@@ -2,16 +2,16 @@ import {
|
|
|
2
2
|
executeValidation,
|
|
3
3
|
publishTargetFor,
|
|
4
4
|
reportMissingTools
|
|
5
|
-
} from "./cli-
|
|
5
|
+
} from "./cli-sq0awkbf.js";
|
|
6
6
|
import {
|
|
7
7
|
log
|
|
8
|
-
} from "./cli-
|
|
8
|
+
} from "./cli-6tfc4ed6.js";
|
|
9
9
|
import {
|
|
10
10
|
UsageError,
|
|
11
11
|
rejectExtraPositionals,
|
|
12
12
|
rejectUnknownFlags
|
|
13
|
-
} from "./cli-
|
|
14
|
-
import"./cli-
|
|
13
|
+
} from "./cli-p8xwsknv.js";
|
|
14
|
+
import"./cli-d55z7rtk.js";
|
|
15
15
|
import {
|
|
16
16
|
EXIT_CODES
|
|
17
17
|
} from "./cli-46ycxe6r.js";
|
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
runBuild,
|
|
7
7
|
splitOutPath,
|
|
8
8
|
startPreviewServer
|
|
9
|
-
} from "./cli-
|
|
9
|
+
} from "./cli-sq0awkbf.js";
|
|
10
10
|
import {
|
|
11
11
|
log
|
|
12
|
-
} from "./cli-
|
|
12
|
+
} from "./cli-6tfc4ed6.js";
|
|
13
13
|
import {
|
|
14
14
|
UsageError,
|
|
15
15
|
parseFormat,
|
|
@@ -17,8 +17,8 @@ import {
|
|
|
17
17
|
rejectExtraPositionals,
|
|
18
18
|
rejectUnknownFlags,
|
|
19
19
|
resolvePort
|
|
20
|
-
} from "./cli-
|
|
21
|
-
import"./cli-
|
|
20
|
+
} from "./cli-p8xwsknv.js";
|
|
21
|
+
import"./cli-d55z7rtk.js";
|
|
22
22
|
import {
|
|
23
23
|
BuildError
|
|
24
24
|
} from "./cli-46ycxe6r.js";
|
|
@@ -11,17 +11,17 @@ import {
|
|
|
11
11
|
publishProviderFor,
|
|
12
12
|
resolvePublishFormat,
|
|
13
13
|
runPublish
|
|
14
|
-
} from "./cli-
|
|
14
|
+
} from "./cli-sq0awkbf.js";
|
|
15
15
|
import {
|
|
16
16
|
FileTokenStore,
|
|
17
17
|
log
|
|
18
|
-
} from "./cli-
|
|
18
|
+
} from "./cli-6tfc4ed6.js";
|
|
19
19
|
import {
|
|
20
20
|
UsageError,
|
|
21
21
|
rejectExtraPositionals,
|
|
22
22
|
rejectUnknownFlags
|
|
23
|
-
} from "./cli-
|
|
24
|
-
import"./cli-
|
|
23
|
+
} from "./cli-p8xwsknv.js";
|
|
24
|
+
import"./cli-d55z7rtk.js";
|
|
25
25
|
import {
|
|
26
26
|
EXIT_CODES
|
|
27
27
|
} from "./cli-46ycxe6r.js";
|
package/dist/render.js
CHANGED
|
@@ -172,6 +172,133 @@ function warn(env, line, type, message, marker) {
|
|
|
172
172
|
env.layoutWarnings = [];
|
|
173
173
|
env.layoutWarnings.push({ line, type, message, marker });
|
|
174
174
|
}
|
|
175
|
+
var COMPONENT_SEVERITIES = ["error", "warning", "info"];
|
|
176
|
+
function blockEnd(tokens, i) {
|
|
177
|
+
if (tokens[i].nesting !== 1)
|
|
178
|
+
return i;
|
|
179
|
+
let depth = 0;
|
|
180
|
+
for (let j = i;j < tokens.length; j++) {
|
|
181
|
+
depth += tokens[j].nesting;
|
|
182
|
+
if (depth === 0)
|
|
183
|
+
return j;
|
|
184
|
+
}
|
|
185
|
+
return tokens.length - 1;
|
|
186
|
+
}
|
|
187
|
+
function inlineText(tokens, from, to) {
|
|
188
|
+
const parts = [];
|
|
189
|
+
for (let k = from;k <= to; k++)
|
|
190
|
+
if (tokens[k].type === "inline")
|
|
191
|
+
parts.push(tokens[k].content);
|
|
192
|
+
return parts.join(`
|
|
193
|
+
`);
|
|
194
|
+
}
|
|
195
|
+
function soleImage(inline) {
|
|
196
|
+
const parts = (inline && inline.children || []).filter((c) => c.type !== "softbreak" && !(c.type === "text" && !c.content.trim()));
|
|
197
|
+
return parts.length === 1 && parts[0].type === "image" ? parts[0] : null;
|
|
198
|
+
}
|
|
199
|
+
function toComponentBlocks(tokens, fallbackLine) {
|
|
200
|
+
const blocks = [];
|
|
201
|
+
for (let i = 0;i < tokens.length; i++) {
|
|
202
|
+
const tok = tokens[i];
|
|
203
|
+
const end = blockEnd(tokens, i);
|
|
204
|
+
const line = tok.map ? tok.map[0] + 1 : tok.meta && tok.meta.line || fallbackLine;
|
|
205
|
+
switch (tok.type) {
|
|
206
|
+
case "heading_open":
|
|
207
|
+
blocks.push({ type: "heading", level: Number(tok.tag.slice(1)), text: inlineText(tokens, i, end), line });
|
|
208
|
+
break;
|
|
209
|
+
case "paragraph_open": {
|
|
210
|
+
const image = soleImage(tokens[i + 1]);
|
|
211
|
+
blocks.push(image ? { type: "image", alt: image.content, src: image.attrGet("src") || "", line } : { type: "paragraph", text: inlineText(tokens, i, end), line });
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
case "bullet_list_open":
|
|
215
|
+
case "ordered_list_open": {
|
|
216
|
+
const items = [];
|
|
217
|
+
for (let k = i + 1;k < end; k++) {
|
|
218
|
+
if (tokens[k].type !== "list_item_open" || tokens[k].level !== tok.level + 1)
|
|
219
|
+
continue;
|
|
220
|
+
const itemEnd = blockEnd(tokens, k);
|
|
221
|
+
const first = tokens.slice(k + 1, itemEnd).find((t) => t.type === "inline");
|
|
222
|
+
items.push(first ? first.content : "");
|
|
223
|
+
k = itemEnd;
|
|
224
|
+
}
|
|
225
|
+
blocks.push({ type: "list", ordered: tok.type === "ordered_list_open", items, line });
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
case "blockquote_open":
|
|
229
|
+
blocks.push({ type: "quote", text: inlineText(tokens, i, end), line });
|
|
230
|
+
break;
|
|
231
|
+
case "fence":
|
|
232
|
+
case "code_block":
|
|
233
|
+
blocks.push({ type: "code", lang: (tok.info || "").trim().split(/\s+/)[0] || "", text: tok.content, line });
|
|
234
|
+
break;
|
|
235
|
+
case "table_open":
|
|
236
|
+
blocks.push({ type: "table", line });
|
|
237
|
+
break;
|
|
238
|
+
case "hr":
|
|
239
|
+
blocks.push({ type: "rule", line });
|
|
240
|
+
break;
|
|
241
|
+
case "html_block":
|
|
242
|
+
blocks.push({ type: "html", text: tok.content, line });
|
|
243
|
+
break;
|
|
244
|
+
case "layout_component_open":
|
|
245
|
+
blocks.push({ type: "component", name: tok.meta.component, line });
|
|
246
|
+
break;
|
|
247
|
+
default:
|
|
248
|
+
blocks.push({ type: tok.type.replace(/_open$/, ""), line });
|
|
249
|
+
}
|
|
250
|
+
i = end;
|
|
251
|
+
}
|
|
252
|
+
return blocks;
|
|
253
|
+
}
|
|
254
|
+
function runComponentValidate(env, decl, meta, tokens, lines) {
|
|
255
|
+
const name = meta.component;
|
|
256
|
+
const line = meta.line || 0;
|
|
257
|
+
const report = (at, type, message, severity) => {
|
|
258
|
+
if (!env.layoutWarnings)
|
|
259
|
+
env.layoutWarnings = [];
|
|
260
|
+
env.layoutWarnings.push(severity ? { line: at, type, message, severity } : { line: at, type, message });
|
|
261
|
+
};
|
|
262
|
+
const failed = (why) => report(line, "component_validate_failed", `@${name}: plugin "${decl.validateOwner}"'s validate() ${why}`);
|
|
263
|
+
let lastLine = line;
|
|
264
|
+
for (const t of tokens)
|
|
265
|
+
if (t.map && t.map[1] > lastLine)
|
|
266
|
+
lastLine = t.map[1];
|
|
267
|
+
const component = {
|
|
268
|
+
name,
|
|
269
|
+
variant: meta.variant || null,
|
|
270
|
+
attrs: { ...meta.attrs },
|
|
271
|
+
line,
|
|
272
|
+
text: lines.slice(line, lastLine).join(`
|
|
273
|
+
`),
|
|
274
|
+
blocks: toComponentBlocks(tokens, line)
|
|
275
|
+
};
|
|
276
|
+
let result;
|
|
277
|
+
try {
|
|
278
|
+
result = decl.validate(component);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
failed(`threw: ${error instanceof Error ? error.message : String(error)}`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (result === undefined || result === null)
|
|
284
|
+
return;
|
|
285
|
+
if (typeof result === "string")
|
|
286
|
+
result = [result];
|
|
287
|
+
if (!Array.isArray(result)) {
|
|
288
|
+
failed("must return a message, an array of problems, or nothing (and must not be async).");
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
for (const entry of result) {
|
|
292
|
+
const problem = typeof entry === "string" ? { message: entry } : entry;
|
|
293
|
+
if (!problem || typeof problem.message !== "string" || !problem.message.trim()) {
|
|
294
|
+
failed("returned a problem without a message.");
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const at = Number.isInteger(problem.line) && problem.line > 0 ? problem.line : line;
|
|
298
|
+
const severity = COMPONENT_SEVERITIES.includes(problem.severity) ? problem.severity : undefined;
|
|
299
|
+
report(at, "component_invalid", `@${name}: ${problem.message}`, severity);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
175
302
|
function proseEscapeHint(kind) {
|
|
176
303
|
return ` If this line is prose that happens to begin with "@${kind}" — a wrapped sentence, for` + ` instance — escape it as \\@${kind} or write it as \`@${kind}\` in backticks, and it stays text.`;
|
|
177
304
|
}
|
|
@@ -274,13 +401,20 @@ function resolveContainerShape(name, pluginName, decl) {
|
|
|
274
401
|
}
|
|
275
402
|
autoCloseAtEof = decl.autoCloseAt.includes("eof");
|
|
276
403
|
}
|
|
277
|
-
|
|
404
|
+
if (decl.validate !== undefined && typeof decl.validate !== "function") {
|
|
405
|
+
throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`validate\` that is not a function.`);
|
|
406
|
+
}
|
|
407
|
+
const check = decl.validate ? { validate: decl.validate, validateOwner: pluginName } : {};
|
|
408
|
+
return { tag, classBase, variants, label, autoCloseAtEof, ...check };
|
|
278
409
|
}
|
|
279
410
|
function resolveMarkerDeclaration(name, rawDecl, rawRegistry, originOf) {
|
|
280
411
|
const pluginName = originOf.get(name);
|
|
281
412
|
if (typeof rawDecl !== "object" || rawDecl === null || Array.isArray(rawDecl)) {
|
|
282
413
|
throw new Error(`Plugin "${pluginName}"'s marker "@${name}" is not a plain object.`);
|
|
283
414
|
}
|
|
415
|
+
if (rawDecl.snippet !== undefined && (typeof rawDecl.snippet !== "string" || !rawDecl.snippet.trim())) {
|
|
416
|
+
throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`snippet\` that is not a non-empty path string.`);
|
|
417
|
+
}
|
|
284
418
|
if (rawDecl.deprecated !== undefined) {
|
|
285
419
|
if (typeof rawDecl.deprecated !== "string" || !rawDecl.deprecated) {
|
|
286
420
|
throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`deprecated\` value that is not a non-empty string.`);
|
|
@@ -495,8 +629,8 @@ function plugin(md, pluginOptions = {}) {
|
|
|
495
629
|
}
|
|
496
630
|
function openDeclaredMarker(meta, decl) {
|
|
497
631
|
const t = new state.Token("layout_component_open", decl.tag, 1);
|
|
498
|
-
t.meta = { line: meta.__line };
|
|
499
632
|
const variant = meta.name || decl.presetVariant || null;
|
|
633
|
+
t.meta = { line: meta.__line, component: meta.kind, variant, attrs: meta.attrs || {}, labelled: false };
|
|
500
634
|
const variantClass = variant && decl.variants && decl.variants[variant] || "";
|
|
501
635
|
const baseClass = [decl.classBase, variantClass].filter(Boolean).join(" ");
|
|
502
636
|
addClasses(t, baseClass, meta.attrs && meta.attrs.class ? meta.attrs.class : "");
|
|
@@ -510,6 +644,7 @@ function plugin(md, pluginOptions = {}) {
|
|
|
510
644
|
labelToken.content = `<${decl.label.tag} class="${escapeAttr(decl.label.class)}">${escapeHtml(value)}</${decl.label.tag}>
|
|
511
645
|
`;
|
|
512
646
|
out.push(labelToken);
|
|
647
|
+
t.meta.labelled = true;
|
|
513
648
|
}
|
|
514
649
|
}
|
|
515
650
|
}
|
|
@@ -771,6 +906,25 @@ function plugin(md, pluginOptions = {}) {
|
|
|
771
906
|
stack.closeAll();
|
|
772
907
|
state.tokens = out;
|
|
773
908
|
});
|
|
909
|
+
if (declaredMarkers && [...declaredMarkers.values()].some((d) => d.validate)) {
|
|
910
|
+
md.core.ruler.push("gp_component_validate", function(state) {
|
|
911
|
+
let lines = null;
|
|
912
|
+
const tokens = state.tokens;
|
|
913
|
+
for (let i = 0;i < tokens.length; i++) {
|
|
914
|
+
const tok = tokens[i];
|
|
915
|
+
if (tok.type !== "layout_component_open")
|
|
916
|
+
continue;
|
|
917
|
+
const decl = declaredMarkers.get(tok.meta.component);
|
|
918
|
+
if (!decl || !decl.validate)
|
|
919
|
+
continue;
|
|
920
|
+
if (!lines)
|
|
921
|
+
lines = state.src.split(`
|
|
922
|
+
`);
|
|
923
|
+
const from = i + (tok.meta.labelled ? 2 : 1);
|
|
924
|
+
runComponentValidate(state.env, decl, tok.meta, tokens.slice(from, blockEnd(tokens, i)), lines);
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
}
|
|
774
928
|
md.renderer.rules.layout_page_break = (tokens, idx) => {
|
|
775
929
|
const cls = tokens[idx].attrGet("class") || "gp-page-break";
|
|
776
930
|
const rangeAttr = tokens[idx].attrGet("data-source-range");
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
executeAndReport
|
|
3
|
-
} from "./cli-
|
|
3
|
+
} from "./cli-sq0awkbf.js";
|
|
4
4
|
import {
|
|
5
5
|
log
|
|
6
|
-
} from "./cli-
|
|
6
|
+
} from "./cli-6tfc4ed6.js";
|
|
7
7
|
import {
|
|
8
8
|
UsageError,
|
|
9
9
|
rejectExtraPositionals,
|
|
10
10
|
rejectUnknownFlags
|
|
11
|
-
} from "./cli-
|
|
12
|
-
import"./cli-
|
|
11
|
+
} from "./cli-p8xwsknv.js";
|
|
12
|
+
import"./cli-d55z7rtk.js";
|
|
13
13
|
import {
|
|
14
14
|
EXIT_CODES
|
|
15
15
|
} from "./cli-46ycxe6r.js";
|
package/package.json
CHANGED