@rse/specbook 0.9.5 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +108 -328
  2. package/dst/specbook-api.d.ts +13 -20
  3. package/dst/specbook-api.js +43 -39
  4. package/dst/specbook-cli.js +42 -55
  5. package/dst/specbook-cmd-describe.d.ts +5 -2
  6. package/dst/specbook-cmd-describe.js +30 -91
  7. package/dst/specbook-cmd-export.d.ts +3 -3
  8. package/dst/specbook-cmd-export.js +23 -19
  9. package/dst/specbook-cmd-init.d.ts +3 -3
  10. package/dst/specbook-cmd-init.js +38 -15
  11. package/dst/specbook-cmd-lint.d.ts +5 -6
  12. package/dst/specbook-cmd-lint.js +61 -24
  13. package/dst/specbook-config.d.ts +2 -2
  14. package/dst/specbook-config.js +82 -73
  15. package/dst/specbook-diagnostic.js +4 -1
  16. package/dst/specbook-diagram.d.ts +6 -5
  17. package/dst/specbook-diagram.js +94 -104
  18. package/dst/specbook-export-ast.d.ts +3 -3
  19. package/dst/specbook-export-ast.js +1 -1
  20. package/dst/specbook-export-common.d.ts +11 -9
  21. package/dst/specbook-export-common.js +34 -22
  22. package/dst/specbook-export-html-search.js +10 -14
  23. package/dst/specbook-export-html.css +37 -32
  24. package/dst/specbook-export-html.d.ts +4 -4
  25. package/dst/specbook-export-html.js +153 -107
  26. package/dst/specbook-export-logo-dark.svg +35 -0
  27. package/dst/specbook-export-logo-light.svg +35 -0
  28. package/dst/specbook-export-md.d.ts +3 -3
  29. package/dst/specbook-export-md.js +50 -24
  30. package/dst/specbook-export-pdf.d.ts +4 -2
  31. package/dst/specbook-export-pdf.js +48 -38
  32. package/dst/{specbook-struct-schema.d.ts → specbook-format-schema.d.ts} +3 -2
  33. package/dst/{specbook-struct-schema.js → specbook-format-schema.js} +8 -4
  34. package/dst/specbook-format-spec.d.ts +31 -0
  35. package/dst/{specbook-struct-spec.js → specbook-format-spec.js} +10 -10
  36. package/dst/specbook-format.md +750 -0
  37. package/dst/specbook-link.d.ts +3 -2
  38. package/dst/specbook-link.js +7 -4
  39. package/dst/specbook-mcp.d.ts +2 -1
  40. package/dst/specbook-mcp.js +30 -62
  41. package/dst/specbook-parse-common.d.ts +5 -3
  42. package/dst/specbook-parse-common.js +8 -0
  43. package/dst/specbook-parse-semantic.d.ts +6 -4
  44. package/dst/specbook-parse-semantic.js +98 -106
  45. package/dst/specbook-parse-syntax.d.ts +2 -2
  46. package/dst/specbook-parse-syntax.js +175 -144
  47. package/dst/specbook-parse-value.d.ts +1 -1
  48. package/dst/specbook-parse-value.js +6 -3
  49. package/dst/specbook-parse.d.ts +4 -7
  50. package/dst/specbook-parse.js +28 -35
  51. package/dst/specbook-verbose.d.ts +2 -0
  52. package/dst/specbook-verbose.js +17 -0
  53. package/package.json +3 -7
  54. package/dst/specbook-cmd-edit.d.ts +0 -9
  55. package/dst/specbook-cmd-edit.js +0 -74
  56. package/dst/specbook-cmd-import.d.ts +0 -9
  57. package/dst/specbook-cmd-import.js +0 -78
  58. package/dst/specbook-export-logo.svg +0 -35
  59. package/dst/specbook-llm.d.ts +0 -13
  60. package/dst/specbook-llm.js +0 -51
  61. package/dst/specbook-struct-spec.d.ts +0 -31
@@ -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>;
@@ -9,10 +9,11 @@ import nunjucks from "nunjucks";
9
9
  import nunjucksAddons from "@rse/nunjucks-addons";
10
10
  import textframe from "textframe";
11
11
  import { Gradia } from "@rse/gradia";
12
- import { buildLinkIndex, resolveUnique, expandReferences, anchorPaths } from "./specbook-link.js";
12
+ import { buildLinkIndex, resolveUnique, expandReferences, anchorPaths, plainText } 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
+ import { collectSchemas } from "./specbook-parse-semantic.js";
16
17
  import { specDiagrams } from "./specbook-diagram.js";
17
18
  /* ==== Templates ==== */
18
19
  /* the built-in Nunjucks templates for the HTML export */
@@ -51,7 +52,6 @@ const templates = {
51
52
  <body>
52
53
  <div class="theme-switch" onclick="themeSwitch()" title="switch color theme">&#x25D0;</div>
53
54
  {{ Document.titlepage }}
54
- {% if not Document.titlepage %}<div class="meta">created: {{ Document.created }}, modified: {{ Document.modified }}</div>{% endif %}
55
55
  {{ Document.toc }}
56
56
  {{ Document.artifacts }}
57
57
  {% if Document.search %}<script>{{ Document.search }}</script>{% endif %}
@@ -118,7 +118,7 @@ const templates = {
118
118
  {%- if Description.block %}
119
119
  <div class="description">{{ Description.description }}{% if Description.rationale %}
120
120
  <p class="rationale">&mdash; <span class="keyword">BECAUSE</span> {{ Description.rationale }}</p>{% endif %}</div>
121
- {%- else %}
121
+ {%- elif Description.description or Description.rationale %}
122
122
  <p class="description">{{ Description.description }}{% if Description.rationale %}
123
123
  <span class="rationale">&mdash; <span class="keyword">BECAUSE</span> {{ Description.rationale }}</span>{% endif %}</p>
124
124
  {%- endif %}
@@ -210,6 +210,18 @@ const marked = new Marked();
210
210
  marked.use({ hooks: { postprocess: (html) => html.replace(/&#8220;/g, quotes[0]).replace(/&#8221;/g, quotes[1])
211
211
  .replace(/&(?![a-zA-Z][a-zA-Z0-9]*;|#\d+;|#x[0-9a-fA-F]+;)/g, "&amp;") } });
212
212
  marked.use(markedSmartypants({ config: 1 }));
213
+ /* the switched-off text escaping of marked also leaves the angle
214
+ brackets of the inline text unescaped, which a browser then takes
215
+ for tags ("Map<String, Object>"), so they are escaped upfront in
216
+ the inline text tokenizer (registered after smartypants, as the
217
+ later-registered tokenizer takes precedence) */
218
+ marked.use({ tokenizer: { inlineText(src) {
219
+ const cap = this.rules.inline.text.exec(src);
220
+ if (cap === null)
221
+ return undefined;
222
+ return { type: "text", raw: cap[0], escaped: true,
223
+ text: cap[0].replace(/</g, "&lt;").replace(/>/g, "&gt;") };
224
+ } } });
213
225
  /* the Nunjucks environment with the @rse/nunjucks-addons extensions */
214
226
  const env = new nunjucks.Environment(null, { autoescape: true });
215
227
  nunjucksAddons(env);
@@ -232,38 +244,66 @@ const diagramCache = new WeakMap();
232
244
  const anchorOf = (object) => anchors?.get(object) ?? object.id;
233
245
  /* expand the inline Markdown of a text (code spans, emphasis, etc.),
234
246
  with Wiki-style references expanded upfront */
235
- const inline = (text) => safe(marked.parseInline(linker !== null ? linker(text) : text));
247
+ const inline = (text) => safe(marked.parseInline(linker !== null ? linker(text) : text, { async: false }));
236
248
  /* expand the full Markdown of a text, keeping its block-level
237
249
  constructs (lists, quotes, code blocks, tables) intact */
238
- const block = (text) => safe(marked.parse(linker !== null ? linker(text) : text));
250
+ const block = (text) => safe(marked.parse(linker !== null ? linker(text) : text, { async: false }));
239
251
  /* check whether a text carries any block-level Markdown, i.e. is
240
252
  anything else than the single paragraph a description usually is */
241
253
  const isBlock = (text) => {
242
254
  const tokens = marked.lexer(text).filter((token) => token.type !== "space");
243
255
  return tokens.length > 0 && !(tokens.length === 1 && tokens[0].type === "paragraph");
244
256
  };
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 */
257
+ /* render a single embedded image file onto an <img> tag with a
258
+ self-contained data: URL (converting the SVG text into one, as an
259
+ SVG inlined as-is would leak its document-global <style> rules
260
+ into all other inlined SVGs sharing the same class names) */
261
+ const renderImage = (content, alt) => {
262
+ const url = content.startsWith("data:") ? content :
263
+ `data:image/svg+xml;base64,${Buffer.from(content, "utf8").toString("base64")}`;
264
+ return `<img src="${url}" alt="${escapeHtml(alt)}"/>`;
265
+ };
266
+ /* wrap the theme variants of an image into their layout-neutral theme
267
+ containers, of which the stylesheet shows just the one matching the
268
+ color theme currently active in the document */
269
+ const renderThemed = (images) => images.map((image, i) => `<span class="theme-${embeddingThemes[i]}-only">${image}</span>`).join("");
270
+ /* render the embedded image files of a text into HTML, taking the image
271
+ alternate texts from the corresponding "![alt](file)" markups and
272
+ pairing up the consecutive theme variants of a "{theme}" markup
273
+ (the empty entries of unreadable files are skipped) */
248
274
  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, ""));
275
+ const result = new Array();
276
+ let i = 0;
277
+ for (const m of text.matchAll(embeddingRegex)) {
278
+ const reference = m[2].trim();
279
+ if (embeddingMimeType(reference) === undefined)
280
+ continue;
281
+ const variants = embeddingVariants(reference);
282
+ const contents = embedding.slice(i, i + variants.length);
283
+ const images = contents.filter((content) => content !== "")
284
+ .map((content) => renderImage(content, m[1].trim()));
285
+ i += variants.length;
286
+ if (variants.length > 1 && images.length === variants.length)
287
+ result.push(renderThemed(images));
288
+ else
289
+ result.push(...images);
290
+ }
291
+ return result;
255
292
  };
293
+ /* the image embedding markup including the horizontal whitespace
294
+ before it, so that its removal leaves no double space behind */
295
+ const embeddingMarkup = new RegExp(`[ \\t]*${embeddingRegex.source}`, "g");
256
296
  /* render a description into HTML, expanding its inline Markdown and
257
297
  moving the file embeddings to the end of the description */
258
298
  const renderDescription = (description) => {
259
299
  const text = description.description
260
- .replace(embeddingRegex, (markup, _alt, reference) => embeddingMimeType(reference.trim()) !== undefined ? "" : markup)
261
- .replace(/(?<=\S)[ \t]{2,}/g, " ").trim();
300
+ .replace(embeddingMarkup, (markup, _alt, reference) => embeddingMimeType(reference.trim()) !== undefined ? "" : markup)
301
+ .trim();
262
302
  const embeddings = renderEmbeddings(description.description, description.embedding ?? []).map((content) => safe(content));
263
303
  const blocked = isBlock(text);
264
304
  return render("Description", { Description: {
265
305
  block: blocked,
266
- description: blocked ? block(text) : inline(text),
306
+ description: text !== "" ? (blocked ? block(text) : inline(text)) : "",
267
307
  rationale: description.rationale !== undefined ?
268
308
  inline(description.rationale) : undefined,
269
309
  embeddings
@@ -271,8 +311,8 @@ const renderDescription = (description) => {
271
311
  };
272
312
  /* collect the "enum(...)"/"tags(...)" constrained properties of the
273
313
  schema configuration, keyed by object kind and property name */
274
- const collectMembers = (schemas, result) => {
275
- for (const schema of schemas) {
314
+ const collectMembers = (nodes, result) => {
315
+ for (const schema of nodes) {
276
316
  for (const property of schema.props ?? []) {
277
317
  if (property.value === undefined)
278
318
  continue;
@@ -289,44 +329,25 @@ const collectMembers = (schemas, result) => {
289
329
  }
290
330
  return result;
291
331
  };
292
- /* map the specification objects onto their schema configuration
293
- nodes (the schema resolution mirrors the semantic validation) */
294
- const collectSchemas = (specification, config) => {
295
- const result = new Map();
296
- const walk = (object, schema) => {
297
- result.set(object, schema);
298
- for (const child of object.childs) {
299
- const childSchema = (schema.childs ?? []).find((c) => c.kind === child.kind);
300
- if (childSchema !== undefined)
301
- walk(child, childSchema);
302
- }
303
- };
304
- for (const artifact of specification.artifacts) {
305
- for (const object of artifact.objects) {
306
- const schema = config.find((s) => (s.kind === object.kind && s.id === object.id) || `${s.kind}-${s.id}` === object.id) ??
307
- config.find((s) => (s.name ?? "").toUpperCase() ===
308
- object.name.replace(/`/g, "").toUpperCase());
309
- if (schema !== undefined)
310
- walk(object, schema);
311
- }
312
- }
313
- return result;
314
- };
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
332
  /* render a property value, badging the individual members of an
319
- "enum(...)" (a single member) or "tags(...)" (a member set) value */
320
- const inlineValue = (kind, key, value) => {
321
- const member = members?.get(`${kind}${plainKey(key)}`);
322
- if (member === undefined)
323
- return inline(value);
324
- const items = member === "tags" ? splitItems(value) : [value.trim()];
325
- return safe(items.map((item) => `<span class="value-member">${inline(item)}</span>`).join(" "));
333
+ "enum(...)" (a single member) or "tags(...)" (a member set) value
334
+ and moving its image embeddings behind the value, rendered from the
335
+ embedded image contents (as for a description), not from the markup */
336
+ const inlineValue = (kind, { key, value, embedding }) => {
337
+ const member = members?.get(`${kind}${key}`);
338
+ const text = value
339
+ .replace(embeddingMarkup, (markup, _alt, reference) => embeddingMimeType(reference.trim()) !== undefined ? "" : markup)
340
+ .trim();
341
+ const embeddings = renderEmbeddings(value, embedding ?? [])
342
+ .map((content) => `<div class="embedding">${content}</div>`).join("");
343
+ if (member === undefined || text === "")
344
+ return safe(`${inline(text)}${embeddings}`);
345
+ const items = member === "tags" ? splitItems(text) : [text];
346
+ return safe(items.map((item) => `<span class="value-member">${inline(item)}</span>`).join(" ") + embeddings);
326
347
  };
327
348
  /* expand the inline Markdown of the property values */
328
349
  const inlineProperties = (kind, properties) => properties.map((property) => ({ key: property.key,
329
- value: inlineValue(kind, property.key, property.value) }));
350
+ value: inlineValue(kind, property) }));
330
351
  /* resolve the format configuration of an object */
331
352
  const formatOf = (object) => schemas?.get(object)?.format;
332
353
  /* determine the maximum table columns configured on an object (at
@@ -340,7 +361,7 @@ const effectiveProperties = (object) => {
340
361
  if (schema?.format?.withUnusedProps !== true)
341
362
  return object.properties;
342
363
  const merged = (schema.props ?? []).flatMap((prop) => {
343
- const present = object.properties.filter((property) => plainKey(property.key) === prop.name);
364
+ const present = object.properties.filter((property) => property.key === prop.name);
344
365
  return present.length > 0 ? present : [{ key: prop.name, value: "" }];
345
366
  });
346
367
  return [...merged, ...object.properties.filter((property) => !merged.includes(property))];
@@ -349,32 +370,30 @@ const effectiveProperties = (object) => {
349
370
  "withUnusedProps" injecting the defined but still unused schema
350
371
  properties (in schema order) as additional columns */
351
372
  const tableShape = (childs) => {
352
- let keys = new Array();
353
- for (const child of childs)
354
- for (const property of child.properties)
355
- if (!keys.includes(property.key))
356
- keys.push(property.key);
373
+ let keys = [...new Set(childs.flatMap((child) => child.properties.map((property) => property.key)))];
357
374
  const schema = schemas?.get(childs[0]);
358
375
  if (schema?.format?.withUnusedProps === true) {
359
- const merged = (schema.props ?? []).flatMap((prop) => {
360
- const present = keys.filter((key) => plainKey(key) === prop.name);
361
- return present.length > 0 ? present : [prop.name];
362
- });
363
- keys = [...merged, ...keys.filter((key) => !merged.includes(key))];
376
+ const names = (schema.props ?? []).map((prop) => prop.name);
377
+ keys = [...names, ...keys.filter((key) => !names.includes(key))];
364
378
  }
365
379
  return { keys, desc: childs.some((child) => child.description !== undefined || child.childs.length > 0) };
366
380
  };
381
+ /* provide the childs of an object taking part in the regular document
382
+ flow: a nested title object leaves the flow, as it is rendered as the
383
+ title page instead (a top-level one instead removes its whole artifact) */
384
+ const flowChilds = (object) => object.childs.filter((child) => !isTitleObject(child));
367
385
  /* decide whether the childs of an object collapse into the concise
368
386
  (tabular) rendering: an explicit "format" type wins, "auto" collapses
369
387
  the deepest level only, and inside an already concise rendering
370
388
  context "auto" childs implicitly stay concise, too */
371
389
  const conciseChilds = (object, schemaMap, concise) => {
372
- if (object.childs.length === 0)
390
+ const childs = flowChilds(object);
391
+ if (childs.length === 0)
373
392
  return false;
374
393
  const type = schemaMap?.get(object)?.format?.type ?? "auto";
375
394
  if (type !== "auto")
376
395
  return type === "concise";
377
- return concise || object.childs.every((child) => child.childs.length === 0);
396
+ return concise || childs.every((child) => child.childs.length === 0);
378
397
  };
379
398
  /* group the childs of an object by their kind, preserving order */
380
399
  const groupChilds = (childs) => {
@@ -394,10 +413,11 @@ const groupChilds = (childs) => {
394
413
  ones as regular nested object renderings pressed into the cell) */
395
414
  const renderCell = (child) => {
396
415
  let html = child.description !== undefined ? renderDescription(child.description) : "";
397
- if (child.childs.length > 0)
416
+ const childs = flowChilds(child);
417
+ if (childs.length > 0)
398
418
  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("");
419
+ groupChilds(childs).map((group) => renderTable(group, maxColumnsOf(child))).join("") :
420
+ childs.map((sub) => renderObject(sub, 6, true)).join("");
401
421
  return html;
402
422
  };
403
423
  /* render a single-kind group of childs into one compact table:
@@ -420,8 +440,8 @@ const renderTable = (childs, maxColumns) => {
420
440
  primary: child.primary,
421
441
  name: inline(child.name),
422
442
  values: keys.map((key) => {
423
- const value = child.properties.find((property) => property.key === key)?.value;
424
- return value !== undefined ? inlineValue(child.kind, key, value) : "";
443
+ const property = child.properties.find((property) => property.key === key);
444
+ return property !== undefined ? inlineValue(child.kind, property) : "";
425
445
  }),
426
446
  description: safe(renderCell(child))
427
447
  }))
@@ -435,9 +455,9 @@ const renderTable = (childs, maxColumns) => {
435
455
  width: Math.round(100 / maxColumns),
436
456
  rows: childs.map((child) => {
437
457
  const cells = keys.map((key) => {
438
- const value = child.properties.find((property) => property.key === key)?.value;
458
+ const property = child.properties.find((property) => property.key === key);
439
459
  return { key, desc: false, span: 1,
440
- value: value !== undefined ? inlineValue(child.kind, key, value) : safe("") };
460
+ value: property !== undefined ? inlineValue(child.kind, property) : "" };
441
461
  });
442
462
  if (desc)
443
463
  cells.push({ key: "Description", desc: true, span: 1,
@@ -460,6 +480,7 @@ const renderTable = (childs, maxColumns) => {
460
480
  /* recursively render an object into HTML */
461
481
  const renderObject = (object, level, concise) => {
462
482
  const properties = effectiveProperties(object);
483
+ const diagram = diagrams?.get(object);
463
484
  return render("Object", { Object: {
464
485
  level: Math.min(level, 6),
465
486
  kind: object.kind,
@@ -467,17 +488,21 @@ const renderObject = (object, level, concise) => {
467
488
  paren: object.paren,
468
489
  primary: object.primary,
469
490
  name: inline(object.name),
470
- diagram: diagrams?.has(object) === true ?
471
- safe(`<div class="diagram">${diagrams.get(object) ?? ""}</div>`) : "",
491
+ diagram: diagram !== undefined ? safe(`<div class="diagram">${diagram}</div>`) : "",
472
492
  properties: properties.length > 0 ?
473
493
  safe(render("Properties", { Properties: inlineProperties(object.kind, properties) })) : "",
474
494
  description: object.description !== undefined ?
475
495
  safe(renderDescription(object.description)) : "",
476
496
  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(""))
497
+ safe(groupChilds(flowChilds(object)).map((group) => renderTable(group, maxColumnsOf(object))).join("")) :
498
+ safe(flowChilds(object).map((child) => renderObject(child, level + 1, concise)).join(""))
479
499
  } });
480
500
  };
501
+ /* format a timestamp as its calendar date in local time (the
502
+ frontmatter timestamps are parsed in local time, too, so the UTC
503
+ date of toISOString() could shift the day) */
504
+ const formatDate = (date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-` +
505
+ String(date.getDate()).padStart(2, "0");
481
506
  /* render the title object into a title page */
482
507
  const renderTitlePage = (object, created, modified) => {
483
508
  const prop = (name) => object.properties.find((property) => property.key === name)?.value;
@@ -489,11 +514,13 @@ const renderTitlePage = (object, created, modified) => {
489
514
  "LANG", "CHARSET", "PAPER-SIZE", "THEME-STYLE", "THEME-TONE"].includes(property.key));
490
515
  /* the logo is rendered above the title, from the embedded image content
491
516
  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 */
517
+ Markdown; without a LOGO property the built-in SpecBook logo is used,
518
+ in both its theme variants */
493
519
  const logo = object.properties.find((property) => property.key === "LOGO");
494
520
  const image = logo !== undefined ? renderEmbeddings(logo.value, logo.embedding ?? []) : [];
495
521
  return render("TitlePage", { TitlePage: {
496
- logo: logo === undefined ? safe(`<img src="${fallbackLogo()}" alt="SpecBook"/>`) :
522
+ logo: logo === undefined ?
523
+ safe(renderThemed(embeddingThemes.map((theme) => `<img src="${fallbackLogo(theme)}" alt="SpecBook"/>`))) :
497
524
  (image.length > 0 ? safe(image.join("")) : inline(logo.value)),
498
525
  title: inline(prop("TITLE") ?? object.name),
499
526
  subtitle: inlineProp("SUBTITLE"),
@@ -516,15 +543,24 @@ export const htmlOutline = (specification, config) => {
516
543
  const paths = anchorPaths(buildLinkIndex(specification));
517
544
  const schemaMap = config !== undefined ? collectSchemas(specification, config) : null;
518
545
  const entry = (object) => ({
519
- title: (object.kind !== "" ? `${object.kind}: ` : "") + object.name,
546
+ title: (object.kind !== "" ? `${object.kind}: ` : "") + plainText(object.name),
520
547
  anchor: paths.get(object) ?? object.id,
521
- childs: conciseChilds(object, schemaMap, false) ? [] : object.childs.map(entry)
548
+ childs: conciseChilds(object, schemaMap, false) ? [] : flowChilds(object).map(entry)
522
549
  });
523
550
  return specification.artifacts
524
551
  .filter((artifact) => !artifact.objects.some(isTitleObject))
525
552
  .flatMap((artifact) => artifact.objects)
526
553
  .map(entry);
527
554
  };
555
+ /* determine the object a title page is rendered from: the title object
556
+ has to carry a non-empty "TITLE" property, as without it there is
557
+ nothing to render a title page from (the object stays suppressed in
558
+ the regular flow nevertheless) */
559
+ const titlePageObject = (specification) => {
560
+ const object = titleObject(specification);
561
+ const title = object?.properties.find((property) => property.key === "TITLE");
562
+ return title !== undefined && title.value.trim() !== "" ? object : undefined;
563
+ };
528
564
  /* render the entire specification into a self-contained HTML document,
529
565
  with the build-time pre-assembled stylesheet embedded inline, the
530
566
  artifact timestamps aggregated into min(Created)/max(Modified), and
@@ -538,23 +574,23 @@ export const renderHtml = async (specification, config, tocPages, css) => {
538
574
  canvas and would dwarf the document text at 1:1 */
539
575
  const scale = 0.75;
540
576
  const cached = diagramCache.get(specification);
541
- diagrams = null;
577
+ let rendered = null;
542
578
  if (cached !== undefined && cached.config === config)
543
- diagrams = cached.diagrams;
579
+ rendered = cached.diagrams;
544
580
  else if (config !== undefined) {
545
- diagrams = new Map();
581
+ rendered = new Map();
546
582
  for (const [object, result] of specDiagrams(specification, config)) {
547
583
  if (result.spec === undefined)
548
584
  continue;
549
585
  try {
550
- const svg = await Gradia.render(result.spec, { format: "svg:embedded" });
551
- diagrams.set(object, svg.replace(/(<svg[^>]*) width="([0-9.]+)" height="([0-9.]+)"/, (_, head, w, h) => `${head} width="${Number(w) * scale}" height="${Number(h) * scale}"`));
586
+ const svg = await Gradia.render(result.spec, { format: "svg:embedded", config: result.config });
587
+ rendered.set(object, svg.replace(/(<svg[^>]*) width="([0-9.]+)" height="([0-9.]+)"/, (_, head, w, h) => `${head} width="${Number(w) * scale}" height="${Number(h) * scale}"`));
552
588
  }
553
589
  catch {
554
590
  /* intentionally omitted */
555
591
  }
556
592
  }
557
- diagramCache.set(specification, { config, diagrams });
593
+ diagramCache.set(specification, { config, diagrams: rendered });
558
594
  }
559
595
  /* the document language selects the smart typography quote style */
560
596
  const lang = documentLang(specification);
@@ -566,6 +602,7 @@ export const renderHtml = async (specification, config, tocPages, css) => {
566
602
  anchors = anchorPaths(index);
567
603
  members = config !== undefined ? collectMembers(config, new Map()) : null;
568
604
  schemas = config !== undefined ? collectSchemas(specification, config) : null;
605
+ diagrams = rendered;
569
606
  linker = (text) => expandReferences(text, (reference) => {
570
607
  const target = resolveUnique(index, reference).target;
571
608
  if (target === undefined)
@@ -577,24 +614,33 @@ export const renderHtml = async (specification, config, tocPages, css) => {
577
614
  const created = new Date(Math.min(...specification.artifacts.map((artifact) => artifact.created.getTime())));
578
615
  const modified = new Date(Math.max(...specification.artifacts.map((artifact) => artifact.modified.getTime())));
579
616
  /* 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);
617
+ const title = titlePageObject(specification);
582
618
  const artifacts = specification.artifacts
583
619
  .filter((artifact) => !artifact.objects.some(isTitleObject));
584
- const entries = artifacts.flatMap((artifact) => artifact.objects)
585
- .map((object) => ({ id: anchorOf(object), kind: object.kind, name: inline(object.name),
586
- page: tocPages?.get(anchorOf(object)) }));
587
- return render("Document", { Document: {
588
- title: documentTitle(specification).title,
589
- lang,
590
- theme: documentThemeStyle(specification)?.toLowerCase(),
591
- css: safe(css ?? stylesheet()),
592
- created: created.toISOString(),
593
- modified: modified.toISOString(),
594
- titlepage: title !== undefined ?
595
- safe(renderTitlePage(title, created.toISOString().slice(0, 10), modified.toISOString().slice(0, 10))) : "",
596
- search: title !== undefined ? safe(searchScript()) : "",
597
- toc: entries.length > 0 ? safe(render("Toc", { Toc: { entries } })) : "",
598
- artifacts: safe(artifacts.map((artifact) => renderArtifact(artifact)).join(""))
599
- } });
620
+ try {
621
+ const entries = artifacts.flatMap((artifact) => artifact.objects)
622
+ .map((object) => ({ id: anchorOf(object), kind: object.kind, name: inline(object.name),
623
+ page: tocPages?.get(anchorOf(object)) }));
624
+ return render("Document", { Document: {
625
+ title: documentTitle(specification).title,
626
+ lang,
627
+ theme: documentThemeStyle(specification)?.toLowerCase(),
628
+ css: safe(css ?? stylesheet()),
629
+ titlepage: title !== undefined ?
630
+ safe(renderTitlePage(title, formatDate(created), formatDate(modified))) : "",
631
+ search: title !== undefined ? safe(searchScript()) : "",
632
+ toc: entries.length > 0 ? safe(render("Toc", { Toc: { entries } })) : "",
633
+ artifacts: safe(artifacts.map((artifact) => renderArtifact(artifact)).join(""))
634
+ } });
635
+ }
636
+ finally {
637
+ /* release the per-document state (also on a rendering failure),
638
+ as it would otherwise retain the specification (and its
639
+ embedded images) until the next rendering */
640
+ linker = null;
641
+ anchors = null;
642
+ members = null;
643
+ schemas = null;
644
+ diagrams = null;
645
+ }
600
646
  };
@@ -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) => string;