mr-md 1.0.0
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 +24 -0
- package/dist/builder.d.ts +169 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +545 -0
- package/dist/client/app.js +314 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/renderer/blocks.d.ts +15 -0
- package/dist/renderer/blocks.d.ts.map +1 -0
- package/dist/renderer/blocks.js +365 -0
- package/dist/renderer/html.d.ts +8 -0
- package/dist/renderer/html.d.ts.map +1 -0
- package/dist/renderer/html.js +141 -0
- package/dist/renderer/index.d.ts +4 -0
- package/dist/renderer/index.d.ts.map +1 -0
- package/dist/renderer/index.js +381 -0
- package/dist/renderer/markdown.d.ts +13 -0
- package/dist/renderer/markdown.d.ts.map +1 -0
- package/dist/renderer/markdown.js +143 -0
- package/dist/renderer/utils.d.ts +10 -0
- package/dist/renderer/utils.d.ts.map +1 -0
- package/dist/renderer/utils.js +59 -0
- package/dist/styles/theme.css +1258 -0
- package/dist/types.d.ts +207 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/package.json +62 -0
- package/src/builder.ts +708 -0
- package/src/client/app.js +314 -0
- package/src/index.ts +34 -0
- package/src/renderer/blocks.ts +452 -0
- package/src/renderer/html.ts +163 -0
- package/src/renderer/index.ts +401 -0
- package/src/renderer/markdown.ts +193 -0
- package/src/renderer/utils.ts +84 -0
- package/src/styles/theme.css +1258 -0
- package/src/types.ts +288 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import katex from "katex";
|
|
2
|
+
import { blockChrome, escapeScriptJson, mdInline, mdToHtml, renderSimulationControls, } from "./markdown.js";
|
|
3
|
+
import hljs from "highlight.js";
|
|
4
|
+
import { resolveAssetSrc, resolveContent } from "./utils.js";
|
|
5
|
+
// HTML escaping utility needed by blocks
|
|
6
|
+
export function escHtml(str) {
|
|
7
|
+
return str
|
|
8
|
+
.replace(/&/g, "&")
|
|
9
|
+
.replace(/</g, "<")
|
|
10
|
+
.replace(/>/g, ">")
|
|
11
|
+
.replace(/"/g, """)
|
|
12
|
+
.replace(/'/g, "'");
|
|
13
|
+
}
|
|
14
|
+
export function escAttr(str) {
|
|
15
|
+
return escHtml(str);
|
|
16
|
+
}
|
|
17
|
+
// ─── Block renderers ──────────────────────────────────────────────────────────
|
|
18
|
+
function renderBlock(block, idx, options) {
|
|
19
|
+
try {
|
|
20
|
+
const result = renderBlockInner(block, idx, options);
|
|
21
|
+
if (result.html &&
|
|
22
|
+
"src" in block &&
|
|
23
|
+
typeof block.src === "string" &&
|
|
24
|
+
block.src.includes(".")) {
|
|
25
|
+
result.html = result.html.replace(/^<([a-zA-Z0-9-]+)([^>]*)>/, `<$1 data-bk-src="${escAttr(block.src)}"$2>`);
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
31
|
+
console.warn(` ⚠ Error rendering block ${idx + 1} (${block.type}): ${msg}`);
|
|
32
|
+
const errorHtml = `<div class="bk-callout bk-callout--warning"><div class="bk-callout-icon"></div><div class="bk-callout-content"><div class="bk-callout-label">Block Error (${escHtml(block.type)})</div><div class="bk-callout-body"><p>${escHtml(msg)}</p></div></div></div>`;
|
|
33
|
+
return { html: errorHtml };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function renderBlockInner(block, idx, options) {
|
|
37
|
+
switch (block.type) {
|
|
38
|
+
case "heading": {
|
|
39
|
+
const md = resolveContent(block.src, options, "md");
|
|
40
|
+
const { html, title } = mdToHtml(md);
|
|
41
|
+
const label = block.title || title || (typeof block.src === "string" && !block.src.includes(".md") ? block.src : "Heading");
|
|
42
|
+
const id = `heading-${idx}`;
|
|
43
|
+
return {
|
|
44
|
+
html: `<section id="${id}" class="bk-section bk-heading">${html}</section>`,
|
|
45
|
+
navItem: { id, label, kind: "heading" },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
case "markdown": {
|
|
49
|
+
const md = resolveContent(block.src, options, "md");
|
|
50
|
+
const { html } = mdToHtml(md);
|
|
51
|
+
return { html: `<div class="bk-markdown">${html}</div>` };
|
|
52
|
+
}
|
|
53
|
+
case "section": {
|
|
54
|
+
const md = resolveContent(block.src, options, "md");
|
|
55
|
+
const { html, title } = mdToHtml(md);
|
|
56
|
+
const label = block.label || title || (typeof block.src === "string" && !block.src.includes(".md") ? block.src : "Section");
|
|
57
|
+
const id = `section-${idx}`;
|
|
58
|
+
return {
|
|
59
|
+
html: `<section id="${id}" class="bk-section bk-subsection">${html}</section>`,
|
|
60
|
+
navItem: { id, label, kind: "section" },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
case "important":
|
|
64
|
+
case "warning":
|
|
65
|
+
case "tip":
|
|
66
|
+
case "note": {
|
|
67
|
+
const variantMap = {
|
|
68
|
+
important: "Important",
|
|
69
|
+
warning: "Warning",
|
|
70
|
+
tip: "Tip",
|
|
71
|
+
note: "Note",
|
|
72
|
+
};
|
|
73
|
+
const label = variantMap[block.type];
|
|
74
|
+
const md = resolveContent(block.src, options, "md");
|
|
75
|
+
const { html } = mdToHtml(md);
|
|
76
|
+
return {
|
|
77
|
+
html: `<div class="bk-callout bk-callout--${block.type}">
|
|
78
|
+
<div class="bk-callout-icon"></div>
|
|
79
|
+
<div class="bk-callout-content">
|
|
80
|
+
<div class="bk-callout-label">${label}</div>
|
|
81
|
+
<div class="bk-callout-body">${html}</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
case "code": {
|
|
87
|
+
const raw = resolveContent(block.src, options, "text"); // Could be file or inline
|
|
88
|
+
const lang = block.lang ??
|
|
89
|
+
(typeof block.src === "string" && block.src.includes(".")
|
|
90
|
+
? (block.src.split(".").pop() ?? "")
|
|
91
|
+
: "");
|
|
92
|
+
let highlighted = escHtml(raw);
|
|
93
|
+
if (lang && hljs.getLanguage(lang)) {
|
|
94
|
+
highlighted = hljs.highlight(raw, { language: lang }).value;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
highlighted = hljs.highlightAuto(raw).value;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
html: `<div class="bk-code-block">
|
|
101
|
+
${block.label ? `<div class="bk-code-header"><span class="bk-code-label">${escHtml(block.label)}</span><span class="bk-code-lang">${lang}</span></div>` : ""}
|
|
102
|
+
<div class="bk-code-scroll">
|
|
103
|
+
<pre><code class="language-${lang} hljs">${highlighted}</code></pre>
|
|
104
|
+
</div>
|
|
105
|
+
</div>`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
case "simulation": {
|
|
109
|
+
const propsJson = JSON.stringify(block.props ?? {});
|
|
110
|
+
const simSrc = resolveContent(block.src, options, "js");
|
|
111
|
+
const simConfig = { js: simSrc, loop: false, dependencies: block.dependencies };
|
|
112
|
+
return {
|
|
113
|
+
html: blockChrome(block.controls === "observe" ? "Simulation" : "Interactive Lab", block.label, block.caption, `${renderSimulationControls(block)}
|
|
114
|
+
<div class="bk-embed-frame bk-embed-interactive">
|
|
115
|
+
<div class="bk-embed-overlay" tabindex="0" role="button" aria-label="Activate interactive simulation">
|
|
116
|
+
<span class="bk-embed-overlay-text">Click to interact</span>
|
|
117
|
+
</div>
|
|
118
|
+
<iframe srcdoc="${iframeDoc(simSrc, propsJson, false, block.dependencies)}"
|
|
119
|
+
sandbox="allow-scripts"
|
|
120
|
+
style="width:100%;height:100%;border:none;display:block;">
|
|
121
|
+
</iframe>
|
|
122
|
+
</div>
|
|
123
|
+
<script type="application/json" class="bk-sim-config">${escapeScriptJson(simConfig)}</script>`, block.accent ?? "blue"),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
case "animation": {
|
|
127
|
+
const animSrc = resolveContent(block.src, options, "js");
|
|
128
|
+
return {
|
|
129
|
+
html: blockChrome("Animation", block.label, block.caption, `<div class="bk-embed-frame bk-embed-interactive">
|
|
130
|
+
<div class="bk-embed-overlay" tabindex="0" role="button" aria-label="Activate interactive animation">
|
|
131
|
+
<span class="bk-embed-overlay-text">Click to interact</span>
|
|
132
|
+
</div>
|
|
133
|
+
<iframe srcdoc="${iframeDoc(animSrc, "{}", block.loop)}"
|
|
134
|
+
sandbox="allow-scripts"
|
|
135
|
+
style="width:100%;height:100%;border:none;display:block;">
|
|
136
|
+
</iframe>
|
|
137
|
+
</div>`, block.accent ?? "neutral"),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
case "media": {
|
|
141
|
+
const src = resolveAssetSrc(block.src, options);
|
|
142
|
+
const media = block.kind === "image"
|
|
143
|
+
? `<img src="${escAttr(src)}" alt="${escAttr(block.alt ?? "")}" loading="lazy">`
|
|
144
|
+
: block.kind === "video"
|
|
145
|
+
? `<video src="${escAttr(src)}" ${block.poster ? `poster="${escAttr(resolveAssetSrc(block.poster, options))}"` : ""} ${block.controls !== false ? "controls" : ""} playsinline></video>`
|
|
146
|
+
: `<audio src="${escAttr(src)}" ${block.controls !== false ? "controls" : ""}></audio>`;
|
|
147
|
+
return {
|
|
148
|
+
html: blockChrome(block.kind, block.label, [block.caption, block.credit ? `Credit: ${block.credit}` : ""]
|
|
149
|
+
.filter(Boolean)
|
|
150
|
+
.join(" "), `<div class="bk-media bk-media--${block.kind}">${media}</div>`, "neutral"),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
case "youtube": {
|
|
154
|
+
const params = new URLSearchParams();
|
|
155
|
+
params.set("rel", "0");
|
|
156
|
+
if (block.start)
|
|
157
|
+
params.set("start", String(block.start));
|
|
158
|
+
return {
|
|
159
|
+
html: blockChrome("YouTube", block.label, block.caption, `<div class="bk-embed-frame">
|
|
160
|
+
<iframe src="https://www.youtube-nocookie.com/embed/${escAttr(block.id)}?${params.toString()}"
|
|
161
|
+
title="${escAttr(block.label ?? "YouTube video")}"
|
|
162
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
163
|
+
allowfullscreen
|
|
164
|
+
referrerpolicy="strict-origin-when-cross-origin"
|
|
165
|
+
loading="lazy"
|
|
166
|
+
style="width:100%;height:100%;border:none;display:block;">
|
|
167
|
+
</iframe>
|
|
168
|
+
</div>`, "neutral"),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
case "latex": {
|
|
172
|
+
const rendered = katex.renderToString(block.tex, {
|
|
173
|
+
throwOnError: false,
|
|
174
|
+
displayMode: block.display ?? true,
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
html: blockChrome("LaTeX", block.label, block.caption, `<div class="${block.display === false ? "bk-latex-inline" : "bk-latex-block"}">${rendered}</div>`, "violet", false),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
case "columns": {
|
|
181
|
+
return {
|
|
182
|
+
html: blockChrome("Columns", block.label, block.caption, `<div class="bk-columns" style="grid-template-columns:${block.columns
|
|
183
|
+
.map((column) => column.width ?? "minmax(0, 1fr)")
|
|
184
|
+
.join(" ")}">
|
|
185
|
+
${block.columns
|
|
186
|
+
.map((column) => {
|
|
187
|
+
const content = column.latex != null
|
|
188
|
+
? `<div class="bk-latex-block">${katex.renderToString(column.latex, { throwOnError: false, displayMode: true })}</div>`
|
|
189
|
+
: mdToHtml(column.markdown ??
|
|
190
|
+
(column.src
|
|
191
|
+
? resolveContent(column.src, options, "md")
|
|
192
|
+
: "")).html;
|
|
193
|
+
return `<div class="bk-column">${content}</div>`;
|
|
194
|
+
})
|
|
195
|
+
.join("")}
|
|
196
|
+
</div>`, "neutral", false),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
case "quiz": {
|
|
200
|
+
let quiz = { questions: [] };
|
|
201
|
+
const rawJson = resolveContent(block.src, options, "json");
|
|
202
|
+
try {
|
|
203
|
+
quiz = JSON.parse(rawJson);
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
207
|
+
if (options.strict !== false) {
|
|
208
|
+
throw new Error(`Invalid Quiz JSON for block ${idx + 1}: ${msg}`);
|
|
209
|
+
}
|
|
210
|
+
console.warn(` ⚠ Invalid Quiz JSON for block ${idx + 1}:`, e);
|
|
211
|
+
return {
|
|
212
|
+
html: `<div class="bk-callout bk-callout--warning"><div class="bk-callout-icon"></div><div class="bk-callout-content"><div class="bk-callout-label">Quiz JSON Error</div><div class="bk-callout-body"><p>${escHtml(msg)}</p></div></div></div>`,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
html: `<div class="bk-quiz" id="quiz-${idx}">
|
|
217
|
+
<div class="bk-quiz-head">
|
|
218
|
+
<span>${escHtml(block.label ?? "Check your understanding")}</span>
|
|
219
|
+
${block.caption ? `<small>${mdInline(block.caption)}</small>` : ""}
|
|
220
|
+
</div>
|
|
221
|
+
<div class="bk-quiz-body">
|
|
222
|
+
${quiz.questions.map((q, qi) => renderQuestion(q, `quiz-${idx}`, qi)).join("\n")}
|
|
223
|
+
</div>
|
|
224
|
+
</div>`,
|
|
225
|
+
navItem: {
|
|
226
|
+
id: `quiz-${idx}`,
|
|
227
|
+
label: block.label ?? "Questions",
|
|
228
|
+
kind: "quiz",
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
case "divider":
|
|
233
|
+
return { html: '<hr class="bk-divider">' };
|
|
234
|
+
default:
|
|
235
|
+
return { html: "" };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function renderQuestion(q, quizId, qi) {
|
|
239
|
+
const qid = `${quizId}-q${qi}`;
|
|
240
|
+
const options = q.options
|
|
241
|
+
.map((opt, oi) => `
|
|
242
|
+
<button class="bk-opt" data-correct="${oi === q.answer}" onclick="bkAnswer(this,'${qid}')">
|
|
243
|
+
<span class="bk-opt-dot"></span><span class="bk-opt-text">${mdInline(opt)}</span>
|
|
244
|
+
</button>`)
|
|
245
|
+
.join("");
|
|
246
|
+
const expHtml = q.explanation
|
|
247
|
+
? `<div class="bk-explanation" id="${qid}-exp" hidden>${mdToHtml(q.explanation).html}</div>`
|
|
248
|
+
: "";
|
|
249
|
+
return `
|
|
250
|
+
<div class="bk-question" id="${qid}">
|
|
251
|
+
<div class="bk-q-text">${mdToHtml(q.q).html}</div>
|
|
252
|
+
<div class="bk-opts">${options}</div>
|
|
253
|
+
${expHtml}
|
|
254
|
+
</div>`;
|
|
255
|
+
}
|
|
256
|
+
// Wraps a JS string in a minimal iframe document
|
|
257
|
+
function iframeDoc(js, props, loop, dependencies) {
|
|
258
|
+
const scriptTags = (dependencies ?? []).map((url) => `<script src="${escAttr(url)}"></script>`).join("\\n");
|
|
259
|
+
const doc = `<!DOCTYPE html><html><head>
|
|
260
|
+
${scriptTags}
|
|
261
|
+
<style>
|
|
262
|
+
html, body { height: 100%; width: 100%; margin: 0; padding: 0; overflow: hidden; background: transparent; display: flex; align-items: center; justify-content: center; }
|
|
263
|
+
canvas { display: block; touch-action: none; transform-origin: center center; flex-shrink: 0; }
|
|
264
|
+
body { font-family: sans-serif; }
|
|
265
|
+
</style>
|
|
266
|
+
</head><body>
|
|
267
|
+
<canvas id="c" width="800" height="500"></canvas>
|
|
268
|
+
<script>
|
|
269
|
+
window.__simProps=${props};
|
|
270
|
+
window.__loop=${loop ?? false};
|
|
271
|
+
window.bkSetupCalled = false;
|
|
272
|
+
window.bkCanvasPoint = function(event, canvas) {
|
|
273
|
+
const c = canvas || event.currentTarget || event.target;
|
|
274
|
+
const rect = c.getBoundingClientRect();
|
|
275
|
+
const logicalW = c.__bkLogicalW || 800;
|
|
276
|
+
const logicalH = c.__bkLogicalH || 500;
|
|
277
|
+
return {
|
|
278
|
+
x: (event.clientX - rect.left) * logicalW / rect.width,
|
|
279
|
+
y: (event.clientY - rect.top) * logicalH / rect.height
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
window.bkFitCanvas = function(c, requestedW, requestedH, options) {
|
|
283
|
+
if (!c) return { scale: 1, width: requestedW, height: requestedH, cssScale: 1 };
|
|
284
|
+
const dpr = window.devicePixelRatio || 1;
|
|
285
|
+
const w = requestedW;
|
|
286
|
+
const h = requestedH;
|
|
287
|
+
|
|
288
|
+
c.__bkLogicalW = w;
|
|
289
|
+
c.__bkLogicalH = h;
|
|
290
|
+
|
|
291
|
+
c.style.width = w + "px";
|
|
292
|
+
c.style.height = h + "px";
|
|
293
|
+
|
|
294
|
+
c.style.position = "relative";
|
|
295
|
+
c.style.left = "auto";
|
|
296
|
+
c.style.top = "auto";
|
|
297
|
+
c.style.transformOrigin = "center center";
|
|
298
|
+
|
|
299
|
+
const scaleX = window.innerWidth / w;
|
|
300
|
+
const scaleY = window.innerHeight / h;
|
|
301
|
+
const cssScale = Math.max(scaleX, scaleY);
|
|
302
|
+
|
|
303
|
+
c.style.transform = "scale(" + cssScale + ")";
|
|
304
|
+
|
|
305
|
+
const physW = Math.max(1, Math.round(w * dpr));
|
|
306
|
+
const physH = Math.max(1, Math.round(h * dpr));
|
|
307
|
+
|
|
308
|
+
if (!options || options.bitmap !== false) {
|
|
309
|
+
if (c.width !== physW || c.height !== physH) {
|
|
310
|
+
c.width = physW;
|
|
311
|
+
c.height = physH;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return { scale: dpr, width: w, height: h, cssScale };
|
|
315
|
+
};
|
|
316
|
+
window.bkSetup = function(requestedW, requestedH, loopFn) {
|
|
317
|
+
window.bkSetupCalled = true;
|
|
318
|
+
const canvas = document.getElementById("c");
|
|
319
|
+
if (!canvas) return;
|
|
320
|
+
const ctx = canvas.getContext("2d");
|
|
321
|
+
|
|
322
|
+
function loop() {
|
|
323
|
+
const fit = window.bkFitCanvas(canvas, requestedW, requestedH);
|
|
324
|
+
if (window.innerWidth >= 32 && window.innerHeight >= 32) {
|
|
325
|
+
ctx.save();
|
|
326
|
+
ctx.scale(fit.scale, fit.scale);
|
|
327
|
+
|
|
328
|
+
loopFn(ctx, fit.width, fit.height);
|
|
329
|
+
|
|
330
|
+
ctx.restore();
|
|
331
|
+
}
|
|
332
|
+
requestAnimationFrame(loop);
|
|
333
|
+
}
|
|
334
|
+
loop();
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
window.addEventListener("message", (event) => {
|
|
338
|
+
if (!event.data || event.data.type !== "bk:set-props") return;
|
|
339
|
+
window.__simProps = { ...window.__simProps, ...event.data.props };
|
|
340
|
+
window.dispatchEvent(new CustomEvent("bk:props", { detail: window.__simProps }));
|
|
341
|
+
});
|
|
342
|
+
try {
|
|
343
|
+
${js}
|
|
344
|
+
} catch (e) {
|
|
345
|
+
console.error("Simulation Error:", e);
|
|
346
|
+
document.body.innerHTML = '<div style="padding:20px;color:red;font-family:monospace">Error: ' + e.message + '</div>';
|
|
347
|
+
}
|
|
348
|
+
if (!window.bkSetupCalled) {
|
|
349
|
+
function fallbackScale() {
|
|
350
|
+
window.bkFitCanvas(document.getElementById("c"), 800, 500, { bitmap: false });
|
|
351
|
+
requestAnimationFrame(fallbackScale);
|
|
352
|
+
}
|
|
353
|
+
fallbackScale();
|
|
354
|
+
}
|
|
355
|
+
</script>
|
|
356
|
+
</body></html>`;
|
|
357
|
+
// We use double quotes for the srcdoc attribute, so we must escape them.
|
|
358
|
+
return doc
|
|
359
|
+
.replace(/&/g, "&")
|
|
360
|
+
.replace(/"/g, """)
|
|
361
|
+
.replace(/'/g, "'")
|
|
362
|
+
.replace(/</g, "<")
|
|
363
|
+
.replace(/>/g, ">");
|
|
364
|
+
}
|
|
365
|
+
export { blockChrome, renderBlock, renderBlockInner };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BuildOptions, Lesson } from "../types.js";
|
|
2
|
+
import type { NavItem } from "./utils.js";
|
|
3
|
+
declare function renderNavItem(item: NavItem): string;
|
|
4
|
+
declare function renderPage(lesson: Lesson, navItems: NavItem[], bodyHtml: string, opts: BuildOptions): string;
|
|
5
|
+
declare function pageCSS(): string;
|
|
6
|
+
declare function clientScript(): string;
|
|
7
|
+
export { clientScript, pageCSS, renderNavItem, renderPage };
|
|
8
|
+
//# sourceMappingURL=html.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/renderer/html.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAI1C,iBAAS,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAQ5C;AAsBD,iBAAS,UAAU,CAClB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,OAAO,EAAE,EACnB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,YAAY,GAChB,MAAM,CAkGR;AAID,iBAAS,OAAO,IAAI,MAAM,CAKzB;AAID,iBAAS,YAAY,IAAI,MAAM,CAE9B;AAED,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,CAAC"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = path.dirname(__filename);
|
|
6
|
+
import { escHtml } from "./blocks.js";
|
|
7
|
+
// ─── Page shell ───────────────────────────────────────────────────────────────
|
|
8
|
+
function renderNavItem(item) {
|
|
9
|
+
const kindClass = item.kind === "heading"
|
|
10
|
+
? "bk-nav-heading"
|
|
11
|
+
: item.kind === "quiz"
|
|
12
|
+
? "bk-nav-quiz"
|
|
13
|
+
: "bk-nav-sub";
|
|
14
|
+
return `<a href="#${item.id}" class="bk-nav-item ${kindClass}" data-id="${item.id}">${escHtml(item.label)}</a>`;
|
|
15
|
+
}
|
|
16
|
+
function renderEndNav(lesson) {
|
|
17
|
+
const { prevSlug, prevTitle, nextSlug, nextTitle } = lesson.meta;
|
|
18
|
+
if (!prevSlug && !nextSlug)
|
|
19
|
+
return "";
|
|
20
|
+
return `<nav class="bk-end-nav" aria-label="Lesson navigation" style="grid-template-columns: repeat(2, minmax(0, 1fr));">
|
|
21
|
+
${prevSlug ? `
|
|
22
|
+
<a class="bk-end-link bk-end-link--prev" href="${prevSlug}.html">
|
|
23
|
+
<span>Previous Lesson</span>
|
|
24
|
+
<strong>${escHtml(prevTitle || "Previous")}</strong>
|
|
25
|
+
</a>
|
|
26
|
+
` : `<div class="bk-end-link" style="visibility:hidden"></div>`}
|
|
27
|
+
${nextSlug ? `
|
|
28
|
+
<a class="bk-end-link bk-end-link--next" href="${nextSlug}.html">
|
|
29
|
+
<span>Next Lesson</span>
|
|
30
|
+
<strong>${escHtml(nextTitle || "Next")}</strong>
|
|
31
|
+
</a>
|
|
32
|
+
` : `<div class="bk-end-link" style="visibility:hidden"></div>`}
|
|
33
|
+
</nav>`;
|
|
34
|
+
}
|
|
35
|
+
function renderPage(lesson, navItems, bodyHtml, opts) {
|
|
36
|
+
const theme = opts.theme ?? "auto";
|
|
37
|
+
const schemeAttr = theme === "auto" ? "" : `data-theme="${theme}"`;
|
|
38
|
+
const preset = opts.preset ?? {};
|
|
39
|
+
const layout = preset.layout ?? "lesson";
|
|
40
|
+
const density = preset.density ?? "comfortable";
|
|
41
|
+
const tone = preset.tone ?? "scholarly";
|
|
42
|
+
const palette = opts.palette ?? "ink";
|
|
43
|
+
const navHtml = navItems.map(renderNavItem).join("\n");
|
|
44
|
+
const endNavHtml = renderEndNav(lesson);
|
|
45
|
+
return `<!DOCTYPE html>
|
|
46
|
+
<html lang="en" data-palette="${palette}" ${schemeAttr}>
|
|
47
|
+
<head>
|
|
48
|
+
<meta charset="UTF-8">
|
|
49
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
50
|
+
<title>${escHtml(lesson.meta.title)}</title>
|
|
51
|
+
${lesson.meta.description ? `<meta name="description" content="${escHtml(lesson.meta.description)}">` : ""}
|
|
52
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.47/dist/katex.min.css">
|
|
53
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/styles/github-dark.min.css">
|
|
54
|
+
${opts.head ?? ""}
|
|
55
|
+
<style>
|
|
56
|
+
${opts.font ? `:root { --font-sans: ${opts.font}, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }` : ""}
|
|
57
|
+
${pageCSS()}
|
|
58
|
+
</style>
|
|
59
|
+
</head>
|
|
60
|
+
<body class="bk-layout-${layout} bk-density-${density} bk-tone-${tone}">
|
|
61
|
+
<div class="bk-shell">
|
|
62
|
+
<aside class="bk-sidebar">
|
|
63
|
+
<div class="bk-sidebar-inner">
|
|
64
|
+
<div class="bk-sidebar-header">
|
|
65
|
+
${lesson.meta.parentSlug ? `<div style="margin-top: 8px;"><a href="${lesson.meta.parentSlug}.html" class="bk-back-link" aria-label="Back to Chapter" style="margin-bottom: 12px;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>Back to Chapter</a></div>` : `<div style="margin-top: 8px;"></div>`}
|
|
66
|
+
<div class="bk-sidebar-title">${escHtml(lesson.meta.title)}</div>
|
|
67
|
+
${lesson.meta.author ? `<div class="bk-sidebar-author">By ${escHtml(lesson.meta.author)}</div>` : ""}
|
|
68
|
+
${lesson.meta.tags?.length ? `<div class="bk-tag-row">${lesson.meta.tags.map((tag) => `<span>${escHtml(tag)}</span>`).join("")}</div>` : ""}
|
|
69
|
+
</div>
|
|
70
|
+
<nav class="bk-nav">${navHtml}</nav>
|
|
71
|
+
<div class="bk-sidebar-footer">
|
|
72
|
+
<button class="bk-icon-btn bk-settings-button" id="bk-settings-button" type="button" aria-expanded="false" aria-controls="bk-theme-panel" title="Display settings">
|
|
73
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
|
74
|
+
<span class="bk-sr-only">Display settings</span>
|
|
75
|
+
</button>
|
|
76
|
+
<div class="bk-theme-panel" id="bk-theme-panel" aria-label="Display settings" hidden>
|
|
77
|
+
<div class="bk-theme-row">
|
|
78
|
+
<span>Theme</span>
|
|
79
|
+
<div class="bk-segmented-control" id="bk-theme-icons">
|
|
80
|
+
<button type="button" class="bk-segment-btn ${theme === "light" ? "active" : ""}" data-theme="light" title="Light" aria-label="Light theme">
|
|
81
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
|
|
82
|
+
</button>
|
|
83
|
+
<button type="button" class="bk-segment-btn ${theme === "dark" ? "active" : ""}" data-theme="dark" title="Dark" aria-label="Dark theme">
|
|
84
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
|
|
85
|
+
</button>
|
|
86
|
+
<button type="button" class="bk-segment-btn ${theme === "auto" ? "active" : ""}" data-theme="auto" title="System" aria-label="System theme">
|
|
87
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="bk-theme-row">
|
|
92
|
+
<span>Palette</span>
|
|
93
|
+
<div class="bk-segmented-control" id="bk-palette-icons">
|
|
94
|
+
<button type="button" class="bk-segment-btn ${palette === "ink" ? "active" : ""}" data-palette="ink" title="Ink" aria-label="Ink palette">
|
|
95
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"></path></svg>
|
|
96
|
+
</button>
|
|
97
|
+
<button type="button" class="bk-segment-btn ${palette === "field" ? "active" : ""}" data-palette="field" title="Field" aria-label="Field palette">
|
|
98
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z"></path><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"></path></svg>
|
|
99
|
+
</button>
|
|
100
|
+
<button type="button" class="bk-segment-btn ${palette === "ember" ? "active" : ""}" data-palette="ember" title="Ember" aria-label="Ember palette">
|
|
101
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 14.5A2.5 2.5 0 0011 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 11-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 002.5 2.5z"></path></svg>
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</aside>
|
|
109
|
+
<button class="bk-sidebar-collapse-floating" id="bk-sidebar-collapse" aria-label="Collapse sidebar">
|
|
110
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>
|
|
111
|
+
</button>
|
|
112
|
+
<main class="bk-main">
|
|
113
|
+
<button class="bk-sidebar-expand" id="bk-sidebar-expand" type="button" aria-label="Expand sidebar">
|
|
114
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>
|
|
115
|
+
</button>
|
|
116
|
+
<article class="bk-content">
|
|
117
|
+
<header class="bk-hero">
|
|
118
|
+
<p class="bk-eyebrow">Interactive Lesson</p>
|
|
119
|
+
<h1 style="view-transition-name: title-${lesson.meta.slug}">${escHtml(lesson.meta.title)}</h1>
|
|
120
|
+
${lesson.meta.description ? `<p class="bk-deck">${escHtml(lesson.meta.description)}</p>` : ""}
|
|
121
|
+
</header>
|
|
122
|
+
${bodyHtml}
|
|
123
|
+
${endNavHtml}
|
|
124
|
+
</article>
|
|
125
|
+
</main>
|
|
126
|
+
</div>
|
|
127
|
+
<script>
|
|
128
|
+
${clientScript()}
|
|
129
|
+
</script>
|
|
130
|
+
</body>
|
|
131
|
+
</html>`;
|
|
132
|
+
}
|
|
133
|
+
// ─── CSS ──────────────────────────────────────────────────────────────────────
|
|
134
|
+
function pageCSS() {
|
|
135
|
+
return fs.readFileSync(path.join(__dirname, "../styles/theme.css"), "utf-8");
|
|
136
|
+
}
|
|
137
|
+
// ─── Client-side script ───────────────────────────────────────────────────────
|
|
138
|
+
function clientScript() {
|
|
139
|
+
return fs.readFileSync(path.join(__dirname, "../client/app.js"), "utf-8");
|
|
140
|
+
}
|
|
141
|
+
export { clientScript, pageCSS, renderNavItem, renderPage };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/renderer/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAOjE,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,YAAiB,GAAG,MAAM,CAatE;AAID,wBAAgB,aAAa,CAC5B,OAAO,EAAE,OAAO,EAChB,IAAI,GAAE,YAAiB,GACrB,MAAM,CAqXR"}
|