@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
package/dst/specbook-api.js
CHANGED
|
@@ -4,38 +4,42 @@
|
|
|
4
4
|
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
|
-
import * as path from "node:path";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
7
|
import { loadConfig } from "./specbook-config.js";
|
|
10
8
|
import { renderDiagnostic, renderDiagnosticVerbose } from "./specbook-diagnostic.js";
|
|
11
9
|
import { initSpecification } from "./specbook-cmd-init.js";
|
|
12
10
|
import { lint } from "./specbook-cmd-lint.js";
|
|
13
11
|
import { exportSpecification, parseOutputSpec, formats } from "./specbook-cmd-export.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { editSpecification } from "./specbook-cmd-edit.js";
|
|
12
|
+
import { describeFormat } from "./specbook-cmd-describe.js";
|
|
13
|
+
import { literal, renderVerbose } from "./specbook-verbose.js";
|
|
17
14
|
/* re-export the central types for API consumers */
|
|
15
|
+
export { literal, renderVerbose };
|
|
18
16
|
export { formats, parseOutputSpec };
|
|
19
17
|
export { renderDiagnostic, renderDiagnosticVerbose };
|
|
20
|
-
/*
|
|
18
|
+
/* our own version, taken from the package manifest, which resides one
|
|
21
19
|
level above both the source and the compiled module directory */
|
|
22
|
-
export const version = (()
|
|
23
|
-
const manifest = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
24
|
-
return JSON.parse(fs.readFileSync(manifest, "utf8")).version;
|
|
25
|
-
})();
|
|
20
|
+
export const version = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
26
21
|
/* the SpecBook API */
|
|
27
22
|
export class SpecBook {
|
|
28
23
|
verbose;
|
|
29
24
|
constructor(options = {}) {
|
|
30
25
|
this.verbose = options.verbose ?? (() => { });
|
|
31
26
|
}
|
|
32
|
-
/*
|
|
33
|
-
|
|
27
|
+
/* bind the verbose sink to a particular command */
|
|
28
|
+
verboseOf(cmd) {
|
|
29
|
+
return (msg) => this.verbose(cmd, msg);
|
|
30
|
+
}
|
|
31
|
+
/* require the file of the mandatory YAML schema configuration */
|
|
32
|
+
requireConfigFile(file) {
|
|
34
33
|
if (file === undefined)
|
|
35
34
|
throw new Error("YAML schema configuration required");
|
|
36
|
-
|
|
35
|
+
return file;
|
|
36
|
+
}
|
|
37
|
+
/* load a mandatory YAML schema configuration, failing on any problem */
|
|
38
|
+
requireConfig(file, verbose) {
|
|
39
|
+
file = this.requireConfigFile(file);
|
|
40
|
+
verbose(`loading configuration "${literal(file)}"`);
|
|
37
41
|
const { config, diagnostics } = loadConfig(file);
|
|
38
|
-
if (config ===
|
|
42
|
+
if (config === undefined)
|
|
39
43
|
throw new Error("invalid configuration:\n" +
|
|
40
44
|
diagnostics.map(renderDiagnostic).join("\n"));
|
|
41
45
|
return config;
|
|
@@ -43,44 +47,44 @@ export class SpecBook {
|
|
|
43
47
|
/* initialize the configured specification artifact files
|
|
44
48
|
below the base directory */
|
|
45
49
|
async init(options) {
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
const verbose = this.verboseOf("init");
|
|
51
|
+
return initSpecification({ config: this.requireConfig(options.config, verbose),
|
|
52
|
+
basedir: options.basedir ?? ".", verbose });
|
|
48
53
|
}
|
|
49
54
|
/* lint the specification Markdown files below the base directory */
|
|
50
55
|
async lint(options) {
|
|
51
|
-
return lint({ config: options.config,
|
|
56
|
+
return lint({ config: this.requireConfigFile(options.config),
|
|
57
|
+
basedir: options.basedir ?? ".", verbose: this.verboseOf("lint") });
|
|
52
58
|
}
|
|
53
59
|
/* export the specification Markdown files below the base directory
|
|
54
60
|
as JSON, JSON5, YAML, TOON, HTML, PDF, or normalized Markdown,
|
|
55
61
|
parsing the input just once and returning one buffer per
|
|
56
|
-
requested format (
|
|
57
|
-
|
|
62
|
+
requested format (strict: any diagnostic prevents the export,
|
|
63
|
+
as a partial or invalid specification must never be emitted) */
|
|
58
64
|
async export(options) {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (result.
|
|
63
|
-
throw new Error("
|
|
65
|
+
const verbose = this.verboseOf("export");
|
|
66
|
+
const result = lint({ config: this.requireConfigFile(options.config),
|
|
67
|
+
basedir: options.basedir ?? ".", verbose });
|
|
68
|
+
if (result.diagnostics.length > 0)
|
|
69
|
+
throw new Error("invalid specification:\n" +
|
|
64
70
|
result.diagnostics.map(renderDiagnostic).join("\n"));
|
|
71
|
+
if (result.specification.artifacts.length === 0)
|
|
72
|
+
throw new Error("unexportable specification: no artifacts found");
|
|
65
73
|
const buffers = new Array();
|
|
66
74
|
for (const format of options.formats ?? ["json"])
|
|
67
|
-
buffers.push(await exportSpecification(result.specification, format,
|
|
75
|
+
buffers.push(await exportSpecification(result.specification, format, verbose, result.config));
|
|
68
76
|
return buffers;
|
|
69
77
|
}
|
|
70
|
-
/* describe the
|
|
78
|
+
/* describe the generic SpecBook models and formats as Markdown,
|
|
79
|
+
optionally pointing to the artifacts of the particular project,
|
|
80
|
+
whose YAML schema configuration is validated if given */
|
|
71
81
|
async describe(options) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
/* apply an edit request to the specification artifact files
|
|
81
|
-
below the base directory */
|
|
82
|
-
async edit(options) {
|
|
83
|
-
return editSpecification({ ...options,
|
|
84
|
-
config: this.requireConfig(options.config), verbose: this.verbose });
|
|
82
|
+
const verbose = this.verboseOf("describe");
|
|
83
|
+
if (options.config !== undefined)
|
|
84
|
+
this.requireConfig(options.config, verbose);
|
|
85
|
+
else if (options.embed === true)
|
|
86
|
+
throw new Error("YAML schema configuration required for embedding");
|
|
87
|
+
verbose("describing the SpecBook models and formats");
|
|
88
|
+
return describeFormat(options);
|
|
85
89
|
}
|
|
86
90
|
}
|
package/dst/specbook-cli.js
CHANGED
|
@@ -6,13 +6,18 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import { Command } from "commander";
|
|
9
|
-
import
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import { SpecBook, renderDiagnostic, renderDiagnosticVerbose, renderVerbose, literal, parseOutputSpec, version } from "./specbook-api.js";
|
|
10
11
|
import { serveMcp } from "./specbook-mcp.js";
|
|
11
|
-
/* route verbose messages to stderr, keeping stdout reserved
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if
|
|
15
|
-
|
|
12
|
+
/* route verbose messages to stderr, keeping stdout reserved for the
|
|
13
|
+
command outputs and the MCP protocol, and qualify every message with
|
|
14
|
+
the tool name plus the scope path of the emitting command, where
|
|
15
|
+
Chalk styles the output only if the terminal supports colors */
|
|
16
|
+
const verboseOf = (opts, ...scope) => (cmd, msg) => {
|
|
17
|
+
if (opts.verbose) {
|
|
18
|
+
const scopePath = [...scope, cmd].map((segment) => chalk.bold(segment)).join(": ");
|
|
19
|
+
process.stderr.write(`specbook: ${scopePath}: ${renderVerbose(msg, chalk.blue)}\n`);
|
|
20
|
+
}
|
|
16
21
|
};
|
|
17
22
|
/* write a buffer to stdout, awaiting the write callback which only
|
|
18
23
|
fires once the data has been flushed to the underlying pipe, so a
|
|
@@ -28,12 +33,12 @@ const writeStdout = (data) => {
|
|
|
28
33
|
});
|
|
29
34
|
};
|
|
30
35
|
/* write a command result to the output file or stdout */
|
|
31
|
-
const writeOutput = async (output, data, verbose) => {
|
|
36
|
+
const writeOutput = async (output, data, cmd, verbose) => {
|
|
32
37
|
if (output === "-")
|
|
33
38
|
await writeStdout(data);
|
|
34
39
|
else {
|
|
35
40
|
await fs.promises.writeFile(output, data);
|
|
36
|
-
verbose(`wrote "${output}" (${Buffer.byteLength(data)} bytes)`);
|
|
41
|
+
verbose(cmd, `wrote "${literal(output)}" (${literal(Buffer.byteLength(data))} bytes)`);
|
|
37
42
|
}
|
|
38
43
|
};
|
|
39
44
|
/* determine an option default value, overridable via a
|
|
@@ -45,10 +50,14 @@ const envDefaultFlag = (name, fallback) => {
|
|
|
45
50
|
const value = process.env[`SPECBOOK_${name.toUpperCase().replace(/-/g, "_")}`];
|
|
46
51
|
return value !== undefined ? !(/^(?:|0|false|no|off)$/i).test(value) : fallback;
|
|
47
52
|
};
|
|
48
|
-
/* provide the
|
|
49
|
-
const
|
|
50
|
-
.option("-v, --verbose", "print verbose processing information to stderr", envDefaultFlag("verbose", false))
|
|
51
|
-
|
|
53
|
+
/* provide the verbose option of all sub-commands */
|
|
54
|
+
const withVerboseOption = (command) => command
|
|
55
|
+
.option("-v, --verbose", "print verbose processing information to stderr", envDefaultFlag("verbose", false));
|
|
56
|
+
/* provide the common options of the specification processing sub-commands,
|
|
57
|
+
for which the YAML schema configuration is mandatory */
|
|
58
|
+
const withCommonOptions = (command) => withVerboseOption(command)
|
|
59
|
+
.requiredOption("-c, --config <yaml-file>", "YAML schema configuration file", envDefault("config"))
|
|
60
|
+
.option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir", "."));
|
|
52
61
|
/* parse the command line */
|
|
53
62
|
const program = new Command();
|
|
54
63
|
program.name("specbook")
|
|
@@ -57,16 +66,16 @@ program.name("specbook")
|
|
|
57
66
|
.action(() => {
|
|
58
67
|
program.help();
|
|
59
68
|
});
|
|
60
|
-
|
|
69
|
+
/* the mcp command runs all other commands as MCP tools over stdio */
|
|
70
|
+
withVerboseOption(program.command("mcp"))
|
|
61
71
|
.description("run as MCP stdio server")
|
|
62
|
-
.option("-v, --verbose", "print verbose processing information to stderr", envDefaultFlag("verbose", false))
|
|
63
72
|
.action(async (opts) => {
|
|
64
|
-
verboseOf(opts)("starting MCP server on stdio");
|
|
65
|
-
await serveMcp(verboseOf(opts));
|
|
73
|
+
verboseOf(opts)("mcp", "starting MCP server on stdio");
|
|
74
|
+
await serveMcp(verboseOf(opts, "mcp"));
|
|
66
75
|
});
|
|
76
|
+
/* the init command creates the configured artifact files */
|
|
67
77
|
withCommonOptions(program.command("init"))
|
|
68
78
|
.description("initialize the configured specification artifact files below the base directory")
|
|
69
|
-
.option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir", "."))
|
|
70
79
|
.action(async (opts) => {
|
|
71
80
|
const specbook = new SpecBook({ verbose: verboseOf(opts) });
|
|
72
81
|
const created = await specbook.init({ config: opts.config, basedir: opts.basedir });
|
|
@@ -74,9 +83,9 @@ withCommonOptions(program.command("init"))
|
|
|
74
83
|
`initialized artifact file(s): ${created.join(", ")}\n` :
|
|
75
84
|
"no artifact files were created\n");
|
|
76
85
|
});
|
|
86
|
+
/* the lint command reports all diagnostics and fails on any of them */
|
|
77
87
|
withCommonOptions(program.command("lint"))
|
|
78
88
|
.description("lint the specification Markdown files below the base directory")
|
|
79
|
-
.option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir", "."))
|
|
80
89
|
.action(async (opts) => {
|
|
81
90
|
const specbook = new SpecBook({ verbose: verboseOf(opts) });
|
|
82
91
|
const result = await specbook.lint({ config: opts.config, basedir: opts.basedir });
|
|
@@ -87,61 +96,39 @@ withCommonOptions(program.command("lint"))
|
|
|
87
96
|
if (result.diagnostics.length > 0)
|
|
88
97
|
process.exitCode = 1;
|
|
89
98
|
else
|
|
90
|
-
verboseOf(opts)("specification valid");
|
|
99
|
+
verboseOf(opts)("lint", "specification valid");
|
|
91
100
|
});
|
|
101
|
+
/* the export command parses the input once and writes every output */
|
|
92
102
|
withCommonOptions(program.command("export"))
|
|
93
103
|
.description("export the specification Markdown files below the base directory " +
|
|
94
104
|
"as JSON, JSON5, YAML, TOON, HTML, PDF, or normalized Markdown")
|
|
95
|
-
.option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir", "."))
|
|
96
105
|
.option("-o, --output [<format>:]<output-file>", "output file (\"-\" for stdout, repeatable), with the format inferred " +
|
|
97
|
-
"from the filename extension unless explicitly prefixed", (value, previous) => previous.concat(value),
|
|
106
|
+
"from the filename extension unless explicitly prefixed", (value, previous) => previous.concat(value), new Array())
|
|
98
107
|
.action(async (opts) => {
|
|
99
108
|
const specbook = new SpecBook({ verbose: verboseOf(opts) });
|
|
100
109
|
const outputs = (opts.output.length > 0 ? opts.output : [envDefault("output") ?? "-"])
|
|
101
|
-
.map(
|
|
110
|
+
.map(parseOutputSpec);
|
|
102
111
|
/* parse the input once and export each distinct format once */
|
|
103
112
|
const distinct = Array.from(new Set(outputs.map(({ format }) => format)));
|
|
104
113
|
const buffers = await specbook.export({ config: opts.config, basedir: opts.basedir,
|
|
105
114
|
formats: distinct });
|
|
106
115
|
for (const { format, output } of outputs)
|
|
107
|
-
await writeOutput(output, buffers[distinct.indexOf(format)], verboseOf(opts));
|
|
116
|
+
await writeOutput(output, buffers[distinct.indexOf(format)], "export", verboseOf(opts));
|
|
108
117
|
});
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
/* the describe command also describes the generic SpecBook models and
|
|
119
|
+
formats alone, so its YAML schema configuration stays optional */
|
|
120
|
+
withVerboseOption(program.command("describe"))
|
|
121
|
+
.description("describe the SpecBook models and formats as Markdown")
|
|
122
|
+
.option("-c, --config <yaml-file>", "YAML schema configuration file", envDefault("config"))
|
|
123
|
+
.option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir"))
|
|
124
|
+
.option("-e, --embed", "embed the YAML schema configuration instead of just referencing it", envDefaultFlag("embed", false))
|
|
111
125
|
.option("-o, --output <markdown-file>", "output file (\"-\" for stdout)", envDefault("output", "-"))
|
|
112
126
|
.action(async (opts) => {
|
|
113
127
|
const specbook = new SpecBook({ verbose: verboseOf(opts) });
|
|
114
|
-
const text = await specbook.describe({ config: opts.config });
|
|
115
|
-
await writeOutput(opts.output, text, verboseOf(opts));
|
|
116
|
-
});
|
|
117
|
-
withCommonOptions(program.command("import"))
|
|
118
|
-
.description("import foreign sources into the specification artifact files below the base directory")
|
|
119
|
-
.option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir", "."))
|
|
120
|
-
.option("--provider <provider>", "AI provider (\"anthropic\", \"openai\", \"openrouter\" or \"ollama\")", envDefault("provider"))
|
|
121
|
-
.option("--model <model>", "AI model", envDefault("model"))
|
|
122
|
-
.argument("<input-files...>", "foreign source files to import")
|
|
123
|
-
.action(async (inputs, opts) => {
|
|
124
|
-
const specbook = new SpecBook({ verbose: verboseOf(opts) });
|
|
125
|
-
const written = await specbook.import({ config: opts.config, basedir: opts.basedir,
|
|
126
|
-
inputs, provider: opts.provider, model: opts.model });
|
|
127
|
-
await writeStdout(written.length > 0 ?
|
|
128
|
-
`imported into artifact file(s): ${written.join(", ")}\n` :
|
|
129
|
-
"no artifact files were changed\n");
|
|
130
|
-
});
|
|
131
|
-
withCommonOptions(program.command("edit"))
|
|
132
|
-
.description("apply a free-text edit request to the specification artifact files below the base directory")
|
|
133
|
-
.option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir", "."))
|
|
134
|
-
.option("--provider <provider>", "AI provider (\"anthropic\", \"openai\", \"openrouter\" or \"ollama\")", envDefault("provider"))
|
|
135
|
-
.option("--model <model>", "AI model", envDefault("model"))
|
|
136
|
-
.argument("<query>", "free-text edit request")
|
|
137
|
-
.action(async (query, opts) => {
|
|
138
|
-
const specbook = new SpecBook({ verbose: verboseOf(opts) });
|
|
139
|
-
const written = await specbook.edit({ config: opts.config, basedir: opts.basedir,
|
|
140
|
-
query, provider: opts.provider, model: opts.model });
|
|
141
|
-
await writeStdout(written.length > 0 ?
|
|
142
|
-
`edited artifact file(s): ${written.join(", ")}\n` :
|
|
143
|
-
"no artifact files were changed\n");
|
|
128
|
+
const text = await specbook.describe({ config: opts.config, basedir: opts.basedir, embed: opts.embed });
|
|
129
|
+
await writeOutput(opts.output, text, "describe", verboseOf(opts));
|
|
144
130
|
});
|
|
131
|
+
/* run the command line program */
|
|
145
132
|
try {
|
|
146
133
|
await program.parseAsync();
|
|
147
134
|
}
|
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export declare const describeFormat: (options: {
|
|
2
|
+
config?: string;
|
|
3
|
+
basedir?: string;
|
|
4
|
+
embed?: boolean;
|
|
5
|
+
}) => string;
|
|
@@ -3,97 +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 * as fs from "node:fs";
|
|
6
7
|
import textframe from "textframe";
|
|
7
|
-
/*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
## Concise Format (used on level 4 and deeper)
|
|
36
|
-
|
|
37
|
-
\`\`\`
|
|
38
|
-
- <kind>: <name>; <key>: <value>; [...]; <statement>, BECAUSE <rationale>.
|
|
39
|
-
\`\`\`
|
|
40
|
-
|
|
41
|
-
Child objects nest as indented list items below their parent item.
|
|
42
|
-
|
|
43
|
-
## Primary Marker
|
|
44
|
-
|
|
45
|
-
In both formats a literal \`(*)\` directly after the \`<name>\` marks the
|
|
46
|
-
object as *primary* (e.g. the primary attribute of an entity). It is
|
|
47
|
-
distinct from an id given as \`(<id>)\` and both may be combined.
|
|
48
|
-
|
|
49
|
-
## Configured Artifacts
|
|
50
|
-
|
|
51
|
-
The following artifacts, object kinds, and properties are configured.
|
|
52
|
-
Object kinds and property names are *case-sensitive* and must be
|
|
53
|
-
written exactly as configured. Properties marked \`required\` must be
|
|
54
|
-
present, and property values must match their given constraint (if
|
|
55
|
-
any, else any value is allowed): \`/xxx/\` requires a match of the
|
|
56
|
-
regular expression, \`[[xxx]]\` requires exactly one Wiki-style link
|
|
57
|
-
reference resolving into the (usually wildcard) reference pattern,
|
|
58
|
-
\`enum(xxx,yyy)\` requires one of the listed members, \`tags(xxx,yyy)\`
|
|
59
|
-
requires a comma-separated set of the listed members (each at most
|
|
60
|
-
once), and \`list(xxx[, ...])\` requires a comma-separated list of
|
|
61
|
-
items, each matching one of the alternative constraints.
|
|
62
|
-
`;
|
|
63
|
-
/* describe a single configured property */
|
|
64
|
-
const describeProperty = (prop, indent) => {
|
|
65
|
-
let line = `${indent}- property \`${prop.name}\` (${prop.optional === true ? "optional" : "required"}`;
|
|
66
|
-
if (prop.value !== undefined)
|
|
67
|
-
line += `, constraint \`${prop.value}\``;
|
|
68
|
-
line += `): ${collapse(prop.desc)}`;
|
|
69
|
-
return line;
|
|
70
|
-
};
|
|
71
|
-
/* describe a single configured object kind (and recursively its childs) */
|
|
72
|
-
const describeObject = (object, level, indent) => {
|
|
73
|
-
const lines = new Array();
|
|
74
|
-
lines.push(`${indent}- object kind \`${object.kind}\` on level ${level}` +
|
|
75
|
-
`${object.optional === true ? " (optional)" : ""}: ${collapse(object.desc)}`);
|
|
76
|
-
for (const prop of object.props ?? [])
|
|
77
|
-
lines.push(describeProperty(prop, `${indent} `));
|
|
78
|
-
for (const child of object.childs ?? [])
|
|
79
|
-
lines.push(describeObject(child, level + 1, `${indent} `));
|
|
80
|
-
return lines.join("\n");
|
|
81
|
-
};
|
|
82
|
-
/* describe the configured specification format as Markdown */
|
|
83
|
-
export const describeConfiguration = (config) => {
|
|
84
|
-
const sections = new Array();
|
|
85
|
-
for (const artifact of config) {
|
|
86
|
-
const lines = new Array();
|
|
87
|
-
lines.push(`### ${artifact.kind}: ${artifact.name ?? ""} (${artifact.kind}-${artifact.id ?? ""})`);
|
|
88
|
-
lines.push("");
|
|
89
|
-
if (artifact.file !== undefined)
|
|
90
|
-
lines.push(`- file: \`${artifact.file}\``);
|
|
91
|
-
lines.push(`- description: ${collapse(artifact.desc)}`);
|
|
92
|
-
for (const prop of artifact.props ?? [])
|
|
93
|
-
lines.push(describeProperty(prop, ""));
|
|
94
|
-
for (const child of artifact.childs ?? [])
|
|
95
|
-
lines.push(describeObject(child, 2, ""));
|
|
96
|
-
sections.push(lines.join("\n"));
|
|
8
|
+
/* provide the build-time bundled description of the generic
|
|
9
|
+
SpecBook models and formats */
|
|
10
|
+
const description = () => fs.readFileSync(new URL("specbook-format.md", import.meta.url), "utf8");
|
|
11
|
+
/* describe the generic SpecBook models and formats as Markdown,
|
|
12
|
+
optionally extended by the references to (or the embeddings of) the
|
|
13
|
+
artifacts of the particular project instantiation */
|
|
14
|
+
export const describeFormat = (options) => {
|
|
15
|
+
const sections = [description()];
|
|
16
|
+
if (options.config !== undefined || options.basedir !== undefined) {
|
|
17
|
+
sections.push("SpecBook Project Instantiation\n" +
|
|
18
|
+
"==============================\n");
|
|
19
|
+
if (options.config !== undefined && options.embed === true)
|
|
20
|
+
sections.push(textframe(`
|
|
21
|
+
The **SpecBook SCHEMA Model** of this particular project is the following
|
|
22
|
+
YAML schema configuration of the file \`${options.config}\`:
|
|
23
|
+
`) + "\n```yaml\n" +
|
|
24
|
+
fs.readFileSync(options.config, "utf8").replace(/\n*$/, "\n") +
|
|
25
|
+
"```\n");
|
|
26
|
+
else if (options.config !== undefined)
|
|
27
|
+
sections.push(textframe(`
|
|
28
|
+
The **SpecBook SCHEMA Model** of this particular project is the YAML
|
|
29
|
+
schema configuration in the file \`${options.config}\`.
|
|
30
|
+
`));
|
|
31
|
+
if (options.basedir !== undefined)
|
|
32
|
+
sections.push(textframe(`
|
|
33
|
+
The **SpecBook SPEC Model** of this particular project is the set of
|
|
34
|
+
specification Markdown files in the directory \`${options.basedir}\`.
|
|
35
|
+
`));
|
|
97
36
|
}
|
|
98
|
-
return
|
|
37
|
+
return sections.join("\n");
|
|
99
38
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
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 declare const formats: readonly ["json", "json5", "yaml", "toon", "html", "pdf", "md"];
|
|
4
4
|
export type ExportFormat = typeof formats[number];
|
|
5
5
|
export declare const parseOutputSpec: (spec: string) => {
|
|
6
6
|
format: ExportFormat;
|
|
7
7
|
output: string;
|
|
8
8
|
};
|
|
9
|
-
export declare const exportSpecification: (specification:
|
|
9
|
+
export declare const exportSpecification: (specification: Spec, format: ExportFormat, verbose: (msg: string) => void, config?: Schema) => Promise<Buffer>;
|
|
@@ -4,35 +4,39 @@
|
|
|
4
4
|
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
5
|
*/
|
|
6
6
|
import { minify } from "@swc/html";
|
|
7
|
-
import { documentTitle, documentLogo, documentCharset, documentThemeTone, subsetStylesheet, documentPaperSize, paperStylesheet } from "./specbook-export-common.js";
|
|
7
|
+
import { documentTitle, documentLogo, documentCharset, documentThemeTone, subsetStylesheet, documentPaperSize, paperStylesheet, charsetCodepoints } from "./specbook-export-common.js";
|
|
8
8
|
import { themeColors, themeStylesheet, themeMapping } from "./specbook-theme.js";
|
|
9
9
|
import { renderAst } from "./specbook-export-ast.js";
|
|
10
10
|
import { renderMarkdown } from "./specbook-export-md.js";
|
|
11
11
|
import { renderHtml, htmlOutline } from "./specbook-export-html.js";
|
|
12
12
|
import { htmlToPdf } from "./specbook-export-pdf.js";
|
|
13
|
+
import { literal } from "./specbook-verbose.js";
|
|
13
14
|
/* the supported export formats */
|
|
14
15
|
export const formats = ["json", "json5", "yaml", "toon", "html", "pdf", "md"];
|
|
15
|
-
/* the filename extensions mapping onto the export formats */
|
|
16
|
-
const extensions =
|
|
17
|
-
json
|
|
18
|
-
json5
|
|
19
|
-
yaml
|
|
20
|
-
toon
|
|
21
|
-
html
|
|
22
|
-
pdf
|
|
23
|
-
md
|
|
24
|
-
|
|
16
|
+
/* the format names and filename extensions mapping onto the export formats */
|
|
17
|
+
const extensions = new Map([
|
|
18
|
+
["json", "json"],
|
|
19
|
+
["json5", "json5"],
|
|
20
|
+
["yaml", "yaml"], ["yml", "yaml"],
|
|
21
|
+
["toon", "toon"],
|
|
22
|
+
["html", "html"], ["htm", "html"],
|
|
23
|
+
["pdf", "pdf"],
|
|
24
|
+
["md", "md"], ["markdown", "md"]
|
|
25
|
+
]);
|
|
25
26
|
/* parse an output specification "[<format>:]<filename>", inferring the
|
|
26
27
|
format from the filename extension when not explicitly given
|
|
27
28
|
(plain "-" for stdout defaults to JSON) */
|
|
28
29
|
export const parseOutputSpec = (spec) => {
|
|
29
30
|
const prefixed = spec.match(/^([a-zA-Z0-9]+):(.+)$/);
|
|
30
|
-
if (prefixed !== null
|
|
31
|
-
|
|
31
|
+
if (prefixed !== null) {
|
|
32
|
+
const explicit = extensions.get(prefixed[1].toLowerCase());
|
|
33
|
+
if (explicit !== undefined)
|
|
34
|
+
return { format: explicit, output: prefixed[2] };
|
|
35
|
+
}
|
|
32
36
|
if (spec === "-")
|
|
33
37
|
return { format: "json", output: spec };
|
|
34
38
|
const ext = spec.match(/\.([a-zA-Z0-9]+)$/);
|
|
35
|
-
const format = ext !== null ? extensions
|
|
39
|
+
const format = ext !== null ? extensions.get(ext[1].toLowerCase()) : undefined;
|
|
36
40
|
if (format !== undefined)
|
|
37
41
|
return { format, output: spec };
|
|
38
42
|
throw new Error(`unable to infer export format from output "${spec}" ` +
|
|
@@ -42,20 +46,20 @@ export const parseOutputSpec = (spec) => {
|
|
|
42
46
|
export const exportSpecification = async (specification, format, verbose, config) => {
|
|
43
47
|
if (!formats.includes(format))
|
|
44
48
|
throw new Error(`unknown export format "${format}"`);
|
|
45
|
-
verbose(`exporting specification as "${format}"`);
|
|
49
|
+
verbose(`exporting specification as "${literal(format)}"`);
|
|
46
50
|
if (format === "json" || format === "json5" || format === "yaml" || format === "toon")
|
|
47
51
|
return renderAst(specification, format, config);
|
|
48
52
|
else if (format === "md")
|
|
49
|
-
return Buffer.from(
|
|
53
|
+
return Buffer.from(renderMarkdown(specification, config), "utf8");
|
|
50
54
|
/* the HTML-based formats share the stylesheet, with the embedded
|
|
51
55
|
fonts subsetted to the CHARSET of the specification (if any) */
|
|
52
56
|
const charset = documentCharset(specification);
|
|
53
|
-
if (charset !== undefined)
|
|
54
|
-
verbose(`subsetting embedded fonts to charset "${charset}"`);
|
|
57
|
+
if (charset !== undefined && charsetCodepoints(charset) !== undefined)
|
|
58
|
+
verbose(`subsetting embedded fonts to charset "${literal(charset)}"`);
|
|
55
59
|
/* the theme tone (THEME-TONE) drives the layer-1 color spread
|
|
56
60
|
variables and the PDF decoration colors */
|
|
57
61
|
const tone = documentThemeTone(specification) ?? "#336699";
|
|
58
|
-
verbose(`generating theme color spreads (tone "${tone}")`);
|
|
62
|
+
verbose(`generating theme color spreads (tone "${literal(tone)}")`);
|
|
59
63
|
const colors = themeColors(tone);
|
|
60
64
|
/* the paper size (PAPER-SIZE) drives the PDF page setup and the
|
|
61
65
|
print-time height constraint of the diagrams */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Schema } from "./specbook-format-schema.js";
|
|
2
2
|
export interface InitOptions {
|
|
3
|
-
config:
|
|
4
|
-
basedir
|
|
3
|
+
config: Schema;
|
|
4
|
+
basedir: string;
|
|
5
5
|
verbose: (msg: string) => void;
|
|
6
6
|
}
|
|
7
7
|
export declare const initSpecification: (options: InitOptions) => string[];
|
package/dst/specbook-cmd-init.js
CHANGED
|
@@ -5,35 +5,58 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as path from "node:path";
|
|
8
|
-
import {
|
|
8
|
+
import { literal } from "./specbook-verbose.js";
|
|
9
|
+
/* the current time in the frontmatter timestamp format */
|
|
10
|
+
const timestamp = () => {
|
|
11
|
+
const d = new Date();
|
|
12
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
13
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
|
|
14
|
+
`${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
15
|
+
};
|
|
9
16
|
/* initialize the configured specification artifact files below the
|
|
10
|
-
base directory with their frontmatter and artifact
|
|
17
|
+
base directory with their frontmatter and artifact headings, where
|
|
18
|
+
all artifacts configured onto the same file reside in it side by
|
|
19
|
+
side, following each other on level 1 */
|
|
11
20
|
export const initSpecification = (options) => {
|
|
12
|
-
|
|
13
|
-
fs.mkdirSync(basedir, { recursive: true });
|
|
21
|
+
fs.mkdirSync(options.basedir, { recursive: true });
|
|
14
22
|
const now = timestamp();
|
|
15
23
|
const created = new Array();
|
|
24
|
+
/* group the configured artifacts by their file, preserving the
|
|
25
|
+
schema order both of the files and of the artifacts within */
|
|
26
|
+
const groups = new Map();
|
|
16
27
|
for (const artifact of options.config) {
|
|
17
28
|
if (artifact.file === undefined)
|
|
18
29
|
continue;
|
|
19
|
-
const
|
|
20
|
-
if (
|
|
21
|
-
|
|
30
|
+
const group = groups.get(artifact.file);
|
|
31
|
+
if (group === undefined)
|
|
32
|
+
groups.set(artifact.file, [artifact]);
|
|
33
|
+
else
|
|
34
|
+
group.push(artifact);
|
|
35
|
+
}
|
|
36
|
+
/* create each absent artifact file with its frontmatter and
|
|
37
|
+
the level 1 headings of its artifacts */
|
|
38
|
+
for (const [file, artifacts] of groups) {
|
|
39
|
+
const target = path.join(options.basedir, file);
|
|
40
|
+
if (fs.existsSync(target)) {
|
|
41
|
+
options.verbose(`skipping existing artifact file "${literal(file)}"`);
|
|
22
42
|
continue;
|
|
23
43
|
}
|
|
24
|
-
const
|
|
25
|
-
|
|
44
|
+
const headings = artifacts.map((artifact) => {
|
|
45
|
+
const name = artifact.name ?? "";
|
|
46
|
+
const paren = artifact.id !== undefined ? ` (${artifact.id})` : "";
|
|
47
|
+
return `# ${artifact.kind}: ${name}${paren}\n\n`;
|
|
48
|
+
}).join("");
|
|
26
49
|
const text = "---\n" +
|
|
27
50
|
`Created: ${now}\n` +
|
|
28
51
|
`Modified: ${now}\n` +
|
|
29
52
|
"---\n" +
|
|
30
53
|
"\n" +
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
fs.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
created.push(
|
|
54
|
+
headings;
|
|
55
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
56
|
+
fs.writeFileSync(target, text, "utf8");
|
|
57
|
+
options.verbose(`created artifact file "${literal(file)}" ` +
|
|
58
|
+
`with ${literal(artifacts.length)} artifact(s)`);
|
|
59
|
+
created.push(file);
|
|
37
60
|
}
|
|
38
61
|
return created;
|
|
39
62
|
};
|