@entropicwarrior/sdoc 0.1.3 → 0.1.4
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/package.json +3 -1
- package/src/slide-pdf.js +99 -0
- package/src/slide-renderer.js +344 -0
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.4",
|
|
6
6
|
"publisher": "entropicwarrior",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
],
|
|
24
24
|
"exports": {
|
|
25
25
|
".": "./index.js",
|
|
26
|
+
"./slides": "./src/slide-renderer.js",
|
|
27
|
+
"./slide-pdf": "./src/slide-pdf.js",
|
|
26
28
|
"./notion": "./src/notion-renderer.js"
|
|
27
29
|
},
|
|
28
30
|
"engines": {
|
package/src/slide-pdf.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// SDOC Slides — PDF export via headless Chrome.
|
|
2
|
+
// Zero dependencies: uses child_process to shell out to the system Chrome/Chromium.
|
|
3
|
+
|
|
4
|
+
const { execFile } = require("child_process");
|
|
5
|
+
const { execFileSync } = require("child_process");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
|
|
10
|
+
const CHROME_PATHS = {
|
|
11
|
+
darwin: [
|
|
12
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
13
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
14
|
+
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
|
|
15
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
16
|
+
],
|
|
17
|
+
linux: [
|
|
18
|
+
"google-chrome",
|
|
19
|
+
"google-chrome-stable",
|
|
20
|
+
"chromium",
|
|
21
|
+
"chromium-browser",
|
|
22
|
+
],
|
|
23
|
+
win32: [
|
|
24
|
+
path.join(process.env.PROGRAMFILES || "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
25
|
+
path.join(process.env["PROGRAMFILES(X86)"] || "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
26
|
+
path.join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function findChrome() {
|
|
31
|
+
if (process.env.CHROME_PATH) {
|
|
32
|
+
return process.env.CHROME_PATH;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const platform = os.platform();
|
|
36
|
+
const candidates = CHROME_PATHS[platform] || [];
|
|
37
|
+
|
|
38
|
+
for (const candidate of candidates) {
|
|
39
|
+
if (path.isAbsolute(candidate)) {
|
|
40
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
41
|
+
} else {
|
|
42
|
+
try {
|
|
43
|
+
const result = execFileSync("which", [candidate], { encoding: "utf-8" }).trim();
|
|
44
|
+
if (result) return result;
|
|
45
|
+
} catch {
|
|
46
|
+
// not found, try next
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function exportPdf(htmlPath, pdfPath) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const chrome = findChrome();
|
|
57
|
+
if (!chrome) {
|
|
58
|
+
reject(new Error(
|
|
59
|
+
"Chrome/Chromium not found. Install Google Chrome or set CHROME_PATH environment variable."
|
|
60
|
+
));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const fileUrl = "file://" + path.resolve(htmlPath);
|
|
65
|
+
const resolvedPdf = path.resolve(pdfPath);
|
|
66
|
+
|
|
67
|
+
// Use a temporary user-data-dir so headless Chrome doesn't conflict
|
|
68
|
+
// with any running browser session (Chrome 112+ new headless shares
|
|
69
|
+
// the browser process by default, which corrupts PDF output).
|
|
70
|
+
const tmpProfile = fs.mkdtempSync(path.join(os.tmpdir(), "sdoc-chrome-"));
|
|
71
|
+
|
|
72
|
+
// 13.333 x 7.5 inches = 16:9 landscape (standard presentation aspect ratio)
|
|
73
|
+
const args = [
|
|
74
|
+
"--headless=new",
|
|
75
|
+
"--disable-gpu",
|
|
76
|
+
"--no-first-run",
|
|
77
|
+
"--no-default-browser-check",
|
|
78
|
+
"--user-data-dir=" + tmpProfile,
|
|
79
|
+
"--no-pdf-header-footer",
|
|
80
|
+
"--print-to-pdf=" + resolvedPdf,
|
|
81
|
+
"--print-to-pdf-paper-width=13.333",
|
|
82
|
+
"--print-to-pdf-paper-height=7.5",
|
|
83
|
+
fileUrl,
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
execFile(chrome, args, { timeout: 30000 }, (err, _stdout, stderr) => {
|
|
87
|
+
// Clean up temp profile
|
|
88
|
+
fs.rm(tmpProfile, { recursive: true, force: true }, () => {});
|
|
89
|
+
|
|
90
|
+
if (err) {
|
|
91
|
+
reject(new Error(`Chrome PDF export failed: ${err.message}\n${stderr}`));
|
|
92
|
+
} else {
|
|
93
|
+
resolve(resolvedPdf);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { findChrome, exportPdf };
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// SDOC Slide Renderer — converts parsed SDOC AST to an HTML slide deck.
|
|
2
|
+
//
|
|
3
|
+
// Usage:
|
|
4
|
+
// const { parseSdoc, extractMeta } = require("./sdoc");
|
|
5
|
+
// const { renderSlides } = require("./slide-renderer");
|
|
6
|
+
// const parsed = parseSdoc(text);
|
|
7
|
+
// const { nodes, meta } = extractMeta(parsed.nodes);
|
|
8
|
+
// const html = renderSlides(nodes, { meta, themeCss, themeJs });
|
|
9
|
+
|
|
10
|
+
const { parseInline, escapeHtml, escapeAttr } = require("./sdoc");
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Inline rendering — produces clean HTML without sdoc-* classes
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
function renderInlineNodes(nodes) {
|
|
17
|
+
return nodes
|
|
18
|
+
.map((node) => {
|
|
19
|
+
switch (node.type) {
|
|
20
|
+
case "text":
|
|
21
|
+
return escapeHtml(node.value);
|
|
22
|
+
case "code":
|
|
23
|
+
return `<code>${escapeHtml(node.value)}</code>`;
|
|
24
|
+
case "em":
|
|
25
|
+
return `<em>${renderInlineNodes(node.children)}</em>`;
|
|
26
|
+
case "strong":
|
|
27
|
+
return `<strong>${renderInlineNodes(node.children)}</strong>`;
|
|
28
|
+
case "strike":
|
|
29
|
+
return `<del>${renderInlineNodes(node.children)}</del>`;
|
|
30
|
+
case "link":
|
|
31
|
+
return `<a href="${escapeAttr(node.href)}" target="_blank" rel="noopener noreferrer">${renderInlineNodes(node.children)}</a>`;
|
|
32
|
+
case "image": {
|
|
33
|
+
const imgParts = [];
|
|
34
|
+
if (node.width) imgParts.push(`width:${escapeAttr(node.width)}`);
|
|
35
|
+
if (node.align === "center") imgParts.push("display:block", "margin-left:auto", "margin-right:auto");
|
|
36
|
+
else if (node.align === "left") imgParts.push("display:block", "float:left", "margin-right:1rem");
|
|
37
|
+
else if (node.align === "right") imgParts.push("display:block", "float:right", "margin-left:1rem");
|
|
38
|
+
const imgStyle = imgParts.length ? ` style="${imgParts.join(";")}"` : "";
|
|
39
|
+
return `<img src="${escapeAttr(node.src)}" alt="${escapeAttr(node.alt)}"${imgStyle} />`;
|
|
40
|
+
}
|
|
41
|
+
case "ref":
|
|
42
|
+
return `@${escapeHtml(node.id)}`;
|
|
43
|
+
default:
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
.join("");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderInline(text) {
|
|
51
|
+
return renderInlineNodes(parseInline(text));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Node rendering — clean slide HTML
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
function renderNode(node) {
|
|
59
|
+
switch (node.type) {
|
|
60
|
+
case "paragraph":
|
|
61
|
+
return `<p>${renderInline(node.text)}</p>`;
|
|
62
|
+
case "list":
|
|
63
|
+
return renderList(node);
|
|
64
|
+
case "table":
|
|
65
|
+
return renderTable(node);
|
|
66
|
+
case "code": {
|
|
67
|
+
if (node.lang === "mermaid") {
|
|
68
|
+
return `<pre class="mermaid">${escapeHtml(node.text)}</pre>`;
|
|
69
|
+
}
|
|
70
|
+
const langClass = node.lang ? ` class="language-${escapeAttr(node.lang)}"` : "";
|
|
71
|
+
return `<pre><code${langClass}>${escapeHtml(node.text)}</code></pre>`;
|
|
72
|
+
}
|
|
73
|
+
case "blockquote": {
|
|
74
|
+
const paragraphs = node.paragraphs
|
|
75
|
+
.map((text) => `<p>${renderInline(text)}</p>`)
|
|
76
|
+
.join("\n");
|
|
77
|
+
return `<blockquote>${paragraphs}</blockquote>`;
|
|
78
|
+
}
|
|
79
|
+
case "hr":
|
|
80
|
+
return `<hr />`;
|
|
81
|
+
case "scope":
|
|
82
|
+
return renderNestedScope(node);
|
|
83
|
+
default:
|
|
84
|
+
return "";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderList(list) {
|
|
89
|
+
const tag = list.listType === "number" ? "ol" : "ul";
|
|
90
|
+
const items = list.items
|
|
91
|
+
.map((item) => {
|
|
92
|
+
const text = item.title ? renderInline(item.title) : "";
|
|
93
|
+
const children = item.children
|
|
94
|
+
.map((child) => renderNode(child))
|
|
95
|
+
.join("\n");
|
|
96
|
+
const body = children ? `\n${children}` : "";
|
|
97
|
+
return `<li>${text}${body}</li>`;
|
|
98
|
+
})
|
|
99
|
+
.join("\n");
|
|
100
|
+
return `<${tag}>\n${items}\n</${tag}>`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function renderTable(table) {
|
|
104
|
+
const opts = table.options || {};
|
|
105
|
+
const classes = [];
|
|
106
|
+
if (opts.borderless) classes.push("borderless");
|
|
107
|
+
if (opts.headerless) classes.push("headerless");
|
|
108
|
+
const classAttr = classes.length ? ` class="${classes.join(" ")}"` : "";
|
|
109
|
+
|
|
110
|
+
let thead = "";
|
|
111
|
+
if (table.headers.length > 0) {
|
|
112
|
+
const headerCells = table.headers
|
|
113
|
+
.map((cell) => `<th>${renderInline(cell)}</th>`)
|
|
114
|
+
.join("");
|
|
115
|
+
thead = `<thead><tr>${headerCells}</tr></thead>`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const bodyRows = table.rows
|
|
119
|
+
.map((row) => {
|
|
120
|
+
const cells = row.map((cell) => `<td>${renderInline(cell)}</td>`).join("");
|
|
121
|
+
return `<tr>${cells}</tr>`;
|
|
122
|
+
})
|
|
123
|
+
.join("\n");
|
|
124
|
+
const tbody = bodyRows ? `<tbody>\n${bodyRows}\n</tbody>` : "";
|
|
125
|
+
|
|
126
|
+
return `<table${classAttr}>${thead}${thead ? "\n" : ""}${tbody}</table>`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function renderNestedScope(scope) {
|
|
130
|
+
const heading = scope.hasHeading !== false && scope.title
|
|
131
|
+
? `<h3>${renderInline(scope.title)}</h3>`
|
|
132
|
+
: "";
|
|
133
|
+
const children = scope.children.map((child) => renderNode(child)).join("\n");
|
|
134
|
+
return `<section>${heading}\n${children}</section>`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function renderChildren(nodes) {
|
|
138
|
+
return nodes.map((node) => renderNode(node)).join("\n");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Slide-level extraction
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
// Extracts config: lines from the beginning of a scope's children.
|
|
146
|
+
// Returns { config: { key: value, ... }, contentNodes: [...] }
|
|
147
|
+
function extractSlideConfig(children) {
|
|
148
|
+
const config = {};
|
|
149
|
+
const contentNodes = [];
|
|
150
|
+
let pastConfig = false;
|
|
151
|
+
|
|
152
|
+
for (const child of children) {
|
|
153
|
+
if (!pastConfig && child.type === "paragraph") {
|
|
154
|
+
const match = child.text.match(/^config\s*:\s*(.+)$/i);
|
|
155
|
+
if (match) {
|
|
156
|
+
const value = match[1].trim();
|
|
157
|
+
// Support multiple config lines; last one wins for the same key
|
|
158
|
+
// For now, config values are simple strings; primary use is layout
|
|
159
|
+
config.layout = value;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
pastConfig = true;
|
|
164
|
+
contentNodes.push(child);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { config, contentNodes };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Separates @notes child scope from other children
|
|
171
|
+
function extractNotes(children) {
|
|
172
|
+
const notes = [];
|
|
173
|
+
const rest = [];
|
|
174
|
+
for (const child of children) {
|
|
175
|
+
if (child.type === "scope" && child.id && child.id.toLowerCase() === "notes") {
|
|
176
|
+
notes.push(child);
|
|
177
|
+
} else {
|
|
178
|
+
rest.push(child);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { notes, contentNodes: rest };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Slide rendering
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
function renderSlide(scope, slideIndex, overlayHtml) {
|
|
189
|
+
const { config, contentNodes: afterConfig } = extractSlideConfig(scope.children);
|
|
190
|
+
const { notes, contentNodes } = extractNotes(afterConfig);
|
|
191
|
+
|
|
192
|
+
const classes = ["slide"];
|
|
193
|
+
if (config.layout) {
|
|
194
|
+
classes.push(config.layout);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const title = scope.hasHeading !== false && scope.title
|
|
198
|
+
? `<h2>${renderInline(scope.title)}</h2>`
|
|
199
|
+
: "";
|
|
200
|
+
|
|
201
|
+
let bodyHtml;
|
|
202
|
+
if (config.layout === "two-column") {
|
|
203
|
+
// In two-column layout, child scopes become columns
|
|
204
|
+
const columns = contentNodes.filter((n) => n.type === "scope");
|
|
205
|
+
const nonColumns = contentNodes.filter((n) => n.type !== "scope");
|
|
206
|
+
const preamble = nonColumns.length ? renderChildren(nonColumns) : "";
|
|
207
|
+
const columnsHtml = columns
|
|
208
|
+
.map((col) => {
|
|
209
|
+
const colTitle = col.hasHeading !== false && col.title
|
|
210
|
+
? `<h3>${renderInline(col.title)}</h3>`
|
|
211
|
+
: "";
|
|
212
|
+
const colContent = renderChildren(col.children);
|
|
213
|
+
return `<div class="column">${colTitle}\n${colContent}</div>`;
|
|
214
|
+
})
|
|
215
|
+
.join("\n");
|
|
216
|
+
bodyHtml = preamble + `\n<div class="columns">\n${columnsHtml}\n</div>`;
|
|
217
|
+
} else {
|
|
218
|
+
bodyHtml = renderChildren(contentNodes);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const notesHtml = notes.length
|
|
222
|
+
? `\n<aside class="notes">${notes.map((n) => renderChildren(n.children)).join("\n")}</aside>`
|
|
223
|
+
: "";
|
|
224
|
+
|
|
225
|
+
const idAttr = scope.id ? ` id="${escapeAttr(scope.id)}"` : "";
|
|
226
|
+
const overlay = overlayHtml || "";
|
|
227
|
+
|
|
228
|
+
return `<div class="${classes.join(" ")}"${idAttr}>\n${title}\n${bodyHtml}${notesHtml}${overlay}\n</div>`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// Main entry point
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
function renderSlides(nodes, options = {}) {
|
|
236
|
+
const {
|
|
237
|
+
meta = {},
|
|
238
|
+
themeCss = "",
|
|
239
|
+
themeJs = ""
|
|
240
|
+
} = options;
|
|
241
|
+
|
|
242
|
+
// The nodes from extractMeta have @meta already stripped.
|
|
243
|
+
// If there's a document scope wrapper, unwrap it to get the slides.
|
|
244
|
+
let slideScopes;
|
|
245
|
+
if (nodes.length === 1 && nodes[0].type === "scope" && nodes[0].children) {
|
|
246
|
+
slideScopes = nodes[0].children;
|
|
247
|
+
} else {
|
|
248
|
+
slideScopes = nodes;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Filter to scope nodes only (skip stray paragraphs at top level)
|
|
252
|
+
const slides = slideScopes.filter((n) => n.type === "scope");
|
|
253
|
+
|
|
254
|
+
// Build per-slide footer: < CONFIDENTIAL ---gap--- Company >
|
|
255
|
+
const footerParts = [];
|
|
256
|
+
footerParts.push(`<span class="nav-prev">‹</span>`);
|
|
257
|
+
if (meta.confidential) {
|
|
258
|
+
const val = meta.confidential.trim();
|
|
259
|
+
const entity = val.toLowerCase() === "true" ? meta.company : val;
|
|
260
|
+
const text = entity
|
|
261
|
+
? `CONFIDENTIAL \u2014 ${escapeHtml(entity)}`
|
|
262
|
+
: "CONFIDENTIAL";
|
|
263
|
+
footerParts.push(`<span class="sdoc-confidential-notice">${text}</span>`);
|
|
264
|
+
}
|
|
265
|
+
footerParts.push(`<span class="slide-footer-gap"></span>`);
|
|
266
|
+
if (meta.company) {
|
|
267
|
+
footerParts.push(`<span class="sdoc-company-footer">${escapeHtml(meta.company)}</span>`);
|
|
268
|
+
}
|
|
269
|
+
footerParts.push(`<span class="nav-next">›</span>`);
|
|
270
|
+
const overlayHtml = `\n<div class="slide-footer">${footerParts.join("")}</div>`;
|
|
271
|
+
|
|
272
|
+
const slidesHtml = slides
|
|
273
|
+
.map((scope, index) => renderSlide(scope, index, overlayHtml))
|
|
274
|
+
.join("\n\n");
|
|
275
|
+
|
|
276
|
+
const title = meta.properties?.title
|
|
277
|
+
|| (nodes.length === 1 && nodes[0].title ? nodes[0].title : "Slides");
|
|
278
|
+
|
|
279
|
+
// Structural styles — always injected regardless of theme.
|
|
280
|
+
const structuralCss = `
|
|
281
|
+
.slide { position: relative; }
|
|
282
|
+
.slide-footer {
|
|
283
|
+
position: absolute; bottom: 20px; left: 32px; right: 32px;
|
|
284
|
+
display: flex; align-items: baseline;
|
|
285
|
+
pointer-events: none;
|
|
286
|
+
}
|
|
287
|
+
.slide-footer-gap { flex: 1; }
|
|
288
|
+
.nav-prev, .nav-next {
|
|
289
|
+
font-size: 1.4em; color: #ccc;
|
|
290
|
+
cursor: pointer; pointer-events: auto;
|
|
291
|
+
user-select: none;
|
|
292
|
+
}
|
|
293
|
+
.sdoc-company-footer {
|
|
294
|
+
font-size: 0.7em; color: rgba(0,0,0,0.35);
|
|
295
|
+
letter-spacing: 0.04em;
|
|
296
|
+
margin-right: 0.8em;
|
|
297
|
+
}
|
|
298
|
+
.sdoc-confidential-notice {
|
|
299
|
+
font-size: 0.65em; font-weight: 600;
|
|
300
|
+
letter-spacing: 0.12em; text-transform: uppercase;
|
|
301
|
+
color: rgba(160, 40, 40, 0.6);
|
|
302
|
+
margin-left: 0.8em;
|
|
303
|
+
}
|
|
304
|
+
@media print {
|
|
305
|
+
@page { size: 13.333in 7.5in; margin: 0; }
|
|
306
|
+
body { overflow: visible; height: auto; }
|
|
307
|
+
.slide {
|
|
308
|
+
display: flex !important;
|
|
309
|
+
position: relative !important;
|
|
310
|
+
opacity: 1 !important;
|
|
311
|
+
pointer-events: auto !important;
|
|
312
|
+
page-break-after: always; break-after: page;
|
|
313
|
+
width: 100vw; height: 100vh; max-width: none;
|
|
314
|
+
page-break-inside: avoid; break-inside: avoid;
|
|
315
|
+
}
|
|
316
|
+
.slide:last-child { page-break-after: auto; break-after: auto; }
|
|
317
|
+
.nav-prev, .nav-next { display: none !important; }
|
|
318
|
+
.notes { display: none; }
|
|
319
|
+
}`;
|
|
320
|
+
|
|
321
|
+
const cssTag = `<style>\n${structuralCss}\n${themeCss}\n</style>`;
|
|
322
|
+
const jsTag = themeJs ? `<script>\n${themeJs}\n</script>` : "";
|
|
323
|
+
const mermaidCdn = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js";
|
|
324
|
+
const mermaidTag = slidesHtml.includes('class="mermaid"')
|
|
325
|
+
? `\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>`
|
|
326
|
+
: "";
|
|
327
|
+
|
|
328
|
+
return `<!DOCTYPE html>
|
|
329
|
+
<html lang="en">
|
|
330
|
+
<head>
|
|
331
|
+
<meta charset="UTF-8">
|
|
332
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
333
|
+
<title>${escapeHtml(title)}</title>
|
|
334
|
+
${cssTag}
|
|
335
|
+
</head>
|
|
336
|
+
<body>
|
|
337
|
+
${slidesHtml}
|
|
338
|
+
|
|
339
|
+
${jsTag}${mermaidTag}
|
|
340
|
+
</body>
|
|
341
|
+
</html>`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
module.exports = { renderSlides, renderSlide, renderNode, renderInline };
|