@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.
- package/README.md +108 -328
- package/dst/specbook-api.d.ts +13 -20
- package/dst/specbook-api.js +43 -39
- package/dst/specbook-cli.js +42 -55
- package/dst/specbook-cmd-describe.d.ts +5 -2
- package/dst/specbook-cmd-describe.js +30 -91
- package/dst/specbook-cmd-export.d.ts +3 -3
- package/dst/specbook-cmd-export.js +23 -19
- package/dst/specbook-cmd-init.d.ts +3 -3
- package/dst/specbook-cmd-init.js +38 -15
- package/dst/specbook-cmd-lint.d.ts +5 -6
- package/dst/specbook-cmd-lint.js +61 -24
- package/dst/specbook-config.d.ts +2 -2
- package/dst/specbook-config.js +82 -73
- package/dst/specbook-diagnostic.js +4 -1
- package/dst/specbook-diagram.d.ts +6 -5
- package/dst/specbook-diagram.js +94 -104
- package/dst/specbook-export-ast.d.ts +3 -3
- package/dst/specbook-export-ast.js +1 -1
- package/dst/specbook-export-common.d.ts +11 -9
- package/dst/specbook-export-common.js +34 -22
- package/dst/specbook-export-html-search.js +10 -14
- package/dst/specbook-export-html.css +37 -32
- package/dst/specbook-export-html.d.ts +4 -4
- package/dst/specbook-export-html.js +153 -107
- package/dst/specbook-export-logo-dark.svg +35 -0
- package/dst/specbook-export-logo-light.svg +35 -0
- package/dst/specbook-export-md.d.ts +3 -3
- package/dst/specbook-export-md.js +50 -24
- package/dst/specbook-export-pdf.d.ts +4 -2
- package/dst/specbook-export-pdf.js +48 -38
- package/dst/{specbook-struct-schema.d.ts → specbook-format-schema.d.ts} +3 -2
- package/dst/{specbook-struct-schema.js → specbook-format-schema.js} +8 -4
- package/dst/specbook-format-spec.d.ts +31 -0
- package/dst/{specbook-struct-spec.js → specbook-format-spec.js} +10 -10
- package/dst/specbook-format.md +750 -0
- package/dst/specbook-link.d.ts +3 -2
- package/dst/specbook-link.js +7 -4
- package/dst/specbook-mcp.d.ts +2 -1
- package/dst/specbook-mcp.js +30 -62
- package/dst/specbook-parse-common.d.ts +5 -3
- package/dst/specbook-parse-common.js +8 -0
- package/dst/specbook-parse-semantic.d.ts +6 -4
- package/dst/specbook-parse-semantic.js +98 -106
- package/dst/specbook-parse-syntax.d.ts +2 -2
- package/dst/specbook-parse-syntax.js +175 -144
- package/dst/specbook-parse-value.d.ts +1 -1
- package/dst/specbook-parse-value.js +6 -3
- package/dst/specbook-parse.d.ts +4 -7
- package/dst/specbook-parse.js +28 -35
- package/dst/specbook-verbose.d.ts +2 -0
- package/dst/specbook-verbose.js +17 -0
- package/package.json +3 -7
- package/dst/specbook-cmd-edit.d.ts +0 -9
- package/dst/specbook-cmd-edit.js +0 -74
- package/dst/specbook-cmd-import.d.ts +0 -9
- package/dst/specbook-cmd-import.js +0 -78
- package/dst/specbook-export-logo.svg +0 -35
- package/dst/specbook-llm.d.ts +0 -13
- package/dst/specbook-llm.js +0 -51
- package/dst/specbook-struct-spec.d.ts +0 -31
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
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:
|
|
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 =
|
|
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 {
|
|
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
|
|
8
|
-
export declare const
|
|
9
|
-
export declare const
|
|
10
|
-
export declare const
|
|
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:
|
|
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:
|
|
30
|
+
export declare const documentTitle: (specification: Spec) => {
|
|
29
31
|
title: string;
|
|
30
32
|
subtitle?: string;
|
|
31
33
|
};
|
|
32
|
-
export declare const documentLogo: (specification:
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
11
9
|
.replace(/"/g, """).replace(/'/g, "'");
|
|
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(
|
|
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(
|
|
17
|
-
/* provide the build-time bundled fallback logo of
|
|
18
|
-
(as a self-contained data: URL, to keep its styles isolated) */
|
|
19
|
-
export const fallbackLogo = () => "data:image/svg+xml;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
|
-
|
|
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
|
-
/*
|
|
62
|
-
|
|
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
|
-
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
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 >=
|
|
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.
|
|
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.
|
|
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
|
|
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"))
|