@entropicwarrior/sdoc 0.1.11 → 0.1.12
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 +11 -0
- package/lexica/specification.sdoc +14 -0
- package/package.json +14 -3
- package/src/sdoc.js +5 -1
- package/src/slide-pdf.js +28 -8
- package/src/slide-renderer.js +22 -3
- package/tools/build-doc.js +214 -0
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
|
|
@@ -606,6 +606,20 @@ Content of Section B.
|
|
|
606
606
|
{
|
|
607
607
|
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
608
|
|
|
609
|
+
# Theme Support @theme-support
|
|
610
|
+
{
|
|
611
|
+
The preview automatically follows the user's VSCode colour theme. Light, dark, and high-contrast themes are all supported.
|
|
612
|
+
|
|
613
|
+
{[.]
|
|
614
|
+
- 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
|
|
615
|
+
- Dark mode overrides the \`--sdoc-*\` CSS custom properties and hardcoded \`rgba()\` colours (table headers, code blocks, confidential notices, error blocks)
|
|
616
|
+
- Mermaid diagrams use the \`dark\` theme when in a dark colour theme and \`neutral\` otherwise
|
|
617
|
+
- Theme changes take effect immediately (CSS-driven) and trigger a full preview rebuild (for Mermaid re-initialisation)
|
|
618
|
+
- Exported HTML follows the user's OS colour preference via \`@media (prefers-color-scheme: dark)\`. Print output always uses light theme colours
|
|
619
|
+
- Custom stylesheets (\`sdoc.config.json\` or \`@meta style\`) that use \`--sdoc-*\` variables inherit dark values automatically. Hardcoded colours in custom styles are not overridden
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
609
623
|
# Collapsible Scopes @collapsible-scopes
|
|
610
624
|
{
|
|
611
625
|
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.
|
|
5
|
+
"version": "0.1.12",
|
|
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",
|
|
@@ -171,12 +177,17 @@
|
|
|
171
177
|
},
|
|
172
178
|
{
|
|
173
179
|
"command": "sdoc.exportHtml",
|
|
174
|
-
"when": "activeWebviewPanelId == 'sdoc.preview'",
|
|
180
|
+
"when": "activeWebviewPanelId == 'sdoc.preview' || editorLangId == sdoc",
|
|
181
|
+
"group": "navigation"
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"command": "sdoc.exportPdf",
|
|
185
|
+
"when": "activeWebviewPanelId == 'sdoc.preview' || editorLangId == sdoc",
|
|
175
186
|
"group": "navigation"
|
|
176
187
|
},
|
|
177
188
|
{
|
|
178
189
|
"command": "sdoc.openInBrowser",
|
|
179
|
-
"when": "activeWebviewPanelId == 'sdoc.preview'",
|
|
190
|
+
"when": "activeWebviewPanelId == 'sdoc.preview' || editorLangId == sdoc",
|
|
180
191
|
"group": "navigation"
|
|
181
192
|
}
|
|
182
193
|
],
|
package/src/sdoc.js
CHANGED
|
@@ -2081,8 +2081,12 @@ function renderHtmlDocumentFromParsed(parsed, title, options = {}) {
|
|
|
2081
2081
|
const cssBase = options.cssOverride ?? DEFAULT_STYLE;
|
|
2082
2082
|
const cssAppend = options.cssAppend ? `\n${options.cssAppend}\n${PRINT_STYLE}` : `\n${PRINT_STYLE}`;
|
|
2083
2083
|
const scriptTag = options.script ? `\n<script>${options.script}</script>` : "";
|
|
2084
|
+
const mermaidTheme = options.mermaidTheme ?? "neutral";
|
|
2085
|
+
const mermaidInit = mermaidTheme === "auto"
|
|
2086
|
+
? `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; }"});`
|
|
2087
|
+
: `mermaid.initialize({startOnLoad:true,theme:"${mermaidTheme}",themeCSS:".node rect, .node polygon, .node circle { rx: 4; ry: 4; }"});`;
|
|
2084
2088
|
const mermaidScript = hasMermaidBlocks(parsed.nodes)
|
|
2085
|
-
? `\n<script src="${MERMAID_CDN}"></script>\n<script
|
|
2089
|
+
? `\n<script src="${MERMAID_CDN}"></script>\n<script>${mermaidInit}</script>`
|
|
2086
2090
|
: "";
|
|
2087
2091
|
const katexCssTag = body.includes('class="katex"')
|
|
2088
2092
|
? `\n<link rel="stylesheet" href="${KATEX_CDN_CSS}" />`
|
package/src/slide-pdf.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// SDOC
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
82
|
-
"--print-to-pdf-paper-height=
|
|
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
|
-
|
|
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 };
|
package/src/slide-renderer.js
CHANGED
|
@@ -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
|
|
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:"
|
|
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
|
+
});
|