@entropicwarrior/sdoc 0.1.11 → 0.1.13

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
@@ -20,6 +20,8 @@ A plain-text documentation format designed for AI-agent efficiency. Explicit bra
20
20
 
21
21
  **A document site builder** — serve a folder of SDOC files as a browsable site with sidebar navigation, search, and split-pane comparison.
22
22
 
23
+ **PDF and HTML export** — export any document to A4 PDF via headless Chrome, or to standalone HTML. Available as both a CLI tool and a VS Code command.
24
+
23
25
  **A VS Code extension** — live preview, sticky scroll, code folding, document symbols, mermaid rendering, and commands for all the above.
24
26
 
25
27
  ## Quick Start
@@ -41,6 +43,15 @@ node tools/build-slides.js deck.sdoc -o slides.html
41
43
 
42
44
  Each top-level scope becomes a slide. Set `type: slides` in `@meta`. See `lexica/slide-authoring.sdoc` for the full authoring guide.
43
45
 
46
+ ### Export to PDF or HTML
47
+
48
+ ```bash
49
+ node tools/build-doc.js doc.sdoc # PDF (requires Chrome)
50
+ node tools/build-doc.js doc.sdoc --html -o doc.html # HTML
51
+ ```
52
+
53
+ Or use **SDOC: Export PDF** / **SDOC: Export HTML** from the VS Code Command Palette.
54
+
44
55
  ### Browse documents
45
56
 
46
57
  ```bash
@@ -91,7 +102,8 @@ Annotate text with semantic meaning that renders as colored highlights:
91
102
  |---|---|---|
92
103
  | `{+text+}` | Positive | Green |
93
104
  | `{=text=}` | Neutral | Blue |
94
- | `{^text^}` | Caution | Amber |
105
+ | `{^text^}` | Note | Amber |
106
+ | `{?text?}` | Caution | Dark amber |
95
107
  | `{!text!}` | Warning | Orange |
96
108
  | `{-text-}` | Negative | Red |
97
109
  | `{~text~}` | Highlight | Yellow |
@@ -181,7 +181,8 @@
181
181
  \`\$\$E = mc^2\$\$\` | Display math (centered)
182
182
  \`\{+text+\}\` | Positive marker (green)
183
183
  \`\{=text=\}\` | Neutral marker (blue)
184
- \`\{^text^\}\` | Caution marker (amber)
184
+ \`\{^text^\}\` | Note marker (amber)
185
+ \`\{?text?\}\` | Caution marker (dark amber)
185
186
  \`\{!text!\}\` | Warning marker (orange)
186
187
  \`\{-text-\}\` | Negative marker (red)
187
188
  \`\{~text~\}\` | Highlight (yellow)
@@ -454,7 +454,8 @@ Content of Section B.
454
454
  - Display math: `$$E = mc^2$$` (centered block)
455
455
  - Positive marker: `{+text+}` (green highlight)
456
456
  - Neutral marker: `{=text=}` (blue highlight)
457
- - Caution marker: `{^text^}` (amber highlight)
457
+ - Note marker: `{^text^}` (amber highlight)
458
+ - Caution marker: `{?text?}` (dark amber highlight)
458
459
  - Warning marker: `{!text!}` (orange highlight)
459
460
  - Negative marker: `{-text-}` (red highlight)
460
461
  - Highlight: `{~text~}` (yellow highlight)
@@ -606,6 +607,20 @@ Content of Section B.
606
607
  {
607
608
  The VSCode extension provides an interactive preview with the following features. These are preview-only behaviours and do not affect the SDOC format or static HTML export.
608
609
 
610
+ # Theme Support @theme-support
611
+ {
612
+ The preview automatically follows the user's VSCode colour theme. Light, dark, and high-contrast themes are all supported.
613
+
614
+ {[.]
615
+ - Theme detection is CSS-driven: VSCode adds \`vscode-dark\`, \`vscode-light\`, \`vscode-high-contrast\`, or \`vscode-high-contrast-light\` classes to the webview body element
616
+ - Dark mode overrides the \`--sdoc-*\` CSS custom properties and hardcoded \`rgba()\` colours (table headers, code blocks, confidential notices, error blocks)
617
+ - Mermaid diagrams use the \`dark\` theme when in a dark colour theme and \`neutral\` otherwise
618
+ - Theme changes take effect immediately (CSS-driven) and trigger a full preview rebuild (for Mermaid re-initialisation)
619
+ - Exported HTML follows the user's OS colour preference via \`@media (prefers-color-scheme: dark)\`. Print output always uses light theme colours
620
+ - Custom stylesheets (\`sdoc.config.json\` or \`@meta style\`) that use \`--sdoc-*\` variables inherit dark values automatically. Hardcoded colours in custom styles are not overridden
621
+ }
622
+ }
623
+
609
624
  # Collapsible Scopes @collapsible-scopes
610
625
  {
611
626
  Scope headings that have children display a toggle triangle, visible on hover. Clicking the triangle collapses the scope's children (hides the content below the heading). Clicking again expands them.
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.11",
5
+ "version": "0.1.13",
6
6
  "publisher": "entropicwarrior",
7
7
  "license": "MIT",
8
8
  "repository": {
@@ -42,6 +42,7 @@
42
42
  "onCommand:sdoc.preview",
43
43
  "onCommand:sdoc.previewToSide",
44
44
  "onCommand:sdoc.exportHtml",
45
+ "onCommand:sdoc.exportPdf",
45
46
  "onCommand:sdoc.openInBrowser",
46
47
  "onCommand:sdoc.browseDocs",
47
48
  "onCommand:sdoc.newKnowledgeFile",
@@ -87,6 +88,11 @@
87
88
  "title": "SDOC: Export HTML",
88
89
  "icon": "$(desktop-download)"
89
90
  },
91
+ {
92
+ "command": "sdoc.exportPdf",
93
+ "title": "SDOC: Export PDF",
94
+ "icon": "$(file-pdf)"
95
+ },
90
96
  {
91
97
  "command": "sdoc.openInBrowser",
92
98
  "title": "SDOC: Open in Browser",
@@ -128,6 +134,12 @@
128
134
  "foreground": "#b08a1a"
129
135
  }
130
136
  },
137
+ {
138
+ "scope": "markup.keyword.caution.sdoc",
139
+ "settings": {
140
+ "foreground": "#c06a00"
141
+ }
142
+ },
131
143
  {
132
144
  "scope": "markup.keyword.warning.sdoc",
133
145
  "settings": {
@@ -171,12 +183,17 @@
171
183
  },
172
184
  {
173
185
  "command": "sdoc.exportHtml",
174
- "when": "activeWebviewPanelId == 'sdoc.preview'",
186
+ "when": "activeWebviewPanelId == 'sdoc.preview' || editorLangId == sdoc",
187
+ "group": "navigation"
188
+ },
189
+ {
190
+ "command": "sdoc.exportPdf",
191
+ "when": "activeWebviewPanelId == 'sdoc.preview' || editorLangId == sdoc",
175
192
  "group": "navigation"
176
193
  },
177
194
  {
178
195
  "command": "sdoc.openInBrowser",
179
- "when": "activeWebviewPanelId == 'sdoc.preview'",
196
+ "when": "activeWebviewPanelId == 'sdoc.preview' || editorLangId == sdoc",
180
197
  "group": "navigation"
181
198
  }
182
199
  ],
@@ -139,6 +139,7 @@ function flattenInlineNodes(nodes, annotations, href) {
139
139
  break;
140
140
  case "mark_positive":
141
141
  case "mark_neutral":
142
+ case "mark_note":
142
143
  case "mark_caution":
143
144
  case "mark_warning":
144
145
  case "mark_negative":
package/src/sdoc.js CHANGED
@@ -8,7 +8,7 @@ const COMMAND_LIST_NUMBER = "{[#]";
8
8
  const COMMAND_TABLE = "{[table]";
9
9
  const COMMAND_CODE_FENCE = "```";
10
10
 
11
- const ESCAPABLE = new Set(["\\", "{", "}", "@", "[", "]", "(", ")", "*", "`", "#", "!", "~", "<", ">", "$", "+", "=", "-", "^"]);
11
+ const ESCAPABLE = new Set(["\\", "{", "}", "@", "[", "]", "(", ")", "*", "`", "#", "!", "~", "<", ">", "$", "+", "=", "-", "^", "?"]);
12
12
 
13
13
  let _katex = null;
14
14
  let _katexLoaded = false;
@@ -1205,7 +1205,8 @@ function parseInline(text) {
1205
1205
  let mt = null;
1206
1206
  if (mc === "+") mt = "mark_positive";
1207
1207
  else if (mc === "=") mt = "mark_neutral";
1208
- else if (mc === "^") mt = "mark_caution";
1208
+ else if (mc === "^") mt = "mark_note";
1209
+ else if (mc === "?") mt = "mark_caution";
1209
1210
  else if (mc === "!") mt = "mark_warning";
1210
1211
  else if (mc === "-") mt = "mark_negative";
1211
1212
  else if (mc === "~") mt = "mark_highlight";
@@ -1366,6 +1367,8 @@ function renderInlineNodes(nodes) {
1366
1367
  return `<span class="sdoc-mark sdoc-mark-positive">${renderInlineNodes(node.children)}</span>`;
1367
1368
  case "mark_neutral":
1368
1369
  return `<span class="sdoc-mark sdoc-mark-neutral">${renderInlineNodes(node.children)}</span>`;
1370
+ case "mark_note":
1371
+ return `<span class="sdoc-mark sdoc-mark-note">${renderInlineNodes(node.children)}</span>`;
1369
1372
  case "mark_caution":
1370
1373
  return `<span class="sdoc-mark sdoc-mark-caution">${renderInlineNodes(node.children)}</span>`;
1371
1374
  case "mark_warning":
@@ -1929,11 +1932,12 @@ const DEFAULT_STYLE = `
1929
1932
  padding: 0.05em 0.3em;
1930
1933
  font-weight: 500;
1931
1934
  }
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; }
1935
+ .sdoc-mark-positive { background-color: rgba(34, 139, 34, 0.15); color: #166016; }
1936
+ .sdoc-mark-neutral { background-color: rgba(59, 130, 195, 0.15); color: #245d8a; }
1937
+ .sdoc-mark-note { background-color: rgba(190, 170, 0, 0.15); color: #6b5d00; }
1938
+ .sdoc-mark-caution { background-color: rgba(217, 130, 10, 0.18); color: #8a5000; }
1939
+ .sdoc-mark-warning { background-color: rgba(245, 115, 0, 0.20); color: #c05600; }
1940
+ .sdoc-mark-negative { background-color: rgba(210, 25, 25, 0.18); color: #a81414; }
1937
1941
  .sdoc-mark-highlight { background-color: rgba(255, 255, 0, 0.75); }
1938
1942
 
1939
1943
  .sdoc-image {
@@ -2081,8 +2085,12 @@ function renderHtmlDocumentFromParsed(parsed, title, options = {}) {
2081
2085
  const cssBase = options.cssOverride ?? DEFAULT_STYLE;
2082
2086
  const cssAppend = options.cssAppend ? `\n${options.cssAppend}\n${PRINT_STYLE}` : `\n${PRINT_STYLE}`;
2083
2087
  const scriptTag = options.script ? `\n<script>${options.script}</script>` : "";
2088
+ const mermaidTheme = options.mermaidTheme ?? "neutral";
2089
+ const mermaidInit = mermaidTheme === "auto"
2090
+ ? `var isDark=window.matchMedia("(prefers-color-scheme:dark)").matches;mermaid.initialize({startOnLoad:true,theme:isDark?"dark":"neutral",themeCSS:".node rect, .node polygon, .node circle { rx: 4; ry: 4; }"});`
2091
+ : `mermaid.initialize({startOnLoad:true,theme:"${mermaidTheme}",themeCSS:".node rect, .node polygon, .node circle { rx: 4; ry: 4; }"});`;
2084
2092
  const mermaidScript = hasMermaidBlocks(parsed.nodes)
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>`
2093
+ ? `\n<script src="${MERMAID_CDN}"></script>\n<script>${mermaidInit}</script>`
2086
2094
  : "";
2087
2095
  const katexCssTag = body.includes('class="katex"')
2088
2096
  ? `\n<link rel="stylesheet" href="${KATEX_CDN_CSS}" />`
package/src/slide-pdf.js CHANGED
@@ -1,4 +1,4 @@
1
- // SDOC Slides — PDF export via headless Chrome.
1
+ // SDOC — PDF export via headless Chrome.
2
2
  // Zero dependencies: uses child_process to shell out to the system Chrome/Chromium.
3
3
 
4
4
  const { execFile } = require("child_process");
@@ -51,7 +51,11 @@ function findChrome() {
51
51
  return null;
52
52
  }
53
53
 
54
- function exportPdf(htmlPath, pdfPath) {
54
+ // Generic PDF export. Options:
55
+ // paperWidth — inches (default 8.27 = A4)
56
+ // paperHeight — inches (default 11.69 = A4)
57
+ // noHeaderFooter — suppress Chrome header/footer (default true)
58
+ function chromePdf(htmlPath, pdfPath, options = {}) {
55
59
  return new Promise((resolve, reject) => {
56
60
  const chrome = findChrome();
57
61
  if (!chrome) {
@@ -69,20 +73,26 @@ function exportPdf(htmlPath, pdfPath) {
69
73
  // the browser process by default, which corrupts PDF output).
70
74
  const tmpProfile = fs.mkdtempSync(path.join(os.tmpdir(), "sdoc-chrome-"));
71
75
 
72
- // 13.333 x 7.5 inches = 16:9 landscape (standard presentation aspect ratio)
76
+ const paperWidth = options.paperWidth ?? 8.27;
77
+ const paperHeight = options.paperHeight ?? 11.69;
78
+
73
79
  const args = [
74
80
  "--headless=new",
75
81
  "--disable-gpu",
76
82
  "--no-first-run",
77
83
  "--no-default-browser-check",
78
84
  "--user-data-dir=" + tmpProfile,
79
- "--no-pdf-header-footer",
80
85
  "--print-to-pdf=" + resolvedPdf,
81
- "--print-to-pdf-paper-width=13.333",
82
- "--print-to-pdf-paper-height=7.5",
83
- fileUrl,
86
+ "--print-to-pdf-paper-width=" + paperWidth,
87
+ "--print-to-pdf-paper-height=" + paperHeight,
84
88
  ];
85
89
 
90
+ if (options.noHeaderFooter !== false) {
91
+ args.push("--no-pdf-header-footer");
92
+ }
93
+
94
+ args.push(fileUrl);
95
+
86
96
  execFile(chrome, args, { timeout: 30000 }, (err, _stdout, stderr) => {
87
97
  // Clean up temp profile
88
98
  fs.rm(tmpProfile, { recursive: true, force: true }, () => {});
@@ -96,4 +106,14 @@ function exportPdf(htmlPath, pdfPath) {
96
106
  });
97
107
  }
98
108
 
99
- module.exports = { findChrome, exportPdf };
109
+ // Slide PDF: 16:9 landscape (13.333 x 7.5 inches)
110
+ function exportSlidePdf(htmlPath, pdfPath) {
111
+ return chromePdf(htmlPath, pdfPath, { paperWidth: 13.333, paperHeight: 7.5 });
112
+ }
113
+
114
+ // Document PDF: A4 portrait (8.27 x 11.69 inches)
115
+ function exportDocPdf(htmlPath, pdfPath) {
116
+ return chromePdf(htmlPath, pdfPath);
117
+ }
118
+
119
+ module.exports = { findChrome, exportSlidePdf, exportDocPdf, chromePdf };
@@ -243,7 +243,8 @@ function renderSlides(nodes, options = {}) {
243
243
  const {
244
244
  meta = {},
245
245
  themeCss = "",
246
- themeJs = ""
246
+ themeJs = "",
247
+ darkMode = false
247
248
  } = options;
248
249
 
249
250
  // The nodes from extractMeta have @meta already stripped.
@@ -325,11 +326,29 @@ function renderSlides(nodes, options = {}) {
325
326
  .notes { display: none; }
326
327
  }`;
327
328
 
328
- const cssTag = `<style>\n${structuralCss}\n${themeCss}\n</style>`;
329
+ const darkCss = darkMode ? `
330
+ /* Dark mode overrides */
331
+ body { background: #1e1e1e; color: #d4d4d4; }
332
+ h1, h2 { color: #e0e0e0; }
333
+ h3 { color: #b0b0b0; }
334
+ p, li { color: #b0b0b0; }
335
+ th { color: #9d9d9d; }
336
+ th, td { border-bottom-color: rgba(255, 255, 255, 0.1); }
337
+ pre { background: rgba(255, 255, 255, 0.06); border-color: rgba(255, 255, 255, 0.1); }
338
+ p code, li code { background: rgba(255, 255, 255, 0.08); }
339
+ blockquote { border-left-color: #5b9bd5; color: #9d9d9d; }
340
+ blockquote p { color: #9d9d9d; }
341
+ .nav-prev, .nav-next { color: rgba(255, 255, 255, 0.7); }
342
+ .sdoc-company-footer { color: rgba(255, 255, 255, 0.35); }
343
+ .sdoc-confidential-notice { color: rgba(235, 120, 120, 0.7); }
344
+ ` : "";
345
+
346
+ const cssTag = `<style>\n${structuralCss}\n${themeCss}\n${darkCss}</style>`;
329
347
  const jsTag = themeJs ? `<script>\n${themeJs}\n</script>` : "";
330
348
  const mermaidCdn = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js";
349
+ const mermaidTheme = darkMode ? "dark" : "neutral";
331
350
  const mermaidTag = slidesHtml.includes('class="mermaid"')
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>`
351
+ ? `\n<script src="${mermaidCdn}"></script>\n<script>mermaid.initialize({startOnLoad:true,theme:"${mermaidTheme}",themeCSS:".node rect, .node polygon, .node circle { rx: 4; ry: 4; }"});</script>`
333
352
  : "";
334
353
  const katexCssCdn = "https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.css";
335
354
  const katexTag = slidesHtml.includes('class="katex"')
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env node
2
+ // SDOC Document — CLI tool for HTML and PDF export
3
+ //
4
+ // Usage:
5
+ // node tools/build-doc.js input.sdoc [-o output] [--html]
6
+ //
7
+ // Default output is PDF (requires Chrome/Chromium).
8
+ // Use --html for HTML-only output (no Chrome needed).
9
+ // If -o is omitted, writes to input.pdf (or input.html with --html).
10
+
11
+ const fs = require("fs");
12
+ const path = require("path");
13
+ const os = require("os");
14
+ const { parseSdoc, extractMeta, resolveIncludes, renderHtmlDocumentFromParsed } = require("../src/sdoc");
15
+
16
+ const CONFIG_FILENAME = "sdoc.config.json";
17
+
18
+ function usage() {
19
+ console.error("Usage: build-doc <input.sdoc> [-o output] [--html]");
20
+ process.exit(1);
21
+ }
22
+
23
+ function readJson(filePath) {
24
+ try {
25
+ const raw = fs.readFileSync(filePath, "utf8");
26
+ return JSON.parse(raw);
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ function loadCss(filePath) {
33
+ if (!filePath) return null;
34
+ try {
35
+ return fs.readFileSync(filePath, "utf8");
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function resolvePath(baseDir, target) {
42
+ if (!target) return "";
43
+ if (path.isAbsolute(target)) return target;
44
+ return path.join(baseDir, target);
45
+ }
46
+
47
+ function mergeConfig(target, config, baseDir) {
48
+ if (!config || typeof config !== "object") return;
49
+ if (typeof config.style === "string") {
50
+ target.style = resolvePath(baseDir, config.style);
51
+ }
52
+ if (config.styleAppend) {
53
+ const list = Array.isArray(config.styleAppend) ? config.styleAppend : [config.styleAppend];
54
+ for (const item of list) {
55
+ if (typeof item === "string") {
56
+ target.styleAppend.push(resolvePath(baseDir, item));
57
+ }
58
+ }
59
+ }
60
+ if (typeof config.header === "string") target.header = config.header;
61
+ if (typeof config.footer === "string") target.footer = config.footer;
62
+ }
63
+
64
+ function loadConfigForFile(filePath) {
65
+ const startDir = path.dirname(path.resolve(filePath));
66
+ const root = path.parse(startDir).root;
67
+ const chain = [];
68
+ let current = startDir;
69
+
70
+ while (current) {
71
+ const configPath = path.join(current, CONFIG_FILENAME);
72
+ if (fs.existsSync(configPath)) {
73
+ const parsed = readJson(configPath);
74
+ if (parsed) chain.push({ dir: current, config: parsed });
75
+ }
76
+ if (current === root) break;
77
+ const parent = path.dirname(current);
78
+ if (parent === current) break;
79
+ current = parent;
80
+ }
81
+
82
+ const merged = { style: null, styleAppend: [], header: "", footer: "" };
83
+ for (const entry of chain.reverse()) {
84
+ mergeConfig(merged, entry.config, entry.dir);
85
+ }
86
+ return merged;
87
+ }
88
+
89
+ function resolveMetaStyles(meta, documentPath) {
90
+ const docDir = documentPath ? path.dirname(documentPath) : "";
91
+ const result = { styleCss: null, styleAppendCss: null };
92
+ if (meta && meta.stylePath) {
93
+ result.styleCss = loadCss(resolvePath(docDir, meta.stylePath));
94
+ }
95
+ if (meta && meta.styleAppendPath) {
96
+ result.styleAppendCss = loadCss(resolvePath(docDir, meta.styleAppendPath));
97
+ }
98
+ return result;
99
+ }
100
+
101
+ async function buildHtml(filePath) {
102
+ const resolvedPath = path.resolve(filePath);
103
+ const text = fs.readFileSync(resolvedPath, "utf8");
104
+ const parsed = parseSdoc(text);
105
+
106
+ if (parsed.errors.length > 0) {
107
+ for (const error of parsed.errors) {
108
+ console.error(`Warning: line ${error.line}: ${error.message}`);
109
+ }
110
+ }
111
+
112
+ const metaResult = extractMeta(parsed.nodes);
113
+ const config = loadConfigForFile(resolvedPath);
114
+ const metaStyles = resolveMetaStyles(metaResult.meta, resolvedPath);
115
+
116
+ const docDir = path.dirname(resolvedPath);
117
+ await resolveIncludes(metaResult.nodes, (src) => {
118
+ const resolved = resolvePath(docDir, src);
119
+ return fs.readFileSync(resolved, "utf8");
120
+ });
121
+
122
+ const cssOverride = metaStyles.styleCss ?? loadCss(config.style);
123
+ const cssAppendParts = [];
124
+ if (config.styleAppend && config.styleAppend.length) {
125
+ for (const stylePath of config.styleAppend) {
126
+ const css = loadCss(stylePath);
127
+ if (css) cssAppendParts.push(css);
128
+ }
129
+ }
130
+ if (metaStyles.styleAppendCss) {
131
+ cssAppendParts.push(metaStyles.styleAppendCss);
132
+ }
133
+
134
+ const title = path.basename(resolvedPath, ".sdoc");
135
+
136
+ return renderHtmlDocumentFromParsed(
137
+ { nodes: metaResult.nodes, errors: parsed.errors },
138
+ title,
139
+ {
140
+ meta: metaResult.meta,
141
+ config,
142
+ cssOverride: cssOverride || undefined,
143
+ cssAppend: cssAppendParts.join("\n") || undefined,
144
+ }
145
+ );
146
+ }
147
+
148
+ async function main() {
149
+ const args = process.argv.slice(2);
150
+ let inputPath = null;
151
+ let outputPath = null;
152
+ let htmlMode = false;
153
+
154
+ for (let i = 0; i < args.length; i++) {
155
+ if (args[i] === "-o" && i + 1 < args.length) {
156
+ outputPath = args[++i];
157
+ } else if (args[i] === "--html") {
158
+ htmlMode = true;
159
+ } else if (args[i] === "--help" || args[i] === "-h") {
160
+ usage();
161
+ } else if (!inputPath) {
162
+ inputPath = args[i];
163
+ } else {
164
+ console.error(`Unknown argument: ${args[i]}`);
165
+ usage();
166
+ }
167
+ }
168
+
169
+ if (!inputPath) usage();
170
+
171
+ const resolvedInput = path.resolve(inputPath);
172
+ if (!fs.existsSync(resolvedInput)) {
173
+ console.error(`File not found: ${resolvedInput}`);
174
+ process.exit(1);
175
+ }
176
+
177
+ const html = await buildHtml(resolvedInput);
178
+
179
+ if (htmlMode) {
180
+ if (!outputPath) {
181
+ outputPath = resolvedInput.replace(/\.sdoc$/i, "") + ".html";
182
+ }
183
+ const resolvedOutput = path.resolve(outputPath);
184
+ fs.mkdirSync(path.dirname(resolvedOutput), { recursive: true });
185
+ fs.writeFileSync(resolvedOutput, html, "utf-8");
186
+ console.log(`HTML: ${resolvedOutput}`);
187
+ } else {
188
+ const { exportDocPdf } = require("../src/slide-pdf");
189
+
190
+ if (!outputPath) {
191
+ outputPath = resolvedInput.replace(/\.sdoc$/i, "") + ".pdf";
192
+ }
193
+ const resolvedOutput = path.resolve(outputPath);
194
+
195
+ // Write HTML to temp file for Chrome
196
+ const tmpHtml = path.join(os.tmpdir(), "sdoc-doc-" + Date.now() + ".html");
197
+ fs.writeFileSync(tmpHtml, html, "utf-8");
198
+
199
+ try {
200
+ await exportDocPdf(tmpHtml, resolvedOutput);
201
+ console.log(`PDF: ${resolvedOutput}`);
202
+ } catch (err) {
203
+ console.error(err.message);
204
+ process.exit(1);
205
+ } finally {
206
+ try { fs.unlinkSync(tmpHtml); } catch {}
207
+ }
208
+ }
209
+ }
210
+
211
+ main().catch((err) => {
212
+ console.error(err.message);
213
+ process.exit(1);
214
+ });