mr-md 1.0.1 → 1.0.3
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 +14 -0
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +85 -1
- package/dist/client/app.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/renderer/blocks.d.ts +2 -2
- package/dist/renderer/blocks.d.ts.map +1 -1
- package/dist/renderer/blocks.js +21 -15
- package/dist/renderer/index.d.ts +2 -1
- package/dist/renderer/index.d.ts.map +1 -1
- package/dist/renderer/index.js +369 -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/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/builder.ts +107 -1
- package/src/client/app.js +1 -1
- package/src/index.ts +1 -1
- package/src/renderer/blocks.ts +20 -14
- package/src/renderer/index.ts +384 -4
- package/src/renderer/markdown.ts +16 -3
- package/src/types.ts +11 -0
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,33 +58,38 @@ 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");
|
|
65
|
-
const { html, title } = mdToHtml(md);
|
|
66
|
-
const label = block.title || title || (typeof block.src === "string" && !block.src.includes(".md") ? block.src : "Heading");
|
|
65
|
+
const { html, title, headings } = mdToHtml(md);
|
|
66
|
+
const label = block.title || title || headings[0]?.text || (typeof block.src === "string" && !block.src.includes(".md") ? block.src.replace(/^#+\s*/, '') : "Heading");
|
|
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": {
|
|
81
86
|
const md = resolveContent(block.src, options, "md");
|
|
82
|
-
const { html, title } = mdToHtml(md);
|
|
83
|
-
const label = block.label || title || (typeof block.src === "string" && !block.src.includes(".md") ? block.src : "Section");
|
|
87
|
+
const { html, title, headings } = mdToHtml(md);
|
|
88
|
+
const label = block.label || title || headings[0]?.text || (typeof block.src === "string" && !block.src.includes(".md") ? block.src.replace(/^#+\s*/, '') : "Section");
|
|
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
|
|
|
@@ -283,7 +288,8 @@ function renderBlockInner(
|
|
|
283
288
|
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
284
289
|
throw new Error("Quiz file not found or invalid JSON format");
|
|
285
290
|
}
|
|
286
|
-
|
|
291
|
+
const parsed = JSON.parse(trimmed);
|
|
292
|
+
quiz = Array.isArray(parsed) ? { questions: parsed } : parsed;
|
|
287
293
|
} catch (e) {
|
|
288
294
|
const msg = e instanceof Error ? e.message : String(e);
|
|
289
295
|
if (options.strict !== false) {
|
|
@@ -305,11 +311,11 @@ function renderBlockInner(
|
|
|
305
311
|
${quiz.questions.map((q, qi) => renderQuestion(q, `quiz-${idx}`, qi)).join("\n")}
|
|
306
312
|
</div>
|
|
307
313
|
</div>`,
|
|
308
|
-
|
|
314
|
+
navItems: [{
|
|
309
315
|
id: `quiz-${idx}`,
|
|
310
316
|
label: block.label ?? "Questions",
|
|
311
317
|
kind: "quiz",
|
|
312
|
-
},
|
|
318
|
+
}],
|
|
313
319
|
};
|
|
314
320
|
}
|
|
315
321
|
|
|
@@ -346,7 +352,7 @@ function renderQuestion(q: QuizQuestion, quizId: string, qi: number): string {
|
|
|
346
352
|
|
|
347
353
|
// Wraps a JS string in a minimal iframe document
|
|
348
354
|
function iframeDoc(js: string, props: string, loop?: boolean, dependencies?: string[]): string {
|
|
349
|
-
const scriptTags = (dependencies ?? []).map((url) => `<script src="${escAttr(url)}"></script>`).join("
|
|
355
|
+
const scriptTags = (dependencies ?? []).map((url) => `<script src="${escAttr(url)}"></script>`).join("\n");
|
|
350
356
|
const doc = `<!DOCTYPE html><html><head>
|
|
351
357
|
${scriptTags}
|
|
352
358
|
<style>
|
package/src/renderer/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BuildOptions, Chapter, Lesson } from "../types.js";
|
|
1
|
+
import type { BuildOptions, Chapter, Course, Lesson } from "../types.js";
|
|
2
2
|
import { escAttr, escHtml, renderBlock } from "./blocks.js";
|
|
3
3
|
import { clientScript, pageCSS, renderPage } from "./html.js"; // Used in renderChapter
|
|
4
4
|
import type { NavItem } from "./utils.js";
|
|
@@ -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
|
|
|
@@ -399,3 +399,383 @@ ${clientScript()}
|
|
|
399
399
|
</body>
|
|
400
400
|
</html>`;
|
|
401
401
|
}
|
|
402
|
+
|
|
403
|
+
// ─── Course Rendering ────────────────────────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
export function renderCourse(
|
|
406
|
+
course: Course,
|
|
407
|
+
opts: BuildOptions = {},
|
|
408
|
+
): string {
|
|
409
|
+
const theme = opts.theme ?? "auto";
|
|
410
|
+
const schemeAttr = theme === "auto" ? "" : `data-theme="${theme}"`;
|
|
411
|
+
const preset = opts.preset ?? {};
|
|
412
|
+
const layout = preset.layout ?? "lesson";
|
|
413
|
+
const density = preset.density ?? "comfortable";
|
|
414
|
+
const tone = preset.tone ?? "scholarly";
|
|
415
|
+
const palette = opts.palette ?? "ink";
|
|
416
|
+
|
|
417
|
+
const navHtml = course.chapters
|
|
418
|
+
.map(
|
|
419
|
+
(c) =>
|
|
420
|
+
`<a href="${escAttr(c.meta.slug)}.html" class="bk-nav-item bk-nav-chapter">${escHtml(c.meta.title)}</a>`,
|
|
421
|
+
)
|
|
422
|
+
.join("\n");
|
|
423
|
+
|
|
424
|
+
const timelineHtml = `
|
|
425
|
+
<div class="bk-chapter-timeline-wrapper">
|
|
426
|
+
<div class="bk-chapter-timeline">
|
|
427
|
+
${course.chapters
|
|
428
|
+
.map(
|
|
429
|
+
(chapter, idx) => `
|
|
430
|
+
<a href="${escAttr(chapter.meta.slug)}.html" class="bk-timeline-card bk-status-${chapter.meta.status ?? "unread"}">
|
|
431
|
+
<div class="bk-timeline-node"></div>
|
|
432
|
+
<div class="bk-timeline-content">
|
|
433
|
+
<h3 class="bk-timeline-title" style="view-transition-name: title-${escAttr(chapter.meta.slug)}">${escHtml(chapter.meta.title)}</h3>
|
|
434
|
+
${chapter.meta.description ? `<p class="bk-timeline-desc">${escHtml(chapter.meta.description)}</p>` : ""}
|
|
435
|
+
<span class="bk-timeline-action">${chapter.meta.status === "completed" ? "Review" : "Start Chapter"} <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="M5 12h14m-7-7 7 7-7 7"/></svg></span>
|
|
436
|
+
</div>
|
|
437
|
+
</a>
|
|
438
|
+
`,
|
|
439
|
+
)
|
|
440
|
+
.join("")}
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
<script>
|
|
444
|
+
if (!CSS.supports('(animation-timeline: view()) and (animation-range: entry)')) {
|
|
445
|
+
const observer = new IntersectionObserver(
|
|
446
|
+
(entries) => {
|
|
447
|
+
for (const entry of entries) {
|
|
448
|
+
if (entry.isIntersecting) {
|
|
449
|
+
entry.target.classList.add('js-visible');
|
|
450
|
+
entry.target.classList.remove('js-hidden');
|
|
451
|
+
observer.unobserve(entry.target);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
{ threshold: 0.1 }
|
|
456
|
+
);
|
|
457
|
+
document.querySelectorAll('.bk-timeline-card').forEach((el) => {
|
|
458
|
+
el.classList.add('js-hidden');
|
|
459
|
+
observer.observe(el);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
</script>`;
|
|
463
|
+
|
|
464
|
+
const chapterStyles = `
|
|
465
|
+
.bk-chapter-timeline-wrapper {
|
|
466
|
+
position: relative;
|
|
467
|
+
width: 100%;
|
|
468
|
+
padding: 1rem 0;
|
|
469
|
+
z-index: 1;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.bk-chapter-timeline {
|
|
473
|
+
display: flex;
|
|
474
|
+
flex-direction: column;
|
|
475
|
+
gap: 3rem;
|
|
476
|
+
padding: 3rem 0;
|
|
477
|
+
position: relative;
|
|
478
|
+
max-width: 800px;
|
|
479
|
+
margin: 0 auto;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/* Minimalist vertical connecting line */
|
|
483
|
+
.bk-chapter-timeline::before {
|
|
484
|
+
content: '';
|
|
485
|
+
position: absolute;
|
|
486
|
+
left: 20px;
|
|
487
|
+
top: 3rem;
|
|
488
|
+
bottom: 3rem;
|
|
489
|
+
width: 1px;
|
|
490
|
+
background: var(--line);
|
|
491
|
+
z-index: 0;
|
|
492
|
+
transform: translateX(-50%);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.bk-timeline-card {
|
|
496
|
+
display: flex;
|
|
497
|
+
align-items: stretch;
|
|
498
|
+
gap: 2rem;
|
|
499
|
+
text-decoration: none !important;
|
|
500
|
+
border: none !important;
|
|
501
|
+
color: inherit;
|
|
502
|
+
position: relative;
|
|
503
|
+
z-index: 1;
|
|
504
|
+
opacity: 1;
|
|
505
|
+
transform: none;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/* Subtle scroll-driven animations */
|
|
509
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
510
|
+
@supports ((animation-timeline: view()) and (animation-range: entry)) {
|
|
511
|
+
.bk-timeline-card {
|
|
512
|
+
animation-name: slide-fade-in;
|
|
513
|
+
animation-fill-mode: both;
|
|
514
|
+
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
|
515
|
+
animation-timeline: view(block);
|
|
516
|
+
animation-range: entry 5% cover 20%;
|
|
517
|
+
}
|
|
518
|
+
@keyframes slide-fade-in {
|
|
519
|
+
0% { opacity: 0; transform: translateY(20px); }
|
|
520
|
+
100% { opacity: 1; transform: translateY(0); }
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/* Fallback for browsers without animation-timeline support */
|
|
526
|
+
.bk-timeline-card.js-hidden {
|
|
527
|
+
opacity: 0;
|
|
528
|
+
transform: translateY(20px);
|
|
529
|
+
}
|
|
530
|
+
.bk-timeline-card.js-visible {
|
|
531
|
+
opacity: 1;
|
|
532
|
+
transform: translateY(0);
|
|
533
|
+
transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1), transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.bk-timeline-node {
|
|
537
|
+
width: 40px;
|
|
538
|
+
height: 40px;
|
|
539
|
+
border-radius: 50%;
|
|
540
|
+
background: var(--paper);
|
|
541
|
+
border: 1px solid var(--accent);
|
|
542
|
+
display: flex;
|
|
543
|
+
align-items: center;
|
|
544
|
+
justify-content: center;
|
|
545
|
+
flex-shrink: 0;
|
|
546
|
+
transition: all 0.3s ease;
|
|
547
|
+
position: relative;
|
|
548
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.02);
|
|
549
|
+
}
|
|
550
|
+
.bk-timeline-node::after {
|
|
551
|
+
content: '';
|
|
552
|
+
width: 10px;
|
|
553
|
+
height: 10px;
|
|
554
|
+
border-radius: 50%;
|
|
555
|
+
background: var(--accent);
|
|
556
|
+
transition: all 0.3s ease;
|
|
557
|
+
}
|
|
558
|
+
.bk-timeline-card:hover .bk-timeline-node {
|
|
559
|
+
background: var(--accent);
|
|
560
|
+
}
|
|
561
|
+
.bk-timeline-card:hover .bk-timeline-node::after {
|
|
562
|
+
background: var(--paper);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.bk-timeline-content {
|
|
566
|
+
background: var(--paper);
|
|
567
|
+
padding: 2rem;
|
|
568
|
+
border-radius: 12px;
|
|
569
|
+
border: 1px solid var(--line);
|
|
570
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02);
|
|
571
|
+
flex: 1;
|
|
572
|
+
transition: all 0.3s ease;
|
|
573
|
+
display: flex;
|
|
574
|
+
flex-direction: column;
|
|
575
|
+
justify-content: center;
|
|
576
|
+
position: relative;
|
|
577
|
+
}
|
|
578
|
+
.bk-timeline-card:hover .bk-timeline-content {
|
|
579
|
+
border-color: color-mix(in srgb, var(--accent) 30%, var(--line));
|
|
580
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04);
|
|
581
|
+
transform: translateY(-2px);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.bk-timeline-title {
|
|
585
|
+
margin: 0 0 0.5rem 0;
|
|
586
|
+
font-family: var(--font-display);
|
|
587
|
+
font-size: 1.6rem;
|
|
588
|
+
color: var(--ink);
|
|
589
|
+
width: fit-content;
|
|
590
|
+
transition: color 0.2s ease;
|
|
591
|
+
}
|
|
592
|
+
.bk-timeline-card:hover .bk-timeline-title {
|
|
593
|
+
color: var(--accent);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.bk-timeline-desc {
|
|
597
|
+
margin: 0 0 1.5rem 0;
|
|
598
|
+
color: var(--muted);
|
|
599
|
+
line-height: 1.6;
|
|
600
|
+
font-size: 1.05rem;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.bk-timeline-action {
|
|
604
|
+
font-weight: 500;
|
|
605
|
+
color: var(--accent);
|
|
606
|
+
display: inline-flex;
|
|
607
|
+
align-items: center;
|
|
608
|
+
gap: 0.4rem;
|
|
609
|
+
font-size: 0.95rem;
|
|
610
|
+
align-self: flex-start;
|
|
611
|
+
transition: all 0.2s ease;
|
|
612
|
+
border-bottom: 1px solid transparent;
|
|
613
|
+
}
|
|
614
|
+
.bk-timeline-card:hover .bk-timeline-action {
|
|
615
|
+
gap: 0.6rem;
|
|
616
|
+
border-bottom-color: var(--accent);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.bk-status-unread .bk-timeline-node {
|
|
620
|
+
border: 1px solid var(--line-strong);
|
|
621
|
+
}
|
|
622
|
+
.bk-status-unread .bk-timeline-node::after {
|
|
623
|
+
background: var(--line-strong);
|
|
624
|
+
transform: scale(0.8);
|
|
625
|
+
}
|
|
626
|
+
.bk-status-unread .bk-timeline-content {
|
|
627
|
+
background: transparent;
|
|
628
|
+
box-shadow: none;
|
|
629
|
+
border: 1px solid transparent;
|
|
630
|
+
}
|
|
631
|
+
.bk-status-unread .bk-timeline-title {
|
|
632
|
+
color: var(--muted);
|
|
633
|
+
}
|
|
634
|
+
.bk-status-unread .bk-timeline-desc {
|
|
635
|
+
color: color-mix(in srgb, var(--muted) 80%, transparent);
|
|
636
|
+
}
|
|
637
|
+
.bk-status-unread .bk-timeline-action {
|
|
638
|
+
color: var(--muted);
|
|
639
|
+
}
|
|
640
|
+
.bk-status-unread:hover .bk-timeline-node {
|
|
641
|
+
border-color: var(--ink);
|
|
642
|
+
background: var(--paper);
|
|
643
|
+
}
|
|
644
|
+
.bk-status-unread:hover .bk-timeline-node::after {
|
|
645
|
+
background: var(--ink);
|
|
646
|
+
transform: scale(1);
|
|
647
|
+
}
|
|
648
|
+
.bk-status-unread:hover .bk-timeline-content {
|
|
649
|
+
background: var(--paper);
|
|
650
|
+
border-color: var(--line);
|
|
651
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02);
|
|
652
|
+
}
|
|
653
|
+
.bk-status-unread:hover .bk-timeline-title {
|
|
654
|
+
color: var(--ink);
|
|
655
|
+
}
|
|
656
|
+
.bk-status-unread:hover .bk-timeline-action {
|
|
657
|
+
color: var(--ink);
|
|
658
|
+
border-bottom-color: var(--ink);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
@media (prefers-color-scheme: dark) {
|
|
662
|
+
.bk-timeline-content {
|
|
663
|
+
background: var(--panel);
|
|
664
|
+
}
|
|
665
|
+
.bk-status-unread .bk-timeline-content {
|
|
666
|
+
background: transparent;
|
|
667
|
+
}
|
|
668
|
+
.bk-status-unread:hover .bk-timeline-content {
|
|
669
|
+
background: var(--panel);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
@media (max-width: 600px) {
|
|
674
|
+
.bk-chapter-timeline::before {
|
|
675
|
+
left: 16px;
|
|
676
|
+
}
|
|
677
|
+
.bk-timeline-card {
|
|
678
|
+
gap: 1.5rem;
|
|
679
|
+
}
|
|
680
|
+
.bk-timeline-node {
|
|
681
|
+
width: 32px;
|
|
682
|
+
height: 32px;
|
|
683
|
+
}
|
|
684
|
+
.bk-timeline-node::after {
|
|
685
|
+
width: 8px;
|
|
686
|
+
height: 8px;
|
|
687
|
+
}
|
|
688
|
+
.bk-timeline-content {
|
|
689
|
+
padding: 1.5rem;
|
|
690
|
+
border-radius: 10px;
|
|
691
|
+
}
|
|
692
|
+
.bk-timeline-title {
|
|
693
|
+
font-size: 1.4rem;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
`;
|
|
697
|
+
|
|
698
|
+
return `<!DOCTYPE html>
|
|
699
|
+
<html lang="en" data-palette="${palette}" ${schemeAttr}>
|
|
700
|
+
<head>
|
|
701
|
+
<meta charset="UTF-8">
|
|
702
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
703
|
+
<title>${escHtml(course.meta.title)}</title>
|
|
704
|
+
${course.meta.description ? `<meta name="description" content="${escHtml(course.meta.description)}">` : ""}
|
|
705
|
+
${opts.head ?? ""}
|
|
706
|
+
<style>
|
|
707
|
+
${opts.font ? `:root { --font-sans: ${opts.font}, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }` : ""}
|
|
708
|
+
${pageCSS()}
|
|
709
|
+
${chapterStyles}
|
|
710
|
+
</style>
|
|
711
|
+
</head>
|
|
712
|
+
<body class="bk-layout-${layout} bk-density-${density} bk-tone-${tone}">
|
|
713
|
+
<div class="bk-shell">
|
|
714
|
+
<aside class="bk-sidebar">
|
|
715
|
+
<div class="bk-sidebar-inner">
|
|
716
|
+
<div class="bk-sidebar-header">
|
|
717
|
+
<div style="margin-top: 8px;"></div>
|
|
718
|
+
<div class="bk-sidebar-title">${escHtml(course.meta.title)}</div>
|
|
719
|
+
</div>
|
|
720
|
+
<nav class="bk-nav">${navHtml}</nav>
|
|
721
|
+
<div class="bk-sidebar-footer">
|
|
722
|
+
<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">
|
|
723
|
+
<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>
|
|
724
|
+
<span class="bk-sr-only">Display settings</span>
|
|
725
|
+
</button>
|
|
726
|
+
<div class="bk-theme-panel" id="bk-theme-panel" aria-label="Display settings" hidden>
|
|
727
|
+
<div class="bk-theme-row">
|
|
728
|
+
<span>Theme</span>
|
|
729
|
+
<div class="bk-segmented-control" id="bk-theme-icons">
|
|
730
|
+
<button type="button" class="bk-segment-btn ${theme === "light" ? "active" : ""}" data-theme="light" title="Light" aria-label="Light theme">
|
|
731
|
+
<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>
|
|
732
|
+
</button>
|
|
733
|
+
<button type="button" class="bk-segment-btn ${theme === "dark" ? "active" : ""}" data-theme="dark" title="Dark" aria-label="Dark theme">
|
|
734
|
+
<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>
|
|
735
|
+
</button>
|
|
736
|
+
<button type="button" class="bk-segment-btn ${theme === "auto" ? "active" : ""}" data-theme="auto" title="System" aria-label="System theme">
|
|
737
|
+
<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>
|
|
738
|
+
</button>
|
|
739
|
+
</div>
|
|
740
|
+
</div>
|
|
741
|
+
<div class="bk-theme-row">
|
|
742
|
+
<span>Palette</span>
|
|
743
|
+
<div class="bk-segmented-control" id="bk-palette-icons">
|
|
744
|
+
<button type="button" class="bk-segment-btn ${palette === "ink" ? "active" : ""}" data-palette="ink" title="Ink" aria-label="Ink palette">
|
|
745
|
+
<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>
|
|
746
|
+
</button>
|
|
747
|
+
<button type="button" class="bk-segment-btn ${palette === "field" ? "active" : ""}" data-palette="field" title="Field" aria-label="Field palette">
|
|
748
|
+
<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>
|
|
749
|
+
</button>
|
|
750
|
+
<button type="button" class="bk-segment-btn ${palette === "ember" ? "active" : ""}" data-palette="ember" title="Ember" aria-label="Ember palette">
|
|
751
|
+
<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>
|
|
752
|
+
</button>
|
|
753
|
+
</div>
|
|
754
|
+
</div>
|
|
755
|
+
</div>
|
|
756
|
+
</div>
|
|
757
|
+
</div>
|
|
758
|
+
</aside>
|
|
759
|
+
<button class="bk-sidebar-collapse-floating" id="bk-sidebar-collapse" aria-label="Collapse sidebar">
|
|
760
|
+
<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>
|
|
761
|
+
</button>
|
|
762
|
+
<main class="bk-main">
|
|
763
|
+
<button class="bk-sidebar-expand" id="bk-sidebar-expand" type="button" aria-label="Expand sidebar">
|
|
764
|
+
<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>
|
|
765
|
+
</button>
|
|
766
|
+
<article class="bk-content" style="max-width: 1000px; margin: 0 auto;">
|
|
767
|
+
<header class="bk-hero" style="border-bottom: none;">
|
|
768
|
+
<p class="bk-eyebrow">Course</p>
|
|
769
|
+
<h1>${escHtml(course.meta.title)}</h1>
|
|
770
|
+
${course.meta.description ? `<p class="bk-deck">${escHtml(course.meta.description)}</p>` : ""}
|
|
771
|
+
</header>
|
|
772
|
+
${timelineHtml}
|
|
773
|
+
</article>
|
|
774
|
+
</main>
|
|
775
|
+
</div>
|
|
776
|
+
<script>
|
|
777
|
+
${clientScript()}
|
|
778
|
+
</script>
|
|
779
|
+
</body>
|
|
780
|
+
</html>`;
|
|
781
|
+
}
|
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/types.ts
CHANGED
|
@@ -286,3 +286,14 @@ export interface Chapter {
|
|
|
286
286
|
meta: ChapterMeta;
|
|
287
287
|
lessons: Lesson[];
|
|
288
288
|
}
|
|
289
|
+
|
|
290
|
+
export interface CourseMeta {
|
|
291
|
+
title: string;
|
|
292
|
+
slug: string;
|
|
293
|
+
description?: string;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export interface Course {
|
|
297
|
+
meta: CourseMeta;
|
|
298
|
+
chapters: Chapter[];
|
|
299
|
+
}
|