@brandon_m_behring/book-scaffold-astro 4.8.0 → 4.10.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 +4 -1
- package/bin/book-scaffold.mjs +1 -1
- package/components/SourceArchive.astro +7 -7
- package/components/XRef.astro +27 -24
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +6 -0
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +6 -0
- package/package.json +13 -6
- package/pages/references.astro +104 -53
- package/scripts/build-bib.mjs +53 -4
- package/scripts/build-labels.mjs +8 -5
- package/src/schemas.ts +3 -0
package/CLAUDE.md
CHANGED
|
@@ -25,6 +25,8 @@ When in doubt, run `grep BOOK_PROFILE .env astro.config.mjs src/content.config.t
|
|
|
25
25
|
|
|
26
26
|
## Frontmatter schemas
|
|
27
27
|
|
|
28
|
+
**Universal field (v4.9.0):** every profile accepts an optional `slug:` string that overrides the URL. A file `99-appendix.mdx` with `slug: appendix` is served at `/chapters/appendix/` — Astro's glob loader maps frontmatter `slug` → `entry.id`, and cross-references (`<XRef>`, via `build-labels`) resolve to the same path. Omit it and the URL falls back to the filename. Use it to keep numbered filenames for ordering while publishing clean URLs.
|
|
29
|
+
|
|
28
30
|
### Academic profile (`src/content.config.ts:academicChapterSchema`)
|
|
29
31
|
|
|
30
32
|
```yaml
|
|
@@ -34,6 +36,7 @@ part: foundations # required: foundations|ssm-core|beyond-ssm|integration
|
|
|
34
36
|
title: "..." # string, required
|
|
35
37
|
status: implemented # required: implemented|chapter_only|prose_only|code_only|reading_only|scaffolded|planned
|
|
36
38
|
# optional:
|
|
39
|
+
slug: ch01-introduction # clean URL override; else filename → /chapters/<slug>/
|
|
37
40
|
roadmap_lines: [10, 42] # [start, end] line refs into roadmap.md
|
|
38
41
|
code_path: experiments/jax/week01/foo.py
|
|
39
42
|
tests_path: experiments/jax/week01/test_foo.py
|
|
@@ -54,7 +57,7 @@ volatility: architectural-pattern # required: stable-principle|architectural-pa
|
|
|
54
57
|
tools_compared: [claude-code] # required, ≥1 of: claude-code|gemini-cli|codex-cli|cross-tool
|
|
55
58
|
last_verified: 2026-05-18 # date, required
|
|
56
59
|
sources: [] # array of source-manifest keys
|
|
57
|
-
# optional: description, draft, updated
|
|
60
|
+
# optional: slug (clean URL override), description, draft, updated
|
|
58
61
|
---
|
|
59
62
|
```
|
|
60
63
|
|
package/bin/book-scaffold.mjs
CHANGED
|
@@ -27,7 +27,7 @@ const HELP = `Usage: book-scaffold <sub-command> [args...]
|
|
|
27
27
|
Sub-commands:
|
|
28
28
|
validate Pre-flight content validator (XRef ids, Cite keys, Figure srcs).
|
|
29
29
|
build-labels Emit src/data/labels.json for cross-references (Phase C).
|
|
30
|
-
build-bib BibTeX ->
|
|
30
|
+
build-bib BibTeX -> references.json (+ sources/manifest.yaml -> sources.json).
|
|
31
31
|
build-figures PDF -> SVG via pdftocairo / pdftoppm fallback (+ TikZ in v4.2.0).
|
|
32
32
|
build-tips Scan chapters for <Tip> instances; emit src/data/tips.json (v4.3.0).
|
|
33
33
|
build-exercises Scan chapters for <Exercise> instances; emit src/data/exercises.json (v4.4.0).
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
* SourceArchive — renders every entry in sources/manifest.yaml grouped
|
|
4
4
|
* by tier in descending authority (T1 → T4).
|
|
5
5
|
*
|
|
6
|
-
* Used
|
|
7
|
-
*
|
|
8
|
-
* always reflects
|
|
6
|
+
* Used by the auto-injected /references page (v4.10.0, #85) and by
|
|
7
|
+
* author-placed source-archive appendices, replacing a static hand-
|
|
8
|
+
* maintained listing with an auto-generated view that always reflects
|
|
9
|
+
* the manifest.
|
|
9
10
|
*
|
|
10
11
|
* Empty-tier behavior: renders an honest placeholder ("No sources at
|
|
11
|
-
* this tier yet.") rather than hiding the section
|
|
12
|
-
*
|
|
13
|
-
* early book, and the gap is visible by design.
|
|
12
|
+
* this tier yet.") rather than hiding the section, so the tier taxonomy
|
|
13
|
+
* stays visible even before a tier has entries.
|
|
14
14
|
*/
|
|
15
15
|
import { sourceTiers } from '@brandon_m_behring/book-scaffold-astro';
|
|
16
16
|
import {
|
|
@@ -53,7 +53,7 @@ function year(d: Date | undefined): string | null {
|
|
|
53
53
|
|
|
54
54
|
{empty ? (
|
|
55
55
|
<p class="source-archive-empty">
|
|
56
|
-
No sources at this tier yet.
|
|
56
|
+
No sources at this tier yet.
|
|
57
57
|
</p>
|
|
58
58
|
) : (
|
|
59
59
|
<ol
|
package/components/XRef.astro
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
2
|
+
// XRef — resolves a `\cref{label}` LaTeX reference to a hyperlink.
|
|
3
|
+
//
|
|
4
|
+
// Reads src/data/labels.json built by scripts/build-labels.mjs
|
|
5
|
+
// (Phase 2.6). For each known id, the map provides:
|
|
6
|
+
// { href: "/chapters/week04#thm-w4-stability", display: "Theorem 4.2" }
|
|
7
|
+
//
|
|
8
|
+
// Runtime: renders `<a href>` for known ids; renders an inline `[?id]`
|
|
9
|
+
// placeholder for unknown ids so the Astro dev server stays running while
|
|
10
|
+
// chapters are being authored or labels are being added.
|
|
11
|
+
//
|
|
12
|
+
// CI: `book-scaffold validate` (Phase 2.6, shipped) catches unknown ids
|
|
13
|
+
// and **fails the build with a non-zero exit code** before unresolved
|
|
14
|
+
// placeholders can reach production. The placeholder is a dev-ergonomic
|
|
15
|
+
// affordance, not a soft-degradation path on the deploy critical line.
|
|
16
|
+
//
|
|
17
|
+
// Bootstrapping note: when porting a book chapter-by-chapter, early
|
|
18
|
+
// chapters that reference yet-to-be-ported targets need either plain-prose
|
|
19
|
+
// substitutes or a temporarily-commented `{/* <XRef …/> */}` until the
|
|
20
|
+
// target chapter (and its `id="…"` attributes) exists. MDX uses JSX-style
|
|
21
|
+
// expression comments — the HTML `<` + bang + `--` form is NOT valid MDX.
|
|
22
|
+
//
|
|
23
|
+
// NB: these are `//` line comments, not a `/** */` block, on purpose — the
|
|
24
|
+
// literal `*/` in the example above would otherwise close a block comment
|
|
25
|
+
// early and break esbuild on every MDX import (the v4.9.0 fix).
|
|
26
|
+
//
|
|
27
|
+
// Usage:
|
|
28
|
+
// By <XRef id="thm:w4:stability" />, the discretized eigenvalues …
|
|
26
29
|
type LabelEntry = { href: string; display: string };
|
|
27
30
|
|
|
28
31
|
// Resolve labels.json from the consumer's project root (Vite resolves `/`
|
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, Y as volatilityLevels, h as ChaptersRenderer, o 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, m as Provenance, R as ResearchPortfolioChapter, n as RouteToggles, S as StatusBadge, p as StyleInput, T as ToolsChapter, V as VolatilityBadge, q as academicChapterSchema, r as academicParts, s as changeKinds, t as changelogSchema, u as chapterStatus, v as citationBackstops, w as composeStyles, x as courseNotesChapterSchema, y as defineProfile, z as defineStyle, D as minimalChapterSchema, E as normalizeFrontmatterConfig, G as patternCategories, H as patternsSchema, I as provenanceObject, J as provenanceSchema, K as researchPortfolioChapterSchema, L as resolvePreset, N as resolveProfile, O as sourceTiers, Q as sourceTiersResearch, U as sourcesSchema, W as toolSlugs, X as toolsChapterSchema } from './types-
|
|
2
|
+
import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, Y as volatilityLevels, h as ChaptersRenderer, o as Style } from './types-CULHImU4.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, m as Provenance, R as ResearchPortfolioChapter, n as RouteToggles, S as StatusBadge, p as StyleInput, T as ToolsChapter, V as VolatilityBadge, q as academicChapterSchema, r as academicParts, s as changeKinds, t as changelogSchema, u as chapterStatus, v as citationBackstops, w as composeStyles, x as courseNotesChapterSchema, y as defineProfile, z as defineStyle, D as minimalChapterSchema, E as normalizeFrontmatterConfig, G as patternCategories, H as patternsSchema, I as provenanceObject, J as provenanceSchema, K as researchPortfolioChapterSchema, L as resolvePreset, N as resolveProfile, O as sourceTiers, Q as sourceTiersResearch, U as sourcesSchema, W as toolSlugs, X as toolsChapterSchema } from './types-CULHImU4.js';
|
|
4
4
|
import 'astro/zod';
|
|
5
5
|
|
|
6
6
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -184,6 +184,8 @@ var academicChapterSchema = z.object({
|
|
|
184
184
|
week: z.number().int().min(1).max(99),
|
|
185
185
|
part: z.enum(academicParts),
|
|
186
186
|
title: z.string().min(1),
|
|
187
|
+
slug: z.string().optional(),
|
|
188
|
+
// v4.9.0: explicit URL slug override (else filename → entry.id)
|
|
187
189
|
status: z.enum(chapterStatus),
|
|
188
190
|
roadmap_lines: z.tuple([z.number().int(), z.number().int()]).optional(),
|
|
189
191
|
code_path: z.string().optional(),
|
|
@@ -203,6 +205,8 @@ var academicChapterSchema = z.object({
|
|
|
203
205
|
});
|
|
204
206
|
var toolsChapterSchema = z.object({
|
|
205
207
|
title: z.string().min(1),
|
|
208
|
+
slug: z.string().optional(),
|
|
209
|
+
// v4.9.0: explicit URL slug override (else filename → entry.id)
|
|
206
210
|
part: z.number().int().min(0).max(10),
|
|
207
211
|
chapter: z.number().int().min(0).max(99),
|
|
208
212
|
volatility: z.enum(volatilityLevels),
|
|
@@ -226,6 +230,8 @@ var sourceTiersResearch = ["T1", "T2", "T3", "T4"];
|
|
|
226
230
|
var courseNotesChapterSchema = z.object({
|
|
227
231
|
// Identity
|
|
228
232
|
title: z.string().min(1),
|
|
233
|
+
slug: z.string().optional(),
|
|
234
|
+
// v4.9.0: explicit URL slug override (else filename → entry.id)
|
|
229
235
|
chapter: z.number().int().min(0).max(99),
|
|
230
236
|
part: z.number().int().min(0).max(20).default(1),
|
|
231
237
|
description: z.string().optional(),
|
package/dist/schemas.d.ts
CHANGED
package/dist/schemas.mjs
CHANGED
|
@@ -68,6 +68,8 @@ var academicChapterSchema = z.object({
|
|
|
68
68
|
week: z.number().int().min(1).max(99),
|
|
69
69
|
part: z.enum(academicParts),
|
|
70
70
|
title: z.string().min(1),
|
|
71
|
+
slug: z.string().optional(),
|
|
72
|
+
// v4.9.0: explicit URL slug override (else filename → entry.id)
|
|
71
73
|
status: z.enum(chapterStatus),
|
|
72
74
|
roadmap_lines: z.tuple([z.number().int(), z.number().int()]).optional(),
|
|
73
75
|
code_path: z.string().optional(),
|
|
@@ -87,6 +89,8 @@ var academicChapterSchema = z.object({
|
|
|
87
89
|
});
|
|
88
90
|
var toolsChapterSchema = z.object({
|
|
89
91
|
title: z.string().min(1),
|
|
92
|
+
slug: z.string().optional(),
|
|
93
|
+
// v4.9.0: explicit URL slug override (else filename → entry.id)
|
|
90
94
|
part: z.number().int().min(0).max(10),
|
|
91
95
|
chapter: z.number().int().min(0).max(99),
|
|
92
96
|
volatility: z.enum(volatilityLevels),
|
|
@@ -110,6 +114,8 @@ var sourceTiersResearch = ["T1", "T2", "T3", "T4"];
|
|
|
110
114
|
var courseNotesChapterSchema = z.object({
|
|
111
115
|
// Identity
|
|
112
116
|
title: z.string().min(1),
|
|
117
|
+
slug: z.string().optional(),
|
|
118
|
+
// v4.9.0: explicit URL slug override (else filename → entry.id)
|
|
113
119
|
chapter: z.number().int().min(0).max(99),
|
|
114
120
|
part: z.number().int().min(0).max(20).default(1),
|
|
115
121
|
description: z.string().optional(),
|
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.10.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
|
@@ -142,15 +142,21 @@
|
|
|
142
142
|
"test": "node --test tests/*.test.mjs"
|
|
143
143
|
},
|
|
144
144
|
"peerDependencies": {
|
|
145
|
-
"astro": "^6.1.7",
|
|
146
145
|
"@astrojs/mdx": "^5.0.3",
|
|
147
146
|
"@astrojs/preact": "^5.1.1",
|
|
147
|
+
"astro": "^6.1.7",
|
|
148
148
|
"preact": "^10.29.1"
|
|
149
149
|
},
|
|
150
150
|
"peerDependenciesMeta": {
|
|
151
|
-
"katex": {
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
"katex": {
|
|
152
|
+
"optional": true
|
|
153
|
+
},
|
|
154
|
+
"rehype-katex": {
|
|
155
|
+
"optional": true
|
|
156
|
+
},
|
|
157
|
+
"remark-math": {
|
|
158
|
+
"optional": true
|
|
159
|
+
}
|
|
154
160
|
},
|
|
155
161
|
"dependencies": {
|
|
156
162
|
"@astrojs/sitemap": "^3.6.1",
|
|
@@ -158,7 +164,8 @@
|
|
|
158
164
|
"@citation-js/plugin-bibtex": "^0.7.21",
|
|
159
165
|
"@fontsource-variable/roboto": "^5.2.10",
|
|
160
166
|
"@fontsource-variable/source-code-pro": "^5.2.7",
|
|
161
|
-
"pagefind": "^1.5.2"
|
|
167
|
+
"pagefind": "^1.5.2",
|
|
168
|
+
"yaml": "^2.9.0"
|
|
162
169
|
},
|
|
163
170
|
"devDependencies": {
|
|
164
171
|
"@types/node": "^22.10.0",
|
package/pages/references.astro
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
|
-
* /references — the book's bibliography page.
|
|
3
|
+
* /references — the book's bibliography + cited-sources page.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* elsewhere on the site links to /references#gu2024mamba.
|
|
5
|
+
* Auto-injected on every profile (integration route table). Reads two
|
|
6
|
+
* independent inputs, each via a defensive `import.meta.glob` (missing file →
|
|
7
|
+
* empty, never a crash — same pattern <XRef> uses for labels.json):
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* - src/data/references.json — BibTeX entries (academic), from build-bib.
|
|
10
|
+
* Each entry's anchor matches its bibkey, so `<Cite key="…" />` links to
|
|
11
|
+
* /references#<bibkey>.
|
|
12
|
+
* - src/data/sources.json — sources/manifest.yaml entries (tools profile),
|
|
13
|
+
* also from build-bib (v4.10.0, #85). Its presence is a profile-safe GATE
|
|
14
|
+
* for the <SourceArchive> render: a falsy `&&` never instantiates the
|
|
15
|
+
* component, so academic/minimal books (which have no `sources` content
|
|
16
|
+
* collection) never call getCollection('sources').
|
|
12
17
|
*/
|
|
13
18
|
import Base from '../layouts/Base.astro';
|
|
19
|
+
import SourceArchive from '../components/SourceArchive.astro';
|
|
14
20
|
|
|
15
21
|
type CslAuthor = { family?: string; given?: string; literal?: string };
|
|
16
22
|
type CslEntry = {
|
|
@@ -29,8 +35,7 @@ type CslEntry = {
|
|
|
29
35
|
note?: string;
|
|
30
36
|
};
|
|
31
37
|
|
|
32
|
-
//
|
|
33
|
-
// list (page renders an "empty bibliography" notice rather than crashing).
|
|
38
|
+
// --- Bibliography: BibTeX → references.json (academic profile). ---
|
|
34
39
|
const refsModules = import.meta.glob<{ default: Record<string, CslEntry> }>(
|
|
35
40
|
'/src/data/references.json',
|
|
36
41
|
{ eager: true },
|
|
@@ -39,6 +44,18 @@ const refsModule = refsModules['/src/data/references.json'];
|
|
|
39
44
|
const map = (refsModule?.default ?? {}) as Record<string, CslEntry>;
|
|
40
45
|
const entries = Object.values(map);
|
|
41
46
|
|
|
47
|
+
// --- Cited sources: sources/manifest.yaml → sources.json (tools profile).
|
|
48
|
+
// Presence-only gate; the actual render reads the live `sources` collection
|
|
49
|
+
// via <SourceArchive>, which only runs when this gate is true (so it is never
|
|
50
|
+
// reached on profiles without a `sources` collection). ---
|
|
51
|
+
const srcModules = import.meta.glob<{ default: unknown[] }>(
|
|
52
|
+
'/src/data/sources.json',
|
|
53
|
+
{ eager: true },
|
|
54
|
+
);
|
|
55
|
+
const sourcesData = (srcModules['/src/data/sources.json']?.default ?? []) as unknown[];
|
|
56
|
+
const hasSources = Array.isArray(sourcesData) && sourcesData.length > 0;
|
|
57
|
+
const hasBib = entries.length > 0;
|
|
58
|
+
|
|
42
59
|
const surname = (a: CslAuthor): string =>
|
|
43
60
|
(a.family ?? a.literal ?? '').toLowerCase();
|
|
44
61
|
|
|
@@ -73,61 +90,95 @@ function arxivUrl(note?: string): string | null {
|
|
|
73
90
|
const m = note.match(/arXiv:\s*(\S+)/i);
|
|
74
91
|
return m ? `https://arxiv.org/abs/${m[1]}` : null;
|
|
75
92
|
}
|
|
93
|
+
|
|
94
|
+
const lede =
|
|
95
|
+
hasBib && hasSources
|
|
96
|
+
? 'Every work cited in this book — the bibliography below, then the external sources grouped by tier.'
|
|
97
|
+
: hasBib
|
|
98
|
+
? 'Every paper, book, and software citation in this book, sorted alphabetically by first-author surname. Click an entry’s anchor to share a deep link, or follow the arXiv / DOI / URL for the source.'
|
|
99
|
+
: hasSources
|
|
100
|
+
? 'Every external source cited in this book, grouped by tier in descending authority.'
|
|
101
|
+
: 'This book has no references yet.';
|
|
76
102
|
---
|
|
77
|
-
<Base
|
|
78
|
-
title="References — Post-Transformers"
|
|
79
|
-
description="Bibliography for the post_transformers guide, generated from guides/shared/references.bib."
|
|
80
|
-
>
|
|
103
|
+
<Base title="References" description="Bibliography and cited sources for this book.">
|
|
81
104
|
<article class="prose">
|
|
82
105
|
<header>
|
|
83
106
|
<h1>References</h1>
|
|
84
|
-
<p class="lede">
|
|
85
|
-
Every paper, book, and software citation in this guide, sorted
|
|
86
|
-
alphabetically by first-author surname. Click an entry's
|
|
87
|
-
anchor to share a deep link, or follow the arXiv / DOI / URL
|
|
88
|
-
for the source.
|
|
89
|
-
</p>
|
|
90
|
-
<p>
|
|
91
|
-
<small>
|
|
92
|
-
{entries.length} entries. Generated from
|
|
93
|
-
<code>guides/shared/references.bib</code> at build time via
|
|
94
|
-
<code>scripts/build-bib.mjs</code>.
|
|
95
|
-
</small>
|
|
96
|
-
</p>
|
|
107
|
+
<p class="lede">{lede}</p>
|
|
97
108
|
</header>
|
|
98
109
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
{
|
|
117
|
-
|
|
110
|
+
{!hasBib && !hasSources && (
|
|
111
|
+
<p class="references-empty">
|
|
112
|
+
No references yet. Add a <code>bibliography.bib</code> (academic) or
|
|
113
|
+
populate <code>sources/manifest.yaml</code> (tools), then run
|
|
114
|
+
<code>npm run build:bib</code>.
|
|
115
|
+
</p>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{hasBib && (
|
|
119
|
+
<section class="references-bibliography">
|
|
120
|
+
{hasSources && <h2>Bibliography</h2>}
|
|
121
|
+
<ol class="references-list">
|
|
122
|
+
{entries.map((e) => {
|
|
123
|
+
const y = year(e);
|
|
124
|
+
const arxiv = arxivUrl(e.note);
|
|
125
|
+
const primaryUrl = arxiv ?? e.URL ?? (e.DOI ? `https://doi.org/${e.DOI}` : null);
|
|
126
|
+
return (
|
|
127
|
+
<li id={e.id} class="reference-entry">
|
|
128
|
+
<span class="reference-key" aria-label="bibkey">[{e.id}]</span>
|
|
129
|
+
<span class="reference-text">
|
|
130
|
+
{formatAuthors(e.author)}
|
|
131
|
+
{y > 0 && <> ({y})</>}.
|
|
118
132
|
{' '}
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
133
|
+
<em>{e.title}</em>.
|
|
134
|
+
{e['container-title'] && <> {e['container-title']}.</>}
|
|
135
|
+
{e.publisher && !e['container-title'] && <> {e.publisher}.</>}
|
|
136
|
+
{e.volume && <> Vol. {e.volume}{e.issue && <>, no. {e.issue}</>}.</>}
|
|
137
|
+
{e.page && <> pp. {e.page}.</>}
|
|
138
|
+
{primaryUrl && (
|
|
139
|
+
<>
|
|
140
|
+
{' '}
|
|
141
|
+
<a href={primaryUrl} rel="external noopener">link</a>.
|
|
142
|
+
</>
|
|
143
|
+
)}
|
|
144
|
+
</span>
|
|
145
|
+
</li>
|
|
146
|
+
);
|
|
147
|
+
})}
|
|
148
|
+
</ol>
|
|
149
|
+
</section>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
{hasSources && (
|
|
153
|
+
<section class="references-sources">
|
|
154
|
+
<h2>Cited sources</h2>
|
|
155
|
+
<p class="references-sources-lede">
|
|
156
|
+
External sources cited inline via <code><Citation></code>, grouped
|
|
157
|
+
by tier in descending authority.
|
|
158
|
+
</p>
|
|
159
|
+
<SourceArchive />
|
|
160
|
+
</section>
|
|
161
|
+
)}
|
|
127
162
|
</article>
|
|
128
163
|
</Base>
|
|
129
164
|
|
|
130
165
|
<style>
|
|
166
|
+
.references-empty {
|
|
167
|
+
color: var(--color-text-muted);
|
|
168
|
+
font-style: italic;
|
|
169
|
+
padding: var(--space-3);
|
|
170
|
+
background: var(--color-bg-subtle);
|
|
171
|
+
border-radius: var(--radius-sm);
|
|
172
|
+
text-indent: 0;
|
|
173
|
+
}
|
|
174
|
+
.references-sources {
|
|
175
|
+
margin-top: var(--space-6);
|
|
176
|
+
}
|
|
177
|
+
.references-sources-lede {
|
|
178
|
+
color: var(--color-text-muted);
|
|
179
|
+
font-size: var(--text-sm);
|
|
180
|
+
text-indent: 0;
|
|
181
|
+
}
|
|
131
182
|
.references-list {
|
|
132
183
|
list-style: none;
|
|
133
184
|
padding: 0;
|
package/scripts/build-bib.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* scripts/build-bib.mjs — Bibliography
|
|
3
|
+
* scripts/build-bib.mjs — Bibliography + source-manifest pipeline.
|
|
4
4
|
*
|
|
5
5
|
* Reads bibliography.bib at scaffold root (BibTeX), parses via @citation-js,
|
|
6
6
|
* emits src/data/references.json keyed by bibkey. The .bib path is
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* elsewhere (e.g. a shared `guides/shared/references.bib` outside the
|
|
9
9
|
* Astro project — the post_transformers pattern).
|
|
10
10
|
*
|
|
11
|
+
* v4.10.0 (#85): ALSO reads sources/manifest.yaml (tools-profile sources,
|
|
12
|
+
* cited inline via <Citation src>) and emits src/data/sources.json so the
|
|
13
|
+
* auto-injected /references page can surface them. The two steps are
|
|
14
|
+
* independent — a tools book with a manifest and no .bib still gets a
|
|
15
|
+
* populated /references.
|
|
16
|
+
*
|
|
11
17
|
* Run on `prebuild` so every Astro build sees fresh bibliography data.
|
|
12
18
|
* Idempotent: re-running with no .bib change produces a byte-identical
|
|
13
19
|
* output (modulo timestamp, which we omit).
|
|
@@ -37,8 +43,10 @@ import { fileURLToPath } from 'node:url';
|
|
|
37
43
|
// --help / -h: non-mutating (closes #14).
|
|
38
44
|
const USAGE = `Usage: book-scaffold build-bib
|
|
39
45
|
|
|
40
|
-
Bibliography
|
|
41
|
-
BOOK_BIB_PATH if set)
|
|
46
|
+
Bibliography + source-manifest pipeline. Reads bibliography.bib (or
|
|
47
|
+
BOOK_BIB_PATH if set) -> src/data/references.json (BibTeX via @citation-js),
|
|
48
|
+
AND sources/manifest.yaml -> src/data/sources.json (tools-profile sources for
|
|
49
|
+
the /references page). Either input may be absent.
|
|
42
50
|
|
|
43
51
|
Env:
|
|
44
52
|
BOOK_BIB_PATH Override path to .bib file (default: ./bibliography.bib).
|
|
@@ -63,8 +71,10 @@ const BIB_PATH = process.env.BOOK_BIB_PATH
|
|
|
63
71
|
? resolve(process.cwd(), process.env.BOOK_BIB_PATH)
|
|
64
72
|
: resolve(PROJECT_ROOT, 'bibliography.bib');
|
|
65
73
|
const OUT_PATH = resolve(PROJECT_ROOT, 'src/data/references.json');
|
|
74
|
+
const SOURCES_PATH = resolve(PROJECT_ROOT, 'sources/manifest.yaml');
|
|
75
|
+
const SOURCES_OUT = resolve(PROJECT_ROOT, 'src/data/sources.json');
|
|
66
76
|
|
|
67
|
-
async function
|
|
77
|
+
async function buildReferences() {
|
|
68
78
|
// Graceful skip when the .bib file is absent (minimal/tools profile, or
|
|
69
79
|
// an academic book that hasn't authored citations yet). Emits an empty
|
|
70
80
|
// references.json so consumers can still `import refs from '...'`.
|
|
@@ -125,6 +135,45 @@ async function main() {
|
|
|
125
135
|
);
|
|
126
136
|
}
|
|
127
137
|
|
|
138
|
+
// v4.10.0 (closes #85): tools-profile books keep their sources in
|
|
139
|
+
// sources/manifest.yaml (cited inline via <Citation src="id" />); the BibTeX
|
|
140
|
+
// path above never sees them, so the auto-injected /references page rendered
|
|
141
|
+
// blank. Emit those sources to src/data/sources.json so references.astro can
|
|
142
|
+
// surface them via the same defensive import.meta.glob it uses for
|
|
143
|
+
// references.json. Absent manifest -> no file written (academic/minimal books
|
|
144
|
+
// degrade to empty, exactly like a missing .bib). YAML is lazy-imported so the
|
|
145
|
+
// --help / no-manifest paths stay dependency-free.
|
|
146
|
+
async function buildSources() {
|
|
147
|
+
let yamlText;
|
|
148
|
+
try {
|
|
149
|
+
yamlText = await readFile(SOURCES_PATH, 'utf8');
|
|
150
|
+
} catch (err) {
|
|
151
|
+
if (err.code === 'ENOENT') return; // no manifest — nothing to emit
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const { parse } = await import('yaml');
|
|
156
|
+
const parsed = parse(yamlText);
|
|
157
|
+
// The manifest is a YAML array of source objects. Keep only well-formed
|
|
158
|
+
// entries (a string `id` is the citation key + the /references anchor target).
|
|
159
|
+
// A blank or comments-only manifest parses to null/undefined/[].
|
|
160
|
+
const sources = Array.isArray(parsed)
|
|
161
|
+
? parsed.filter((s) => s && typeof s.id === 'string')
|
|
162
|
+
: [];
|
|
163
|
+
|
|
164
|
+
await mkdir(dirname(SOURCES_OUT), { recursive: true });
|
|
165
|
+
await writeFile(SOURCES_OUT, JSON.stringify(sources, null, 2) + '\n', 'utf8');
|
|
166
|
+
console.log(
|
|
167
|
+
`build-bib: ${sources.length} source${sources.length === 1 ? '' : 's'} -> ` +
|
|
168
|
+
`${SOURCES_OUT.replace(PROJECT_ROOT + '/', '')}`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function main() {
|
|
173
|
+
await buildReferences();
|
|
174
|
+
await buildSources();
|
|
175
|
+
}
|
|
176
|
+
|
|
128
177
|
main().catch((err) => {
|
|
129
178
|
console.error(`build-bib: failed`);
|
|
130
179
|
console.error(err);
|
package/scripts/build-labels.mjs
CHANGED
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
* - tools profile: `chapter` field (number).
|
|
16
16
|
* - academic profile: `week` field (number).
|
|
17
17
|
*
|
|
18
|
-
* Slug used for the href
|
|
19
|
-
* the consumer's pages
|
|
20
|
-
* using `[...slug].astro`
|
|
21
|
-
* filenames identically
|
|
18
|
+
* Slug used for the href: the chapter's frontmatter `slug:` if set,
|
|
19
|
+
* else filename minus `.mdx`. The href shape mirrors the consumer's pages
|
|
20
|
+
* router: `/chapters/<slug>#<id>`. Academic books using `[...slug].astro`
|
|
21
|
+
* get the same shape since Astro slugifies filenames identically when no
|
|
22
|
+
* frontmatter override is present.
|
|
22
23
|
*
|
|
23
24
|
* Optional override:
|
|
24
25
|
* <Theorem id="…" label="Custom display" />
|
|
@@ -178,7 +179,9 @@ async function main() {
|
|
|
178
179
|
const source = await readFile(file, 'utf8');
|
|
179
180
|
const fm = parseFrontmatter(source);
|
|
180
181
|
const chapterNum = chapterNumberOf(fm);
|
|
181
|
-
const slug =
|
|
182
|
+
const slug = (typeof fm.slug === 'string' && fm.slug.length > 0)
|
|
183
|
+
? fm.slug
|
|
184
|
+
: basename(file).replace(/\.mdx?$/, '');
|
|
182
185
|
|
|
183
186
|
// Per-chapter counters reset for each file.
|
|
184
187
|
const counters = {};
|
package/src/schemas.ts
CHANGED
|
@@ -116,6 +116,7 @@ export const academicChapterSchema = z.object({
|
|
|
116
116
|
week: z.number().int().min(1).max(99),
|
|
117
117
|
part: z.enum(academicParts),
|
|
118
118
|
title: z.string().min(1),
|
|
119
|
+
slug: z.string().optional(), // v4.9.0: explicit URL slug override (else filename → entry.id)
|
|
119
120
|
status: z.enum(chapterStatus),
|
|
120
121
|
roadmap_lines: z.tuple([z.number().int(), z.number().int()]).optional(),
|
|
121
122
|
code_path: z.string().optional(),
|
|
@@ -136,6 +137,7 @@ export const academicChapterSchema = z.object({
|
|
|
136
137
|
|
|
137
138
|
export const toolsChapterSchema = z.object({
|
|
138
139
|
title: z.string().min(1),
|
|
140
|
+
slug: z.string().optional(), // v4.9.0: explicit URL slug override (else filename → entry.id)
|
|
139
141
|
part: z.number().int().min(0).max(10),
|
|
140
142
|
chapter: z.number().int().min(0).max(99),
|
|
141
143
|
volatility: z.enum(volatilityLevels),
|
|
@@ -178,6 +180,7 @@ export const sourceTiersResearch = ['T1', 'T2', 'T3', 'T4'] as const;
|
|
|
178
180
|
export const courseNotesChapterSchema = z.object({
|
|
179
181
|
// Identity
|
|
180
182
|
title: z.string().min(1),
|
|
183
|
+
slug: z.string().optional(), // v4.9.0: explicit URL slug override (else filename → entry.id)
|
|
181
184
|
chapter: z.number().int().min(0).max(99),
|
|
182
185
|
part: z.number().int().min(0).max(20).default(1),
|
|
183
186
|
description: z.string().optional(),
|