@entropicwarrior/sdoc 0.1.8 → 0.1.10

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/README.md CHANGED
@@ -107,4 +107,4 @@ Release branches merge to both `main` and `develop`. No direct pushes to `main`
107
107
 
108
108
  ## License
109
109
 
110
- [MIT](LICENSE) — 2026 Irreversible Inc.
110
+ [MIT](LICENSE) — 2026 @entropicwarrior
@@ -3,6 +3,8 @@
3
3
  # Meta @meta
4
4
  {
5
5
  type: skill
6
+
7
+ sdoc-version: 0.1
6
8
  }
7
9
 
8
10
  # About @about
@@ -175,6 +177,13 @@
175
177
  \`**text**\` | Strong
176
178
  \`~~text~~\` | Strikethrough
177
179
  \`\\\`code\\\`\` | Inline code
180
+ \`\$x^2\$\` | Inline math (KaTeX)
181
+ \`\$\$E = mc^2\$\$\` | Display math (centered)
182
+ \`\{+text+\}\` | Positive marker (green)
183
+ \`\{=text=\}\` | Neutral marker (blue)
184
+ \`\{!text!\}\` | Warning marker (amber)
185
+ \`\{-text-\}\` | Negative marker (red)
186
+ \`\{~text~\}\` | Highlight (yellow)
178
187
  }
179
188
 
180
189
  Links: \`[Link text](https://example.com)\`
@@ -192,6 +201,8 @@
192
201
  ```
193
202
 
194
203
  Autolinks: \`\<https://example.com\>\`
204
+
205
+ Math: Use \`\$...\$\` for inline math and \`\$\$...\$\$\` for display math. Use \`\\\`\\\`\\\`math\` code fences for multi-line equations. A plain \`\$\` followed by a digit (e.g. \`\$100\`) does not trigger math mode.
195
206
  }
196
207
 
197
208
  # References @references
@@ -252,6 +263,19 @@
252
263
  Mermaid supports flowcharts (\`graph\`), sequence diagrams (\`sequenceDiagram\`), class diagrams (\`classDiagram\`), state diagrams (\`stateDiagram-v2\`), and more. The Mermaid library is loaded from CDN only when a document contains mermaid blocks.
253
264
  }
254
265
 
266
+ # Math Blocks @math-blocks
267
+ {
268
+ Code blocks with the \`math\` language tag are rendered as display equations via KaTeX:
269
+
270
+ ````
271
+ ```math
272
+ \\int_{-\\infty}^{\\infty} e^{-x^2} \\, dx = \\sqrt{\\pi}
273
+ ```
274
+ ````
275
+
276
+ Math blocks render as centered display equations with no copy button. The KaTeX CSS is loaded from CDN only when a document contains math content.
277
+ }
278
+
255
279
  # Blockquotes @blockquotes
256
280
  {
257
281
  ```
@@ -268,6 +292,8 @@
268
292
  @meta {
269
293
  type: doc
270
294
 
295
+ sdoc-version: 0.1
296
+
271
297
  company: Irreversible Inc.
272
298
 
273
299
  confidential: true
@@ -305,6 +331,9 @@
305
331
  \`uuid: ...\` — unique identifier.
306
332
 
307
333
  \`tags: tag1, tag2\` — comma-separated tags.
334
+
335
+ \`sdoc-version: 0.1\` — SDOC format version. A parser warning is
336
+ emitted when this key is missing from \`@meta\`.
308
337
  }
309
338
 
310
339
  # About Scope @about-scope
@@ -324,9 +353,9 @@
324
353
  {
325
354
  Backslash escapes special characters: \`\\\\\` \`\\{\` \`\\}\` \`\\@\`
326
355
  \`\\[\` \`\\]\` \`\\(\` \`\\)\` \`\\*\` \`\\~\` \`\\#\` \`\\!\` \`\\\<\`
327
- \`\\\>\`
356
+ \`\\\>\` \`\\\$\` \`\\+\` \`\\=\` \`\\-\`
328
357
 
329
- A line starting with \`\\#\` renders as a literal \`#\` (not a heading).
358
+ A line starting with \`\\#\` renders as a literal \`#\` (not a heading). Use \`\\\$\` to prevent a dollar sign from starting math mode.
330
359
  }
331
360
  }
332
361
 
@@ -3,6 +3,8 @@
3
3
  # Meta @meta
4
4
  {
5
5
  type: skill
6
+
7
+ sdoc-version: 0.1
6
8
  }
7
9
 
8
10
  # About @about
@@ -3,6 +3,8 @@
3
3
  # Meta @meta
4
4
  {
5
5
  type: doc
6
+
7
+ sdoc-version: 0.1
6
8
  }
7
9
 
8
10
  # About @about
@@ -448,16 +450,25 @@ Content of Section B.
448
450
  - Strong: `**strong**`
449
451
  - Strikethrough: `~~strike~~`
450
452
  - Inline code: `` `code` ``
453
+ - Inline math: `$x^2$` (rendered via KaTeX)
454
+ - Display math: `$$E = mc^2$$` (centered block)
455
+ - Positive marker: `{+text+}` (green highlight)
456
+ - Neutral marker: `{=text=}` (blue highlight)
457
+ - Warning marker: `{!text!}` (amber highlight)
458
+ - Negative marker: `{-text-}` (red highlight)
459
+ - Highlight: `{~text~}` (yellow highlight)
451
460
  }
461
+
462
+ Inline math requires non-whitespace immediately after the opening `$` and before the closing `$`. A plain `$` followed by a digit (e.g. `$100`) does not trigger math mode because there is no closing `$`.
452
463
  }
453
464
 
454
465
  # Escaping @escaping
455
466
  {
456
- In normal text (including headings and paragraphs), a backslash escapes: `\\` `\{` `\}` `\@` `\[` `\]` `\(` `\)` `\*` `\~` `\#` `\!` `\<` `\>` and `` \` ``.
467
+ In normal text (including headings and paragraphs), a backslash escapes: `\\` `\{` `\}` `\@` `\[` `\]` `\(` `\)` `\*` `\~` `\#` `\!` `\<` `\>` `\$` `\+` `\=` `\-` and `` \` ``.
457
468
 
458
469
  Escapes are processed before reference detection.
459
470
 
460
- If a line begins with `\#`, it is treated as a normal paragraph line (rendered with a literal `#`). If a line begins with `\>`, it is treated as a normal paragraph line (rendered with a literal `>`).
471
+ If a line begins with `\#`, it is treated as a normal paragraph line (rendered with a literal `#`). If a line begins with `\>`, it is treated as a normal paragraph line (rendered with a literal `>`). Use `\$` to prevent a dollar sign from starting math mode.
461
472
  }
462
473
 
463
474
  # Code Blocks @code-blocks
@@ -474,6 +485,8 @@ Content of Section B.
474
485
  - The opening and closing fences must be on their own lines
475
486
  - Anything inside is treated as raw text (no parsing, no escapes)
476
487
  - Optional language tag after the opening fence
488
+ - The special language tag `math` renders the block as a display equation via KaTeX (no copy button)
489
+ - The special language tag `mermaid` renders the block as an SVG diagram via the Mermaid library
477
490
  }
478
491
 
479
492
  # Include by Link @code-includes
@@ -570,13 +583,15 @@ Content of Section B.
570
583
  date: 2026-02-09
571
584
  version: 1.0
572
585
  status: Draft
586
+ sdoc-version: 0.1
573
587
  }
574
588
  ```
575
589
 
576
590
  {[.]
577
591
  - Key matching is case-insensitive
578
592
  - The pattern requires at least one space after the colon (`key: value`, not `key:value`)
579
- - Well-known keys: `style`, `styleappend`/`style-append`, `header`, `footer`
593
+ - Well-known keys: `style`, `styleappend`/`style-append`, `header`, `footer`, `sdoc-version`
594
+ - `sdoc-version` identifies the SDOC format version the document targets (current: `0.1`). A parser warning is emitted when this key is missing.
580
595
  - All other keys are stored as custom properties (e.g., `author`, `date`, `version`, `status`, `tags`)
581
596
  - Sub-scope syntax takes precedence: if both `# Style { path }` and `style: path` exist, the sub-scope value wins
582
597
  - Key:value and sub-scope syntax can be mixed freely in the same meta scope
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@entropicwarrior/sdoc",
3
3
  "displayName": "SDOC",
4
4
  "description": "A plain-text documentation format with explicit brace scoping — deterministic parsing, AI-agent efficiency, and 10-50x token savings vs Markdown.",
5
- "version": "0.1.8",
5
+ "version": "0.1.10",
6
6
  "publisher": "entropicwarrior",
7
7
  "license": "MIT",
8
8
  "repository": {
@@ -76,6 +76,7 @@ function richText(content, href, annotations) {
76
76
  function enforceContentLimit(richTexts, limit) {
77
77
  const result = [];
78
78
  for (const rt of richTexts) {
79
+ if (!rt.text) { result.push(rt); continue; }
79
80
  const content = rt.text.content;
80
81
  if (content.length <= limit) {
81
82
  result.push(rt);
@@ -128,6 +129,21 @@ function flattenInlineNodes(nodes, annotations, href) {
128
129
  case "image":
129
130
  // Images are extracted at the block level; skip here.
130
131
  break;
132
+ case "math_inline":
133
+ case "math_display":
134
+ result.push({
135
+ type: "equation",
136
+ equation: { expression: node.value },
137
+ annotations: ann
138
+ });
139
+ break;
140
+ case "mark_positive":
141
+ case "mark_neutral":
142
+ case "mark_warning":
143
+ case "mark_negative":
144
+ case "mark_highlight":
145
+ result.push(...flattenInlineNodes(node.children, ann, href));
146
+ break;
131
147
  default:
132
148
  break;
133
149
  }
@@ -160,7 +176,7 @@ function extractImagesFromInline(text) {
160
176
  }
161
177
 
162
178
  const rt = enforceContentLimit(flattenInlineNodes(textNodes), RICH_TEXT_LIMIT);
163
- const hasText = rt.some((r) => r.text.content.trim() !== "");
179
+ const hasText = rt.some((r) => (r.text ? r.text.content.trim() !== "" : r.type === "equation"));
164
180
 
165
181
  const imageBlocks = [];
166
182
  for (const img of images) {
@@ -258,6 +274,9 @@ function renderNode(node, depth, nestLevel) {
258
274
  case "table":
259
275
  return renderTable(node);
260
276
  case "code":
277
+ if (node.lang === "math") {
278
+ return [{ type: "equation", equation: { expression: node.text || "" } }];
279
+ }
261
280
  return renderCode(node);
262
281
  case "blockquote":
263
282
  return renderBlockquote(node);
package/src/sdoc.js CHANGED
@@ -1,3 +1,5 @@
1
+ const SDOC_FORMAT_VERSION = "0.1";
2
+
1
3
  const COMMAND_HEADING = "#";
2
4
  const COMMAND_SCOPE_OPEN = "{";
3
5
  const COMMAND_SCOPE_CLOSE = "}";
@@ -6,7 +8,29 @@ const COMMAND_LIST_NUMBER = "{[#]";
6
8
  const COMMAND_TABLE = "{[table]";
7
9
  const COMMAND_CODE_FENCE = "```";
8
10
 
9
- const ESCAPABLE = new Set(["\\", "{", "}", "@", "[", "]", "(", ")", "*", "`", "#", "!", "~", "<", ">"]);
11
+ const ESCAPABLE = new Set(["\\", "{", "}", "@", "[", "]", "(", ")", "*", "`", "#", "!", "~", "<", ">", "$", "+", "=", "-"]);
12
+
13
+ let _katex = null;
14
+ let _katexLoaded = false;
15
+
16
+ function getKatex() {
17
+ if (!_katexLoaded) {
18
+ _katexLoaded = true;
19
+ try {
20
+ _katex = require(require("path").join(__dirname, "..", "vendor", "katex.min.js"));
21
+ } catch { _katex = null; }
22
+ }
23
+ return _katex;
24
+ }
25
+
26
+ function renderKatex(latex, displayMode) {
27
+ const katex = getKatex();
28
+ if (katex) {
29
+ return katex.renderToString(latex, { displayMode, throwOnError: false });
30
+ }
31
+ const cls = displayMode ? "sdoc-math-fallback sdoc-math-display-fallback" : "sdoc-math-fallback";
32
+ return `<code class="${cls}">${escapeHtml(latex)}</code>`;
33
+ }
10
34
 
11
35
  function isTableCommand(text) {
12
36
  return /^\{\[table(?:\s+[^\]]*?)?\]$/.test(text);
@@ -1132,6 +1156,28 @@ function parseInline(text) {
1132
1156
  }
1133
1157
  }
1134
1158
 
1159
+ // Display math $$...$$ (must come before $ check)
1160
+ if (text.startsWith("$$", i)) {
1161
+ const end = findUnescaped(text, i + 2, "$$");
1162
+ if (end !== -1 && end > i + 2) {
1163
+ flush();
1164
+ nodes.push({ type: "math_display", value: text.slice(i + 2, end) });
1165
+ i = end + 2;
1166
+ continue;
1167
+ }
1168
+ }
1169
+
1170
+ // Inline math $...$ (require non-whitespace after open and before close)
1171
+ if (ch === "$" && next && next !== " " && next !== "\t" && next !== "$") {
1172
+ const end = findUnescaped(text, i + 1, "$");
1173
+ if (end !== -1 && end > i + 1 && text[end - 1] !== " " && text[end - 1] !== "\t") {
1174
+ flush();
1175
+ nodes.push({ type: "math_inline", value: text.slice(i + 1, end) });
1176
+ i = end + 1;
1177
+ continue;
1178
+ }
1179
+ }
1180
+
1135
1181
  if (text.startsWith("**", i)) {
1136
1182
  const end = findUnescaped(text, i + 2, "**");
1137
1183
  if (end !== -1) {
@@ -1154,6 +1200,25 @@ function parseInline(text) {
1154
1200
  }
1155
1201
  }
1156
1202
 
1203
+ if (ch === "{") {
1204
+ const mc = next;
1205
+ let mt = null;
1206
+ if (mc === "+") mt = "mark_positive";
1207
+ else if (mc === "=") mt = "mark_neutral";
1208
+ else if (mc === "!") mt = "mark_warning";
1209
+ else if (mc === "-") mt = "mark_negative";
1210
+ else if (mc === "~") mt = "mark_highlight";
1211
+ if (mt) {
1212
+ const end = findUnescaped(text, i + 2, mc + "}");
1213
+ if (end !== -1) {
1214
+ flush();
1215
+ nodes.push({ type: mt, children: parseInline(text.slice(i + 2, end)) });
1216
+ i = end + 2;
1217
+ continue;
1218
+ }
1219
+ }
1220
+ }
1221
+
1157
1222
  if (ch === "*") {
1158
1223
  const end = findUnescaped(text, i + 1, "*");
1159
1224
  if (end !== -1) {
@@ -1296,6 +1361,16 @@ function renderInlineNodes(nodes) {
1296
1361
  return `<strong>${renderInlineNodes(node.children)}</strong>`;
1297
1362
  case "strike":
1298
1363
  return `<del>${renderInlineNodes(node.children)}</del>`;
1364
+ case "mark_positive":
1365
+ return `<span class="sdoc-mark sdoc-mark-positive">${renderInlineNodes(node.children)}</span>`;
1366
+ case "mark_neutral":
1367
+ return `<span class="sdoc-mark sdoc-mark-neutral">${renderInlineNodes(node.children)}</span>`;
1368
+ case "mark_warning":
1369
+ return `<span class="sdoc-mark sdoc-mark-warning">${renderInlineNodes(node.children)}</span>`;
1370
+ case "mark_negative":
1371
+ return `<span class="sdoc-mark sdoc-mark-negative">${renderInlineNodes(node.children)}</span>`;
1372
+ case "mark_highlight":
1373
+ return `<mark class="sdoc-mark sdoc-mark-highlight">${renderInlineNodes(node.children)}</mark>`;
1299
1374
  case "link":
1300
1375
  return `<a class="sdoc-link" href="${escapeAttr(node.href)}" target="_blank" rel="noopener noreferrer">${renderInlineNodes(
1301
1376
  node.children
@@ -1309,6 +1384,10 @@ function renderInlineNodes(nodes) {
1309
1384
  const imgStyle = imgParts.length ? ` style="${imgParts.join(";")}"` : "";
1310
1385
  return `<img class="sdoc-image" src="${escapeAttr(node.src)}" alt="${escapeAttr(node.alt)}"${imgStyle} />`;
1311
1386
  }
1387
+ case "math_inline":
1388
+ return `<span class="sdoc-math sdoc-math-inline">${renderKatex(node.value, false)}</span>`;
1389
+ case "math_display":
1390
+ return `<span class="sdoc-math sdoc-math-display">${renderKatex(node.value, true)}</span>`;
1312
1391
  default:
1313
1392
  return "";
1314
1393
  }
@@ -1452,8 +1531,11 @@ function renderNode(node, depth) {
1452
1531
  if (node.lang === "mermaid") {
1453
1532
  return `<pre class="mermaid"${dl}>${escapeHtml(node.text)}</pre>`;
1454
1533
  }
1534
+ if (node.lang === "math") {
1535
+ return `<div class="sdoc-math sdoc-math-block"${dl}>${renderKatex(node.text, true)}</div>`;
1536
+ }
1455
1537
  const langClass = node.lang ? ` class="language-${escapeAttr(node.lang)}"` : "";
1456
- return `<pre class="sdoc-code"${dl}><code${langClass}>${escapeHtml(node.text)}</code></pre>`;
1538
+ return `<div class="sdoc-code-wrap"${dl}><pre class="sdoc-code"><code${langClass}>${escapeHtml(node.text)}</code></pre><button class="sdoc-copy-btn" title="Copy code">\u29C9</button></div>`;
1457
1539
  }
1458
1540
  default:
1459
1541
  return "";
@@ -1484,7 +1566,7 @@ function extractMeta(nodes) {
1484
1566
  }
1485
1567
 
1486
1568
  if (metaIndex === -1) {
1487
- return { nodes, meta: {} };
1569
+ return { nodes, meta: {}, warnings: [] };
1488
1570
  }
1489
1571
 
1490
1572
  const metaNode = searchNodes[metaIndex];
@@ -1545,14 +1627,19 @@ function extractMeta(nodes) {
1545
1627
  meta.company = meta.properties.company || null;
1546
1628
  meta.confidential = meta.properties.confidential || null;
1547
1629
 
1630
+ const warnings = [];
1631
+ if (!meta.properties["sdoc-version"]) {
1632
+ warnings.push("Missing sdoc-version in @meta (current format version is " + SDOC_FORMAT_VERSION + ")");
1633
+ }
1634
+
1548
1635
  if (doc) {
1549
1636
  // @meta was inside the document scope — strip it from children
1550
1637
  const filteredChildren = doc.children.filter((_, index) => index !== metaIndex);
1551
1638
  const stripped = { ...doc, children: filteredChildren };
1552
- return { nodes: [stripped], meta };
1639
+ return { nodes: [stripped], meta, warnings };
1553
1640
  }
1554
1641
  const bodyNodes = nodes.filter((_, index) => index !== metaIndex);
1555
- return { nodes: bodyNodes, meta };
1642
+ return { nodes: bodyNodes, meta, warnings };
1556
1643
  }
1557
1644
 
1558
1645
  function collectParagraphText(nodes) {
@@ -1834,6 +1921,17 @@ const DEFAULT_STYLE = `
1834
1921
  padding: 0 0.2em;
1835
1922
  }
1836
1923
 
1924
+ .sdoc-mark {
1925
+ border-radius: 3px;
1926
+ padding: 0.05em 0.3em;
1927
+ font-weight: 500;
1928
+ }
1929
+ .sdoc-mark-positive { background-color: rgba(34, 139, 34, 0.15); color: #166016; }
1930
+ .sdoc-mark-neutral { background-color: rgba(59, 130, 195, 0.15); color: #245d8a; }
1931
+ .sdoc-mark-warning { background-color: rgba(200, 150, 30, 0.18); color: #7a5f0e; }
1932
+ .sdoc-mark-negative { background-color: rgba(187, 50, 50, 0.15); color: #911e1e; }
1933
+ .sdoc-mark-highlight { background-color: rgba(255, 255, 0, 0.75); }
1934
+
1837
1935
  .sdoc-image {
1838
1936
  display: inline-block;
1839
1937
  max-width: 100%;
@@ -1879,10 +1977,37 @@ const DEFAULT_STYLE = `
1879
1977
  `;
1880
1978
 
1881
1979
  const PRINT_STYLE = `
1980
+ .sdoc-code-wrap {
1981
+ position: relative;
1982
+ }
1983
+ .sdoc-copy-btn {
1984
+ position: absolute;
1985
+ top: 6px;
1986
+ right: 6px;
1987
+ z-index: 1;
1988
+ background: transparent;
1989
+ border: none;
1990
+ border-radius: 4px;
1991
+ cursor: pointer;
1992
+ font-size: 0.85rem;
1993
+ line-height: 1;
1994
+ padding: 2px 6px;
1995
+ opacity: 0;
1996
+ transition: opacity 0.15s;
1997
+ }
1998
+ .sdoc-code-wrap:hover .sdoc-copy-btn {
1999
+ opacity: 0.75;
2000
+ }
2001
+ .sdoc-copy-btn:hover {
2002
+ opacity: 1 !important;
2003
+ }
1882
2004
  @media print {
1883
2005
  html {
1884
2006
  font-size: 80%;
1885
2007
  }
2008
+ .sdoc-copy-btn {
2009
+ display: none;
2010
+ }
1886
2011
  body {
1887
2012
  height: auto;
1888
2013
  overflow: visible;
@@ -1902,10 +2027,12 @@ const PRINT_STYLE = `
1902
2027
  white-space: pre-wrap;
1903
2028
  word-wrap: break-word;
1904
2029
  }
2030
+ .sdoc-mark { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
1905
2031
  }
1906
2032
  `;
1907
2033
 
1908
2034
  const MERMAID_CDN = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js";
2035
+ const KATEX_CDN_CSS = "https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.css";
1909
2036
 
1910
2037
  function hasMermaidBlocks(nodes) {
1911
2038
  for (const node of nodes) {
@@ -1953,6 +2080,9 @@ function renderHtmlDocumentFromParsed(parsed, title, options = {}) {
1953
2080
  const mermaidScript = hasMermaidBlocks(parsed.nodes)
1954
2081
  ? `\n<script src="${MERMAID_CDN}"></script>\n<script>mermaid.initialize({startOnLoad:true,theme:"neutral",themeCSS:".node rect, .node polygon, .node circle { rx: 4; ry: 4; }"});</script>`
1955
2082
  : "";
2083
+ const katexCssTag = body.includes('class="katex"')
2084
+ ? `\n<link rel="stylesheet" href="${KATEX_CDN_CSS}" />`
2085
+ : "";
1956
2086
 
1957
2087
  return `<!DOCTYPE html>
1958
2088
  <html lang="en">
@@ -1962,7 +2092,7 @@ function renderHtmlDocumentFromParsed(parsed, title, options = {}) {
1962
2092
  <title>${escapeHtml(title)}</title>
1963
2093
  <style>
1964
2094
  ${cssBase}${cssAppend}
1965
- </style>
2095
+ </style>${katexCssTag}
1966
2096
  </head>
1967
2097
  <body>
1968
2098
  <div class="sdoc-shell">
@@ -2209,6 +2339,7 @@ async function resolveIncludes(nodes, resolverFn) {
2209
2339
  }
2210
2340
 
2211
2341
  module.exports = {
2342
+ SDOC_FORMAT_VERSION,
2212
2343
  parseSdoc,
2213
2344
  extractMeta,
2214
2345
  resolveIncludes,
@@ -2224,6 +2355,7 @@ module.exports = {
2224
2355
  extractAbout,
2225
2356
  // Low-level helpers for custom renderers (e.g. slide-renderer)
2226
2357
  parseInline,
2358
+ renderKatex,
2227
2359
  escapeHtml,
2228
2360
  escapeAttr
2229
2361
  };
@@ -7,7 +7,7 @@
7
7
  // const { nodes, meta } = extractMeta(parsed.nodes);
8
8
  // const html = renderSlides(nodes, { meta, themeCss, themeJs });
9
9
 
10
- const { parseInline, escapeHtml, escapeAttr } = require("./sdoc");
10
+ const { parseInline, renderKatex, escapeHtml, escapeAttr } = require("./sdoc");
11
11
 
12
12
  // ---------------------------------------------------------------------------
13
13
  // Inline rendering — produces clean HTML without sdoc-* classes
@@ -40,6 +40,10 @@ function renderInlineNodes(nodes) {
40
40
  }
41
41
  case "ref":
42
42
  return `@${escapeHtml(node.id)}`;
43
+ case "math_inline":
44
+ return `<span class="sdoc-math sdoc-math-inline">${renderKatex(node.value, false)}</span>`;
45
+ case "math_display":
46
+ return `<span class="sdoc-math sdoc-math-display">${renderKatex(node.value, true)}</span>`;
43
47
  default:
44
48
  return "";
45
49
  }
@@ -67,6 +71,9 @@ function renderNode(node) {
67
71
  if (node.lang === "mermaid") {
68
72
  return `<pre class="mermaid">${escapeHtml(node.text)}</pre>`;
69
73
  }
74
+ if (node.lang === "math") {
75
+ return `<div class="sdoc-math sdoc-math-block">${renderKatex(node.text, true)}</div>`;
76
+ }
70
77
  const langClass = node.lang ? ` class="language-${escapeAttr(node.lang)}"` : "";
71
78
  return `<pre><code${langClass}>${escapeHtml(node.text)}</code></pre>`;
72
79
  }
@@ -324,6 +331,10 @@ function renderSlides(nodes, options = {}) {
324
331
  const mermaidTag = slidesHtml.includes('class="mermaid"')
325
332
  ? `\n<script src="${mermaidCdn}"></script>\n<script>mermaid.initialize({startOnLoad:true,theme:"neutral",themeCSS:".node rect, .node polygon, .node circle { rx: 4; ry: 4; }"});</script>`
326
333
  : "";
334
+ const katexCssCdn = "https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.css";
335
+ const katexTag = slidesHtml.includes('class="katex"')
336
+ ? `\n<link rel="stylesheet" href="${katexCssCdn}" />`
337
+ : "";
327
338
 
328
339
  return `<!DOCTYPE html>
329
340
  <html lang="en">
@@ -331,7 +342,7 @@ function renderSlides(nodes, options = {}) {
331
342
  <meta charset="UTF-8">
332
343
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
333
344
  <title>${escapeHtml(title)}</title>
334
- ${cssTag}
345
+ ${cssTag}${katexTag}
335
346
  </head>
336
347
  <body>
337
348
  ${slidesHtml}