@rse/specbook 1.3.0 → 1.3.2
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/dst/specbook-api.d.ts +13 -16
- package/dst/specbook-api.js +34 -18
- package/dst/specbook-cli.js +8 -6
- package/dst/specbook-cmd-describe.js +11 -7
- package/dst/specbook-cmd-export.js +6 -0
- package/dst/specbook-cmd-preview.d.ts +1 -0
- package/dst/specbook-cmd-preview.js +5 -4
- package/dst/specbook-config.js +11 -6
- package/dst/specbook-diagnostic.js +1 -1
- package/dst/specbook-diagram.js +74 -58
- package/dst/specbook-export-common.d.ts +2 -2
- package/dst/specbook-export-common.js +29 -18
- package/dst/specbook-export-html-search.js +10 -4
- package/dst/specbook-export-html.css +21 -21
- package/dst/specbook-export-html.js +102 -71
- package/dst/specbook-export-image.js +9 -1
- package/dst/specbook-export-md.js +3 -3
- package/dst/specbook-export-pdf.js +12 -6
- package/dst/specbook-ignore.js +35 -18
- package/dst/specbook-link.js +2 -2
- package/dst/specbook-mcp.js +5 -4
- package/dst/specbook-parse-common.js +6 -3
- package/dst/specbook-parse-semantic.js +8 -9
- package/dst/specbook-parse-syntax.js +13 -4
- package/dst/specbook-project.js +16 -3
- package/dst/specbook-theme.js +6 -2
- package/dst/specbook-verbose.js +3 -2
- package/package.json +10 -10
|
@@ -142,11 +142,11 @@ export const renderMarkdown = async (specification, config, rebase, verbose) =>
|
|
|
142
142
|
};
|
|
143
143
|
/* embed and re-base on clones of the objects (as the AST is shared
|
|
144
144
|
with the other exports), moving the diagrams along */
|
|
145
|
-
const
|
|
145
|
+
const embedObjectMd = (object, from, to) => {
|
|
146
146
|
const clone = {
|
|
147
147
|
...object,
|
|
148
148
|
properties: object.properties.map((property) => ({ ...property, value: embedMd(property.value, property.embedding ?? [], from, to) })),
|
|
149
|
-
children: object.children.map((child) =>
|
|
149
|
+
children: object.children.map((child) => embedObjectMd(child, from, to))
|
|
150
150
|
};
|
|
151
151
|
if (object.description !== undefined)
|
|
152
152
|
clone.description = { ...object.description,
|
|
@@ -160,7 +160,7 @@ export const renderMarkdown = async (specification, config, rebase, verbose) =>
|
|
|
160
160
|
const origin = rebase?.origins.get(artifact);
|
|
161
161
|
const from = origin !== undefined ? path.resolve(path.dirname(origin)) : undefined;
|
|
162
162
|
const to = rebase !== undefined ? path.resolve(rebase.dir) : undefined;
|
|
163
|
-
return artifact.objects.map((object) =>
|
|
163
|
+
return artifact.objects.map((object) => embedObjectMd(object, from, to));
|
|
164
164
|
});
|
|
165
165
|
const definitions = Array.from(labels, ([url, label]) => `[${label}]: ${url}\n`).join("");
|
|
166
166
|
/* the single frontmatter block (only recognized at the start of a
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
5
|
*/
|
|
6
|
-
import fs from "node:fs";
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
7
|
import { escapeHtml, paperSetup, paperLength, paperSizeDefault } from "./specbook-export-common.js";
|
|
8
8
|
import { literal } from "./specbook-verbose.js";
|
|
9
9
|
/* extract the per-anchor page numbers from a Chromium-generated PDF,
|
|
@@ -130,9 +130,10 @@ const drawBrandBar = async (doc, accent) => {
|
|
|
130
130
|
height: page.getHeight(), color: rgb(r, g, b) });
|
|
131
131
|
};
|
|
132
132
|
/* the launch options of the Chromium-class browser printing the PDF,
|
|
133
|
-
resolved just once per process
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
resolved just once per process (a failed resolution excepted, so a
|
|
134
|
+
long-running process retries after the remedy), as probing a
|
|
135
|
+
system-installed Google Chrome has to actually launch it and as the
|
|
136
|
+
export preflight and the renderer both ask for the very same browser */
|
|
136
137
|
let browserOptions;
|
|
137
138
|
/* the remedy of an unavailable Playwright Chromium, shared by the
|
|
138
139
|
fallback notice and the missing-browser error, as both have to point
|
|
@@ -191,10 +192,15 @@ const resolveBrowser = async (verbose) => {
|
|
|
191
192
|
inside the rendering and with a Playwright-internal message */
|
|
192
193
|
export const requireBrowser = async (verbose) => {
|
|
193
194
|
browserOptions ??= resolveBrowser(verbose);
|
|
194
|
-
const options = await browserOptions
|
|
195
|
-
|
|
195
|
+
const options = await browserOptions.catch((err) => {
|
|
196
|
+
browserOptions = undefined;
|
|
197
|
+
throw err;
|
|
198
|
+
});
|
|
199
|
+
if (options === undefined) {
|
|
200
|
+
browserOptions = undefined;
|
|
196
201
|
throw new Error("the PDF export requires a Chromium-class browser, but neither the " +
|
|
197
202
|
`Playwright Chromium nor a system-installed Google Chrome was found -- ${chromiumRemedy}`);
|
|
203
|
+
}
|
|
198
204
|
return options;
|
|
199
205
|
};
|
|
200
206
|
/* the bound of the ToC page number fixpoint iteration, as the page
|
package/dst/specbook-ignore.js
CHANGED
|
@@ -12,9 +12,12 @@ import picomatch from "picomatch";
|
|
|
12
12
|
honoring the anchored-vs-floating, directory-only, and negation
|
|
13
13
|
semantics of the pattern; an anchored pattern resolves relative to
|
|
14
14
|
the "base" directory its rule file governs, given as a POSIX path
|
|
15
|
-
relative to the working tree root
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
relative to the working tree root, and "nocase" folds the case of
|
|
16
|
+
the pattern like Git does under "core.ignoreCase" */
|
|
17
|
+
const compileRule = (line, base, nocase) => {
|
|
18
|
+
/* Git drops the trailing whitespace only (unless a backslash
|
|
19
|
+
escapes it), while a leading one is part of the pattern */
|
|
20
|
+
let pattern = line.replace(/(?<!\\)\s+$/, "");
|
|
18
21
|
if (pattern === "" || pattern.startsWith("#"))
|
|
19
22
|
return null;
|
|
20
23
|
let negated = false;
|
|
@@ -27,20 +30,28 @@ const compileRule = (line, base) => {
|
|
|
27
30
|
dirOnly = true;
|
|
28
31
|
pattern = pattern.slice(0, -1);
|
|
29
32
|
}
|
|
33
|
+
/* a trailing "/**" matches everything inside the directory but not
|
|
34
|
+
the directory itself (so a later negation can still re-include a
|
|
35
|
+
file below it), whereas picomatch would match the directory, too */
|
|
36
|
+
if (pattern.endsWith("/**"))
|
|
37
|
+
pattern += "/*";
|
|
30
38
|
/* a pattern carrying a slash anywhere but at its end is anchored at
|
|
31
39
|
the directory of its rule file, while every other one floats and
|
|
32
40
|
hence matches at any depth below it */
|
|
33
41
|
const anchored = pattern.includes("/");
|
|
34
42
|
if (pattern.startsWith("/"))
|
|
35
43
|
pattern = pattern.slice(1);
|
|
44
|
+
/* a negated character class is "[!...]" in the wildmatch of Git,
|
|
45
|
+
while picomatch knows the "[^...]" spelling only */
|
|
46
|
+
pattern = pattern.replace(/\[!/g, "[^");
|
|
36
47
|
const glob = anchored ? (base === "" ? pattern : `${base}/${pattern}`) : `**/${pattern}`;
|
|
37
|
-
const isMatch = picomatch(glob, { dot: true, nobrace: true, noextglob: true, nonegate: true });
|
|
48
|
+
const isMatch = picomatch(glob, { dot: true, nobrace: true, noextglob: true, nonegate: true, nocase });
|
|
38
49
|
return { matcher: isMatch, negated, dirOnly };
|
|
39
50
|
};
|
|
40
51
|
/* load the rules of a single exclude file, given as an absolute "file"
|
|
41
52
|
and the working-tree-relative "base" its anchored patterns resolve
|
|
42
53
|
against; an absent or unreadable file contributes no rules */
|
|
43
|
-
const loadRules = (file, base) => {
|
|
54
|
+
const loadRules = (file, base, nocase) => {
|
|
44
55
|
let text;
|
|
45
56
|
try {
|
|
46
57
|
text = fs.readFileSync(file, "utf8");
|
|
@@ -50,7 +61,7 @@ const loadRules = (file, base) => {
|
|
|
50
61
|
}
|
|
51
62
|
const rules = new Array();
|
|
52
63
|
for (const line of text.split(/\r?\n/)) {
|
|
53
|
-
const rule = compileRule(line, base);
|
|
64
|
+
const rule = compileRule(line, base, nocase);
|
|
54
65
|
if (rule !== null)
|
|
55
66
|
rules.push(rule);
|
|
56
67
|
}
|
|
@@ -70,25 +81,28 @@ const workingTree = (dir) => {
|
|
|
70
81
|
current = parent;
|
|
71
82
|
}
|
|
72
83
|
};
|
|
73
|
-
/*
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
let file = "";
|
|
84
|
+
/* query a single Git configuration value of a working tree (its "~"
|
|
85
|
+
expanded and its booleans normalized by Git itself through the
|
|
86
|
+
"--type"), where an unset value or no Git at all yields "" */
|
|
87
|
+
const gitConfig = (root, type, key) => {
|
|
78
88
|
try {
|
|
79
|
-
|
|
89
|
+
return execFileSync("git", ["config", "--get", `--type=${type}`, key], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
80
90
|
}
|
|
81
91
|
catch {
|
|
82
|
-
|
|
92
|
+
return "";
|
|
83
93
|
}
|
|
94
|
+
};
|
|
95
|
+
/* resolve the global Git excludes file: the configured
|
|
96
|
+
"core.excludesFile", or else the XDG location Git falls back onto
|
|
97
|
+
when that configuration value is unset */
|
|
98
|
+
const globalFile = (root) => {
|
|
99
|
+
const file = gitConfig(root, "path", "core.excludesFile");
|
|
84
100
|
if (file === "") {
|
|
85
101
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
86
102
|
return xdg !== undefined && xdg !== "" ?
|
|
87
103
|
path.join(xdg, "git", "ignore") :
|
|
88
104
|
path.join(os.homedir(), ".config", "git", "ignore");
|
|
89
105
|
}
|
|
90
|
-
if (file.startsWith("~/"))
|
|
91
|
-
return path.join(os.homedir(), file.slice(2));
|
|
92
106
|
return file;
|
|
93
107
|
};
|
|
94
108
|
/* resolve the repository-local exclude file of a working tree, held by
|
|
@@ -121,16 +135,19 @@ export const excluder = (dir) => {
|
|
|
121
135
|
const root = workingTree(dir);
|
|
122
136
|
if (root === null)
|
|
123
137
|
return () => false;
|
|
138
|
+
/* Git folds the case of its exclude patterns on a case-insensitive
|
|
139
|
+
file system ("core.ignoreCase", set by Git itself on init/clone) */
|
|
140
|
+
const nocase = gitConfig(root, "bool", "core.ignoreCase") === "true";
|
|
124
141
|
/* the rules which no directory of the working tree owns and which
|
|
125
142
|
hence govern it as a whole, in ascending Git precedence */
|
|
126
|
-
const
|
|
143
|
+
const baseRules = [...loadRules(globalFile(root), "", nocase), ...loadRules(infoFile(root), "", nocase)];
|
|
127
144
|
/* the per-directory ".gitignore" rules, loaded on first use, as one
|
|
128
145
|
run queries many files sharing the very same directories */
|
|
129
146
|
const cache = new Map();
|
|
130
147
|
const rulesOf = (relDir) => {
|
|
131
148
|
let rules = cache.get(relDir);
|
|
132
149
|
if (rules === undefined) {
|
|
133
|
-
rules = loadRules(path.join(root, relDir, ".gitignore"), relDir);
|
|
150
|
+
rules = loadRules(path.join(root, relDir, ".gitignore"), relDir, nocase);
|
|
134
151
|
cache.set(relDir, rules);
|
|
135
152
|
}
|
|
136
153
|
return rules;
|
|
@@ -147,7 +164,7 @@ export const excluder = (dir) => {
|
|
|
147
164
|
directory excludes the file itself, as Git never descends
|
|
148
165
|
into it -- and, for the same reason, a rule below such an
|
|
149
166
|
ancestor can never re-include the file */
|
|
150
|
-
let rules =
|
|
167
|
+
let rules = baseRules;
|
|
151
168
|
let relDir = "";
|
|
152
169
|
for (let i = 0; i < segments.length; i++) {
|
|
153
170
|
rules = rules.concat(rulesOf(relDir));
|
package/dst/specbook-link.js
CHANGED
|
@@ -141,8 +141,8 @@ const nearest = (index, matches, from) => {
|
|
|
141
141
|
i++;
|
|
142
142
|
return i;
|
|
143
143
|
};
|
|
144
|
-
const proximities = matches.map(
|
|
145
|
-
const best =
|
|
144
|
+
const proximities = matches.map(proximity);
|
|
145
|
+
const best = Math.max(0, ...proximities);
|
|
146
146
|
return matches.filter((_, i) => proximities[i] === best);
|
|
147
147
|
};
|
|
148
148
|
/* resolve a reference into a unique target: a single segment tries the
|
package/dst/specbook-mcp.js
CHANGED
|
@@ -157,10 +157,11 @@ export const serveMcp = async (verbose) => {
|
|
|
157
157
|
}
|
|
158
158
|
}, async (args) => {
|
|
159
159
|
try {
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
const { output, ...options } = args;
|
|
161
|
+
const text = await specbook.describe({ ...options, compress: options.compress ?? 2 });
|
|
162
|
+
if (output !== undefined && output !== "-") {
|
|
163
|
+
await fs.promises.writeFile(outputOf(args, output), text);
|
|
164
|
+
return { content: [{ type: "text", text: `described specification format into "${output}"` }] };
|
|
164
165
|
}
|
|
165
166
|
return { content: [{ type: "text", text }] };
|
|
166
167
|
}
|
|
@@ -59,9 +59,12 @@ export const embeddingMimeType = (reference) => {
|
|
|
59
59
|
/* the number of embedding entries an image embedding markup occupies,
|
|
60
60
|
given its inline reference: one per variant of an embeddable one, none
|
|
61
61
|
for any other one, and one for a reference-style markup (no reference) */
|
|
62
|
-
export const embeddingCount = (reference) =>
|
|
63
|
-
(
|
|
64
|
-
|
|
62
|
+
export const embeddingCount = (reference) => {
|
|
63
|
+
if (reference === undefined)
|
|
64
|
+
return 1;
|
|
65
|
+
const trimmed = reference.trim();
|
|
66
|
+
return embeddingMimeType(trimmed) !== undefined ? embeddingVariants(trimmed).length : 0;
|
|
67
|
+
};
|
|
65
68
|
/* the image definition ("[label]: data:...") a reference-style image
|
|
66
69
|
embedding refers to: a base64 data: URL of an embeddable image type */
|
|
67
70
|
export const embeddingDataRegex = /^data:(image\/(?:svg\+xml|png|jpeg|webp));base64,([A-Za-z0-9+/=]+)$/;
|
|
@@ -108,7 +108,7 @@ const checkPropValue = (ctx, object, prop, expr, property, meta) => {
|
|
|
108
108
|
checkLocal(ctx, object, prop, singleTarget(ctx, object, item), meta);
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
-
else if (!directMatches(expr, plainText(property.value)))
|
|
111
|
+
else if (!directMatches(expr, plainText(property.value).trim()))
|
|
112
112
|
ctx.diagnose(meta.file, meta.line, expr.kind === "enum" ?
|
|
113
113
|
`property "${prop.name}" value "${property.value}" is not a member of "${prop.value}"` :
|
|
114
114
|
`property "${prop.name}" value "${property.value}" does not match pattern "${prop.value}"`);
|
|
@@ -330,14 +330,13 @@ const checkAutomata = (ctx, schemas) => {
|
|
|
330
330
|
the node set is skipped, as the property checks report it) */
|
|
331
331
|
const succs = new Map(nodes.map((node) => [node, new Set()]));
|
|
332
332
|
const preds = new Map(nodes.map((node) => [node, new Set()]));
|
|
333
|
-
for (const edge of object.children.filter((child) => child.kind === automaton.edges))
|
|
334
|
-
const source
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
333
|
+
for (const edge of object.children.filter((child) => child.kind === automaton.edges))
|
|
334
|
+
for (const source of referencedObjects(ctx, edge, automaton.source))
|
|
335
|
+
for (const target of referencedObjects(ctx, edge, automaton.target))
|
|
336
|
+
if (nodeSet.has(source) && nodeSet.has(target)) {
|
|
337
|
+
succs.get(source)?.add(target);
|
|
338
|
+
preds.get(target)?.add(source);
|
|
339
|
+
}
|
|
341
340
|
/* the transitive closure of a node set along a neighbor map */
|
|
342
341
|
const closure = (starts, next) => {
|
|
343
342
|
const seen = new Set(starts);
|
|
@@ -183,8 +183,9 @@ const parseList = (ctx, list, object, file, line) => {
|
|
|
183
183
|
const property = { key: km[1].trim(), value };
|
|
184
184
|
ctx.propMeta.set(property, { line });
|
|
185
185
|
object.properties.push(property);
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
const ignored = item.tokens.find((sub) => sub.type !== "text" && sub.type !== "paragraph" && sub.type !== "space");
|
|
187
|
+
if (ignored !== undefined)
|
|
188
|
+
ctx.diagnose(file, line, `nested ${ignored.type} below property "${property.key}" ignored`);
|
|
188
189
|
}
|
|
189
190
|
else
|
|
190
191
|
ctx.diagnose(file, line, `unrecognized list item "${first}"`);
|
|
@@ -269,13 +270,16 @@ const parseConcise = (ctx, item, parent, file, line, group) => {
|
|
|
269
270
|
object.description = splitDescription(statements.join("; ").replace(/\.\s*$/, ""));
|
|
270
271
|
/* recurse into nested concise-format child objects, tracking the
|
|
271
272
|
source line of the nested list within the item (an ordered
|
|
272
|
-
nested list
|
|
273
|
+
nested list or any other block content is not supported, so
|
|
274
|
+
it is reported, not lost) */
|
|
273
275
|
let subLine = line;
|
|
274
276
|
for (const sub of item.tokens) {
|
|
275
277
|
if (sub.type === "list" && !sub.ordered)
|
|
276
278
|
parseList(ctx, sub, object, file, subLine);
|
|
277
279
|
else if (sub.type === "list")
|
|
278
280
|
ctx.diagnose(file, subLine, `nested ordered list below ${kind} "${name}" ignored`);
|
|
281
|
+
else if (sub.type !== "text" && sub.type !== "paragraph" && sub.type !== "space")
|
|
282
|
+
ctx.diagnose(file, subLine, `nested ${sub.type} below ${kind} "${name}" ignored`);
|
|
279
283
|
subLine += (sub.raw.match(/\n/g) ?? []).length;
|
|
280
284
|
}
|
|
281
285
|
parent.children.push(object);
|
|
@@ -304,6 +308,10 @@ const parseHeading = (ctx, token, state, stamps, file, line) => {
|
|
|
304
308
|
state.stack.length = depth - 1;
|
|
305
309
|
return;
|
|
306
310
|
}
|
|
311
|
+
/* a level 1 object takes its parenthesized token as the id right
|
|
312
|
+
away, as the artifact resolution of the semantic phase matches
|
|
313
|
+
it against the configured id before any property could consume
|
|
314
|
+
the token (a nested object receives it there, see "validate") */
|
|
307
315
|
const object = {
|
|
308
316
|
kind: heading.kind,
|
|
309
317
|
id: heading.id ?? (depth === 1 ? heading.paren : undefined) ?? slugify(heading.name),
|
|
@@ -337,7 +345,8 @@ const unsupportedTokens = {
|
|
|
337
345
|
};
|
|
338
346
|
/* parse a single source file into its artifacts */
|
|
339
347
|
export const parseFile = (ctx, source) => {
|
|
340
|
-
|
|
348
|
+
/* strip a leading byte order mark, as it would hide the frontmatter */
|
|
349
|
+
const { present, created, modified, body, offset } = parseFrontmatter(source.text.replace(/^\uFEFF/, ""));
|
|
341
350
|
if (!present)
|
|
342
351
|
ctx.diagnose(source.file, 1, "missing frontmatter (\"Created:\"/\"Modified:\" block)");
|
|
343
352
|
else
|
package/dst/specbook-project.js
CHANGED
|
@@ -14,12 +14,22 @@ export const projectFile = ".specbook.yaml";
|
|
|
14
14
|
/* the schema of the YAML project configuration: the files or glob
|
|
15
15
|
patterns of the YAML schema configuration and the base directory
|
|
16
16
|
of the specification Markdown files */
|
|
17
|
-
const
|
|
17
|
+
const ProjectEntries = v.strictObject({
|
|
18
18
|
config: v.optional(v.union([v.pipe(v.string(), v.minLength(1)),
|
|
19
19
|
v.pipe(v.array(v.pipe(v.string(), v.minLength(1))), v.minLength(1))])),
|
|
20
20
|
basedir: v.optional(v.pipe(v.string(), v.minLength(1)))
|
|
21
21
|
}, (issue) => issue.expected === "never" ? "unknown project configuration entry" :
|
|
22
22
|
"expected a map of project configuration entries");
|
|
23
|
+
/* whether a path names an existing regular file, where an
|
|
24
|
+
inaccessible one (not just an absent one) counts as absent */
|
|
25
|
+
const isFile = (file) => {
|
|
26
|
+
try {
|
|
27
|
+
return fs.statSync(file, { throwIfNoEntry: false })?.isFile() === true;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
23
33
|
/* find the project configuration file in the closest ancestor-or-self
|
|
24
34
|
of the directory (up to the filesystem root, as the file can reside
|
|
25
35
|
even above a nested Git working tree) */
|
|
@@ -27,7 +37,7 @@ const findProject = (dir) => {
|
|
|
27
37
|
let current = path.resolve(dir);
|
|
28
38
|
for (;;) {
|
|
29
39
|
const file = path.join(current, projectFile);
|
|
30
|
-
if (
|
|
40
|
+
if (isFile(file))
|
|
31
41
|
return file;
|
|
32
42
|
const parent = path.dirname(current);
|
|
33
43
|
if (parent === current)
|
|
@@ -58,6 +68,7 @@ export const loadProject = (dir) => {
|
|
|
58
68
|
const file = findProject(dir);
|
|
59
69
|
if (file === undefined)
|
|
60
70
|
return undefined;
|
|
71
|
+
/* parse the YAML, reporting its syntax errors as positioned diagnostics */
|
|
61
72
|
const diagnostics = new Array();
|
|
62
73
|
const lines = new LineCounter();
|
|
63
74
|
const yaml = readProject(file);
|
|
@@ -71,7 +82,7 @@ export const loadProject = (dir) => {
|
|
|
71
82
|
message: "invalid YAML syntax: " + err.message.replace(/ at line \d+, column \d+(?::\n[\s\S]*)?$/, "")
|
|
72
83
|
});
|
|
73
84
|
/* an empty or comment-only file carries no entries at all */
|
|
74
|
-
const result = v.safeParse(
|
|
85
|
+
const result = v.safeParse(ProjectEntries, doc.errors.length === 0 ? (doc.toJS() ?? {}) : {});
|
|
75
86
|
if (!result.success)
|
|
76
87
|
for (const issue of result.issues) {
|
|
77
88
|
const keys = (issue.path ?? []).map((item) => item.key);
|
|
@@ -79,9 +90,11 @@ export const loadProject = (dir) => {
|
|
|
79
90
|
diagnostics.push({ file, ...lineColOfPath(doc, lines, keys), severity: "error",
|
|
80
91
|
message: `invalid project configuration: ${where}${issue.message}` });
|
|
81
92
|
}
|
|
93
|
+
/* (the second condition just narrows the result type below) */
|
|
82
94
|
if (diagnostics.length > 0 || !result.success)
|
|
83
95
|
throw new Error("invalid project configuration:\n" +
|
|
84
96
|
diagnostics.map(renderDiagnostic).join("\n"));
|
|
97
|
+
/* resolve the relative paths against the directory of the file */
|
|
85
98
|
const base = path.dirname(file);
|
|
86
99
|
const { config, basedir } = result.output;
|
|
87
100
|
const patterns = typeof config === "string" ? [config] : config;
|
package/dst/specbook-theme.js
CHANGED
|
@@ -9,7 +9,11 @@ import { parse, generate } from "@rse/mrcs";
|
|
|
9
9
|
and a signal spread derived from the second tone (if given) or
|
|
10
10
|
else from the complement-transposed first tone */
|
|
11
11
|
export const themeColors = (tone) => {
|
|
12
|
-
const
|
|
12
|
+
const tones = tone.trim().split(/\s+/);
|
|
13
|
+
if (tones.length > 2 || !tones.every((t) => (/^#[0-9a-fA-F]{6}$/).test(t)))
|
|
14
|
+
throw new Error(`invalid theme color tone "${tone}" (expected "#RRGGBB", ` +
|
|
15
|
+
"optionally followed by a second one)");
|
|
16
|
+
const [accent, signal = `${accent}^`] = tones;
|
|
13
17
|
return {
|
|
14
18
|
base: generate(parse("#000000/32")),
|
|
15
19
|
accent: generate(parse(`${accent}+40-5/32`)),
|
|
@@ -27,7 +31,7 @@ export const themeStylesheet = (colors) => ":root {\n" +
|
|
|
27
31
|
aligned with the layer-2 CSS mapping of the HTML rendering */
|
|
28
32
|
export const themeMapping = (colors, style) => {
|
|
29
33
|
const idx = style === "dark" ?
|
|
30
|
-
{ muted: 22, symbol: 20, border: 16, accent:
|
|
34
|
+
{ muted: 22, symbol: 20, border: 16, accent: 22 } :
|
|
31
35
|
{ muted: 22, symbol: 24, border: 28, accent: 12 };
|
|
32
36
|
return {
|
|
33
37
|
muted: colors.base[idx.muted - 1],
|
package/dst/specbook-verbose.js
CHANGED
|
@@ -15,10 +15,11 @@ export const parseVerbosity = (value) => {
|
|
|
15
15
|
const level = (/^(?:true|yes|on)$/i).test(text) ? 1 :
|
|
16
16
|
(/^(?:false|no|off)$/i).test(text) ? 0 :
|
|
17
17
|
(/^\d+$/).test(text) ? Number(text) : NaN;
|
|
18
|
-
|
|
18
|
+
const verbosity = verbosities.find((n) => n === level);
|
|
19
|
+
if (verbosity === undefined)
|
|
19
20
|
throw new Error(`unknown verbosity "${value}" ` +
|
|
20
21
|
`(supported: ${verbosities.join(", ")})`);
|
|
21
|
-
return
|
|
22
|
+
return verbosity;
|
|
22
23
|
};
|
|
23
24
|
/* the sentinel characters delimiting a marked literal value, taken
|
|
24
25
|
from the Unicode private use area, so they never clash with the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rse/specbook",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"description": "Markdown-based Specification Format (API, CLI and MCP service)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [ "dst" ],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"marked": "18.0.
|
|
20
|
+
"marked": "18.0.14",
|
|
21
21
|
"marked-smartypants": "1.1.12",
|
|
22
22
|
"yaml": "2.9.1",
|
|
23
23
|
"json5": "2.2.3",
|
|
24
24
|
"pdf-lib": "1.17.1",
|
|
25
|
-
"subset-font": "2.
|
|
26
|
-
"sharp": "0.35.
|
|
25
|
+
"subset-font": "2.9.0",
|
|
26
|
+
"sharp": "0.35.5",
|
|
27
27
|
"svgo": "4.1.0",
|
|
28
28
|
"@rse/mrcs": "0.9.2",
|
|
29
29
|
"@rse/gradia": "1.4.2",
|
|
@@ -32,36 +32,36 @@
|
|
|
32
32
|
"nunjucks": "3.2.4",
|
|
33
33
|
"@rse/nunjucks-addons": "2.2.0",
|
|
34
34
|
"textframe": "1.2.1",
|
|
35
|
-
"@swc/html": "1.16.
|
|
35
|
+
"@swc/html": "1.16.12",
|
|
36
36
|
"tokenizr": "2.2.1",
|
|
37
37
|
"playwright": "1.63.0",
|
|
38
38
|
"source-code-error": "1.2.0",
|
|
39
39
|
"commander": "15.0.0",
|
|
40
|
-
"chalk": "6.0.
|
|
40
|
+
"chalk": "6.0.1",
|
|
41
41
|
"chokidar5": "npm:chokidar@5.0.0",
|
|
42
42
|
"glob": "13.0.6",
|
|
43
43
|
"picomatch": "4.0.7",
|
|
44
44
|
"es-toolkit": "1.52.0",
|
|
45
45
|
"fastify": "5.12.5",
|
|
46
46
|
"@fastify/websocket": "11.3.1",
|
|
47
|
-
"@modelcontextprotocol/sdk": "1.
|
|
47
|
+
"@modelcontextprotocol/sdk": "1.31.0",
|
|
48
48
|
"zod": "4.6.5"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"chokidar-cli": "3.0.0",
|
|
52
52
|
"eslint": "10.9.0",
|
|
53
53
|
"@eslint/js": "10.0.1",
|
|
54
|
-
"typescript-eslint": "8.
|
|
54
|
+
"typescript-eslint": "8.71.0",
|
|
55
55
|
"eslint-plugin-promise": "7.3.0",
|
|
56
56
|
"neostandard": "0.14.0-next.1",
|
|
57
57
|
"globals": "17.12.0",
|
|
58
58
|
"markdownlint": "0.41.1",
|
|
59
59
|
"markdownlint-cli2": "0.23.3",
|
|
60
|
-
"@types/node": "26.6.
|
|
60
|
+
"@types/node": "26.6.3",
|
|
61
61
|
"@types/picomatch": "4.0.3",
|
|
62
62
|
"@types/subset-font": "2.3.0",
|
|
63
63
|
"@types/nunjucks": "3.2.6",
|
|
64
|
-
"@types/ws": "8.18.
|
|
64
|
+
"@types/ws": "8.18.2",
|
|
65
65
|
"typescript": "6.0.3",
|
|
66
66
|
"tsx": "4.23.15",
|
|
67
67
|
"source-sans": "3.52.0",
|