@brandon_m_behring/book-scaffold-astro 4.31.0 → 5.0.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/CLAUDE.md +37 -2
- package/MIGRATION-v4-to-v5.md +183 -0
- package/README.md +30 -1
- package/components/AssessmentTest.astro +26 -5
- package/components/BookLink.astro +6 -3
- package/components/ChapterNav.astro +1 -1
- package/components/Cite.astro +37 -4
- package/components/ExerciseSolutions.astro +24 -4
- package/components/Flashcards.tsx +19 -11
- package/components/NavContent.astro +60 -11
- package/components/ObjectiveMap.astro +14 -2
- package/components/PartReview.astro +32 -4
- package/components/PatternTimeline.astro +6 -2
- package/components/Rationale.astro +20 -3
- package/components/Sidebar.astro +11 -3
- package/components/SourceArchive.astro +33 -3
- package/components/Term.astro +18 -1
- package/components/Theorem.astro +15 -5
- package/components/TipsCard.astro +40 -3
- package/components/XRef.astro +14 -2
- package/dist/components/ExamRunner.d.ts +1 -1
- package/dist/components/Flashcards.d.ts +6 -1
- package/dist/components/Flashcards.mjs +14 -11
- package/dist/{exam-manifest-X9IrX1G3.d.ts → exam-manifest-DttY7kyZ.d.ts} +1 -1
- package/dist/index.d.ts +76 -8
- package/dist/index.mjs +517 -43
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +311 -32
- package/dist/{types-DgSlAew3.d.ts → types-D1QZgKMO.d.ts} +73 -21
- package/layouts/Base.astro +38 -9
- package/layouts/Chapter.astro +10 -2
- package/package.json +4 -2
- package/pages/answers.astro +25 -6
- package/pages/book.astro +75 -0
- package/pages/chapters/[...slug].astro +39 -7
- package/pages/chapters-book.astro +17 -0
- package/pages/chapters.astro +87 -9
- package/pages/convergence.astro +40 -10
- package/pages/corpus-apparatus/answers.astro +15 -0
- package/pages/corpus-apparatus/convergence.astro +15 -0
- package/pages/corpus-apparatus/exercises.astro +15 -0
- package/pages/corpus-apparatus/flashcards.astro +15 -0
- package/pages/corpus-apparatus/glossary.astro +15 -0
- package/pages/corpus-apparatus/practice-exam.astro +15 -0
- package/pages/corpus-apparatus/print.astro +15 -0
- package/pages/corpus-apparatus/references.astro +15 -0
- package/pages/corpus-apparatus/tips.astro +15 -0
- package/pages/exercises.astro +18 -5
- package/pages/flashcards.astro +26 -5
- package/pages/glossary.astro +20 -3
- package/pages/index.astro +54 -1
- package/pages/practice-exam.astro +8 -2
- package/pages/print.astro +7 -2
- package/pages/references.astro +26 -6
- package/pages/search.astro +65 -4
- package/pages/tips.astro +17 -4
- package/recipes/09-validation.md +1 -1
- package/recipes/15-defining-styles.md +6 -6
- package/recipes/20-anki-export.md +15 -8
- package/recipes/21-multi-guide-single-app.md +293 -44
- package/recipes/22-responsive-nav-and-multibook-routing.md +7 -1
- package/recipes/README.md +2 -2
- package/scripts/build-bib.mjs +113 -35
- package/scripts/build-exercises.mjs +101 -24
- package/scripts/build-labels.mjs +199 -194
- package/scripts/build-tips.mjs +95 -23
- package/scripts/corpus-tooling.mjs +268 -0
- package/scripts/resolve-book-config.mjs +99 -1
- package/scripts/validate.mjs +676 -100
- package/scripts/walk-mdx.mjs +16 -4
- package/src/lib/book-link.ts +72 -0
- package/src/lib/chapters.ts +10 -4
- package/src/lib/corpus-collateral.ts +9 -0
- package/src/lib/corpus.ts +458 -0
- package/src/lib/define-style.ts +28 -15
- package/src/lib/exam-manifest.ts +4 -1
- package/src/lib/patterns.ts +15 -6
- package/src/lib/questions.ts +8 -3
- package/src/types.ts +603 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
3
|
+
import { corpusBookHasApparatusRoute } from '../../src/lib/corpus';
|
|
4
|
+
import ExercisesPage from '../exercises.astro';
|
|
5
|
+
|
|
6
|
+
export function getStaticPaths() {
|
|
7
|
+
if (!bookConfig.corpus) return [];
|
|
8
|
+
return bookConfig.corpus.books
|
|
9
|
+
.filter((book) => corpusBookHasApparatusRoute(bookConfig.corpus!, book.id, 'exercises', bookConfig.apparatusRoutes))
|
|
10
|
+
.map((book) => ({ params: { book: book.id }, props: { bookId: book.id } }));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { bookId } = Astro.props;
|
|
14
|
+
---
|
|
15
|
+
<ExercisesPage bookId={bookId} />
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
3
|
+
import { corpusBookHasApparatusRoute } from '../../src/lib/corpus';
|
|
4
|
+
import FlashcardsPage from '../flashcards.astro';
|
|
5
|
+
|
|
6
|
+
export function getStaticPaths() {
|
|
7
|
+
if (!bookConfig.corpus) return [];
|
|
8
|
+
return bookConfig.corpus.books
|
|
9
|
+
.filter((book) => corpusBookHasApparatusRoute(bookConfig.corpus!, book.id, 'flashcards', bookConfig.apparatusRoutes))
|
|
10
|
+
.map((book) => ({ params: { book: book.id }, props: { bookId: book.id } }));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { bookId } = Astro.props;
|
|
14
|
+
---
|
|
15
|
+
<FlashcardsPage bookId={bookId} />
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
3
|
+
import { corpusBookHasApparatusRoute } from '../../src/lib/corpus';
|
|
4
|
+
import GlossaryPage from '../glossary.astro';
|
|
5
|
+
|
|
6
|
+
export function getStaticPaths() {
|
|
7
|
+
if (!bookConfig.corpus) return [];
|
|
8
|
+
return bookConfig.corpus.books
|
|
9
|
+
.filter((book) => corpusBookHasApparatusRoute(bookConfig.corpus!, book.id, 'glossary', bookConfig.apparatusRoutes))
|
|
10
|
+
.map((book) => ({ params: { book: book.id }, props: { bookId: book.id } }));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { bookId } = Astro.props;
|
|
14
|
+
---
|
|
15
|
+
<GlossaryPage bookId={bookId} />
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
3
|
+
import { corpusBookHasApparatusRoute } from '../../src/lib/corpus';
|
|
4
|
+
import PracticeExamPage from '../practice-exam.astro';
|
|
5
|
+
|
|
6
|
+
export function getStaticPaths() {
|
|
7
|
+
if (!bookConfig.corpus) return [];
|
|
8
|
+
return bookConfig.corpus.books
|
|
9
|
+
.filter((book) => corpusBookHasApparatusRoute(bookConfig.corpus!, book.id, 'practice-exam', bookConfig.apparatusRoutes))
|
|
10
|
+
.map((book) => ({ params: { book: book.id }, props: { bookId: book.id } }));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { bookId } = Astro.props;
|
|
14
|
+
---
|
|
15
|
+
<PracticeExamPage bookId={bookId} />
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
3
|
+
import { corpusBookHasApparatusRoute } from '../../src/lib/corpus';
|
|
4
|
+
import PrintPage from '../print.astro';
|
|
5
|
+
|
|
6
|
+
export function getStaticPaths() {
|
|
7
|
+
if (!bookConfig.corpus) return [];
|
|
8
|
+
return bookConfig.corpus.books
|
|
9
|
+
.filter((book) => corpusBookHasApparatusRoute(bookConfig.corpus!, book.id, 'print', bookConfig.apparatusRoutes))
|
|
10
|
+
.map((book) => ({ params: { book: book.id }, props: { bookId: book.id } }));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { bookId } = Astro.props;
|
|
14
|
+
---
|
|
15
|
+
<PrintPage bookId={bookId} />
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
3
|
+
import { corpusBookHasApparatusRoute } from '../../src/lib/corpus';
|
|
4
|
+
import ReferencesPage from '../references.astro';
|
|
5
|
+
|
|
6
|
+
export function getStaticPaths() {
|
|
7
|
+
if (!bookConfig.corpus) return [];
|
|
8
|
+
return bookConfig.corpus.books
|
|
9
|
+
.filter((book) => corpusBookHasApparatusRoute(bookConfig.corpus!, book.id, 'references', bookConfig.apparatusRoutes))
|
|
10
|
+
.map((book) => ({ params: { book: book.id }, props: { bookId: book.id } }));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { bookId } = Astro.props;
|
|
14
|
+
---
|
|
15
|
+
<ReferencesPage bookId={bookId} />
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
3
|
+
import { corpusBookHasApparatusRoute } from '../../src/lib/corpus';
|
|
4
|
+
import TipsPage from '../tips.astro';
|
|
5
|
+
|
|
6
|
+
export function getStaticPaths() {
|
|
7
|
+
if (!bookConfig.corpus) return [];
|
|
8
|
+
return bookConfig.corpus.books
|
|
9
|
+
.filter((book) => corpusBookHasApparatusRoute(bookConfig.corpus!, book.id, 'tips', bookConfig.apparatusRoutes))
|
|
10
|
+
.map((book) => ({ params: { book: book.id }, props: { bookId: book.id } }));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { bookId } = Astro.props;
|
|
14
|
+
---
|
|
15
|
+
<TipsPage bookId={bookId} />
|
package/pages/exercises.astro
CHANGED
|
@@ -11,16 +11,29 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import Base from '../layouts/Base.astro';
|
|
13
13
|
import { normalizeBase } from '../src/lib/nav-href';
|
|
14
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
15
|
+
import { selectBookArtifact } from '../src/lib/corpus';
|
|
16
|
+
|
|
17
|
+
interface Props { bookId?: string; }
|
|
18
|
+
const { bookId } = Astro.props;
|
|
14
19
|
|
|
15
20
|
// v4.4.0 fix: use Vite's import.meta.glob with a project-root-relative
|
|
16
21
|
// path (same lesson as tips.astro). `/src/data/...` is consumer-project-
|
|
17
22
|
// rooted in Vite's resolver and works across all consumer build contexts.
|
|
18
|
-
|
|
23
|
+
type ExerciseIndex = Record<string, Array<{ id: string; problem: string }>>;
|
|
24
|
+
const exModules = import.meta.glob<{ default: unknown }>(
|
|
19
25
|
'/src/data/exercises.json',
|
|
20
26
|
{ eager: true },
|
|
21
27
|
);
|
|
22
28
|
const exEntry = exModules['/src/data/exercises.json'];
|
|
23
|
-
const byChapter = exEntry
|
|
29
|
+
const byChapter = exEntry
|
|
30
|
+
? selectBookArtifact<ExerciseIndex>(
|
|
31
|
+
exEntry.default,
|
|
32
|
+
bookConfig.corpus,
|
|
33
|
+
bookId,
|
|
34
|
+
'src/data/exercises.json',
|
|
35
|
+
)
|
|
36
|
+
: {};
|
|
24
37
|
const loadError = exEntry
|
|
25
38
|
? null
|
|
26
39
|
: 'src/data/exercises.json not found — run `npx book-scaffold build-exercises` to generate.';
|
|
@@ -31,7 +44,7 @@ const total = chapters.reduce((sum, c) => sum + byChapter[c].length, 0);
|
|
|
31
44
|
// #142: chapter deep-links must prefix the deploy base.
|
|
32
45
|
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
33
46
|
---
|
|
34
|
-
<Base title="Exercises" description="All exercises in this book, grouped by chapter with deep links.">
|
|
47
|
+
<Base title="Exercises" description="All exercises in this book, grouped by chapter with deep links." bookId={bookId}>
|
|
35
48
|
<article class="prose">
|
|
36
49
|
<h1>Exercises</h1>
|
|
37
50
|
{loadError && (
|
|
@@ -51,12 +64,12 @@ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
|
51
64
|
{chapters.map((chapter) => (
|
|
52
65
|
<section class="exercises-chapter" id={`chapter-${chapter}`}>
|
|
53
66
|
<h2 class="exercises-chapter-title">
|
|
54
|
-
Chapter: <a href={`${baseUrl}chapters/${chapter}/`}>{chapter}</a>
|
|
67
|
+
Chapter: <a href={`${baseUrl}chapters/${bookId ? `${bookId}/` : ''}${chapter}/`}>{chapter}</a>
|
|
55
68
|
</h2>
|
|
56
69
|
<ol class="exercises-list">
|
|
57
70
|
{byChapter[chapter].map((ex) => (
|
|
58
71
|
<li class="exercises-item">
|
|
59
|
-
<a href={`${baseUrl}chapters/${chapter}/#exercise-${ex.id}`} class="exercises-link">
|
|
72
|
+
<a href={`${baseUrl}chapters/${bookId ? `${bookId}/` : ''}${chapter}/#exercise-${ex.id}`} class="exercises-link">
|
|
60
73
|
<strong class="exercises-id">Exercise {ex.id}</strong>
|
|
61
74
|
<span class="exercises-preview">{ex.problem.slice(0, 120)}{ex.problem.length > 120 ? '…' : ''}</span>
|
|
62
75
|
</a>
|
package/pages/flashcards.astro
CHANGED
|
@@ -24,30 +24,47 @@ import Base from '../layouts/Base.astro';
|
|
|
24
24
|
import { getCollection, render } from 'astro:content';
|
|
25
25
|
import Flashcards from '@brandon_m_behring/book-scaffold-astro/components/Flashcards';
|
|
26
26
|
import { buildFlashcardDeck } from '../src/lib/flashcards';
|
|
27
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
28
|
+
import { localCorpusEntryId } from '../src/lib/corpus';
|
|
27
29
|
import '../styles/flashcards.css';
|
|
28
30
|
|
|
31
|
+
interface Props { bookId?: string; }
|
|
32
|
+
const { bookId } = Astro.props;
|
|
33
|
+
|
|
29
34
|
// Presence-gate: only touch the collection when the directory holds files.
|
|
30
35
|
const glossModules = import.meta.glob('/src/content/glossary/**/*.{md,mdx}', {
|
|
31
36
|
query: '?raw',
|
|
32
37
|
import: 'default',
|
|
33
38
|
eager: true,
|
|
34
39
|
});
|
|
35
|
-
const hasGlossary =
|
|
40
|
+
const hasGlossary = bookId
|
|
41
|
+
? Object.keys(glossModules).some((path) => path.includes(`/glossary/${bookId}/`))
|
|
42
|
+
: Object.keys(glossModules).length > 0;
|
|
36
43
|
|
|
37
|
-
const
|
|
38
|
-
const
|
|
44
|
+
const allEntries = hasGlossary ? await getCollection('glossary', (e) => !e.data.draft) : [];
|
|
45
|
+
const entries = bookId
|
|
46
|
+
? allEntries.filter((entry) => entry.id.startsWith(`${bookId}/`))
|
|
47
|
+
: allEntries;
|
|
48
|
+
const localId = (id: string) => bookId && bookConfig.corpus
|
|
49
|
+
? localCorpusEntryId(bookConfig.corpus, bookId, id)
|
|
50
|
+
: id;
|
|
51
|
+
const deck = buildFlashcardDeck(entries.map((entry) => ({ ...entry, id: localId(entry.id) })));
|
|
39
52
|
// Render definitions in deck (alphabetical) order so cards and manifest agree.
|
|
40
|
-
const byId = new Map(entries.map((e) => [e.id, e]));
|
|
53
|
+
const byId = new Map(entries.map((e) => [localId(e.id), e]));
|
|
41
54
|
const rendered = await Promise.all(
|
|
42
55
|
deck.map(async (card) => {
|
|
43
56
|
const entry = byId.get(card.id)!;
|
|
44
57
|
return { ...card, Content: (await render(entry)).Content };
|
|
45
58
|
}),
|
|
46
59
|
);
|
|
60
|
+
const storageKey = bookId
|
|
61
|
+
? `book:${import.meta.env.BASE_URL}:flashcards:${bookId}:known`
|
|
62
|
+
: undefined;
|
|
47
63
|
---
|
|
48
64
|
<Base
|
|
49
65
|
title="Flashcards"
|
|
50
66
|
description="Glossary flashcards: shuffled recall practice with flip-to-check and known/still-learning buckets."
|
|
67
|
+
bookId={bookId}
|
|
51
68
|
>
|
|
52
69
|
<article class="prose flashcards-page" data-flashcards-root>
|
|
53
70
|
<h1>Flashcards</h1>
|
|
@@ -65,7 +82,11 @@ const rendered = await Promise.all(
|
|
|
65
82
|
{/* Without JS the island's deck controls would render dead — hide
|
|
66
83
|
them; the full card list below stays readable. */}
|
|
67
84
|
<noscript><style is:inline>.flashcards-runner { display: none; }</style></noscript>
|
|
68
|
-
<Flashcards
|
|
85
|
+
<Flashcards
|
|
86
|
+
client:idle
|
|
87
|
+
deck={deck.map(({ id, front }) => ({ id, front }))}
|
|
88
|
+
storageKey={storageKey}
|
|
89
|
+
/>
|
|
69
90
|
<section class="flashcards-list">
|
|
70
91
|
{rendered.map((card) => {
|
|
71
92
|
const Content = card.Content;
|
package/pages/glossary.astro
CHANGED
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import Base from '../layouts/Base.astro';
|
|
18
18
|
import { getCollection, render } from 'astro:content';
|
|
19
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
20
|
+
import { localCorpusEntryId } from '../src/lib/corpus';
|
|
21
|
+
|
|
22
|
+
interface Props { bookId?: string; }
|
|
23
|
+
const { bookId } = Astro.props;
|
|
19
24
|
|
|
20
25
|
// Presence-gate: only touch the collection when the directory holds files.
|
|
21
26
|
const glossModules = import.meta.glob('/src/content/glossary/**/*.{md,mdx}', {
|
|
@@ -23,14 +28,25 @@ const glossModules = import.meta.glob('/src/content/glossary/**/*.{md,mdx}', {
|
|
|
23
28
|
import: 'default',
|
|
24
29
|
eager: true,
|
|
25
30
|
});
|
|
26
|
-
const hasGlossary =
|
|
31
|
+
const hasGlossary = bookId
|
|
32
|
+
? Object.keys(glossModules).some((path) => path.includes(`/glossary/${bookId}/`))
|
|
33
|
+
: Object.keys(glossModules).length > 0;
|
|
27
34
|
|
|
28
|
-
const
|
|
35
|
+
const allEntries = hasGlossary ? await getCollection('glossary', (e) => !e.data.draft) : [];
|
|
36
|
+
const entries = bookId
|
|
37
|
+
? allEntries.filter((entry) => entry.id.startsWith(`${bookId}/`))
|
|
38
|
+
: allEntries;
|
|
29
39
|
// Alphabetical by display term (locale-aware so case/accents sort naturally).
|
|
30
40
|
entries.sort((a, b) => a.data.term.localeCompare(b.data.term));
|
|
31
41
|
|
|
32
42
|
const rendered = await Promise.all(
|
|
33
|
-
entries.map(async (e) => ({
|
|
43
|
+
entries.map(async (e) => ({
|
|
44
|
+
id: bookId && bookConfig.corpus
|
|
45
|
+
? localCorpusEntryId(bookConfig.corpus, bookId, e.id)
|
|
46
|
+
: e.id,
|
|
47
|
+
data: e.data,
|
|
48
|
+
Content: (await render(e)).Content,
|
|
49
|
+
})),
|
|
34
50
|
);
|
|
35
51
|
const total = rendered.length;
|
|
36
52
|
// id → display term, so "See also" links show the friendly term, not the kebab id.
|
|
@@ -39,6 +55,7 @@ const termById = new Map(rendered.map((t) => [t.id, t.data.term]));
|
|
|
39
55
|
<Base
|
|
40
56
|
title="Glossary"
|
|
41
57
|
description="Searchable key-terms glossary, listed alphabetically."
|
|
58
|
+
bookId={bookId}
|
|
42
59
|
>
|
|
43
60
|
<article class="prose glossary">
|
|
44
61
|
<h1>Glossary</h1>
|
package/pages/index.astro
CHANGED
|
@@ -60,10 +60,17 @@ const ROUTE_LABELS: Record<string, { label: string; href: string }> = {
|
|
|
60
60
|
|
|
61
61
|
const visibleRoutes = enabledRoutes
|
|
62
62
|
.filter((name) => name in ROUTE_LABELS)
|
|
63
|
+
.filter((name) => !bookConfig.corpus || name === 'chapters' || name === 'search')
|
|
63
64
|
.map((name) => ROUTE_LABELS[name]!);
|
|
64
65
|
---
|
|
65
66
|
|
|
66
|
-
<Base
|
|
67
|
+
<Base
|
|
68
|
+
title={title}
|
|
69
|
+
description={description ?? undefined}
|
|
70
|
+
showChrome={!bookConfig.corpus}
|
|
71
|
+
showSidebar={!bookConfig.corpus}
|
|
72
|
+
pagefindSurface={bookConfig.corpus ? 'corpus' : undefined}
|
|
73
|
+
>
|
|
67
74
|
<article class="prose landing">
|
|
68
75
|
<h1>{title}</h1>
|
|
69
76
|
{description && <p class="lead">{description}</p>}
|
|
@@ -79,6 +86,22 @@ const visibleRoutes = enabledRoutes
|
|
|
79
86
|
</>
|
|
80
87
|
)}
|
|
81
88
|
|
|
89
|
+
{bookConfig.corpus && (
|
|
90
|
+
<section aria-labelledby="corpus-books-heading">
|
|
91
|
+
<h2 id="corpus-books-heading" class="read-heading">Books</h2>
|
|
92
|
+
<ol class="book-list">
|
|
93
|
+
{bookConfig.corpus.books.map((book) => (
|
|
94
|
+
<li>
|
|
95
|
+
<a href={`${baseUrl}${book.id}/`}>
|
|
96
|
+
<strong>{book.title}</strong>
|
|
97
|
+
{book.description && <span>{book.description}</span>}
|
|
98
|
+
</a>
|
|
99
|
+
</li>
|
|
100
|
+
))}
|
|
101
|
+
</ol>
|
|
102
|
+
</section>
|
|
103
|
+
)}
|
|
104
|
+
|
|
82
105
|
{portfolio && portfolio !== false && (
|
|
83
106
|
<p class="portfolio-footer">
|
|
84
107
|
<small>
|
|
@@ -117,6 +140,36 @@ const visibleRoutes = enabledRoutes
|
|
|
117
140
|
font-family: var(--font-sans, sans-serif);
|
|
118
141
|
font-size: 1rem;
|
|
119
142
|
}
|
|
143
|
+
.book-list {
|
|
144
|
+
list-style: none;
|
|
145
|
+
padding: 0;
|
|
146
|
+
display: grid;
|
|
147
|
+
gap: var(--space-4);
|
|
148
|
+
}
|
|
149
|
+
.book-list li {
|
|
150
|
+
margin: 0;
|
|
151
|
+
}
|
|
152
|
+
.book-list a {
|
|
153
|
+
display: block;
|
|
154
|
+
padding: var(--space-4);
|
|
155
|
+
border: 1px solid var(--color-border);
|
|
156
|
+
border-radius: var(--radius-md);
|
|
157
|
+
color: inherit;
|
|
158
|
+
text-decoration: none;
|
|
159
|
+
background: var(--color-bg-subtle);
|
|
160
|
+
}
|
|
161
|
+
.book-list a:hover,
|
|
162
|
+
.book-list a:focus-visible {
|
|
163
|
+
border-color: var(--color-link);
|
|
164
|
+
}
|
|
165
|
+
.book-list strong,
|
|
166
|
+
.book-list span {
|
|
167
|
+
display: block;
|
|
168
|
+
}
|
|
169
|
+
.book-list span {
|
|
170
|
+
margin-top: var(--space-1);
|
|
171
|
+
color: var(--color-text-muted);
|
|
172
|
+
}
|
|
120
173
|
.portfolio-footer {
|
|
121
174
|
margin-top: 3rem;
|
|
122
175
|
color: var(--color-text-muted, #555);
|
|
@@ -42,15 +42,20 @@ import { getAllQuestions, groupByDomain } from '../src/lib/questions';
|
|
|
42
42
|
import { assertKnownDomain } from '../src/lib/exam-domains';
|
|
43
43
|
import { buildExamManifest } from '../src/lib/exam-manifest';
|
|
44
44
|
|
|
45
|
+
interface Props { bookId?: string; }
|
|
46
|
+
const { bookId } = Astro.props;
|
|
47
|
+
|
|
45
48
|
// Presence-gate: only touch the collection when the directory holds files.
|
|
46
49
|
const questionModules = import.meta.glob('/src/content/questions/**/*.{md,mdx}', {
|
|
47
50
|
query: '?raw',
|
|
48
51
|
import: 'default',
|
|
49
52
|
eager: true,
|
|
50
53
|
});
|
|
51
|
-
const hasQuestions =
|
|
54
|
+
const hasQuestions = bookId
|
|
55
|
+
? Object.keys(questionModules).some((path) => path.includes(`/questions/${bookId}/`))
|
|
56
|
+
: Object.keys(questionModules).length > 0;
|
|
52
57
|
|
|
53
|
-
const entries = hasQuestions ? await getAllQuestions() : [];
|
|
58
|
+
const entries = hasQuestions ? await getAllQuestions(bookId) : [];
|
|
54
59
|
|
|
55
60
|
// Fail-loud: every question's domain must be in the examDomains registry.
|
|
56
61
|
for (const entry of entries) {
|
|
@@ -78,6 +83,7 @@ const manifest = buildExamManifest(entries);
|
|
|
78
83
|
<Base
|
|
79
84
|
title="Practice question bank"
|
|
80
85
|
description="All practice questions in this book, grouped by exam domain, with a scored practice-exam mode and answers behind a reveal."
|
|
86
|
+
bookId={bookId}
|
|
81
87
|
>
|
|
82
88
|
<article class="prose practice-exam" data-exam-root>
|
|
83
89
|
<h1>Practice question bank</h1>
|
package/pages/print.astro
CHANGED
|
@@ -26,7 +26,12 @@ import { getAllChapters } from '../src/lib/chapters';
|
|
|
26
26
|
import ChapterHeader from '../components/ChapterHeader.astro';
|
|
27
27
|
import mdxComponents from 'virtual:book-scaffold/mdx-components';
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
interface Props { bookId?: string; }
|
|
30
|
+
const { bookId } = Astro.props;
|
|
31
|
+
const allChapters = await getAllChapters();
|
|
32
|
+
const chapters = bookId
|
|
33
|
+
? allChapters.filter((entry) => entry.id.startsWith(`${bookId}/`))
|
|
34
|
+
: allChapters;
|
|
30
35
|
const rendered = await Promise.all(
|
|
31
36
|
chapters.map(async (entry) => {
|
|
32
37
|
const { Content } = await render(entry);
|
|
@@ -35,7 +40,7 @@ const rendered = await Promise.all(
|
|
|
35
40
|
);
|
|
36
41
|
---
|
|
37
42
|
|
|
38
|
-
<Base title="Book (print edition)" description="Full book rendered as a single paginated document for PDF export.">
|
|
43
|
+
<Base title="Book (print edition)" description="Full book rendered as a single paginated document for PDF export." bookId={bookId}>
|
|
39
44
|
<div class="prose print-edition">
|
|
40
45
|
{rendered.map(({ entry, Content }) => (
|
|
41
46
|
<section class="chapter-print">
|
package/pages/references.astro
CHANGED
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import Base from '../layouts/Base.astro';
|
|
19
19
|
import SourceArchive from '../components/SourceArchive.astro';
|
|
20
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
21
|
+
import { selectBookArtifact } from '../src/lib/corpus';
|
|
22
|
+
|
|
23
|
+
interface Props { bookId?: string; }
|
|
24
|
+
const { bookId } = Astro.props;
|
|
20
25
|
|
|
21
26
|
type CslAuthor = { family?: string; given?: string; literal?: string };
|
|
22
27
|
type CslEntry = {
|
|
@@ -36,23 +41,38 @@ type CslEntry = {
|
|
|
36
41
|
};
|
|
37
42
|
|
|
38
43
|
// --- Bibliography: BibTeX → references.json (academic profile). ---
|
|
39
|
-
const refsModules = import.meta.glob<{ default:
|
|
44
|
+
const refsModules = import.meta.glob<{ default: unknown }>(
|
|
40
45
|
'/src/data/references.json',
|
|
41
46
|
{ eager: true },
|
|
42
47
|
);
|
|
43
48
|
const refsModule = refsModules['/src/data/references.json'];
|
|
44
|
-
const map =
|
|
49
|
+
const map = refsModule
|
|
50
|
+
? selectBookArtifact<Record<string, CslEntry>>(
|
|
51
|
+
refsModule.default,
|
|
52
|
+
bookConfig.corpus,
|
|
53
|
+
bookId,
|
|
54
|
+
'src/data/references.json',
|
|
55
|
+
)
|
|
56
|
+
: {};
|
|
45
57
|
const entries = Object.values(map);
|
|
46
58
|
|
|
47
59
|
// --- Cited sources: sources/manifest.yaml → sources.json (tools profile).
|
|
48
60
|
// Presence-only gate; the actual render reads the live `sources` collection
|
|
49
61
|
// via <SourceArchive>, which only runs when this gate is true (so it is never
|
|
50
62
|
// reached on profiles without a `sources` collection). ---
|
|
51
|
-
const srcModules = import.meta.glob<{ default: unknown
|
|
63
|
+
const srcModules = import.meta.glob<{ default: unknown }>(
|
|
52
64
|
'/src/data/sources.json',
|
|
53
65
|
{ eager: true },
|
|
54
66
|
);
|
|
55
|
-
const
|
|
67
|
+
const sourcesModule = srcModules['/src/data/sources.json'];
|
|
68
|
+
const sourcesData = sourcesModule
|
|
69
|
+
? selectBookArtifact<unknown[]>(
|
|
70
|
+
sourcesModule.default,
|
|
71
|
+
bookConfig.corpus,
|
|
72
|
+
bookId,
|
|
73
|
+
'src/data/sources.json',
|
|
74
|
+
)
|
|
75
|
+
: [];
|
|
56
76
|
const hasSources = Array.isArray(sourcesData) && sourcesData.length > 0;
|
|
57
77
|
const hasBib = entries.length > 0;
|
|
58
78
|
|
|
@@ -100,7 +120,7 @@ const lede =
|
|
|
100
120
|
? 'Every external source cited in this book, grouped by tier in descending authority.'
|
|
101
121
|
: 'This book has no references yet.';
|
|
102
122
|
---
|
|
103
|
-
<Base title="References" description="Bibliography and cited sources for this book.">
|
|
123
|
+
<Base title="References" description="Bibliography and cited sources for this book." bookId={bookId}>
|
|
104
124
|
<article class="prose">
|
|
105
125
|
<header>
|
|
106
126
|
<h1>References</h1>
|
|
@@ -156,7 +176,7 @@ const lede =
|
|
|
156
176
|
External sources cited inline via <code><Citation></code>, grouped
|
|
157
177
|
by tier in descending authority.
|
|
158
178
|
</p>
|
|
159
|
-
<SourceArchive />
|
|
179
|
+
<SourceArchive sources={bookConfig.corpus ? sourcesData : undefined} />
|
|
160
180
|
</section>
|
|
161
181
|
)}
|
|
162
182
|
</article>
|
package/pages/search.astro
CHANGED
|
@@ -12,15 +12,24 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import Base from '../layouts/Base.astro';
|
|
14
14
|
import { normalizeBase } from '../src/lib/nav-href';
|
|
15
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
15
16
|
|
|
16
17
|
// #142: Pagefind writes its bundle to dist/pagefind/, served under the deploy
|
|
17
18
|
// base — so the asset URLs must prefix BASE_URL or they 404 under a non-root base.
|
|
18
19
|
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
20
|
+
const corpusBooks = bookConfig.corpus?.books ?? [];
|
|
21
|
+
const corpusBookIds = corpusBooks.map((book) => book.id);
|
|
22
|
+
const searchOwner = bookConfig.title ?? (bookConfig.corpus ? 'Book corpus' : 'book-scaffold-astro');
|
|
23
|
+
const searchDescription = bookConfig.corpus
|
|
24
|
+
? 'Full-text search across the books in this corpus. All queries run in the browser.'
|
|
25
|
+
: 'Full-text search across the book. Indexed via Pagefind at build time; all queries run in the browser.';
|
|
19
26
|
---
|
|
20
27
|
|
|
21
28
|
<Base
|
|
22
|
-
title=
|
|
23
|
-
description=
|
|
29
|
+
title={`Search · ${searchOwner}`}
|
|
30
|
+
description={searchDescription}
|
|
31
|
+
showSidebar={!bookConfig.corpus}
|
|
32
|
+
pagefindSurface={bookConfig.corpus ? 'corpus' : undefined}
|
|
24
33
|
>
|
|
25
34
|
<link rel="stylesheet" href={`${baseUrl}pagefind/pagefind-ui.css`} slot="head" />
|
|
26
35
|
|
|
@@ -31,20 +40,55 @@ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
|
31
40
|
build time and is less than 100 KB total — queries never leave
|
|
32
41
|
the page.
|
|
33
42
|
</p>
|
|
43
|
+
{bookConfig.corpus && (
|
|
44
|
+
<label class="search-book-filter" for="search-book">
|
|
45
|
+
<span>Book</span>
|
|
46
|
+
<select id="search-book" name="book">
|
|
47
|
+
<option value="">All books</option>
|
|
48
|
+
{corpusBooks.map((book) => <option value={book.id}>{book.title}</option>)}
|
|
49
|
+
</select>
|
|
50
|
+
</label>
|
|
51
|
+
)}
|
|
34
52
|
<div id="search"></div>
|
|
35
53
|
</div>
|
|
36
54
|
|
|
37
55
|
<script src={`${baseUrl}pagefind/pagefind-ui.js`} is:inline></script>
|
|
38
|
-
<script is:inline>
|
|
56
|
+
<script is:inline define:vars={{ corpusBookIds }}>
|
|
39
57
|
// Initialize after the Pagefind bundle has loaded.
|
|
40
58
|
window.addEventListener('DOMContentLoaded', () => {
|
|
41
59
|
if (typeof window.PagefindUI === 'function') {
|
|
42
|
-
new window.
|
|
60
|
+
var params = new URLSearchParams(window.location.search);
|
|
61
|
+
var requestedBook = params.get('book');
|
|
62
|
+
var selectedBook = requestedBook && corpusBookIds.indexOf(requestedBook) !== -1
|
|
63
|
+
? requestedBook
|
|
64
|
+
: '';
|
|
65
|
+
if (requestedBook && !selectedBook) {
|
|
66
|
+
params.delete('book');
|
|
67
|
+
var canonicalQuery = params.toString();
|
|
68
|
+
history.replaceState(null, '', location.pathname + (canonicalQuery ? '?' + canonicalQuery : '') + location.hash);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
var ui = new window.PagefindUI({
|
|
43
72
|
element: '#search',
|
|
44
73
|
showImages: false,
|
|
45
74
|
showSubResults: true,
|
|
46
75
|
resetStyles: false,
|
|
47
76
|
});
|
|
77
|
+
if (selectedBook) ui.triggerFilters({ book: [selectedBook] });
|
|
78
|
+
|
|
79
|
+
var selector = document.getElementById('search-book');
|
|
80
|
+
if (selector) {
|
|
81
|
+
selector.value = selectedBook;
|
|
82
|
+
selector.addEventListener('change', function () {
|
|
83
|
+
selectedBook = selector.value;
|
|
84
|
+
var next = new URLSearchParams(window.location.search);
|
|
85
|
+
if (selectedBook) next.set('book', selectedBook);
|
|
86
|
+
else next.delete('book');
|
|
87
|
+
var query = next.toString();
|
|
88
|
+
history.replaceState(null, '', location.pathname + (query ? '?' + query : '') + location.hash);
|
|
89
|
+
ui.triggerFilters(selectedBook ? { book: [selectedBook] } : {});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
48
92
|
} else {
|
|
49
93
|
document.getElementById('search').innerHTML =
|
|
50
94
|
'<p><em>Search index not found. Run <code>npm run build</code> first; dev mode does not include the index.</em></p>';
|
|
@@ -57,6 +101,23 @@ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
|
57
101
|
.pagefind-ui__form {
|
|
58
102
|
margin-top: var(--space-4);
|
|
59
103
|
}
|
|
104
|
+
.search-book-filter {
|
|
105
|
+
display: grid;
|
|
106
|
+
gap: var(--space-1);
|
|
107
|
+
max-width: 24rem;
|
|
108
|
+
margin: var(--space-5) 0 var(--space-3);
|
|
109
|
+
font-family: var(--font-sans);
|
|
110
|
+
font-size: var(--text-sm);
|
|
111
|
+
font-weight: 600;
|
|
112
|
+
}
|
|
113
|
+
.search-book-filter select {
|
|
114
|
+
padding: var(--space-2) var(--space-3);
|
|
115
|
+
border: 1px solid var(--color-border);
|
|
116
|
+
border-radius: var(--radius-md);
|
|
117
|
+
background: var(--color-bg-subtle);
|
|
118
|
+
color: var(--color-text);
|
|
119
|
+
font: inherit;
|
|
120
|
+
}
|
|
60
121
|
.pagefind-ui__search-input {
|
|
61
122
|
font-family: var(--font-body) !important;
|
|
62
123
|
font-size: var(--text-base) !important;
|
package/pages/tips.astro
CHANGED
|
@@ -11,18 +11,31 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import Base from '../layouts/Base.astro';
|
|
13
13
|
import { normalizeBase } from '../src/lib/nav-href';
|
|
14
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
15
|
+
import { selectBookArtifact } from '../src/lib/corpus';
|
|
16
|
+
|
|
17
|
+
interface Props { bookId?: string; }
|
|
18
|
+
const { bookId } = Astro.props;
|
|
14
19
|
|
|
15
20
|
// v4.4.0 fix: use Vite's import.meta.glob with a project-root-relative
|
|
16
21
|
// path. The previous `../../../src/data/tips.json` resolution failed when
|
|
17
22
|
// the auto-injected route was processed from node_modules (or any context
|
|
18
23
|
// other than a workspace symlink). `/src/data/...` is consumer-project-rooted
|
|
19
24
|
// in Vite's resolver and works across all consumer build contexts.
|
|
20
|
-
|
|
25
|
+
type TipIndexEntry = { n: number; title: string; chapter: string; preview: string };
|
|
26
|
+
const tipsModules = import.meta.glob<{ default: unknown }>(
|
|
21
27
|
'/src/data/tips.json',
|
|
22
28
|
{ eager: true },
|
|
23
29
|
);
|
|
24
30
|
const tipsEntry = tipsModules['/src/data/tips.json'];
|
|
25
|
-
const tips = tipsEntry
|
|
31
|
+
const tips = tipsEntry
|
|
32
|
+
? selectBookArtifact<TipIndexEntry[]>(
|
|
33
|
+
tipsEntry.default,
|
|
34
|
+
bookConfig.corpus,
|
|
35
|
+
bookId,
|
|
36
|
+
'src/data/tips.json',
|
|
37
|
+
)
|
|
38
|
+
: [];
|
|
26
39
|
const loadError = tipsEntry
|
|
27
40
|
? null
|
|
28
41
|
: 'src/data/tips.json not found — run `npx book-scaffold build-tips` to generate.';
|
|
@@ -30,7 +43,7 @@ const loadError = tipsEntry
|
|
|
30
43
|
// #142: chapter back-links must prefix the deploy base.
|
|
31
44
|
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
32
45
|
---
|
|
33
|
-
<Base title="Tips" description="Numbered tips from this book, drawn from <Tip> instances in chapters.">
|
|
46
|
+
<Base title="Tips" description="Numbered tips from this book, drawn from <Tip> instances in chapters." bookId={bookId}>
|
|
34
47
|
<article class="prose">
|
|
35
48
|
<h1>Tips</h1>
|
|
36
49
|
{loadError && (
|
|
@@ -51,7 +64,7 @@ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
|
51
64
|
<p class="tips-index-preview">{tip.preview}</p>
|
|
52
65
|
)}
|
|
53
66
|
<p class="tips-index-meta">
|
|
54
|
-
From <a href={`${baseUrl}chapters/${tip.chapter}/`}>{tip.chapter}</a>
|
|
67
|
+
From <a href={`${baseUrl}chapters/${bookId ? `${bookId}/` : ''}${tip.chapter}/`}>{tip.chapter}</a>
|
|
55
68
|
</p>
|
|
56
69
|
</li>
|
|
57
70
|
))}
|