@rse/specbook 0.9.5 → 0.9.7

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.
Files changed (61) hide show
  1. package/README.md +108 -328
  2. package/dst/specbook-api.d.ts +13 -20
  3. package/dst/specbook-api.js +43 -39
  4. package/dst/specbook-cli.js +42 -55
  5. package/dst/specbook-cmd-describe.d.ts +5 -2
  6. package/dst/specbook-cmd-describe.js +30 -91
  7. package/dst/specbook-cmd-export.d.ts +3 -3
  8. package/dst/specbook-cmd-export.js +23 -19
  9. package/dst/specbook-cmd-init.d.ts +3 -3
  10. package/dst/specbook-cmd-init.js +38 -15
  11. package/dst/specbook-cmd-lint.d.ts +5 -6
  12. package/dst/specbook-cmd-lint.js +61 -24
  13. package/dst/specbook-config.d.ts +2 -2
  14. package/dst/specbook-config.js +82 -73
  15. package/dst/specbook-diagnostic.js +4 -1
  16. package/dst/specbook-diagram.d.ts +6 -5
  17. package/dst/specbook-diagram.js +94 -104
  18. package/dst/specbook-export-ast.d.ts +3 -3
  19. package/dst/specbook-export-ast.js +1 -1
  20. package/dst/specbook-export-common.d.ts +11 -9
  21. package/dst/specbook-export-common.js +34 -22
  22. package/dst/specbook-export-html-search.js +10 -14
  23. package/dst/specbook-export-html.css +37 -32
  24. package/dst/specbook-export-html.d.ts +4 -4
  25. package/dst/specbook-export-html.js +153 -107
  26. package/dst/specbook-export-logo-dark.svg +35 -0
  27. package/dst/specbook-export-logo-light.svg +35 -0
  28. package/dst/specbook-export-md.d.ts +3 -3
  29. package/dst/specbook-export-md.js +50 -24
  30. package/dst/specbook-export-pdf.d.ts +4 -2
  31. package/dst/specbook-export-pdf.js +48 -38
  32. package/dst/{specbook-struct-schema.d.ts → specbook-format-schema.d.ts} +3 -2
  33. package/dst/{specbook-struct-schema.js → specbook-format-schema.js} +8 -4
  34. package/dst/specbook-format-spec.d.ts +31 -0
  35. package/dst/{specbook-struct-spec.js → specbook-format-spec.js} +10 -10
  36. package/dst/specbook-format.md +750 -0
  37. package/dst/specbook-link.d.ts +3 -2
  38. package/dst/specbook-link.js +7 -4
  39. package/dst/specbook-mcp.d.ts +2 -1
  40. package/dst/specbook-mcp.js +30 -62
  41. package/dst/specbook-parse-common.d.ts +5 -3
  42. package/dst/specbook-parse-common.js +8 -0
  43. package/dst/specbook-parse-semantic.d.ts +6 -4
  44. package/dst/specbook-parse-semantic.js +98 -106
  45. package/dst/specbook-parse-syntax.d.ts +2 -2
  46. package/dst/specbook-parse-syntax.js +175 -144
  47. package/dst/specbook-parse-value.d.ts +1 -1
  48. package/dst/specbook-parse-value.js +6 -3
  49. package/dst/specbook-parse.d.ts +4 -7
  50. package/dst/specbook-parse.js +28 -35
  51. package/dst/specbook-verbose.d.ts +2 -0
  52. package/dst/specbook-verbose.js +17 -0
  53. package/package.json +3 -7
  54. package/dst/specbook-cmd-edit.d.ts +0 -9
  55. package/dst/specbook-cmd-edit.js +0 -74
  56. package/dst/specbook-cmd-import.d.ts +0 -9
  57. package/dst/specbook-cmd-import.js +0 -78
  58. package/dst/specbook-export-logo.svg +0 -35
  59. package/dst/specbook-llm.d.ts +0 -13
  60. package/dst/specbook-llm.js +0 -51
  61. package/dst/specbook-struct-spec.d.ts +0 -31
@@ -1,4 +1,4 @@
1
- import type { Specification } from "./specbook-struct-spec.js";
2
- import type { SchemaSpecification } from "./specbook-struct-schema.js";
1
+ import type { Spec } from "./specbook-format-spec.js";
2
+ import type { Schema } from "./specbook-format-schema.js";
3
3
  export type AstFormat = "json" | "json5" | "yaml" | "toon";
4
- export declare const renderAst: (specification: Specification, format: AstFormat, config?: SchemaSpecification) => Promise<Buffer>;
4
+ export declare const renderAst: (specification: Spec, format: AstFormat, config?: Schema) => Buffer;
@@ -8,7 +8,7 @@ import { stringify as stringifyYaml } from "yaml";
8
8
  import { encode as encodeToon } from "@toon-format/toon";
9
9
  import { specDiagrams } from "./specbook-diagram.js";
10
10
  /* render the specification AST into a serialization format */
11
- export const renderAst = async (specification, format, config) => {
11
+ export const renderAst = (specification, format, config) => {
12
12
  /* reduce the specification to plain JSON values (ISO date strings) */
13
13
  const plain = JSON.parse(JSON.stringify(specification));
14
14
  /* attach the Gradia specs of the diagram-configured objects as
@@ -1,13 +1,15 @@
1
- import type { Specification, Object as SpecObject } from "./specbook-struct-spec.js";
1
+ import type { Spec, SpecObject } from "./specbook-format-spec.js";
2
+ import type { embeddingThemes } from "./specbook-parse-common.js";
2
3
  export declare const escapeHtml: (text: string) => string;
3
4
  export declare const stylesheet: () => string;
4
5
  export declare const searchScript: () => string;
5
- export declare const fallbackLogo: () => string;
6
+ export declare const fallbackLogo: (theme: (typeof embeddingThemes)[number]) => string;
6
7
  export declare const isTitleObject: (object: SpecObject) => boolean;
7
- export declare const documentLang: (specification: Specification) => string | undefined;
8
- export declare const documentCharset: (specification: Specification) => string | undefined;
9
- export declare const documentThemeStyle: (specification: Specification) => string | undefined;
10
- export declare const documentThemeTone: (specification: Specification) => string | undefined;
8
+ export declare const titleObject: (specification: Spec) => SpecObject | undefined;
9
+ export declare const documentLang: (specification: Spec) => string | undefined;
10
+ export declare const documentCharset: (specification: Spec) => string | undefined;
11
+ export declare const documentThemeStyle: (specification: Spec) => string | undefined;
12
+ export declare const documentThemeTone: (specification: Spec) => string | undefined;
11
13
  export type PaperSetup = {
12
14
  unit: "mm" | "in";
13
15
  height: number;
@@ -21,12 +23,12 @@ export type PaperSetup = {
21
23
  export declare const paperSizeDefault = "A4";
22
24
  export declare const paperSetup: (paper: string) => PaperSetup;
23
25
  export declare const paperLength: (setup: PaperSetup, value: number) => string;
24
- export declare const documentPaperSize: (specification: Specification) => string;
26
+ export declare const documentPaperSize: (specification: Spec) => string;
25
27
  export declare const paperStylesheet: (paper: string) => string;
26
28
  export declare const charsetCodepoints: (charset: string) => number[] | undefined;
27
29
  export declare const subsetStylesheet: (charset?: string) => Promise<string>;
28
- export declare const documentTitle: (specification: Specification) => {
30
+ export declare const documentTitle: (specification: Spec) => {
29
31
  title: string;
30
32
  subtitle?: string;
31
33
  };
32
- export declare const documentLogo: (specification: Specification) => string;
34
+ export declare const documentLogo: (specification: Spec) => string;
@@ -3,24 +3,36 @@
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 path from "node:path";
7
6
  import fs from "node:fs";
8
- import { fileURLToPath } from "node:url";
9
7
  /* escape a text for embedding into template HTML (text and attributes) */
10
8
  export const escapeHtml = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
11
9
  .replace(/"/g, "&quot;").replace(/'/g, "&#39;");
12
10
  /* provide the build-time pre-assembled stylesheet (with the
13
11
  font faces already inlined as base64 data: URIs) */
14
- export const stylesheet = () => fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "specbook-export-html.css"), "utf8");
12
+ export const stylesheet = () => fs.readFileSync(new URL("specbook-export-html.css", import.meta.url), "utf8");
15
13
  /* provide the build-time bundled client-side fuzzy search script */
16
- export const searchScript = () => fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "specbook-export-html-search.js"), "utf8");
17
- /* provide the build-time bundled fallback logo of SpecBook itself
18
- (as a self-contained data: URL, to keep its styles isolated) */
19
- export const fallbackLogo = () => "data:image/svg+xml;base64," + fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "specbook-export-logo.svg")).toString("base64");
14
+ export const searchScript = () => fs.readFileSync(new URL("specbook-export-html-search.js", import.meta.url), "utf8");
15
+ /* provide a theme variant of the build-time bundled fallback logo of
16
+ SpecBook itself (as a self-contained data: URL, to keep its styles isolated) */
17
+ export const fallbackLogo = (theme) => "data:image/svg+xml;base64," +
18
+ fs.readFileSync(new URL(`specbook-export-logo-${theme}.svg`, import.meta.url)).toString("base64");
20
19
  /* check whether an object is the specification title object */
21
20
  export const isTitleObject = (object) => object.kind === "META" && object.name.toUpperCase() === "TITLE";
22
- /* determine the title object of the specification */
23
- const titleObject = (specification) => specification.artifacts.flatMap((artifact) => artifact.objects).find(isTitleObject);
21
+ /* determine the title object of the specification, searched at any
22
+ nesting level in document order, where the first match wins */
23
+ export const titleObject = (specification) => {
24
+ const search = (objects) => {
25
+ for (const object of objects) {
26
+ if (isTitleObject(object))
27
+ return object;
28
+ const found = search(object.childs);
29
+ if (found !== undefined)
30
+ return found;
31
+ }
32
+ return undefined;
33
+ };
34
+ return search(specification.artifacts.flatMap((artifact) => artifact.objects));
35
+ };
24
36
  /* determine a property value of the title object */
25
37
  const titleProperty = (specification, name) => titleObject(specification)?.properties.find((property) => property.key === name)?.value;
26
38
  /* determine the document language (LANG) from the title object */
@@ -58,14 +70,15 @@ export const documentPaperSize = (specification) => {
58
70
  `(expected ${paperSizes.join(", ")})`);
59
71
  return paper;
60
72
  };
61
- /* provide the paper-dependent print stylesheet: a diagram is scaled
62
- down to still fit onto a single page (the paper height less the
63
- print margins, the own vertical margins of the diagram, and the
64
- room its introducing heading claims above it) and is never broken
65
- across a page boundary; without the heading reserve a maximally
73
+ /* the vertical room (in rem) the introducing heading of a diagram
74
+ claims above it on the same page: without this reserve a maximally
66
75
  sized diagram could not share its page with the heading, which
67
76
  would defeat the "break-after: avoid" bundling of the stylesheet */
68
77
  const headingReserve = 6;
78
+ /* provide the paper-dependent print stylesheet: a diagram is scaled
79
+ down to still fit onto a single page (the paper height less the
80
+ print margins, the own vertical margins of the diagram, and the
81
+ heading reserve above it) and is never broken across a page boundary */
69
82
  export const paperStylesheet = (paper) => {
70
83
  const setup = paperSetup(paper);
71
84
  const avail = setup.height - setup.margin.top - setup.margin.bottom;
@@ -118,9 +131,7 @@ const typographyGlyphs = [
118
131
  glyphs (no charset or a full Unicode charset keeps the fonts complete) */
119
132
  export const subsetStylesheet = async (charset) => {
120
133
  const css = stylesheet();
121
- if (charset === undefined)
122
- return css;
123
- const codepoints = charsetCodepoints(charset);
134
+ const codepoints = charset !== undefined ? charsetCodepoints(charset) : undefined;
124
135
  if (codepoints === undefined)
125
136
  return css;
126
137
  const { default: subsetFont } = await import("subset-font");
@@ -142,14 +153,15 @@ export const documentTitle = (specification) => ({
142
153
  subtitle: titleProperty(specification, "SUBTITLE")
143
154
  });
144
155
  /* determine the document logo as a self-contained data: URL, taken from the
145
- embedded image of the LOGO property of the title object, and falling back
146
- onto the built-in SpecBook logo (for use in isolated rendering contexts,
147
- like the print header/footer, which load no external resources) */
156
+ first embedded image of the LOGO property of the title object -- the light
157
+ variant of a "{theme}" reference -- and falling back onto the built-in
158
+ SpecBook logo (for use in isolated rendering contexts, like the print
159
+ header/footer, which load no external resources and are always light) */
148
160
  export const documentLogo = (specification) => {
149
161
  const content = titleObject(specification)
150
162
  ?.properties.find((property) => property.key === "LOGO")?.embedding?.[0];
151
- if (content === undefined)
152
- return fallbackLogo();
163
+ if (content === undefined || content === "")
164
+ return fallbackLogo("light");
153
165
  return content.startsWith("data:") ? content :
154
166
  `data:image/svg+xml;base64,${Buffer.from(content, "utf8").toString("base64")}`;
155
167
  };
@@ -18,7 +18,7 @@
18
18
 
19
19
  /* shortest word still eligible for fuzzy matching: below it the
20
20
  edit distance would equate it with almost any short word */
21
- const FUZZY_MIN = 4
21
+ const fuzzyMin = 4
22
22
 
23
23
  /* the searchable units (paragraphs, list items, table rows, and
24
24
  headings), the corpus vocabulary, and the per-word cache of the
@@ -101,7 +101,7 @@
101
101
  if (result !== undefined)
102
102
  return result
103
103
  result = []
104
- if (word.length >= FUZZY_MIN)
104
+ if (word.length >= fuzzyMin)
105
105
  vocab.forEach((given) => {
106
106
  if (Math.abs(word.length - given.length) <= 1
107
107
  && (diceCoefficient(word, given) >= 0.50
@@ -115,19 +115,21 @@
115
115
  /* match a query word against a unit, as a literal substring or as
116
116
  a fuzzy variant, remembering the variants for the highlighting */
117
117
  const wordMatch = (word, unit, variants) => {
118
- if (unit.text.indexOf(word) >= 0)
118
+ if (unit.text.includes(word))
119
119
  return true
120
120
  const near = fuzzyWords(word).filter((v) => unit.words.has(v))
121
121
  near.forEach((v) => { variants.add(v) })
122
122
  return near.length > 0
123
123
  }
124
124
 
125
+ /* escape a text for its literal use inside a regular expression */
126
+ const escapeRegex = (s) =>
127
+ s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
128
+
125
129
  /* the alternation matching everything worth highlighting: the
126
130
  query words as plain substrings plus the fuzzy variants as
127
131
  whole words, longest alternative first, so a variant is never
128
132
  cut short by a query word nested inside it */
129
- const escapeRegex = (s) =>
130
- s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
131
133
  const buildRegex = (words, variants) => {
132
134
  const alts = words.map((w) => ({ len: w.length, src: escapeRegex(w) }))
133
135
  .concat(variants.map((v) => ({ len: v.length,
@@ -203,7 +205,7 @@
203
205
  units.forEach((unit) => { unit.keep = false })
204
206
  parts.forEach((part) => {
205
207
  let matched = units.filter((unit) =>
206
- part.every((word) => unit.text.indexOf(word) >= 0))
208
+ part.every((word) => unit.text.includes(word)))
207
209
  if (matched.length === 0)
208
210
  matched = units.filter((unit) =>
209
211
  part.every((word) => wordMatch(word, unit, variants)))
@@ -226,7 +228,7 @@
226
228
  const heading = section.firstElementChild
227
229
  if (heading !== null && /^H[1-6]$/.test(heading.tagName))
228
230
  heading.classList.add("search-keep")
229
- section = section.parentElement !== null ? section.parentElement.closest("section") : null
231
+ section = section.parentElement?.closest("section") ?? null
230
232
  }
231
233
  })
232
234
 
@@ -244,13 +246,7 @@
244
246
  })
245
247
 
246
248
  /* highlight every matched word within the kept units */
247
- const words = []
248
- parts.forEach((part) => {
249
- part.forEach((word) => {
250
- if (!words.includes(word))
251
- words.push(word)
252
- })
253
- })
249
+ const words = Array.from(new Set(parts.flat()))
254
250
  const regex = buildRegex(words, Array.from(variants))
255
251
  units.forEach((unit) => {
256
252
  if (unit.el.classList.contains("search-keep"))