@rse/specbook 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +106 -19
  2. package/dst/specbook-api.d.ts +29 -9
  3. package/dst/specbook-api.js +108 -33
  4. package/dst/specbook-cli.js +84 -25
  5. package/dst/specbook-cmd-describe.d.ts +7 -1
  6. package/dst/specbook-cmd-describe.js +56 -11
  7. package/dst/specbook-cmd-export.d.ts +2 -1
  8. package/dst/specbook-cmd-export.js +46 -3
  9. package/dst/specbook-cmd-lint.d.ts +2 -1
  10. package/dst/specbook-cmd-lint.js +12 -8
  11. package/dst/specbook-cmd-preview.d.ts +12 -0
  12. package/dst/specbook-cmd-preview.js +57 -0
  13. package/dst/specbook-config.d.ts +1 -1
  14. package/dst/specbook-config.js +187 -37
  15. package/dst/specbook-diagnostic.d.ts +2 -0
  16. package/dst/specbook-diagnostic.js +4 -2
  17. package/dst/specbook-diagram.d.ts +1 -0
  18. package/dst/specbook-diagram.js +0 -0
  19. package/dst/specbook-export-ast.js +4 -2
  20. package/dst/specbook-export-common.js +5 -3
  21. package/dst/specbook-export-html-search.js +118 -30
  22. package/dst/specbook-export-html.css +1140 -233
  23. package/dst/specbook-export-html.d.ts +1 -1
  24. package/dst/specbook-export-html.js +764 -120
  25. package/dst/specbook-export-md.js +4 -2
  26. package/dst/specbook-format-schema.d.ts +33 -1
  27. package/dst/specbook-format-schema.js +34 -2
  28. package/dst/specbook-format.md +193 -25
  29. package/dst/specbook-format.yaml +2291 -299
  30. package/dst/specbook-link.d.ts +2 -1
  31. package/dst/specbook-link.js +43 -3
  32. package/dst/specbook-mcp.js +19 -10
  33. package/dst/specbook-parse-common.d.ts +4 -2
  34. package/dst/specbook-parse-common.js +6 -3
  35. package/dst/specbook-parse-semantic.js +290 -30
  36. package/dst/specbook-parse-syntax.js +3 -1
  37. package/dst/specbook-parse-value.d.ts +1 -0
  38. package/dst/specbook-parse-value.js +17 -3
  39. package/dst/specbook-parse.js +1 -1
  40. package/package.json +14 -6
package/README.md CHANGED
@@ -13,8 +13,8 @@ About
13
13
  format* which can be configured for the specifications of particular
14
14
  contexts through a YAML-based *schema configuration*. SpecBook allows
15
15
  specifications to be *initialized*, *linted*, *exported* (JSON, JSON5,
16
- YAML, TOON, HTML, PDF, and normalized Markdown), and *described* to
17
- LLMs.
16
+ YAML, TOON, HTML, PDF, and normalized Markdown), *previewed* (HTML, live
17
+ in the browser), and *described* to LLMs.
18
18
 
19
19
  **SpecBook** ships with an API class with methods `<xxx>()`, a
20
20
  CLI with commands `specbook <xxx>`, and an MCP service with tools
@@ -44,8 +44,9 @@ CLI with commands `specbook <xxx>`, and an MCP service with tools
44
44
  The YAML schema configuration strictly defines the allowed object
45
45
  kinds, hierarchies, and properties, whose values are constrained by an
46
46
  expression language (regex, enum, tags, list, and reference). Violations
47
- are reported as file- and line-precise diagnostics. A bundled standard
48
- schema configuration applies if no particular one is given.
47
+ are reported as file- and line-precise diagnostics, while the reference
48
+ coverage an object kind demands is reported as a warning only. A bundled
49
+ standard schema configuration applies if no particular one is given.
49
50
 
50
51
  - **Object Model Diagram Visualisation**:
51
52
  Object kinds can declare "graph", "hub", or "grid" diagrams in the
@@ -68,7 +69,8 @@ CLI with commands `specbook <xxx>`, and an MCP service with tools
68
69
 
69
70
  - **HTML Export for Developers**:
70
71
  The HTML export is a self-contained single document with a
71
- table-of-contents, full-text search, embedded images, and a light/dark
72
+ table-of-contents (also as a slide-in side panel for quick navigation),
73
+ full-text search, embedded images, and a light/dark
72
74
  theme toggle. It is intended for the day-to-day online reading during
73
75
  development. The HTML export can map all object kinds to nested
74
76
  sections or compact tables, or an automatic mixture which collapses
@@ -107,31 +109,55 @@ export class SpecBook {
107
109
  /* initialize the configured specification artifact files below the
108
110
  base directory and return the list of the generated files */
109
111
  init (options: {
110
- config?: string, /* YAML schema configuration (default: bundled standard one) */
112
+ config?: string[], /* YAML schema configuration files or glob patterns, merged
113
+ in order (default: bundled standard one, named "std") */
111
114
  basedir?: string /* base directory (default: ".") */
112
115
  }): Promise<string[]>
113
116
 
114
117
  /* parse and validate the specification Markdown files below the base directory */
115
118
  lint (options: {
116
- config?: string,
119
+ config?: string[],
117
120
  basedir?: string
118
121
  }): Promise<LintResult>
119
122
 
120
123
  /* export the specification, parsing the input just once and
121
- returning one buffer per requested format (strict: any diagnostic
122
- rejects the export with an "Error") */
124
+ returning one buffer per requested format (strict: any error
125
+ diagnostic rejects the export with an "Error", while the warnings
126
+ are surfaced as "notice" verbose messages) */
123
127
  export (options: {
124
- config?: string,
128
+ config?: string[],
125
129
  basedir?: string,
126
- formats?: ExportFormat[] /* requested output formats (default: [ "json" ]) */
130
+ formats?: ExportFormat[], /* requested output formats (default: [ "json" ]) */
131
+ realtime?: boolean /* inject the live preview script into the HTML */
127
132
  }): Promise<Buffer[]>
128
133
 
134
+ /* export the specification like "export" and then keep it in sync by
135
+ re-exporting it on every change of a specification file (a failed
136
+ re-export is reported and leaves the observe loop intact) */
137
+ watch (options: {
138
+ config?: string[],
139
+ basedir?: string,
140
+ formats?: ExportFormat[],
141
+ realtime?: boolean,
142
+ onExport: (buffers: Buffer[]) => void | Promise<void>
143
+ }): Promise<void>
144
+
145
+ /* serve the HTML export as a live preview on "http://<addr>:<port>/",
146
+ kept in sync like "watch" and updated in the browser on every change */
147
+ preview (options: {
148
+ config?: string[],
149
+ basedir?: string,
150
+ addr?: string, /* listening IP address (default: "127.0.0.1") */
151
+ port?: number /* listening TCP port (default: 12345) */
152
+ }): Promise<void>
153
+
129
154
  /* describe the SpecBook models and formats, optionally pointing to
130
155
  the artifacts of the particular project */
131
156
  describe (options: {
132
- config?: string,
157
+ config?: string[],
133
158
  basedir?: string,
134
159
  embed?: boolean, /* embed the schema configuration instead of referencing it */
160
+ compress?: CompressLevel, /* compression level of the schema configuration (default: 1) */
135
161
  format?: DescribeFormat, /* output format (default: "md") */
136
162
  part?: DescribePart /* document part (default: "all") */
137
163
  }): Promise<string>
@@ -149,21 +175,25 @@ export interface LintResult {
149
175
  specification: Spec
150
176
  diagnostics: Diagnostic[]
151
177
  config?: Schema
178
+ files: string[] /* the artifact files plus their embedded assets */
152
179
  }
153
180
  export interface Diagnostic {
154
181
  file: string
155
182
  line: number
156
183
  column: number
184
+ severity: "error" | "warning"
157
185
  message: string
158
186
  }
159
187
 
160
- /* the supported export formats and describe formats/parts */
188
+ /* the supported export formats and describe formats/parts/compression levels */
161
189
  export const formats: readonly [ "json", "json5", "yaml", "toon", "html", "pdf", "md" ]
162
190
  export const describeFormats: readonly [ "md", "raw" ]
163
191
  export const describeParts: readonly [ "all", "meta", "schema", "spec" ]
192
+ export const compressLevels: readonly [ 0, 1, 2, 3 ]
164
193
  export type ExportFormat = typeof formats[number]
165
194
  export type DescribeFormat = typeof describeFormats[number]
166
195
  export type DescribePart = typeof describeParts[number]
196
+ export type CompressLevel = typeof compressLevels[number]
167
197
 
168
198
  /* the Abstract Syntax Tree (AST) of a parsed specification */
169
199
  export type Spec = {
@@ -251,13 +281,23 @@ $ specbook export \
251
281
  [-v|--verbose] \
252
282
  [-c|--config <schema-yaml-file>] \
253
283
  [-b|--basedir <spec-md-file-basedir>] \
254
- [-o|--output [<format>:]<output-file>] [...]
284
+ [-o|--output [<format>:]<output-file>] \
285
+ [-w|--watch] \
286
+ [...]
287
+
288
+ $ specbook preview \
289
+ [-v|--verbose] \
290
+ [-c|--config <schema-yaml-file>] \
291
+ [-b|--basedir <spec-md-file-basedir>] \
292
+ [-a|--addr <ip-addr>] \
293
+ [-p|--port <tcp-port>]
255
294
 
256
295
  $ specbook describe \
257
296
  [-v|--verbose] \
258
297
  [-c|--config <schema-yaml-file>] \
259
298
  [-b|--basedir <spec-md-file-basedir>] \
260
299
  [-e|--embed] \
300
+ [-z|--compress [<level>]] \
261
301
  [-f|--format <format>] \
262
302
  [-p|--part <part>] \
263
303
  [-o|--output <output-file>]
@@ -279,8 +319,22 @@ Options:
279
319
  A referenced file which is absent is reported, unless all of its
280
320
  artifacts are `optional`, and so is an artifact absent from its present
281
321
  file, unless it is `optional` itself. Both `lint` and `export` report
282
- all diagnostics and fail on any of them, so a partial or invalid
283
- specification is never exported.
322
+ all diagnostics and fail on any error among them (a warning, like a
323
+ lapse of the reference coverage a `referenced` object kind demands,
324
+ is reported only), so a partial or invalid specification is never
325
+ exported.
326
+ The option accepts glob patterns and can be given multiple times: the
327
+ matching files (in the order of the patterns and alphabetically within
328
+ a pattern, where the literal `std` names the bundled standard schema
329
+ configuration, and where a pattern matching no file is an error) are
330
+ merged in order into one effective schema configuration, the later
331
+ files into the earlier ones. The objects merge deeply, while the
332
+ elements of the lists are matched by identity (artifacts by `kind`
333
+ plus `id`/`name`, nested objects by `kind`, properties by `name`, and
334
+ scalar entries by value): a matching element is merged into its
335
+ counterpart, an unmatched one is appended. Every file has to be valid
336
+ YAML on its own, while the merged result alone is validated against
337
+ the schema of the configuration.
284
338
 
285
339
  - `-b|--basedir <spec-md-file-basedir>`:
286
340
  The base directory (default: `.`) is the directory the referenced
@@ -295,18 +349,48 @@ Options:
295
349
  The `md` export format normalizes the entire corpus into a *single*
296
350
  Markdown document.
297
351
 
352
+ - `-w|--watch` (`export` only):
353
+ Keep the outputs in sync with their sources: after the regular export,
354
+ the referenced artifact files and all assets they embed are observed,
355
+ and every change re-exports the specification once the sources stayed
356
+ silent for one second. A failed re-export is reported and leaves the
357
+ observe loop intact, so a transiently invalid specification does not
358
+ end the watch. The outputs have to be regular files, as `-` (stdout)
359
+ cannot receive a repeated export.
360
+
361
+ - `-a|--addr <ip-addr>`, `-p|--port <tcp-port>` (`preview` only):
362
+ The IP address (default: `127.0.0.1`) and TCP port (default: `12345`)
363
+ the live preview listens on. The HTML export is served on
364
+ `http://<ip-addr>:<tcp-port>/`, kept in sync with its sources exactly
365
+ like `export --watch`, and updated in the browser after every change
366
+ through a WebSocket connection the served page keeps open, where the
367
+ document is replaced in place, so the scroll position survives (a
368
+ failed re-export keeps the previous HTML in place). A status icon
369
+ left of the theme switcher shows the connection state: grey while
370
+ connected, in the search highlight color while disconnected, and
371
+ blinking for 2s after every update.
372
+
298
373
  - `-o|--output <output-file>` (`describe` only):
299
374
  The output file (default: `-` for stdout) receives the described
300
375
  Markdown document.
301
376
 
302
377
  - `-e|--embed` (`describe` only):
303
- Embed the given YAML schema configuration verbatim instead of just
378
+ Embed the given YAML schema configuration itself instead of just
304
379
  referencing it, so the resulting document describes the specification
305
380
  format entirely on its own.
306
381
 
382
+ - `-z|--compress [<level>]` (`describe` only):
383
+ The compression level (default and bare flag: `1`) of the YAML
384
+ schema configuration (embedded into the Markdown or emitted as the
385
+ raw file content), so the configuration costs fewer tokens: `0`
386
+ emits it verbatim, `1` re-emits it with 2-space indentation, unwrapped
387
+ lines, and without comments, `2` additionally leaves out its `refs` fields, and
388
+ `3` additionally leaves out its `desc` fields of objects and properties.
389
+
307
390
  - `-f|--format <format>` (`describe` only):
308
391
  The output format (default: `md`) switches from the rendered Markdown
309
- onto the raw original file content with `raw`, which is available for
392
+ onto the raw original file content (the `schema` one compressed by the
393
+ `-z|--compress` level) with `raw`, which is available for
310
394
  the file-backed parts `meta` and `schema` only.
311
395
 
312
396
  - `-p|--part <part>` (`describe` only):
@@ -319,7 +403,10 @@ Options:
319
403
  The default value of every CLI option `--xxx` can be overridden
320
404
  by a corresponding `SPECBOOK_XXX` environment variable (e.g.
321
405
  `SPECBOOK_BASEDIR`, `SPECBOOK_CONFIG`, `SPECBOOK_OUTPUT`,
322
- `SPECBOOK_VERBOSE`), while an explicitly supplied option always wins.
406
+ `SPECBOOK_VERBOSE`, `SPECBOOK_ADDR`, `SPECBOOK_PORT`), while an
407
+ explicitly supplied option always wins. As `-c|--config` is repeatable,
408
+ `SPECBOOK_CONFIG` carries a list of patterns separated by the path
409
+ delimiter of the platform (`:` on Unix, `;` on Windows).
323
410
 
324
411
  Example:
325
412
 
@@ -1,13 +1,16 @@
1
- import { renderDiagnostic, renderDiagnosticVerbose, type Diagnostic } from "./specbook-diagnostic.js";
1
+ import { renderDiagnostic, renderDiagnosticVerbose, type Diagnostic, type DiagnosticSeverity } from "./specbook-diagnostic.js";
2
2
  import { type LintResult } from "./specbook-cmd-lint.js";
3
3
  import { parseOutputSpec, formats, type ExportFormat } from "./specbook-cmd-export.js";
4
- import { describeFormats, describeParts, parseDescribeFormat, parseDescribePart, type DescribeFormat, type DescribePart } from "./specbook-cmd-describe.js";
4
+ import { previewAddr, previewPort } from "./specbook-cmd-preview.js";
5
+ import { describeFormats, describeParts, parseDescribeFormat, parseDescribePart, compressLevels, parseCompressLevel, type DescribeFormat, type DescribePart, type CompressLevel } from "./specbook-cmd-describe.js";
5
6
  import { literal, renderVerbose, type Verbose, type VerboseLevel } from "./specbook-verbose.js";
6
7
  export { literal, renderVerbose, type Verbose, type VerboseLevel };
7
8
  export { formats, parseOutputSpec, type ExportFormat };
9
+ export { previewAddr, previewPort };
8
10
  export { describeFormats, describeParts, parseDescribeFormat, parseDescribePart };
9
- export { type DescribeFormat, type DescribePart };
10
- export { renderDiagnostic, renderDiagnosticVerbose, type Diagnostic };
11
+ export { compressLevels, parseCompressLevel };
12
+ export { type DescribeFormat, type DescribePart, type CompressLevel };
13
+ export { renderDiagnostic, renderDiagnosticVerbose, type Diagnostic, type DiagnosticSeverity };
11
14
  export { type LintResult };
12
15
  export type { Spec, SpecArtifact, SpecObject, SpecDescription, SpecProperty } from "./specbook-format-spec.js";
13
16
  export type { Schema, SchemaObject, SchemaProperty } from "./specbook-format-schema.js";
@@ -21,25 +24,42 @@ export declare class SpecBook {
21
24
  private verbose;
22
25
  constructor(options?: SpecBookOptions);
23
26
  private verboseOf;
24
- private configFile;
27
+ private configFiles;
25
28
  private requireConfig;
26
29
  init(options: {
27
- config?: string;
30
+ config?: string[];
28
31
  basedir?: string;
29
32
  }): Promise<string[]>;
30
33
  lint(options: {
31
- config?: string;
34
+ config?: string[];
32
35
  basedir?: string;
33
36
  }): Promise<LintResult>;
37
+ private renderFormats;
34
38
  export(options: {
35
- config?: string;
39
+ config?: string[];
36
40
  basedir?: string;
37
41
  formats?: ExportFormat[];
42
+ realtime?: boolean;
38
43
  }): Promise<Buffer[]>;
44
+ private observe;
45
+ watch(options: {
46
+ config?: string[];
47
+ basedir?: string;
48
+ formats?: ExportFormat[];
49
+ realtime?: boolean;
50
+ onExport: (buffers: Buffer[]) => void | Promise<void>;
51
+ }): Promise<void>;
52
+ preview(options: {
53
+ config?: string[];
54
+ basedir?: string;
55
+ addr?: string;
56
+ port?: number;
57
+ }): Promise<void>;
39
58
  describe(options: {
40
- config?: string;
59
+ config?: string[];
41
60
  basedir?: string;
42
61
  embed?: boolean;
62
+ compress?: CompressLevel;
43
63
  format?: DescribeFormat;
44
64
  part?: DescribePart;
45
65
  }): Promise<string>;
@@ -5,18 +5,22 @@
5
5
  */
6
6
  import * as fs from "node:fs";
7
7
  import { fileURLToPath } from "node:url";
8
+ import { glob } from "glob";
8
9
  import { loadConfig } from "./specbook-config.js";
9
10
  import { renderDiagnostic, renderDiagnosticVerbose } from "./specbook-diagnostic.js";
10
11
  import { initSpecification } from "./specbook-cmd-init.js";
11
12
  import { lint } from "./specbook-cmd-lint.js";
12
- import { exportSpecification, parseOutputSpec, formats } from "./specbook-cmd-export.js";
13
- import { describeFormat, describeFormats, describeParts, parseDescribeFormat, parseDescribePart } from "./specbook-cmd-describe.js";
13
+ import { exportSpecification, watchSpecification, parseOutputSpec, formats } from "./specbook-cmd-export.js";
14
+ import { servePreview, previewAddr, previewPort } from "./specbook-cmd-preview.js";
15
+ import { describeFormat, describeFormats, describeParts, parseDescribeFormat, parseDescribePart, compressLevels, parseCompressLevel } from "./specbook-cmd-describe.js";
14
16
  import { requireBrowser } from "./specbook-export-pdf.js";
15
17
  import { literal, renderVerbose } from "./specbook-verbose.js";
16
18
  /* re-export the central types for API consumers */
17
19
  export { literal, renderVerbose };
18
20
  export { formats, parseOutputSpec };
21
+ export { previewAddr, previewPort };
19
22
  export { describeFormats, describeParts, parseDescribeFormat, parseDescribePart };
23
+ export { compressLevels, parseCompressLevel };
20
24
  export { renderDiagnostic, renderDiagnosticVerbose };
21
25
  /* our own version, taken from the package manifest, which resides one
22
26
  level above both the source and the compiled module directory */
@@ -35,16 +39,32 @@ export class SpecBook {
35
39
  verboseOf(cmd) {
36
40
  return (msg, level = "debug") => this.verbose(cmd, msg, level);
37
41
  }
38
- /* determine the file of the YAML schema configuration, falling
39
- back onto the bundled standard schema configuration */
40
- configFile(file) {
41
- return file ?? standardConfig;
42
+ /* determine the files of the YAML schema configuration out of the
43
+ glob patterns, in the order of the patterns and alphabetically
44
+ within a pattern, where the literal "std" names the bundled
45
+ standard schema configuration, which is also the fallback if no
46
+ pattern is given at all */
47
+ async configFiles(patterns) {
48
+ if (patterns === undefined || patterns.length === 0)
49
+ return [standardConfig];
50
+ const files = new Array();
51
+ for (const pattern of patterns) {
52
+ if (pattern === "std") {
53
+ files.push(standardConfig);
54
+ continue;
55
+ }
56
+ const matches = await glob(pattern, { nodir: true });
57
+ if (matches.length === 0)
58
+ throw new Error(`no configuration file matches "${pattern}"`);
59
+ files.push(...matches.sort());
60
+ }
61
+ return files;
42
62
  }
43
- /* load a YAML schema configuration, failing on any problem */
44
- requireConfig(file, verbose) {
45
- file = this.configFile(file);
46
- verbose(`loading schema configuration "${literal(file)}"`);
47
- const { config, diagnostics } = loadConfig(file);
63
+ /* load the YAML schema configuration out of its files, failing on any problem */
64
+ requireConfig(files, verbose) {
65
+ verbose("loading schema configuration " +
66
+ files.map((file) => `"${literal(file)}"`).join(", "));
67
+ const { config, diagnostics } = loadConfig(files);
48
68
  if (config === undefined)
49
69
  throw new Error("invalid configuration:\n" +
50
70
  diagnostics.map(renderDiagnostic).join("\n"));
@@ -54,19 +74,36 @@ export class SpecBook {
54
74
  below the base directory */
55
75
  async init(options) {
56
76
  const verbose = this.verboseOf("init");
57
- return initSpecification({ config: this.requireConfig(options.config, verbose),
77
+ return initSpecification({ config: this.requireConfig(await this.configFiles(options.config), verbose),
58
78
  basedir: options.basedir ?? ".", verbose });
59
79
  }
60
80
  /* lint the specification Markdown files below the base directory */
61
81
  async lint(options) {
62
- return lint({ config: this.configFile(options.config),
82
+ return lint({ config: await this.configFiles(options.config),
63
83
  basedir: options.basedir ?? ".", verbose: this.verboseOf("lint") });
64
84
  }
85
+ /* render an already parsed specification into the requested formats
86
+ (strict: any error diagnostic prevents the export, as a partial or
87
+ invalid specification must never be emitted, while the warnings
88
+ are just surfaced as notices), where "realtime" injects the
89
+ client-side script of the live preview into the HTML */
90
+ async renderFormats(result, formats, verbose, realtime) {
91
+ if (result.diagnostics.some((diagnostic) => diagnostic.severity === "error"))
92
+ throw new Error("invalid specification:\n" +
93
+ result.diagnostics.map(renderDiagnostic).join("\n"));
94
+ for (const diagnostic of result.diagnostics)
95
+ verbose(renderDiagnostic(diagnostic), "notice");
96
+ if (result.specification.artifacts.length === 0)
97
+ throw new Error("unexportable specification: no artifacts found");
98
+ const buffers = new Array();
99
+ for (const format of formats)
100
+ buffers.push(await exportSpecification(result.specification, format, verbose, result.config, realtime));
101
+ return buffers;
102
+ }
65
103
  /* export the specification Markdown files below the base directory
66
104
  as JSON, JSON5, YAML, TOON, HTML, PDF, or normalized Markdown,
67
105
  parsing the input just once and returning one buffer per
68
- requested format (strict: any diagnostic prevents the export,
69
- as a partial or invalid specification must never be emitted) */
106
+ requested format */
70
107
  async export(options) {
71
108
  const verbose = this.verboseOf("export");
72
109
  const requested = options.formats ?? ["json"];
@@ -74,23 +111,62 @@ export class SpecBook {
74
111
  export fail before the specification is even parsed */
75
112
  if (requested.includes("pdf"))
76
113
  await requireBrowser(verbose);
77
- const result = lint({ config: this.configFile(options.config),
78
- basedir: options.basedir ?? ".", verbose });
79
- if (result.diagnostics.length > 0)
80
- throw new Error("invalid specification:\n" +
81
- result.diagnostics.map(renderDiagnostic).join("\n"));
82
- if (result.specification.artifacts.length === 0)
83
- throw new Error("unexportable specification: no artifacts found");
84
- const buffers = new Array();
85
- for (const format of requested)
86
- buffers.push(await exportSpecification(result.specification, format, verbose, result.config));
87
- return buffers;
114
+ return this.renderFormats(lint({ config: await this.configFiles(options.config),
115
+ basedir: options.basedir ?? ".", verbose }), requested, verbose, options.realtime === true);
116
+ }
117
+ /* keep an export in sync with its sources (the shared core of
118
+ "watch" and "preview"): every change of an artifact file or of one
119
+ of its embedded assets re-exports the specification and hands the
120
+ buffers to "onExport" once the sources fell silent again. A failed
121
+ re-export is reported and leaves the observe loop intact, so a
122
+ transiently invalid specification does not end the watch. The
123
+ returned promise settles once the initial export is done, while
124
+ the active watcher keeps the process alive afterwards */
125
+ async observe(options, verbose) {
126
+ const config = await this.configFiles(options.config);
127
+ return watchSpecification(async () => {
128
+ /* the lint result carries the files to observe even for an
129
+ invalid specification, so a failing export still keeps the
130
+ observe loop fed with an up-to-date file set */
131
+ const result = lint({ config, basedir: options.basedir ?? ".", verbose });
132
+ try {
133
+ await options.onExport(await this.renderFormats(result, options.formats, verbose, options.realtime));
134
+ }
135
+ catch (err) {
136
+ verbose("export failed: " +
137
+ (err instanceof Error ? err.message : String(err)), "notice");
138
+ }
139
+ return result.files;
140
+ }, verbose);
141
+ }
142
+ /* export the specification like "export" and then keep the export
143
+ in sync with its sources (see "observe") */
144
+ async watch(options) {
145
+ const verbose = this.verboseOf("export");
146
+ const requested = options.formats ?? ["json"];
147
+ if (requested.includes("pdf"))
148
+ await requireBrowser(verbose);
149
+ return this.observe({ ...options, formats: requested,
150
+ realtime: options.realtime === true }, verbose);
151
+ }
152
+ /* serve the HTML export of the specification as a live preview on
153
+ "http://<addr>:<port>/": the export is kept in sync with its
154
+ sources (see "observe") and every fresh export is pushed to the
155
+ connected browsers as an in-place document update, through the
156
+ client-side script the "realtime" export injects into the HTML */
157
+ async preview(options) {
158
+ const verbose = this.verboseOf("preview");
159
+ const server = await servePreview({ addr: options.addr ?? previewAddr,
160
+ port: options.port ?? previewPort, verbose });
161
+ return this.observe({ config: options.config, basedir: options.basedir,
162
+ formats: ["html"], realtime: true,
163
+ onExport: ([html]) => server.update(html) }, verbose);
88
164
  }
89
165
  /* describe the generic SpecBook models and formats as Markdown (or
90
166
  as the raw original file content), either entirely or reduced to
91
167
  a single part, optionally pointing to the artifacts of the
92
168
  particular project, whose YAML schema configuration is validated
93
- before use */
169
+ before use and optionally emitted compressed by level */
94
170
  async describe(options) {
95
171
  const verbose = this.verboseOf("describe");
96
172
  const format = options.format ?? "md";
@@ -98,15 +174,14 @@ export class SpecBook {
98
174
  /* the schema-bearing parts require a schema configuration, so
99
175
  they fall back onto the bundled standard one, which is always
100
176
  embedded, as its bundled file is no meaningful reference */
101
- const standard = options.config === undefined
102
- && (part === "all" || part === "schema" || options.embed === true);
103
- const config = options.config ?? (standard ? standardConfig : undefined);
177
+ const given = options.config !== undefined && options.config.length > 0;
178
+ const standard = !given && (part === "all" || part === "schema" || options.embed === true);
179
+ const config = given || standard ? await this.configFiles(options.config) : undefined;
104
180
  const embed = options.embed === true || standard;
105
181
  const basedir = options.basedir ?? (part === "spec" ? "." : undefined);
106
- if (config !== undefined)
107
- this.requireConfig(config, verbose);
182
+ const schema = config !== undefined ? this.requireConfig(config, verbose) : undefined;
108
183
  verbose(`describing the "${literal(part)}" part of the SpecBook models and formats ` +
109
184
  `in the "${literal(format)}" format`);
110
- return describeFormat({ ...options, config, basedir, embed, standard, format, part });
185
+ return describeFormat({ ...options, config, schema, basedir, embed, standard, format, part });
111
186
  }
112
187
  }