@entropicwarrior/sdoc 0.1.9 → 0.1.11

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
@@ -77,6 +77,72 @@ Or use the **SDOC: Browse Documents** command from the VS Code Command Palette.
77
77
  }
78
78
  ```
79
79
 
80
+ ## Features
81
+
82
+ ### Inline Formatting
83
+
84
+ `*emphasis*`, `**strong**`, `~~strikethrough~~`, `` `inline code` ``, `[links](url)`, and `<https://autolinks>`.
85
+
86
+ ### Semantic Markers
87
+
88
+ Annotate text with semantic meaning that renders as colored highlights:
89
+
90
+ | Syntax | Meaning | Color |
91
+ |---|---|---|
92
+ | `{+text+}` | Positive | Green |
93
+ | `{=text=}` | Neutral | Blue |
94
+ | `{^text^}` | Caution | Amber |
95
+ | `{!text!}` | Warning | Orange |
96
+ | `{-text-}` | Negative | Red |
97
+ | `{~text~}` | Highlight | Yellow |
98
+
99
+ Markers nest with other inline formatting: `{+**all checks** passed+}`.
100
+
101
+ ### Math
102
+
103
+ Inline math with `$x^2 + y^2$`, display math with `$$E = mc^2$$`, and multi-line equations with ` ```math ` code fences. Rendered via KaTeX.
104
+
105
+ ### Code Blocks
106
+
107
+ Fenced with triple backticks, optional language tag for syntax highlighting. The `src:` directive includes external files inline:
108
+
109
+ ````
110
+ ```json src:./config.json lines:1-10
111
+ ```
112
+ ````
113
+
114
+ ### Mermaid Diagrams
115
+
116
+ Code blocks tagged `mermaid` render as SVG diagrams — flowcharts, sequence diagrams, class diagrams, state diagrams, and more.
117
+
118
+ ### Images
119
+
120
+ Markdown-style images with optional width and alignment:
121
+
122
+ ```
123
+ ![Photo](image.png =50% center)
124
+ ```
125
+
126
+ ### Tables
127
+
128
+ Pipe-delimited tables with optional `borderless` and `headerless` flags.
129
+
130
+ ### Lists
131
+
132
+ Bullet lists (`-`), numbered lists (`1.`), and task lists (`- [ ]` / `- [x]`). Items can have rich body content including nested lists, code blocks, and paragraphs.
133
+
134
+ ### References
135
+
136
+ Tag any section with `@id` and cross-reference it anywhere with `@id` — renders as a clickable link.
137
+
138
+ ### Slides
139
+
140
+ Turn any SDOC file into an HTML slide deck with themes, layouts (center, two-column), speaker notes, and PDF export.
141
+
142
+ ### Custom Styling
143
+
144
+ Per-folder `sdoc.config.json` or per-file `@meta` scope for custom CSS, headers, footers, and confidentiality banners. Configs cascade from workspace root to file.
145
+
80
146
  ## Learning the Format
81
147
 
82
148
  - `docs/guide/intro.sdoc` — what SDOC is and why
@@ -107,4 +173,4 @@ Release branches merge to both `main` and `develop`. No direct pushes to `main`
107
173
 
108
174
  ## License
109
175
 
110
- [MIT](LICENSE) — 2026 Irreversible Inc.
176
+ [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,14 @@
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^\}\` | Caution marker (amber)
185
+ \`\{!text!\}\` | Warning marker (orange)
186
+ \`\{-text-\}\` | Negative marker (red)
187
+ \`\{~text~\}\` | Highlight (yellow)
178
188
  }
179
189
 
180
190
  Links: \`[Link text](https://example.com)\`
@@ -192,6 +202,8 @@
192
202
  ```
193
203
 
194
204
  Autolinks: \`\<https://example.com\>\`
205
+
206
+ 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
207
  }
196
208
 
197
209
  # References @references
@@ -252,6 +264,19 @@
252
264
  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
265
  }
254
266
 
267
+ # Math Blocks @math-blocks
268
+ {
269
+ Code blocks with the \`math\` language tag are rendered as display equations via KaTeX:
270
+
271
+ ````
272
+ ```math
273
+ \\int_{-\\infty}^{\\infty} e^{-x^2} \\, dx = \\sqrt{\\pi}
274
+ ```
275
+ ````
276
+
277
+ 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.
278
+ }
279
+
255
280
  # Blockquotes @blockquotes
256
281
  {
257
282
  ```
@@ -268,6 +293,8 @@
268
293
  @meta {
269
294
  type: doc
270
295
 
296
+ sdoc-version: 0.1
297
+
271
298
  company: Irreversible Inc.
272
299
 
273
300
  confidential: true
@@ -305,6 +332,9 @@
305
332
  \`uuid: ...\` — unique identifier.
306
333
 
307
334
  \`tags: tag1, tag2\` — comma-separated tags.
335
+
336
+ \`sdoc-version: 0.1\` — SDOC format version. A parser warning is
337
+ emitted when this key is missing from \`@meta\`.
308
338
  }
309
339
 
310
340
  # About Scope @about-scope
@@ -324,9 +354,9 @@
324
354
  {
325
355
  Backslash escapes special characters: \`\\\\\` \`\\{\` \`\\}\` \`\\@\`
326
356
  \`\\[\` \`\\]\` \`\\(\` \`\\)\` \`\\*\` \`\\~\` \`\\#\` \`\\!\` \`\\\<\`
327
- \`\\\>\`
357
+ \`\\\>\` \`\\\$\` \`\\+\` \`\\=\` \`\\-\` \`\\^\`
328
358
 
329
- A line starting with \`\\#\` renders as a literal \`#\` (not a heading).
359
+ A line starting with \`\\#\` renders as a literal \`#\` (not a heading). Use \`\\\$\` to prevent a dollar sign from starting math mode.
330
360
  }
331
361
  }
332
362
 
@@ -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,26 @@ 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
+ - Caution marker: `{^text^}` (amber highlight)
458
+ - Warning marker: `{!text!}` (orange highlight)
459
+ - Negative marker: `{-text-}` (red highlight)
460
+ - Highlight: `{~text~}` (yellow highlight)
451
461
  }
462
+
463
+ 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
464
  }
453
465
 
454
466
  # Escaping @escaping
455
467
  {
456
- In normal text (including headings and paragraphs), a backslash escapes: `\\` `\{` `\}` `\@` `\[` `\]` `\(` `\)` `\*` `\~` `\#` `\!` `\<` `\>` and `` \` ``.
468
+ In normal text (including headings and paragraphs), a backslash escapes: `\\` `\{` `\}` `\@` `\[` `\]` `\(` `\)` `\*` `\~` `\#` `\!` `\<` `\>` `\$` `\+` `\=` `\-` `\^` and `` \` ``.
457
469
 
458
470
  Escapes are processed before reference detection.
459
471
 
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 `>`).
472
+ 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
473
  }
462
474
 
463
475
  # Code Blocks @code-blocks
@@ -474,6 +486,8 @@ Content of Section B.
474
486
  - The opening and closing fences must be on their own lines
475
487
  - Anything inside is treated as raw text (no parsing, no escapes)
476
488
  - Optional language tag after the opening fence
489
+ - The special language tag `math` renders the block as a display equation via KaTeX (no copy button)
490
+ - The special language tag `mermaid` renders the block as an SVG diagram via the Mermaid library
477
491
  }
478
492
 
479
493
  # Include by Link @code-includes
@@ -570,13 +584,15 @@ Content of Section B.
570
584
  date: 2026-02-09
571
585
  version: 1.0
572
586
  status: Draft
587
+ sdoc-version: 0.1
573
588
  }
574
589
  ```
575
590
 
576
591
  {[.]
577
592
  - Key matching is case-insensitive
578
593
  - 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`
594
+ - Well-known keys: `style`, `styleappend`/`style-append`, `header`, `footer`, `sdoc-version`
595
+ - `sdoc-version` identifies the SDOC format version the document targets (current: `0.1`). A parser warning is emitted when this key is missing.
580
596
  - All other keys are stored as custom properties (e.g., `author`, `date`, `version`, `status`, `tags`)
581
597
  - Sub-scope syntax takes precedence: if both `# Style { path }` and `style: path` exist, the sub-scope value wins
582
598
  - Key:value and sub-scope syntax can be mixed freely in the same meta scope
@@ -773,7 +789,7 @@ Content of Section A.
773
789
  - Comment syntax (if any)
774
790
  - Duplicate ID resolution (error vs warning vs nearest-scope)
775
791
  - Additional list types (checkboxes, alpha, roman)
776
- - Additional inline formatting (underline, highlight)
792
+ - Additional inline formatting (underline)
777
793
  }
778
794
  }
779
795
  }
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.9",
5
+ "version": "0.1.11",
6
6
  "publisher": "entropicwarrior",
7
7
  "license": "MIT",
8
8
  "repository": {
@@ -113,6 +113,34 @@
113
113
  "configurationDefaults": {
114
114
  "[sdoc]": {
115
115
  "editor.wordWrap": "on"
116
+ },
117
+ "editor.tokenColorCustomizations": {
118
+ "textMateRules": [
119
+ {
120
+ "scope": "markup.quote.sdoc",
121
+ "settings": {
122
+ "foreground": "#3b82c3"
123
+ }
124
+ },
125
+ {
126
+ "scope": "markup.changed.sdoc",
127
+ "settings": {
128
+ "foreground": "#b08a1a"
129
+ }
130
+ },
131
+ {
132
+ "scope": "markup.keyword.warning.sdoc",
133
+ "settings": {
134
+ "foreground": "#d97706"
135
+ }
136
+ },
137
+ {
138
+ "scope": "markup.other.highlight.sdoc",
139
+ "settings": {
140
+ "foreground": "#a68a00"
141
+ }
142
+ }
143
+ ]
116
144
  }
117
145
  },
118
146
  "languageModelTools": [
@@ -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,22 @@ 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_caution":
143
+ case "mark_warning":
144
+ case "mark_negative":
145
+ case "mark_highlight":
146
+ result.push(...flattenInlineNodes(node.children, ann, href));
147
+ break;
131
148
  default:
132
149
  break;
133
150
  }
@@ -160,7 +177,7 @@ function extractImagesFromInline(text) {
160
177
  }
161
178
 
162
179
  const rt = enforceContentLimit(flattenInlineNodes(textNodes), RICH_TEXT_LIMIT);
163
- const hasText = rt.some((r) => r.text.content.trim() !== "");
180
+ const hasText = rt.some((r) => (r.text ? r.text.content.trim() !== "" : r.type === "equation"));
164
181
 
165
182
  const imageBlocks = [];
166
183
  for (const img of images) {
@@ -258,6 +275,9 @@ function renderNode(node, depth, nestLevel) {
258
275
  case "table":
259
276
  return renderTable(node);
260
277
  case "code":
278
+ if (node.lang === "math") {
279
+ return [{ type: "equation", equation: { expression: node.text || "" } }];
280
+ }
261
281
  return renderCode(node);
262
282
  case "blockquote":
263
283
  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,26 @@ 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_caution";
1209
+ else if (mc === "!") mt = "mark_warning";
1210
+ else if (mc === "-") mt = "mark_negative";
1211
+ else if (mc === "~") mt = "mark_highlight";
1212
+ if (mt) {
1213
+ const end = findUnescaped(text, i + 2, mc + "}");
1214
+ if (end !== -1) {
1215
+ flush();
1216
+ nodes.push({ type: mt, children: parseInline(text.slice(i + 2, end)) });
1217
+ i = end + 2;
1218
+ continue;
1219
+ }
1220
+ }
1221
+ }
1222
+
1157
1223
  if (ch === "*") {
1158
1224
  const end = findUnescaped(text, i + 1, "*");
1159
1225
  if (end !== -1) {
@@ -1296,6 +1362,18 @@ function renderInlineNodes(nodes) {
1296
1362
  return `<strong>${renderInlineNodes(node.children)}</strong>`;
1297
1363
  case "strike":
1298
1364
  return `<del>${renderInlineNodes(node.children)}</del>`;
1365
+ case "mark_positive":
1366
+ return `<span class="sdoc-mark sdoc-mark-positive">${renderInlineNodes(node.children)}</span>`;
1367
+ case "mark_neutral":
1368
+ return `<span class="sdoc-mark sdoc-mark-neutral">${renderInlineNodes(node.children)}</span>`;
1369
+ case "mark_caution":
1370
+ return `<span class="sdoc-mark sdoc-mark-caution">${renderInlineNodes(node.children)}</span>`;
1371
+ case "mark_warning":
1372
+ return `<span class="sdoc-mark sdoc-mark-warning">${renderInlineNodes(node.children)}</span>`;
1373
+ case "mark_negative":
1374
+ return `<span class="sdoc-mark sdoc-mark-negative">${renderInlineNodes(node.children)}</span>`;
1375
+ case "mark_highlight":
1376
+ return `<mark class="sdoc-mark sdoc-mark-highlight">${renderInlineNodes(node.children)}</mark>`;
1299
1377
  case "link":
1300
1378
  return `<a class="sdoc-link" href="${escapeAttr(node.href)}" target="_blank" rel="noopener noreferrer">${renderInlineNodes(
1301
1379
  node.children
@@ -1309,6 +1387,10 @@ function renderInlineNodes(nodes) {
1309
1387
  const imgStyle = imgParts.length ? ` style="${imgParts.join(";")}"` : "";
1310
1388
  return `<img class="sdoc-image" src="${escapeAttr(node.src)}" alt="${escapeAttr(node.alt)}"${imgStyle} />`;
1311
1389
  }
1390
+ case "math_inline":
1391
+ return `<span class="sdoc-math sdoc-math-inline">${renderKatex(node.value, false)}</span>`;
1392
+ case "math_display":
1393
+ return `<span class="sdoc-math sdoc-math-display">${renderKatex(node.value, true)}</span>`;
1312
1394
  default:
1313
1395
  return "";
1314
1396
  }
@@ -1452,6 +1534,9 @@ function renderNode(node, depth) {
1452
1534
  if (node.lang === "mermaid") {
1453
1535
  return `<pre class="mermaid"${dl}>${escapeHtml(node.text)}</pre>`;
1454
1536
  }
1537
+ if (node.lang === "math") {
1538
+ return `<div class="sdoc-math sdoc-math-block"${dl}>${renderKatex(node.text, true)}</div>`;
1539
+ }
1455
1540
  const langClass = node.lang ? ` class="language-${escapeAttr(node.lang)}"` : "";
1456
1541
  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
1542
  }
@@ -1484,7 +1569,7 @@ function extractMeta(nodes) {
1484
1569
  }
1485
1570
 
1486
1571
  if (metaIndex === -1) {
1487
- return { nodes, meta: {} };
1572
+ return { nodes, meta: {}, warnings: [] };
1488
1573
  }
1489
1574
 
1490
1575
  const metaNode = searchNodes[metaIndex];
@@ -1545,14 +1630,19 @@ function extractMeta(nodes) {
1545
1630
  meta.company = meta.properties.company || null;
1546
1631
  meta.confidential = meta.properties.confidential || null;
1547
1632
 
1633
+ const warnings = [];
1634
+ if (!meta.properties["sdoc-version"]) {
1635
+ warnings.push("Missing sdoc-version in @meta (current format version is " + SDOC_FORMAT_VERSION + ")");
1636
+ }
1637
+
1548
1638
  if (doc) {
1549
1639
  // @meta was inside the document scope — strip it from children
1550
1640
  const filteredChildren = doc.children.filter((_, index) => index !== metaIndex);
1551
1641
  const stripped = { ...doc, children: filteredChildren };
1552
- return { nodes: [stripped], meta };
1642
+ return { nodes: [stripped], meta, warnings };
1553
1643
  }
1554
1644
  const bodyNodes = nodes.filter((_, index) => index !== metaIndex);
1555
- return { nodes: bodyNodes, meta };
1645
+ return { nodes: bodyNodes, meta, warnings };
1556
1646
  }
1557
1647
 
1558
1648
  function collectParagraphText(nodes) {
@@ -1834,6 +1924,18 @@ const DEFAULT_STYLE = `
1834
1924
  padding: 0 0.2em;
1835
1925
  }
1836
1926
 
1927
+ .sdoc-mark {
1928
+ border-radius: 3px;
1929
+ padding: 0.05em 0.3em;
1930
+ font-weight: 500;
1931
+ }
1932
+ .sdoc-mark-positive { background-color: rgba(34, 139, 34, 0.15); color: #166016; }
1933
+ .sdoc-mark-neutral { background-color: rgba(59, 130, 195, 0.15); color: #245d8a; }
1934
+ .sdoc-mark-caution { background-color: rgba(200, 150, 30, 0.18); color: #7a5f0e; }
1935
+ .sdoc-mark-warning { background-color: rgba(255, 120, 0, 0.18); color: #b35400; }
1936
+ .sdoc-mark-negative { background-color: rgba(187, 50, 50, 0.15); color: #911e1e; }
1937
+ .sdoc-mark-highlight { background-color: rgba(255, 255, 0, 0.75); }
1938
+
1837
1939
  .sdoc-image {
1838
1940
  display: inline-block;
1839
1941
  max-width: 100%;
@@ -1929,10 +2031,12 @@ const PRINT_STYLE = `
1929
2031
  white-space: pre-wrap;
1930
2032
  word-wrap: break-word;
1931
2033
  }
2034
+ .sdoc-mark { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
1932
2035
  }
1933
2036
  `;
1934
2037
 
1935
2038
  const MERMAID_CDN = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js";
2039
+ const KATEX_CDN_CSS = "https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.css";
1936
2040
 
1937
2041
  function hasMermaidBlocks(nodes) {
1938
2042
  for (const node of nodes) {
@@ -1980,6 +2084,9 @@ function renderHtmlDocumentFromParsed(parsed, title, options = {}) {
1980
2084
  const mermaidScript = hasMermaidBlocks(parsed.nodes)
1981
2085
  ? `\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>`
1982
2086
  : "";
2087
+ const katexCssTag = body.includes('class="katex"')
2088
+ ? `\n<link rel="stylesheet" href="${KATEX_CDN_CSS}" />`
2089
+ : "";
1983
2090
 
1984
2091
  return `<!DOCTYPE html>
1985
2092
  <html lang="en">
@@ -1989,7 +2096,7 @@ function renderHtmlDocumentFromParsed(parsed, title, options = {}) {
1989
2096
  <title>${escapeHtml(title)}</title>
1990
2097
  <style>
1991
2098
  ${cssBase}${cssAppend}
1992
- </style>
2099
+ </style>${katexCssTag}
1993
2100
  </head>
1994
2101
  <body>
1995
2102
  <div class="sdoc-shell">
@@ -2236,6 +2343,7 @@ async function resolveIncludes(nodes, resolverFn) {
2236
2343
  }
2237
2344
 
2238
2345
  module.exports = {
2346
+ SDOC_FORMAT_VERSION,
2239
2347
  parseSdoc,
2240
2348
  extractMeta,
2241
2349
  resolveIncludes,
@@ -2251,6 +2359,7 @@ module.exports = {
2251
2359
  extractAbout,
2252
2360
  // Low-level helpers for custom renderers (e.g. slide-renderer)
2253
2361
  parseInline,
2362
+ renderKatex,
2254
2363
  escapeHtml,
2255
2364
  escapeAttr
2256
2365
  };
@@ -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}