@entropicwarrior/sdoc 0.1.10 → 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 +77 -0
- package/lexica/sdoc-authoring.sdoc +3 -2
- package/lexica/specification.sdoc +18 -3
- package/package.json +42 -3
- package/src/notion-renderer.js +1 -0
- package/src/sdoc.js +11 -3
- 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
|
|
@@ -77,6 +88,72 @@ Or use the **SDOC: Browse Documents** command from the VS Code Command Palette.
|
|
|
77
88
|
}
|
|
78
89
|
```
|
|
79
90
|
|
|
91
|
+
## Features
|
|
92
|
+
|
|
93
|
+
### Inline Formatting
|
|
94
|
+
|
|
95
|
+
`*emphasis*`, `**strong**`, `~~strikethrough~~`, `` `inline code` ``, `[links](url)`, and `<https://autolinks>`.
|
|
96
|
+
|
|
97
|
+
### Semantic Markers
|
|
98
|
+
|
|
99
|
+
Annotate text with semantic meaning that renders as colored highlights:
|
|
100
|
+
|
|
101
|
+
| Syntax | Meaning | Color |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `{+text+}` | Positive | Green |
|
|
104
|
+
| `{=text=}` | Neutral | Blue |
|
|
105
|
+
| `{^text^}` | Caution | Amber |
|
|
106
|
+
| `{!text!}` | Warning | Orange |
|
|
107
|
+
| `{-text-}` | Negative | Red |
|
|
108
|
+
| `{~text~}` | Highlight | Yellow |
|
|
109
|
+
|
|
110
|
+
Markers nest with other inline formatting: `{+**all checks** passed+}`.
|
|
111
|
+
|
|
112
|
+
### Math
|
|
113
|
+
|
|
114
|
+
Inline math with `$x^2 + y^2$`, display math with `$$E = mc^2$$`, and multi-line equations with ` ```math ` code fences. Rendered via KaTeX.
|
|
115
|
+
|
|
116
|
+
### Code Blocks
|
|
117
|
+
|
|
118
|
+
Fenced with triple backticks, optional language tag for syntax highlighting. The `src:` directive includes external files inline:
|
|
119
|
+
|
|
120
|
+
````
|
|
121
|
+
```json src:./config.json lines:1-10
|
|
122
|
+
```
|
|
123
|
+
````
|
|
124
|
+
|
|
125
|
+
### Mermaid Diagrams
|
|
126
|
+
|
|
127
|
+
Code blocks tagged `mermaid` render as SVG diagrams — flowcharts, sequence diagrams, class diagrams, state diagrams, and more.
|
|
128
|
+
|
|
129
|
+
### Images
|
|
130
|
+
|
|
131
|
+
Markdown-style images with optional width and alignment:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+

|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Tables
|
|
138
|
+
|
|
139
|
+
Pipe-delimited tables with optional `borderless` and `headerless` flags.
|
|
140
|
+
|
|
141
|
+
### Lists
|
|
142
|
+
|
|
143
|
+
Bullet lists (`-`), numbered lists (`1.`), and task lists (`- [ ]` / `- [x]`). Items can have rich body content including nested lists, code blocks, and paragraphs.
|
|
144
|
+
|
|
145
|
+
### References
|
|
146
|
+
|
|
147
|
+
Tag any section with `@id` and cross-reference it anywhere with `@id` — renders as a clickable link.
|
|
148
|
+
|
|
149
|
+
### Slides
|
|
150
|
+
|
|
151
|
+
Turn any SDOC file into an HTML slide deck with themes, layouts (center, two-column), speaker notes, and PDF export.
|
|
152
|
+
|
|
153
|
+
### Custom Styling
|
|
154
|
+
|
|
155
|
+
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.
|
|
156
|
+
|
|
80
157
|
## Learning the Format
|
|
81
158
|
|
|
82
159
|
- `docs/guide/intro.sdoc` — what SDOC is and why
|
|
@@ -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
|
-
\`\{
|
|
184
|
+
\`\{^text^\}\` | Caution marker (amber)
|
|
185
|
+
\`\{!text!\}\` | Warning marker (orange)
|
|
185
186
|
\`\{-text-\}\` | Negative marker (red)
|
|
186
187
|
\`\{~text~\}\` | Highlight (yellow)
|
|
187
188
|
}
|
|
@@ -353,7 +354,7 @@
|
|
|
353
354
|
{
|
|
354
355
|
Backslash escapes special characters: \`\\\\\` \`\\{\` \`\\}\` \`\\@\`
|
|
355
356
|
\`\\[\` \`\\]\` \`\\(\` \`\\)\` \`\\*\` \`\\~\` \`\\#\` \`\\!\` \`\\\<\`
|
|
356
|
-
\`\\\>\` \`\\\$\` \`\\+\` \`\\=\` \`\\-\`
|
|
357
|
+
\`\\\>\` \`\\\$\` \`\\+\` \`\\=\` \`\\-\` \`\\^\`
|
|
357
358
|
|
|
358
359
|
A line starting with \`\\#\` renders as a literal \`#\` (not a heading). Use \`\\\$\` to prevent a dollar sign from starting math mode.
|
|
359
360
|
}
|
|
@@ -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
|
+
- Caution marker: `{^text^}` (amber highlight)
|
|
458
|
+
- Warning marker: `{!text!}` (orange highlight)
|
|
458
459
|
- Negative marker: `{-text-}` (red highlight)
|
|
459
460
|
- Highlight: `{~text~}` (yellow highlight)
|
|
460
461
|
}
|
|
@@ -464,7 +465,7 @@ Content of Section B.
|
|
|
464
465
|
|
|
465
466
|
# Escaping @escaping
|
|
466
467
|
{
|
|
467
|
-
In normal text (including headings and paragraphs), a backslash escapes: `\\` `\{` `\}` `\@` `\[` `\]` `\(` `\)` `\*` `\~` `\#` `\!` `\<` `\>` `\$` `\+` `\=` `\-` and `` \` ``.
|
|
468
|
+
In normal text (including headings and paragraphs), a backslash escapes: `\\` `\{` `\}` `\@` `\[` `\]` `\(` `\)` `\*` `\~` `\#` `\!` `\<` `\>` `\$` `\+` `\=` `\-` `\^` and `` \` ``.
|
|
468
469
|
|
|
469
470
|
Escapes are processed before reference detection.
|
|
470
471
|
|
|
@@ -605,6 +606,20 @@ Content of Section B.
|
|
|
605
606
|
{
|
|
606
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.
|
|
607
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
|
+
|
|
608
623
|
# Collapsible Scopes @collapsible-scopes
|
|
609
624
|
{
|
|
610
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.
|
|
@@ -788,7 +803,7 @@ Content of Section A.
|
|
|
788
803
|
- Comment syntax (if any)
|
|
789
804
|
- Duplicate ID resolution (error vs warning vs nearest-scope)
|
|
790
805
|
- Additional list types (checkboxes, alpha, roman)
|
|
791
|
-
- Additional inline formatting (underline
|
|
806
|
+
- Additional inline formatting (underline)
|
|
792
807
|
}
|
|
793
808
|
}
|
|
794
809
|
}
|
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",
|
|
@@ -113,6 +119,34 @@
|
|
|
113
119
|
"configurationDefaults": {
|
|
114
120
|
"[sdoc]": {
|
|
115
121
|
"editor.wordWrap": "on"
|
|
122
|
+
},
|
|
123
|
+
"editor.tokenColorCustomizations": {
|
|
124
|
+
"textMateRules": [
|
|
125
|
+
{
|
|
126
|
+
"scope": "markup.quote.sdoc",
|
|
127
|
+
"settings": {
|
|
128
|
+
"foreground": "#3b82c3"
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"scope": "markup.changed.sdoc",
|
|
133
|
+
"settings": {
|
|
134
|
+
"foreground": "#b08a1a"
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"scope": "markup.keyword.warning.sdoc",
|
|
139
|
+
"settings": {
|
|
140
|
+
"foreground": "#d97706"
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"scope": "markup.other.highlight.sdoc",
|
|
145
|
+
"settings": {
|
|
146
|
+
"foreground": "#a68a00"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
]
|
|
116
150
|
}
|
|
117
151
|
},
|
|
118
152
|
"languageModelTools": [
|
|
@@ -143,12 +177,17 @@
|
|
|
143
177
|
},
|
|
144
178
|
{
|
|
145
179
|
"command": "sdoc.exportHtml",
|
|
146
|
-
"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",
|
|
147
186
|
"group": "navigation"
|
|
148
187
|
},
|
|
149
188
|
{
|
|
150
189
|
"command": "sdoc.openInBrowser",
|
|
151
|
-
"when": "activeWebviewPanelId == 'sdoc.preview'",
|
|
190
|
+
"when": "activeWebviewPanelId == 'sdoc.preview' || editorLangId == sdoc",
|
|
152
191
|
"group": "navigation"
|
|
153
192
|
}
|
|
154
193
|
],
|
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,6 +1205,7 @@ 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
1209
|
else if (mc === "!") mt = "mark_warning";
|
|
1209
1210
|
else if (mc === "-") mt = "mark_negative";
|
|
1210
1211
|
else if (mc === "~") mt = "mark_highlight";
|
|
@@ -1365,6 +1366,8 @@ function renderInlineNodes(nodes) {
|
|
|
1365
1366
|
return `<span class="sdoc-mark sdoc-mark-positive">${renderInlineNodes(node.children)}</span>`;
|
|
1366
1367
|
case "mark_neutral":
|
|
1367
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>`;
|
|
1368
1371
|
case "mark_warning":
|
|
1369
1372
|
return `<span class="sdoc-mark sdoc-mark-warning">${renderInlineNodes(node.children)}</span>`;
|
|
1370
1373
|
case "mark_negative":
|
|
@@ -1928,7 +1931,8 @@ const DEFAULT_STYLE = `
|
|
|
1928
1931
|
}
|
|
1929
1932
|
.sdoc-mark-positive { background-color: rgba(34, 139, 34, 0.15); color: #166016; }
|
|
1930
1933
|
.sdoc-mark-neutral { background-color: rgba(59, 130, 195, 0.15); color: #245d8a; }
|
|
1931
|
-
.sdoc-mark-
|
|
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; }
|
|
1932
1936
|
.sdoc-mark-negative { background-color: rgba(187, 50, 50, 0.15); color: #911e1e; }
|
|
1933
1937
|
.sdoc-mark-highlight { background-color: rgba(255, 255, 0, 0.75); }
|
|
1934
1938
|
|
|
@@ -2077,8 +2081,12 @@ function renderHtmlDocumentFromParsed(parsed, title, options = {}) {
|
|
|
2077
2081
|
const cssBase = options.cssOverride ?? DEFAULT_STYLE;
|
|
2078
2082
|
const cssAppend = options.cssAppend ? `\n${options.cssAppend}\n${PRINT_STYLE}` : `\n${PRINT_STYLE}`;
|
|
2079
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; }"});`;
|
|
2080
2088
|
const mermaidScript = hasMermaidBlocks(parsed.nodes)
|
|
2081
|
-
? `\n<script src="${MERMAID_CDN}"></script>\n<script
|
|
2089
|
+
? `\n<script src="${MERMAID_CDN}"></script>\n<script>${mermaidInit}</script>`
|
|
2082
2090
|
: "";
|
|
2083
2091
|
const katexCssTag = body.includes('class="katex"')
|
|
2084
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
|
+
});
|