@brandon_m_behring/book-scaffold-astro 4.3.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/book-scaffold.mjs +2 -0
- package/components/ExerciseSolutions.astro +86 -11
- package/components/TipsCard.astro +7 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +18 -5
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +15 -5
- package/package.json +1 -1
- package/pages/exercises.astro +65 -0
- package/pages/tips.astro +14 -8
- package/scripts/build-exercises.mjs +125 -0
- package/src/profile-kit.ts +8 -0
- package/src/profiles/academic.ts +1 -0
- package/src/profiles/course-notes.ts +1 -0
- package/src/profiles/minimal.ts +1 -0
- package/src/profiles/research-portfolio.ts +1 -0
- package/src/profiles/tools.ts +1 -0
package/bin/book-scaffold.mjs
CHANGED
|
@@ -18,6 +18,7 @@ const handlers = {
|
|
|
18
18
|
'build-bib': '../scripts/build-bib.mjs',
|
|
19
19
|
'build-figures': '../scripts/build-figures.mjs',
|
|
20
20
|
'build-tips': '../scripts/build-tips.mjs',
|
|
21
|
+
'build-exercises': '../scripts/build-exercises.mjs',
|
|
21
22
|
'render-notebooks': '../scripts/render-notebooks.mjs',
|
|
22
23
|
};
|
|
23
24
|
|
|
@@ -29,6 +30,7 @@ Sub-commands:
|
|
|
29
30
|
build-bib BibTeX -> CSL JSON for the <Cite> component.
|
|
30
31
|
build-figures PDF -> SVG via pdftocairo / pdftoppm fallback (+ TikZ in v4.2.0).
|
|
31
32
|
build-tips Scan chapters for <Tip> instances; emit src/data/tips.json (v4.3.0).
|
|
33
|
+
build-exercises Scan chapters for <Exercise> instances; emit src/data/exercises.json (v4.4.0).
|
|
32
34
|
render-notebooks ipynb -> HTML via Jupyter nbconvert.
|
|
33
35
|
|
|
34
36
|
--help, -h This message.
|
|
@@ -1,22 +1,97 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
|
-
* ExerciseSolutions — chapter-end wrapper
|
|
4
|
-
* heading + container for <Solution> elements (v4.3.0, closes #71).
|
|
3
|
+
* ExerciseSolutions — chapter-end wrapper (v4.3.0 #71 + v4.4.0 auto mode).
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
* elements. The wrapper provides:
|
|
8
|
-
* - <h2>Exercise solutions</h2> heading
|
|
9
|
-
* - Container styling (separates the section visually from preceding prose)
|
|
10
|
-
* - Anchor (`#exercise-solutions`) so the section is linkable
|
|
5
|
+
* Two modes:
|
|
11
6
|
*
|
|
12
|
-
* v4.3.0
|
|
13
|
-
*
|
|
14
|
-
*
|
|
7
|
+
* 1. Manual (default; v4.3.0): author places <Solution for="..."> elements
|
|
8
|
+
* inside the wrapper. Full control; works with no build script.
|
|
9
|
+
*
|
|
10
|
+
* <ExerciseSolutions>
|
|
11
|
+
* <Solution for="ex-1">Solution text.</Solution>
|
|
12
|
+
* </ExerciseSolutions>
|
|
13
|
+
*
|
|
14
|
+
* 2. Auto (v4.4.0): reads src/data/exercises.json (emitted by
|
|
15
|
+
* `book-scaffold build-exercises`), scopes by the current chapter
|
|
16
|
+
* (derived from Astro.url.pathname), and renders an auto-generated
|
|
17
|
+
* list of exercise problem statements with placeholder solution
|
|
18
|
+
* slots. Author fills solutions by hand-editing the rendered HTML
|
|
19
|
+
* OR by switching back to manual mode.
|
|
20
|
+
*
|
|
21
|
+
* <ExerciseSolutions auto />
|
|
22
|
+
*
|
|
23
|
+
* Auto mode requires:
|
|
24
|
+
* - `routes.chapters: true` (or a custom `/chapters/<slug>/` route pattern)
|
|
25
|
+
* - `book-scaffold build-exercises` run before astro build (typically wired
|
|
26
|
+
* into the `prebuild` script in package.json)
|
|
27
|
+
*
|
|
28
|
+
* Graceful skip: when exercises.json is missing OR no exercises found for
|
|
29
|
+
* the current chapter, auto mode renders an empty section with a hint.
|
|
15
30
|
*
|
|
16
31
|
* Family: book-genre (cross-profile).
|
|
17
32
|
*/
|
|
33
|
+
interface Props {
|
|
34
|
+
/** v4.4.0: if true, auto-collect exercises from src/data/exercises.json
|
|
35
|
+
* (scoped to the current chapter via Astro.url.pathname). Default false
|
|
36
|
+
* uses the manual-slot mode from v4.3.0. */
|
|
37
|
+
auto?: boolean;
|
|
38
|
+
}
|
|
39
|
+
const { auto = false } = Astro.props;
|
|
40
|
+
|
|
41
|
+
// Derive current chapter slug from URL when in auto mode. Astro.url.pathname
|
|
42
|
+
// for `/chapters/<slug>/` returns `/chapters/<slug>/`; strip prefix+suffix.
|
|
43
|
+
let chapterSlug = '';
|
|
44
|
+
let exercises: Array<{ id: string; problem: string }> = [];
|
|
45
|
+
let autoError: string | null = null;
|
|
46
|
+
|
|
47
|
+
if (auto) {
|
|
48
|
+
const path = Astro.url.pathname;
|
|
49
|
+
const m = path.match(/^\/chapters\/([^/]+)\//);
|
|
50
|
+
chapterSlug = m ? m[1] : '';
|
|
51
|
+
if (!chapterSlug) {
|
|
52
|
+
autoError = `<ExerciseSolutions auto /> requires a /chapters/<slug>/ URL; got ${path}.`;
|
|
53
|
+
} else {
|
|
54
|
+
// v4.4.0 fix: project-root-relative import via Vite's import.meta.glob
|
|
55
|
+
// (the previous `../../../src/data/exercises.json` failed in node_modules contexts).
|
|
56
|
+
const exModules = import.meta.glob<{ default: Record<string, Array<{ id: string; problem: string }>> }>(
|
|
57
|
+
'/src/data/exercises.json',
|
|
58
|
+
{ eager: true },
|
|
59
|
+
);
|
|
60
|
+
const exEntry = exModules['/src/data/exercises.json'];
|
|
61
|
+
if (!exEntry) {
|
|
62
|
+
autoError = `src/data/exercises.json not found — run \`npx book-scaffold build-exercises\` first.`;
|
|
63
|
+
} else {
|
|
64
|
+
exercises = exEntry.default[chapterSlug] ?? [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
18
68
|
---
|
|
19
69
|
<section class="exercise-solutions" id="exercise-solutions">
|
|
20
70
|
<h2 class="exercise-solutions-heading">Exercise solutions</h2>
|
|
21
|
-
|
|
71
|
+
{!auto && (
|
|
72
|
+
<div class="exercise-solutions-body"><slot /></div>
|
|
73
|
+
)}
|
|
74
|
+
{auto && autoError && (
|
|
75
|
+
<p class="exercise-solutions-hint">{autoError}</p>
|
|
76
|
+
)}
|
|
77
|
+
{auto && !autoError && exercises.length === 0 && (
|
|
78
|
+
<p class="exercise-solutions-hint">
|
|
79
|
+
No <code><Exercise></code> instances found in chapter <code>{chapterSlug}</code>.
|
|
80
|
+
Run <code>npx book-scaffold build-exercises</code> after adding exercises.
|
|
81
|
+
</p>
|
|
82
|
+
)}
|
|
83
|
+
{auto && !autoError && exercises.length > 0 && (
|
|
84
|
+
<div class="exercise-solutions-body">
|
|
85
|
+
{exercises.map((ex) => (
|
|
86
|
+
<div class="solution" id={`solution-${ex.id}`}>
|
|
87
|
+
<div class="solution-header">
|
|
88
|
+
<strong class="solution-label">Exercise {ex.id}</strong>
|
|
89
|
+
<a href={`#exercise-${ex.id}`} class="solution-backlink">↑ Exercise</a>
|
|
90
|
+
</div>
|
|
91
|
+
<blockquote class="solution-problem">{ex.problem}</blockquote>
|
|
92
|
+
<p class="solution-placeholder"><em>Add your solution here.</em></p>
|
|
93
|
+
</div>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
22
97
|
</section>
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
* empty container with a note (doesn't fail the build). Consumers who
|
|
14
14
|
* don't use the tips feature can include the component without prep.
|
|
15
15
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
tips
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
// v4.4.0 fix: project-root-relative import via Vite's import.meta.glob
|
|
17
|
+
// (the previous `../../../src/data/tips.json` failed in node_modules contexts).
|
|
18
|
+
const tipsModules = import.meta.glob<{ default: Array<{ n: number; title: string; chapter: string; preview: string }> }>(
|
|
19
|
+
'/src/data/tips.json',
|
|
20
|
+
{ eager: true },
|
|
21
|
+
);
|
|
22
|
+
const tips = tipsModules['/src/data/tips.json']?.default ?? [];
|
|
23
23
|
---
|
|
24
24
|
<aside class="tips-card" role="contentinfo">
|
|
25
25
|
<h2 class="tips-card-title">Tips</h2>
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AstroUserConfig, AstroIntegration } from 'astro';
|
|
2
|
-
import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, Q as volatilityLevels, h as ChaptersRenderer, n as Style } from './types-
|
|
3
|
-
export { A as AcademicChapter, B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, i as CourseNotesChapter, F as FreshnessAffordance, j as FrontmatterRouteConfig, M as MinimalChapter, P as PartKey, k as PartialRouteToggles, l as ProfileDefinition, R as ResearchPortfolioChapter, m as RouteToggles, S as StatusBadge, o as StyleInput, T as ToolsChapter, V as VolatilityBadge, p as academicChapterSchema, q as academicParts, r as changeKinds, s as changelogSchema, t as chapterStatus, u as composeStyles, v as courseNotesChapterSchema, w as defineProfile, x as defineStyle, y as minimalChapterSchema, z as normalizeFrontmatterConfig, D as patternCategories, E as patternsSchema, G as researchPortfolioChapterSchema, H as resolvePreset, I as resolveProfile, J as sourceTiers, K as sourceTiersResearch, L as sourcesSchema, N as toolSlugs, O as toolsChapterSchema } from './types-
|
|
2
|
+
import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, Q as volatilityLevels, h as ChaptersRenderer, n as Style } from './types-BhlCranN.js';
|
|
3
|
+
export { A as AcademicChapter, B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, i as CourseNotesChapter, F as FreshnessAffordance, j as FrontmatterRouteConfig, M as MinimalChapter, P as PartKey, k as PartialRouteToggles, l as ProfileDefinition, R as ResearchPortfolioChapter, m as RouteToggles, S as StatusBadge, o as StyleInput, T as ToolsChapter, V as VolatilityBadge, p as academicChapterSchema, q as academicParts, r as changeKinds, s as changelogSchema, t as chapterStatus, u as composeStyles, v as courseNotesChapterSchema, w as defineProfile, x as defineStyle, y as minimalChapterSchema, z as normalizeFrontmatterConfig, D as patternCategories, E as patternsSchema, G as researchPortfolioChapterSchema, H as resolvePreset, I as resolveProfile, J as sourceTiers, K as sourceTiersResearch, L as sourcesSchema, N as toolSlugs, O as toolsChapterSchema } from './types-BhlCranN.js';
|
|
4
4
|
import 'astro/zod';
|
|
5
5
|
|
|
6
6
|
declare function defineBookConfig(opts: BookConfigOptions): Promise<AstroUserConfig>;
|
package/dist/index.mjs
CHANGED
|
@@ -378,8 +378,10 @@ var academicProfile = defineProfile({
|
|
|
378
378
|
// tools-profile-specific
|
|
379
379
|
frontmatter: false,
|
|
380
380
|
// opt-in per book; see #7
|
|
381
|
-
tips: false
|
|
381
|
+
tips: false,
|
|
382
382
|
// v4.3.0 #70: opt-in per book; requires build-tips
|
|
383
|
+
exercises: false
|
|
384
|
+
// v4.4.0: opt-in per book; requires build-exercises
|
|
383
385
|
},
|
|
384
386
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
385
387
|
katex: true,
|
|
@@ -494,8 +496,10 @@ var toolsProfile = defineProfile({
|
|
|
494
496
|
// tools profile ships convergence dashboard
|
|
495
497
|
frontmatter: false,
|
|
496
498
|
// opt-in per book; see #7
|
|
497
|
-
tips: false
|
|
499
|
+
tips: false,
|
|
498
500
|
// v4.3.0 #70: opt-in per book
|
|
501
|
+
exercises: false
|
|
502
|
+
// v4.4.0: opt-in per book
|
|
499
503
|
},
|
|
500
504
|
styles: [
|
|
501
505
|
"tokens.css",
|
|
@@ -574,8 +578,10 @@ var minimalProfile = defineProfile({
|
|
|
574
578
|
convergence: false,
|
|
575
579
|
frontmatter: false,
|
|
576
580
|
// opt-in per book; see #7
|
|
577
|
-
tips: false
|
|
581
|
+
tips: false,
|
|
578
582
|
// v4.3.0 #70: opt-in per book
|
|
583
|
+
exercises: false
|
|
584
|
+
// v4.4.0: opt-in per book
|
|
579
585
|
},
|
|
580
586
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
581
587
|
// v3.7.0 (#35): minimal aliases tools schema; fallback renderer field-dispatches if a consumer opts into routes.chapters
|
|
@@ -595,8 +601,10 @@ var courseNotesProfile = defineProfile({
|
|
|
595
601
|
convergence: false,
|
|
596
602
|
frontmatter: false,
|
|
597
603
|
// opt-in per book; see #7
|
|
598
|
-
tips: false
|
|
604
|
+
tips: false,
|
|
599
605
|
// v4.3.0 #70: opt-in per book
|
|
606
|
+
exercises: false
|
|
607
|
+
// v4.4.0: opt-in per book
|
|
600
608
|
},
|
|
601
609
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
602
610
|
// v3.7.0 (#35): course-notes schema has tools-style fields (chapter, volatility, sources) — fallback renderer dispatches via tools renderer
|
|
@@ -617,8 +625,10 @@ var researchPortfolioProfile = defineProfile({
|
|
|
617
625
|
// tools-profile-specific
|
|
618
626
|
frontmatter: true,
|
|
619
627
|
// portfolios universally need title/disclosure/banner pages
|
|
620
|
-
tips: false
|
|
628
|
+
tips: false,
|
|
621
629
|
// v4.3.0 #70: opt-in per book
|
|
630
|
+
exercises: false
|
|
631
|
+
// v4.4.0: opt-in per book
|
|
622
632
|
},
|
|
623
633
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
624
634
|
katex: true,
|
|
@@ -820,6 +830,9 @@ var ROUTE_REGISTRY = {
|
|
|
820
830
|
// v4.3.0 (#70): cross-volume numbered-tips index. Opt-in via
|
|
821
831
|
// routes.tips: true; pairs with build-tips script + <Tip> component.
|
|
822
832
|
tips: { pattern: "/tips", file: "tips.astro" },
|
|
833
|
+
// v4.4.0: exercises index by chapter. Opt-in via routes.exercises: true;
|
|
834
|
+
// pairs with build-exercises script + <ExerciseSolutions auto /> mode.
|
|
835
|
+
exercises: { pattern: "/exercises", file: "exercises.astro" },
|
|
823
836
|
// v3.4.0 (#7): consumer-collection-backed frontmatter route. Opt-in via
|
|
824
837
|
// routes: { frontmatter: true } AND content.config.ts defining the
|
|
825
838
|
// collection (use frontmatterCollection() helper from /schemas subpath).
|
package/dist/schemas.d.ts
CHANGED
package/dist/schemas.mjs
CHANGED
|
@@ -263,8 +263,10 @@ var academicProfile = defineProfile({
|
|
|
263
263
|
// tools-profile-specific
|
|
264
264
|
frontmatter: false,
|
|
265
265
|
// opt-in per book; see #7
|
|
266
|
-
tips: false
|
|
266
|
+
tips: false,
|
|
267
267
|
// v4.3.0 #70: opt-in per book; requires build-tips
|
|
268
|
+
exercises: false
|
|
269
|
+
// v4.4.0: opt-in per book; requires build-exercises
|
|
268
270
|
},
|
|
269
271
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
270
272
|
katex: true,
|
|
@@ -379,8 +381,10 @@ var toolsProfile = defineProfile({
|
|
|
379
381
|
// tools profile ships convergence dashboard
|
|
380
382
|
frontmatter: false,
|
|
381
383
|
// opt-in per book; see #7
|
|
382
|
-
tips: false
|
|
384
|
+
tips: false,
|
|
383
385
|
// v4.3.0 #70: opt-in per book
|
|
386
|
+
exercises: false
|
|
387
|
+
// v4.4.0: opt-in per book
|
|
384
388
|
},
|
|
385
389
|
styles: [
|
|
386
390
|
"tokens.css",
|
|
@@ -459,8 +463,10 @@ var minimalProfile = defineProfile({
|
|
|
459
463
|
convergence: false,
|
|
460
464
|
frontmatter: false,
|
|
461
465
|
// opt-in per book; see #7
|
|
462
|
-
tips: false
|
|
466
|
+
tips: false,
|
|
463
467
|
// v4.3.0 #70: opt-in per book
|
|
468
|
+
exercises: false
|
|
469
|
+
// v4.4.0: opt-in per book
|
|
464
470
|
},
|
|
465
471
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
466
472
|
// v3.7.0 (#35): minimal aliases tools schema; fallback renderer field-dispatches if a consumer opts into routes.chapters
|
|
@@ -480,8 +486,10 @@ var courseNotesProfile = defineProfile({
|
|
|
480
486
|
convergence: false,
|
|
481
487
|
frontmatter: false,
|
|
482
488
|
// opt-in per book; see #7
|
|
483
|
-
tips: false
|
|
489
|
+
tips: false,
|
|
484
490
|
// v4.3.0 #70: opt-in per book
|
|
491
|
+
exercises: false
|
|
492
|
+
// v4.4.0: opt-in per book
|
|
485
493
|
},
|
|
486
494
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
487
495
|
// v3.7.0 (#35): course-notes schema has tools-style fields (chapter, volatility, sources) — fallback renderer dispatches via tools renderer
|
|
@@ -502,8 +510,10 @@ var researchPortfolioProfile = defineProfile({
|
|
|
502
510
|
// tools-profile-specific
|
|
503
511
|
frontmatter: true,
|
|
504
512
|
// portfolios universally need title/disclosure/banner pages
|
|
505
|
-
tips: false
|
|
513
|
+
tips: false,
|
|
506
514
|
// v4.3.0 #70: opt-in per book
|
|
515
|
+
exercises: false
|
|
516
|
+
// v4.4.0: opt-in per book
|
|
507
517
|
},
|
|
508
518
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
509
519
|
katex: true,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brandon_m_behring/book-scaffold-astro",
|
|
3
3
|
"description": "Astro 6 + MDX toolkit for long-form technical books. Profile-aware (academic / tools / minimal); ships Tufte typography, KaTeX, BibTeX citations, Pagefind, Cloudflare Workers deploy. See PACKAGE_DESIGN.md for the API contract.",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Auto-injected /exercises route (v4.4.0).
|
|
4
|
+
*
|
|
5
|
+
* Gated on `routes.exercises: true`. Reads `src/data/exercises.json`
|
|
6
|
+
* (emitted by `book-scaffold build-exercises`); renders ordered list
|
|
7
|
+
* grouped by chapter with deep links into the chapter routes.
|
|
8
|
+
*
|
|
9
|
+
* Graceful skip: if exercises.json doesn't exist, renders an instructions
|
|
10
|
+
* page (doesn't fail the build).
|
|
11
|
+
*/
|
|
12
|
+
import Base from '../layouts/Base.astro';
|
|
13
|
+
|
|
14
|
+
// v4.4.0 fix: use Vite's import.meta.glob with a project-root-relative
|
|
15
|
+
// path (same lesson as tips.astro). `/src/data/...` is consumer-project-
|
|
16
|
+
// rooted in Vite's resolver and works across all consumer build contexts.
|
|
17
|
+
const exModules = import.meta.glob<{ default: Record<string, Array<{ id: string; problem: string }>> }>(
|
|
18
|
+
'/src/data/exercises.json',
|
|
19
|
+
{ eager: true },
|
|
20
|
+
);
|
|
21
|
+
const exEntry = exModules['/src/data/exercises.json'];
|
|
22
|
+
const byChapter = exEntry?.default ?? {};
|
|
23
|
+
const loadError = exEntry
|
|
24
|
+
? null
|
|
25
|
+
: 'src/data/exercises.json not found — run `npx book-scaffold build-exercises` to generate.';
|
|
26
|
+
|
|
27
|
+
const chapters = Object.keys(byChapter).sort();
|
|
28
|
+
const total = chapters.reduce((sum, c) => sum + byChapter[c].length, 0);
|
|
29
|
+
---
|
|
30
|
+
<Base title="Exercises" description="All exercises in this book, grouped by chapter with deep links.">
|
|
31
|
+
<article class="prose">
|
|
32
|
+
<h1>Exercises</h1>
|
|
33
|
+
{loadError && (
|
|
34
|
+
<p class="exercises-empty">{loadError}</p>
|
|
35
|
+
)}
|
|
36
|
+
{!loadError && total === 0 && (
|
|
37
|
+
<p class="exercises-empty">
|
|
38
|
+
No exercises defined yet. Add <code><Exercise id="...">...</Exercise></code> to a chapter
|
|
39
|
+
and run <code>npx book-scaffold build-exercises</code>.
|
|
40
|
+
</p>
|
|
41
|
+
)}
|
|
42
|
+
{total > 0 && (
|
|
43
|
+
<p class="exercises-summary">
|
|
44
|
+
{total} exercise{total === 1 ? '' : 's'} across {chapters.length} chapter{chapters.length === 1 ? '' : 's'}.
|
|
45
|
+
</p>
|
|
46
|
+
)}
|
|
47
|
+
{chapters.map((chapter) => (
|
|
48
|
+
<section class="exercises-chapter" id={`chapter-${chapter}`}>
|
|
49
|
+
<h2 class="exercises-chapter-title">
|
|
50
|
+
Chapter: <a href={`/chapters/${chapter}/`}>{chapter}</a>
|
|
51
|
+
</h2>
|
|
52
|
+
<ol class="exercises-list">
|
|
53
|
+
{byChapter[chapter].map((ex) => (
|
|
54
|
+
<li class="exercises-item">
|
|
55
|
+
<a href={`/chapters/${chapter}/#exercise-${ex.id}`} class="exercises-link">
|
|
56
|
+
<strong class="exercises-id">Exercise {ex.id}</strong>
|
|
57
|
+
<span class="exercises-preview">{ex.problem.slice(0, 120)}{ex.problem.length > 120 ? '…' : ''}</span>
|
|
58
|
+
</a>
|
|
59
|
+
</li>
|
|
60
|
+
))}
|
|
61
|
+
</ol>
|
|
62
|
+
</section>
|
|
63
|
+
))}
|
|
64
|
+
</article>
|
|
65
|
+
</Base>
|
package/pages/tips.astro
CHANGED
|
@@ -11,14 +11,20 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import Base from '../layouts/Base.astro';
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
14
|
+
// v4.4.0 fix: use Vite's import.meta.glob with a project-root-relative
|
|
15
|
+
// path. The previous `../../../src/data/tips.json` resolution failed when
|
|
16
|
+
// the auto-injected route was processed from node_modules (or any context
|
|
17
|
+
// other than a workspace symlink). `/src/data/...` is consumer-project-rooted
|
|
18
|
+
// in Vite's resolver and works across all consumer build contexts.
|
|
19
|
+
const tipsModules = import.meta.glob<{ default: Array<{ n: number; title: string; chapter: string; preview: string }> }>(
|
|
20
|
+
'/src/data/tips.json',
|
|
21
|
+
{ eager: true },
|
|
22
|
+
);
|
|
23
|
+
const tipsEntry = tipsModules['/src/data/tips.json'];
|
|
24
|
+
const tips = tipsEntry?.default ?? [];
|
|
25
|
+
const loadError = tipsEntry
|
|
26
|
+
? null
|
|
27
|
+
: 'src/data/tips.json not found — run `npx book-scaffold build-tips` to generate.';
|
|
22
28
|
---
|
|
23
29
|
<Base title="Tips" description="Numbered tips from this book, drawn from <Tip> instances in chapters.">
|
|
24
30
|
<article class="prose">
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/build-exercises.mjs — emit src/data/exercises.json from <Exercise>
|
|
4
|
+
* instances in chapter MDX (v4.4.0, closes v4.3.0 backlog).
|
|
5
|
+
*
|
|
6
|
+
* Sister script to build-tips.mjs (v4.3.0). Scans chapters honoring
|
|
7
|
+
* loader.base override (via readChaptersBase from walk-mdx.mjs). Extracts
|
|
8
|
+
* <Exercise id="X">body</Exercise> via 4-branch regex (same quote-style
|
|
9
|
+
* portability lesson as build-tips). Emits a chapter-keyed object so the
|
|
10
|
+
* /exercises route + <ExerciseSolutions auto> can scope by chapter.
|
|
11
|
+
*
|
|
12
|
+
* Output shape:
|
|
13
|
+
* {
|
|
14
|
+
* "ch1-foundations": [
|
|
15
|
+
* { "id": "ex-1", "problem": "Given..." },
|
|
16
|
+
* { "id": "ex-2", "problem": "Refactor..." }
|
|
17
|
+
* ],
|
|
18
|
+
* ...
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* Graceful no-op: if no <Exercise> instances exist, writes {} (doesn't
|
|
22
|
+
* fail builds for consumers who don't use the feature).
|
|
23
|
+
*/
|
|
24
|
+
import { writeFile, mkdir, readFile } from 'node:fs/promises';
|
|
25
|
+
import { resolve, dirname, basename } from 'node:path';
|
|
26
|
+
import { walkMdx, readChaptersBase } from './walk-mdx.mjs';
|
|
27
|
+
|
|
28
|
+
const USAGE = `Usage: book-scaffold build-exercises
|
|
29
|
+
|
|
30
|
+
Scan chapter MDX for <Exercise id="X">body</Exercise> occurrences; emit
|
|
31
|
+
src/data/exercises.json keyed by chapter slug. Used by the /exercises
|
|
32
|
+
auto-route + <ExerciseSolutions auto> mode when routes.exercises: true.
|
|
33
|
+
|
|
34
|
+
Env:
|
|
35
|
+
BOOK_CHAPTERS_DIR Override chapters dir (default: src/content/chapters).
|
|
36
|
+
BOOK_EXERCISES_OUT Override output path (default: src/data/exercises.json).
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--help, -h Print this message and exit (non-mutating).
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
43
|
+
process.stdout.write(USAGE);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const CWD = process.cwd();
|
|
48
|
+
const CHAPTERS_DIR = await readChaptersBase(CWD);
|
|
49
|
+
const OUTPUT_PATH = process.env.BOOK_EXERCISES_OUT ?? 'src/data/exercises.json';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extract <Exercise id="..."> tags and their body content from MDX.
|
|
53
|
+
*
|
|
54
|
+
* Returns array of { id, problem }. Problem is the full body content
|
|
55
|
+
* (trimmed + whitespace-normalized); the /exercises route + ExerciseSolutions
|
|
56
|
+
* auto truncate for display. Full content kept so consumers using
|
|
57
|
+
* `<ExerciseSolutions auto />` see the entire problem statement.
|
|
58
|
+
*
|
|
59
|
+
* Limitations (same as build-tips):
|
|
60
|
+
* - Doesn't handle nested <Exercise> tags
|
|
61
|
+
* - String attributes must use single OR double quotes (not template literals)
|
|
62
|
+
* - id may not contain a literal quote of the same type used as delimiter
|
|
63
|
+
*/
|
|
64
|
+
function extractExercises(source) {
|
|
65
|
+
const exercises = [];
|
|
66
|
+
// 4-branch alternation (single/single, double/double, mixed) — no
|
|
67
|
+
// backreference for cross-runtime regex portability (v4.1.2 lesson).
|
|
68
|
+
// Since Exercise has only 1 attribute (id), we just need 2 branches.
|
|
69
|
+
const re = new RegExp(
|
|
70
|
+
[
|
|
71
|
+
`<Exercise\\s+id="([^"]+)"\\s*>([\\s\\S]*?)</Exercise>`,
|
|
72
|
+
`<Exercise\\s+id='([^']+)'\\s*>([\\s\\S]*?)</Exercise>`,
|
|
73
|
+
].join('|'),
|
|
74
|
+
'g',
|
|
75
|
+
);
|
|
76
|
+
for (const match of source.matchAll(re)) {
|
|
77
|
+
const [, id1, body1, id2, body2] = match;
|
|
78
|
+
const id = id1 || id2;
|
|
79
|
+
const body = body1 || body2 || '';
|
|
80
|
+
if (!id) continue;
|
|
81
|
+
const problem = body.trim().replace(/\s+/g, ' ');
|
|
82
|
+
exercises.push({ id, problem });
|
|
83
|
+
}
|
|
84
|
+
return exercises;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function main() {
|
|
88
|
+
const byChapter = {};
|
|
89
|
+
let totalExercises = 0;
|
|
90
|
+
let chaptersWithExercises = 0;
|
|
91
|
+
|
|
92
|
+
for await (const rel of walkMdx(CHAPTERS_DIR)) {
|
|
93
|
+
const chapterPath = resolve(CHAPTERS_DIR, rel);
|
|
94
|
+
const chapterSlug = basename(rel).replace(/\.mdx?$/, '');
|
|
95
|
+
let source;
|
|
96
|
+
try {
|
|
97
|
+
source = await readFile(chapterPath, 'utf8');
|
|
98
|
+
} catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const exercises = extractExercises(source);
|
|
102
|
+
if (exercises.length > 0) {
|
|
103
|
+
byChapter[chapterSlug] = exercises;
|
|
104
|
+
totalExercises += exercises.length;
|
|
105
|
+
chaptersWithExercises += 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const outPath = resolve(CWD, OUTPUT_PATH);
|
|
110
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
111
|
+
await writeFile(outPath, JSON.stringify(byChapter, null, 2) + '\n');
|
|
112
|
+
process.stdout.write(
|
|
113
|
+
`build-exercises: ${totalExercises} exercise${totalExercises === 1 ? '' : 's'} across ` +
|
|
114
|
+
`${chaptersWithExercises} chapter${chaptersWithExercises === 1 ? '' : 's'} → ${OUTPUT_PATH}\n`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
main().catch((err) => {
|
|
119
|
+
console.error('build-exercises: failed');
|
|
120
|
+
console.error(err.message ?? err);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Export for tests.
|
|
125
|
+
export { extractExercises };
|
package/src/profile-kit.ts
CHANGED
|
@@ -57,6 +57,14 @@ export interface RouteToggles {
|
|
|
57
57
|
* defineBookConfig({ routes: { tips: true } }).
|
|
58
58
|
*/
|
|
59
59
|
tips: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* v4.4.0: auto-inject `/exercises` route listing all `<Exercise id="...">`
|
|
62
|
+
* instances from chapter MDX, grouped by chapter with deep links into
|
|
63
|
+
* the chapter routes. Requires running `book-scaffold build-exercises`
|
|
64
|
+
* (via prebuild) which emits src/data/exercises.json. Default `false` per
|
|
65
|
+
* profile — opt in via defineBookConfig({ routes: { exercises: true } }).
|
|
66
|
+
*/
|
|
67
|
+
exercises: boolean;
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
/** Profile definition — declarative shape for one book profile. */
|
package/src/profiles/academic.ts
CHANGED
|
@@ -24,6 +24,7 @@ export const academicProfile = defineProfile({
|
|
|
24
24
|
convergence: false, // tools-profile-specific
|
|
25
25
|
frontmatter: false, // opt-in per book; see #7
|
|
26
26
|
tips: false, // v4.3.0 #70: opt-in per book; requires build-tips
|
|
27
|
+
exercises: false, // v4.4.0: opt-in per book; requires build-exercises
|
|
27
28
|
},
|
|
28
29
|
styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css'],
|
|
29
30
|
katex: true,
|
|
@@ -26,6 +26,7 @@ export const courseNotesProfile = defineProfile({
|
|
|
26
26
|
convergence: false,
|
|
27
27
|
frontmatter: false, // opt-in per book; see #7
|
|
28
28
|
tips: false, // v4.3.0 #70: opt-in per book
|
|
29
|
+
exercises: false, // v4.4.0: opt-in per book
|
|
29
30
|
},
|
|
30
31
|
styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css'],
|
|
31
32
|
// v3.7.0 (#35): course-notes schema has tools-style fields (chapter, volatility, sources) — fallback renderer dispatches via tools renderer
|
package/src/profiles/minimal.ts
CHANGED
|
@@ -21,6 +21,7 @@ export const minimalProfile = defineProfile({
|
|
|
21
21
|
convergence: false,
|
|
22
22
|
frontmatter: false, // opt-in per book; see #7
|
|
23
23
|
tips: false, // v4.3.0 #70: opt-in per book
|
|
24
|
+
exercises: false, // v4.4.0: opt-in per book
|
|
24
25
|
},
|
|
25
26
|
styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css'],
|
|
26
27
|
// v3.7.0 (#35): minimal aliases tools schema; fallback renderer field-dispatches if a consumer opts into routes.chapters
|
|
@@ -37,6 +37,7 @@ export const researchPortfolioProfile = defineProfile({
|
|
|
37
37
|
convergence: false, // tools-profile-specific
|
|
38
38
|
frontmatter: true, // portfolios universally need title/disclosure/banner pages
|
|
39
39
|
tips: false, // v4.3.0 #70: opt-in per book
|
|
40
|
+
exercises: false, // v4.4.0: opt-in per book
|
|
40
41
|
},
|
|
41
42
|
styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css'],
|
|
42
43
|
katex: true, // math is common in research content
|
package/src/profiles/tools.ts
CHANGED
|
@@ -21,6 +21,7 @@ export const toolsProfile = defineProfile({
|
|
|
21
21
|
convergence: true, // tools profile ships convergence dashboard
|
|
22
22
|
frontmatter: false, // opt-in per book; see #7
|
|
23
23
|
tips: false, // v4.3.0 #70: opt-in per book
|
|
24
|
+
exercises: false, // v4.4.0: opt-in per book
|
|
24
25
|
},
|
|
25
26
|
styles: [
|
|
26
27
|
'tokens.css', 'layout.css', 'callouts.css', 'chapter.css',
|