@rse/specbook 1.3.1 → 1.4.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 (45) hide show
  1. package/README.md +5 -2
  2. package/dst/specbook-api.js +7 -5
  3. package/dst/specbook-cli.js +0 -0
  4. package/dst/specbook-cmd-export.js +2 -2
  5. package/dst/specbook-coverage.js +1 -1
  6. package/dst/specbook-diagram.d.ts +1 -1
  7. package/dst/specbook-diagram.js +9 -3
  8. package/dst/specbook-export-ast.d.ts +1 -1
  9. package/dst/specbook-export-ast.js +21 -4
  10. package/dst/specbook-export-code.d.ts +14 -0
  11. package/dst/specbook-export-code.js +155 -0
  12. package/dst/specbook-export-common.d.ts +4 -2
  13. package/dst/specbook-export-common.js +110 -6
  14. package/dst/specbook-export-diagram.d.ts +15 -0
  15. package/dst/specbook-export-diagram.js +211 -0
  16. package/dst/specbook-export-html-fold.d.ts +20 -0
  17. package/dst/specbook-export-html-fold.js +458 -0
  18. package/dst/specbook-export-html-render.d.ts +51 -0
  19. package/dst/specbook-export-html-render.js +749 -0
  20. package/dst/specbook-export-html-search.js +6 -5
  21. package/dst/specbook-export-html-template.d.ts +21 -0
  22. package/dst/specbook-export-html-template.js +282 -0
  23. package/dst/specbook-export-html-widget.d.ts +12 -0
  24. package/dst/specbook-export-html-widget.js +605 -0
  25. package/dst/specbook-export-html.css +325 -50
  26. package/dst/specbook-export-html.js +62 -1931
  27. package/dst/specbook-export-image.d.ts +2 -0
  28. package/dst/specbook-export-image.js +161 -9
  29. package/dst/specbook-export-md.js +60 -12
  30. package/dst/specbook-format-schema.d.ts +1 -1
  31. package/dst/specbook-format-schema.js +1 -1
  32. package/dst/specbook-format-spec.d.ts +1 -0
  33. package/dst/specbook-format-spec.js +1 -0
  34. package/dst/specbook-format.md +169 -17
  35. package/dst/specbook-format.yaml +224 -23
  36. package/dst/specbook-ignore.js +2 -0
  37. package/dst/specbook-link.js +21 -5
  38. package/dst/specbook-parse-common.d.ts +24 -0
  39. package/dst/specbook-parse-common.js +137 -10
  40. package/dst/specbook-parse-semantic.js +2 -0
  41. package/dst/specbook-parse-syntax.js +81 -20
  42. package/dst/specbook-parse.js +1 -1
  43. package/dst/specbook-theme.d.ts +1 -0
  44. package/dst/specbook-theme.js +2 -0
  45. package/package.json +17 -11
@@ -3,6 +3,7 @@
3
3
  ** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
4
  ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
5
  */
6
+ import { bundledLanguagesInfo } from "shiki/langs";
6
7
  /* the state shared between the syntactic and semantic parsing phases */
7
8
  export class ParseContext {
8
9
  diagnostics = new Array();
@@ -40,21 +41,129 @@ export const embeddingThemes = ["light", "dark"];
40
41
  export const embeddingVariants = (reference) => reference.includes("{theme}") ?
41
42
  embeddingThemes.map((theme) => reference.replace(/\{theme\}/g, theme)) :
42
43
  [reference];
43
- /* the embeddable file types and their MIME types */
44
- const embeddingTypes = {
44
+ /* the MIME type of a PDF document */
45
+ export const pdfType = "application/pdf";
46
+ /* the MIME type of a source code file, carrying its language as the
47
+ "lang" parameter */
48
+ export const codeType = "text/x-code";
49
+ /* the embeddable file types and their MIME types: the images plus the
50
+ diagram sources (Mermaid and D2) and the documents (PDF), which the
51
+ exports render */
52
+ const fileTypes = {
45
53
  svg: "image/svg+xml",
46
54
  png: "image/png",
47
55
  jpg: "image/jpeg",
48
56
  jpeg: "image/jpeg",
49
- webp: "image/webp"
57
+ webp: "image/webp",
58
+ mmd: "text/vnd.mermaid",
59
+ mermaid: "text/vnd.mermaid",
60
+ d2: "text/vnd.d2",
61
+ pdf: pdfType
50
62
  };
51
- /* map a local image embedding reference onto its MIME type
52
- (URLs and other file types are not embeddable) */
53
- export const embeddingMimeType = (reference) => {
63
+ /* the code languages of the code listings, keyed by every id and alias
64
+ of the Shiki languages (except those the file types above take) and
65
+ mapped onto the language id */
66
+ const codeLanguages = new Map(bundledLanguagesInfo
67
+ .flatMap((info) => [info.id, ...info.aliases ?? []].map((name) => [name, info.id]))
68
+ .filter(([name]) => fileTypes[name] === undefined));
69
+ /* map a language name (of a fenced code block) onto the language id
70
+ of a code listing (undefined for no or an unknown language) */
71
+ export const codeLanguage = (name) => codeLanguages.get(name.toLowerCase());
72
+ /* the embeddable file types plus the source code files, whose file
73
+ extensions are the language ids and aliases */
74
+ const embeddingTypes = new Map([
75
+ ...Array.from(codeLanguages, ([name, id]) => [name, `${codeType};lang=${id}`]),
76
+ ...Object.entries(fileTypes)
77
+ ]);
78
+ /* the patterns of the parameters: a 1-based line or page number, a
79
+ range of lines, and the hunks of lines to mark */
80
+ const ordinal = /^[1-9]\d*$/;
81
+ const lineRange = /^[1-9]\d*(?:-[1-9]\d*)?$/;
82
+ const lineHunks = /^[1-9]\d*(?:-[1-9]\d*)?(?:,[1-9]\d*(?:-[1-9]\d*)?)*$/;
83
+ /* turn the hunks of a "mark" parameter into a line number membership test
84
+ (checking the ranges instead of expanding them, as a range is unbounded) */
85
+ export const markedLines = (mark) => {
86
+ const hunks = mark.split(",").filter((hunk) => hunk !== "").map((hunk) => {
87
+ const [first, last = first] = hunk.split("-").map(Number);
88
+ return { first, last };
89
+ });
90
+ return { has: (line) => hunks.some((hunk) => line >= hunk.first && line <= hunk.last) };
91
+ };
92
+ /* split the info string of a fenced code block into its language and its
93
+ whitespace-separated listing parameters ("start" and "mark"), plus an
94
+ error message for an unknown or repeated key or an invalid value */
95
+ const fenceParams = { start: ordinal, mark: lineHunks };
96
+ export const fenceInfo = (info) => {
97
+ const [lang = "", ...pairs] = (info ?? "").trim().split(/\s+/);
98
+ const params = {};
99
+ for (const pair of pairs) {
100
+ const m = pair.match(/^([a-z]+)=(.*)$/s);
101
+ if (m === null || params[m[1]] !== undefined || !(fenceParams[m[1]]?.test(m[2]) ?? false))
102
+ return { lang, params, error: `invalid parameter "${pair}" of fenced code block` };
103
+ params[m[1]] = m[2];
104
+ }
105
+ return { lang, params };
106
+ };
107
+ /* the fragment parameters ("#<key>=<value>&...") each file type accepts,
108
+ with the patterns of their values: the treatment on the dark theme
109
+ of an image and a document, the 1-based page of a document, and the
110
+ excerpt and the marked lines of a source code file */
111
+ const darkAuto = /^(?:auto|invert|none)$/;
112
+ const fragmentParams = {
113
+ "image/svg+xml": { dark: darkAuto },
114
+ "image/png": { dark: darkAuto },
115
+ "image/jpeg": { dark: darkAuto },
116
+ "image/webp": { dark: darkAuto },
117
+ [pdfType]: { page: ordinal, dark: darkAuto },
118
+ [codeType]: { lines: lineRange, mark: lineHunks }
119
+ };
120
+ /* split an embedding reference into its file and its fragment parameters
121
+ (URI-decoded and in the canonical order of its type), or undefined for
122
+ a fragment its type does not accept (an unknown or repeated key, or an
123
+ invalid value) */
124
+ export const embeddingFragment = (reference, type) => {
125
+ const hash = reference.indexOf("#", reference.lastIndexOf("/") + 1);
126
+ if (hash < 0)
127
+ return { file: reference, params: {} };
128
+ const accepted = fragmentParams[type.replace(/;.*$/, "")] ?? {};
129
+ const given = new Map();
130
+ for (const pair of reference.slice(hash + 1).split("&")) {
131
+ const m = pair.match(/^([a-z]+)=(.*)$/s);
132
+ if (m === null || given.has(m[1]))
133
+ return undefined;
134
+ let value = m[2];
135
+ try {
136
+ value = decodeURIComponent(value);
137
+ }
138
+ catch {
139
+ /* an invalid escape sequence is taken literally */
140
+ }
141
+ if (!Object.hasOwn(accepted, m[1]) || !accepted[m[1]].test(value))
142
+ return undefined;
143
+ given.set(m[1], value);
144
+ }
145
+ const params = {};
146
+ for (const key of Object.keys(accepted)) {
147
+ const value = given.get(key);
148
+ if (value !== undefined)
149
+ params[key] = value;
150
+ }
151
+ return { file: reference.slice(0, hash), params };
152
+ };
153
+ /* map a local image embedding reference onto the MIME type of its file
154
+ (URLs and other file types are not embeddable), regardless of any
155
+ fragment */
156
+ export const embeddingFileType = (reference) => {
54
157
  if (/^[a-z][a-z0-9+.-]+:/i.test(reference))
55
158
  return undefined;
56
- const extension = reference.match(/\.([a-z0-9]+)$/i)?.[1].toLowerCase();
57
- return extension !== undefined ? embeddingTypes[extension] : undefined;
159
+ const m = reference.match(/^.*?\.([a-z0-9]+)(?:#.*)?$/is);
160
+ return m !== null ? embeddingTypes.get(m[1].toLowerCase()) : undefined;
161
+ };
162
+ /* map a local image embedding reference onto its MIME type, provided its
163
+ fragment (if any) is one its type accepts */
164
+ export const embeddingMimeType = (reference) => {
165
+ const type = embeddingFileType(reference);
166
+ return type !== undefined && embeddingFragment(reference, type) !== undefined ? type : undefined;
58
167
  };
59
168
  /* the number of embedding entries an image embedding markup occupies,
60
169
  given its inline reference: one per variant of an embeddable one, none
@@ -66,5 +175,23 @@ export const embeddingCount = (reference) => {
66
175
  return embeddingMimeType(trimmed) !== undefined ? embeddingVariants(trimmed).length : 0;
67
176
  };
68
177
  /* the image definition ("[label]: data:...") a reference-style image
69
- embedding refers to: a base64 data: URL of an embeddable image type */
70
- export const embeddingDataRegex = /^data:(image\/(?:svg\+xml|png|jpeg|webp));base64,([A-Za-z0-9+/=]+)$/;
178
+ embedding refers to: a base64 data: URL of an embeddable image type,
179
+ optionally marked with its treatment on the dark theme, or of a source
180
+ code file, carrying its language, start line, and marked lines */
181
+ export const embeddingDataRegex = /^data:(image\/(?:svg\+xml|png|jpeg|webp)|text\/x-code;lang=[^;,]+;start=\d+(?:;mark=[^;,]+)?)(;dark=(?:invert|none))?;base64,([A-Za-z0-9+/=]+)$/;
182
+ /* split an embedded image or document content (an SVG text or a data:
183
+ URL) into its plain content and its explicit treatment on the dark
184
+ theme, which a "#dark=invert" or "#dark=none" fragment (deviating from
185
+ the default "auto") turns into the "dark" data: URL parameter directly
186
+ behind the MIME type (an SVG hence becoming a data: URL, too) */
187
+ export const darkMark = (content) => {
188
+ const m = content.match(/^data:([^;,]+);dark=(invert|none)((?:;[a-z]+=[^;,]*)*);base64,(.*)$/s);
189
+ if (m === null)
190
+ return { content };
191
+ return { dark: m[2], content: m[1] === "image/svg+xml" && m[3] === "" ?
192
+ Buffer.from(m[4], "base64").toString("utf8") : `data:${m[1]}${m[3]};base64,${m[4]}` };
193
+ };
194
+ /* mark a plain embedded image or document content with its explicit
195
+ treatment on the dark theme */
196
+ export const markDark = (content, dark) => content.startsWith("data:") ? content.replace(/^(data:[^;,]+)/, `$1;dark=${dark}`) :
197
+ `data:image/svg+xml;dark=${dark};base64,${Buffer.from(content, "utf8").toString("base64")}`;
@@ -516,6 +516,8 @@ export const validateReferences = (ctx, specification) => {
516
516
  check(object, object.description.description, meta.file, meta.line);
517
517
  if (object.description.rationale !== undefined)
518
518
  check(object, object.description.rationale, meta.file, meta.line);
519
+ if (object.description.elaboration !== undefined)
520
+ check(object, object.description.elaboration, meta.file, meta.line);
519
521
  }
520
522
  object.children.forEach(walk);
521
523
  };
@@ -6,15 +6,22 @@
6
6
  import * as fs from "node:fs";
7
7
  import * as path from "node:path";
8
8
  import { marked } from "marked";
9
- import { becauseRegex, embeddingRegex, embeddingDataRegex, embeddingMimeType, embeddingVariants } from "./specbook-parse-common.js";
10
- /* split a description text into statement and optional rationale */
9
+ import { becauseRegex, embeddingRegex, embeddingDataRegex, embeddingMimeType, embeddingFileType, embeddingVariants, embeddingFragment, markDark, pdfType, codeType, codeLanguage, fenceInfo } from "./specbook-parse-common.js";
10
+ /* split a description text into statement and optional rationale, where
11
+ the rationale ends with its paragraph and any blocks following it
12
+ (further paragraphs, images, diagrams) form the elaboration */
11
13
  const splitDescription = (text) => {
12
14
  const m = text.match(becauseRegex);
13
15
  if (m === null || m.index === undefined)
14
16
  return { description: text };
17
+ const rest = text.slice(m.index + m[0].length);
18
+ const end = rest.search(/\n[ \t]*\n/);
19
+ if (end < 0)
20
+ return { description: text.slice(0, m.index), rationale: rest };
15
21
  return {
16
22
  description: text.slice(0, m.index),
17
- rationale: text.slice(m.index + m[0].length)
23
+ rationale: rest.slice(0, end),
24
+ elaboration: rest.slice(end).trim()
18
25
  };
19
26
  };
20
27
  /* strip the trailing explicit Wiki-style anchor ("{{xxx}}"), the
@@ -100,7 +107,7 @@ const parseFrontmatter = (text) => {
100
107
  return { present: false, created: missing, modified: missing, body: text, offset: 0 };
101
108
  /* grab a single timestamp key with its raw value and source line */
102
109
  const grab = (key) => {
103
- const km = m[1].match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
110
+ const km = m[1].match(new RegExp(`^${key}:[ \\t]*(.+)$`, "m"));
104
111
  if (km === null || km.index === undefined)
105
112
  return missing;
106
113
  const line = 2 + (m[1].slice(0, km.index).match(/\n/g) ?? []).length;
@@ -116,13 +123,15 @@ const parseFrontmatter = (text) => {
116
123
  offset: (m[0].match(/\n/g) ?? []).length
117
124
  };
118
125
  };
119
- /* recursively load the image files embedded via "![alt](file)" into the
120
- description and the property values of an object (SVG as-is, PNG/JPEG/WebP
121
- as base64 data: URLs), resolving the references relative to the source
122
- file and expanding a "{theme}" reference into its theme variants,
123
- which are loaded into consecutive embedding entries (an unreadable
124
- file leaves an empty entry, keeping the positions aligned), while a
125
- reference-style "![alt][label]" takes the image of its definition */
126
+ /* recursively load the files embedded via "![alt](file)" into the
127
+ description and the property values of an object (SVG as-is, the
128
+ PNG/JPEG/WebP images, PDF documents, Mermaid/D2 diagram sources, and
129
+ source code excerpts as base64 data: URLs), resolving the references
130
+ relative to the source file and expanding a "{theme}" reference into
131
+ its theme variants, which are loaded into consecutive embedding
132
+ entries (an unreadable file leaves an empty entry, keeping the
133
+ positions aligned), while a reference-style "![alt][label]" takes
134
+ the image of its definition */
126
135
  const embed = (ctx, object, file, defs) => {
127
136
  const load = (target, text, line) => {
128
137
  for (const m of text.matchAll(embeddingRegex)) {
@@ -137,16 +146,58 @@ const embed = (ctx, object, file, defs) => {
137
146
  }
138
147
  const reference = m[2].trim();
139
148
  const type = embeddingMimeType(reference);
140
- if (type === undefined)
149
+ if (type === undefined) {
150
+ if (embeddingFileType(reference) !== undefined)
151
+ ctx.diagnose(file, line, `invalid fragment of embedding "${reference}"`);
141
152
  continue;
153
+ }
142
154
  for (const variant of embeddingVariants(reference)) {
155
+ /* the page a fragment of a document selects travels as a
156
+ parameter of its data: URL, while an image or document
157
+ gets marked with an explicit treatment on the dark theme
158
+ deviating from its default ("auto"), unless its "{theme}"
159
+ variants already serve the dark theme */
143
160
  target.embedding ??= [];
144
- const asset = path.resolve(path.dirname(file), variant);
161
+ const split = embeddingFragment(variant, type) ?? { file: variant, params: {} };
162
+ const image = type.startsWith("image/") || type === pdfType;
163
+ const asset = path.resolve(path.dirname(file), split.file);
164
+ const params = Object.entries(split.params).filter(([key]) => key !== "dark")
165
+ .map(([key, value]) => `;${key}=${encodeURIComponent(value)}`).join("");
145
166
  ctx.assets.add(asset);
167
+ if (type.startsWith(`${codeType};`)) {
168
+ /* a source code file is embedded as the excerpt of its
169
+ lines, numbered from its first line, with the hunks
170
+ of its marked lines */
171
+ try {
172
+ const lines = fs.readFileSync(asset, "utf8").replace(/\r?\n$/, "").split(/\r?\n/);
173
+ const range = split.params.lines ?? `1-${lines.length}`;
174
+ const [first, last = first] = range.split("-").map(Number);
175
+ if (first > lines.length || last < first) {
176
+ target.embedding.push("");
177
+ ctx.diagnose(file, line, `empty line range "${range}" of embedding "${variant}"`);
178
+ continue;
179
+ }
180
+ const mark = split.params.mark !== undefined ?
181
+ `;mark=${encodeURIComponent(split.params.mark)}` : "";
182
+ const code = lines.slice(first - 1, last).join("\n");
183
+ target.embedding.push(`data:${type};start=${first}${mark};base64,` +
184
+ Buffer.from(code, "utf8").toString("base64"));
185
+ }
186
+ catch (err) {
187
+ target.embedding.push("");
188
+ ctx.diagnose(file, line, `unreadable embedding file "${variant}": ` +
189
+ (err instanceof Error ? err.message : String(err)));
190
+ }
191
+ continue;
192
+ }
146
193
  try {
147
194
  const data = fs.readFileSync(asset);
148
- target.embedding.push(type === "image/svg+xml" ?
149
- data.toString("utf8") : `data:${type};base64,${data.toString("base64")}`);
195
+ const content = type === "image/svg+xml" ?
196
+ data.toString("utf8") : `data:${type}${params};base64,${data.toString("base64")}`;
197
+ const dark = split.params.dark;
198
+ const mark = image && !reference.includes("{theme}")
199
+ && (dark === "invert" || dark === "none") ? dark : undefined;
200
+ target.embedding.push(mark !== undefined ? markDark(content, mark) : content);
150
201
  }
151
202
  catch (err) {
152
203
  target.embedding.push("");
@@ -157,8 +208,17 @@ const embed = (ctx, object, file, defs) => {
157
208
  }
158
209
  };
159
210
  const line = ctx.metaOf(object).line;
160
- if (object.description !== undefined)
211
+ if (object.description !== undefined) {
161
212
  load(object.description, object.description.description, line);
213
+ load(object.description, object.description.elaboration ?? "", line);
214
+ /* validate the listing parameters of the fenced code blocks */
215
+ for (const text of [object.description.description, object.description.elaboration ?? ""])
216
+ marked.walkTokens(marked.lexer(text), (token) => {
217
+ const info = token.type === "code" ? fenceInfo(token.lang) : undefined;
218
+ if (info?.error !== undefined && codeLanguage(info.lang) !== undefined)
219
+ ctx.diagnose(file, line, info.error);
220
+ });
221
+ }
162
222
  for (const property of object.properties)
163
223
  load(property, property.value, ctx.propMeta.get(property)?.line ?? line);
164
224
  for (const child of object.children)
@@ -415,20 +475,21 @@ export const parseFile = (ctx, source) => {
415
475
  }
416
476
  else {
417
477
  /* an image definition of the Markdown renderer is the one
418
- supported link definition (its SVG kept as-is again) */
478
+ supported link definition (its SVG kept as-is again,
479
+ unless marked as inverted on the dark theme) */
419
480
  const def = token.type === "def" ? token : undefined;
420
481
  const m = def?.href.match(embeddingDataRegex) ?? null;
421
482
  const unsupported = unsupportedTokens[token.type];
422
483
  if (def !== undefined && m !== null)
423
- defs.set(def.tag, m[1] === "image/svg+xml" ?
424
- Buffer.from(m[2], "base64").toString("utf8") : def.href);
484
+ defs.set(def.tag, m[1] === "image/svg+xml" && m[2] === undefined ?
485
+ Buffer.from(m[3], "base64").toString("utf8") : def.href);
425
486
  else if (unsupported !== undefined)
426
487
  ctx.diagnose(source.file, line, `unsupported ${unsupported} content ignored`);
427
488
  }
428
489
  line += (token.raw.match(/\n/g) ?? []).length;
429
490
  }
430
491
  flush();
431
- /* load the embedded image files of all fully parsed objects */
492
+ /* load the embedded files of all fully parsed objects */
432
493
  for (const artifact of state.artifacts)
433
494
  for (const object of artifact.objects)
434
495
  embed(ctx, object, source.file, defs);
@@ -52,7 +52,7 @@ export const specStatistics = (specification) => {
52
52
  objects++;
53
53
  const texts = [object.name, ...object.properties.map((p) => p.value)];
54
54
  if (object.description !== undefined)
55
- texts.push(object.description.description, object.description.rationale ?? "");
55
+ texts.push(object.description.description, object.description.rationale ?? "", object.description.elaboration ?? "");
56
56
  for (const text of texts)
57
57
  links += (plainText(text).match(referenceRegex) ?? []).length;
58
58
  object.children.forEach(walk);
@@ -4,6 +4,7 @@ export type ThemeColors = {
4
4
  accent: string[];
5
5
  signal: string[];
6
6
  };
7
+ export declare const themeToneDefault = "#336699";
7
8
  export declare const themeColors: (tone: string) => ThemeColors;
8
9
  export declare const themeStylesheet: (colors: ThemeColors) => string;
9
10
  export type ThemeMapping = {
@@ -4,6 +4,8 @@
4
4
  ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
5
  */
6
6
  import { parse, generate } from "@rse/mrcs";
7
+ /* the theme color tone of a document carrying no THEME-TONE */
8
+ export const themeToneDefault = "#336699";
7
9
  /* generate the theme color spreads from the theme color tone(s):
8
10
  a grey base spread, an accent spread derived from the first tone,
9
11
  and a signal spread derived from the second tone (if given) or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rse/specbook",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "publishConfig": { "access": "public" },
5
5
  "description": "Markdown-based Specification Format (API, CLI and MCP service)",
6
6
  "license": "Apache-2.0",
@@ -23,49 +23,55 @@
23
23
  "json5": "2.2.3",
24
24
  "pdf-lib": "1.17.1",
25
25
  "subset-font": "2.9.0",
26
- "sharp": "0.35.4",
26
+ "sharp": "0.35.5",
27
27
  "svgo": "4.1.0",
28
28
  "@rse/mrcs": "0.9.2",
29
29
  "@rse/gradia": "1.4.2",
30
+ "beautiful-mermaid": "1.1.3",
31
+ "@terrastruct/d2": "0.1.33",
32
+ "shiki": "4.5.0",
33
+ "pdfjs-dist": "6.4.299",
34
+ "@napi-rs/canvas": "1.0.10",
30
35
  "@toon-format/toon": "4.1.1",
31
36
  "valibot": "1.5.0",
32
37
  "nunjucks": "3.2.4",
33
38
  "@rse/nunjucks-addons": "2.2.0",
34
39
  "textframe": "1.2.1",
35
- "@swc/html": "1.16.2",
40
+ "@swc/html": "1.16.13",
36
41
  "tokenizr": "2.2.1",
37
42
  "playwright": "1.63.0",
38
43
  "source-code-error": "1.2.0",
39
44
  "commander": "15.0.0",
40
- "chalk": "6.0.0",
45
+ "chalk": "6.0.1",
41
46
  "chokidar5": "npm:chokidar@5.0.0",
42
47
  "glob": "13.0.6",
43
48
  "picomatch": "4.0.7",
44
49
  "es-toolkit": "1.52.0",
45
50
  "fastify": "5.12.5",
46
- "@fastify/websocket": "11.3.1",
47
- "@modelcontextprotocol/sdk": "1.30.0",
51
+ "@fastify/websocket": "11.3.3",
52
+ "@modelcontextprotocol/sdk": "1.32.0",
48
53
  "zod": "4.6.5"
49
54
  },
50
55
  "devDependencies": {
51
56
  "chokidar-cli": "3.0.0",
52
57
  "eslint": "10.9.0",
53
58
  "@eslint/js": "10.0.1",
54
- "typescript-eslint": "8.70.1",
59
+ "typescript-eslint": "8.71.0",
55
60
  "eslint-plugin-promise": "7.3.0",
56
61
  "neostandard": "0.14.0-next.1",
57
- "globals": "17.12.0",
62
+ "globals": "17.13.0",
63
+ "eslint-plugin-yml": "3.8.1",
58
64
  "markdownlint": "0.41.1",
59
65
  "markdownlint-cli2": "0.23.3",
60
- "@types/node": "26.6.2",
66
+ "@types/node": "26.6.4",
61
67
  "@types/picomatch": "4.0.3",
62
68
  "@types/subset-font": "2.3.0",
63
69
  "@types/nunjucks": "3.2.6",
64
- "@types/ws": "8.18.1",
70
+ "@types/ws": "8.18.2",
65
71
  "typescript": "6.0.3",
66
72
  "tsx": "4.23.15",
67
73
  "source-sans": "3.52.0",
68
- "source-code-pro": "2.42.0",
74
+ "@fontsource/dejavu-mono": "5.3.0",
69
75
  "@fontsource/noto-sans-symbols": "5.3.0",
70
76
  "postcss": "8.5.28",
71
77
  "postcss-cli": "12.0.0",