@brandon_m_behring/book-scaffold-astro 4.3.0 → 4.5.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 +19 -3
- package/dist/index.mjs +71 -8
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +25 -5
- package/package.json +1 -1
- package/pages/exercises.astro +65 -0
- package/pages/index.astro +111 -0
- package/pages/tips.astro +14 -8
- package/scripts/build-exercises.mjs +125 -0
- package/src/profile-kit.ts +18 -0
- package/src/profiles/academic.ts +2 -0
- package/src/profiles/course-notes.ts +2 -0
- package/src/profiles/minimal.ts +2 -0
- package/src/profiles/research-portfolio.ts +2 -0
- package/src/profiles/tools.ts +2 -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,8 +1,24 @@
|
|
|
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-DLIpEgTm.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-DLIpEgTm.js';
|
|
4
4
|
import 'astro/zod';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* v4.5.0: Default portfolio backlink baked into the scaffold. Rendered in
|
|
8
|
+
* the auto-injected `/` landing footer for every consumer that doesn't
|
|
9
|
+
* explicitly override `portfolio` in defineBookConfig.
|
|
10
|
+
*
|
|
11
|
+
* Single source of truth for the brandon-behring.dev URL across all
|
|
12
|
+
* consumers — update here, bump scaffold version, every consumer inherits
|
|
13
|
+
* on next build. This is the intentional Brandon-specific default
|
|
14
|
+
* discussed in plan §Phase 6-pre. Consumers outside the brandon-behring.dev
|
|
15
|
+
* ecosystem set `portfolio: false` (no link) or pass `{ url, label }` to
|
|
16
|
+
* override.
|
|
17
|
+
*/
|
|
18
|
+
declare const BRANDON_PORTFOLIO_DEFAULT: {
|
|
19
|
+
readonly url: "https://brandon-behring.dev";
|
|
20
|
+
readonly label: "brandon-behring.dev";
|
|
21
|
+
};
|
|
6
22
|
declare function defineBookConfig(opts: BookConfigOptions): Promise<AstroUserConfig>;
|
|
7
23
|
|
|
8
24
|
declare function bookScaffoldIntegration(opts: BookScaffoldIntegrationOptions): AstroIntegration;
|
|
@@ -244,4 +260,4 @@ type TipsConfigInput = Omit<TipsConfig, typeof TipsConfigBrand | '__tipsConfigVe
|
|
|
244
260
|
*/
|
|
245
261
|
declare function defineTips(opts: TipsConfigInput): TipsConfig;
|
|
246
262
|
|
|
247
|
-
export { BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, type Freshness, type FreshnessStatus, Style, type TipsConfig, type TipsConfigInput, type VolatilityLevel, academicChaptersRenderer, academicStyle, bookScaffoldIntegration, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, defineTips, fallbackChaptersRenderer, freshnessLabel, getFreshness, minimalStyle, researchPortfolioStyle, toolsChaptersRenderer, toolsStyle, volatilityLevels };
|
|
263
|
+
export { BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, type Freshness, type FreshnessStatus, Style, type TipsConfig, type TipsConfigInput, type VolatilityLevel, academicChaptersRenderer, academicStyle, bookScaffoldIntegration, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, defineTips, fallbackChaptersRenderer, freshnessLabel, getFreshness, minimalStyle, researchPortfolioStyle, toolsChaptersRenderer, toolsStyle, volatilityLevels };
|
package/dist/index.mjs
CHANGED
|
@@ -378,8 +378,12 @@ 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
|
|
385
|
+
landing: true
|
|
386
|
+
// v4.5.0: auto-inject minimal root landing; consumers override via src/pages/index.astro
|
|
383
387
|
},
|
|
384
388
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
385
389
|
katex: true,
|
|
@@ -494,8 +498,12 @@ var toolsProfile = defineProfile({
|
|
|
494
498
|
// tools profile ships convergence dashboard
|
|
495
499
|
frontmatter: false,
|
|
496
500
|
// opt-in per book; see #7
|
|
497
|
-
tips: false
|
|
501
|
+
tips: false,
|
|
498
502
|
// v4.3.0 #70: opt-in per book
|
|
503
|
+
exercises: false,
|
|
504
|
+
// v4.4.0: opt-in per book
|
|
505
|
+
landing: true
|
|
506
|
+
// v4.5.0: auto-inject minimal root landing
|
|
499
507
|
},
|
|
500
508
|
styles: [
|
|
501
509
|
"tokens.css",
|
|
@@ -574,8 +582,12 @@ var minimalProfile = defineProfile({
|
|
|
574
582
|
convergence: false,
|
|
575
583
|
frontmatter: false,
|
|
576
584
|
// opt-in per book; see #7
|
|
577
|
-
tips: false
|
|
585
|
+
tips: false,
|
|
578
586
|
// v4.3.0 #70: opt-in per book
|
|
587
|
+
exercises: false,
|
|
588
|
+
// v4.4.0: opt-in per book
|
|
589
|
+
landing: true
|
|
590
|
+
// v4.5.0: auto-inject minimal root landing
|
|
579
591
|
},
|
|
580
592
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
581
593
|
// v3.7.0 (#35): minimal aliases tools schema; fallback renderer field-dispatches if a consumer opts into routes.chapters
|
|
@@ -595,8 +607,12 @@ var courseNotesProfile = defineProfile({
|
|
|
595
607
|
convergence: false,
|
|
596
608
|
frontmatter: false,
|
|
597
609
|
// opt-in per book; see #7
|
|
598
|
-
tips: false
|
|
610
|
+
tips: false,
|
|
599
611
|
// v4.3.0 #70: opt-in per book
|
|
612
|
+
exercises: false,
|
|
613
|
+
// v4.4.0: opt-in per book
|
|
614
|
+
landing: true
|
|
615
|
+
// v4.5.0: auto-inject minimal root landing
|
|
600
616
|
},
|
|
601
617
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
602
618
|
// v3.7.0 (#35): course-notes schema has tools-style fields (chapter, volatility, sources) — fallback renderer dispatches via tools renderer
|
|
@@ -617,8 +633,12 @@ var researchPortfolioProfile = defineProfile({
|
|
|
617
633
|
// tools-profile-specific
|
|
618
634
|
frontmatter: true,
|
|
619
635
|
// portfolios universally need title/disclosure/banner pages
|
|
620
|
-
tips: false
|
|
636
|
+
tips: false,
|
|
621
637
|
// v4.3.0 #70: opt-in per book
|
|
638
|
+
exercises: false,
|
|
639
|
+
// v4.4.0: opt-in per book
|
|
640
|
+
landing: true
|
|
641
|
+
// v4.5.0: auto-inject minimal root landing
|
|
622
642
|
},
|
|
623
643
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
624
644
|
katex: true,
|
|
@@ -820,6 +840,14 @@ var ROUTE_REGISTRY = {
|
|
|
820
840
|
// v4.3.0 (#70): cross-volume numbered-tips index. Opt-in via
|
|
821
841
|
// routes.tips: true; pairs with build-tips script + <Tip> component.
|
|
822
842
|
tips: { pattern: "/tips", file: "tips.astro" },
|
|
843
|
+
// v4.4.0: exercises index by chapter. Opt-in via routes.exercises: true;
|
|
844
|
+
// pairs with build-exercises script + <ExerciseSolutions auto /> mode.
|
|
845
|
+
exercises: { pattern: "/exercises", file: "exercises.astro" },
|
|
846
|
+
// v4.5.0: minimal root landing page. Reads title/description/portfolio/routes
|
|
847
|
+
// from vite.define-injected import.meta.env vars. Default-on per profile;
|
|
848
|
+
// consumers with their own src/pages/index.astro override (file-system route
|
|
849
|
+
// wins over injectRoute).
|
|
850
|
+
landing: { pattern: "/", file: "index.astro" },
|
|
823
851
|
// v3.4.0 (#7): consumer-collection-backed frontmatter route. Opt-in via
|
|
824
852
|
// routes: { frontmatter: true } AND content.config.ts defining the
|
|
825
853
|
// collection (use frontmatterCollection() helper from /schemas subpath).
|
|
@@ -837,7 +865,16 @@ function resolvePage(file) {
|
|
|
837
865
|
return fileURLToPath(new URL(`../pages/${file}`, import.meta.url));
|
|
838
866
|
}
|
|
839
867
|
function bookScaffoldIntegration(opts) {
|
|
840
|
-
const {
|
|
868
|
+
const {
|
|
869
|
+
profile,
|
|
870
|
+
routes: userOverrides = {},
|
|
871
|
+
extraStyles = [],
|
|
872
|
+
mdxComponentsModule,
|
|
873
|
+
// v4.5.0: landing-page data, propagated via vite.define to /index.astro.
|
|
874
|
+
title,
|
|
875
|
+
description,
|
|
876
|
+
portfolio
|
|
877
|
+
} = opts;
|
|
841
878
|
const def = PROFILES[profile];
|
|
842
879
|
const fmNormalized = normalizeFrontmatterConfig(userOverrides.frontmatter);
|
|
843
880
|
const fmEnabled = fmNormalized?.enabled ?? def.routes.frontmatter;
|
|
@@ -878,12 +915,20 @@ function bookScaffoldIntegration(opts) {
|
|
|
878
915
|
const consumerRoot = fileURLToPath(config.root);
|
|
879
916
|
const resolvedMdxPath = resolveMdxComponentsPath(consumerRoot, mdxComponentsModule);
|
|
880
917
|
const presetLiteral = JSON.stringify(profile);
|
|
918
|
+
const enabledRouteNames = Object.entries(enabledRoutes).filter(([, on]) => on).map(([name]) => name);
|
|
881
919
|
updateConfig({
|
|
882
920
|
vite: {
|
|
883
921
|
plugins: [makeMdxComponentsVitePlugin(resolvedMdxPath)],
|
|
884
922
|
define: {
|
|
885
923
|
"import.meta.env.BOOK_PRESET": presetLiteral,
|
|
886
|
-
"import.meta.env.BOOK_PROFILE": presetLiteral
|
|
924
|
+
"import.meta.env.BOOK_PROFILE": presetLiteral,
|
|
925
|
+
// v4.5.0: landing-page data. JSON.stringify on undefined → 'undefined'
|
|
926
|
+
// (which evaluates to JavaScript undefined at use site); on object →
|
|
927
|
+
// the JSON literal; on false → 'false'.
|
|
928
|
+
"import.meta.env.BOOK_TITLE": JSON.stringify(title ?? null),
|
|
929
|
+
"import.meta.env.BOOK_DESCRIPTION": JSON.stringify(description ?? null),
|
|
930
|
+
"import.meta.env.BOOK_PORTFOLIO": JSON.stringify(portfolio ?? null),
|
|
931
|
+
"import.meta.env.BOOK_ROUTES_ENABLED": JSON.stringify(enabledRouteNames)
|
|
887
932
|
}
|
|
888
933
|
}
|
|
889
934
|
});
|
|
@@ -893,6 +938,10 @@ function bookScaffoldIntegration(opts) {
|
|
|
893
938
|
}
|
|
894
939
|
|
|
895
940
|
// src/config.ts
|
|
941
|
+
var BRANDON_PORTFOLIO_DEFAULT = {
|
|
942
|
+
url: "https://brandon-behring.dev",
|
|
943
|
+
label: "brandon-behring.dev"
|
|
944
|
+
};
|
|
896
945
|
function v3MigrationError(opts) {
|
|
897
946
|
const v3Value = opts.preset ?? opts.profile;
|
|
898
947
|
const v3FieldUsed = "preset" in opts ? "preset" : "profile";
|
|
@@ -973,6 +1022,7 @@ async function defineBookConfig(opts) {
|
|
|
973
1022
|
}
|
|
974
1023
|
]);
|
|
975
1024
|
}
|
|
1025
|
+
const resolvedPortfolio = opts.portfolio === false ? false : opts.portfolio ?? BRANDON_PORTFOLIO_DEFAULT;
|
|
976
1026
|
const integrations = [
|
|
977
1027
|
mdx(),
|
|
978
1028
|
preact(),
|
|
@@ -980,7 +1030,12 @@ async function defineBookConfig(opts) {
|
|
|
980
1030
|
profile,
|
|
981
1031
|
routes: mergedRoutes,
|
|
982
1032
|
mdxComponentsModule,
|
|
983
|
-
extraStyles: mergedExtraStyles
|
|
1033
|
+
extraStyles: mergedExtraStyles,
|
|
1034
|
+
// v4.5.0: pass landing-page data through to the integration so it can
|
|
1035
|
+
// be exposed to the auto-injected /index.astro via vite.define.
|
|
1036
|
+
title: opts.title,
|
|
1037
|
+
description: opts.description,
|
|
1038
|
+
portfolio: resolvedPortfolio
|
|
984
1039
|
}),
|
|
985
1040
|
...mergedExtraIntegrations
|
|
986
1041
|
];
|
|
@@ -1015,6 +1070,10 @@ async function defineBookConfig(opts) {
|
|
|
1015
1070
|
extraStyles: _extraStyles,
|
|
1016
1071
|
markdown: _markdown,
|
|
1017
1072
|
katexMacros: _katexMacros,
|
|
1073
|
+
// v4.5.0: strip new landing-related opts so they don't leak into AstroUserConfig.
|
|
1074
|
+
title: _title,
|
|
1075
|
+
description: _description,
|
|
1076
|
+
portfolio: _portfolio,
|
|
1018
1077
|
...rest
|
|
1019
1078
|
} = opts;
|
|
1020
1079
|
void _styles;
|
|
@@ -1026,6 +1085,9 @@ async function defineBookConfig(opts) {
|
|
|
1026
1085
|
void _extraStyles;
|
|
1027
1086
|
void _markdown;
|
|
1028
1087
|
void _katexMacros;
|
|
1088
|
+
void _title;
|
|
1089
|
+
void _description;
|
|
1090
|
+
void _portfolio;
|
|
1029
1091
|
const katexExternals = wantsKatex ? [] : ["remark-math", "rehype-katex", "katex"];
|
|
1030
1092
|
const config = {
|
|
1031
1093
|
site,
|
|
@@ -1102,6 +1164,7 @@ function defineTips(opts) {
|
|
|
1102
1164
|
export {
|
|
1103
1165
|
BOOK_PRESETS,
|
|
1104
1166
|
BOOK_PROFILES,
|
|
1167
|
+
BRANDON_PORTFOLIO_DEFAULT,
|
|
1105
1168
|
BUILTIN_STYLES,
|
|
1106
1169
|
BookConfigError,
|
|
1107
1170
|
academicChapterSchema,
|
package/dist/schemas.d.ts
CHANGED
package/dist/schemas.mjs
CHANGED
|
@@ -263,8 +263,12 @@ 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
|
|
270
|
+
landing: true
|
|
271
|
+
// v4.5.0: auto-inject minimal root landing; consumers override via src/pages/index.astro
|
|
268
272
|
},
|
|
269
273
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
270
274
|
katex: true,
|
|
@@ -379,8 +383,12 @@ var toolsProfile = defineProfile({
|
|
|
379
383
|
// tools profile ships convergence dashboard
|
|
380
384
|
frontmatter: false,
|
|
381
385
|
// opt-in per book; see #7
|
|
382
|
-
tips: false
|
|
386
|
+
tips: false,
|
|
383
387
|
// v4.3.0 #70: opt-in per book
|
|
388
|
+
exercises: false,
|
|
389
|
+
// v4.4.0: opt-in per book
|
|
390
|
+
landing: true
|
|
391
|
+
// v4.5.0: auto-inject minimal root landing
|
|
384
392
|
},
|
|
385
393
|
styles: [
|
|
386
394
|
"tokens.css",
|
|
@@ -459,8 +467,12 @@ var minimalProfile = defineProfile({
|
|
|
459
467
|
convergence: false,
|
|
460
468
|
frontmatter: false,
|
|
461
469
|
// opt-in per book; see #7
|
|
462
|
-
tips: false
|
|
470
|
+
tips: false,
|
|
463
471
|
// v4.3.0 #70: opt-in per book
|
|
472
|
+
exercises: false,
|
|
473
|
+
// v4.4.0: opt-in per book
|
|
474
|
+
landing: true
|
|
475
|
+
// v4.5.0: auto-inject minimal root landing
|
|
464
476
|
},
|
|
465
477
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
466
478
|
// v3.7.0 (#35): minimal aliases tools schema; fallback renderer field-dispatches if a consumer opts into routes.chapters
|
|
@@ -480,8 +492,12 @@ var courseNotesProfile = defineProfile({
|
|
|
480
492
|
convergence: false,
|
|
481
493
|
frontmatter: false,
|
|
482
494
|
// opt-in per book; see #7
|
|
483
|
-
tips: false
|
|
495
|
+
tips: false,
|
|
484
496
|
// v4.3.0 #70: opt-in per book
|
|
497
|
+
exercises: false,
|
|
498
|
+
// v4.4.0: opt-in per book
|
|
499
|
+
landing: true
|
|
500
|
+
// v4.5.0: auto-inject minimal root landing
|
|
485
501
|
},
|
|
486
502
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
487
503
|
// v3.7.0 (#35): course-notes schema has tools-style fields (chapter, volatility, sources) — fallback renderer dispatches via tools renderer
|
|
@@ -502,8 +518,12 @@ var researchPortfolioProfile = defineProfile({
|
|
|
502
518
|
// tools-profile-specific
|
|
503
519
|
frontmatter: true,
|
|
504
520
|
// portfolios universally need title/disclosure/banner pages
|
|
505
|
-
tips: false
|
|
521
|
+
tips: false,
|
|
506
522
|
// v4.3.0 #70: opt-in per book
|
|
523
|
+
exercises: false,
|
|
524
|
+
// v4.4.0: opt-in per book
|
|
525
|
+
landing: true
|
|
526
|
+
// v4.5.0: auto-inject minimal root landing
|
|
507
527
|
},
|
|
508
528
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
509
529
|
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.5.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>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* /index — minimal default landing page (v4.5.0).
|
|
4
|
+
*
|
|
5
|
+
* Auto-injected by bookScaffoldIntegration when routes.landing === true
|
|
6
|
+
* (default for every profile). Consumers with their own src/pages/index.astro
|
|
7
|
+
* override automatically — file-system routes win over injectRoute, no extra
|
|
8
|
+
* config needed.
|
|
9
|
+
*
|
|
10
|
+
* Reads book identity + portfolio + enabled-routes from vite.define-injected
|
|
11
|
+
* env vars (see integration.ts §4.5.0). Renders:
|
|
12
|
+
* - h1 with book title (fallback: 'book-scaffold-astro')
|
|
13
|
+
* - lead paragraph with description (omitted if not set)
|
|
14
|
+
* - "Read" list of links to enabled scaffold routes (filtered to only
|
|
15
|
+
* routes the integration actually injected; respects routes.<x>: false)
|
|
16
|
+
* - portfolio backlink in the footer (omitted if portfolio: false)
|
|
17
|
+
*
|
|
18
|
+
* Override paths:
|
|
19
|
+
* - Disable entirely: defineBookConfig({ routes: { landing: false } })
|
|
20
|
+
* - Compose differently: create src/pages/index.astro in your consumer
|
|
21
|
+
* repo (file wins; this page never renders)
|
|
22
|
+
* - Change portfolio target: defineBookConfig({ portfolio: { url, label } })
|
|
23
|
+
* or defineBookConfig({ portfolio: false })
|
|
24
|
+
*/
|
|
25
|
+
import Base from '../layouts/Base.astro';
|
|
26
|
+
|
|
27
|
+
// Vite-injected at build time. JSON.stringify(value ?? null) is the
|
|
28
|
+
// integration's convention; null means "not set", an object/string means real value.
|
|
29
|
+
const title = (import.meta.env.BOOK_TITLE as string | null) ?? 'book-scaffold-astro';
|
|
30
|
+
const description = import.meta.env.BOOK_DESCRIPTION as string | null;
|
|
31
|
+
const portfolio = import.meta.env.BOOK_PORTFOLIO as { url: string; label: string } | false | null;
|
|
32
|
+
const enabledRoutes = (import.meta.env.BOOK_ROUTES_ENABLED as string[] | undefined) ?? [];
|
|
33
|
+
|
|
34
|
+
// Map from internal route name → display label + URL. Only routes that
|
|
35
|
+
// produce a single landing-list entry are listed here (frontmatter is a
|
|
36
|
+
// slug pattern; chaptersSlug is dynamic; landing IS this page).
|
|
37
|
+
const ROUTE_LABELS: Record<string, { label: string; href: string }> = {
|
|
38
|
+
chapters: { label: 'Chapters', href: '/chapters/' },
|
|
39
|
+
search: { label: 'Search', href: '/search/' },
|
|
40
|
+
references: { label: 'References', href: '/references/' },
|
|
41
|
+
print: { label: 'Print view', href: '/print/' },
|
|
42
|
+
convergence: { label: 'Convergence', href: '/convergence/' },
|
|
43
|
+
tips: { label: 'Tips', href: '/tips/' },
|
|
44
|
+
exercises: { label: 'Exercises', href: '/exercises/' },
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const visibleRoutes = enabledRoutes
|
|
48
|
+
.filter((name) => name in ROUTE_LABELS)
|
|
49
|
+
.map((name) => ROUTE_LABELS[name]!);
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
<Base title={title} description={description ?? undefined}>
|
|
53
|
+
<article class="prose landing">
|
|
54
|
+
<h1>{title}</h1>
|
|
55
|
+
{description && <p class="lead">{description}</p>}
|
|
56
|
+
|
|
57
|
+
{visibleRoutes.length > 0 && (
|
|
58
|
+
<>
|
|
59
|
+
<h2 class="read-heading">Read</h2>
|
|
60
|
+
<ul class="route-list">
|
|
61
|
+
{visibleRoutes.map((r) => (
|
|
62
|
+
<li><a href={r.href}>{r.label}</a></li>
|
|
63
|
+
))}
|
|
64
|
+
</ul>
|
|
65
|
+
</>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
{portfolio && portfolio !== false && (
|
|
69
|
+
<p class="portfolio-footer">
|
|
70
|
+
<small>
|
|
71
|
+
Part of <a href={portfolio.url}>{portfolio.label}</a>
|
|
72
|
+
</small>
|
|
73
|
+
</p>
|
|
74
|
+
)}
|
|
75
|
+
</article>
|
|
76
|
+
</Base>
|
|
77
|
+
|
|
78
|
+
<style>
|
|
79
|
+
.landing {
|
|
80
|
+
max-width: 60ch;
|
|
81
|
+
margin: 2rem auto;
|
|
82
|
+
}
|
|
83
|
+
.lead {
|
|
84
|
+
font-size: 1.1rem;
|
|
85
|
+
line-height: 1.5;
|
|
86
|
+
margin: 1.25rem 0 2.5rem 0;
|
|
87
|
+
}
|
|
88
|
+
.read-heading {
|
|
89
|
+
font-size: 0.85rem;
|
|
90
|
+
text-transform: uppercase;
|
|
91
|
+
letter-spacing: 0.08em;
|
|
92
|
+
color: var(--color-text-muted, #555);
|
|
93
|
+
margin-top: 2.5rem;
|
|
94
|
+
margin-bottom: 0.75rem;
|
|
95
|
+
}
|
|
96
|
+
.route-list {
|
|
97
|
+
list-style: none;
|
|
98
|
+
padding: 0;
|
|
99
|
+
margin: 0;
|
|
100
|
+
}
|
|
101
|
+
.route-list li {
|
|
102
|
+
padding: 0.4rem 0;
|
|
103
|
+
font-family: var(--font-sans, sans-serif);
|
|
104
|
+
font-size: 1rem;
|
|
105
|
+
}
|
|
106
|
+
.portfolio-footer {
|
|
107
|
+
margin-top: 3rem;
|
|
108
|
+
color: var(--color-text-muted, #555);
|
|
109
|
+
font-family: var(--font-sans, sans-serif);
|
|
110
|
+
}
|
|
111
|
+
</style>
|
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
|
@@ -41,6 +41,16 @@ export interface RouteToggles {
|
|
|
41
41
|
print: boolean;
|
|
42
42
|
chapters: boolean;
|
|
43
43
|
convergence: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* v4.5.0: auto-inject a minimal `/` landing page that reads the book's
|
|
46
|
+
* `title`, `description`, and `portfolio` from defineBookConfig and
|
|
47
|
+
* renders a route list (filtered to enabled routes). Defaults to `true`
|
|
48
|
+
* on every profile. Consumers with their own `src/pages/index.astro` keep
|
|
49
|
+
* their custom landing (file-system routes win over `injectRoute`). Set
|
|
50
|
+
* to `false` to suppress the auto-injection entirely without writing
|
|
51
|
+
* a custom landing.
|
|
52
|
+
*/
|
|
53
|
+
landing: boolean;
|
|
44
54
|
/**
|
|
45
55
|
* v3.4.0 (closes #7): auto-inject `/frontmatter/[slug]/` rendering a
|
|
46
56
|
* consumer-defined `frontmatter` content collection. Default `false` per
|
|
@@ -57,6 +67,14 @@ export interface RouteToggles {
|
|
|
57
67
|
* defineBookConfig({ routes: { tips: true } }).
|
|
58
68
|
*/
|
|
59
69
|
tips: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* v4.4.0: auto-inject `/exercises` route listing all `<Exercise id="...">`
|
|
72
|
+
* instances from chapter MDX, grouped by chapter with deep links into
|
|
73
|
+
* the chapter routes. Requires running `book-scaffold build-exercises`
|
|
74
|
+
* (via prebuild) which emits src/data/exercises.json. Default `false` per
|
|
75
|
+
* profile — opt in via defineBookConfig({ routes: { exercises: true } }).
|
|
76
|
+
*/
|
|
77
|
+
exercises: boolean;
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
/** Profile definition — declarative shape for one book profile. */
|
package/src/profiles/academic.ts
CHANGED
|
@@ -24,6 +24,8 @@ 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
|
|
28
|
+
landing: true, // v4.5.0: auto-inject minimal root landing; consumers override via src/pages/index.astro
|
|
27
29
|
},
|
|
28
30
|
styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css'],
|
|
29
31
|
katex: true,
|
|
@@ -26,6 +26,8 @@ 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
|
|
30
|
+
landing: true, // v4.5.0: auto-inject minimal root landing
|
|
29
31
|
},
|
|
30
32
|
styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css'],
|
|
31
33
|
// 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,8 @@ 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
|
|
25
|
+
landing: true, // v4.5.0: auto-inject minimal root landing
|
|
24
26
|
},
|
|
25
27
|
styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css'],
|
|
26
28
|
// v3.7.0 (#35): minimal aliases tools schema; fallback renderer field-dispatches if a consumer opts into routes.chapters
|
|
@@ -37,6 +37,8 @@ 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
|
|
41
|
+
landing: true, // v4.5.0: auto-inject minimal root landing
|
|
40
42
|
},
|
|
41
43
|
styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css'],
|
|
42
44
|
katex: true, // math is common in research content
|
package/src/profiles/tools.ts
CHANGED
|
@@ -21,6 +21,8 @@ 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
|
|
25
|
+
landing: true, // v4.5.0: auto-inject minimal root landing
|
|
24
26
|
},
|
|
25
27
|
styles: [
|
|
26
28
|
'tokens.css', 'layout.css', 'callouts.css', 'chapter.css',
|