deckrun 1.3.0 → 1.6.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.
package/dist/preview.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { RESET_CSS, SLIDE_CSS, DECOR_CSS } from "./generate.js";
2
2
  import { findFont, FONT_IDS, fontOverrideCss, SIZE_IDS, DEFAULT_SIZE, DEFAULT_THEME, decorMapJson, decorOf, googleFontsHref, hljsHref, hljsMapJson, resolveSizeName, sizeSwitchableCss, themeSwitchableCss, } from "./themes.js";
3
+ import { DEFAULT_TEMPLATE, DEFAULT_TRANSITION, resolveTemplateName, resolveTransitionName, TEMPLATE_CSS, TRANSITION_CSS, } from "./presentation-options.js";
4
+ import { RICH_CONTENT_CSS, RICH_CONTENT_RUNTIME, richContentHead, } from "./rich-content.js";
5
+ import { FRAGMENT_CSS, FRAGMENT_RUNTIME } from "./fragments.js";
3
6
  /** Virtual viewport the preview renders at, so `vw` sizing matches a projector. */
4
7
  export const PREVIEW_WIDTH = 1600;
5
8
  export const PREVIEW_HEIGHT = 900;
@@ -8,21 +11,24 @@ export const PREVIEW_HEIGHT = 900;
8
11
  * own stylesheet, so what the editor shows is what `deckrun file.md` renders.
9
12
  * Slides arrive over postMessage; nothing is fetched or parsed in here.
10
13
  */
11
- export function generatePreviewHtml(initialTheme = DEFAULT_THEME, initialSize = DEFAULT_SIZE, fonts = {}) {
14
+ export function generatePreviewHtml(initialTheme = DEFAULT_THEME, initialSize = DEFAULT_SIZE, fonts = {}, initialTemplate = DEFAULT_TEMPLATE, initialTransition = DEFAULT_TRANSITION) {
12
15
  const size = resolveSizeName(initialSize);
16
+ const template = resolveTemplateName(initialTemplate);
17
+ const transition = resolveTransitionName(initialTransition);
13
18
  const head = findFont(fonts.head);
14
19
  const body = findFont(fonts.body);
15
20
  const fontAttrs = (head ? ` data-head="${head}"` : "") + (body ? ` data-body="${body}"` : "");
16
21
  return `<!DOCTYPE html>
17
- <html lang="en" data-theme="${initialTheme}" data-decor="${decorOf(initialTheme)}" data-size="${size}"${fontAttrs}>
22
+ <html lang="en" data-theme="${initialTheme}" data-decor="${decorOf(initialTheme)}" data-size="${size}" data-template="${template}" data-transition="${transition}"${fontAttrs}>
18
23
  <head>
19
24
  <meta charset="UTF-8">
20
- <title>preview</title>
25
+ <title>preview · deckrun</title>
21
26
  <link rel="preconnect" href="https://fonts.googleapis.com">
22
27
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
23
28
  <link href="${googleFontsHref()}" rel="stylesheet">
24
29
  <link rel="stylesheet" id="hljs-theme" href="${hljsHref(initialTheme)}">
25
30
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
31
+ ${richContentHead({ math: true, mermaid: true }, "local")}
26
32
  <style>
27
33
  ${RESET_CSS}
28
34
 
@@ -34,6 +40,14 @@ ${fontOverrideCss()}
34
40
 
35
41
  ${SLIDE_CSS}
36
42
 
43
+ ${TEMPLATE_CSS}
44
+
45
+ ${TRANSITION_CSS}
46
+
47
+ ${FRAGMENT_CSS}
48
+
49
+ ${RICH_CONTENT_CSS}
50
+
37
51
  ${DECOR_CSS}
38
52
 
39
53
  /* ── Preview overrides ────────────────────────────────────────────────── */
@@ -119,6 +133,11 @@ body.is-empty #pv-empty { display: flex; }
119
133
  <div id="presentation"></div>
120
134
  <div id="pv-empty">nothing to preview yet</div>
121
135
  <script>
136
+ ${FRAGMENT_RUNTIME}
137
+
138
+ ${RICH_CONTENT_RUNTIME}
139
+ </script>
140
+ <script>
122
141
  (function () {
123
142
  'use strict';
124
143
 
@@ -144,7 +163,7 @@ body.is-empty #pv-empty { display: flex; }
144
163
 
145
164
  function highlight(root) {
146
165
  if (!window.hljs) return;
147
- var blocks = root.querySelectorAll('pre code');
166
+ var blocks = root.querySelectorAll('pre code:not(.language-mermaid):not(.lang-mermaid)');
148
167
  for (var i = 0; i < blocks.length; i++) {
149
168
  if (!blocks[i].dataset.highlighted) {
150
169
  try { window.hljs.highlightElement(blocks[i]); } catch (e) {}
@@ -157,7 +176,19 @@ body.is-empty #pv-empty { display: flex; }
157
176
  if (mode !== 'single') return;
158
177
  var content = stage.querySelector('.slide__content');
159
178
  var over = false;
160
- if (content) over = content.scrollHeight - content.clientHeight > 6;
179
+ if (content) {
180
+ over = content.scrollHeight - content.clientHeight > 6 ||
181
+ content.scrollWidth - content.clientWidth > 6;
182
+
183
+ // KaTeX display equations and Mermaid hosts deliberately hide their own
184
+ // overflow to keep a projected slide tidy. Inspect them separately so a
185
+ // clipped formula or diagram still triggers the editor's overflow nudge.
186
+ var rich = content.querySelectorAll('.katex-display, .mermaid');
187
+ for (var i = 0; !over && i < rich.length; i++) {
188
+ over = rich[i].scrollHeight - rich[i].clientHeight > 6 ||
189
+ rich[i].scrollWidth - rich[i].clientWidth > 6;
190
+ }
191
+ }
161
192
  send({ type: 'overflow', index: index, overflow: over });
162
193
  }
163
194
 
@@ -166,8 +197,10 @@ body.is-empty #pv-empty { display: flex; }
166
197
  stage.innerHTML = slides[index] || '';
167
198
  var el = stage.querySelector('.slide');
168
199
  if (el) el.classList.add('is-active');
200
+ if (window.deckrunPrepareFragments) window.deckrunPrepareFragments(stage, true);
169
201
  highlight(stage);
170
- requestAnimationFrame(reportOverflow);
202
+ var rich = window.deckrunRenderRichContent ? window.deckrunRenderRichContent(stage) : Promise.resolve();
203
+ rich.then(function () { requestAnimationFrame(reportOverflow); });
171
204
  }
172
205
 
173
206
  function renderGrid() {
@@ -195,7 +228,10 @@ body.is-empty #pv-empty { display: flex; }
195
228
  });
196
229
  stage.appendChild(frag);
197
230
  scaleThumbs();
231
+ if (window.deckrunPrepareFragments) window.deckrunPrepareFragments(stage, true);
198
232
  highlight(stage);
233
+ var rich = window.deckrunRenderRichContent ? window.deckrunRenderRichContent(stage) : Promise.resolve();
234
+ rich.then(scaleThumbs);
199
235
  }
200
236
 
201
237
  function scaleThumbs() {
@@ -220,6 +256,86 @@ body.is-empty #pv-empty { display: flex; }
220
256
  if (thumb) send({ type: 'goto', index: parseInt(thumb.dataset.index, 10) });
221
257
  });
222
258
 
259
+ window.addEventListener('keydown', function (e) {
260
+ var mod = e.metaKey || e.ctrlKey;
261
+ if (mod) {
262
+ var k = e.key.toLowerCase();
263
+ if (k === 'enter') { e.preventDefault(); send({ type: 'action', action: 'present' }); }
264
+ else if (k === 'k' && !e.shiftKey) { e.preventDefault(); send({ type: 'action', action: 'palette' }); }
265
+ else if (k === 'g') { e.preventDefault(); send({ type: 'action', action: 'grid' }); }
266
+ else if (k === 'o') { e.preventDefault(); send({ type: 'action', action: 'decks' }); }
267
+ else if (k === '/') { e.preventDefault(); send({ type: 'action', action: 'guide' }); }
268
+ else if (k === 'p' || (k === 's' && e.shiftKey)) { e.preventDefault(); send({ type: 'action', action: 'pdf' }); }
269
+ else if (k === 's') { e.preventDefault(); send({ type: 'action', action: 'download' }); }
270
+ else if (k === 'l' && e.shiftKey) { e.preventDefault(); send({ type: 'action', action: 'theme' }); }
271
+ return;
272
+ }
273
+
274
+ if (mode === 'grid') {
275
+ var thumbs = stage.querySelectorAll('.pv-thumb');
276
+ if (!thumbs.length) return;
277
+ var cols = 1;
278
+ if (thumbs.length > 1) {
279
+ var firstTop = thumbs[0].offsetTop;
280
+ for (var c = 1; c < thumbs.length; c++) {
281
+ if (thumbs[c].offsetTop !== firstTop) { cols = c; break; }
282
+ }
283
+ if (cols === 1 && thumbs.length > 1 && thumbs[1].offsetTop === firstTop) {
284
+ cols = thumbs.length;
285
+ }
286
+ }
287
+ var step = 0;
288
+ if (e.key === 'ArrowRight') step = 1;
289
+ else if (e.key === 'ArrowLeft') step = -1;
290
+ else if (e.key === 'ArrowDown') step = cols;
291
+ else if (e.key === 'ArrowUp') step = -cols;
292
+ else if (e.key === 'Home') { e.preventDefault(); send({ type: 'goto', index: 0 }); return; }
293
+ else if (e.key === 'End') { e.preventDefault(); send({ type: 'goto', index: slides.length - 1 }); return; }
294
+ else if (e.key === 'Enter') { e.preventDefault(); send({ type: 'goto', index: index }); return; }
295
+ else if (e.key === 'Escape') { e.preventDefault(); send({ type: 'goto', index: index }); return; }
296
+ else return;
297
+
298
+ e.preventDefault();
299
+ var next = Math.max(0, Math.min(slides.length - 1, index + step));
300
+ index = next;
301
+ var cur = stage.querySelector('.pv-thumb.is-current');
302
+ if (cur) cur.classList.remove('is-current');
303
+ var nextThumb = stage.querySelector('.pv-thumb[data-index="' + index + '"]');
304
+ if (nextThumb) {
305
+ nextThumb.classList.add('is-current');
306
+ nextThumb.scrollIntoView({ block: 'nearest' });
307
+ }
308
+ send({ type: 'index-select', index: index });
309
+ return;
310
+ }
311
+
312
+ // Single mode: Alt navigation or standard keys
313
+ if (e.altKey) {
314
+ if (e.key === 'ArrowDown' || e.key === 'ArrowRight' || e.key === 'PageDown') {
315
+ e.preventDefault();
316
+ send({ type: 'nav', delta: 1 });
317
+ } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft' || e.key === 'PageUp') {
318
+ e.preventDefault();
319
+ send({ type: 'nav', delta: -1 });
320
+ }
321
+ return;
322
+ }
323
+
324
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ' || e.key === 'PageDown') {
325
+ e.preventDefault();
326
+ send({ type: 'nav', delta: 1 });
327
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp' || e.key === 'PageUp') {
328
+ e.preventDefault();
329
+ send({ type: 'nav', delta: -1 });
330
+ } else if (e.key === 'Home') {
331
+ e.preventDefault();
332
+ send({ type: 'goto', index: 0 });
333
+ } else if (e.key === 'End') {
334
+ e.preventDefault();
335
+ send({ type: 'goto', index: slides.length - 1 });
336
+ }
337
+ });
338
+
223
339
  window.addEventListener('resize', function () {
224
340
  if (mode === 'grid') scaleThumbs();
225
341
  });
@@ -250,9 +366,11 @@ body.is-empty #pv-empty { display: flex; }
250
366
  // theme, which delete does and an assignment of '' would not.
251
367
  applyFont('head', m.head);
252
368
  applyFont('body', m.body);
369
+ if (m.template) document.documentElement.dataset.template = m.template;
370
+ if (m.transition) document.documentElement.dataset.transition = m.transition;
253
371
  // Type size and face both change how tall a slide's content runs, so the
254
372
  // editor's overflow warning has to be re-measured against them.
255
- requestAnimationFrame(reportOverflow);
373
+ render();
256
374
  } else if (m.type === 'index') {
257
375
  index = m.index;
258
376
  if (mode === 'grid') {
@@ -0,0 +1,170 @@
1
+ export function richContentFeatures(slides) {
2
+ let math = false;
3
+ let mermaid = false;
4
+ for (const slide of slides) {
5
+ if (!math && (slide.html.includes('class="math-source"') || slide.html.includes("math-source"))) {
6
+ math = true;
7
+ }
8
+ if (!mermaid &&
9
+ (slide.html.includes("language-mermaid") ||
10
+ slide.html.includes("lang-mermaid") ||
11
+ slide.html.includes('class="mermaid"'))) {
12
+ mermaid = true;
13
+ }
14
+ }
15
+ return { math, mermaid };
16
+ }
17
+ export function richContentHead(features, source = "local") {
18
+ const parts = [];
19
+ if (features.math) {
20
+ if (source === "cdn") {
21
+ parts.push('<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.18.4/dist/katex.min.css">');
22
+ parts.push('<script src="https://cdn.jsdelivr.net/npm/katex@0.18.4/dist/katex.min.js"></script>');
23
+ }
24
+ else {
25
+ parts.push('<link rel="stylesheet" href="/__vendor/katex.min.css">');
26
+ parts.push('<script src="/__vendor/katex.min.js"></script>');
27
+ }
28
+ }
29
+ if (features.mermaid) {
30
+ if (source === "cdn") {
31
+ parts.push('<script src="https://cdn.jsdelivr.net/npm/mermaid@11.17.2/dist/mermaid.min.js"></script>');
32
+ }
33
+ else {
34
+ parts.push('<script src="/__vendor/mermaid.min.js"></script>');
35
+ }
36
+ }
37
+ return parts.join("\n ");
38
+ }
39
+ export const RICH_CONTENT_CSS = `/* ── Rich Content (Math & Diagrams) ─────────────────────────────────────── */
40
+
41
+ .math-source {
42
+ font-family: inherit;
43
+ }
44
+
45
+ div.math-source {
46
+ display: flex;
47
+ justify-content: center;
48
+ margin: 1.2em 0;
49
+ overflow-x: auto;
50
+ overflow-y: hidden;
51
+ }
52
+
53
+ span.math-source {
54
+ display: inline;
55
+ }
56
+
57
+ .katex-display {
58
+ margin: 0.8em 0;
59
+ overflow-x: auto;
60
+ overflow-y: hidden;
61
+ }
62
+
63
+ .katex {
64
+ font-size: 1.15em;
65
+ text-rendering: auto;
66
+ }
67
+
68
+ .math-error {
69
+ color: var(--maroon, #f38ba8);
70
+ background: var(--surface0, rgba(255, 0, 0, 0.1));
71
+ padding: 2px 6px;
72
+ border-radius: 4px;
73
+ font-family: var(--font-mono, monospace);
74
+ font-size: 0.85em;
75
+ }
76
+
77
+ .mermaid {
78
+ display: flex;
79
+ justify-content: center;
80
+ align-items: center;
81
+ margin: 1.2em auto;
82
+ max-width: 100%;
83
+ overflow: hidden;
84
+ }
85
+
86
+ .mermaid svg {
87
+ max-width: 100%;
88
+ height: auto;
89
+ }
90
+
91
+ .mermaid-error {
92
+ color: var(--maroon, #f38ba8);
93
+ background: var(--surface0, rgba(255, 0, 0, 0.1));
94
+ border: 1px solid var(--maroon, #f38ba8);
95
+ border-radius: 6px;
96
+ padding: 12px 16px;
97
+ font-family: var(--font-mono, monospace);
98
+ font-size: 0.9em;
99
+ white-space: pre-wrap;
100
+ margin: 1em 0;
101
+ }
102
+ `;
103
+ export const RICH_CONTENT_RUNTIME = `(function () {
104
+ window.deckrunRenderRichContent = function (root) {
105
+ if (!root) return Promise.resolve();
106
+
107
+ // 1. Render KaTeX math
108
+ if (window.katex) {
109
+ var mathNodes = root.querySelectorAll('.math-source:not([data-rendered])');
110
+ for (var i = 0; i < mathNodes.length; i++) {
111
+ var el = mathNodes[i];
112
+ var tex = el.textContent || '';
113
+ var isDisplay = el.dataset.display === 'true';
114
+ try {
115
+ window.katex.render(tex, el, {
116
+ displayMode: isDisplay,
117
+ throwOnError: false,
118
+ output: 'htmlAndMathml'
119
+ });
120
+ el.setAttribute('data-rendered', 'true');
121
+ } catch (err) {
122
+ el.innerHTML = '<span class="math-error">' + (err && err.message ? err.message : 'Math rendering error') + '</span>';
123
+ el.setAttribute('data-rendered', 'true');
124
+ }
125
+ }
126
+ }
127
+
128
+ // 2. Render Mermaid diagrams
129
+ var mermaidPromises = [];
130
+ var codeBlocks = root.querySelectorAll('pre code.language-mermaid, pre code.lang-mermaid');
131
+ if (codeBlocks.length > 0 && window.mermaid) {
132
+ try {
133
+ window.mermaid.initialize({
134
+ startOnLoad: false,
135
+ theme: 'dark',
136
+ securityLevel: 'loose'
137
+ });
138
+ } catch (e) {}
139
+
140
+ for (var j = 0; j < codeBlocks.length; j++) {
141
+ (function (codeEl) {
142
+ var preEl = codeEl.closest('pre');
143
+ if (!preEl || preEl.dataset.rendered) return;
144
+ preEl.dataset.rendered = 'true';
145
+ var code = codeEl.textContent || '';
146
+ var container = document.createElement('div');
147
+ container.className = 'mermaid';
148
+ preEl.parentNode.insertBefore(container, preEl);
149
+ preEl.style.display = 'none';
150
+
151
+ var id = 'mermaid-' + Math.random().toString(36).slice(2, 10);
152
+ var p = window.mermaid.render(id, code)
153
+ .then(function (res) {
154
+ container.innerHTML = res.svg;
155
+ preEl.remove();
156
+ })
157
+ .catch(function (err) {
158
+ container.className = 'mermaid-error';
159
+ container.textContent = 'Mermaid Error: ' + (err && err.message ? err.message : String(err));
160
+ preEl.remove();
161
+ });
162
+ mermaidPromises.push(p);
163
+ })(codeBlocks[j]);
164
+ }
165
+ }
166
+
167
+ return Promise.all(mermaidPromises).then(function () {});
168
+ };
169
+ })();
170
+ `;
package/dist/themes.js CHANGED
@@ -53,7 +53,7 @@ export function fontName(key) {
53
53
  }
54
54
  const SANS = "system-ui, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif";
55
55
  const SERIF = "Georgia, 'Times New Roman', serif";
56
- const MONO = "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
56
+ const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
57
57
  export const FONTS = {
58
58
  inter: { param: "Inter:wght@300;400;500;600;700;800", stack: `'Inter', ${SANS}`, kind: "sans" },
59
59
  spaceGrotesk: { param: "Space+Grotesk:wght@400;500;600;700", stack: `'Space Grotesk', ${SANS}`, kind: "sans" },
@@ -71,7 +71,7 @@ export const FONTS = {
71
71
  newsreader: { param: "Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;1,6..72,400", stack: `'Newsreader', ${SERIF}`, kind: "serif" },
72
72
  lora: { param: "Lora:ital,wght@0,400;0,500;0,600;0,700;1,400", stack: `'Lora', ${SERIF}`, kind: "serif" },
73
73
  plexMono: { param: "IBM+Plex+Mono:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400", stack: `'IBM Plex Mono', ${MONO}`, kind: "mono" },
74
- jetbrains: { param: "JetBrains+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400", stack: `'JetBrains Mono', ${MONO}`, kind: "mono" },
74
+ jetbrains: { param: "JetBrains+Mono:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400", stack: `'JetBrains Mono', ${MONO}`, kind: "mono" },
75
75
  firaCode: { param: "Fira+Code:wght@400;500;600;700", stack: `'Fira Code', ${MONO}`, kind: "mono" },
76
76
  spaceMono: { param: "Space+Mono:ital,wght@0,400;0,700;1,400", stack: `'Space Mono', ${MONO}`, kind: "mono" },
77
77
  sourceCodePro: { param: "Source+Code+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400", stack: `'Source Code Pro', ${MONO}`, kind: "mono" },
@@ -85,6 +85,7 @@ export const FONTS = {
85
85
  */
86
86
  export function googleFontsHref(themes = THEME_IDS.slice(), extra = []) {
87
87
  const wanted = new Set();
88
+ wanted.add("jetbrains");
88
89
  for (const input of themes) {
89
90
  const t = THEMES[resolveThemeName(input)];
90
91
  wanted.add(t.type.display);
@@ -162,7 +163,7 @@ const SPECS = {
162
163
  red: "#bf616a", mauve: "#b48ead", pink: "#d3a3c6",
163
164
  },
164
165
  roles: { accent: "sapphire", accent2: "blue", accent3: "teal" },
165
- type: { display: "manrope", body: "manrope", mono: "jetbrains", weight: 800, tracking: "-0.035em" },
166
+ type: { display: "jetbrains", body: "jetbrains", mono: "jetbrains", weight: 700, tracking: "-0.025em" },
166
167
  decor: "waves",
167
168
  hljs: `${HL}nord.min.css`,
168
169
  },
@@ -399,7 +400,7 @@ export const THEME_IDS = [
399
400
  export const THEMES = Object.fromEntries(THEME_IDS.map((id) => [id, { id, ...SPECS[id] }]));
400
401
  /** `dark` and `light` predate the registry and still name the two originals. */
401
402
  const ALIASES = {
402
- dark: "midnight",
403
+ dark: "nord",
403
404
  light: "daylight",
404
405
  mocha: "midnight",
405
406
  latte: "daylight",
@@ -407,7 +408,7 @@ const ALIASES = {
407
408
  "rose-pine": "rosepine",
408
409
  "rose-quartz": "rosequartz",
409
410
  };
410
- export const DEFAULT_THEME = "midnight";
411
+ export const DEFAULT_THEME = "nord";
411
412
  /** A theme id from untrusted input, or `null` if it names nothing. */
412
413
  export function findTheme(input) {
413
414
  if (!input)
@@ -498,6 +499,8 @@ function themeVars(t) {
498
499
  ["glow", alpha(accent, dark ? 0.34 : 0.22)],
499
500
  ["gradient", `linear-gradient(115deg, ${accent}, ${accent2} 58%, ${accent3})`],
500
501
  ["accent-fade", `linear-gradient(90deg, ${accent}, ${alpha(accent, 0.45)} 62%, transparent)`],
502
+ ["selection-bg", alpha(accent, dark ? 0.22 : 0.18)],
503
+ ["selection-text", "inherit"],
501
504
  ["surface-soft", alpha(n.surface0, dark ? 0.34 : 0.42)],
502
505
  ["crust-overlay", alpha(n.crust, dark ? 0.84 : 0.88)],
503
506
  ["scrim", dark ? "rgba(0, 0, 0, 0.5)" : alpha(n.text, 0.26)],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deckrun",
3
- "version": "1.3.0",
3
+ "version": "1.6.0",
4
4
  "description": "A local-first presentation tool for writing, editing, presenting, and exporting Markdown slides or self-contained HTML docs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,11 +11,14 @@
11
11
  ],
12
12
  "scripts": {
13
13
  "build": "tsc && chmod +x dist/index.js",
14
- "dev": "tsx src/index.ts"
14
+ "dev": "tsx src/index.ts",
15
+ "test": "npm run build && node --test"
15
16
  },
16
17
  "dependencies": {
17
18
  "commander": "^11.1.0",
19
+ "katex": "^0.18.4",
18
20
  "marked": "^9.1.6",
21
+ "mermaid": "^11.17.2",
19
22
  "open": "^9.1.0"
20
23
  },
21
24
  "devDependencies": {