@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 +13 -1
- package/lexica/sdoc-authoring.sdoc +2 -1
- package/lexica/specification.sdoc +16 -1
- package/package.json +20 -3
- package/src/notion-renderer.js +1 -0
- package/src/sdoc.js +16 -8
- 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
|
|
@@ -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^}` |
|
|
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^\}\` |
|
|
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
|
-
-
|
|
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.
|
|
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
|
],
|
package/src/notion-renderer.js
CHANGED
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 = "
|
|
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
|
|
1933
|
-
.sdoc-mark-neutral
|
|
1934
|
-
.sdoc-mark-
|
|
1935
|
-
.sdoc-mark-
|
|
1936
|
-
.sdoc-mark-
|
|
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
|
|
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
|
|
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
|
+
});
|