@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.
@@ -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
- const cm = first.match(/^([^:;]+):\s+(.+;.*)$/);
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 && !(/\sBECAUSE\s/.test(segment))) {
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 heading = parseHeadingText(token.text);
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 === "" && token.depth > 1) {
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[token.depth - 2];
288
+ const parent = stack[depth - 2];
288
289
  if (parent === undefined)
289
- ctx.diagnose(source.file, line, `heading level ${token.depth} without parent object`);
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 ?? (token.depth === 1 ? heading.paren : undefined) ?? slugify(heading.name),
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 (token.depth === 1) {
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[token.depth - 2];
320
+ const parent = stack[depth - 2];
320
321
  if (parent === undefined) {
321
- ctx.diagnose(source.file, line, `heading level ${token.depth} without parent object`);
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 = token.depth - 1;
328
- stack[token.depth - 1] = object;
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) => part.trim()).filter((part) => 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
  };
@@ -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.object({
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.object({
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.object({
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.object({
42
+ const SchemaObject = v.strictObject({
43
43
  kind: v.string(),
44
44
  name: v.optional(v.string()),
45
45
  id: v.optional(v.string()),
@@ -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: 28, muted: 22, symbol: 20, border: 16, accent: 24 } :
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",
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.9.8",
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.39.5",
48
- "@eslint/js": "9.39.5",
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
- "eslint-plugin-import": "2.32.0",
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",