@rse/specbook 0.9.3 → 0.9.5
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 +19 -4
- package/dst/specbook-api.d.ts +1 -0
- package/dst/specbook-api.js +10 -1
- package/dst/specbook-cli.js +14 -14
- package/dst/specbook-cmd-describe.js +3 -1
- package/dst/specbook-cmd-edit.js +6 -1
- package/dst/specbook-cmd-export.js +3 -2
- package/dst/specbook-cmd-import.js +6 -1
- package/dst/specbook-cmd-init.js +4 -2
- package/dst/specbook-cmd-lint.js +4 -2
- package/dst/specbook-config.js +22 -7
- package/dst/specbook-diagram.js +67 -57
- package/dst/specbook-export-ast.js +5 -2
- package/dst/specbook-export-common.d.ts +0 -1
- package/dst/specbook-export-common.js +32 -19
- package/dst/specbook-export-html-search.js +6 -4
- package/dst/specbook-export-html.css +7 -0
- package/dst/specbook-export-html.js +32 -13
- package/dst/specbook-export-md.js +10 -6
- package/dst/specbook-export-pdf.js +4 -3
- package/dst/specbook-llm.js +18 -28
- package/dst/specbook-mcp.js +2 -2
- package/dst/specbook-parse-semantic.js +73 -58
- package/dst/specbook-parse-syntax.js +17 -16
- package/dst/specbook-parse-value.js +8 -2
- package/dst/specbook-parse.js +4 -0
- package/dst/specbook-struct-schema.js +5 -5
- package/dst/specbook-theme.js +1 -1
- package/package.json +5 -6
|
@@ -3,8 +3,8 @@
|
|
|
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";
|
|
7
|
-
import path from "node:path";
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
8
|
import { marked } from "marked";
|
|
9
9
|
import { embeddingRegex, embeddingMimeType } from "./specbook-parse-common.js";
|
|
10
10
|
/* split a description text into statement and optional rationale */
|
|
@@ -89,7 +89,7 @@ const parseFrontmatter = (text) => {
|
|
|
89
89
|
const line = 2 + (m[1].slice(0, km.index).match(/\n/g) ?? []).length;
|
|
90
90
|
const value = km[1].trim();
|
|
91
91
|
const date = new Date(value);
|
|
92
|
-
return { date: isNaN(date.getTime()) ? null : date, line, value };
|
|
92
|
+
return { date: Number.isNaN(date.getTime()) ? null : date, line, value };
|
|
93
93
|
};
|
|
94
94
|
return {
|
|
95
95
|
present: true,
|
|
@@ -137,8 +137,7 @@ const parseList = (ctx, list, object, file, line) => {
|
|
|
137
137
|
/* a concise-format item carries a further segment on its first
|
|
138
138
|
line, or ends it with the segment terminator (the case of an
|
|
139
139
|
object without properties, whose description follows below) */
|
|
140
|
-
|
|
141
|
-
if (cm !== null)
|
|
140
|
+
if (/^[^:;]+:\s+.+;.*$/.test(first))
|
|
142
141
|
parseConcise(ctx, item, object, file, line);
|
|
143
142
|
else {
|
|
144
143
|
const km = first.match(/^([^:;]+):\s*(.*)$/);
|
|
@@ -234,7 +233,7 @@ const parseConcise = (ctx, item, parent, file, line, group) => {
|
|
|
234
233
|
const statements = new Array();
|
|
235
234
|
for (const segment of segments) {
|
|
236
235
|
const km = segment.match(/^([^:;]+):\s+(.+)$/);
|
|
237
|
-
if (km !== null && !(/\
|
|
236
|
+
if (km !== null && !(/\s(?:\*\*BECAUSE\*\*|BECAUSE)\s/.test(segment))) {
|
|
238
237
|
const property = { key: km[1].trim(), value: km[2].trim() };
|
|
239
238
|
ctx.propMeta.set(property, { line });
|
|
240
239
|
object.properties.push(property);
|
|
@@ -261,6 +260,7 @@ export const parseFile = (ctx, source) => {
|
|
|
261
260
|
ctx.diagnose(source.file, info.line, info.value === null ?
|
|
262
261
|
`missing "${key}:" frontmatter key` :
|
|
263
262
|
`invalid "${key}:" frontmatter timestamp "${info.value}"`);
|
|
263
|
+
/* the state tracked while walking the Markdown tokens */
|
|
264
264
|
const artifacts = new Array();
|
|
265
265
|
const stack = new Array();
|
|
266
266
|
let current = null;
|
|
@@ -278,15 +278,16 @@ export const parseFile = (ctx, source) => {
|
|
|
278
278
|
if (token.type === "heading") {
|
|
279
279
|
flush();
|
|
280
280
|
group = null;
|
|
281
|
-
const
|
|
281
|
+
const { depth, text } = token;
|
|
282
|
+
const heading = parseHeadingText(text);
|
|
282
283
|
if (heading.malformed !== undefined)
|
|
283
284
|
ctx.diagnose(source.file, line, `malformed anchor "${heading.malformed}" in heading`);
|
|
284
|
-
if (heading.name === "" &&
|
|
285
|
+
if (heading.name === "" && depth > 1) {
|
|
285
286
|
/* a grouping container heading (e.g. "### STATES") collects
|
|
286
287
|
objects of its singular kind below the parent object */
|
|
287
|
-
const parent = stack[
|
|
288
|
+
const parent = stack[depth - 2];
|
|
288
289
|
if (parent === undefined)
|
|
289
|
-
ctx.diagnose(source.file, line, `heading level ${
|
|
290
|
+
ctx.diagnose(source.file, line, `heading level ${depth} without parent object`);
|
|
290
291
|
else {
|
|
291
292
|
group = { parent, kind: heading.kind.replace(/S$/, "") };
|
|
292
293
|
current = parent;
|
|
@@ -295,7 +296,7 @@ export const parseFile = (ctx, source) => {
|
|
|
295
296
|
else {
|
|
296
297
|
const object = {
|
|
297
298
|
kind: heading.kind,
|
|
298
|
-
id: heading.id ?? (
|
|
299
|
+
id: heading.id ?? (depth === 1 ? heading.paren : undefined) ?? slugify(heading.name),
|
|
299
300
|
name: heading.name,
|
|
300
301
|
properties: [],
|
|
301
302
|
childs: []
|
|
@@ -307,7 +308,7 @@ export const parseFile = (ctx, source) => {
|
|
|
307
308
|
if (heading.primary)
|
|
308
309
|
object.primary = true;
|
|
309
310
|
ctx.objectMeta.set(object, { file: source.file, line });
|
|
310
|
-
if (
|
|
311
|
+
if (depth === 1) {
|
|
311
312
|
artifacts.push({
|
|
312
313
|
created: created.date ?? new Date(),
|
|
313
314
|
modified: modified.date ?? new Date(),
|
|
@@ -316,16 +317,16 @@ export const parseFile = (ctx, source) => {
|
|
|
316
317
|
stack.length = 0;
|
|
317
318
|
}
|
|
318
319
|
else {
|
|
319
|
-
const parent = stack[
|
|
320
|
+
const parent = stack[depth - 2];
|
|
320
321
|
if (parent === undefined) {
|
|
321
|
-
ctx.diagnose(source.file, line, `heading level ${
|
|
322
|
+
ctx.diagnose(source.file, line, `heading level ${depth} without parent object`);
|
|
322
323
|
line += (token.raw.match(/\n/g) ?? []).length;
|
|
323
324
|
continue;
|
|
324
325
|
}
|
|
325
326
|
parent.childs.push(object);
|
|
326
327
|
}
|
|
327
|
-
stack.length =
|
|
328
|
-
stack[
|
|
328
|
+
stack.length = depth - 1;
|
|
329
|
+
stack[depth - 1] = object;
|
|
329
330
|
current = object;
|
|
330
331
|
}
|
|
331
332
|
}
|
|
@@ -43,7 +43,7 @@ const parseValueExpr = (tokens, pos, nested) => {
|
|
|
43
43
|
}
|
|
44
44
|
catch (err) {
|
|
45
45
|
throw new Error(`invalid regular expression "/${source}/": ` +
|
|
46
|
-
(err instanceof Error ? err.message : String(err)));
|
|
46
|
+
(err instanceof Error ? err.message : String(err)), { cause: err });
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
else if (token.type === "reference")
|
|
@@ -119,5 +119,11 @@ export const splitItems = (text) => {
|
|
|
119
119
|
else
|
|
120
120
|
parts[parts.length - 1] += char;
|
|
121
121
|
}
|
|
122
|
-
return parts.map((part) =>
|
|
122
|
+
return parts.map((part) => {
|
|
123
|
+
/* unquote a fully double-quoted item, mirroring the member
|
|
124
|
+
quoting of the value expression lexer */
|
|
125
|
+
const item = part.trim();
|
|
126
|
+
const m = item.match(/^"((?:\\"|[^"])*)"$/);
|
|
127
|
+
return m !== null ? m[1].replace(/\\"/g, "\"") : item;
|
|
128
|
+
}).filter((part) => part !== "");
|
|
123
129
|
};
|
package/dst/specbook-parse.js
CHANGED
|
@@ -18,10 +18,14 @@ export class Parser {
|
|
|
18
18
|
/* parse all source files into the specification AST and
|
|
19
19
|
optionally validate the result against a configuration */
|
|
20
20
|
parse(sources, config) {
|
|
21
|
+
/* reset the parsing context to allow a reuse of the parser */
|
|
22
|
+
this.ctx = new ParseContext();
|
|
23
|
+
/* parse all source files into their artifacts */
|
|
21
24
|
const artifacts = new Array();
|
|
22
25
|
for (const source of sources)
|
|
23
26
|
artifacts.push(...parseFile(this.ctx, source));
|
|
24
27
|
const specification = { artifacts };
|
|
28
|
+
/* validate the resulting specification AST */
|
|
25
29
|
if (artifacts.length > 0) {
|
|
26
30
|
this.ctx.linkIndex = buildLinkIndex(specification);
|
|
27
31
|
if (config !== undefined)
|
|
@@ -13,13 +13,13 @@ const GradiaConfigSchemas = Object.fromEntries(Object.entries(Gradia.config).map
|
|
|
13
13
|
typeof value === "boolean" ? v.boolean() : typeof value === "number" ? v.number() : v.string()]));
|
|
14
14
|
const SchemaDiagramConfig = v.partial(v.strictObject(GradiaConfigSchemas, (issue) => issue.expected === "never" ? "unknown Gradia rendering option" :
|
|
15
15
|
"expected a map of Gradia rendering options"));
|
|
16
|
-
const SchemaProperty = v.
|
|
16
|
+
const SchemaProperty = v.strictObject({
|
|
17
17
|
name: v.string(),
|
|
18
18
|
desc: v.optional(v.string()),
|
|
19
19
|
value: v.optional(v.string()),
|
|
20
20
|
optional: v.optional(v.boolean())
|
|
21
21
|
});
|
|
22
|
-
const SchemaDiagram = v.
|
|
22
|
+
const SchemaDiagram = v.strictObject({
|
|
23
23
|
type: v.optional(v.picklist(["graph", "hub", "grid"])),
|
|
24
24
|
nodes: v.optional(v.string()),
|
|
25
25
|
edges: v.optional(v.string()),
|
|
@@ -34,12 +34,12 @@ const SchemaDiagram = v.object({
|
|
|
34
34
|
properties: v.optional(v.array(v.string())),
|
|
35
35
|
config: v.optional(SchemaDiagramConfig)
|
|
36
36
|
});
|
|
37
|
-
const SchemaFormat = v.
|
|
37
|
+
const SchemaFormat = v.strictObject({
|
|
38
38
|
type: v.optional(v.picklist(["auto", "complex", "concise"])),
|
|
39
|
-
maxTableColumns: v.optional(v.number()),
|
|
39
|
+
maxTableColumns: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1))),
|
|
40
40
|
withUnusedProps: v.optional(v.boolean())
|
|
41
41
|
});
|
|
42
|
-
const SchemaObject = v.
|
|
42
|
+
const SchemaObject = v.strictObject({
|
|
43
43
|
kind: v.string(),
|
|
44
44
|
name: v.optional(v.string()),
|
|
45
45
|
id: v.optional(v.string()),
|
package/dst/specbook-theme.js
CHANGED
|
@@ -26,7 +26,7 @@ export const themeStylesheet = (colors) => ":root {\n" +
|
|
|
26
26
|
aligned with the layer-2 CSS mapping of the HTML rendering */
|
|
27
27
|
export const themeMapping = (colors, style) => {
|
|
28
28
|
const idx = style === "dark" ?
|
|
29
|
-
{ text:
|
|
29
|
+
{ text: 29, muted: 22, symbol: 20, border: 16, accent: 24 } :
|
|
30
30
|
{ text: 5, muted: 22, symbol: 24, border: 28, accent: 12 };
|
|
31
31
|
return {
|
|
32
32
|
text: colors.base[idx.text - 1],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rse/specbook",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"description": "Markdown-based Specification Format (API, CLI and MCP service)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"pdf-lib": "1.17.1",
|
|
25
25
|
"subset-font": "2.5.0",
|
|
26
26
|
"@rse/mrcs": "0.9.2",
|
|
27
|
-
"@rse/gradia": "0.
|
|
27
|
+
"@rse/gradia": "1.0.0",
|
|
28
28
|
"@toon-format/toon": "4.1.1",
|
|
29
29
|
"valibot": "1.4.2",
|
|
30
30
|
"nunjucks": "3.2.4",
|
|
@@ -44,12 +44,11 @@
|
|
|
44
44
|
"zod": "4.4.3"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"eslint": "9.
|
|
48
|
-
"@eslint/js": "
|
|
47
|
+
"eslint": "10.9.0",
|
|
48
|
+
"@eslint/js": "10.0.1",
|
|
49
49
|
"typescript-eslint": "8.67.0",
|
|
50
50
|
"eslint-plugin-promise": "7.3.0",
|
|
51
|
-
"
|
|
52
|
-
"neostandard": "0.13.0",
|
|
51
|
+
"neostandard": "0.14.0-next.1",
|
|
53
52
|
"globals": "17.11.0",
|
|
54
53
|
"@types/node": "26.2.0",
|
|
55
54
|
"@types/subset-font": "2.3.0",
|