mr-md 2.0.0-beta → 2.1.0-beta
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 +1 -1
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +57 -29
- package/dist/client/app.js +55 -26
- package/dist/renderer/blocks.d.ts.map +1 -1
- package/dist/renderer/blocks.js +20 -11
- package/dist/renderer/html.d.ts +1 -0
- package/dist/renderer/html.d.ts.map +1 -1
- package/dist/renderer/html.js +25 -20
- package/dist/renderer/index.d.ts.map +1 -1
- package/dist/renderer/index.js +5 -91
- package/dist/renderer/markdown.d.ts.map +1 -1
- package/dist/renderer/markdown.js +44 -7
- package/dist/renderer/utils.d.ts.map +1 -1
- package/dist/renderer/utils.js +14 -5
- package/dist/styles/theme.css +239 -78
- package/dist/types.d.ts +4 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -5
- package/src/builder.ts +63 -32
- package/src/client/app.js +55 -26
- package/src/renderer/blocks.ts +20 -11
- package/src/renderer/html.ts +46 -23
- package/src/renderer/index.ts +13 -91
- package/src/renderer/markdown.ts +49 -9
- package/src/renderer/utils.ts +14 -5
- package/src/styles/theme.css +239 -78
- package/src/types.ts +4 -1
package/src/builder.ts
CHANGED
|
@@ -31,6 +31,10 @@ import type {
|
|
|
31
31
|
YouTubeOptions,
|
|
32
32
|
} from "./types.js";
|
|
33
33
|
|
|
34
|
+
import { fileURLToPath } from "url";
|
|
35
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
36
|
+
const __dirname = path.dirname(__filename);
|
|
37
|
+
|
|
34
38
|
function getCallerDir(): string | undefined {
|
|
35
39
|
const err = new Error();
|
|
36
40
|
const stack = err.stack?.split("\n");
|
|
@@ -47,24 +51,69 @@ function getCallerDir(): string | undefined {
|
|
|
47
51
|
p = p.replace(/^file:\/\//, "");
|
|
48
52
|
}
|
|
49
53
|
if (p.startsWith("/") && p[2] === ":") {
|
|
50
|
-
p = p.substring(1);
|
|
54
|
+
p = p.substring(1);
|
|
51
55
|
}
|
|
52
|
-
|
|
53
56
|
if (!builderFilePath) {
|
|
54
57
|
builderFilePath = p;
|
|
55
58
|
continue;
|
|
56
59
|
}
|
|
57
|
-
|
|
58
60
|
if (p === builderFilePath) {
|
|
59
61
|
continue;
|
|
60
62
|
}
|
|
61
|
-
|
|
62
63
|
return path.dirname(p);
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
return undefined;
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
function copyAssets(outDir: string) {
|
|
72
|
+
const assetsDir = path.join(outDir, "assets");
|
|
73
|
+
if (!fs.existsSync(assetsDir)) fs.mkdirSync(assetsDir, { recursive: true });
|
|
74
|
+
|
|
75
|
+
const srcStylesDir = path.join(__dirname, "styles");
|
|
76
|
+
const srcClientDir = path.join(__dirname, "client");
|
|
77
|
+
|
|
78
|
+
const fallbackStylesDir = path.join(__dirname, "../src/styles");
|
|
79
|
+
const fallbackClientDir = path.join(__dirname, "../src/client");
|
|
80
|
+
|
|
81
|
+
const copyDir = (src: string, dest: string) => {
|
|
82
|
+
if (!fs.existsSync(src)) return;
|
|
83
|
+
for (const file of fs.readdirSync(src)) {
|
|
84
|
+
fs.copyFileSync(path.join(src, file), path.join(dest, file));
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (fs.existsSync(srcStylesDir)) copyDir(srcStylesDir, assetsDir);
|
|
89
|
+
else copyDir(fallbackStylesDir, assetsDir);
|
|
90
|
+
|
|
91
|
+
if (fs.existsSync(srcClientDir)) copyDir(srcClientDir, assetsDir);
|
|
92
|
+
else copyDir(fallbackClientDir, assetsDir);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function mergeOptions(options: BuildOptions, callerDir?: string): BuildOptions {
|
|
96
|
+
const merged: BuildOptions = {
|
|
97
|
+
outDir: options.outDir ?? (callerDir ? path.join(callerDir, "out") : path.join(process.cwd(), "out")),
|
|
98
|
+
contentBase: options.contentBase ?? callerDir ?? process.cwd(),
|
|
99
|
+
theme: options.theme ?? "auto",
|
|
100
|
+
palette: options.palette ?? "ink",
|
|
101
|
+
strict: options.strict ?? process.env.NODE_ENV !== "development",
|
|
102
|
+
standalone: options.standalone ?? true,
|
|
103
|
+
};
|
|
104
|
+
for (const [k, v] of Object.entries(options)) {
|
|
105
|
+
if (v !== undefined && k !== "preset") {
|
|
106
|
+
(merged as any)[k] = v;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
merged.preset = {
|
|
110
|
+
layout: options.preset?.layout ?? "lesson",
|
|
111
|
+
density: options.preset?.density ?? "comfortable",
|
|
112
|
+
tone: options.preset?.tone ?? "scholarly",
|
|
113
|
+
};
|
|
114
|
+
return merged;
|
|
115
|
+
}
|
|
116
|
+
|
|
68
117
|
// ─── LessonBuilder ────────────────────────────────────────────────────────────
|
|
69
118
|
|
|
70
119
|
export class LessonBuilder {
|
|
@@ -86,20 +135,7 @@ export class LessonBuilder {
|
|
|
86
135
|
title,
|
|
87
136
|
slug,
|
|
88
137
|
};
|
|
89
|
-
this.options =
|
|
90
|
-
outDir: options.outDir ?? (callerDir ? path.join(callerDir, "out") : "./out"),
|
|
91
|
-
contentBase: options.contentBase ?? callerDir ?? ".",
|
|
92
|
-
theme: options.theme ?? "auto",
|
|
93
|
-
palette: options.palette ?? "ink",
|
|
94
|
-
strict: options.strict ?? process.env.NODE_ENV !== "development",
|
|
95
|
-
preset: {
|
|
96
|
-
layout: "lesson",
|
|
97
|
-
density: "comfortable",
|
|
98
|
-
tone: "scholarly",
|
|
99
|
-
...options.preset,
|
|
100
|
-
},
|
|
101
|
-
...options,
|
|
102
|
-
};
|
|
138
|
+
this.options = mergeOptions(options, callerDir);
|
|
103
139
|
}
|
|
104
140
|
|
|
105
141
|
// ── Meta setters ────────────────────────────────────────────────────────────
|
|
@@ -494,6 +530,10 @@ export class LessonBuilder {
|
|
|
494
530
|
const outPath = path.join(outDir, `${this.meta.slug}.html`);
|
|
495
531
|
fs.writeFileSync(outPath, html, "utf-8");
|
|
496
532
|
|
|
533
|
+
if (this.options.standalone === false) {
|
|
534
|
+
copyAssets(outDir);
|
|
535
|
+
}
|
|
536
|
+
|
|
497
537
|
const relPath = path.relative(process.cwd(), outPath);
|
|
498
538
|
console.log(` ✓ Built lesson (${this.blocks.length} blocks) → ${relPath}`);
|
|
499
539
|
return outPath;
|
|
@@ -658,20 +698,7 @@ export class ChapterBuilder {
|
|
|
658
698
|
title,
|
|
659
699
|
slug,
|
|
660
700
|
};
|
|
661
|
-
this.options =
|
|
662
|
-
outDir: options.outDir ?? (callerDir ? path.join(callerDir, "out") : "./out"),
|
|
663
|
-
contentBase: options.contentBase ?? callerDir ?? ".",
|
|
664
|
-
theme: options.theme ?? "auto",
|
|
665
|
-
palette: options.palette ?? "ink",
|
|
666
|
-
strict: options.strict ?? process.env.NODE_ENV !== "development",
|
|
667
|
-
preset: {
|
|
668
|
-
layout: "lesson",
|
|
669
|
-
density: "comfortable",
|
|
670
|
-
tone: "scholarly",
|
|
671
|
-
...options.preset,
|
|
672
|
-
},
|
|
673
|
-
...options,
|
|
674
|
-
};
|
|
701
|
+
this.options = mergeOptions(options, callerDir);
|
|
675
702
|
}
|
|
676
703
|
|
|
677
704
|
slug(slug: string): this {
|
|
@@ -728,6 +755,10 @@ export class ChapterBuilder {
|
|
|
728
755
|
const outPath = path.join(outDir, `${this.meta.slug}.html`);
|
|
729
756
|
fs.writeFileSync(outPath, html, "utf-8");
|
|
730
757
|
|
|
758
|
+
if (this.options.standalone === false) {
|
|
759
|
+
copyAssets(outDir);
|
|
760
|
+
}
|
|
761
|
+
|
|
731
762
|
const relPath = path.relative(process.cwd(), outPath);
|
|
732
763
|
console.log(
|
|
733
764
|
` ✓ Built chapter (${this.lessonBuilders.length} lessons) → ${relPath}`,
|
package/src/client/app.js
CHANGED
|
@@ -15,11 +15,10 @@ function bkSimDoc(js, props, loop, dependencies) {
|
|
|
15
15
|
";window.bkSetupCalled=false;" +
|
|
16
16
|
'window.bkCanvasPoint=function(e,c){const r=(c||e.currentTarget||e.target).getBoundingClientRect(),w=(c&&c.__bkLogicalW)||800,h=(c&&c.__bkLogicalH)||500;return{x:(e.clientX-r.left)*w/r.width,y:(e.clientY-r.top)*h/r.height}};' +
|
|
17
17
|
'window.bkFitCanvas=function(c,reqW,reqH,o){if(!c)return{scale:1,width:reqW,height:reqH,cssScale:1};const d=window.devicePixelRatio||1;const w=reqW;const h=reqH;c.__bkLogicalW=w;c.__bkLogicalH=h;c.style.width=w+"px";c.style.height=h+"px";c.style.position="relative";c.style.left="auto";c.style.top="auto";c.style.transformOrigin="center center";const sx=window.innerWidth/w,sy=window.innerHeight/h,cssS=Math.max(sx,sy);c.style.transform="scale("+cssS+")";const pw=Math.max(1,Math.round(w*d)),ph=Math.max(1,Math.round(h*d));if(!o||o.bitmap!==false){if(c.width!==pw||c.height!==ph){c.width=pw;c.height=ph}}return{scale:d,width:w,height:h,cssScale:cssS}};' +
|
|
18
|
-
'window.bkSetup=function(w,h,f){window.bkSetupCalled=true;const c=document.getElementById("c");if(!c)return;const ctx=c.getContext("2d");let loopId=null;let fit=window.bkFitCanvas(c,w,h);function l(){if(window.innerWidth>=32&&window.innerHeight>=32){ctx.save();ctx.scale(fit.scale,fit.scale);f(ctx,fit.width,fit.height);ctx.restore()}if(window.__loop){loopId=requestAnimationFrame(l)}else{loopId=null}}function i(){if(window.innerWidth>=32&&window.innerHeight>=32){fit=window.bkFitCanvas(c,w,h);l()}else{requestAnimationFrame(i)}}i();window.addEventListener("resize",function(){fit=window.bkFitCanvas(c,w,h);if(!window.__loop&&window.innerWidth>=32&&window.innerHeight>=32&&!loopId){ctx.save();ctx.scale(fit.scale,fit.scale);f(ctx,fit.width,fit.height);ctx.restore()}});window.addEventListener("message",function(event){if(!event.data)return;if(event.data.type==="bk:play"){window.__loop=true;if(!loopId)loopId=requestAnimationFrame(l)}else if(event.data.type==="bk:pause"){window.__loop=false}});};' +
|
|
19
|
-
'window.addEventListener("message",function(event){if(!event.data||event.data.type!=="bk:set-props")return;window.__simProps=Object.assign({},window.__simProps,event.data.props);window.dispatchEvent(new CustomEvent("bk:props",{detail:window.__simProps}));});' +
|
|
18
|
+
'window.bkSetup=function(w,h,f){window.bkSetupCalled=true;const c=document.getElementById("c");if(!c)return;const ctx=c.getContext("2d");let loopId=null;let fit=window.bkFitCanvas(c,w,h);function l(){if(window.innerWidth>=32&&window.innerHeight>=32){ctx.save();ctx.scale(fit.scale,fit.scale);f(ctx,fit.width,fit.height);ctx.restore()}if(window.__loop){loopId=requestAnimationFrame(l)}else{loopId=null}}function i(){if(window.innerWidth>=32&&window.innerHeight>=32){fit=window.bkFitCanvas(c,w,h);l()}else{requestAnimationFrame(i)}}i();window.addEventListener("resize",function(){fit=window.bkFitCanvas(c,w,h);if(!window.__loop&&window.innerWidth>=32&&window.innerHeight>=32&&!loopId){ctx.save();ctx.scale(fit.scale,fit.scale);f(ctx,fit.width,fit.height);ctx.restore()}});window.addEventListener("message",function(event){if(event.source!==window.parent)return;if(!event.data)return;if(event.data.type==="bk:play"){window.__loop=true;if(!loopId)loopId=requestAnimationFrame(l)}else if(event.data.type==="bk:pause"){window.__loop=false}else if(event.data.type==="bk:set-props"){window.__simProps=Object.assign({},window.__simProps,event.data.props);window.dispatchEvent(new CustomEvent("bk:props",{detail:window.__simProps}));}});};' +
|
|
20
19
|
"try{" +
|
|
21
20
|
js +
|
|
22
|
-
'}catch(e){console.error("Simulation Error:",e);document.
|
|
21
|
+
'}catch(e){console.error("Simulation Error:",e);const d=document.createElement("div");d.style.cssText="padding:20px;color:red;font-family:monospace";d.textContent="Error: "+e.message;document.body.innerHTML="";document.body.appendChild(d);}' +
|
|
23
22
|
"if(!window.bkSetupCalled){function fallbackScale(){window.bkFitCanvas(document.getElementById('c'),800,500,{bitmap:false});}fallbackScale();window.addEventListener('resize', fallbackScale);}" +
|
|
24
23
|
"</" + "script></body></html>"
|
|
25
24
|
);
|
|
@@ -76,27 +75,14 @@ function bkWireMaximizeControls() {
|
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
function bkWireInteractiveFrames() {
|
|
79
|
-
const
|
|
78
|
+
const interactiveHandler = (e) => {
|
|
80
79
|
const obj = e.target.closest?.(".bk-object");
|
|
81
|
-
|
|
82
|
-
const frame = obj.querySelector(".bk-embed-interactive");
|
|
83
|
-
if (frame) {
|
|
84
|
-
frame.classList.add("is-interactive");
|
|
85
|
-
const iframe = frame.querySelector("iframe");
|
|
86
|
-
if (iframe && iframe.contentWindow) {
|
|
87
|
-
iframe.contentWindow.postMessage({ type: "bk:play" }, "*");
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
document.addEventListener("pointerdown", activate, { passive: true });
|
|
92
|
-
document.addEventListener("focusin", activate, { passive: true });
|
|
80
|
+
const activateFrame = obj ? obj.querySelector(".bk-embed-interactive") : null;
|
|
93
81
|
|
|
94
|
-
const exitInteractive = (e) => {
|
|
95
82
|
document
|
|
96
83
|
.querySelectorAll(".bk-embed-interactive.is-interactive")
|
|
97
84
|
.forEach((frame) => {
|
|
98
|
-
|
|
99
|
-
if (!container.contains(e.target)) {
|
|
85
|
+
if (frame !== activateFrame) {
|
|
100
86
|
frame.classList.remove("is-interactive");
|
|
101
87
|
const iframe = frame.querySelector("iframe");
|
|
102
88
|
if (iframe && iframe.contentWindow) {
|
|
@@ -104,9 +90,17 @@ function bkWireInteractiveFrames() {
|
|
|
104
90
|
}
|
|
105
91
|
}
|
|
106
92
|
});
|
|
93
|
+
|
|
94
|
+
if (activateFrame && !activateFrame.classList.contains("is-interactive")) {
|
|
95
|
+
activateFrame.classList.add("is-interactive");
|
|
96
|
+
const iframe = activateFrame.querySelector("iframe");
|
|
97
|
+
if (iframe && iframe.contentWindow) {
|
|
98
|
+
iframe.contentWindow.postMessage({ type: "bk:play" }, "*");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
107
101
|
};
|
|
108
|
-
document.addEventListener("pointerdown",
|
|
109
|
-
document.addEventListener("focusin",
|
|
102
|
+
document.addEventListener("pointerdown", interactiveHandler, { passive: true });
|
|
103
|
+
document.addEventListener("focusin", interactiveHandler, { passive: true });
|
|
110
104
|
|
|
111
105
|
const obs = new IntersectionObserver((entries) => {
|
|
112
106
|
entries.forEach((e) => {
|
|
@@ -192,6 +186,9 @@ function bkBroadcastTheme(targetWindow) {
|
|
|
192
186
|
}
|
|
193
187
|
|
|
194
188
|
window.addEventListener("message", function(e) {
|
|
189
|
+
const isValidSource = Array.from(document.querySelectorAll("iframe")).some(f => f.contentWindow === e.source);
|
|
190
|
+
if (!isValidSource) return;
|
|
191
|
+
|
|
195
192
|
if (e.data && e.data.type === "bk:request-theme") {
|
|
196
193
|
// Small delay to ensure CSS has applied if this happens right on load
|
|
197
194
|
requestAnimationFrame(() => bkBroadcastTheme(e.source));
|
|
@@ -266,13 +263,16 @@ function bkWireThemeControls() {
|
|
|
266
263
|
|
|
267
264
|
if (savedTheme) {
|
|
268
265
|
updateThemeBtn(savedTheme);
|
|
266
|
+
root.setAttribute("data-theme", savedTheme);
|
|
269
267
|
}
|
|
270
268
|
if (savedPalette) {
|
|
271
269
|
const normalizedPalette = savedPalette === "green" ? "field" : savedPalette;
|
|
272
270
|
updatePaletteBtn(normalizedPalette);
|
|
271
|
+
root.setAttribute("data-palette", normalizedPalette);
|
|
273
272
|
}
|
|
274
273
|
if (savedUi) {
|
|
275
274
|
updateUiBtn(savedUi);
|
|
275
|
+
root.setAttribute("data-ui", savedUi);
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
button &&
|
|
@@ -344,15 +344,44 @@ function bkWireThemeControls() {
|
|
|
344
344
|
}
|
|
345
345
|
|
|
346
346
|
// Quiz interaction
|
|
347
|
-
// biome-ignore lint/correctness/noUnusedVariables: Used in generated HTML
|
|
348
347
|
function bkAnswer(btn, qid) {
|
|
349
|
-
// biome-ignore lint/correctness/noUnusedVariables: Kept for clarity
|
|
350
|
-
const isCorrect = btn.dataset.correct === "true";
|
|
351
348
|
const question = document.getElementById(qid);
|
|
349
|
+
if (!question) return;
|
|
350
|
+
const quiz = question.closest(".bk-quiz");
|
|
351
|
+
const dataEl = quiz ? quiz.querySelector(".bk-quiz-data") : null;
|
|
352
|
+
let isCorrect = false;
|
|
353
|
+
|
|
354
|
+
if (dataEl) {
|
|
355
|
+
try {
|
|
356
|
+
const answers = JSON.parse(dataEl.textContent || "[]");
|
|
357
|
+
// qid is format "quiz-IDX-qQI"
|
|
358
|
+
const match = qid.match(/-q(\d+)$/);
|
|
359
|
+
if (match) {
|
|
360
|
+
const qi = parseInt(match[1], 10);
|
|
361
|
+
const optIdx = parseInt(btn.dataset.optIdx, 10);
|
|
362
|
+
isCorrect = answers[qi] === optIdx;
|
|
363
|
+
}
|
|
364
|
+
} catch(e) {}
|
|
365
|
+
}
|
|
366
|
+
|
|
352
367
|
question.querySelectorAll(".bk-opt").forEach((b) => {
|
|
353
|
-
|
|
368
|
+
b.disabled = true; // Disable buttons for screen readers
|
|
369
|
+
const optIdx = parseInt(b.dataset.optIdx, 10);
|
|
370
|
+
// If we know the answer, highlight it
|
|
371
|
+
if (dataEl) {
|
|
372
|
+
try {
|
|
373
|
+
const answers = JSON.parse(dataEl.textContent || "[]");
|
|
374
|
+
const match = qid.match(/-q(\d+)$/);
|
|
375
|
+
if (match && answers[parseInt(match[1], 10)] === optIdx) {
|
|
376
|
+
b.classList.add("correct");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
} catch(e) {}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (b === btn && isCorrect) {
|
|
354
383
|
b.classList.add("correct");
|
|
355
|
-
} else if (b === btn) {
|
|
384
|
+
} else if (b === btn && !isCorrect) {
|
|
356
385
|
b.classList.add("wrong");
|
|
357
386
|
} else {
|
|
358
387
|
b.classList.add("disabled");
|
package/src/renderer/blocks.ts
CHANGED
|
@@ -234,7 +234,17 @@ function renderBlockInner(
|
|
|
234
234
|
loading="lazy"
|
|
235
235
|
style="width:100%;height:100%;border:none;display:block;">
|
|
236
236
|
</iframe>
|
|
237
|
-
</div
|
|
237
|
+
</div>
|
|
238
|
+
<script>
|
|
239
|
+
if (!window._bkYtBlurSetup) {
|
|
240
|
+
window._bkYtBlurSetup = true;
|
|
241
|
+
window.addEventListener('mousemove', function() {
|
|
242
|
+
if (document.activeElement && document.activeElement.tagName === 'IFRAME') {
|
|
243
|
+
document.activeElement.blur();
|
|
244
|
+
}
|
|
245
|
+
}, { passive: true });
|
|
246
|
+
}
|
|
247
|
+
</script>`,
|
|
238
248
|
"neutral",
|
|
239
249
|
),
|
|
240
250
|
};
|
|
@@ -264,7 +274,7 @@ function renderBlockInner(
|
|
|
264
274
|
block.label,
|
|
265
275
|
block.caption,
|
|
266
276
|
`<div class="bk-columns" style="grid-template-columns:${block.columns
|
|
267
|
-
.map((column) => column.width ?? "minmax(0, 1fr)")
|
|
277
|
+
.map((column) => escAttr(column.width ?? "minmax(0, 1fr)"))
|
|
268
278
|
.join(" ")}">
|
|
269
279
|
${block.columns
|
|
270
280
|
.map((column) => {
|
|
@@ -316,6 +326,7 @@ function renderBlockInner(
|
|
|
316
326
|
<div class="bk-quiz-body">
|
|
317
327
|
${quiz.questions.map((q, qi) => renderQuestion(q, `quiz-${idx}`, qi)).join("\n")}
|
|
318
328
|
</div>
|
|
329
|
+
<script type="application/json" class="bk-quiz-data">${escapeScriptJson(quiz.questions.map(q => q.answer))}</script>
|
|
319
330
|
</div>`,
|
|
320
331
|
navItems: [{
|
|
321
332
|
id: `quiz-${idx}`,
|
|
@@ -338,7 +349,7 @@ function renderQuestion(q: QuizQuestion, quizId: string, qi: number): string {
|
|
|
338
349
|
const options = q.options
|
|
339
350
|
.map(
|
|
340
351
|
(opt, oi) => `
|
|
341
|
-
<button class="bk-opt" data-
|
|
352
|
+
<button class="bk-opt" data-opt-idx="${oi}" onclick="bkAnswer(this,'${escAttr(qid)}')">
|
|
342
353
|
<span class="bk-opt-dot"></span><span class="bk-opt-text">${mdInline(opt)}</span>
|
|
343
354
|
</button>`,
|
|
344
355
|
)
|
|
@@ -514,7 +525,11 @@ try {
|
|
|
514
525
|
${js}
|
|
515
526
|
} catch (e) {
|
|
516
527
|
console.error("Simulation Error:", e);
|
|
517
|
-
|
|
528
|
+
const errDiv = document.createElement('div');
|
|
529
|
+
errDiv.style.cssText = "padding:20px;color:red;font-family:monospace";
|
|
530
|
+
errDiv.textContent = 'Error: ' + e.message;
|
|
531
|
+
document.body.innerHTML = '';
|
|
532
|
+
document.body.appendChild(errDiv);
|
|
518
533
|
}
|
|
519
534
|
if (!window.bkSetupCalled) {
|
|
520
535
|
function fallbackScale() {
|
|
@@ -525,13 +540,7 @@ if (!window.bkSetupCalled) {
|
|
|
525
540
|
}
|
|
526
541
|
</script>
|
|
527
542
|
</body></html>`;
|
|
528
|
-
|
|
529
|
-
return doc
|
|
530
|
-
.replace(/&/g, "&")
|
|
531
|
-
.replace(/"/g, """)
|
|
532
|
-
.replace(/'/g, "'")
|
|
533
|
-
.replace(/</g, "<")
|
|
534
|
-
.replace(/>/g, ">");
|
|
543
|
+
return escAttr(doc);
|
|
535
544
|
}
|
|
536
545
|
|
|
537
546
|
export { blockChrome, renderBlock, renderBlockInner };
|
package/src/renderer/html.ts
CHANGED
|
@@ -42,11 +42,13 @@ function renderEndNav(lesson: Lesson): string {
|
|
|
42
42
|
</nav>`;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
export function renderLayout(
|
|
46
|
+
title: string,
|
|
47
|
+
description: string | undefined,
|
|
48
|
+
navHtml: string,
|
|
49
|
+
contentHtml: string,
|
|
49
50
|
opts: BuildOptions,
|
|
51
|
+
extraSidebar: string = "",
|
|
50
52
|
): string {
|
|
51
53
|
const theme = opts.theme ?? "light";
|
|
52
54
|
const schemeAttr = `data-theme="${theme}"`;
|
|
@@ -56,8 +58,7 @@ function renderPage(
|
|
|
56
58
|
const tone = preset.tone ?? "scholarly";
|
|
57
59
|
const palette = opts.palette ?? "ink";
|
|
58
60
|
const ui = opts.ui ?? "standard";
|
|
59
|
-
const
|
|
60
|
-
const endNavHtml = renderEndNav(lesson);
|
|
61
|
+
const safeFont = opts.font ? opts.font.replace(/[;{}<>\\]/g, "") : "";
|
|
61
62
|
|
|
62
63
|
return `<!DOCTYPE html>
|
|
63
64
|
<html lang="en" data-palette="${palette}" data-ui="${ui}" ${schemeAttr}>
|
|
@@ -75,26 +76,21 @@ function renderPage(
|
|
|
75
76
|
</script>
|
|
76
77
|
<meta charset="UTF-8">
|
|
77
78
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
78
|
-
<title>${escHtml(
|
|
79
|
-
${
|
|
79
|
+
<title>${escHtml(title)}</title>
|
|
80
|
+
${description ? `<meta name="description" content="${escHtml(description)}">` : ""}
|
|
80
81
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.47/dist/katex.min.css">
|
|
81
82
|
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,650;9..144,760&family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&family=Archivo:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Syne:wght@600;700;800&family=Playfair+Display:ital,wght@0,400..700;1,400..700&family=Lora:ital,wght@0,400..700;1,400..700&display=swap" rel="stylesheet">
|
|
82
83
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/styles/github-dark.min.css">
|
|
83
84
|
${opts.head ?? ""}
|
|
84
|
-
|
|
85
|
-
${opts.font ? `:root { --font-sans: ${opts.font}, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }` : ""}
|
|
86
|
-
${pageCSS()}
|
|
87
|
-
</style>
|
|
85
|
+
${opts.standalone === false ? `<link rel="stylesheet" href="assets/theme.css?v=${Date.now()}">` : `<style>\n${safeFont ? `:root { --font-sans: ${safeFont}, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }` : ""}\n${pageCSS()}\n</style>`}
|
|
88
86
|
</head>
|
|
89
87
|
<body class="bk-layout-${layout} bk-density-${density} bk-tone-${tone}">
|
|
90
88
|
<div class="bk-shell">
|
|
91
89
|
<aside class="bk-sidebar">
|
|
92
90
|
<div class="bk-sidebar-inner">
|
|
93
91
|
<div class="bk-sidebar-header">
|
|
94
|
-
${
|
|
95
|
-
<div class="bk-sidebar-title">${escHtml(
|
|
96
|
-
${lesson.meta.author ? `<div class="bk-sidebar-author">By ${escHtml(lesson.meta.author)}</div>` : ""}
|
|
97
|
-
${lesson.meta.tags?.length ? `<div class="bk-tag-row">${lesson.meta.tags.map((tag) => `<span>${escHtml(tag)}</span>`).join("")}</div>` : ""}
|
|
92
|
+
${extraSidebar}
|
|
93
|
+
<div class="bk-sidebar-title">${escHtml(title)}</div>
|
|
98
94
|
</div>
|
|
99
95
|
<nav class="bk-nav">${navHtml}</nav>
|
|
100
96
|
<div class="bk-sidebar-footer">
|
|
@@ -156,6 +152,30 @@ ${pageCSS()}
|
|
|
156
152
|
<button class="bk-sidebar-expand" id="bk-sidebar-expand" type="button" aria-label="Expand sidebar">
|
|
157
153
|
<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>
|
|
158
154
|
</button>
|
|
155
|
+
${contentHtml}
|
|
156
|
+
</main>
|
|
157
|
+
</div>
|
|
158
|
+
${opts.standalone === false ? `<script src="assets/app.js?v=${Date.now()}"></script>` : `<script>\n${clientScript()}\n</script>`}
|
|
159
|
+
</body>
|
|
160
|
+
</html>`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function renderPage(
|
|
164
|
+
lesson: Lesson,
|
|
165
|
+
navItems: NavItem[],
|
|
166
|
+
bodyHtml: string,
|
|
167
|
+
opts: BuildOptions,
|
|
168
|
+
): string {
|
|
169
|
+
const navHtml = navItems.map(renderNavItem).join("\n");
|
|
170
|
+
const endNavHtml = renderEndNav(lesson);
|
|
171
|
+
|
|
172
|
+
const extraSidebar = `
|
|
173
|
+
${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>`}
|
|
174
|
+
`;
|
|
175
|
+
const authorHtml = lesson.meta.author ? `<div class="bk-sidebar-author">By ${escHtml(lesson.meta.author)}</div>` : "";
|
|
176
|
+
const tagsHtml = lesson.meta.tags?.length ? `<div class="bk-tag-row">${lesson.meta.tags.map((tag) => `<span>${escHtml(tag)}</span>`).join("")}</div>` : "";
|
|
177
|
+
|
|
178
|
+
const contentHtml = `
|
|
159
179
|
<article class="bk-content">
|
|
160
180
|
<header class="bk-hero">
|
|
161
181
|
<p class="bk-eyebrow">Interactive Lesson</p>
|
|
@@ -165,13 +185,16 @@ ${pageCSS()}
|
|
|
165
185
|
${bodyHtml}
|
|
166
186
|
${endNavHtml}
|
|
167
187
|
</article>
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
188
|
+
`;
|
|
189
|
+
|
|
190
|
+
return renderLayout(
|
|
191
|
+
lesson.meta.title,
|
|
192
|
+
lesson.meta.description,
|
|
193
|
+
navHtml,
|
|
194
|
+
contentHtml,
|
|
195
|
+
opts,
|
|
196
|
+
extraSidebar + authorHtml + tagsHtml
|
|
197
|
+
);
|
|
175
198
|
}
|
|
176
199
|
|
|
177
200
|
// ─── CSS ──────────────────────────────────────────────────────────────────────
|
package/src/renderer/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BuildOptions, Chapter, Lesson } from "../types.js";
|
|
2
2
|
import { escAttr, escHtml, renderBlock } from "./blocks.js";
|
|
3
|
-
import { clientScript, pageCSS, renderPage } from "./html.js"; // Used in renderChapter
|
|
3
|
+
import { clientScript, pageCSS, renderPage, renderLayout } from "./html.js"; // Used in renderChapter
|
|
4
4
|
import type { NavItem } from "./utils.js";
|
|
5
5
|
|
|
6
6
|
// ─── Main render function ─────────────────────────────────────────────────────
|
|
@@ -316,89 +316,7 @@ export function renderChapter(
|
|
|
316
316
|
}
|
|
317
317
|
`;
|
|
318
318
|
|
|
319
|
-
|
|
320
|
-
<html lang="en" data-palette="${palette}" data-ui="${ui}" ${schemeAttr}>
|
|
321
|
-
<head>
|
|
322
|
-
<meta charset="UTF-8">
|
|
323
|
-
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,650;9..144,760&family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&family=Archivo:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Syne:wght@600;700;800&family=Playfair+Display:ital,wght@0,400..700;1,400..700&family=Lora:ital,wght@0,400..700;1,400..700&display=swap" rel="stylesheet">
|
|
324
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
325
|
-
<title>${escHtml(chapter.meta.title)}</title>
|
|
326
|
-
${chapter.meta.description ? `<meta name="description" content="${escHtml(chapter.meta.description)}">` : ""}
|
|
327
|
-
${opts.head ?? ""}
|
|
328
|
-
<style>
|
|
329
|
-
${opts.font ? `:root { --font-sans: ${opts.font}, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }` : ""}
|
|
330
|
-
${pageCSS()}
|
|
331
|
-
${chapterStyles}
|
|
332
|
-
</style>
|
|
333
|
-
</head>
|
|
334
|
-
<body class="bk-layout-${layout} bk-density-${density} bk-tone-${tone}">
|
|
335
|
-
<div class="bk-shell">
|
|
336
|
-
<aside class="bk-sidebar">
|
|
337
|
-
<div class="bk-sidebar-inner">
|
|
338
|
-
<div class="bk-sidebar-header">
|
|
339
|
-
<div style="margin-top: 8px;"></div>
|
|
340
|
-
<div class="bk-sidebar-title">${escHtml(chapter.meta.title)}</div>
|
|
341
|
-
</div>
|
|
342
|
-
<nav class="bk-nav">${navHtml}</nav>
|
|
343
|
-
<div class="bk-sidebar-footer">
|
|
344
|
-
<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">
|
|
345
|
-
<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>
|
|
346
|
-
<span class="bk-sr-only">Display settings</span>
|
|
347
|
-
</button>
|
|
348
|
-
<div class="bk-theme-panel" id="bk-theme-panel" aria-label="Display settings" hidden>
|
|
349
|
-
<div class="bk-theme-row">
|
|
350
|
-
<span>Theme</span>
|
|
351
|
-
<div class="bk-segmented-control" id="bk-theme-icons">
|
|
352
|
-
<button type="button" class="bk-segment-btn ${theme === "light" ? "active" : ""}" data-theme="light" title="Light" aria-label="Light theme">
|
|
353
|
-
<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>
|
|
354
|
-
</button>
|
|
355
|
-
<button type="button" class="bk-segment-btn ${theme === "auto" ? "active" : (!theme ? "active" : "")}" data-theme="auto" title="System" aria-label="System theme">
|
|
356
|
-
<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>
|
|
357
|
-
</button>
|
|
358
|
-
<button type="button" class="bk-segment-btn ${theme === "dark" ? "active" : ""}" data-theme="dark" title="Dark" aria-label="Dark theme">
|
|
359
|
-
<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>
|
|
360
|
-
</button>
|
|
361
|
-
</div>
|
|
362
|
-
</div>
|
|
363
|
-
<div class="bk-theme-row">
|
|
364
|
-
<span>Palette</span>
|
|
365
|
-
<div class="bk-segmented-control" id="bk-palette-icons">
|
|
366
|
-
<button type="button" class="bk-segment-btn ${palette === "ink" ? "active" : ""}" data-palette="ink" title="Ink" aria-label="Ink palette">
|
|
367
|
-
<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>
|
|
368
|
-
</button>
|
|
369
|
-
<button type="button" class="bk-segment-btn ${palette === "field" ? "active" : ""}" data-palette="field" title="Field" aria-label="Field palette">
|
|
370
|
-
<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 12"></path></svg>
|
|
371
|
-
</button>
|
|
372
|
-
<button type="button" class="bk-segment-btn ${palette === "ember" ? "active" : ""}" data-palette="ember" title="Ember" aria-label="Ember palette">
|
|
373
|
-
<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>
|
|
374
|
-
</button>
|
|
375
|
-
</div>
|
|
376
|
-
</div>
|
|
377
|
-
<div class="bk-theme-row">
|
|
378
|
-
<span>UI</span>
|
|
379
|
-
<div class="bk-segmented-control" id="bk-ui-icons">
|
|
380
|
-
<button type="button" class="bk-segment-btn ${ui === 'standard' ? 'active' : ''}" data-ui="standard" title="Standard" aria-label="Standard UI">
|
|
381
|
-
<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="3" y="3" width="18" height="18" rx="4" ry="4"></rect><line x1="9" y1="3" x2="9" y2="21"></line></svg>
|
|
382
|
-
</button>
|
|
383
|
-
<button type="button" class="bk-segment-btn ${ui === 'neo' ? 'active' : ''}" data-ui="neo" title="Neo Brutalist" aria-label="Neo UI">
|
|
384
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><rect x="3" y="3" width="18" height="18"></rect><path d="M3 10h18"></path><path d="M10 10v11"></path></svg>
|
|
385
|
-
</button>
|
|
386
|
-
<button type="button" class="bk-segment-btn ${ui === 'playful' ? 'active' : ''}" data-ui="playful" title="Playful" aria-label="Playful UI">
|
|
387
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="6" ry="6"></rect><circle cx="8.5" cy="8.5" r="1.5" fill="currentColor"></circle><circle cx="15.5" cy="15.5" r="1.5" fill="currentColor"></circle></svg>
|
|
388
|
-
</button>
|
|
389
|
-
</div>
|
|
390
|
-
</div>
|
|
391
|
-
</div>
|
|
392
|
-
</div>
|
|
393
|
-
</div>
|
|
394
|
-
</aside>
|
|
395
|
-
<button class="bk-sidebar-collapse-floating" id="bk-sidebar-collapse" aria-label="Collapse sidebar">
|
|
396
|
-
<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>
|
|
397
|
-
</button>
|
|
398
|
-
<main class="bk-main">
|
|
399
|
-
<button class="bk-sidebar-expand" id="bk-sidebar-expand" type="button" aria-label="Expand sidebar">
|
|
400
|
-
<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>
|
|
401
|
-
</button>
|
|
319
|
+
const contentHtml = `
|
|
402
320
|
<article class="bk-content" style="max-width: 1000px; margin: 0 auto;">
|
|
403
321
|
<header class="bk-hero" style="border-bottom: none;">
|
|
404
322
|
<p class="bk-eyebrow">Chapter</p>
|
|
@@ -407,11 +325,15 @@ ${chapterStyles}
|
|
|
407
325
|
</header>
|
|
408
326
|
${timelineHtml}
|
|
409
327
|
</article>
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
328
|
+
`;
|
|
329
|
+
|
|
330
|
+
const headHtml = `<style>${chapterStyles}</style>`;
|
|
331
|
+
|
|
332
|
+
return renderLayout(
|
|
333
|
+
chapter.meta.title,
|
|
334
|
+
chapter.meta.description,
|
|
335
|
+
navHtml,
|
|
336
|
+
contentHtml,
|
|
337
|
+
{ ...opts, head: (opts.head ?? "") + headHtml }
|
|
338
|
+
);
|
|
417
339
|
}
|