@rse/specbook 0.9.2 → 0.9.3
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 +194 -13
- package/dst/specbook-api.d.ts +0 -1
- package/dst/specbook-api.js +1 -1
- package/dst/specbook-cli.js +1 -2
- package/dst/specbook-cmd-export.d.ts +1 -1
- package/dst/specbook-cmd-export.js +3 -3
- package/dst/specbook-diagram.js +14 -0
- package/dst/specbook-export-html.css +14 -0
- package/dst/specbook-export-html.d.ts +2 -2
- package/dst/specbook-export-html.js +188 -59
- package/dst/specbook-mcp.js +2 -4
- package/dst/specbook-struct-schema.d.ts +7 -0
- package/dst/specbook-struct-schema.js +7 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -33,18 +33,46 @@ service with tools `specbook_<cmd>()`.
|
|
|
33
33
|
Specification Format
|
|
34
34
|
--------------------
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
### Artifact Files
|
|
37
|
+
|
|
38
|
+
A specification consists of Markdown *artifact* files, which are
|
|
39
|
+
scanned recursively from a base directory. Every file has to start with
|
|
40
|
+
a frontmatter block carrying the `Created:` and `Modified:` timestamps
|
|
41
|
+
(format `yyyy-LL-dd HH:mm`):
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
---
|
|
45
|
+
Created: 2026-06-18 10:18
|
|
46
|
+
Modified: 2026-06-18 10:18
|
|
47
|
+
---
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Every level 1 heading opens a new *artifact*, so usually each file
|
|
51
|
+
carries exactly one. Both ATX headings (`# <text/>`, `## <text/>`, ...)
|
|
52
|
+
and Setext headings (`<text/>` underlined with `===` or `---`) are
|
|
53
|
+
supported.
|
|
54
|
+
|
|
55
|
+
### Object Model
|
|
56
|
+
|
|
57
|
+
An artifact carries a tree of *objects*. Every object has a mandatory
|
|
58
|
+
*kind* (e.g. `ENTITY`), a mandatory *name*, a unique anchor *id*
|
|
59
|
+
(explicitly given or derived from the name), an optional *primary*
|
|
60
|
+
marker, optional *properties* (key/value pairs), an optional
|
|
61
|
+
*description* statement with an optional rationale, and optional *child*
|
|
62
|
+
objects. Which kinds, properties, and nestings are allowed is defined by
|
|
63
|
+
the schema configuration (see below); object kinds and property keys are
|
|
64
|
+
case-sensitive and have to be written exactly as configured. Backquotes
|
|
65
|
+
in names and property values are preserved as code markup for rendering,
|
|
66
|
+
but are ignored when matching names, references, and constraints.
|
|
42
67
|
|
|
43
68
|
Three concrete syntaxes exist:
|
|
44
69
|
|
|
45
70
|
### Complex Format
|
|
46
71
|
|
|
47
|
-
Usually used on object hierarchy levels 1-3 (`#`, `##`, `###`).
|
|
72
|
+
Usually used on object hierarchy levels 1-3 (`#`, `##`, `###`). A
|
|
73
|
+
heading opens the object, an optional unordered list carries its
|
|
74
|
+
properties, and the remaining content up to the next heading is its
|
|
75
|
+
description:
|
|
48
76
|
|
|
49
77
|
```
|
|
50
78
|
# <kind/>: <name/> (<id/>)
|
|
@@ -55,16 +83,38 @@ Usually used on object hierarchy levels 1-3 (`#`, `##`, `###`).
|
|
|
55
83
|
<statement/>, BECAUSE <rationale/>.
|
|
56
84
|
```
|
|
57
85
|
|
|
86
|
+
The heading level directly reflects the nesting level: a `##` object
|
|
87
|
+
becomes a child of the preceding `#` object, a `###` object a child of
|
|
88
|
+
the preceding `##` object, etc. Skipping a level (a heading without a
|
|
89
|
+
parent object on the level above) is an error.
|
|
90
|
+
|
|
58
91
|
### Concise Format
|
|
59
92
|
|
|
60
|
-
Usually used on level 4 and deeper
|
|
61
|
-
|
|
93
|
+
Usually used on level 4 and deeper. A single unordered list item carries
|
|
94
|
+
the entire object as `;`-separated segments -- the `<kind/>: <name/>`
|
|
95
|
+
head, the `<key/>: <value/>` properties, and the trailing description --
|
|
96
|
+
and may wrap over multiple (indented) lines:
|
|
62
97
|
|
|
63
98
|
```
|
|
64
99
|
- <kind/>: <name/>; <key/>: <value/>; [...];
|
|
65
100
|
<statement/>, BECAUSE <rationale/>.
|
|
66
101
|
```
|
|
67
102
|
|
|
103
|
+
A list item is recognized as a Concise Format object (instead of a
|
|
104
|
+
property of the enclosing object) by carrying at least one `;` on its
|
|
105
|
+
first line. Every segment of the form `<key/>: <value/>` becomes a
|
|
106
|
+
property (unless it contains a ` BECAUSE `), all remaining segments are
|
|
107
|
+
joined into the description. Child objects nest as indented list items
|
|
108
|
+
below their parent item, where key/value items become properties and
|
|
109
|
+
concise items become child objects of that parent:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
- <kind/>: <name/>; <key/>: <value/>;
|
|
113
|
+
<statement/>, BECAUSE <rationale/>.
|
|
114
|
+
- <kind/>: <name/>; <key/>: <value/>;
|
|
115
|
+
<statement/>, BECAUSE <rationale/>.
|
|
116
|
+
```
|
|
117
|
+
|
|
68
118
|
### Grouped Format
|
|
69
119
|
|
|
70
120
|
A heading carrying just a *kind* and no `<kind/>: <name/>` pair opens a
|
|
@@ -81,13 +131,126 @@ with the (optionally backquoted) name directly:
|
|
|
81
131
|
|
|
82
132
|
The heading kind is singularized by stripping one trailing `S`, so both
|
|
83
133
|
`### STATES` and `### STATE` group objects of kind `STATE`. The grouped
|
|
84
|
-
objects become childs of the object the heading is nested under.
|
|
134
|
+
objects become childs of the object the heading is nested under. An
|
|
135
|
+
unquoted name must not contain a `:` (else the item would look like a
|
|
136
|
+
key/value pair) -- backquote it in this case.
|
|
85
137
|
|
|
86
138
|
When **SpecBook** itself generates specification Markdown, it emits
|
|
87
139
|
the Complex Format on levels 1-3 and the Concise Format from level 4
|
|
88
140
|
upwards. When **SpecBook** edits existing files, it mirrors the format
|
|
89
141
|
each existing object already uses.
|
|
90
142
|
|
|
143
|
+
### Names, Anchors, and Ids
|
|
144
|
+
|
|
145
|
+
In all formats, the `<name/>` may carry trailing decorations, in any
|
|
146
|
+
order:
|
|
147
|
+
|
|
148
|
+
- `{{<id/>}}`: the explicit Wiki-style anchor, setting the object id
|
|
149
|
+
(e.g. `## ENTITY: Attendee Browser {{attendee}}`).
|
|
150
|
+
|
|
151
|
+
- `(*)`: the *primary* marker, flagging the object as primary (e.g.
|
|
152
|
+
the primary attributes of an entity).
|
|
153
|
+
|
|
154
|
+
- `(<token/>)`: a parenthesized token with three possible roles: on
|
|
155
|
+
level 1 it becomes the artifact id (e.g. `# SPEC: Data Model (DM)`);
|
|
156
|
+
on deeper levels it is either accepted as the value of a still
|
|
157
|
+
missing configured property whose constraint it matches (e.g.
|
|
158
|
+
`### SCENARIO: Successful Token Login (Main)` satisfying an
|
|
159
|
+
`enum(Main,Alternative,Exceptional)` property), or else acts as the
|
|
160
|
+
implicit anchor id.
|
|
161
|
+
|
|
162
|
+
- `<a id="<id/>"></a>`: an HTML anchor at the very end of a heading,
|
|
163
|
+
as an alternative to `{{<id/>}}` (emitted by **SpecBook** itself
|
|
164
|
+
when exporting normalized Markdown).
|
|
165
|
+
|
|
166
|
+
When no explicit id is given, the id is derived by slugifying the name
|
|
167
|
+
(lowercased, with non-alphanumeric character runs dashed). An explicit
|
|
168
|
+
`{{<id/>}}` anchor always takes precedence over a `(<token/>)` id. When
|
|
169
|
+
the schema configures a fixed `id` for an object, it has to be
|
|
170
|
+
explicitly written in the input, via either `{{<id/>}}` or `(<id/>)`.
|
|
171
|
+
|
|
172
|
+
### Properties
|
|
173
|
+
|
|
174
|
+
In the Complex Format, each property is an unordered list item of the
|
|
175
|
+
form `<key/>: <value/>`, whose value may continue on the following
|
|
176
|
+
indented lines of the item (joined with spaces). In the Concise Format,
|
|
177
|
+
each property is a `<key/>: <value/>` segment. A property key may carry
|
|
178
|
+
a trailing parenthesized annotation (e.g. `WHEN (Context)`), which is
|
|
179
|
+
kept for rendering but ignored when matching the key against the schema.
|
|
180
|
+
|
|
181
|
+
A multi-token convenience exists for pattern-constrained properties:
|
|
182
|
+
when a property value fails its (regex or enum) constraint as a whole,
|
|
183
|
+
but consists of multiple whitespace-separated tokens of which one
|
|
184
|
+
matches the property's own constraint, that token is kept as the value
|
|
185
|
+
and the remaining tokens are distributed (case-insensitively) across the
|
|
186
|
+
other still unset pattern-constrained properties of the object. Tokens
|
|
187
|
+
assignable to no property are reported as errors.
|
|
188
|
+
|
|
189
|
+
### Descriptions and Rationales
|
|
190
|
+
|
|
191
|
+
The description of a Complex Format object is all block content below
|
|
192
|
+
its heading (up to the next heading) except the property list:
|
|
193
|
+
paragraphs, blockquotes, ordered lists (e.g. scenario steps), and
|
|
194
|
+
fenced code blocks. A fenced code block
|
|
195
|
+
of language `gradia` is skipped, as it is the derived diagram which
|
|
196
|
+
**SpecBook** itself emits into exported Markdown and which must not
|
|
197
|
+
become authored content on a re-parse. In the Concise Format, the
|
|
198
|
+
description is formed by the non-property segments of the item.
|
|
199
|
+
|
|
200
|
+
A description is split into its *statement* and *rationale* at the first
|
|
201
|
+
`, BECAUSE ` (or `, **BECAUSE** `) occurrence:
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
The event is created and configured but not visible to attendees,
|
|
205
|
+
BECAUSE an event needs a private setup phase.
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Wiki-Style References
|
|
209
|
+
|
|
210
|
+
A `[[<reference/>]]` anywhere in a name, property value, description, or
|
|
211
|
+
rationale is a Wiki-style reference to another object, which is checked
|
|
212
|
+
for unique resolvability and rendered as a hyperlink in the HTML/PDF
|
|
213
|
+
exports. A reference is a `.`-separated path of segments, where each
|
|
214
|
+
segment matches a single object as:
|
|
215
|
+
|
|
216
|
+
- `<id/>`: the object id or explicit anchor (e.g. `[[attendee]]`)
|
|
217
|
+
- `<name/>`: the object name (e.g. `[[Attendee Browser]]`)
|
|
218
|
+
- `<kind/>:<name-or-id/>`: kind-qualified (e.g. `[[ENTITY:Event]]`)
|
|
219
|
+
- `<kind/>:*` or `*`: wildcards (for match sets, see below)
|
|
220
|
+
|
|
221
|
+
Each part can be double-quoted to allow spaces, dots, and colons inside
|
|
222
|
+
it (e.g. `[["Vue.js"]]`, whose dot would otherwise be taken as a path
|
|
223
|
+
separator). A multi-segment path matches object chains connected by
|
|
224
|
+
direct parent-to-child steps, where the leading segments up to the root
|
|
225
|
+
may be omitted (e.g. `[[FV.relay]]` matches the object `relay` directly
|
|
226
|
+
below the artifact with id `FV`).
|
|
227
|
+
|
|
228
|
+
A reference has to resolve to exactly one object: a single-segment
|
|
229
|
+
reference tries the variants (1) id/anchor, (2) name, and (3)
|
|
230
|
+
`<kind/>:<name-or-id/>` in order, where the first variant yielding any
|
|
231
|
+
matches decides; several matches are an ambiguity and zero matches an
|
|
232
|
+
unresolvable reference, both reported by the linter. Wildcard references
|
|
233
|
+
resolve into match *sets* and are used in the schema configuration to
|
|
234
|
+
constrain reference-valued properties (e.g. `[[PERSONA:*]]` or
|
|
235
|
+
`[[DM.ENTITY:*]]`).
|
|
236
|
+
|
|
237
|
+
### Image Embeddings
|
|
238
|
+
|
|
239
|
+
A Markdown image `` inside a description or property
|
|
240
|
+
value embeds a local image file, resolved relative to the artifact file:
|
|
241
|
+
SVG files are inlined as-is, PNG/JPEG files are embedded as base64
|
|
242
|
+
`data:` URLs. URLs and other file types are left untouched.
|
|
243
|
+
|
|
244
|
+
### Normalization
|
|
245
|
+
|
|
246
|
+
The semantic phase validates the parsed objects against the schema
|
|
247
|
+
configuration (unknown kinds and properties, missing required properties
|
|
248
|
+
and child kinds, and violated value constraints are reported with
|
|
249
|
+
file/line-precise diagnostics) and then normalizes the specification:
|
|
250
|
+
artifacts, child objects, and properties are stably reordered along the
|
|
251
|
+
schema definition, so the exports always present them in their
|
|
252
|
+
configured canonical order.
|
|
253
|
+
|
|
91
254
|
Schema Configuration
|
|
92
255
|
--------------------
|
|
93
256
|
|
|
@@ -110,8 +273,11 @@ object itself plus all objects below it), `center` names the object a
|
|
|
110
273
|
`hub` is projected onto (default: `self`), `onlyConnected` drops the
|
|
111
274
|
edge-less nodes of a `graph`, `collapse` (default: `true`) omits a
|
|
112
275
|
degenerated diagram consisting of a single node only, `qualified` labels
|
|
113
|
-
every node with its object kind,
|
|
114
|
-
|
|
276
|
+
every node with its object kind, `properties` lists the property names
|
|
277
|
+
whose values are attached to the nodes as key/value annotations (with
|
|
278
|
+
every `[[xxx]]` reference stripped to its target object name), and
|
|
279
|
+
`config` passes arbitrary Gradia rendering options (e.g.
|
|
280
|
+
`grid-columns-max: 5`) through to the diagram.
|
|
115
281
|
|
|
116
282
|
The edges are derived from the `[[xxx]]` references of the node objects
|
|
117
283
|
-- from their property values only, or, with `links: all`, from their
|
|
@@ -133,6 +299,22 @@ demonstrate:
|
|
|
133
299
|
A `grid` is edge-less by definition and hence accepts neither `edges`
|
|
134
300
|
nor `hierarchy`.
|
|
135
301
|
|
|
302
|
+
An object kind can furthermore carry a `format` field, which controls
|
|
303
|
+
how the HTML/PDF export renders the child objects of every object of
|
|
304
|
+
that kind. Its `type` selects the rendering: `complex` (nested
|
|
305
|
+
sections), `concise` (compact per-kind tables), or the default `auto`,
|
|
306
|
+
which collapses only the deepest level into tables. Below a `concise`
|
|
307
|
+
object, unconfigured childs implicitly stay `concise` and render as
|
|
308
|
+
sub-tables inside the description cells, while an explicitly configured
|
|
309
|
+
`type` is always honored, even a `complex` rendering pressed into a
|
|
310
|
+
cell. `maxTableColumns` (default: `4`) bounds the columns of the compact
|
|
311
|
+
tables below the object (wider groups chunk their properties into
|
|
312
|
+
embedded per-object tables), and `withUnusedProps` (default: `false`)
|
|
313
|
+
unconditionally renders the defined but unused properties of that kind,
|
|
314
|
+
as empty property table lines or table columns. Objects collapsed into
|
|
315
|
+
tables carry no headings and hence leave the table of contents and the
|
|
316
|
+
PDF outline.
|
|
317
|
+
|
|
136
318
|
Usage
|
|
137
319
|
-----
|
|
138
320
|
|
|
@@ -142,7 +324,6 @@ Usage
|
|
|
142
324
|
$ specbook init [-v] [-c <yaml-file>] [-b <basedir>]
|
|
143
325
|
$ specbook lint [-v] [-c <yaml-file>] [-b <basedir>]
|
|
144
326
|
$ specbook export [-v] [-c <yaml-file>] [-b <basedir>] [-o [<format>:]<output-file>] [...]
|
|
145
|
-
[--max-table-columns <count>]
|
|
146
327
|
$ specbook describe [-v] [-c <yaml-file>] [-o <markdown-file>]
|
|
147
328
|
$ specbook import [-v] [-c <yaml-file>] [-b <basedir>] <input-files...>
|
|
148
329
|
$ specbook edit [-v] [-c <yaml-file>] [-b <basedir>] <query>
|
package/dst/specbook-api.d.ts
CHANGED
package/dst/specbook-api.js
CHANGED
|
@@ -55,7 +55,7 @@ export class SpecBook {
|
|
|
55
55
|
result.diagnostics.map(renderDiagnostic).join("\n"));
|
|
56
56
|
const buffers = [];
|
|
57
57
|
for (const format of options.formats ?? ["json"])
|
|
58
|
-
buffers.push(await exportSpecification(result.specification, format, this.verbose,
|
|
58
|
+
buffers.push(await exportSpecification(result.specification, format, this.verbose, result.config));
|
|
59
59
|
return buffers;
|
|
60
60
|
}
|
|
61
61
|
/* describe the configured specification format as Markdown */
|
package/dst/specbook-cli.js
CHANGED
|
@@ -95,7 +95,6 @@ common(program.command("export"))
|
|
|
95
95
|
.option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir", "."))
|
|
96
96
|
.option("-o, --output [<format>:]<output-file>", "output file (\"-\" for stdout, repeatable), with the format inferred " +
|
|
97
97
|
"from the filename extension unless explicitly prefixed", (value, previous) => previous.concat(value), [])
|
|
98
|
-
.option("--max-table-columns <count>", "maximum name/property/description columns for the compact table rendering", (value) => parseInt(value, 10), parseInt(envDefault("max-table-columns", "4") ?? "4", 10))
|
|
99
98
|
.action(async (opts) => {
|
|
100
99
|
const specbook = new SpecBook({ verbose: verboseOf(opts) });
|
|
101
100
|
const outputs = (opts.output.length > 0 ? opts.output : [envDefault("output") ?? "-"])
|
|
@@ -103,7 +102,7 @@ common(program.command("export"))
|
|
|
103
102
|
/* parse the input once and export each distinct format once */
|
|
104
103
|
const distinct = Array.from(new Set(outputs.map(({ format }) => format)));
|
|
105
104
|
const buffers = await specbook.export({ config: opts.config, basedir: opts.basedir,
|
|
106
|
-
formats: distinct
|
|
105
|
+
formats: distinct });
|
|
107
106
|
for (const { format, output } of outputs)
|
|
108
107
|
await writeOutput(output, buffers[distinct.indexOf(format)], verboseOf(opts));
|
|
109
108
|
});
|
|
@@ -6,4 +6,4 @@ export declare const parseOutputSpec: (spec: string) => {
|
|
|
6
6
|
format: ExportFormat;
|
|
7
7
|
output: string;
|
|
8
8
|
};
|
|
9
|
-
export declare const exportSpecification: (specification: Specification, format: ExportFormat, verbose: (msg: string) => void,
|
|
9
|
+
export declare const exportSpecification: (specification: Specification, format: ExportFormat, verbose: (msg: string) => void, config?: SchemaSpecification) => Promise<Buffer>;
|
|
@@ -38,7 +38,7 @@ export const parseOutputSpec = (spec) => {
|
|
|
38
38
|
"(use an explicit \"<format>:<filename>\" specification)");
|
|
39
39
|
};
|
|
40
40
|
/* export a specification into the requested format */
|
|
41
|
-
export const exportSpecification = async (specification, format, verbose,
|
|
41
|
+
export const exportSpecification = async (specification, format, verbose, config) => {
|
|
42
42
|
if (!formats.includes(format))
|
|
43
43
|
throw new Error(`unknown export format "${format}"`);
|
|
44
44
|
verbose(`exporting specification as "${format}"`);
|
|
@@ -62,7 +62,7 @@ export const exportSpecification = async (specification, format, verbose, maxTab
|
|
|
62
62
|
const css = themeStylesheet(colors) + await subsetStylesheet(charset) + paperStylesheet(paper);
|
|
63
63
|
if (format === "html") {
|
|
64
64
|
/* compress the rendered HTML (whitespace, comments, and inline CSS/JS) */
|
|
65
|
-
const html = await renderHtml(specification,
|
|
65
|
+
const html = await renderHtml(specification, config, undefined, css);
|
|
66
66
|
const minified = await minify(Buffer.from(html, "utf8"), {
|
|
67
67
|
collapseWhitespaces: "smart",
|
|
68
68
|
removeComments: true,
|
|
@@ -74,5 +74,5 @@ export const exportSpecification = async (specification, format, verbose, maxTab
|
|
|
74
74
|
else
|
|
75
75
|
/* the PDF export (like print in general) always uses the light
|
|
76
76
|
theme, so its decoration colors are the light mapping, too */
|
|
77
|
-
return htmlToPdf((tocPages) => renderHtml(specification,
|
|
77
|
+
return htmlToPdf((tocPages) => renderHtml(specification, config, tocPages, css), { ...documentTitle(specification), logo: documentLogo(specification) }, htmlOutline(specification, config), verbose, css, themeMapping(colors, "light"), paper);
|
|
78
78
|
};
|
package/dst/specbook-diagram.js
CHANGED
|
@@ -210,6 +210,20 @@ const deriveDiagram = (object, diagram, index, anchors, parents) => {
|
|
|
210
210
|
attrs.push(`type: ${atom(node.kind)}`);
|
|
211
211
|
if (type === "hub" ? node === center : node.primary === true)
|
|
212
212
|
attrs.push("primary: true");
|
|
213
|
+
/* attach the values of the configured properties as Gradia
|
|
214
|
+
key/value attributes (a node lacking a property is fine, as
|
|
215
|
+
the node set can mix objects of different kinds), with every
|
|
216
|
+
"[[...]]" reference stripped to its target object name */
|
|
217
|
+
for (const key of diagram.properties ?? []) {
|
|
218
|
+
const value = node.properties.find((property) => plainKey(property.key) === key)?.value;
|
|
219
|
+
if (value !== undefined) {
|
|
220
|
+
const text = value.replace(referenceRegex, (_, reference) => {
|
|
221
|
+
const target = resolveUnique(index, reference.trim()).target;
|
|
222
|
+
return target !== undefined ? target.name : reference.trim();
|
|
223
|
+
});
|
|
224
|
+
attrs.push(`${atom(key)}: ${atom(plainText(text))}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
213
227
|
lines.push(`${atom(anchor)}: ${atom(plainText(node.name))} [ ${attrs.join(", ")} ]`);
|
|
214
228
|
}
|
|
215
229
|
if (edges.length > 0) {
|
|
@@ -192,6 +192,20 @@ table.objects div.description { margin: 0; }
|
|
|
192
192
|
table.objects div.description > :first-child { margin-top: 0; }
|
|
193
193
|
table.objects div.description > :last-child { margin-bottom: 0; }
|
|
194
194
|
|
|
195
|
+
/* the embedded per-object chunk tables of the over-wide compact tables */
|
|
196
|
+
table.objects td.chunks { padding: 0; }
|
|
197
|
+
table.chunks { border-collapse: collapse; table-layout: fixed;
|
|
198
|
+
width: 100%; }
|
|
199
|
+
table.objects table.chunks tr td { background-color: transparent; }
|
|
200
|
+
table.objects table.chunks td:first-child { font-weight: normal; }
|
|
201
|
+
|
|
202
|
+
/* the nested renderings inside the description cells of the compact
|
|
203
|
+
tables ("concise" sub-tables and pressed-in "complex" objects),
|
|
204
|
+
reduced in size to fit into the cell */
|
|
205
|
+
table.objects td table.objects { font-size: 85%; margin: 0.5rem 0; }
|
|
206
|
+
table.objects td section { font-size: 85%; margin: 0.5rem 0; }
|
|
207
|
+
table.objects td section h6 { margin: 0.5rem 0 0.25rem 0; }
|
|
208
|
+
|
|
195
209
|
p.description,
|
|
196
210
|
div.description { margin: 1.0rem 0 0.5rem 0; }
|
|
197
211
|
div.description p { margin: 0.5rem 0; }
|
|
@@ -5,5 +5,5 @@ export type OutlineEntry = {
|
|
|
5
5
|
anchor: string;
|
|
6
6
|
childs: OutlineEntry[];
|
|
7
7
|
};
|
|
8
|
-
export declare const htmlOutline: (specification: Specification,
|
|
9
|
-
export declare const renderHtml: (specification: Specification,
|
|
8
|
+
export declare const htmlOutline: (specification: Specification, config?: SchemaSpecification) => OutlineEntry[];
|
|
9
|
+
export declare const renderHtml: (specification: Specification, config?: SchemaSpecification, tocPages?: Map<string, number>, css?: string) => Promise<string>;
|
|
@@ -144,6 +144,32 @@ const templates = {
|
|
|
144
144
|
{% endfor %}
|
|
145
145
|
</tbody>
|
|
146
146
|
</table>
|
|
147
|
+
`,
|
|
148
|
+
/* <TableChunked/> */
|
|
149
|
+
"TableChunked": textframe `
|
|
150
|
+
<table class="objects">
|
|
151
|
+
<thead>
|
|
152
|
+
<tr>
|
|
153
|
+
<th class="object-kind" style="width: {{ Table.width }}%">{{ Table.head }}</th>
|
|
154
|
+
<th class="description">Properties</th>
|
|
155
|
+
</tr>
|
|
156
|
+
</thead>
|
|
157
|
+
<tbody>
|
|
158
|
+
{% for row in Table.rows %}
|
|
159
|
+
<tr id="{{ row.id }}">
|
|
160
|
+
<td>{{ row.name }}{% if row.primary %} <span class="primary-marker">⌘</span>{% endif %}{% if row.paren %} <span class="anchor-paren">({{ row.paren }})</span>{% endif %} <a href="#{{ row.id }}"><span class="anchor-symbol">⚓︎</span></a></td>
|
|
161
|
+
<td class="chunks">
|
|
162
|
+
<table class="chunks">
|
|
163
|
+
{% for chunk in row.chunks %}
|
|
164
|
+
<tr>{% for cell in chunk %}<th{% if not cell.desc %} class="property-name"{% endif %}{% if cell.span > 1 %} colspan="{{ cell.span }}"{% endif %}>{{ cell.key }}</th>{% endfor %}</tr>
|
|
165
|
+
<tr>{% for cell in chunk %}<td{% if cell.span > 1 %} colspan="{{ cell.span }}"{% endif %}>{{ cell.value }}</td>{% endfor %}</tr>
|
|
166
|
+
{% endfor %}
|
|
167
|
+
</table>
|
|
168
|
+
</td>
|
|
169
|
+
</tr>
|
|
170
|
+
{% endfor %}
|
|
171
|
+
</tbody>
|
|
172
|
+
</table>
|
|
147
173
|
`
|
|
148
174
|
};
|
|
149
175
|
/* ==== Rendering ==== */
|
|
@@ -185,11 +211,12 @@ nunjucksAddons(env);
|
|
|
185
211
|
const safe = (html) => new nunjucks.runtime.SafeString(html);
|
|
186
212
|
const render = (name, context) => env.renderString(templates[name], context);
|
|
187
213
|
/* the active per-document reference expander, fully-qualified
|
|
188
|
-
anchor paths, enum/tags property value kinds,
|
|
189
|
-
diagram SVGs (all set during HTML rendering) */
|
|
214
|
+
anchor paths, enum/tags property value kinds, object schema nodes,
|
|
215
|
+
and pre-rendered diagram SVGs (all set during HTML rendering) */
|
|
190
216
|
let linker = null;
|
|
191
217
|
let anchors = null;
|
|
192
218
|
let members = null;
|
|
219
|
+
let schemas = null;
|
|
193
220
|
let diagrams = null;
|
|
194
221
|
/* determine the fully-qualified anchor path of an object */
|
|
195
222
|
const anchorOf = (object) => anchors?.get(object) ?? object.id;
|
|
@@ -252,6 +279,32 @@ const collectMembers = (schemas, result) => {
|
|
|
252
279
|
}
|
|
253
280
|
return result;
|
|
254
281
|
};
|
|
282
|
+
/* map the specification objects onto their schema configuration
|
|
283
|
+
nodes (the schema resolution mirrors the semantic validation) */
|
|
284
|
+
const collectSchemas = (specification, config) => {
|
|
285
|
+
const result = new Map();
|
|
286
|
+
const walk = (object, schema) => {
|
|
287
|
+
result.set(object, schema);
|
|
288
|
+
for (const child of object.childs) {
|
|
289
|
+
const childSchema = (schema.childs ?? []).find((c) => c.kind === child.kind);
|
|
290
|
+
if (childSchema !== undefined)
|
|
291
|
+
walk(child, childSchema);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
for (const artifact of specification.artifacts) {
|
|
295
|
+
for (const object of artifact.objects) {
|
|
296
|
+
const schema = config.find((s) => (s.kind === object.kind && s.id === object.id) || `${s.kind}-${s.id}` === object.id) ??
|
|
297
|
+
config.find((s) => (s.name ?? "").toUpperCase() ===
|
|
298
|
+
object.name.replace(/`/g, "").toUpperCase());
|
|
299
|
+
if (schema !== undefined)
|
|
300
|
+
walk(object, schema);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return result;
|
|
304
|
+
};
|
|
305
|
+
/* strip a trailing parenthesized annotation from a
|
|
306
|
+
property key (e.g. "WHEN (Context)") */
|
|
307
|
+
const plainKey = (key) => key.replace(/\s*\([^)]*\)\s*$/, "").trim();
|
|
255
308
|
/* render a property value, badging the individual members of an
|
|
256
309
|
"enum(...)" (a single member) or "tags(...)" (a member set) value */
|
|
257
310
|
const inlineValue = (kind, key, value) => {
|
|
@@ -264,26 +317,53 @@ const inlineValue = (kind, key, value) => {
|
|
|
264
317
|
/* expand the inline Markdown of the property values */
|
|
265
318
|
const inlineProperties = (kind, properties) => properties.map((property) => ({ key: property.key,
|
|
266
319
|
value: inlineValue(kind, property.key, property.value) }));
|
|
267
|
-
/*
|
|
320
|
+
/* resolve the format configuration of an object */
|
|
321
|
+
const formatOf = (object) => schemas?.get(object)?.format;
|
|
322
|
+
/* determine the maximum table columns configured on an object */
|
|
323
|
+
const maxColumnsOf = (object) => formatOf(object)?.maxTableColumns ?? 4;
|
|
324
|
+
/* determine the effective properties of an object, with
|
|
325
|
+
"withUnusedProps" injecting the defined but still unused schema
|
|
326
|
+
properties (in schema order) as empty key/value entries */
|
|
327
|
+
const effectiveProperties = (object) => {
|
|
328
|
+
const schema = schemas?.get(object);
|
|
329
|
+
if (schema?.format?.withUnusedProps !== true)
|
|
330
|
+
return object.properties;
|
|
331
|
+
const merged = (schema.props ?? []).flatMap((prop) => {
|
|
332
|
+
const present = object.properties.filter((property) => plainKey(property.key) === prop.name);
|
|
333
|
+
return present.length > 0 ? present : [{ key: prop.name, value: "" }];
|
|
334
|
+
});
|
|
335
|
+
return [...merged, ...object.properties.filter((property) => !merged.includes(property))];
|
|
336
|
+
};
|
|
337
|
+
/* determine the column shape of a potential compact table, with
|
|
338
|
+
"withUnusedProps" injecting the defined but still unused schema
|
|
339
|
+
properties (in schema order) as additional columns */
|
|
268
340
|
const tableShape = (childs) => {
|
|
269
|
-
|
|
341
|
+
let keys = new Array();
|
|
270
342
|
for (const child of childs)
|
|
271
343
|
for (const property of child.properties)
|
|
272
344
|
if (!keys.includes(property.key))
|
|
273
345
|
keys.push(property.key);
|
|
274
|
-
|
|
346
|
+
const schema = schemas?.get(childs[0]);
|
|
347
|
+
if (schema?.format?.withUnusedProps === true) {
|
|
348
|
+
const merged = (schema.props ?? []).flatMap((prop) => {
|
|
349
|
+
const present = keys.filter((key) => plainKey(key) === prop.name);
|
|
350
|
+
return present.length > 0 ? present : [prop.name];
|
|
351
|
+
});
|
|
352
|
+
keys = [...merged, ...keys.filter((key) => !merged.includes(key))];
|
|
353
|
+
}
|
|
354
|
+
return { keys, desc: childs.some((child) => child.description !== undefined || child.childs.length > 0) };
|
|
275
355
|
};
|
|
276
|
-
/*
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
356
|
+
/* decide whether the childs of an object collapse into the concise
|
|
357
|
+
(tabular) rendering: an explicit "format" type wins, "auto" collapses
|
|
358
|
+
the deepest level only, and inside an already concise rendering
|
|
359
|
+
context "auto" childs implicitly stay concise, too */
|
|
360
|
+
const conciseChilds = (object, schemaMap, concise) => {
|
|
361
|
+
if (object.childs.length === 0)
|
|
282
362
|
return false;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
return
|
|
286
|
-
|
|
363
|
+
const type = schemaMap?.get(object)?.format?.type ?? "auto";
|
|
364
|
+
if (type !== "auto")
|
|
365
|
+
return type === "concise";
|
|
366
|
+
return concise || object.childs.every((child) => child.childs.length === 0);
|
|
287
367
|
};
|
|
288
368
|
/* group the childs of an object by their kind, preserving order */
|
|
289
369
|
const groupChilds = (childs) => {
|
|
@@ -297,49 +377,96 @@ const groupChilds = (childs) => {
|
|
|
297
377
|
}
|
|
298
378
|
return [...groups.values()];
|
|
299
379
|
};
|
|
300
|
-
/* render
|
|
301
|
-
|
|
302
|
-
|
|
380
|
+
/* render the description cell of a table row: the description of the
|
|
381
|
+
object followed by its recursively rendered childs (implicitly or
|
|
382
|
+
explicitly concise childs as nested sub-tables, explicitly complex
|
|
383
|
+
ones as regular nested object renderings pressed into the cell) */
|
|
384
|
+
const renderCell = (child) => {
|
|
385
|
+
let html = child.description !== undefined ? renderDescription(child.description) : "";
|
|
386
|
+
if (child.childs.length > 0)
|
|
387
|
+
html += conciseChilds(child, schemas, true) ?
|
|
388
|
+
groupChilds(child.childs).map((group) => renderTable(group, maxColumnsOf(child))).join("") :
|
|
389
|
+
child.childs.map((sub) => renderObject(sub, 6, true)).join("");
|
|
390
|
+
return html;
|
|
391
|
+
};
|
|
392
|
+
/* render a single-kind group of childs into one compact table:
|
|
393
|
+
the name first, then the property columns, then the description;
|
|
394
|
+
a group wider than maxColumns instead chunks the property and
|
|
395
|
+
description cells of every object into an embedded per-object table */
|
|
396
|
+
const renderTable = (childs, maxColumns) => {
|
|
303
397
|
const { keys, desc } = tableShape(childs);
|
|
304
|
-
|
|
398
|
+
if (1 + keys.length + (desc ? 1 : 0) <= maxColumns)
|
|
399
|
+
return render("Table", { Table: {
|
|
400
|
+
head: childs[0].kind !== "" ? childs[0].kind : "Name",
|
|
401
|
+
keys,
|
|
402
|
+
desc,
|
|
403
|
+
/* under the fixed table layout the description column claims
|
|
404
|
+
twice the share of a regular column, compressing the others */
|
|
405
|
+
width: Math.round(200 / (keys.length + 3)),
|
|
406
|
+
rows: childs.map((child) => ({
|
|
407
|
+
id: anchorOf(child),
|
|
408
|
+
paren: child.paren,
|
|
409
|
+
primary: child.primary,
|
|
410
|
+
name: inline(child.name),
|
|
411
|
+
values: keys.map((key) => {
|
|
412
|
+
const value = child.properties.find((property) => property.key === key)?.value;
|
|
413
|
+
return value !== undefined ? inlineValue(child.kind, key, value) : "";
|
|
414
|
+
}),
|
|
415
|
+
description: safe(renderCell(child))
|
|
416
|
+
}))
|
|
417
|
+
} });
|
|
418
|
+
/* the embedded rows hold at most maxColumns - 1 cells (of the
|
|
419
|
+
group-wide union of property keys, plus the trailing description),
|
|
420
|
+
with the last cell spanning the leftover columns of the final row */
|
|
421
|
+
const size = Math.max(1, maxColumns - 1);
|
|
422
|
+
return render("TableChunked", { Table: {
|
|
305
423
|
head: childs[0].kind !== "" ? childs[0].kind : "Name",
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
twice the share of a regular column, compressing the others */
|
|
310
|
-
width: Math.round(200 / (keys.length + 3)),
|
|
311
|
-
rows: childs.map((child) => ({
|
|
312
|
-
id: anchorOf(child),
|
|
313
|
-
paren: child.paren,
|
|
314
|
-
primary: child.primary,
|
|
315
|
-
name: inline(child.name),
|
|
316
|
-
values: keys.map((key) => {
|
|
424
|
+
width: Math.round(100 / maxColumns),
|
|
425
|
+
rows: childs.map((child) => {
|
|
426
|
+
const cells = keys.map((key) => {
|
|
317
427
|
const value = child.properties.find((property) => property.key === key)?.value;
|
|
318
|
-
return
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
428
|
+
return { key, desc: false, span: 1,
|
|
429
|
+
value: value !== undefined ? inlineValue(child.kind, key, value) : safe("") };
|
|
430
|
+
});
|
|
431
|
+
if (desc)
|
|
432
|
+
cells.push({ key: "Description", desc: true, span: 1,
|
|
433
|
+
value: safe(renderCell(child)) });
|
|
434
|
+
const chunks = new Array();
|
|
435
|
+
for (let i = 0; i < cells.length; i += size)
|
|
436
|
+
chunks.push(cells.slice(i, i + size));
|
|
437
|
+
const last = chunks[chunks.length - 1];
|
|
438
|
+
last[last.length - 1].span = size - last.length + 1;
|
|
439
|
+
return {
|
|
440
|
+
id: anchorOf(child),
|
|
441
|
+
paren: child.paren,
|
|
442
|
+
primary: child.primary,
|
|
443
|
+
name: inline(child.name),
|
|
444
|
+
chunks
|
|
445
|
+
};
|
|
446
|
+
})
|
|
323
447
|
} });
|
|
324
448
|
};
|
|
325
449
|
/* recursively render an object into HTML */
|
|
326
|
-
const renderObject = (object, level,
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
450
|
+
const renderObject = (object, level, concise) => {
|
|
451
|
+
const properties = effectiveProperties(object);
|
|
452
|
+
return render("Object", { Object: {
|
|
453
|
+
level: Math.min(level, 6),
|
|
454
|
+
kind: object.kind,
|
|
455
|
+
id: anchorOf(object),
|
|
456
|
+
paren: object.paren,
|
|
457
|
+
primary: object.primary,
|
|
458
|
+
name: inline(object.name),
|
|
459
|
+
diagram: diagrams?.has(object) === true ?
|
|
460
|
+
safe(`<div class="diagram">${diagrams.get(object) ?? ""}</div>`) : "",
|
|
461
|
+
properties: properties.length > 0 ?
|
|
462
|
+
safe(render("Properties", { Properties: inlineProperties(object.kind, properties) })) : "",
|
|
463
|
+
description: object.description !== undefined ?
|
|
464
|
+
safe(renderDescription(object.description)) : "",
|
|
465
|
+
childs: conciseChilds(object, schemas, concise) ?
|
|
466
|
+
safe(groupChilds(object.childs).map((group) => renderTable(group, maxColumnsOf(object))).join("")) :
|
|
467
|
+
safe(object.childs.map((child) => renderObject(child, level + 1, concise)).join(""))
|
|
468
|
+
} });
|
|
469
|
+
};
|
|
343
470
|
/* render the title object into a title page */
|
|
344
471
|
const renderTitlePage = (object, created, modified) => {
|
|
345
472
|
const prop = (name) => object.properties.find((property) => property.key === name)?.value;
|
|
@@ -365,17 +492,18 @@ const renderTitlePage = (object, created, modified) => {
|
|
|
365
492
|
} });
|
|
366
493
|
};
|
|
367
494
|
/* render an artifact into HTML */
|
|
368
|
-
const renderArtifact = (artifact
|
|
369
|
-
objects: safe(artifact.objects.map((object) => renderObject(object, 1,
|
|
495
|
+
const renderArtifact = (artifact) => render("Artifact", { Artifact: {
|
|
496
|
+
objects: safe(artifact.objects.map((object) => renderObject(object, 1, false)).join(""))
|
|
370
497
|
} });
|
|
371
498
|
/* derive the hierarchy of the rendered object headings, skipping the
|
|
372
|
-
title page object and the
|
|
373
|
-
export const htmlOutline = (specification,
|
|
499
|
+
title page object and the childs collapsing into compact tables */
|
|
500
|
+
export const htmlOutline = (specification, config) => {
|
|
374
501
|
const paths = anchorPaths(buildLinkIndex(specification));
|
|
502
|
+
const schemaMap = config !== undefined ? collectSchemas(specification, config) : null;
|
|
375
503
|
const entry = (object) => ({
|
|
376
504
|
title: (object.kind !== "" ? `${object.kind}: ` : "") + object.name,
|
|
377
505
|
anchor: paths.get(object) ?? object.id,
|
|
378
|
-
childs:
|
|
506
|
+
childs: conciseChilds(object, schemaMap, false) ? [] : object.childs.map(entry)
|
|
379
507
|
});
|
|
380
508
|
return specification.artifacts
|
|
381
509
|
.filter((artifact) => !artifact.objects.some(isTitleObject))
|
|
@@ -386,7 +514,7 @@ export const htmlOutline = (specification, maxColumns) => {
|
|
|
386
514
|
with the build-time pre-assembled stylesheet embedded inline, the
|
|
387
515
|
artifact timestamps aggregated into min(Created)/max(Modified), and
|
|
388
516
|
optional per-anchor page numbers attached to the ToC entries */
|
|
389
|
-
export const renderHtml = async (specification,
|
|
517
|
+
export const renderHtml = async (specification, config, tocPages, css) => {
|
|
390
518
|
/* the document language selects the smart typography quote style */
|
|
391
519
|
const lang = documentLang(specification);
|
|
392
520
|
quotes = quoteStyles[lang?.toLowerCase().split(/[-_]/)[0] ?? "en"] ?? quoteStyles.en;
|
|
@@ -418,6 +546,7 @@ export const renderHtml = async (specification, maxColumns, config, tocPages, cs
|
|
|
418
546
|
const index = buildLinkIndex(specification);
|
|
419
547
|
anchors = anchorPaths(index);
|
|
420
548
|
members = config !== undefined ? collectMembers(config, new Map()) : null;
|
|
549
|
+
schemas = config !== undefined ? collectSchemas(specification, config) : null;
|
|
421
550
|
linker = (text) => expandReferences(text, (reference) => {
|
|
422
551
|
const target = resolveUnique(index, reference).target;
|
|
423
552
|
if (target === undefined)
|
|
@@ -447,6 +576,6 @@ export const renderHtml = async (specification, maxColumns, config, tocPages, cs
|
|
|
447
576
|
safe(renderTitlePage(title, created.toISOString().slice(0, 10), modified.toISOString().slice(0, 10))) : "",
|
|
448
577
|
search: title !== undefined ? safe(searchScript()) : "",
|
|
449
578
|
toc: entries.length > 0 ? safe(render("Toc", { Toc: { entries } })) : "",
|
|
450
|
-
artifacts: safe(artifacts.map((artifact) => renderArtifact(artifact
|
|
579
|
+
artifacts: safe(artifacts.map((artifact) => renderArtifact(artifact)).join(""))
|
|
451
580
|
} });
|
|
452
581
|
};
|
package/dst/specbook-mcp.js
CHANGED
|
@@ -70,14 +70,12 @@ export const serveMcp = async (verbose) => {
|
|
|
70
70
|
config: z.string().optional().describe("YAML schema configuration file"),
|
|
71
71
|
basedir: z.string().optional().describe("base directory of the specification Markdown files (default: \".\")"),
|
|
72
72
|
format: z.enum(formats).optional().describe("output format (default: json)"),
|
|
73
|
-
output: z.string().optional().describe("output file path")
|
|
74
|
-
maxTableColumns: z.number().optional().describe("maximum name/property/description " +
|
|
75
|
-
"columns for the compact table rendering (default: 4)")
|
|
73
|
+
output: z.string().optional().describe("output file path")
|
|
76
74
|
}
|
|
77
75
|
}, async (args) => {
|
|
78
76
|
try {
|
|
79
77
|
const [data] = await specbook.export({ config: args.config, basedir: args.basedir,
|
|
80
|
-
formats: [args.format ?? "json"]
|
|
78
|
+
formats: [args.format ?? "json"] });
|
|
81
79
|
if (args.output !== undefined) {
|
|
82
80
|
await fs.promises.writeFile(args.output, data);
|
|
83
81
|
return { content: [{ type: "text", text: `exported specification into "${args.output}" (${data.length} bytes)` }] };
|
|
@@ -9,6 +9,7 @@ export type SchemaObject = {
|
|
|
9
9
|
desc?: string;
|
|
10
10
|
optional?: boolean;
|
|
11
11
|
diagram?: SchemaDiagram;
|
|
12
|
+
format?: SchemaFormat;
|
|
12
13
|
props?: SchemaProperty[];
|
|
13
14
|
childs?: SchemaObject[];
|
|
14
15
|
};
|
|
@@ -24,8 +25,14 @@ export type SchemaDiagram = {
|
|
|
24
25
|
onlyConnected?: boolean;
|
|
25
26
|
collapse?: boolean;
|
|
26
27
|
qualified?: boolean;
|
|
28
|
+
properties?: string[];
|
|
27
29
|
config?: Partial<GradiaConfig>;
|
|
28
30
|
};
|
|
31
|
+
export type SchemaFormat = {
|
|
32
|
+
type?: "auto" | "complex" | "concise";
|
|
33
|
+
maxTableColumns?: number;
|
|
34
|
+
withUnusedProps?: boolean;
|
|
35
|
+
};
|
|
29
36
|
export type SchemaProperty = {
|
|
30
37
|
name: string;
|
|
31
38
|
desc?: string;
|
|
@@ -31,8 +31,14 @@ const SchemaDiagram = v.object({
|
|
|
31
31
|
onlyConnected: v.optional(v.boolean()),
|
|
32
32
|
collapse: v.optional(v.boolean()),
|
|
33
33
|
qualified: v.optional(v.boolean()),
|
|
34
|
+
properties: v.optional(v.array(v.string())),
|
|
34
35
|
config: v.optional(SchemaDiagramConfig)
|
|
35
36
|
});
|
|
37
|
+
const SchemaFormat = v.object({
|
|
38
|
+
type: v.optional(v.picklist(["auto", "complex", "concise"])),
|
|
39
|
+
maxTableColumns: v.optional(v.number()),
|
|
40
|
+
withUnusedProps: v.optional(v.boolean())
|
|
41
|
+
});
|
|
36
42
|
const SchemaObject = v.object({
|
|
37
43
|
kind: v.string(),
|
|
38
44
|
name: v.optional(v.string()),
|
|
@@ -41,6 +47,7 @@ const SchemaObject = v.object({
|
|
|
41
47
|
desc: v.optional(v.string()),
|
|
42
48
|
optional: v.optional(v.boolean()),
|
|
43
49
|
diagram: v.optional(SchemaDiagram),
|
|
50
|
+
format: v.optional(SchemaFormat),
|
|
44
51
|
props: v.optional(v.array(SchemaProperty)),
|
|
45
52
|
childs: v.optional(v.array(v.lazy(() => SchemaObject)))
|
|
46
53
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rse/specbook",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"description": "Markdown-based Specification Format (API, CLI and MCP service)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"json5": "2.2.3",
|
|
24
24
|
"pdf-lib": "1.17.1",
|
|
25
25
|
"subset-font": "2.5.0",
|
|
26
|
-
"@rse/mrcs": "0.9.
|
|
27
|
-
"@rse/gradia": "0.9.
|
|
26
|
+
"@rse/mrcs": "0.9.2",
|
|
27
|
+
"@rse/gradia": "0.9.8",
|
|
28
28
|
"@toon-format/toon": "4.1.1",
|
|
29
29
|
"valibot": "1.4.2",
|
|
30
30
|
"nunjucks": "3.2.4",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"@swc/html": "1.16.1",
|
|
34
34
|
"tokenizr": "2.2.1",
|
|
35
35
|
"playwright": "1.62.1",
|
|
36
|
-
"ai": "7.0.
|
|
36
|
+
"ai": "7.0.69",
|
|
37
37
|
"@ai-sdk/anthropic": "4.0.39",
|
|
38
38
|
"@ai-sdk/openai": "4.0.43",
|
|
39
39
|
"@openrouter/ai-sdk-provider": "3.0.0",
|