@entropicwarrior/sdoc 0.2.0 → 0.2.2

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
@@ -87,20 +87,26 @@ All `.sdoc` files are designed for progressive disclosure — read the `@about`
87
87
  {
88
88
  Unlimited nesting. Each scope is independently addressable.
89
89
 
90
- {[code lang=python]
91
- def hello():
92
- print("Hello from SDOC")
93
- }
90
+ ```python
91
+ def hello():
92
+ print("Hello from SDOC")
93
+ ```
94
94
  }
95
95
 
96
- # A List
96
+ # Status :example
97
97
  {
98
- {[.]
99
- - First item
100
- - Second item with **bold**
101
- - Third item
98
+ {[table 60% center]
99
+ Endpoint | Status
100
+ /v2/api | {+Active+}
101
+ /v1/api | {-Deprecated-}
102
102
  }
103
103
  }
104
+
105
+ # Internal Notes :comment
106
+ {
107
+ This scope is invisible in rendered output but
108
+ stays in the AST for tooling and agents.
109
+ }
104
110
  }
105
111
  ```
106
112
 
@@ -153,7 +159,7 @@ Markdown-style images with optional width and alignment:
153
159
 
154
160
  ### Tables
155
161
 
156
- Pipe-delimited tables with optional `borderless` and `headerless` flags.
162
+ Pipe-delimited tables with optional flags for appearance (`borderless`, `headerless`), width (`auto`, `60%`, `400px`), and alignment (`left`, `center`, `right`). All flags compose freely.
157
163
 
158
164
  ### Lists
159
165
 
@@ -167,6 +173,18 @@ Tag any section with `@id` and cross-reference it anywhere with `@id` — render
167
173
 
168
174
  Turn any SDOC file into an HTML slide deck with themes, layouts (center, two-column), speaker notes, and PDF export.
169
175
 
176
+ ### Scope Types
177
+
178
+ Classify scopes with a `:type` annotation — `:schema`, `:warning`, `:deprecated`, `:example`, or any custom label. Types render as `data-scope-type` attributes and CSS classes for styling.
179
+
180
+ ### Data Blocks
181
+
182
+ Tag a JSON code fence with `:data` and the parser validates and stores the parsed result on the AST node. `extractDataBlocks()` gives programmatic access. Ideal for embedding schemas, configs, and structured metadata alongside prose.
183
+
184
+ ### Comment Scopes
185
+
186
+ A `:comment` scope is excluded from rendered output but stays in the AST — perfect for agent instructions, internal notes, and build metadata that readers shouldn't see.
187
+
170
188
  ### Custom Styling
171
189
 
172
190
  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.
@@ -286,7 +286,7 @@ Content of Section B.
286
286
  \`\{~text~\}\` | Highlight (yellow)
287
287
  }
288
288
 
289
- Links: \`[Link text](https://example.com)\`
289
+ Links: \`[Link text](https://example.com)\` or \`[Other doc](./other-file.sdoc)\`. Relative paths resolve from the document's directory.
290
290
 
291
291
  Images: \`![Alt text](path/to/image.png)\`
292
292
 
@@ -671,5 +671,20 @@ Content of Section B.
671
671
  }
672
672
  ```
673
673
  }
674
+
675
+ # @References Inside Link Labels @refs-in-link-labels
676
+ {
677
+ Inline \`@references\` are parsed everywhere, including inside link labels. If you mention a scope ID in a link label, escape the \`@\` to prevent it being treated as a reference to the current document:
678
+
679
+ **Wrong:** \`[See domain-model.sdoc @my-section](./domain-model.sdoc#my-section)\`
680
+
681
+ The \`@my-section\` is parsed as a reference and flagged as broken (it does not exist in *this* file).
682
+
683
+ **Right:** \`[See domain-model.sdoc \\@my-section](./domain-model.sdoc#my-section)\`
684
+
685
+ Or simply omit the \`@\` from the label — the URL fragment already carries the target:
686
+
687
+ **Also right:** \`[See domain-model.sdoc § my-section](./domain-model.sdoc#my-section)\`
688
+ }
674
689
  }
675
690
  }
@@ -395,16 +395,26 @@ Content of Section B.
395
395
  - A reference is `@id` in text (unescaped)
396
396
  - References link to the scope with that ID
397
397
  - ID uniqueness is strongly recommended; tooling may warn on duplicates
398
+ - References are parsed inside link labels — use `\@` to include a literal `@` in a link label without triggering a reference
398
399
  }
399
400
  }
400
401
 
401
- # External Links @external-links
402
+ # Links @links
402
403
  {
403
- Markdown-style links:
404
+ Markdown-style links with absolute URLs or relative file paths:
404
405
 
405
406
  ```
406
407
  [label](https://example.com)
408
+ [other doc](./other-file.sdoc)
409
+ [parent doc](../guide/intro.sdoc)
407
410
  ```
411
+
412
+ {[.]
413
+ - Absolute URLs (any scheme) open externally
414
+ - Relative paths are resolved from the document's directory
415
+ - Fragments (`./file.sdoc#section`) and query strings are stripped for file resolution
416
+ - Tooling may warn on broken relative links (target file does not exist)
417
+ }
408
418
  }
409
419
 
410
420
  # Autolinks @autolinks
package/package.json CHANGED
@@ -2,12 +2,12 @@
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.2.0",
5
+ "version": "0.2.2",
6
6
  "publisher": "entropicwarrior",
7
7
  "license": "MIT",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/entropicwarrior/sdoc"
10
+ "url": "git+https://github.com/entropicwarrior/sdoc.git"
11
11
  },
12
12
  "homepage": "https://github.com/entropicwarrior/sdoc",
13
13
  "bugs": {
@@ -22,7 +22,7 @@
22
22
  "ai-agent"
23
23
  ],
24
24
  "bin": {
25
- "sdoc-sync-notion": "./tools/sync-notion.js"
25
+ "sdoc-sync-notion": "tools/sync-notion.js"
26
26
  },
27
27
  "exports": {
28
28
  ".": "./index.js",
package/src/sdoc.js CHANGED
@@ -1379,6 +1379,9 @@ function renderInlineNodes(nodes) {
1379
1379
  return escapeHtml(node.value);
1380
1380
  case "ref": {
1381
1381
  const href = `#${escapeAttr(node.id)}`;
1382
+ if (_renderOptions.brokenRefIds && _renderOptions.brokenRefIds.has(node.id)) {
1383
+ return `<a class="sdoc-ref sdoc-broken-ref" href="${href}"><span class="sdoc-broken-icon">\u26A0</span>@${escapeHtml(node.id)}</a>`;
1384
+ }
1382
1385
  return `<a class="sdoc-ref" href="${href}">@${escapeHtml(node.id)}</a>`;
1383
1386
  }
1384
1387
  case "code":
@@ -1404,6 +1407,11 @@ function renderInlineNodes(nodes) {
1404
1407
  case "mark_highlight":
1405
1408
  return `<mark class="sdoc-mark sdoc-mark-highlight">${renderInlineNodes(node.children)}</mark>`;
1406
1409
  case "link":
1410
+ if (_renderOptions.brokenLinkHrefs && _renderOptions.brokenLinkHrefs.has(node.href)) {
1411
+ return `<a class="sdoc-link sdoc-broken-link" href="${escapeAttr(node.href)}" target="_blank" rel="noopener noreferrer"><span class="sdoc-broken-icon">\u26A0</span>${renderInlineNodes(
1412
+ node.children
1413
+ )}</a>`;
1414
+ }
1407
1415
  return `<a class="sdoc-link" href="${escapeAttr(node.href)}" target="_blank" rel="noopener noreferrer">${renderInlineNodes(
1408
1416
  node.children
1409
1417
  )}</a>`;
@@ -1981,6 +1989,19 @@ const DEFAULT_STYLE = `
1981
1989
  .sdoc-mark-negative { background-color: rgba(210, 25, 25, 0.18); color: #a81414; }
1982
1990
  .sdoc-mark-highlight { background-color: rgba(255, 255, 0, 0.75); }
1983
1991
 
1992
+ .sdoc-broken-ref, .sdoc-link.sdoc-broken-link {
1993
+ color: #c33;
1994
+ text-decoration: wavy underline #c33;
1995
+ text-underline-offset: 2px;
1996
+ background: rgba(204, 51, 51, 0.08);
1997
+ border-radius: 2px;
1998
+ padding: 0 0.15em;
1999
+ }
2000
+ .sdoc-broken-icon {
2001
+ font-size: 0.75em;
2002
+ margin-right: 0.15em;
2003
+ }
2004
+
1984
2005
  .sdoc-image {
1985
2006
  display: inline-block;
1986
2007
  max-width: 100%;
@@ -2093,11 +2114,44 @@ const PRINT_STYLE = `
2093
2114
  word-wrap: break-word;
2094
2115
  }
2095
2116
  .sdoc-mark { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
2117
+ .sdoc-table { break-inside: avoid; }
2118
+ .sdoc-code { break-inside: avoid; }
2119
+ .sdoc-blockquote { break-inside: avoid; }
2096
2120
  }
2097
2121
  `;
2098
2122
 
2099
2123
  const MERMAID_CDN = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js";
2100
2124
  const KATEX_CDN_CSS = "https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.css";
2125
+ const HLJS_CDN = "https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11.11.1/highlight.min.js";
2126
+
2127
+ // Custom highlight.js grammar for SDOC language – registered before highlightElement calls
2128
+ const HLJS_SDOC_GRAMMAR = `hljs.registerLanguage('sdoc',function(hljs){return{name:'SDOC',aliases:['sdoc'],contains:[
2129
+ hljs.COMMENT('^\\\\s*//','$'),
2130
+ {className:'section',begin:'^\\\\s*#+\\\\s+',end:'$',contains:[
2131
+ {className:'symbol',begin:'@[A-Za-z_][A-Za-z0-9_-]*'}
2132
+ ]},
2133
+ {className:'meta',begin:'^\\\\s*@[A-Za-z_][A-Za-z0-9_-]*',end:'(?=\\\\s*\\\\{|$)'},
2134
+ {className:'code',begin:'\`\`\`',end:'\`\`\`',contains:[hljs.BACKSLASH_ESCAPE]},
2135
+ {className:'code',begin:'\`[^\`]+\`'},
2136
+ {className:'string',begin:'\\\\[',end:'\\\\]\\\\([^)]*\\\\)',contains:[
2137
+ {className:'link',begin:'\\\\(',end:'\\\\)'}
2138
+ ]},
2139
+ {className:'keyword',begin:'\\\\{[!?+\\\\-=~^]',end:'[!?+\\\\-=~^]\\\\}'},
2140
+ {className:'keyword',begin:'\\\\{\\\\[(?:\\\\.|#|\\\\d+|table)\\\\]'},
2141
+ {className:'strong',begin:'\\\\*\\\\*',end:'\\\\*\\\\*'},
2142
+ {className:'emphasis',begin:'(?<!\\\\*)\\\\*(?!\\\\*)',end:'\\\\*(?!\\\\*)'},
2143
+ {className:'deletion',begin:'~~',end:'~~'},
2144
+ {className:'bullet',begin:'^\\\\s*[-]\\\\s'},
2145
+ {className:'bullet',begin:'^\\\\s*\\\\d+[.)]\\\\ '},
2146
+ {className:'quote',begin:'^\\\\s*>',end:'$'},
2147
+ {className:'symbol',begin:'(?<!\\\\\\\\)@[A-Za-z_][A-Za-z0-9_-]*'},
2148
+ {className:'attr',begin:'[A-Za-z_][A-Za-z0-9_-]*(?=\\\\s*:)',end:':',excludeEnd:true}
2149
+ ]}});`;
2150
+
2151
+ // Highlight.js GitHub light theme (inline so it works inside shadow DOM in the web viewer)
2152
+ const HLJS_LIGHT_CSS = "pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}";
2153
+ // Highlight.js GitHub dark theme color overrides (used in @media and VS Code dark-mode CSS)
2154
+ const HLJS_DARK_COLORS_CSS = ".hljs{color:#c9d1d9;background:transparent}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}";
2101
2155
 
2102
2156
  function hasMermaidBlocks(nodes) {
2103
2157
  for (const node of nodes) {
@@ -2112,9 +2166,21 @@ function hasMermaidBlocks(nodes) {
2112
2166
  return false;
2113
2167
  }
2114
2168
 
2115
- function renderHtmlDocumentFromParsed(parsed, title, options = {}) {
2116
- _renderOptions = options.renderOptions ?? {};
2117
- const body = parsed.nodes
2169
+ function hasHighlightableCodeBlocks(nodes) {
2170
+ for (const node of nodes) {
2171
+ if (node.type === "code" && node.lang && node.lang !== "mermaid" && node.lang !== "math") return true;
2172
+ if (node.children && hasHighlightableCodeBlocks(node.children)) return true;
2173
+ if (node.items) {
2174
+ for (const item of node.items) {
2175
+ if (item.children && hasHighlightableCodeBlocks(item.children)) return true;
2176
+ }
2177
+ }
2178
+ }
2179
+ return false;
2180
+ }
2181
+
2182
+ function renderBodyNodes(nodes) {
2183
+ return nodes
2118
2184
  .map((node, index) => {
2119
2185
  if (node.type === "scope" && index === 0) {
2120
2186
  return renderScope(node, 1, true);
@@ -2122,6 +2188,17 @@ function renderHtmlDocumentFromParsed(parsed, title, options = {}) {
2122
2188
  return renderNode(node, 1);
2123
2189
  })
2124
2190
  .join("\n");
2191
+ }
2192
+
2193
+ function renderHtmlBody(text) {
2194
+ const parsed = parseSdoc(text);
2195
+ const metaResult = extractMeta(parsed.nodes);
2196
+ return renderBodyNodes(metaResult.nodes);
2197
+ }
2198
+
2199
+ function renderHtmlDocumentFromParsed(parsed, title, options = {}) {
2200
+ _renderOptions = options.renderOptions ?? {};
2201
+ const body = renderBodyNodes(parsed.nodes);
2125
2202
  _renderOptions = {};
2126
2203
  const errorHtml = renderErrors(parsed.errors);
2127
2204
 
@@ -2152,6 +2229,15 @@ function renderHtmlDocumentFromParsed(parsed, title, options = {}) {
2152
2229
  const katexCssTag = body.includes('class="katex"')
2153
2230
  ? `\n<link rel="stylesheet" href="${KATEX_CDN_CSS}" />`
2154
2231
  : "";
2232
+ const hasHljs = hasHighlightableCodeBlocks(parsed.nodes);
2233
+ // Highlight.js CSS is inlined (not a <link>) so it is extracted by parseDocHtml in the web viewer
2234
+ // and applied inside shadow DOM. The @media query handles dark mode in browsers.
2235
+ const hljsCssInline = hasHljs
2236
+ ? `\n${HLJS_LIGHT_CSS}\n.sdoc-code code.hljs{padding:0;background:transparent}\n@media (prefers-color-scheme:dark){${HLJS_DARK_COLORS_CSS}}`
2237
+ : "";
2238
+ const hljsScript = hasHljs
2239
+ ? `\n<script src="${HLJS_CDN}"></script>\n<script>${HLJS_SDOC_GRAMMAR}\ndocument.querySelectorAll('pre.sdoc-code code[class*="language-"]').forEach(function(b){hljs.highlightElement(b);});</script>`
2240
+ : "";
2155
2241
 
2156
2242
  return `<!DOCTYPE html>
2157
2243
  <html lang="en">
@@ -2160,7 +2246,7 @@ function renderHtmlDocumentFromParsed(parsed, title, options = {}) {
2160
2246
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
2161
2247
  <title>${escapeHtml(title)}</title>
2162
2248
  <style>
2163
- ${cssBase}${cssAppend}
2249
+ ${cssBase}${cssAppend}${hljsCssInline}
2164
2250
  </style>${katexCssTag}
2165
2251
  </head>
2166
2252
  <body>
@@ -2174,7 +2260,7 @@ ${cssBase}${cssAppend}
2174
2260
  </main>
2175
2261
  </div>
2176
2262
  ${footerContent ? `<footer class="sdoc-page-footer">${footerContent}</footer>` : ""}
2177
- </div>${scriptTag}${mermaidScript}
2263
+ </div>${scriptTag}${mermaidScript}${hljsScript}
2178
2264
  </body>
2179
2265
  </html>`;
2180
2266
  }
@@ -2430,6 +2516,127 @@ function extractAbout(nodes) {
2430
2516
  return null;
2431
2517
  }
2432
2518
 
2519
+ function collectAllIds(nodes) {
2520
+ const ids = new Set();
2521
+ function walk(nodeList) {
2522
+ for (const node of nodeList) {
2523
+ if (node.type === "scope") {
2524
+ if (node.id) ids.add(node.id);
2525
+ if (node.title) ids.add(slugify(node.title));
2526
+ }
2527
+ if (node.children) walk(node.children);
2528
+ if (node.type === "list" && node.items) {
2529
+ walk(node.items);
2530
+ }
2531
+ }
2532
+ }
2533
+ walk(nodes);
2534
+ return ids;
2535
+ }
2536
+
2537
+ function collectInlineRefs(nodes) {
2538
+ const refs = [];
2539
+ const links = [];
2540
+
2541
+ function walkInlineNodes(inlineNodes, lineStart, lineEnd) {
2542
+ for (const node of inlineNodes) {
2543
+ if (node.type === "ref") {
2544
+ refs.push({ id: node.id, lineStart, lineEnd });
2545
+ } else if (node.type === "link") {
2546
+ links.push({ href: node.href, lineStart, lineEnd });
2547
+ }
2548
+ if (node.children) {
2549
+ walkInlineNodes(node.children, lineStart, lineEnd);
2550
+ }
2551
+ }
2552
+ }
2553
+
2554
+ function processText(text, lineStart, lineEnd) {
2555
+ const inlineNodes = parseInline(text);
2556
+ walkInlineNodes(inlineNodes, lineStart, lineEnd);
2557
+ }
2558
+
2559
+ function walk(nodeList) {
2560
+ for (const node of nodeList) {
2561
+ if (node.type === "paragraph" && node.text) {
2562
+ processText(node.text, node.lineStart, node.lineEnd);
2563
+ } else if (node.type === "blockquote" && node.paragraphs) {
2564
+ for (const para of node.paragraphs) {
2565
+ processText(para, node.lineStart, node.lineEnd);
2566
+ }
2567
+ } else if (node.type === "scope") {
2568
+ if (node.title) {
2569
+ processText(node.title, node.lineStart, node.lineStart);
2570
+ }
2571
+ if (node.children) walk(node.children);
2572
+ } else if (node.type === "list" && node.items) {
2573
+ walk(node.items);
2574
+ } else if (node.type === "table") {
2575
+ if (node.headers) {
2576
+ for (const cell of node.headers) {
2577
+ processText(cell, node.lineStart, node.lineEnd);
2578
+ }
2579
+ }
2580
+ if (node.rows) {
2581
+ for (const row of node.rows) {
2582
+ for (const cell of row) {
2583
+ processText(cell, node.lineStart, node.lineEnd);
2584
+ }
2585
+ }
2586
+ }
2587
+ }
2588
+ // Handle list items (no type field, but have title/children)
2589
+ if (!node.type) {
2590
+ if (node.title) processText(node.title, node.lineStart || 0, node.lineEnd || node.lineStart || 0);
2591
+ if (node.children) walk(node.children);
2592
+ }
2593
+ }
2594
+ }
2595
+ walk(nodes);
2596
+ return { refs, links };
2597
+ }
2598
+
2599
+ function validateRefs(nodes, options = {}) {
2600
+ const ids = collectAllIds(nodes);
2601
+ const externalIds = options.externalIds || new Set();
2602
+ const { refs, links } = collectInlineRefs(nodes);
2603
+ const warnings = [];
2604
+
2605
+ for (const ref of refs) {
2606
+ if (!ids.has(ref.id) && !externalIds.has(ref.id)) {
2607
+ warnings.push({
2608
+ type: "broken-ref",
2609
+ id: ref.id,
2610
+ message: `Broken reference: @${ref.id} does not match any scope ID or title`,
2611
+ lineStart: ref.lineStart,
2612
+ lineEnd: ref.lineEnd
2613
+ });
2614
+ }
2615
+ }
2616
+
2617
+ for (const link of links) {
2618
+ const href = link.href;
2619
+ if (/^https?:\/\//i.test(href) || /^mailto:/i.test(href) || href.startsWith("#") || href.startsWith("data:")) {
2620
+ continue;
2621
+ }
2622
+ if (options.resolveFilePath) {
2623
+ const filePath = href.split("#")[0].split("?")[0];
2624
+ if (!filePath) continue;
2625
+ if (!options.resolveFilePath(filePath)) {
2626
+ warnings.push({
2627
+ type: "broken-link",
2628
+ href,
2629
+ message: `Broken link: file not found — ${href}`,
2630
+ lineStart: link.lineStart,
2631
+ lineEnd: link.lineEnd
2632
+ });
2633
+ }
2634
+ }
2635
+ }
2636
+
2637
+ return warnings;
2638
+ }
2639
+
2433
2640
  async function resolveIncludes(nodes, resolverFn) {
2434
2641
  for (const node of nodes) {
2435
2642
  if (node.type === "code" && node.src) {
@@ -2466,6 +2673,7 @@ module.exports = {
2466
2673
  resolveIncludes,
2467
2674
  renderFragment,
2468
2675
  renderTextParagraphs,
2676
+ renderHtmlBody,
2469
2677
  renderHtmlDocumentFromParsed,
2470
2678
  renderHtmlDocument,
2471
2679
  formatSdoc,
@@ -2476,6 +2684,10 @@ module.exports = {
2476
2684
  extractAbout,
2477
2685
  extractDataBlocks,
2478
2686
  KNOWN_SCOPE_TYPES,
2687
+ // Validation
2688
+ collectAllIds,
2689
+ collectInlineRefs,
2690
+ validateRefs,
2479
2691
  // Low-level helpers for custom renderers (e.g. slide-renderer)
2480
2692
  parseInline,
2481
2693
  renderKatex,
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ // SDOC → HTML fragment translator
3
+ //
4
+ // Usage:
5
+ // node tools/sdoc2html input.sdoc # read from file
6
+ // cat input.sdoc | node tools/sdoc2html # read from stdin
7
+ //
8
+ // Writes an HTML fragment to stdout (no <!DOCTYPE>, no <html>/<body> wrapper).
9
+ // Designed for integration with github/markup as a translator script.
10
+
11
+ const fs = require("fs");
12
+ const { renderHtmlBody } = require("../src/sdoc");
13
+
14
+ function main() {
15
+ let input;
16
+ const filename = process.argv[2];
17
+
18
+ if (filename) {
19
+ try {
20
+ input = fs.readFileSync(filename, "utf8");
21
+ } catch (err) {
22
+ process.stderr.write(`sdoc2html: ${err.message}\n`);
23
+ process.exit(1);
24
+ }
25
+ } else {
26
+ input = fs.readFileSync(0, "utf8");
27
+ }
28
+
29
+ process.stdout.write(renderHtmlBody(input) + "\n");
30
+ }
31
+
32
+ main();