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 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
- constructor(title: string, options?: BuildOptions);
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;
@@ -1 +1 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"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"}
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: title
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
- parentOpts.contentBase ??
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
- constructor(title, options = {}) {
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: title
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
- navItem?: NavItem;
8
+ navItems?: NavItem[];
9
9
  };
10
10
  declare function renderBlockInner(block: Block, idx: number, options: BuildOptions): {
11
11
  html: string;
12
- navItem?: NavItem;
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,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAuBrC;AAED,iBAAS,gBAAgB,CACxB,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,YAAY,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAgQrC;AAuID,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,gBAAgB,EAAE,CAAC"}
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"}
@@ -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
- navItem: { id, label, kind: "heading" },
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
- return { html: `<div class="bk-markdown">${html}</div>` };
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
- navItem: { id, label, kind: "section" },
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 = JSON.stringify(block.props ?? {});
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
- quiz = JSON.parse(rawJson);
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
- navItem: {
226
- id: `quiz-${idx}`,
227
- label: block.label ?? "Questions",
228
- kind: "quiz",
229
- },
235
+ navItems: [{
236
+ id: `quiz-${idx}`,
237
+ label: block.label ?? "Questions",
238
+ kind: "quiz",
239
+ }],
230
240
  };
231
241
  }
232
242
  case "divider":
@@ -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, navItem } = renderBlock(block, idx, opts);
8
+ const { html, navItems } = renderBlock(block, idx, opts);
9
9
  bodyItems.push(html);
10
- if (navItem) {
11
- structuredNavItems.push(navItem);
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,CAsE7D;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"}
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
- let html = marked.parse(processedMd);
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,CA4BR;AAED,iBAAS,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,MAAM,CAmCnE;AAED,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,CAAC"}
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"}
@@ -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:|\/)/.test(src))
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.resolve(options.contentBase ?? ".", src);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mr-md",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Mr Markdown is an opinionated TypeScript SDK for building interactive, single-file learning pages.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
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: title
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
- parentOpts.contentBase ??
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: title
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
  }
@@ -29,7 +29,7 @@ function renderBlock(
29
29
  block: Block,
30
30
  idx: number,
31
31
  options: BuildOptions,
32
- ): { html: string; navItem?: NavItem } {
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; navItem?: NavItem } {
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
- navItem: { id, label, kind: "heading" },
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
- return { html: `<div class="bk-markdown">${html}</div>` };
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
- navItem: { id, label, kind: "section" },
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 = JSON.stringify(block.props ?? {});
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
- quiz = JSON.parse(rawJson);
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
- navItem: {
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
 
@@ -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, navItem } = renderBlock(block, idx, opts);
13
+ const { html, navItems } = renderBlock(block, idx, opts);
14
14
  bodyItems.push(html);
15
- if (navItem) {
16
- structuredNavItems.push(navItem);
15
+ if (navItems) {
16
+ structuredNavItems.push(...navItems);
17
17
  }
18
18
  });
19
19
 
@@ -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
- let html = marked.parse(processedMd) as string;
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 {
@@ -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:|\/)/.test(src)) return src;
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.resolve(options.contentBase ?? ".", src);
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}`);