@rse/specbook 0.9.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/README.md +199 -0
- package/dst/specbook-api.d.ts +47 -0
- package/dst/specbook-api.js +77 -0
- package/dst/specbook-cli.d.ts +2 -0
- package/dst/specbook-cli.js +156 -0
- package/dst/specbook-cmd-describe.d.ts +2 -0
- package/dst/specbook-cmd-describe.js +97 -0
- package/dst/specbook-cmd-edit.d.ts +9 -0
- package/dst/specbook-cmd-edit.js +69 -0
- package/dst/specbook-cmd-export.d.ts +9 -0
- package/dst/specbook-cmd-export.js +78 -0
- package/dst/specbook-cmd-import.d.ts +9 -0
- package/dst/specbook-cmd-import.js +73 -0
- package/dst/specbook-cmd-init.d.ts +7 -0
- package/dst/specbook-cmd-init.js +37 -0
- package/dst/specbook-cmd-lint.d.ts +15 -0
- package/dst/specbook-cmd-lint.js +46 -0
- package/dst/specbook-config.d.ts +6 -0
- package/dst/specbook-config.js +91 -0
- package/dst/specbook-diagnostic.d.ts +8 -0
- package/dst/specbook-diagnostic.js +23 -0
- package/dst/specbook-diagram.d.ts +9 -0
- package/dst/specbook-diagram.js +271 -0
- package/dst/specbook-export-ast.d.ts +4 -0
- package/dst/specbook-export-ast.js +36 -0
- package/dst/specbook-export-common.d.ts +33 -0
- package/dst/specbook-export-common.js +142 -0
- package/dst/specbook-export-html-search.js +272 -0
- package/dst/specbook-export-html.css +259 -0
- package/dst/specbook-export-html.d.ts +9 -0
- package/dst/specbook-export-html.js +452 -0
- package/dst/specbook-export-logo.svg +35 -0
- package/dst/specbook-export-md.d.ts +3 -0
- package/dst/specbook-export-md.js +74 -0
- package/dst/specbook-export-pdf.d.ts +7 -0
- package/dst/specbook-export-pdf.js +198 -0
- package/dst/specbook-link.d.ts +17 -0
- package/dst/specbook-link.js +113 -0
- package/dst/specbook-llm.d.ts +13 -0
- package/dst/specbook-llm.js +61 -0
- package/dst/specbook-mcp.d.ts +1 -0
- package/dst/specbook-mcp.js +163 -0
- package/dst/specbook-parse-common.d.ts +26 -0
- package/dst/specbook-parse-common.js +33 -0
- package/dst/specbook-parse-semantic.d.ts +5 -0
- package/dst/specbook-parse-semantic.js +254 -0
- package/dst/specbook-parse-syntax.d.ts +3 -0
- package/dst/specbook-parse-syntax.js +363 -0
- package/dst/specbook-parse-value.d.ts +19 -0
- package/dst/specbook-parse-value.js +123 -0
- package/dst/specbook-parse.d.ts +8 -0
- package/dst/specbook-parse.js +43 -0
- package/dst/specbook-struct-schema.d.ts +35 -0
- package/dst/specbook-struct-schema.js +47 -0
- package/dst/specbook-struct-spec.d.ts +31 -0
- package/dst/specbook-struct-spec.js +36 -0
- package/dst/specbook-theme.d.ts +16 -0
- package/dst/specbook-theme.js +38 -0
- package/package.json +77 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Specification } from "./specbook-struct-spec.js";
|
|
2
|
+
import type { SchemaSpecification } from "./specbook-struct-schema.js";
|
|
3
|
+
export declare const formats: readonly ["json", "json5", "yaml", "toon", "html", "pdf", "md"];
|
|
4
|
+
export type ExportFormat = typeof formats[number];
|
|
5
|
+
export declare const parseOutputSpec: (spec: string) => {
|
|
6
|
+
format: ExportFormat;
|
|
7
|
+
output: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const exportSpecification: (specification: Specification, format: ExportFormat, verbose: (msg: string) => void, maxTableColumns?: number, config?: SchemaSpecification) => Promise<Buffer>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Specification Book (SpecBook)
|
|
3
|
+
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import { minify } from "@swc/html";
|
|
7
|
+
import { documentTitle, documentLogo, documentCharset, documentThemeTone, subsetStylesheet, documentPaperSize, paperStylesheet } from "./specbook-export-common.js";
|
|
8
|
+
import { themeColors, themeStylesheet, themeMapping } from "./specbook-theme.js";
|
|
9
|
+
import { renderAst } from "./specbook-export-ast.js";
|
|
10
|
+
import { renderMarkdown } from "./specbook-export-md.js";
|
|
11
|
+
import { renderHtml, htmlOutline } from "./specbook-export-html.js";
|
|
12
|
+
import { htmlToPdf } from "./specbook-export-pdf.js";
|
|
13
|
+
/* the supported export formats */
|
|
14
|
+
export const formats = ["json", "json5", "yaml", "toon", "html", "pdf", "md"];
|
|
15
|
+
/* the filename extensions mapping onto the export formats */
|
|
16
|
+
const extensions = {
|
|
17
|
+
json: "json",
|
|
18
|
+
json5: "json5",
|
|
19
|
+
yaml: "yaml", yml: "yaml",
|
|
20
|
+
toon: "toon",
|
|
21
|
+
html: "html", htm: "html",
|
|
22
|
+
pdf: "pdf",
|
|
23
|
+
md: "md", markdown: "md"
|
|
24
|
+
};
|
|
25
|
+
/* parse an output specification "[<format>:]<filename>", inferring the
|
|
26
|
+
format from the filename extension when not explicitly given
|
|
27
|
+
(plain "-" for stdout defaults to JSON) */
|
|
28
|
+
export const parseOutputSpec = (spec) => {
|
|
29
|
+
const prefixed = spec.match(/^([a-zA-Z0-9]+):(.+)$/);
|
|
30
|
+
if (prefixed !== null && formats.includes(prefixed[1]))
|
|
31
|
+
return { format: prefixed[1], output: prefixed[2] };
|
|
32
|
+
if (spec === "-")
|
|
33
|
+
return { format: "json", output: spec };
|
|
34
|
+
const ext = spec.match(/\.([a-zA-Z0-9]+)$/);
|
|
35
|
+
if (ext !== null && extensions[ext[1].toLowerCase()] !== undefined)
|
|
36
|
+
return { format: extensions[ext[1].toLowerCase()], output: spec };
|
|
37
|
+
throw new Error(`unable to infer export format from output "${spec}" ` +
|
|
38
|
+
"(use an explicit \"<format>:<filename>\" specification)");
|
|
39
|
+
};
|
|
40
|
+
/* export a specification into the requested format */
|
|
41
|
+
export const exportSpecification = async (specification, format, verbose, maxTableColumns = 4, config) => {
|
|
42
|
+
if (!formats.includes(format))
|
|
43
|
+
throw new Error(`unknown export format "${format}"`);
|
|
44
|
+
verbose(`exporting specification as "${format}"`);
|
|
45
|
+
if (format === "json" || format === "json5" || format === "yaml" || format === "toon")
|
|
46
|
+
return renderAst(specification, format, config);
|
|
47
|
+
else if (format === "md")
|
|
48
|
+
return Buffer.from(await renderMarkdown(specification, config), "utf8");
|
|
49
|
+
/* the HTML-based formats share the stylesheet, with the embedded
|
|
50
|
+
fonts subsetted to the CHARSET of the specification (if any) */
|
|
51
|
+
const charset = documentCharset(specification);
|
|
52
|
+
if (charset !== undefined)
|
|
53
|
+
verbose(`subsetting embedded fonts to charset "${charset}"`);
|
|
54
|
+
/* the theme tone (THEME-TONE) drives the layer-1 color spread
|
|
55
|
+
variables and the PDF decoration colors */
|
|
56
|
+
const tone = documentThemeTone(specification) ?? "#336699";
|
|
57
|
+
verbose(`generating theme color spreads (tone "${tone}")`);
|
|
58
|
+
const colors = themeColors(tone);
|
|
59
|
+
/* the paper size (PAPER-SIZE) drives the PDF page setup and the
|
|
60
|
+
print-time height constraint of the diagrams */
|
|
61
|
+
const paper = documentPaperSize(specification);
|
|
62
|
+
const css = themeStylesheet(colors) + await subsetStylesheet(charset) + paperStylesheet(paper);
|
|
63
|
+
if (format === "html") {
|
|
64
|
+
/* compress the rendered HTML (whitespace, comments, and inline CSS/JS) */
|
|
65
|
+
const html = await renderHtml(specification, maxTableColumns, config, undefined, css);
|
|
66
|
+
const minified = await minify(Buffer.from(html, "utf8"), {
|
|
67
|
+
collapseWhitespaces: "smart",
|
|
68
|
+
removeComments: true,
|
|
69
|
+
minifyCss: true,
|
|
70
|
+
minifyJs: true
|
|
71
|
+
});
|
|
72
|
+
return Buffer.from(minified.code, "utf8");
|
|
73
|
+
}
|
|
74
|
+
else
|
|
75
|
+
/* the PDF export (like print in general) always uses the light
|
|
76
|
+
theme, so its decoration colors are the light mapping, too */
|
|
77
|
+
return htmlToPdf((tocPages) => renderHtml(specification, maxTableColumns, config, tocPages, css), { ...documentTitle(specification), logo: documentLogo(specification) }, htmlOutline(specification, maxTableColumns), verbose, css, themeMapping(colors, "light"), paper);
|
|
78
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type SchemaSpecification } from "./specbook-struct-schema.js";
|
|
2
|
+
import { type AiOptions } from "./specbook-llm.js";
|
|
3
|
+
export interface ImportOptions extends AiOptions {
|
|
4
|
+
config: SchemaSpecification;
|
|
5
|
+
basedir?: string;
|
|
6
|
+
inputs: string[];
|
|
7
|
+
verbose: (msg: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare const importSpecification: (options: ImportOptions) => Promise<string[]>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Specification Book (SpecBook)
|
|
3
|
+
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import textframe from "textframe";
|
|
9
|
+
import { describeConfiguration } from "./specbook-cmd-describe.js";
|
|
10
|
+
import { complete, timestamp, renderFileBlock, parseFileBlocks } from "./specbook-llm.js";
|
|
11
|
+
/* the LLM instruction for the import command */
|
|
12
|
+
const instruction = (description, now) => textframe `
|
|
13
|
+
You are the import engine of SpecBook, a Markdown-based specification
|
|
14
|
+
format. Import the information of the given foreign sources into the
|
|
15
|
+
Markdown specification artifact files of the project, generating or
|
|
16
|
+
updating the artifact files so they faithfully reflect the imported
|
|
17
|
+
information. Strictly honor the following rules:
|
|
18
|
+
|
|
19
|
+
- Faithful reflection: the artifacts must reflect the imported
|
|
20
|
+
sources exactly -- no more, no less.
|
|
21
|
+
- No fabrication: never invent content the sources do not support;
|
|
22
|
+
leave gaps open instead of guessing.
|
|
23
|
+
- Surgical updates: when updating an existing artifact, change only
|
|
24
|
+
what the imported information actually requires.
|
|
25
|
+
- Level-appropriate translation: re-express the imported facts at the
|
|
26
|
+
artifact's level of abstraction; do not copy verbatim.
|
|
27
|
+
- Format conformance: strictly follow the specification format
|
|
28
|
+
described below.
|
|
29
|
+
- Markdown generation: emit the Complex Format on levels 1-3
|
|
30
|
+
("#"/"##"/"###") and the Concise Format on level 4 and deeper.
|
|
31
|
+
- Timestamps: use "${now}" for the "Created:" and "Modified:"
|
|
32
|
+
frontmatter of generated files, and refresh "Modified:" to
|
|
33
|
+
"${now}" on updated files.
|
|
34
|
+
- Files: generate or update ONLY the configured artifact files listed
|
|
35
|
+
in the format description below -- never any other file.
|
|
36
|
+
- Output: return ONLY the generated or updated files, each as a block
|
|
37
|
+
of the exact form below, and no other text at all:
|
|
38
|
+
|
|
39
|
+
<<<FILE: <name>>>>
|
|
40
|
+
<content>
|
|
41
|
+
<<<END-FILE>>>
|
|
42
|
+
|
|
43
|
+
${description}
|
|
44
|
+
`;
|
|
45
|
+
/* import foreign sources into the specification artifact files */
|
|
46
|
+
export const importSpecification = async (options) => {
|
|
47
|
+
const basedir = options.basedir ?? ".";
|
|
48
|
+
const files = options.config.map((artifact) => artifact.file).filter((file) => file !== undefined);
|
|
49
|
+
/* gather the existing artifact files and the foreign sources */
|
|
50
|
+
const existing = files
|
|
51
|
+
.filter((file) => fs.existsSync(path.join(basedir, file)))
|
|
52
|
+
.map((file) => renderFileBlock(file, fs.readFileSync(path.join(basedir, file), "utf8")));
|
|
53
|
+
const sources = options.inputs
|
|
54
|
+
.map((input) => renderFileBlock(input, fs.readFileSync(input, "utf8")));
|
|
55
|
+
/* let the LLM generate or update the artifact files */
|
|
56
|
+
options.verbose(`importing ${options.inputs.length} foreign source(s) into "${basedir}"`);
|
|
57
|
+
const system = instruction(describeConfiguration(options.config), timestamp());
|
|
58
|
+
const prompt = "EXISTING ARTIFACT FILES:\n\n" + (existing.length > 0 ? existing.join("\n\n") : "(none)") +
|
|
59
|
+
"\n\nFOREIGN SOURCES TO IMPORT:\n\n" + sources.join("\n\n");
|
|
60
|
+
const response = await complete(options, system, prompt);
|
|
61
|
+
/* write back all returned artifact files */
|
|
62
|
+
const written = new Array();
|
|
63
|
+
for (const block of parseFileBlocks(response)) {
|
|
64
|
+
if (!files.includes(block.name)) {
|
|
65
|
+
options.verbose(`ignoring unconfigured file "${block.name}"`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
fs.writeFileSync(path.join(basedir, block.name), block.content, "utf8");
|
|
69
|
+
options.verbose(`wrote artifact file "${block.name}"`);
|
|
70
|
+
written.push(block.name);
|
|
71
|
+
}
|
|
72
|
+
return written;
|
|
73
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type SchemaSpecification } from "./specbook-struct-schema.js";
|
|
2
|
+
export interface InitOptions {
|
|
3
|
+
config: SchemaSpecification;
|
|
4
|
+
basedir?: string;
|
|
5
|
+
verbose: (msg: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare const initSpecification: (options: InitOptions) => string[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Specification Book (SpecBook)
|
|
3
|
+
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { timestamp } from "./specbook-llm.js";
|
|
9
|
+
/* initialize the configured specification artifact files below the
|
|
10
|
+
base directory with their frontmatter and artifact heading */
|
|
11
|
+
export const initSpecification = (options) => {
|
|
12
|
+
const basedir = options.basedir ?? ".";
|
|
13
|
+
fs.mkdirSync(basedir, { recursive: true });
|
|
14
|
+
const now = timestamp();
|
|
15
|
+
const created = new Array();
|
|
16
|
+
for (const artifact of options.config) {
|
|
17
|
+
if (artifact.file === undefined)
|
|
18
|
+
continue;
|
|
19
|
+
const file = path.join(basedir, artifact.file);
|
|
20
|
+
if (fs.existsSync(file)) {
|
|
21
|
+
options.verbose(`skipping existing artifact file "${artifact.file}"`);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const name = (artifact.name ?? "").toUpperCase();
|
|
25
|
+
const text = "---\n" +
|
|
26
|
+
`Created: ${now}\n` +
|
|
27
|
+
`Modified: ${now}\n` +
|
|
28
|
+
"---\n" +
|
|
29
|
+
"\n" +
|
|
30
|
+
`# ${artifact.kind}: ${name} (${artifact.kind}-${artifact.id})\n` +
|
|
31
|
+
"\n";
|
|
32
|
+
fs.writeFileSync(file, text, "utf8");
|
|
33
|
+
options.verbose(`created artifact file "${artifact.file}"`);
|
|
34
|
+
created.push(artifact.file);
|
|
35
|
+
}
|
|
36
|
+
return created;
|
|
37
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Diagnostic } from "./specbook-diagnostic.js";
|
|
2
|
+
import { type Specification } from "./specbook-struct-spec.js";
|
|
3
|
+
import { type SchemaSpecification } from "./specbook-struct-schema.js";
|
|
4
|
+
export interface LintOptions {
|
|
5
|
+
config?: string;
|
|
6
|
+
basedir: string;
|
|
7
|
+
verbose: (msg: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export interface LintResult {
|
|
10
|
+
specification: Specification;
|
|
11
|
+
diagnostics: Diagnostic[];
|
|
12
|
+
config?: SchemaSpecification;
|
|
13
|
+
}
|
|
14
|
+
export declare const scanMarkdown: (basedir: string) => string[];
|
|
15
|
+
export declare const lint: (options: LintOptions) => LintResult;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Specification Book (SpecBook)
|
|
3
|
+
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { loadConfig } from "./specbook-config.js";
|
|
9
|
+
import { parseSpecification } from "./specbook-parse.js";
|
|
10
|
+
/* recursively scan a base directory for Markdown files */
|
|
11
|
+
export const scanMarkdown = (basedir) => fs.readdirSync(basedir, { recursive: true, withFileTypes: true })
|
|
12
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
|
|
13
|
+
.map((entry) => path.join(entry.parentPath, entry.name))
|
|
14
|
+
.sort();
|
|
15
|
+
/* lint the specification Markdown files below the base directory
|
|
16
|
+
against the optional configuration */
|
|
17
|
+
export const lint = (options) => {
|
|
18
|
+
const diagnostics = new Array();
|
|
19
|
+
/* load the optional YAML schema configuration */
|
|
20
|
+
let config;
|
|
21
|
+
if (options.config !== undefined) {
|
|
22
|
+
options.verbose(`loading configuration "${options.config}"`);
|
|
23
|
+
const loaded = loadConfig(options.config);
|
|
24
|
+
diagnostics.push(...loaded.diagnostics);
|
|
25
|
+
config = loaded.config ?? undefined;
|
|
26
|
+
}
|
|
27
|
+
/* parse and validate the specification Markdown files */
|
|
28
|
+
const files = scanMarkdown(options.basedir);
|
|
29
|
+
options.verbose(`parsing ${files.length} specification file(s) below "${options.basedir}"`);
|
|
30
|
+
if (files.length === 0)
|
|
31
|
+
diagnostics.push({ file: options.basedir, line: 1, column: 1,
|
|
32
|
+
message: "no Markdown files found below base directory" });
|
|
33
|
+
const sources = new Array();
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
try {
|
|
36
|
+
sources.push({ file, text: fs.readFileSync(file, "utf8") });
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
diagnostics.push({ file, line: 1, column: 1,
|
|
40
|
+
message: `unreadable file: ${err instanceof Error ? err.message : String(err)}` });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const result = parseSpecification(sources, config);
|
|
44
|
+
diagnostics.push(...result.diagnostics);
|
|
45
|
+
return { specification: result.specification, diagnostics, config };
|
|
46
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Specification Book (SpecBook)
|
|
3
|
+
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import { parse as parseYaml, parseDocument, YAMLParseError } from "yaml";
|
|
8
|
+
import * as v from "valibot";
|
|
9
|
+
import { SchemaSpecification } from "./specbook-struct-schema.js";
|
|
10
|
+
import { compileValueExpr } from "./specbook-parse-value.js";
|
|
11
|
+
/* determine line/column of a YAML document path via the CST */
|
|
12
|
+
const lineColOfPath = (yaml, cst, path) => {
|
|
13
|
+
for (let i = path.length; i >= 0; i--) {
|
|
14
|
+
const node = cst.getIn(path.slice(0, i), true);
|
|
15
|
+
if (typeof node === "object" && node !== null
|
|
16
|
+
&& "range" in node && Array.isArray(node.range)) {
|
|
17
|
+
const start = node.range[0];
|
|
18
|
+
const lines = yaml.slice(0, start).split("\n");
|
|
19
|
+
return { line: lines.length, column: lines[lines.length - 1].length + 1 };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { line: 1, column: 1 };
|
|
23
|
+
};
|
|
24
|
+
/* load and validate a YAML schema configuration file */
|
|
25
|
+
export const loadConfig = (file) => {
|
|
26
|
+
const diagnostics = [];
|
|
27
|
+
const yaml = fs.readFileSync(file, "utf8");
|
|
28
|
+
/* syntactically parse the YAML content */
|
|
29
|
+
let obj;
|
|
30
|
+
try {
|
|
31
|
+
obj = parseYaml(yaml);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const e = err instanceof YAMLParseError ? err : null;
|
|
35
|
+
diagnostics.push({
|
|
36
|
+
file,
|
|
37
|
+
line: e?.linePos?.[0]?.line ?? 1,
|
|
38
|
+
column: e?.linePos?.[0]?.col ?? 1,
|
|
39
|
+
message: "invalid YAML syntax: " +
|
|
40
|
+
(err instanceof Error ? err.message.replace(/:(?:.|\r?\n)*$/, "") : String(err))
|
|
41
|
+
});
|
|
42
|
+
return { config: null, diagnostics };
|
|
43
|
+
}
|
|
44
|
+
/* semantically validate against the schema of the configuration */
|
|
45
|
+
const cst = parseDocument(yaml);
|
|
46
|
+
const result = v.safeParse(SchemaSpecification, obj);
|
|
47
|
+
if (!result.success) {
|
|
48
|
+
for (const issue of result.issues) {
|
|
49
|
+
const path = (issue.path ?? []).map((item) => item.key);
|
|
50
|
+
const pos = lineColOfPath(yaml, cst, path);
|
|
51
|
+
diagnostics.push({ file, line: pos.line, column: pos.column,
|
|
52
|
+
message: `invalid configuration: ${path.join(".")}: ${issue.message}` });
|
|
53
|
+
}
|
|
54
|
+
return { config: null, diagnostics };
|
|
55
|
+
}
|
|
56
|
+
/* reject "file" fields below the first (artifact) level */
|
|
57
|
+
const checkFileField = (objects, path, depth) => {
|
|
58
|
+
objects.forEach((object, i) => {
|
|
59
|
+
if (depth > 1 && object.file !== undefined) {
|
|
60
|
+
const pos = lineColOfPath(yaml, cst, [...path, i, "file"]);
|
|
61
|
+
diagnostics.push({ file, line: pos.line, column: pos.column,
|
|
62
|
+
message: `"file" field is only allowed on the first (artifact) level (found on level ${depth})` });
|
|
63
|
+
}
|
|
64
|
+
if (object.childs !== undefined)
|
|
65
|
+
checkFileField(object.childs, [...path, i, "childs"], depth + 1);
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
checkFileField(result.output, [], 1);
|
|
69
|
+
/* reject syntactically invalid property value expressions */
|
|
70
|
+
const checkValueExpr = (objects, path) => {
|
|
71
|
+
objects.forEach((object, i) => {
|
|
72
|
+
(object.props ?? []).forEach((prop, j) => {
|
|
73
|
+
if (prop.value === undefined)
|
|
74
|
+
return;
|
|
75
|
+
try {
|
|
76
|
+
compileValueExpr(prop.value);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const pos = lineColOfPath(yaml, cst, [...path, i, "props", j, "value"]);
|
|
80
|
+
diagnostics.push({ file, line: pos.line, column: pos.column,
|
|
81
|
+
message: `invalid value expression "${prop.value}": ` +
|
|
82
|
+
(err instanceof Error ? err.message : String(err)) });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
if (object.childs !== undefined)
|
|
86
|
+
checkValueExpr(object.childs, [...path, i, "childs"]);
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
checkValueExpr(result.output, []);
|
|
90
|
+
return { config: diagnostics.length === 0 ? result.output : null, diagnostics };
|
|
91
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface Diagnostic {
|
|
2
|
+
file: string;
|
|
3
|
+
line: number;
|
|
4
|
+
column: number;
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const renderDiagnostic: (diagnostic: Diagnostic) => string;
|
|
8
|
+
export declare const renderDiagnosticVerbose: (diagnostic: Diagnostic, colors?: boolean) => string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Specification Book (SpecBook)
|
|
3
|
+
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import sourceCodeError from "source-code-error";
|
|
8
|
+
/* render a diagnostic as a standard single-line message */
|
|
9
|
+
export const renderDiagnostic = (diagnostic) => `${diagnostic.file}:${diagnostic.line}:${diagnostic.column}: ${diagnostic.message}`;
|
|
10
|
+
/* render a diagnostic as a multi-line message with the affected
|
|
11
|
+
source snippet, falling back to the single-line message when the
|
|
12
|
+
source file is unreadable (e.g. the file is a directory) */
|
|
13
|
+
export const renderDiagnosticVerbose = (diagnostic, colors = false) => {
|
|
14
|
+
let code;
|
|
15
|
+
try {
|
|
16
|
+
code = fs.readFileSync(diagnostic.file, "utf8");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return `${renderDiagnostic(diagnostic)}\n`;
|
|
20
|
+
}
|
|
21
|
+
return sourceCodeError({ message: diagnostic.message, filename: diagnostic.file,
|
|
22
|
+
code, line: diagnostic.line, column: diagnostic.column, colors });
|
|
23
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Specification, Object as SpecObject } from "./specbook-struct-spec.js";
|
|
2
|
+
import type { SchemaSpecification } from "./specbook-struct-schema.js";
|
|
3
|
+
import { type ParseContext } from "./specbook-parse-common.js";
|
|
4
|
+
export interface DiagramResult {
|
|
5
|
+
spec?: string;
|
|
6
|
+
errors: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare const specDiagrams: (specification: Specification, config: SchemaSpecification) => Map<SpecObject, DiagramResult>;
|
|
9
|
+
export declare const validateDiagrams: (ctx: ParseContext, specification: Specification, config: SchemaSpecification) => void;
|