mr-md 1.0.0 → 1.0.2
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/dist/builder.d.ts +4 -3
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +55 -18
- package/dist/renderer/blocks.d.ts +2 -2
- package/dist/renderer/blocks.d.ts.map +1 -1
- package/dist/renderer/blocks.js +22 -12
- package/dist/renderer/index.js +3 -3
- package/dist/renderer/markdown.d.ts +5 -0
- package/dist/renderer/markdown.d.ts.map +1 -1
- package/dist/renderer/markdown.js +13 -2
- package/dist/renderer/utils.d.ts.map +1 -1
- package/dist/renderer/utils.js +13 -2
- package/package.json +1 -1
- package/src/builder.ts +61 -18
- package/src/renderer/blocks.ts +21 -11
- package/src/renderer/index.ts +3 -3
- package/src/renderer/markdown.ts +16 -3
- package/src/renderer/utils.ts +14 -2
package/dist/builder.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare class LessonBuilder {
|
|
|
4
4
|
private blocks;
|
|
5
5
|
private options;
|
|
6
6
|
private _rawOptions;
|
|
7
|
-
constructor(title: string, options?: BuildOptions);
|
|
7
|
+
constructor(title: string, options?: BuildOptions, callerDir?: string);
|
|
8
8
|
/**
|
|
9
9
|
* Sets the URL slug for the generated HTML file.
|
|
10
10
|
* Automatically generated from the title by default.
|
|
@@ -26,7 +26,7 @@ export declare class LessonBuilder {
|
|
|
26
26
|
/** Curated production defaults for the generated lesson shell. */
|
|
27
27
|
preset(preset: NonNullable<BuildOptions["preset"]>): this;
|
|
28
28
|
/** @internal Used by ChapterBuilder to push down shared config */
|
|
29
|
-
_inheritOptions(parentOpts: BuildOptions): void;
|
|
29
|
+
_inheritOptions(parentOpts: BuildOptions, parentRawOpts?: BuildOptions): void;
|
|
30
30
|
/** @internal Used by ChapterBuilder */
|
|
31
31
|
_setParentSlug(slug: string): void;
|
|
32
32
|
/** @internal Used by ChapterBuilder */
|
|
@@ -146,7 +146,8 @@ export declare class ChapterBuilder {
|
|
|
146
146
|
private meta;
|
|
147
147
|
private lessonBuilders;
|
|
148
148
|
private options;
|
|
149
|
-
|
|
149
|
+
private _rawOptions;
|
|
150
|
+
constructor(title: string, options?: BuildOptions, callerDir?: string);
|
|
150
151
|
slug(slug: string): this;
|
|
151
152
|
description(text: string): this;
|
|
152
153
|
status(status: "completed" | "active" | "locked"): this;
|
package/dist/builder.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAEX,gBAAgB,EAEhB,YAAY,EACZ,YAAY,EACZ,OAAO,EAGP,UAAU,EAEV,cAAc,EAId,YAAY,EACZ,MAAM,EACN,UAAU,EAGV,YAAY,EACZ,SAAS,EAIT,iBAAiB,EAEjB,cAAc,EACd,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"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"}
|
package/dist/builder.js
CHANGED
|
@@ -1,24 +1,56 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { render, renderChapter } from "./renderer/index.js";
|
|
4
|
+
function getCallerDir() {
|
|
5
|
+
const err = new Error();
|
|
6
|
+
const stack = err.stack?.split("\n");
|
|
7
|
+
if (!stack || stack.length < 2)
|
|
8
|
+
return undefined;
|
|
9
|
+
let builderFilePath;
|
|
10
|
+
for (let i = 1; i < stack.length; i++) {
|
|
11
|
+
const line = stack[i];
|
|
12
|
+
const match = line.match(/\((.*?):\d+:\d+\)/) || line.match(/at (.*?):\d+:\d+/);
|
|
13
|
+
if (match) {
|
|
14
|
+
let p = match[1];
|
|
15
|
+
if (p.startsWith("file://")) {
|
|
16
|
+
p = p.replace(/^file:\/\//, "");
|
|
17
|
+
}
|
|
18
|
+
if (p.startsWith("/") && p[2] === ":") {
|
|
19
|
+
p = p.substring(1); // Handle Windows paths like /C:/
|
|
20
|
+
}
|
|
21
|
+
if (!builderFilePath) {
|
|
22
|
+
builderFilePath = p;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (p === builderFilePath) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
return path.dirname(p);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
4
33
|
// ─── LessonBuilder ────────────────────────────────────────────────────────────
|
|
5
34
|
export class LessonBuilder {
|
|
6
35
|
meta;
|
|
7
36
|
blocks = [];
|
|
8
37
|
options;
|
|
9
38
|
_rawOptions;
|
|
10
|
-
constructor(title, options = {}) {
|
|
39
|
+
constructor(title, options = {}, callerDir) {
|
|
11
40
|
this._rawOptions = options;
|
|
41
|
+
let slug = title
|
|
42
|
+
.toLowerCase()
|
|
43
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
44
|
+
.replace(/(^-|-$)/g, "");
|
|
45
|
+
if (!slug)
|
|
46
|
+
slug = "lesson";
|
|
12
47
|
this.meta = {
|
|
13
48
|
title,
|
|
14
|
-
slug
|
|
15
|
-
.toLowerCase()
|
|
16
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
17
|
-
.replace(/(^-|-$)/g, ""),
|
|
49
|
+
slug,
|
|
18
50
|
};
|
|
19
51
|
this.options = {
|
|
20
52
|
outDir: options.outDir ?? "./out",
|
|
21
|
-
contentBase: options.contentBase ?? ".",
|
|
53
|
+
contentBase: options.contentBase ?? callerDir ?? ".",
|
|
22
54
|
theme: options.theme ?? "auto",
|
|
23
55
|
palette: options.palette ?? "ink",
|
|
24
56
|
strict: options.strict ?? true,
|
|
@@ -75,11 +107,11 @@ export class LessonBuilder {
|
|
|
75
107
|
return this;
|
|
76
108
|
}
|
|
77
109
|
/** @internal Used by ChapterBuilder to push down shared config */
|
|
78
|
-
_inheritOptions(parentOpts) {
|
|
110
|
+
_inheritOptions(parentOpts, parentRawOpts) {
|
|
79
111
|
this.options = {
|
|
80
|
-
outDir: this._rawOptions.outDir ?? parentOpts.outDir ?? this.options.outDir,
|
|
112
|
+
outDir: this._rawOptions.outDir ?? (parentRawOpts?.outDir || parentOpts.outDir) ?? this.options.outDir,
|
|
81
113
|
contentBase: this._rawOptions.contentBase ??
|
|
82
|
-
|
|
114
|
+
parentRawOpts?.contentBase ??
|
|
83
115
|
this.options.contentBase,
|
|
84
116
|
theme: this._rawOptions.theme ?? parentOpts.theme ?? this.options.theme,
|
|
85
117
|
palette: this._rawOptions.palette ?? parentOpts.palette ?? this.options.palette,
|
|
@@ -359,7 +391,7 @@ export class LessonBuilder {
|
|
|
359
391
|
* @example const l = lesson("Introduction to Kinematics").markdown("intro.md");
|
|
360
392
|
*/
|
|
361
393
|
export function lesson(title, options = {}) {
|
|
362
|
-
return new LessonBuilder(title, options);
|
|
394
|
+
return new LessonBuilder(title, options, getCallerDir());
|
|
363
395
|
}
|
|
364
396
|
function normalizeSimulationOptions(opts, legacyHeight, fileConfig = null) {
|
|
365
397
|
let inline;
|
|
@@ -448,17 +480,22 @@ export class ChapterBuilder {
|
|
|
448
480
|
meta;
|
|
449
481
|
lessonBuilders = [];
|
|
450
482
|
options;
|
|
451
|
-
|
|
483
|
+
_rawOptions;
|
|
484
|
+
constructor(title, options = {}, callerDir) {
|
|
485
|
+
this._rawOptions = options;
|
|
486
|
+
let slug = title
|
|
487
|
+
.toLowerCase()
|
|
488
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
489
|
+
.replace(/(^-|-$)/g, "");
|
|
490
|
+
if (!slug)
|
|
491
|
+
slug = "chapter";
|
|
452
492
|
this.meta = {
|
|
453
493
|
title,
|
|
454
|
-
slug
|
|
455
|
-
.toLowerCase()
|
|
456
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
457
|
-
.replace(/(^-|-$)/g, ""),
|
|
494
|
+
slug,
|
|
458
495
|
};
|
|
459
496
|
this.options = {
|
|
460
497
|
outDir: options.outDir ?? "./out",
|
|
461
|
-
contentBase: options.contentBase ?? ".",
|
|
498
|
+
contentBase: options.contentBase ?? callerDir ?? ".",
|
|
462
499
|
theme: options.theme ?? "auto",
|
|
463
500
|
palette: options.palette ?? "ink",
|
|
464
501
|
strict: options.strict ?? true,
|
|
@@ -484,7 +521,7 @@ export class ChapterBuilder {
|
|
|
484
521
|
return this;
|
|
485
522
|
}
|
|
486
523
|
lesson(lessonBuilder) {
|
|
487
|
-
lessonBuilder._inheritOptions(this.options);
|
|
524
|
+
lessonBuilder._inheritOptions(this.options, this._rawOptions);
|
|
488
525
|
lessonBuilder._setParentSlug(this.meta.slug);
|
|
489
526
|
this.lessonBuilders.push(lessonBuilder);
|
|
490
527
|
return this;
|
|
@@ -541,5 +578,5 @@ export class ChapterBuilder {
|
|
|
541
578
|
* .build();
|
|
542
579
|
*/
|
|
543
580
|
export function chapter(title, options = {}) {
|
|
544
|
-
return new ChapterBuilder(title, options);
|
|
581
|
+
return new ChapterBuilder(title, options, getCallerDir());
|
|
545
582
|
}
|
|
@@ -5,11 +5,11 @@ export declare function escHtml(str: string): string;
|
|
|
5
5
|
export declare function escAttr(str: string): string;
|
|
6
6
|
declare function renderBlock(block: Block, idx: number, options: BuildOptions): {
|
|
7
7
|
html: string;
|
|
8
|
-
|
|
8
|
+
navItems?: NavItem[];
|
|
9
9
|
};
|
|
10
10
|
declare function renderBlockInner(block: Block, idx: number, options: BuildOptions): {
|
|
11
11
|
html: string;
|
|
12
|
-
|
|
12
|
+
navItems?: NavItem[];
|
|
13
13
|
};
|
|
14
14
|
export { blockChrome, renderBlock, renderBlockInner };
|
|
15
15
|
//# sourceMappingURL=blocks.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blocks.d.ts","sourceRoot":"","sources":["../../src/renderer/blocks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,EAA0B,MAAM,aAAa,CAAC;AAC/E,OAAO,EACN,WAAW,EAKX,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,KAAK,OAAO,EAAmC,MAAM,YAAY,CAAC;AAG3E,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO3C;AACD,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE3C;AAID,iBAAS,WAAW,CACnB,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,YAAY,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,
|
|
1
|
+
{"version":3,"file":"blocks.d.ts","sourceRoot":"","sources":["../../src/renderer/blocks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,EAA0B,MAAM,aAAa,CAAC;AAC/E,OAAO,EACN,WAAW,EAKX,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,KAAK,OAAO,EAAmC,MAAM,YAAY,CAAC;AAG3E,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO3C;AACD,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE3C;AAID,iBAAS,WAAW,CACnB,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,YAAY,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;CAAE,CAuBxC;AAED,iBAAS,gBAAgB,CACxB,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,YAAY,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;CAAE,CA0QxC;AAuID,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,gBAAgB,EAAE,CAAC"}
|
package/dist/renderer/blocks.js
CHANGED
|
@@ -42,13 +42,18 @@ function renderBlockInner(block, idx, options) {
|
|
|
42
42
|
const id = `heading-${idx}`;
|
|
43
43
|
return {
|
|
44
44
|
html: `<section id="${id}" class="bk-section bk-heading">${html}</section>`,
|
|
45
|
-
|
|
45
|
+
navItems: [{ id, label, kind: "heading" }],
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
48
|
case "markdown": {
|
|
49
49
|
const md = resolveContent(block.src, options, "md");
|
|
50
|
-
const { html } = mdToHtml(md);
|
|
51
|
-
|
|
50
|
+
const { html, headings } = mdToHtml(md);
|
|
51
|
+
const navItems = headings.map(h => ({
|
|
52
|
+
id: h.id,
|
|
53
|
+
label: h.text,
|
|
54
|
+
kind: h.level === 2 ? "heading" : "section"
|
|
55
|
+
}));
|
|
56
|
+
return { html: `<div class="bk-markdown">${html}</div>`, navItems: navItems.length > 0 ? navItems : undefined };
|
|
52
57
|
}
|
|
53
58
|
case "section": {
|
|
54
59
|
const md = resolveContent(block.src, options, "md");
|
|
@@ -57,7 +62,7 @@ function renderBlockInner(block, idx, options) {
|
|
|
57
62
|
const id = `section-${idx}`;
|
|
58
63
|
return {
|
|
59
64
|
html: `<section id="${id}" class="bk-section bk-subsection">${html}</section>`,
|
|
60
|
-
|
|
65
|
+
navItems: [{ id, label, kind: "section" }],
|
|
61
66
|
};
|
|
62
67
|
}
|
|
63
68
|
case "important":
|
|
@@ -85,8 +90,9 @@ function renderBlockInner(block, idx, options) {
|
|
|
85
90
|
}
|
|
86
91
|
case "code": {
|
|
87
92
|
const raw = resolveContent(block.src, options, "text"); // Could be file or inline
|
|
93
|
+
const isInlineCode = typeof block.src === "string" && (block.src.includes("\n") || block.src.includes(" "));
|
|
88
94
|
const lang = block.lang ??
|
|
89
|
-
(typeof block.src === "string" && block.src.includes(".")
|
|
95
|
+
(typeof block.src === "string" && !isInlineCode && block.src.includes(".")
|
|
90
96
|
? (block.src.split(".").pop() ?? "")
|
|
91
97
|
: "");
|
|
92
98
|
let highlighted = escHtml(raw);
|
|
@@ -106,7 +112,7 @@ function renderBlockInner(block, idx, options) {
|
|
|
106
112
|
};
|
|
107
113
|
}
|
|
108
114
|
case "simulation": {
|
|
109
|
-
const propsJson =
|
|
115
|
+
const propsJson = escapeScriptJson(block.props ?? {});
|
|
110
116
|
const simSrc = resolveContent(block.src, options, "js");
|
|
111
117
|
const simConfig = { js: simSrc, loop: false, dependencies: block.dependencies };
|
|
112
118
|
return {
|
|
@@ -200,7 +206,11 @@ function renderBlockInner(block, idx, options) {
|
|
|
200
206
|
let quiz = { questions: [] };
|
|
201
207
|
const rawJson = resolveContent(block.src, options, "json");
|
|
202
208
|
try {
|
|
203
|
-
|
|
209
|
+
const trimmed = rawJson.trim();
|
|
210
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
211
|
+
throw new Error("Quiz file not found or invalid JSON format");
|
|
212
|
+
}
|
|
213
|
+
quiz = JSON.parse(trimmed);
|
|
204
214
|
}
|
|
205
215
|
catch (e) {
|
|
206
216
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -222,11 +232,11 @@ function renderBlockInner(block, idx, options) {
|
|
|
222
232
|
${quiz.questions.map((q, qi) => renderQuestion(q, `quiz-${idx}`, qi)).join("\n")}
|
|
223
233
|
</div>
|
|
224
234
|
</div>`,
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
235
|
+
navItems: [{
|
|
236
|
+
id: `quiz-${idx}`,
|
|
237
|
+
label: block.label ?? "Questions",
|
|
238
|
+
kind: "quiz",
|
|
239
|
+
}],
|
|
230
240
|
};
|
|
231
241
|
}
|
|
232
242
|
case "divider":
|
package/dist/renderer/index.js
CHANGED
|
@@ -5,10 +5,10 @@ export function render(lesson, opts = {}) {
|
|
|
5
5
|
const bodyItems = [];
|
|
6
6
|
const structuredNavItems = [];
|
|
7
7
|
lesson.blocks.forEach((block, idx) => {
|
|
8
|
-
const { html,
|
|
8
|
+
const { html, navItems } = renderBlock(block, idx, opts);
|
|
9
9
|
bodyItems.push(html);
|
|
10
|
-
if (
|
|
11
|
-
structuredNavItems.push(
|
|
10
|
+
if (navItems) {
|
|
11
|
+
structuredNavItems.push(...navItems);
|
|
12
12
|
}
|
|
13
13
|
});
|
|
14
14
|
return renderPage(lesson, structuredNavItems, bodyItems.join("\n"), opts);
|
|
@@ -2,6 +2,11 @@ import type { Block } from "../types.js";
|
|
|
2
2
|
declare function mdToHtml(md: string): {
|
|
3
3
|
html: string;
|
|
4
4
|
title: string;
|
|
5
|
+
headings: {
|
|
6
|
+
id: string;
|
|
7
|
+
text: string;
|
|
8
|
+
level: number;
|
|
9
|
+
}[];
|
|
5
10
|
};
|
|
6
11
|
declare function blockChrome(kind: string, label: string | undefined, caption: string | undefined, body: string, accent?: string, allowMaximize?: boolean): string;
|
|
7
12
|
declare function mdInline(text: string): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/renderer/markdown.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAYzC,iBAAS,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/renderer/markdown.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAYzC,iBAAS,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAAE,CAmFtH;AAeD,iBAAS,WAAW,CACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,IAAI,EAAE,MAAM,EACZ,MAAM,SAAY,EAClB,aAAa,UAAO,GAClB,MAAM,CAYR;AAED,iBAAS,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAmBtC;AAED,iBAAS,wBAAwB,CAChC,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC,GAC3C,MAAM,CAiCR;AAED,iBAAS,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAIhD;AAED,OAAO,EACN,WAAW,EACX,gBAAgB,EAChB,QAAQ,EACR,QAAQ,EACR,wBAAwB,GACxB,CAAC"}
|
|
@@ -41,7 +41,18 @@ function mdToHtml(md) {
|
|
|
41
41
|
codeBlocks.forEach((match, id) => {
|
|
42
42
|
processedMd = processedMd.replace(`@@BK_CODE_${id}@@`, () => match);
|
|
43
43
|
});
|
|
44
|
-
|
|
44
|
+
const headings = [];
|
|
45
|
+
let headingIdCounter = 0;
|
|
46
|
+
const renderer = new marked.Renderer();
|
|
47
|
+
renderer.heading = ({ tokens, depth, text }) => {
|
|
48
|
+
const id = `bk-heading-${headingIdCounter++}`;
|
|
49
|
+
if (depth === 2 || depth === 3) {
|
|
50
|
+
const plainText = text.replace(/<[^>]+>/g, "");
|
|
51
|
+
headings.push({ id, text: plainText, level: depth });
|
|
52
|
+
}
|
|
53
|
+
return `<h${depth} id="${id}" class="bk-heading-${depth}">${text}</h${depth}>`;
|
|
54
|
+
};
|
|
55
|
+
let html = marked.parse(processedMd, { renderer });
|
|
45
56
|
// Restore math
|
|
46
57
|
mathBlocks.forEach((tex, id) => {
|
|
47
58
|
const rendered = katex.renderToString(tex, {
|
|
@@ -60,7 +71,7 @@ function mdToHtml(md) {
|
|
|
60
71
|
});
|
|
61
72
|
html = html.replace(`@@BK_MATH_INLINE_${id}@@`, () => rendered);
|
|
62
73
|
});
|
|
63
|
-
return { html, title };
|
|
74
|
+
return { html, title, headings };
|
|
64
75
|
}
|
|
65
76
|
function escHtml(s) {
|
|
66
77
|
return s
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/renderer/utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,OAAO;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;CACrC;AAID,iBAAS,cAAc,CACtB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,YAAY,EACrB,YAAY,GAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAAG,MAAe,GAClD,MAAM,
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/renderer/utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,OAAO;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;CACrC;AAID,iBAAS,cAAc,CACtB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,YAAY,EACrB,YAAY,GAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAAG,MAAe,GAClD,MAAM,CAmCR;AAED,iBAAS,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,MAAM,CAwCnE;AAED,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,CAAC"}
|
package/dist/renderer/utils.js
CHANGED
|
@@ -4,6 +4,12 @@ import * as path from "path";
|
|
|
4
4
|
function resolveContent(src, options, expectedType = "text") {
|
|
5
5
|
if (src.includes("\n"))
|
|
6
6
|
return src;
|
|
7
|
+
if (/^https?:\/\//.test(src)) {
|
|
8
|
+
if (options.strict !== false) {
|
|
9
|
+
throw new Error(`Remote URLs are not yet supported for content files: ${src}`);
|
|
10
|
+
}
|
|
11
|
+
return src;
|
|
12
|
+
}
|
|
7
13
|
const isLikelyFilePath = (expectedType !== "text" && src.endsWith(`.${expectedType}`)) ||
|
|
8
14
|
src.startsWith("/") ||
|
|
9
15
|
src.startsWith("./") ||
|
|
@@ -24,9 +30,14 @@ function resolveContent(src, options, expectedType = "text") {
|
|
|
24
30
|
return src;
|
|
25
31
|
}
|
|
26
32
|
function resolveAssetSrc(src, options) {
|
|
27
|
-
if (/^(https?:|data
|
|
33
|
+
if (/^(https?:|data:)/.test(src))
|
|
34
|
+
return src;
|
|
35
|
+
const isWebAbsolute = src.startsWith("/") && !fs.existsSync(src);
|
|
36
|
+
if (isWebAbsolute)
|
|
28
37
|
return src;
|
|
29
|
-
const filePath = path.
|
|
38
|
+
const filePath = path.isAbsolute(src)
|
|
39
|
+
? src
|
|
40
|
+
: path.resolve(options.contentBase ?? ".", src);
|
|
30
41
|
if (!fs.existsSync(filePath)) {
|
|
31
42
|
if (options.strict !== false)
|
|
32
43
|
throw new Error(`Missing media asset: ${filePath}`);
|
package/package.json
CHANGED
package/src/builder.ts
CHANGED
|
@@ -31,6 +31,40 @@ import type {
|
|
|
31
31
|
YouTubeOptions,
|
|
32
32
|
} from "./types.js";
|
|
33
33
|
|
|
34
|
+
function getCallerDir(): string | undefined {
|
|
35
|
+
const err = new Error();
|
|
36
|
+
const stack = err.stack?.split("\n");
|
|
37
|
+
if (!stack || stack.length < 2) return undefined;
|
|
38
|
+
|
|
39
|
+
let builderFilePath: string | undefined;
|
|
40
|
+
|
|
41
|
+
for (let i = 1; i < stack.length; i++) {
|
|
42
|
+
const line = stack[i];
|
|
43
|
+
const match = line.match(/\((.*?):\d+:\d+\)/) || line.match(/at (.*?):\d+:\d+/);
|
|
44
|
+
if (match) {
|
|
45
|
+
let p = match[1];
|
|
46
|
+
if (p.startsWith("file://")) {
|
|
47
|
+
p = p.replace(/^file:\/\//, "");
|
|
48
|
+
}
|
|
49
|
+
if (p.startsWith("/") && p[2] === ":") {
|
|
50
|
+
p = p.substring(1); // Handle Windows paths like /C:/
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!builderFilePath) {
|
|
54
|
+
builderFilePath = p;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (p === builderFilePath) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return path.dirname(p);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
34
68
|
// ─── LessonBuilder ────────────────────────────────────────────────────────────
|
|
35
69
|
|
|
36
70
|
export class LessonBuilder {
|
|
@@ -39,18 +73,22 @@ export class LessonBuilder {
|
|
|
39
73
|
private options: BuildOptions;
|
|
40
74
|
private _rawOptions: BuildOptions;
|
|
41
75
|
|
|
42
|
-
constructor(title: string, options: BuildOptions = {}) {
|
|
76
|
+
constructor(title: string, options: BuildOptions = {}, callerDir?: string) {
|
|
43
77
|
this._rawOptions = options;
|
|
78
|
+
|
|
79
|
+
let slug = title
|
|
80
|
+
.toLowerCase()
|
|
81
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
82
|
+
.replace(/(^-|-$)/g, "");
|
|
83
|
+
if (!slug) slug = "lesson";
|
|
84
|
+
|
|
44
85
|
this.meta = {
|
|
45
86
|
title,
|
|
46
|
-
slug
|
|
47
|
-
.toLowerCase()
|
|
48
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
49
|
-
.replace(/(^-|-$)/g, ""),
|
|
87
|
+
slug,
|
|
50
88
|
};
|
|
51
89
|
this.options = {
|
|
52
90
|
outDir: options.outDir ?? "./out",
|
|
53
|
-
contentBase: options.contentBase ?? ".",
|
|
91
|
+
contentBase: options.contentBase ?? callerDir ?? ".",
|
|
54
92
|
theme: options.theme ?? "auto",
|
|
55
93
|
palette: options.palette ?? "ink",
|
|
56
94
|
strict: options.strict ?? true,
|
|
@@ -115,13 +153,13 @@ export class LessonBuilder {
|
|
|
115
153
|
}
|
|
116
154
|
|
|
117
155
|
/** @internal Used by ChapterBuilder to push down shared config */
|
|
118
|
-
_inheritOptions(parentOpts: BuildOptions) {
|
|
156
|
+
_inheritOptions(parentOpts: BuildOptions, parentRawOpts?: BuildOptions) {
|
|
119
157
|
this.options = {
|
|
120
158
|
outDir:
|
|
121
|
-
this._rawOptions.outDir ?? parentOpts.outDir ?? this.options.outDir,
|
|
159
|
+
this._rawOptions.outDir ?? (parentRawOpts?.outDir || parentOpts.outDir) ?? this.options.outDir,
|
|
122
160
|
contentBase:
|
|
123
161
|
this._rawOptions.contentBase ??
|
|
124
|
-
|
|
162
|
+
parentRawOpts?.contentBase ??
|
|
125
163
|
this.options.contentBase,
|
|
126
164
|
theme: this._rawOptions.theme ?? parentOpts.theme ?? this.options.theme,
|
|
127
165
|
palette:
|
|
@@ -476,7 +514,7 @@ export function lesson(
|
|
|
476
514
|
title: string,
|
|
477
515
|
options: BuildOptions = {},
|
|
478
516
|
): LessonBuilder {
|
|
479
|
-
return new LessonBuilder(title, options);
|
|
517
|
+
return new LessonBuilder(title, options, getCallerDir());
|
|
480
518
|
}
|
|
481
519
|
|
|
482
520
|
function normalizeSimulationOptions(
|
|
@@ -594,18 +632,23 @@ export class ChapterBuilder {
|
|
|
594
632
|
private meta: ChapterMeta;
|
|
595
633
|
private lessonBuilders: LessonBuilder[] = [];
|
|
596
634
|
private options: BuildOptions;
|
|
635
|
+
private _rawOptions: BuildOptions;
|
|
636
|
+
|
|
637
|
+
constructor(title: string, options: BuildOptions = {}, callerDir?: string) {
|
|
638
|
+
this._rawOptions = options;
|
|
639
|
+
let slug = title
|
|
640
|
+
.toLowerCase()
|
|
641
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
642
|
+
.replace(/(^-|-$)/g, "");
|
|
643
|
+
if (!slug) slug = "chapter";
|
|
597
644
|
|
|
598
|
-
constructor(title: string, options: BuildOptions = {}) {
|
|
599
645
|
this.meta = {
|
|
600
646
|
title,
|
|
601
|
-
slug
|
|
602
|
-
.toLowerCase()
|
|
603
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
604
|
-
.replace(/(^-|-$)/g, ""),
|
|
647
|
+
slug,
|
|
605
648
|
};
|
|
606
649
|
this.options = {
|
|
607
650
|
outDir: options.outDir ?? "./out",
|
|
608
|
-
contentBase: options.contentBase ?? ".",
|
|
651
|
+
contentBase: options.contentBase ?? callerDir ?? ".",
|
|
609
652
|
theme: options.theme ?? "auto",
|
|
610
653
|
palette: options.palette ?? "ink",
|
|
611
654
|
strict: options.strict ?? true,
|
|
@@ -635,7 +678,7 @@ export class ChapterBuilder {
|
|
|
635
678
|
}
|
|
636
679
|
|
|
637
680
|
lesson(lessonBuilder: LessonBuilder): this {
|
|
638
|
-
lessonBuilder._inheritOptions(this.options);
|
|
681
|
+
lessonBuilder._inheritOptions(this.options, this._rawOptions);
|
|
639
682
|
lessonBuilder._setParentSlug(this.meta.slug);
|
|
640
683
|
this.lessonBuilders.push(lessonBuilder);
|
|
641
684
|
return this;
|
|
@@ -704,5 +747,5 @@ export function chapter(
|
|
|
704
747
|
title: string,
|
|
705
748
|
options: BuildOptions = {},
|
|
706
749
|
): ChapterBuilder {
|
|
707
|
-
return new ChapterBuilder(title, options);
|
|
750
|
+
return new ChapterBuilder(title, options, getCallerDir());
|
|
708
751
|
}
|
package/src/renderer/blocks.ts
CHANGED
|
@@ -29,7 +29,7 @@ function renderBlock(
|
|
|
29
29
|
block: Block,
|
|
30
30
|
idx: number,
|
|
31
31
|
options: BuildOptions,
|
|
32
|
-
): { html: string;
|
|
32
|
+
): { html: string; navItems?: NavItem[] } {
|
|
33
33
|
try {
|
|
34
34
|
const result = renderBlockInner(block, idx, options);
|
|
35
35
|
if (
|
|
@@ -58,7 +58,7 @@ function renderBlockInner(
|
|
|
58
58
|
block: Block,
|
|
59
59
|
idx: number,
|
|
60
60
|
options: BuildOptions,
|
|
61
|
-
): { html: string;
|
|
61
|
+
): { html: string; navItems?: NavItem[] } {
|
|
62
62
|
switch (block.type) {
|
|
63
63
|
case "heading": {
|
|
64
64
|
const md = resolveContent(block.src, options, "md");
|
|
@@ -67,14 +67,19 @@ function renderBlockInner(
|
|
|
67
67
|
const id = `heading-${idx}`;
|
|
68
68
|
return {
|
|
69
69
|
html: `<section id="${id}" class="bk-section bk-heading">${html}</section>`,
|
|
70
|
-
|
|
70
|
+
navItems: [{ id, label, kind: "heading" }],
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
case "markdown": {
|
|
75
75
|
const md = resolveContent(block.src, options, "md");
|
|
76
|
-
const { html } = mdToHtml(md);
|
|
77
|
-
|
|
76
|
+
const { html, headings } = mdToHtml(md);
|
|
77
|
+
const navItems: NavItem[] = headings.map(h => ({
|
|
78
|
+
id: h.id,
|
|
79
|
+
label: h.text,
|
|
80
|
+
kind: h.level === 2 ? "heading" : "section"
|
|
81
|
+
}));
|
|
82
|
+
return { html: `<div class="bk-markdown">${html}</div>`, navItems: navItems.length > 0 ? navItems : undefined };
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
case "section": {
|
|
@@ -84,7 +89,7 @@ function renderBlockInner(
|
|
|
84
89
|
const id = `section-${idx}`;
|
|
85
90
|
return {
|
|
86
91
|
html: `<section id="${id}" class="bk-section bk-subsection">${html}</section>`,
|
|
87
|
-
|
|
92
|
+
navItems: [{ id, label, kind: "section" }],
|
|
88
93
|
};
|
|
89
94
|
}
|
|
90
95
|
|
|
@@ -114,9 +119,10 @@ function renderBlockInner(
|
|
|
114
119
|
|
|
115
120
|
case "code": {
|
|
116
121
|
const raw = resolveContent(block.src, options, "text"); // Could be file or inline
|
|
122
|
+
const isInlineCode = typeof block.src === "string" && (block.src.includes("\n") || block.src.includes(" "));
|
|
117
123
|
const lang =
|
|
118
124
|
block.lang ??
|
|
119
|
-
(typeof block.src === "string" && block.src.includes(".")
|
|
125
|
+
(typeof block.src === "string" && !isInlineCode && block.src.includes(".")
|
|
120
126
|
? (block.src.split(".").pop() ?? "")
|
|
121
127
|
: "");
|
|
122
128
|
let highlighted = escHtml(raw);
|
|
@@ -136,7 +142,7 @@ function renderBlockInner(
|
|
|
136
142
|
}
|
|
137
143
|
|
|
138
144
|
case "simulation": {
|
|
139
|
-
const propsJson =
|
|
145
|
+
const propsJson = escapeScriptJson(block.props ?? {});
|
|
140
146
|
const simSrc = resolveContent(block.src, options, "js");
|
|
141
147
|
const simConfig = { js: simSrc, loop: false, dependencies: block.dependencies };
|
|
142
148
|
return {
|
|
@@ -278,7 +284,11 @@ function renderBlockInner(
|
|
|
278
284
|
let quiz: QuizFile = { questions: [] };
|
|
279
285
|
const rawJson = resolveContent(block.src, options, "json");
|
|
280
286
|
try {
|
|
281
|
-
|
|
287
|
+
const trimmed = rawJson.trim();
|
|
288
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
289
|
+
throw new Error("Quiz file not found or invalid JSON format");
|
|
290
|
+
}
|
|
291
|
+
quiz = JSON.parse(trimmed);
|
|
282
292
|
} catch (e) {
|
|
283
293
|
const msg = e instanceof Error ? e.message : String(e);
|
|
284
294
|
if (options.strict !== false) {
|
|
@@ -300,11 +310,11 @@ function renderBlockInner(
|
|
|
300
310
|
${quiz.questions.map((q, qi) => renderQuestion(q, `quiz-${idx}`, qi)).join("\n")}
|
|
301
311
|
</div>
|
|
302
312
|
</div>`,
|
|
303
|
-
|
|
313
|
+
navItems: [{
|
|
304
314
|
id: `quiz-${idx}`,
|
|
305
315
|
label: block.label ?? "Questions",
|
|
306
316
|
kind: "quiz",
|
|
307
|
-
},
|
|
317
|
+
}],
|
|
308
318
|
};
|
|
309
319
|
}
|
|
310
320
|
|
package/src/renderer/index.ts
CHANGED
|
@@ -10,10 +10,10 @@ export function render(lesson: Lesson, opts: BuildOptions = {}): string {
|
|
|
10
10
|
const structuredNavItems: NavItem[] = [];
|
|
11
11
|
|
|
12
12
|
lesson.blocks.forEach((block, idx) => {
|
|
13
|
-
const { html,
|
|
13
|
+
const { html, navItems } = renderBlock(block, idx, opts);
|
|
14
14
|
bodyItems.push(html);
|
|
15
|
-
if (
|
|
16
|
-
structuredNavItems.push(
|
|
15
|
+
if (navItems) {
|
|
16
|
+
structuredNavItems.push(...navItems);
|
|
17
17
|
}
|
|
18
18
|
});
|
|
19
19
|
|
package/src/renderer/markdown.ts
CHANGED
|
@@ -14,7 +14,7 @@ marked.use(markedHighlight({
|
|
|
14
14
|
|
|
15
15
|
// ─── Markdown Rendering (using Marked + KaTeX) ───────────────────────────────
|
|
16
16
|
|
|
17
|
-
function mdToHtml(md: string): { html: string; title: string } {
|
|
17
|
+
function mdToHtml(md: string): { html: string; title: string; headings: { id: string; text: string; level: number }[] } {
|
|
18
18
|
let title = "";
|
|
19
19
|
|
|
20
20
|
// Extract first H1 or H2 as title
|
|
@@ -55,7 +55,20 @@ function mdToHtml(md: string): { html: string; title: string } {
|
|
|
55
55
|
processedMd = processedMd.replace(`@@BK_CODE_${id}@@`, () => match);
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
const headings: { id: string; text: string; level: number }[] = [];
|
|
59
|
+
let headingIdCounter = 0;
|
|
60
|
+
|
|
61
|
+
const renderer = new marked.Renderer();
|
|
62
|
+
renderer.heading = ({ tokens, depth, text }) => {
|
|
63
|
+
const id = `bk-heading-${headingIdCounter++}`;
|
|
64
|
+
if (depth === 2 || depth === 3) {
|
|
65
|
+
const plainText = text.replace(/<[^>]+>/g, "");
|
|
66
|
+
headings.push({ id, text: plainText, level: depth });
|
|
67
|
+
}
|
|
68
|
+
return `<h${depth} id="${id}" class="bk-heading-${depth}">${text}</h${depth}>`;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
let html = marked.parse(processedMd, { renderer }) as string;
|
|
59
72
|
|
|
60
73
|
// Restore math
|
|
61
74
|
mathBlocks.forEach((tex, id) => {
|
|
@@ -83,7 +96,7 @@ function mdToHtml(md: string): { html: string; title: string } {
|
|
|
83
96
|
html = html.replace(`@@BK_MATH_INLINE_${id}@@`, () => rendered);
|
|
84
97
|
});
|
|
85
98
|
|
|
86
|
-
return { html, title };
|
|
99
|
+
return { html, title, headings };
|
|
87
100
|
}
|
|
88
101
|
|
|
89
102
|
function escHtml(s: string): string {
|
package/src/renderer/utils.ts
CHANGED
|
@@ -17,6 +17,13 @@ function resolveContent(
|
|
|
17
17
|
): string {
|
|
18
18
|
if (src.includes("\n")) return src;
|
|
19
19
|
|
|
20
|
+
if (/^https?:\/\//.test(src)) {
|
|
21
|
+
if (options.strict !== false) {
|
|
22
|
+
throw new Error(`Remote URLs are not yet supported for content files: ${src}`);
|
|
23
|
+
}
|
|
24
|
+
return src;
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
const isLikelyFilePath =
|
|
21
28
|
(expectedType !== "text" && src.endsWith(`.${expectedType}`)) ||
|
|
22
29
|
src.startsWith("/") ||
|
|
@@ -45,9 +52,14 @@ function resolveContent(
|
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
function resolveAssetSrc(src: string, options: BuildOptions): string {
|
|
48
|
-
if (/^(https?:|data
|
|
55
|
+
if (/^(https?:|data:)/.test(src)) return src;
|
|
56
|
+
|
|
57
|
+
const isWebAbsolute = src.startsWith("/") && !fs.existsSync(src);
|
|
58
|
+
if (isWebAbsolute) return src;
|
|
49
59
|
|
|
50
|
-
const filePath = path.
|
|
60
|
+
const filePath = path.isAbsolute(src)
|
|
61
|
+
? src
|
|
62
|
+
: path.resolve(options.contentBase ?? ".", src);
|
|
51
63
|
if (!fs.existsSync(filePath)) {
|
|
52
64
|
if (options.strict !== false)
|
|
53
65
|
throw new Error(`Missing media asset: ${filePath}`);
|