mulmocast 2.6.16 → 2.6.17

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 (53) hide show
  1. package/lib/actions/viewer.js +1 -1
  2. package/lib/cli/bin.js +0 -0
  3. package/lib/mcp/server.js +0 -0
  4. package/lib/types/schema.d.ts +457 -457
  5. package/lib/types/schema.js +2 -2
  6. package/lib/utils/context.d.ts +728 -728
  7. package/lib/utils/image_plugins/slide.d.ts +1 -1
  8. package/lib/utils/image_plugins/slide.js +1 -1
  9. package/package.json +7 -6
  10. package/lib/data/styles.d.ts +0 -255
  11. package/lib/data/styles.js +0 -284
  12. package/lib/slide/blocks.d.ts +0 -13
  13. package/lib/slide/blocks.js +0 -251
  14. package/lib/slide/index.d.ts +0 -6
  15. package/lib/slide/index.js +0 -7
  16. package/lib/slide/layouts/big_quote.d.ts +0 -2
  17. package/lib/slide/layouts/big_quote.js +0 -19
  18. package/lib/slide/layouts/columns.d.ts +0 -2
  19. package/lib/slide/layouts/columns.js +0 -53
  20. package/lib/slide/layouts/comparison.d.ts +0 -2
  21. package/lib/slide/layouts/comparison.js +0 -25
  22. package/lib/slide/layouts/funnel.d.ts +0 -2
  23. package/lib/slide/layouts/funnel.js +0 -25
  24. package/lib/slide/layouts/grid.d.ts +0 -2
  25. package/lib/slide/layouts/grid.js +0 -38
  26. package/lib/slide/layouts/index.d.ts +0 -3
  27. package/lib/slide/layouts/index.js +0 -46
  28. package/lib/slide/layouts/matrix.d.ts +0 -2
  29. package/lib/slide/layouts/matrix.js +0 -53
  30. package/lib/slide/layouts/split.d.ts +0 -2
  31. package/lib/slide/layouts/split.js +0 -51
  32. package/lib/slide/layouts/stats.d.ts +0 -2
  33. package/lib/slide/layouts/stats.js +0 -23
  34. package/lib/slide/layouts/table.d.ts +0 -2
  35. package/lib/slide/layouts/table.js +0 -10
  36. package/lib/slide/layouts/timeline.d.ts +0 -2
  37. package/lib/slide/layouts/timeline.js +0 -27
  38. package/lib/slide/layouts/title.d.ts +0 -2
  39. package/lib/slide/layouts/title.js +0 -19
  40. package/lib/slide/layouts/waterfall.d.ts +0 -2
  41. package/lib/slide/layouts/waterfall.js +0 -63
  42. package/lib/slide/render.d.ts +0 -17
  43. package/lib/slide/render.js +0 -101
  44. package/lib/slide/schema.d.ts +0 -9309
  45. package/lib/slide/schema.js +0 -434
  46. package/lib/slide/utils.d.ts +0 -76
  47. package/lib/slide/utils.js +0 -243
  48. package/lib/types/slide.d.ts +0 -9309
  49. package/lib/types/slide.js +0 -434
  50. package/lib/utils/browser_pool.d.ts +0 -5
  51. package/lib/utils/browser_pool.js +0 -39
  52. package/lib/utils/markdown.d.ts +0 -3
  53. package/lib/utils/markdown.js +0 -49
@@ -1,434 +0,0 @@
1
- import { z } from "zod";
2
- // ═══════════════════════════════════════════════════════════
3
- // Foundation: Colors & Typography
4
- // ═══════════════════════════════════════════════════════════
5
- /** 6-digit hex without '#' prefix, e.g. "3B82F6" */
6
- const hexColorSchema = z.string().regex(/^[0-9A-Fa-f]{6}$/);
7
- /** Semantic accent colors usable on cards, badges, borders, etc. */
8
- export const accentColorKeySchema = z.enum(["primary", "accent", "success", "warning", "danger", "info", "highlight"]);
9
- export const slideThemeColorsSchema = z.object({
10
- bg: hexColorSchema,
11
- bgCard: hexColorSchema,
12
- bgCardAlt: hexColorSchema,
13
- text: hexColorSchema,
14
- textMuted: hexColorSchema,
15
- textDim: hexColorSchema,
16
- primary: hexColorSchema,
17
- accent: hexColorSchema,
18
- success: hexColorSchema,
19
- warning: hexColorSchema,
20
- danger: hexColorSchema,
21
- info: hexColorSchema,
22
- highlight: hexColorSchema,
23
- });
24
- export const slideThemeFontsSchema = z.object({
25
- title: z.string(),
26
- body: z.string(),
27
- mono: z.string(),
28
- });
29
- export const slideThemeSchema = z.object({
30
- colors: slideThemeColorsSchema,
31
- fonts: slideThemeFontsSchema,
32
- });
33
- // ═══════════════════════════════════════════════════════════
34
- // Content Blocks — the atoms of slide content
35
- // ═══════════════════════════════════════════════════════════
36
- export const textBlockSchema = z.object({
37
- type: z.literal("text"),
38
- value: z.string(),
39
- align: z.enum(["left", "center", "right"]).optional(),
40
- bold: z.boolean().optional(),
41
- dim: z.boolean().optional(),
42
- fontSize: z.number().optional(),
43
- color: accentColorKeySchema.optional(),
44
- });
45
- /** Sub-bullet item: plain string or object with text */
46
- const subBulletItemSchema = z.union([z.string(), z.object({ text: z.string() })]);
47
- /** Bullet item: plain string or object with text and optional sub-items (2 levels max) */
48
- export const bulletItemSchema = z.union([
49
- z.string(),
50
- z.object({
51
- text: z.string(),
52
- items: z.array(subBulletItemSchema).optional(),
53
- }),
54
- ]);
55
- export const bulletsBlockSchema = z.object({
56
- type: z.literal("bullets"),
57
- items: z.array(bulletItemSchema),
58
- ordered: z.boolean().optional(),
59
- icon: z.string().optional(),
60
- });
61
- export const codeBlockSchema = z.object({
62
- type: z.literal("code"),
63
- code: z.string(),
64
- language: z.string().optional(),
65
- });
66
- export const calloutBlockSchema = z.object({
67
- type: z.literal("callout"),
68
- text: z.string(),
69
- label: z.string().optional(),
70
- color: accentColorKeySchema.optional(),
71
- style: z.enum(["quote", "info", "warning"]).optional(),
72
- });
73
- export const metricBlockSchema = z.object({
74
- type: z.literal("metric"),
75
- value: z.string(),
76
- label: z.string(),
77
- color: accentColorKeySchema.optional(),
78
- change: z.string().optional(),
79
- });
80
- export const dividerBlockSchema = z.object({
81
- type: z.literal("divider"),
82
- color: accentColorKeySchema.optional(),
83
- });
84
- export const imageBlockSchema = z.object({
85
- type: z.literal("image"),
86
- src: z.string(),
87
- alt: z.string().optional(),
88
- fit: z.enum(["contain", "cover"]).optional(),
89
- });
90
- export const imageRefBlockSchema = z.object({
91
- type: z.literal("imageRef"),
92
- ref: z.string(),
93
- alt: z.string().optional(),
94
- fit: z.enum(["contain", "cover"]).optional(),
95
- });
96
- export const chartBlockSchema = z.object({
97
- type: z.literal("chart"),
98
- chartData: z.record(z.string(), z.unknown()),
99
- title: z.string().optional(),
100
- });
101
- export const mermaidBlockSchema = z.object({
102
- type: z.literal("mermaid"),
103
- code: z.string(),
104
- title: z.string().optional(),
105
- });
106
- export const tableCellValueSchema = z.union([
107
- z.string(),
108
- z.object({
109
- text: z.string(),
110
- color: accentColorKeySchema.optional(),
111
- bold: z.boolean().optional(),
112
- badge: z.boolean().optional(),
113
- }),
114
- ]);
115
- export const tableBlockSchema = z.object({
116
- type: z.literal("table"),
117
- title: z.string().optional(),
118
- headers: z.array(z.string()).optional(),
119
- rows: z.array(z.array(tableCellValueSchema)),
120
- rowHeaders: z.boolean().optional(),
121
- striped: z.boolean().optional(),
122
- });
123
- /** Block schemas shared between contentBlockSchema and nonSectionContentBlockSchema */
124
- const baseBlockSchemas = [
125
- textBlockSchema,
126
- bulletsBlockSchema,
127
- codeBlockSchema,
128
- calloutBlockSchema,
129
- metricBlockSchema,
130
- dividerBlockSchema,
131
- imageBlockSchema,
132
- imageRefBlockSchema,
133
- chartBlockSchema,
134
- mermaidBlockSchema,
135
- tableBlockSchema,
136
- ];
137
- /** All content block types except section (used inside section to prevent recursion) */
138
- const nonSectionContentBlockSchema = z.discriminatedUnion("type", [...baseBlockSchemas]);
139
- export const sectionBlockSchema = z.object({
140
- type: z.literal("section"),
141
- label: z.string(),
142
- color: accentColorKeySchema.optional(),
143
- content: z.array(nonSectionContentBlockSchema).optional(),
144
- text: z.string().optional(),
145
- sidebar: z.boolean().optional(),
146
- });
147
- export const contentBlockSchema = z.discriminatedUnion("type", [...baseBlockSchemas, sectionBlockSchema]);
148
- // ═══════════════════════════════════════════════════════════
149
- // Shared Components
150
- // ═══════════════════════════════════════════════════════════
151
- /** Bottom-of-slide callout bar */
152
- export const calloutBarSchema = z.object({
153
- text: z.string(),
154
- label: z.string().optional(),
155
- color: accentColorKeySchema.optional(),
156
- align: z.enum(["left", "center"]).optional(),
157
- leftBar: z.boolean().optional(),
158
- });
159
- /** Reusable card definition — used by columns, grid */
160
- export const cardSchema = z.object({
161
- title: z.string(),
162
- accentColor: accentColorKeySchema.optional(),
163
- content: z.array(contentBlockSchema).optional(),
164
- footer: z.string().optional(),
165
- label: z.string().optional(),
166
- num: z.number().optional(),
167
- icon: z.string().optional(),
168
- });
169
- // ═══════════════════════════════════════════════════════════
170
- // Slide-level styling — orthogonal to layout
171
- // ═══════════════════════════════════════════════════════════
172
- export const slideStyleSchema = z.object({
173
- bgColor: z.string().optional(),
174
- decorations: z.boolean().optional(),
175
- bgOpacity: z.number().optional(),
176
- footer: z.string().optional(),
177
- });
178
- /** Common slide properties shared across all layouts */
179
- const slideBaseFields = {
180
- accentColor: accentColorKeySchema.optional(),
181
- style: slideStyleSchema.optional(),
182
- };
183
- // ═══════════════════════════════════════════════════════════
184
- // Layouts
185
- // ═══════════════════════════════════════════════════════════
186
- // ─── title ───
187
- export const titleSlideSchema = z.object({
188
- layout: z.literal("title"),
189
- ...slideBaseFields,
190
- title: z.string(),
191
- subtitle: z.string().optional(),
192
- author: z.string().optional(),
193
- note: z.string().optional(),
194
- });
195
- // ─── columns ───
196
- export const columnsSlideSchema = z.object({
197
- layout: z.literal("columns"),
198
- ...slideBaseFields,
199
- title: z.string(),
200
- stepLabel: z.string().optional(),
201
- subtitle: z.string().optional(),
202
- columns: z.array(cardSchema),
203
- showArrows: z.boolean().optional(),
204
- callout: calloutBarSchema.optional(),
205
- bottomText: z.string().optional(),
206
- });
207
- // ─── comparison ───
208
- export const comparisonPanelSchema = z.object({
209
- title: z.string(),
210
- accentColor: accentColorKeySchema.optional(),
211
- content: z.array(contentBlockSchema).optional(),
212
- footer: z.string().optional(),
213
- });
214
- export const comparisonSlideSchema = z.object({
215
- layout: z.literal("comparison"),
216
- ...slideBaseFields,
217
- title: z.string(),
218
- stepLabel: z.string().optional(),
219
- subtitle: z.string().optional(),
220
- left: comparisonPanelSchema,
221
- right: comparisonPanelSchema,
222
- callout: calloutBarSchema.optional(),
223
- });
224
- // ─── grid ───
225
- export const gridItemSchema = z.object({
226
- title: z.string(),
227
- description: z.string().optional(),
228
- accentColor: accentColorKeySchema.optional(),
229
- num: z.number().optional(),
230
- icon: z.string().optional(),
231
- content: z.array(contentBlockSchema).optional(),
232
- });
233
- export const gridSlideSchema = z.object({
234
- layout: z.literal("grid"),
235
- ...slideBaseFields,
236
- title: z.string(),
237
- subtitle: z.string().optional(),
238
- gridColumns: z.number().optional(),
239
- items: z.array(gridItemSchema),
240
- footer: z.string().optional(),
241
- });
242
- // ─── bigQuote ───
243
- export const bigQuoteSlideSchema = z.object({
244
- layout: z.literal("bigQuote"),
245
- ...slideBaseFields,
246
- quote: z.string(),
247
- author: z.string().optional(),
248
- role: z.string().optional(),
249
- });
250
- // ─── stats ───
251
- export const statItemSchema = z.object({
252
- value: z.string(),
253
- label: z.string(),
254
- color: accentColorKeySchema.optional(),
255
- change: z.string().optional(),
256
- });
257
- export const statsSlideSchema = z.object({
258
- layout: z.literal("stats"),
259
- ...slideBaseFields,
260
- title: z.string(),
261
- stepLabel: z.string().optional(),
262
- subtitle: z.string().optional(),
263
- stats: z.array(statItemSchema),
264
- callout: calloutBarSchema.optional(),
265
- });
266
- // ─── timeline ───
267
- export const timelineItemSchema = z.object({
268
- date: z.string(),
269
- title: z.string(),
270
- description: z.string().optional(),
271
- color: accentColorKeySchema.optional(),
272
- done: z.boolean().optional(),
273
- });
274
- export const timelineSlideSchema = z.object({
275
- layout: z.literal("timeline"),
276
- ...slideBaseFields,
277
- title: z.string(),
278
- stepLabel: z.string().optional(),
279
- subtitle: z.string().optional(),
280
- items: z.array(timelineItemSchema),
281
- });
282
- // ─── split ───
283
- export const splitPanelSchema = z.object({
284
- title: z.string().optional(),
285
- subtitle: z.string().optional(),
286
- label: z.string().optional(),
287
- labelBadge: z.boolean().optional(),
288
- accentColor: accentColorKeySchema.optional(),
289
- content: z.array(contentBlockSchema).optional(),
290
- dark: z.boolean().optional(),
291
- ratio: z.number().optional(),
292
- valign: z.enum(["top", "center", "bottom"]).optional(),
293
- });
294
- export const splitSlideSchema = z.object({
295
- layout: z.literal("split"),
296
- ...slideBaseFields,
297
- left: splitPanelSchema.optional(),
298
- right: splitPanelSchema.optional(),
299
- });
300
- // ─── matrix ───
301
- export const matrixCellSchema = z.object({
302
- label: z.string(),
303
- items: z.array(z.string()).optional(),
304
- content: z.array(contentBlockSchema).optional(),
305
- accentColor: accentColorKeySchema.optional(),
306
- });
307
- export const matrixSlideSchema = z.object({
308
- layout: z.literal("matrix"),
309
- ...slideBaseFields,
310
- title: z.string(),
311
- stepLabel: z.string().optional(),
312
- subtitle: z.string().optional(),
313
- rows: z.number().optional(),
314
- cols: z.number().optional(),
315
- xAxis: z
316
- .object({
317
- low: z.string().optional(),
318
- high: z.string().optional(),
319
- label: z.string().optional(),
320
- })
321
- .optional(),
322
- yAxis: z
323
- .object({
324
- low: z.string().optional(),
325
- high: z.string().optional(),
326
- label: z.string().optional(),
327
- })
328
- .optional(),
329
- cells: z.array(matrixCellSchema),
330
- });
331
- // ─── table ───
332
- export const tableSlideSchema = z.object({
333
- layout: z.literal("table"),
334
- ...slideBaseFields,
335
- title: z.string(),
336
- stepLabel: z.string().optional(),
337
- subtitle: z.string().optional(),
338
- headers: z.array(z.string()),
339
- rows: z.array(z.array(tableCellValueSchema)),
340
- rowHeaders: z.boolean().optional(),
341
- striped: z.boolean().optional(),
342
- callout: calloutBarSchema.optional(),
343
- });
344
- // ─── waterfall ───
345
- export const waterfallItemSchema = z.object({
346
- label: z.string(),
347
- value: z.number(),
348
- isTotal: z.boolean().optional(),
349
- color: accentColorKeySchema.optional(),
350
- });
351
- export const waterfallSlideSchema = z.object({
352
- layout: z.literal("waterfall"),
353
- ...slideBaseFields,
354
- title: z.string(),
355
- stepLabel: z.string().optional(),
356
- subtitle: z.string().optional(),
357
- items: z.array(waterfallItemSchema),
358
- unit: z.string().optional(),
359
- callout: calloutBarSchema.optional(),
360
- });
361
- // ─── funnel ───
362
- export const funnelStageSchema = z.object({
363
- label: z.string(),
364
- value: z.string().optional(),
365
- description: z.string().optional(),
366
- color: accentColorKeySchema.optional(),
367
- });
368
- export const funnelSlideSchema = z.object({
369
- layout: z.literal("funnel"),
370
- ...slideBaseFields,
371
- title: z.string(),
372
- stepLabel: z.string().optional(),
373
- subtitle: z.string().optional(),
374
- stages: z.array(funnelStageSchema),
375
- callout: calloutBarSchema.optional(),
376
- });
377
- // ═══════════════════════════════════════════════════════════
378
- // Branding — logo & background image overlay
379
- // ═══════════════════════════════════════════════════════════
380
- /**
381
- * Media source for branding assets (self-contained definition to avoid
382
- * circular dependency with src/types/schema.ts).
383
- */
384
- const slideMediaSourceSchema = z.discriminatedUnion("kind", [
385
- z.object({ kind: z.literal("url"), url: z.url() }).strict(),
386
- z.object({ kind: z.literal("base64"), data: z.string().min(1) }).strict(),
387
- z.object({ kind: z.literal("path"), path: z.string().min(1) }).strict(),
388
- ]);
389
- const slideBackgroundImageSourceSchema = z.object({
390
- source: slideMediaSourceSchema,
391
- size: z.enum(["cover", "contain", "fill", "auto"]).optional(),
392
- opacity: z.number().min(0).max(1).optional(),
393
- bgOpacity: z.number().min(0).max(1).optional(),
394
- });
395
- export const slideBrandingLogoSchema = z
396
- .object({
397
- source: slideMediaSourceSchema,
398
- position: z.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).default("top-right"),
399
- width: z.number().positive().default(120),
400
- })
401
- .strict();
402
- export const slideBrandingSchema = z
403
- .object({
404
- logo: slideBrandingLogoSchema.optional(),
405
- backgroundImage: slideBackgroundImageSourceSchema.optional(),
406
- })
407
- .strict();
408
- // ═══════════════════════════════════════════════════════════
409
- // Slide Union & Media Schema
410
- // ═══════════════════════════════════════════════════════════
411
- export const slideLayoutSchema = z.discriminatedUnion("layout", [
412
- titleSlideSchema,
413
- columnsSlideSchema,
414
- comparisonSlideSchema,
415
- gridSlideSchema,
416
- bigQuoteSlideSchema,
417
- statsSlideSchema,
418
- timelineSlideSchema,
419
- splitSlideSchema,
420
- matrixSlideSchema,
421
- tableSlideSchema,
422
- funnelSlideSchema,
423
- waterfallSlideSchema,
424
- ]);
425
- /** Media schema registered in mulmoImageAssetSchema */
426
- export const mulmoSlideMediaSchema = z
427
- .object({
428
- type: z.literal("slide"),
429
- theme: slideThemeSchema.optional(),
430
- slide: slideLayoutSchema,
431
- reference: z.string().optional(),
432
- branding: slideBrandingSchema.nullable().optional(),
433
- })
434
- .strict();
@@ -1,5 +0,0 @@
1
- import puppeteer from "puppeteer";
2
- /** Get a shared browser instance. Launches one if none exists. */
3
- export declare const getBrowser: () => Promise<puppeteer.Browser>;
4
- /** Close the shared browser instance. Call at the end of processing. */
5
- export declare const closeBrowser: () => Promise<void>;
@@ -1,39 +0,0 @@
1
- import puppeteer from "puppeteer";
2
- const isCI = process.env.CI === "true";
3
- const launchArgs = isCI ? ["--no-sandbox", "--allow-file-access-from-files"] : ["--allow-file-access-from-files"];
4
- let browserInstance = null;
5
- let launchPromise = null;
6
- const launchBrowser = async () => {
7
- const browser = await puppeteer.launch({ args: launchArgs });
8
- browser.on("disconnected", () => {
9
- browserInstance = null;
10
- launchPromise = null;
11
- });
12
- return browser;
13
- };
14
- /** Get a shared browser instance. Launches one if none exists. */
15
- export const getBrowser = async () => {
16
- if (browserInstance?.connected) {
17
- return browserInstance;
18
- }
19
- // Prevent multiple concurrent launches
20
- if (!launchPromise) {
21
- launchPromise = launchBrowser().then((browser) => {
22
- browserInstance = browser;
23
- launchPromise = null;
24
- return browser;
25
- });
26
- }
27
- return launchPromise;
28
- };
29
- /** Close the shared browser instance. Call at the end of processing. */
30
- export const closeBrowser = async () => {
31
- if (launchPromise) {
32
- await launchPromise;
33
- }
34
- if (browserInstance?.connected) {
35
- await browserInstance.close();
36
- }
37
- browserInstance = null;
38
- launchPromise = null;
39
- };
@@ -1,3 +0,0 @@
1
- export declare const renderHTMLToImage: (html: string, outputPath: string, width: number, height: number, isMermaid?: boolean, omitBackground?: boolean) => Promise<void>;
2
- export declare const renderMarkdownToImage: (markdown: string, style: string, outputPath: string, width: number, height: number) => Promise<void>;
3
- export declare const interpolate: (template: string, data: Record<string, string>) => string;
@@ -1,49 +0,0 @@
1
- import { marked } from "marked";
2
- import puppeteer from "puppeteer";
3
- const isCI = process.env.CI === "true";
4
- export const renderHTMLToImage = async (html, outputPath, width, height, isMermaid = false, omitBackground = false) => {
5
- // Use Puppeteer to render HTML to an image
6
- const browser = await puppeteer.launch({
7
- args: isCI ? ["--no-sandbox"] : [],
8
- });
9
- const page = await browser.newPage();
10
- // Set the page content to the HTML generated from the Markdown
11
- await page.setContent(html);
12
- // Adjust page settings if needed (like width, height, etc.)
13
- await page.setViewport({ width, height });
14
- await page.addStyleTag({ content: "html,body{margin:0;padding:0;overflow:hidden}" });
15
- if (isMermaid) {
16
- await page.waitForFunction(() => {
17
- const el = document.querySelector(".mermaid");
18
- return el && el.dataset.ready === "true";
19
- }, { timeout: 20000 });
20
- }
21
- // Wait for Chart.js to finish rendering if this is a chart
22
- if (html.includes("data-chart-ready")) {
23
- await page.waitForFunction(() => {
24
- const canvas = document.querySelector("canvas[data-chart-ready='true']");
25
- return !!canvas;
26
- }, { timeout: 20000 });
27
- }
28
- // Measure the size of the page and scale the page to the width and height
29
- await page.evaluate(({ vw, vh }) => {
30
- const de = document.documentElement;
31
- const sw = Math.max(de.scrollWidth, document.body.scrollWidth || 0);
32
- const sh = Math.max(de.scrollHeight, document.body.scrollHeight || 0);
33
- const scale = Math.min(vw / (sw || vw), vh / (sh || vh), 1); // <=1 で縮小のみ
34
- de.style.overflow = "hidden";
35
- document.body.style.zoom = String(scale);
36
- }, { vw: width, vh: height });
37
- // Step 3: Capture screenshot of the page (which contains the Markdown-rendered HTML)
38
- await page.screenshot({ path: outputPath, omitBackground });
39
- await browser.close();
40
- };
41
- export const renderMarkdownToImage = async (markdown, style, outputPath, width, height) => {
42
- const header = `<head><style>${style}</style></head>`;
43
- const body = await marked(markdown);
44
- const html = `<html>${header}<body>${body}</body></html>`;
45
- await renderHTMLToImage(html, outputPath, width, height);
46
- };
47
- export const interpolate = (template, data) => {
48
- return template.replace(/\$\{(.*?)\}/g, (_, key) => data[key.trim()] ?? "");
49
- };