@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.
- package/bin/book-scaffold.mjs +1 -1
- package/components/SourceArchive.astro +7 -7
- package/package.json +13 -6
- package/pages/references.astro +104 -53
- package/scripts/build-bib.mjs +53 -4
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/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);
|