@brandon_m_behring/book-scaffold-astro 4.9.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.
@@ -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 -> CSL JSON for the <Cite> component.
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 from Appendix D (`d-source-archive-index.mdx`) to replace the
7
- * static hand-maintained listing with an auto-generated view that
8
- * always reflects the manifest.
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. This matches the
12
- * pedagogy of Appendix D the archive is intentionally sparse in the
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. Appendix D is intentionally sparse in the early book; this slot fills as chapters leave draft.
56
+ No sources at this tier yet.
57
57
  </p>
58
58
  ) : (
59
59
  <ol
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.9.0",
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": { "optional": true },
152
- "rehype-katex": { "optional": true },
153
- "remark-math": { "optional": true }
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",
@@ -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
- * Renders every entry in src/data/references.json (produced by
6
- * scripts/build-bib.mjs from guides/shared/references.bib). Each
7
- * entry gets an anchor ID matching its bibkey, so `<Cite key="gu2024mamba" />`
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
- * Sorted alphabetically by first-author surname, then by year.
11
- * arXiv-style notes are surfaced as direct links when present.
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
- // Resolve references.json from consumer's project root. Missing file -> empty
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
- <ol class="references-list">
100
- {entries.map((e) => {
101
- const y = year(e);
102
- const arxiv = arxivUrl(e.note);
103
- const primaryUrl = arxiv ?? e.URL ?? (e.DOI ? `https://doi.org/${e.DOI}` : null);
104
- return (
105
- <li id={e.id} class="reference-entry">
106
- <span class="reference-key" aria-label="bibkey">[{e.id}]</span>
107
- <span class="reference-text">
108
- {formatAuthors(e.author)}
109
- {y > 0 && <> ({y})</>}.
110
- {' '}
111
- <em>{e.title}</em>.
112
- {e['container-title'] && <> {e['container-title']}.</>}
113
- {e.publisher && !e['container-title'] && <> {e.publisher}.</>}
114
- {e.volume && <> Vol. {e.volume}{e.issue && <>, no. {e.issue}</>}.</>}
115
- {e.page && <> pp. {e.page}.</>}
116
- {primaryUrl && (
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
- <a href={primaryUrl} rel="external noopener">link</a>.
120
- </>
121
- )}
122
- </span>
123
- </li>
124
- );
125
- })}
126
- </ol>
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>&lt;Citation&gt;</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;
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * scripts/build-bib.mjs — Bibliography pipeline (academic profile).
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 pipeline (academic profile). Reads bibliography.bib (or
41
- BOOK_BIB_PATH if set), parses via @citation-js, emits src/data/references.json.
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 main() {
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);