@rse/specbook 0.9.5 → 0.9.6

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 (52) hide show
  1. package/README.md +58 -328
  2. package/dst/specbook-api.d.ts +13 -20
  3. package/dst/specbook-api.js +28 -25
  4. package/dst/specbook-cli.js +34 -50
  5. package/dst/specbook-cmd-describe.d.ts +5 -2
  6. package/dst/specbook-cmd-describe.js +32 -91
  7. package/dst/specbook-cmd-export.d.ts +3 -3
  8. package/dst/specbook-cmd-export.js +4 -3
  9. package/dst/specbook-cmd-init.d.ts +2 -2
  10. package/dst/specbook-cmd-init.js +35 -13
  11. package/dst/specbook-cmd-lint.d.ts +6 -6
  12. package/dst/specbook-cmd-lint.js +36 -23
  13. package/dst/specbook-config.d.ts +2 -2
  14. package/dst/specbook-config.js +2 -2
  15. package/dst/specbook-diagram.d.ts +4 -4
  16. package/dst/specbook-diagram.js +21 -15
  17. package/dst/specbook-export-ast.d.ts +3 -3
  18. package/dst/specbook-export-common.d.ts +10 -9
  19. package/dst/specbook-export-common.js +23 -9
  20. package/dst/specbook-export-html.css +13 -3
  21. package/dst/specbook-export-html.d.ts +4 -4
  22. package/dst/specbook-export-html.js +61 -32
  23. package/dst/specbook-export-logo-dark.svg +35 -0
  24. package/dst/specbook-export-logo-light.svg +35 -0
  25. package/dst/specbook-export-md.d.ts +3 -3
  26. package/dst/{specbook-struct-schema.d.ts → specbook-format-schema.d.ts} +3 -2
  27. package/dst/{specbook-struct-schema.js → specbook-format-schema.js} +2 -1
  28. package/dst/specbook-format-spec.d.ts +31 -0
  29. package/dst/{specbook-struct-spec.js → specbook-format-spec.js} +10 -10
  30. package/dst/specbook-format.md +734 -0
  31. package/dst/specbook-link.d.ts +2 -2
  32. package/dst/specbook-mcp.d.ts +2 -1
  33. package/dst/specbook-mcp.js +13 -52
  34. package/dst/specbook-parse-common.d.ts +5 -3
  35. package/dst/specbook-parse-common.js +8 -0
  36. package/dst/specbook-parse-semantic.d.ts +4 -4
  37. package/dst/specbook-parse-semantic.js +4 -65
  38. package/dst/specbook-parse-syntax.d.ts +2 -2
  39. package/dst/specbook-parse-syntax.js +18 -14
  40. package/dst/specbook-parse.d.ts +4 -4
  41. package/dst/specbook-parse.js +3 -3
  42. package/dst/specbook-verbose.d.ts +2 -0
  43. package/dst/specbook-verbose.js +16 -0
  44. package/package.json +3 -7
  45. package/dst/specbook-cmd-edit.d.ts +0 -9
  46. package/dst/specbook-cmd-edit.js +0 -74
  47. package/dst/specbook-cmd-import.d.ts +0 -9
  48. package/dst/specbook-cmd-import.js +0 -78
  49. package/dst/specbook-export-logo.svg +0 -35
  50. package/dst/specbook-llm.d.ts +0 -13
  51. package/dst/specbook-llm.js +0 -51
  52. package/dst/specbook-struct-spec.d.ts +0 -31
@@ -1,13 +1,14 @@
1
- import type { Specification, Object as SpecObject } from "./specbook-struct-spec.js";
1
+ import type { Spec, SpecObject } from "./specbook-format-spec.js";
2
2
  export declare const escapeHtml: (text: string) => string;
3
3
  export declare const stylesheet: () => string;
4
4
  export declare const searchScript: () => string;
5
- export declare const fallbackLogo: () => string;
5
+ export declare const fallbackLogo: (theme: string) => string;
6
6
  export declare const isTitleObject: (object: SpecObject) => boolean;
7
- export declare const documentLang: (specification: Specification) => string | undefined;
8
- export declare const documentCharset: (specification: Specification) => string | undefined;
9
- export declare const documentThemeStyle: (specification: Specification) => string | undefined;
10
- export declare const documentThemeTone: (specification: Specification) => string | undefined;
7
+ export declare const titleObject: (specification: Spec) => SpecObject | undefined;
8
+ export declare const documentLang: (specification: Spec) => string | undefined;
9
+ export declare const documentCharset: (specification: Spec) => string | undefined;
10
+ export declare const documentThemeStyle: (specification: Spec) => string | undefined;
11
+ export declare const documentThemeTone: (specification: Spec) => string | undefined;
11
12
  export type PaperSetup = {
12
13
  unit: "mm" | "in";
13
14
  height: number;
@@ -21,12 +22,12 @@ export type PaperSetup = {
21
22
  export declare const paperSizeDefault = "A4";
22
23
  export declare const paperSetup: (paper: string) => PaperSetup;
23
24
  export declare const paperLength: (setup: PaperSetup, value: number) => string;
24
- export declare const documentPaperSize: (specification: Specification) => string;
25
+ export declare const documentPaperSize: (specification: Spec) => string;
25
26
  export declare const paperStylesheet: (paper: string) => string;
26
27
  export declare const charsetCodepoints: (charset: string) => number[] | undefined;
27
28
  export declare const subsetStylesheet: (charset?: string) => Promise<string>;
28
- export declare const documentTitle: (specification: Specification) => {
29
+ export declare const documentTitle: (specification: Spec) => {
29
30
  title: string;
30
31
  subtitle?: string;
31
32
  };
32
- export declare const documentLogo: (specification: Specification) => string;
33
+ export declare const documentLogo: (specification: Spec) => string;
@@ -14,13 +14,26 @@ export const escapeHtml = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&
14
14
  export const stylesheet = () => fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "specbook-export-html.css"), "utf8");
15
15
  /* provide the build-time bundled client-side fuzzy search script */
16
16
  export const searchScript = () => fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "specbook-export-html-search.js"), "utf8");
17
- /* provide the build-time bundled fallback logo of SpecBook itself
18
- (as a self-contained data: URL, to keep its styles isolated) */
19
- export const fallbackLogo = () => "data:image/svg+xml;base64," + fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "specbook-export-logo.svg")).toString("base64");
17
+ /* provide a theme variant of the build-time bundled fallback logo of
18
+ SpecBook itself (as a self-contained data: URL, to keep its styles isolated) */
19
+ export const fallbackLogo = (theme) => "data:image/svg+xml;base64," + fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), `specbook-export-logo-${theme}.svg`)).toString("base64");
20
20
  /* check whether an object is the specification title object */
21
21
  export const isTitleObject = (object) => object.kind === "META" && object.name.toUpperCase() === "TITLE";
22
- /* determine the title object of the specification */
23
- const titleObject = (specification) => specification.artifacts.flatMap((artifact) => artifact.objects).find(isTitleObject);
22
+ /* determine the title object of the specification, searched at any
23
+ nesting level in document order, where the first match wins */
24
+ export const titleObject = (specification) => {
25
+ const search = (objects) => {
26
+ for (const object of objects) {
27
+ if (isTitleObject(object))
28
+ return object;
29
+ const found = search(object.childs);
30
+ if (found !== undefined)
31
+ return found;
32
+ }
33
+ return undefined;
34
+ };
35
+ return search(specification.artifacts.flatMap((artifact) => artifact.objects));
36
+ };
24
37
  /* determine a property value of the title object */
25
38
  const titleProperty = (specification, name) => titleObject(specification)?.properties.find((property) => property.key === name)?.value;
26
39
  /* determine the document language (LANG) from the title object */
@@ -142,14 +155,15 @@ export const documentTitle = (specification) => ({
142
155
  subtitle: titleProperty(specification, "SUBTITLE")
143
156
  });
144
157
  /* determine the document logo as a self-contained data: URL, taken from the
145
- embedded image of the LOGO property of the title object, and falling back
146
- onto the built-in SpecBook logo (for use in isolated rendering contexts,
147
- like the print header/footer, which load no external resources) */
158
+ first embedded image of the LOGO property of the title object -- the light
159
+ variant of a "{theme}" reference -- and falling back onto the built-in
160
+ SpecBook logo (for use in isolated rendering contexts, like the print
161
+ header/footer, which load no external resources and are always light) */
148
162
  export const documentLogo = (specification) => {
149
163
  const content = titleObject(specification)
150
164
  ?.properties.find((property) => property.key === "LOGO")?.embedding?.[0];
151
165
  if (content === undefined)
152
- return fallbackLogo();
166
+ return fallbackLogo("light");
153
167
  return content.startsWith("data:") ? content :
154
168
  `data:image/svg+xml;base64,${Buffer.from(content, "utf8").toString("base64")}`;
155
169
  };
@@ -94,7 +94,7 @@ html.theme-dark { --theme-color-specbook-background: var(--theme-co
94
94
  --gradia-color-group-name: var(--theme-color-accent-15);
95
95
  --gradia-color-group-box: var(--theme-color-accent-1);
96
96
  --gradia-color-group-border: var(--theme-color-accent-7);
97
- --gradia-color-edge-line: var(--theme-color-base-16);
97
+ --gradia-color-edge-line: var(--theme-color-base-20);
98
98
  --gradia-color-edge-name: var(--theme-color-base-29);
99
99
  --gradia-color-edge-arity: var(--theme-color-base-29);
100
100
  --gradia-color-edge-halo: var(--theme-color-specbook-background); }
@@ -222,13 +222,24 @@ div.description li { margin: 0.1rem 0; }
222
222
  div.embedding { margin: 0.5rem 0; }
223
223
  div.embedding img,
224
224
  div.embedding svg { max-width: 100%; height: auto; }
225
+
226
+ /* the two theme variants of a "{theme}" image embedding: the containers
227
+ are layout-neutral and only the variant matching the active theme stays
228
+ visible, where print (like the theme mapping above) always falls back
229
+ onto the light variant */
230
+ span.theme-light-only { display: contents; }
231
+ span.theme-dark-only { display: none; }
232
+ @media screen {
233
+ html.theme-dark span.theme-light-only { display: none; }
234
+ html.theme-dark span.theme-dark-only { display: contents; }
235
+ }
236
+
225
237
  div.diagram { margin: 1.0rem 0; }
226
238
  div.diagram svg { max-width: 100%; height: auto; }
227
239
  span.rationale { color: var(--theme-color-specbook-muted); }
228
240
  p.rationale { color: var(--theme-color-specbook-muted); margin: 0.5rem 0 0 0; }
229
241
  span.keyword { font-weight: bold; }
230
242
  code { font-family: "Source Code Pro", monospace; font-size: 1.0em; letter-spacing: -0.030rem; }
231
- div.meta { color: var(--theme-color-specbook-muted); font-size: 0.8rem; }
232
243
 
233
244
  div.titlepage { text-align: center; margin: 6rem 0 4rem 0;
234
245
  page-break-after: always; }
@@ -261,7 +272,6 @@ div.search { display: none; }
261
272
  /* the search result filtering and matched word highlighting */
262
273
  .search-hide { display: none; }
263
274
  body.searching nav.toc,
264
- body.searching div.meta,
265
275
  body.searching div.embedding { display: none; }
266
276
  mark.search-hit { color: var(--theme-color-specbook-mark-text);
267
277
  background-color: var(--theme-color-specbook-mark-back);
@@ -1,9 +1,9 @@
1
- import type { Specification } from "./specbook-struct-spec.js";
2
- import type { SchemaSpecification } from "./specbook-struct-schema.js";
1
+ import type { Spec } from "./specbook-format-spec.js";
2
+ import type { Schema } from "./specbook-format-schema.js";
3
3
  export type OutlineEntry = {
4
4
  title: string;
5
5
  anchor: string;
6
6
  childs: OutlineEntry[];
7
7
  };
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>;
8
+ export declare const htmlOutline: (specification: Spec, config?: Schema) => OutlineEntry[];
9
+ export declare const renderHtml: (specification: Spec, config?: Schema, tocPages?: Map<string, number>, css?: string) => Promise<string>;
@@ -11,8 +11,8 @@ import textframe from "textframe";
11
11
  import { Gradia } from "@rse/gradia";
12
12
  import { buildLinkIndex, resolveUnique, expandReferences, anchorPaths } from "./specbook-link.js";
13
13
  import { compileValueExpr, splitItems } from "./specbook-parse-value.js";
14
- import { embeddingRegex, embeddingMimeType } from "./specbook-parse-common.js";
15
- import { escapeHtml, stylesheet, searchScript, fallbackLogo, isTitleObject, documentTitle, documentLang, documentThemeStyle } from "./specbook-export-common.js";
14
+ import { embeddingRegex, embeddingMimeType, embeddingThemes, embeddingVariants } from "./specbook-parse-common.js";
15
+ import { escapeHtml, stylesheet, searchScript, fallbackLogo, isTitleObject, titleObject, documentTitle, documentLang, documentThemeStyle } from "./specbook-export-common.js";
16
16
  import { specDiagrams } from "./specbook-diagram.js";
17
17
  /* ==== Templates ==== */
18
18
  /* the built-in Nunjucks templates for the HTML export */
@@ -51,7 +51,6 @@ const templates = {
51
51
  <body>
52
52
  <div class="theme-switch" onclick="themeSwitch()" title="switch color theme">&#x25D0;</div>
53
53
  {{ Document.titlepage }}
54
- {% if not Document.titlepage %}<div class="meta">created: {{ Document.created }}, modified: {{ Document.modified }}</div>{% endif %}
55
54
  {{ Document.toc }}
56
55
  {{ Document.artifacts }}
57
56
  {% if Document.search %}<script>{{ Document.search }}</script>{% endif %}
@@ -242,16 +241,35 @@ const isBlock = (text) => {
242
241
  const tokens = marked.lexer(text).filter((token) => token.type !== "space");
243
242
  return tokens.length > 0 && !(tokens.length === 1 && tokens[0].type === "paragraph");
244
243
  };
245
- /* render the embedded image files of a text into HTML (SVG inlined
246
- as-is, PNG/JPEG placed onto <img> tags), taking the image alternate
247
- texts from the corresponding "![alt](file)" markups */
244
+ /* render a single embedded image file into HTML (SVG inlined as-is,
245
+ PNG/JPEG placed onto an <img> tag) */
246
+ const renderImage = (content, alt) => content.startsWith("data:") ?
247
+ `<img src="${content}" alt="${escapeHtml(alt)}"/>` :
248
+ content.replace(/^\s*<\?xml[^>]*\?>\s*(?:<!DOCTYPE[^>]*>\s*)?/i, "");
249
+ /* wrap the theme variants of an image into their layout-neutral theme
250
+ containers, of which the stylesheet shows just the one matching the
251
+ color theme currently active in the document */
252
+ const renderThemed = (images) => images.map((image, i) => `<span class="theme-${embeddingThemes[i]}-only">${image}</span>`).join("");
253
+ /* render the embedded image files of a text into HTML, taking the image
254
+ alternate texts from the corresponding "![alt](file)" markups and
255
+ pairing up the consecutive theme variants of a "{theme}" markup */
248
256
  const renderEmbeddings = (text, embedding) => {
249
- const alts = [...text.matchAll(embeddingRegex)]
250
- .filter((m) => embeddingMimeType(m[2].trim()) !== undefined)
251
- .map((m) => m[1].trim());
252
- return embedding.map((content, i) => content.startsWith("data:") ?
253
- `<img src="${content}" alt="${escapeHtml(alts[i] ?? "")}"/>` :
254
- content.replace(/^\s*<\?xml[^>]*\?>\s*(?:<!DOCTYPE[^>]*>\s*)?/i, ""));
257
+ const result = new Array();
258
+ let i = 0;
259
+ for (const m of text.matchAll(embeddingRegex)) {
260
+ const reference = m[2].trim();
261
+ if (embeddingMimeType(reference) === undefined)
262
+ continue;
263
+ const variants = embeddingVariants(reference);
264
+ const images = embedding.slice(i, i + variants.length)
265
+ .map((content) => renderImage(content, m[1].trim()));
266
+ i += variants.length;
267
+ if (variants.length > 1 && images.length === variants.length)
268
+ result.push(renderThemed(images));
269
+ else
270
+ result.push(...images);
271
+ }
272
+ return result;
255
273
  };
256
274
  /* render a description into HTML, expanding its inline Markdown and
257
275
  moving the file embeddings to the end of the description */
@@ -312,13 +330,10 @@ const collectSchemas = (specification, config) => {
312
330
  }
313
331
  return result;
314
332
  };
315
- /* strip a trailing parenthesized annotation from a
316
- property key (e.g. "WHEN (Context)") */
317
- const plainKey = (key) => key.replace(/\s*\([^)]*\)\s*$/, "").trim();
318
333
  /* render a property value, badging the individual members of an
319
334
  "enum(...)" (a single member) or "tags(...)" (a member set) value */
320
335
  const inlineValue = (kind, key, value) => {
321
- const member = members?.get(`${kind}${plainKey(key)}`);
336
+ const member = members?.get(`${kind}${key}`);
322
337
  if (member === undefined)
323
338
  return inline(value);
324
339
  const items = member === "tags" ? splitItems(value) : [value.trim()];
@@ -340,7 +355,7 @@ const effectiveProperties = (object) => {
340
355
  if (schema?.format?.withUnusedProps !== true)
341
356
  return object.properties;
342
357
  const merged = (schema.props ?? []).flatMap((prop) => {
343
- const present = object.properties.filter((property) => plainKey(property.key) === prop.name);
358
+ const present = object.properties.filter((property) => property.key === prop.name);
344
359
  return present.length > 0 ? present : [{ key: prop.name, value: "" }];
345
360
  });
346
361
  return [...merged, ...object.properties.filter((property) => !merged.includes(property))];
@@ -357,24 +372,29 @@ const tableShape = (childs) => {
357
372
  const schema = schemas?.get(childs[0]);
358
373
  if (schema?.format?.withUnusedProps === true) {
359
374
  const merged = (schema.props ?? []).flatMap((prop) => {
360
- const present = keys.filter((key) => plainKey(key) === prop.name);
375
+ const present = keys.filter((key) => key === prop.name);
361
376
  return present.length > 0 ? present : [prop.name];
362
377
  });
363
378
  keys = [...merged, ...keys.filter((key) => !merged.includes(key))];
364
379
  }
365
380
  return { keys, desc: childs.some((child) => child.description !== undefined || child.childs.length > 0) };
366
381
  };
382
+ /* provide the childs of an object taking part in the regular document
383
+ flow: a nested title object leaves the flow, as it is rendered as the
384
+ title page instead (a top-level one instead removes its whole artifact) */
385
+ const flowChilds = (object) => object.childs.filter((child) => !isTitleObject(child));
367
386
  /* decide whether the childs of an object collapse into the concise
368
387
  (tabular) rendering: an explicit "format" type wins, "auto" collapses
369
388
  the deepest level only, and inside an already concise rendering
370
389
  context "auto" childs implicitly stay concise, too */
371
390
  const conciseChilds = (object, schemaMap, concise) => {
372
- if (object.childs.length === 0)
391
+ const childs = flowChilds(object);
392
+ if (childs.length === 0)
373
393
  return false;
374
394
  const type = schemaMap?.get(object)?.format?.type ?? "auto";
375
395
  if (type !== "auto")
376
396
  return type === "concise";
377
- return concise || object.childs.every((child) => child.childs.length === 0);
397
+ return concise || childs.every((child) => child.childs.length === 0);
378
398
  };
379
399
  /* group the childs of an object by their kind, preserving order */
380
400
  const groupChilds = (childs) => {
@@ -394,10 +414,11 @@ const groupChilds = (childs) => {
394
414
  ones as regular nested object renderings pressed into the cell) */
395
415
  const renderCell = (child) => {
396
416
  let html = child.description !== undefined ? renderDescription(child.description) : "";
397
- if (child.childs.length > 0)
417
+ const childs = flowChilds(child);
418
+ if (childs.length > 0)
398
419
  html += conciseChilds(child, schemas, true) ?
399
- groupChilds(child.childs).map((group) => renderTable(group, maxColumnsOf(child))).join("") :
400
- child.childs.map((sub) => renderObject(sub, 6, true)).join("");
420
+ groupChilds(childs).map((group) => renderTable(group, maxColumnsOf(child))).join("") :
421
+ childs.map((sub) => renderObject(sub, 6, true)).join("");
401
422
  return html;
402
423
  };
403
424
  /* render a single-kind group of childs into one compact table:
@@ -474,8 +495,8 @@ const renderObject = (object, level, concise) => {
474
495
  description: object.description !== undefined ?
475
496
  safe(renderDescription(object.description)) : "",
476
497
  childs: conciseChilds(object, schemas, concise) ?
477
- safe(groupChilds(object.childs).map((group) => renderTable(group, maxColumnsOf(object))).join("")) :
478
- safe(object.childs.map((child) => renderObject(child, level + 1, concise)).join(""))
498
+ safe(groupChilds(flowChilds(object)).map((group) => renderTable(group, maxColumnsOf(object))).join("")) :
499
+ safe(flowChilds(object).map((child) => renderObject(child, level + 1, concise)).join(""))
479
500
  } });
480
501
  };
481
502
  /* render the title object into a title page */
@@ -489,11 +510,13 @@ const renderTitlePage = (object, created, modified) => {
489
510
  "LANG", "CHARSET", "PAPER-SIZE", "THEME-STYLE", "THEME-TONE"].includes(property.key));
490
511
  /* the logo is rendered above the title, from the embedded image content
491
512
  of the LOGO property or, for a non-embeddable reference, as its inline
492
- Markdown; without a LOGO property the built-in SpecBook logo is used */
513
+ Markdown; without a LOGO property the built-in SpecBook logo is used,
514
+ in both its theme variants */
493
515
  const logo = object.properties.find((property) => property.key === "LOGO");
494
516
  const image = logo !== undefined ? renderEmbeddings(logo.value, logo.embedding ?? []) : [];
495
517
  return render("TitlePage", { TitlePage: {
496
- logo: logo === undefined ? safe(`<img src="${fallbackLogo()}" alt="SpecBook"/>`) :
518
+ logo: logo === undefined ?
519
+ safe(renderThemed(embeddingThemes.map((theme) => `<img src="${fallbackLogo(theme)}" alt="SpecBook"/>`))) :
497
520
  (image.length > 0 ? safe(image.join("")) : inline(logo.value)),
498
521
  title: inline(prop("TITLE") ?? object.name),
499
522
  subtitle: inlineProp("SUBTITLE"),
@@ -518,13 +541,22 @@ export const htmlOutline = (specification, config) => {
518
541
  const entry = (object) => ({
519
542
  title: (object.kind !== "" ? `${object.kind}: ` : "") + object.name,
520
543
  anchor: paths.get(object) ?? object.id,
521
- childs: conciseChilds(object, schemaMap, false) ? [] : object.childs.map(entry)
544
+ childs: conciseChilds(object, schemaMap, false) ? [] : flowChilds(object).map(entry)
522
545
  });
523
546
  return specification.artifacts
524
547
  .filter((artifact) => !artifact.objects.some(isTitleObject))
525
548
  .flatMap((artifact) => artifact.objects)
526
549
  .map(entry);
527
550
  };
551
+ /* determine the object a title page is rendered from: the title object
552
+ has to carry a non-empty "TITLE" property, as without it there is
553
+ nothing to render a title page from (the object stays suppressed in
554
+ the regular flow nevertheless) */
555
+ const titlePageObject = (specification) => {
556
+ const object = titleObject(specification);
557
+ const title = object?.properties.find((property) => property.key === "TITLE");
558
+ return title !== undefined && title.value.trim() !== "" ? object : undefined;
559
+ };
528
560
  /* render the entire specification into a self-contained HTML document,
529
561
  with the build-time pre-assembled stylesheet embedded inline, the
530
562
  artifact timestamps aggregated into min(Created)/max(Modified), and
@@ -577,8 +609,7 @@ export const renderHtml = async (specification, config, tocPages, css) => {
577
609
  const created = new Date(Math.min(...specification.artifacts.map((artifact) => artifact.created.getTime())));
578
610
  const modified = new Date(Math.max(...specification.artifacts.map((artifact) => artifact.modified.getTime())));
579
611
  /* a "META: Title" object becomes the title page and leaves the regular flow */
580
- const title = specification.artifacts
581
- .flatMap((artifact) => artifact.objects).find(isTitleObject);
612
+ const title = titlePageObject(specification);
582
613
  const artifacts = specification.artifacts
583
614
  .filter((artifact) => !artifact.objects.some(isTitleObject));
584
615
  const entries = artifacts.flatMap((artifact) => artifact.objects)
@@ -589,8 +620,6 @@ export const renderHtml = async (specification, config, tocPages, css) => {
589
620
  lang,
590
621
  theme: documentThemeStyle(specification)?.toLowerCase(),
591
622
  css: safe(css ?? stylesheet()),
592
- created: created.toISOString(),
593
- modified: modified.toISOString(),
594
623
  titlepage: title !== undefined ?
595
624
  safe(renderTitlePage(title, created.toISOString().slice(0, 10), modified.toISOString().slice(0, 10))) : "",
596
625
  search: title !== undefined ? safe(searchScript()) : "",
@@ -0,0 +1,35 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 260.8 110.5">
3
+ <!-- Generator: Adobe Illustrator 30.7.0, SVG Export Plug-In . SVG Version: 2.1.4 Build 114) -->
4
+ <defs>
5
+ <style>
6
+ .st0 {
7
+ fill: none;
8
+ }
9
+
10
+ .st1 {
11
+ fill: #69c;
12
+ }
13
+
14
+ .st2 {
15
+ fill: #9fc9eb;
16
+ }
17
+ </style>
18
+ </defs>
19
+ <g>
20
+ <rect class="st0" x="128" y="5.9" width="297.7" height="245.4"/>
21
+ <path class="st2" d="M132.6,41.3c2.6,1.3,6.7,2.7,10.8,2.7s6.8-1.9,6.8-4.7-2-4.2-7.2-6c-7.2-2.6-11.9-6.5-11.9-12.8s6.2-12.9,16.3-12.9,8.4,1,11,2.2l-2.2,7.8c-1.7-.8-4.8-2-9-2s-6.3,2-6.3,4.2,2.4,4,8,6.1c7.6,2.8,11.1,6.8,11.1,12.9s-5.5,13.4-17.3,13.4-9.8-1.3-12.2-2.7l2-8Z"/>
22
+ <path class="st2" d="M163.3,30.5c0-4.1-.1-7.6-.3-10.5h8.4l.4,4.4h.1c2.3-3.3,5.9-5.1,10.4-5.1,6.8,0,13,6,13,15.9s-7.2,16.8-14.2,16.8-6.7-1.5-8.1-3.6h-.1v15.6h-9.7V30.5ZM173,37.9c0,.8,0,1.4.2,2,.6,2.6,2.9,4.6,5.7,4.6,4.2,0,6.7-3.5,6.7-8.8s-2.2-8.8-6.6-8.8-5.2,2-5.8,4.9c-.1.5-.2,1.2-.2,1.7v4.3Z"/>
23
+ <path class="st2" d="M206,38.8c.3,4,4.3,6,8.8,6s6-.4,8.6-1.3l1.3,6.6c-3.2,1.3-7.1,2-11.3,2-10.6,0-16.7-6.1-16.7-15.9s4.9-16.7,15.8-16.7,14,7.9,14,15.6-.2,3.1-.3,3.8h-20.2ZM217.5,32.1c0-2.4-1-6.3-5.5-6.3s-5.8,3.8-6,6.3h11.5Z"/>
24
+ <path class="st2" d="M252.6,50.5c-1.7.8-5,1.5-8.7,1.5-10.1,0-16.6-6.2-16.6-16s6.3-16.6,17.9-16.6,5.4.4,7.4,1.2l-1.5,7.2c-1.2-.5-2.9-1-5.4-1-5.1,0-8.4,3.6-8.4,8.8,0,5.7,3.8,8.8,8.6,8.8s4.1-.4,5.6-1l1.2,7.2Z"/>
25
+ </g>
26
+ <path class="st2" d="M25.4,18.5h0c-1,0-2,.4-2.7,1.1-.7.7-1.1,1.7-1.1,2.8v58.4c0,2.1,1.7,3.9,3.9,3.9,9.1,0,24.3,1.9,34.7,12.9v-61.1c0-.7-.2-1.4-.5-2-8.6-13.9-25.1-16-34.2-16Z"/>
27
+ <path class="st2" d="M104.8,80.9V22.4c0-1-.4-2-1.1-2.8-.7-.7-1.7-1.1-2.7-1.1h0c-9.1,0-25.6,2.1-34.2,16-.4.6-.5,1.2-.5,2v61.1c10.5-11,25.7-12.9,34.7-12.9,2.2,0,3.9-1.8,3.9-3.9Z"/>
28
+ <path class="st1" d="M113.7,32h-2.8v48.8c0,5.5-4.5,9.9-9.9,9.9-7.7,0-20.4,1.5-29.4,10,15.5-3.8,31.9-1.3,41.2.8,1.2.3,2.4,0,3.3-.8.9-.7,1.5-1.9,1.5-3v-61.9c0-2.1-1.7-3.9-3.9-3.9Z"/>
29
+ <path class="st1" d="M15.4,80.9v-48.8h-2.8c-2.1,0-3.9,1.7-3.9,3.9v61.9c0,1.2.5,2.3,1.5,3,.9.7,2.1,1,3.3.8,9.3-2.1,25.7-4.6,41.2-.8-9-8.5-21.7-10-29.4-10-5.5,0-9.9-4.5-9.9-9.9Z"/>
30
+ <g>
31
+ <path class="st1" d="M131.7,55.6c2.6-.6,6.8-1,11-1,6,0,9.9,1,12.7,3.4,2.4,1.8,3.9,4.6,3.9,8.2s-3,8.5-7.9,10.2v.2c4.4,1,9.6,4.7,9.6,11.6s-1.6,7.1-4,9.3c-3.2,3-8.4,4.4-16,4.4s-7.3-.3-9.3-.6v-45.7ZM137.7,74.6h5.5c6.3,0,10-3.4,10-7.9s-4.1-7.6-10.2-7.6-4.4.2-5.3.4v15.1ZM137.7,96.9c1.2.2,2.9.3,5,.3,6.2,0,12-2.3,12-9.1s-5.5-9-12-9h-5v17.8Z"/>
32
+ <path class="st1" d="M206.1,66.9c-9.3,0-16.6,6.6-16.6,17.8s0,.2,0,.4h0c-.3,7.2-4.5,12.3-10.1,12.3s-10.2-5.5-10.2-12.8,3.1-13,10.3-13,7,2.6,8.5,5.8c.8-2.1,1.9-4,3.4-5.5-2.8-3.1-6.9-4.9-11.8-4.9-9.3,0-16.6,6.6-16.6,17.8s7,17.2,16,17.2,16.2-5.1,16.6-16.8h0c0-.2,0-.4,0-.6,0-6.4,3.1-13,10.3-13s10,7.2,10,12.8-4.4,13-10.2,13-7.3-2.6-8.9-6.5c-.7,2.3-1.9,4.2-3.3,5.8,2.9,3.4,7.1,5.3,12,5.3,8.1,0,16.6-5.4,16.6-17.8s-6.5-17.2-16.1-17.2Z"/>
33
+ <path class="st1" d="M230.6,83.8h.1c.8-1.2,1.9-2.6,2.9-3.8l9.9-11.5h7.3l-12.9,13.7,14.7,19.7h-7.5l-11.5-16-3.1,3.5v12.6h-6v-49.1h6v31Z"/>
34
+ </g>
35
+ </svg>
@@ -0,0 +1,35 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 260.8 110.5">
3
+ <!-- Generator: Adobe Illustrator 30.7.0, SVG Export Plug-In . SVG Version: 2.1.4 Build 114) -->
4
+ <defs>
5
+ <style>
6
+ .st0 {
7
+ fill: none;
8
+ }
9
+
10
+ .st1 {
11
+ fill: #69c;
12
+ }
13
+
14
+ .st2 {
15
+ fill: #33669a;
16
+ }
17
+ </style>
18
+ </defs>
19
+ <g>
20
+ <rect class="st0" x="128" y="5.9" width="297.7" height="245.4"/>
21
+ <path class="st2" d="M132.6,41.3c2.6,1.3,6.7,2.7,10.8,2.7s6.8-1.9,6.8-4.7-2-4.2-7.2-6c-7.2-2.6-11.9-6.5-11.9-12.8s6.2-12.9,16.3-12.9,8.4,1,11,2.2l-2.2,7.8c-1.7-.8-4.8-2-9-2s-6.3,2-6.3,4.2,2.4,4,8,6.1c7.6,2.8,11.1,6.8,11.1,12.9s-5.5,13.4-17.3,13.4-9.8-1.3-12.2-2.7l2-8Z"/>
22
+ <path class="st2" d="M163.3,30.5c0-4.1-.1-7.6-.3-10.5h8.4l.4,4.4h.1c2.3-3.3,5.9-5.1,10.4-5.1,6.8,0,13,6,13,15.9s-7.2,16.8-14.2,16.8-6.7-1.5-8.1-3.6h-.1v15.6h-9.7V30.5ZM173,37.9c0,.8,0,1.4.2,2,.6,2.6,2.9,4.6,5.7,4.6,4.2,0,6.7-3.5,6.7-8.8s-2.2-8.8-6.6-8.8-5.2,2-5.8,4.9c-.1.5-.2,1.2-.2,1.7v4.3Z"/>
23
+ <path class="st2" d="M206,38.8c.3,4,4.3,6,8.8,6s6-.4,8.6-1.3l1.3,6.6c-3.2,1.3-7.1,2-11.3,2-10.6,0-16.7-6.1-16.7-15.9s4.9-16.7,15.8-16.7,14,7.9,14,15.6-.2,3.1-.3,3.8h-20.2ZM217.5,32.1c0-2.4-1-6.3-5.5-6.3s-5.8,3.8-6,6.3h11.5Z"/>
24
+ <path class="st2" d="M252.6,50.5c-1.7.8-5,1.5-8.7,1.5-10.1,0-16.6-6.2-16.6-16s6.3-16.6,17.9-16.6,5.4.4,7.4,1.2l-1.5,7.2c-1.2-.5-2.9-1-5.4-1-5.1,0-8.4,3.6-8.4,8.8,0,5.7,3.8,8.8,8.6,8.8s4.1-.4,5.6-1l1.2,7.2Z"/>
25
+ </g>
26
+ <path class="st2" d="M25.4,18.5h0c-1,0-2,.4-2.7,1.1-.7.7-1.1,1.7-1.1,2.8v58.4c0,2.1,1.7,3.9,3.9,3.9,9.1,0,24.3,1.9,34.7,12.9v-61.1c0-.7-.2-1.4-.5-2-8.6-13.9-25.1-16-34.2-16Z"/>
27
+ <path class="st2" d="M104.8,80.9V22.4c0-1-.4-2-1.1-2.8-.7-.7-1.7-1.1-2.7-1.1h0c-9.1,0-25.6,2.1-34.2,16-.4.6-.5,1.2-.5,2v61.1c10.5-11,25.7-12.9,34.7-12.9,2.2,0,3.9-1.8,3.9-3.9Z"/>
28
+ <path class="st1" d="M113.7,32h-2.8v48.8c0,5.5-4.5,9.9-9.9,9.9-7.7,0-20.4,1.5-29.4,10,15.5-3.8,31.9-1.3,41.2.8,1.2.3,2.4,0,3.3-.8.9-.7,1.5-1.9,1.5-3v-61.9c0-2.1-1.7-3.9-3.9-3.9Z"/>
29
+ <path class="st1" d="M15.4,80.9v-48.8h-2.8c-2.1,0-3.9,1.7-3.9,3.9v61.9c0,1.2.5,2.3,1.5,3,.9.7,2.1,1,3.3.8,9.3-2.1,25.7-4.6,41.2-.8-9-8.5-21.7-10-29.4-10-5.5,0-9.9-4.5-9.9-9.9Z"/>
30
+ <g>
31
+ <path class="st1" d="M131.7,55.6c2.6-.6,6.8-1,11-1,6,0,9.9,1,12.7,3.4,2.4,1.8,3.9,4.6,3.9,8.2s-3,8.5-7.9,10.2v.2c4.4,1,9.6,4.7,9.6,11.6s-1.6,7.1-4,9.3c-3.2,3-8.4,4.4-16,4.4s-7.3-.3-9.3-.6v-45.7ZM137.7,74.6h5.5c6.3,0,10-3.4,10-7.9s-4.1-7.6-10.2-7.6-4.4.2-5.3.4v15.1ZM137.7,96.9c1.2.2,2.9.3,5,.3,6.2,0,12-2.3,12-9.1s-5.5-9-12-9h-5v17.8Z"/>
32
+ <path class="st1" d="M206.1,66.9c-9.3,0-16.6,6.6-16.6,17.8s0,.2,0,.4h0c-.3,7.2-4.5,12.3-10.1,12.3s-10.2-5.5-10.2-12.8,3.1-13,10.3-13,7,2.6,8.5,5.8c.8-2.1,1.9-4,3.4-5.5-2.8-3.1-6.9-4.9-11.8-4.9-9.3,0-16.6,6.6-16.6,17.8s7,17.2,16,17.2,16.2-5.1,16.6-16.8h0c0-.2,0-.4,0-.6,0-6.4,3.1-13,10.3-13s10,7.2,10,12.8-4.4,13-10.2,13-7.3-2.6-8.9-6.5c-.7,2.3-1.9,4.2-3.3,5.8,2.9,3.4,7.1,5.3,12,5.3,8.1,0,16.6-5.4,16.6-17.8s-6.5-17.2-16.1-17.2Z"/>
33
+ <path class="st1" d="M230.6,83.8h.1c.8-1.2,1.9-2.6,2.9-3.8l9.9-11.5h7.3l-12.9,13.7,14.7,19.7h-7.5l-11.5-16-3.1,3.5v12.6h-6v-49.1h6v31Z"/>
34
+ </g>
35
+ </svg>
@@ -1,3 +1,3 @@
1
- import type { Specification } from "./specbook-struct-spec.js";
2
- import type { SchemaSpecification } from "./specbook-struct-schema.js";
3
- export declare const renderMarkdown: (specification: Specification, config?: SchemaSpecification) => Promise<string>;
1
+ import type { Spec } from "./specbook-format-spec.js";
2
+ import type { Schema } from "./specbook-format-schema.js";
3
+ export declare const renderMarkdown: (specification: Spec, config?: Schema) => Promise<string>;
@@ -1,6 +1,6 @@
1
1
  import * as v from "valibot";
2
2
  import { type Config as GradiaConfig } from "@rse/gradia";
3
- export type SchemaSpecification = SchemaObject[];
3
+ export type Schema = SchemaObject[];
4
4
  export type SchemaObject = {
5
5
  kind: string;
6
6
  name?: string;
@@ -19,6 +19,7 @@ export type SchemaDiagram = {
19
19
  edges?: string;
20
20
  center?: string;
21
21
  links?: "props" | "all";
22
+ edgeSource?: string;
22
23
  edgeTarget?: string;
23
24
  edgeArity?: string;
24
25
  hierarchy?: boolean;
@@ -39,4 +40,4 @@ export type SchemaProperty = {
39
40
  value?: string;
40
41
  optional?: boolean;
41
42
  };
42
- export declare const SchemaSpecification: v.GenericSchema<SchemaSpecification>;
43
+ export declare const Schema: v.GenericSchema<Schema>;
@@ -25,6 +25,7 @@ const SchemaDiagram = v.strictObject({
25
25
  edges: v.optional(v.string()),
26
26
  center: v.optional(v.string()),
27
27
  links: v.optional(v.picklist(["props", "all"])),
28
+ edgeSource: v.optional(v.string()),
28
29
  edgeTarget: v.optional(v.string()),
29
30
  edgeArity: v.optional(v.string()),
30
31
  hierarchy: v.optional(v.boolean()),
@@ -51,4 +52,4 @@ const SchemaObject = v.strictObject({
51
52
  props: v.optional(v.array(SchemaProperty)),
52
53
  childs: v.optional(v.array(v.lazy(() => SchemaObject)))
53
54
  });
54
- export const SchemaSpecification = v.pipe(v.array(SchemaObject), v.minLength(1));
55
+ export const Schema = v.pipe(v.array(SchemaObject), v.minLength(1));
@@ -0,0 +1,31 @@
1
+ import * as v from "valibot";
2
+ export type Spec = {
3
+ artifacts: SpecArtifact[];
4
+ };
5
+ export type SpecArtifact = {
6
+ created: Date;
7
+ modified: Date;
8
+ objects: SpecObject[];
9
+ };
10
+ export type SpecObject = {
11
+ kind: string;
12
+ id: string;
13
+ anchor?: string;
14
+ paren?: string;
15
+ name: string;
16
+ primary?: boolean;
17
+ description?: SpecDescription;
18
+ properties: SpecProperty[];
19
+ childs: SpecObject[];
20
+ };
21
+ export type SpecDescription = {
22
+ description: string;
23
+ rationale?: string;
24
+ embedding?: string[];
25
+ };
26
+ export type SpecProperty = {
27
+ key: string;
28
+ value: string;
29
+ embedding?: string[];
30
+ };
31
+ export declare const Spec: v.GenericSchema<Spec>;
@@ -5,32 +5,32 @@
5
5
  */
6
6
  import * as v from "valibot";
7
7
  /* ==== Schema ==== */
8
- const Property = v.object({
8
+ const SpecProperty = v.object({
9
9
  key: v.string(),
10
10
  value: v.string(),
11
11
  embedding: v.optional(v.array(v.string()))
12
12
  });
13
- const Description = v.object({
13
+ const SpecDescription = v.object({
14
14
  description: v.string(),
15
15
  rationale: v.optional(v.string()),
16
16
  embedding: v.optional(v.array(v.string()))
17
17
  });
18
- const Object = v.object({
18
+ const SpecObject = v.object({
19
19
  kind: v.string(),
20
20
  id: v.string(),
21
21
  anchor: v.optional(v.string()),
22
22
  paren: v.optional(v.string()),
23
23
  name: v.string(),
24
24
  primary: v.optional(v.boolean()),
25
- description: v.optional(Description),
26
- properties: v.array(Property),
27
- childs: v.array(v.lazy(() => Object))
25
+ description: v.optional(SpecDescription),
26
+ properties: v.array(SpecProperty),
27
+ childs: v.array(v.lazy(() => SpecObject))
28
28
  });
29
- const Artifact = v.object({
29
+ const SpecArtifact = v.object({
30
30
  created: v.date(),
31
31
  modified: v.date(),
32
- objects: v.pipe(v.array(Object), v.minLength(1))
32
+ objects: v.pipe(v.array(SpecObject), v.minLength(1))
33
33
  });
34
- export const Specification = v.object({
35
- artifacts: v.pipe(v.array(Artifact), v.minLength(1))
34
+ export const Spec = v.object({
35
+ artifacts: v.pipe(v.array(SpecArtifact), v.minLength(1))
36
36
  });