@brandon_m_behring/book-scaffold-astro 4.1.2 → 4.2.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/package.json +1 -1
- package/recipes/16-tikz-figures.md +102 -0
- package/scripts/build-figures.mjs +90 -1
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.2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Recipe 16 — TikZ figures (v4.2.0+)
|
|
2
|
+
|
|
3
|
+
`book-scaffold build-figures` (v4.2.0+) auto-compiles TikZ standalone `.tex` sources to PDF via `pdflatex`, then converts the PDF to SVG via the existing pdf2svg pipeline. Closes [#17](https://github.com/brandon-behring/book-scaffold-astro/issues/17).
|
|
4
|
+
|
|
5
|
+
## TL;DR
|
|
6
|
+
|
|
7
|
+
Drop `figures/<topic>/diagram.tex` (standalone TikZ source). Run `npm run build:figures` (or it's wired into `prebuild`). Get `public/figures/<topic>/diagram.svg` ready to reference in MDX as `<Figure src="/figures/<topic>/diagram.svg" />`.
|
|
8
|
+
|
|
9
|
+
## The TikZ source
|
|
10
|
+
|
|
11
|
+
Use the `standalone` document class; configure for SVG via `tikz` option. Recommended:
|
|
12
|
+
|
|
13
|
+
```latex
|
|
14
|
+
\documentclass[tikz,border=2mm]{standalone}
|
|
15
|
+
\usepackage{tikz}
|
|
16
|
+
\usetikzlibrary{positioning, shapes, arrows} % whichever libraries you need
|
|
17
|
+
\begin{document}
|
|
18
|
+
\begin{tikzpicture}
|
|
19
|
+
\node (a) at (0,0) {Hello};
|
|
20
|
+
\node (b) at (3,0) {World};
|
|
21
|
+
\draw[->] (a) -- (b);
|
|
22
|
+
\end{tikzpicture}
|
|
23
|
+
\end{document}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The `border=2mm` adds a small margin around the figure so it doesn't crop right at the edge.
|
|
27
|
+
|
|
28
|
+
## Discovery rule
|
|
29
|
+
|
|
30
|
+
`build-figures` walks `figures/` (or `BOOK_FIGURES_PATH`) for both `.pdf` and `.tex` files. For each `.tex` source:
|
|
31
|
+
|
|
32
|
+
- If no sibling `.pdf` exists → compile.
|
|
33
|
+
- If `.pdf` exists but `.tex` is newer → recompile.
|
|
34
|
+
- If `.pdf` is newer than (or equal in mtime to) `.tex` → skip (use the existing PDF).
|
|
35
|
+
|
|
36
|
+
This means consumers who ship pre-compiled `.pdf` figures alongside their `.tex` sources don't pay the compilation cost on every build.
|
|
37
|
+
|
|
38
|
+
## Working directory
|
|
39
|
+
|
|
40
|
+
`pdflatex` runs in `figures/<topic>/` (the directory containing the source). This makes TikZ `\input{}` relative paths work correctly and keeps intermediate files (`.aux`, `.log`) alongside the source for easy debugging.
|
|
41
|
+
|
|
42
|
+
## Gitignore intermediate files
|
|
43
|
+
|
|
44
|
+
Add to your project's `.gitignore`:
|
|
45
|
+
|
|
46
|
+
```gitignore
|
|
47
|
+
figures/**/*.aux
|
|
48
|
+
figures/**/*.log
|
|
49
|
+
figures/**/*.fdb_latexmk
|
|
50
|
+
figures/**/*.fls
|
|
51
|
+
figures/**/*.synctex.gz
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The intermediate `.pdf` files generated from `.tex` SHOULD be committed — they let consumers without TeX Live still see your figures (the SVGs still get generated from the committed PDFs).
|
|
55
|
+
|
|
56
|
+
## Required: TeX Live install
|
|
57
|
+
|
|
58
|
+
`pdflatex` is a system dependency (not an npm package). Install:
|
|
59
|
+
|
|
60
|
+
- **macOS**: `brew install --cask mactex` (full) or `brew install --cask basictex` (minimal — add packages via `tlmgr`)
|
|
61
|
+
- **Ubuntu/Debian**: `sudo apt-get install texlive-base texlive-pictures` (minimal for TikZ)
|
|
62
|
+
- **Other**: https://www.tug.org/texlive/
|
|
63
|
+
|
|
64
|
+
If `pdflatex` is missing but `.tex` files are present, `build-figures` prints a clear ERROR with the install link and continues processing any `.pdf`-only topics. Doesn't crash the build.
|
|
65
|
+
|
|
66
|
+
## CI workflow note
|
|
67
|
+
|
|
68
|
+
If your CI builds rely on regenerating SVGs from `.tex` sources (rather than just serving committed SVGs), add TeX Live to your CI workflow:
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
- name: Install TeX Live for TikZ figures
|
|
72
|
+
run: sudo apt-get install -y texlive-base texlive-pictures
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This adds ~200 MB to the runner — only do it if your figures actually change between commits. The recommended pattern is to commit both `.tex` AND the generated `.pdf`/`.svg`, treating regeneration as a local-dev concern.
|
|
76
|
+
|
|
77
|
+
## Debugging compilation failures
|
|
78
|
+
|
|
79
|
+
When pdflatex fails on a `.tex` source, `build-figures` prints the stderr (and falls back to stdout) and continues with the remaining figures. Don't fail the whole build for one broken figure.
|
|
80
|
+
|
|
81
|
+
To debug interactively:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
cd figures/<topic>/
|
|
85
|
+
pdflatex diagram.tex
|
|
86
|
+
# read diagram.log for the full error trace
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Common failures:
|
|
90
|
+
- **Missing package**: `! LaTeX Error: File 'tikz-cd.sty' not found.` → install with `tlmgr install tikz-cd` (macOS/Linux Tex Live) or `sudo apt-get install texlive-tikz-cd` (Debian).
|
|
91
|
+
- **Syntax error**: `! Undefined control sequence.` → check the `.log` file for the line number.
|
|
92
|
+
- **Compilation hang**: should auto-resolve via `-halt-on-error -interaction=nonstopmode` flags the scaffold uses.
|
|
93
|
+
|
|
94
|
+
## Feedback loop
|
|
95
|
+
|
|
96
|
+
If you hit friction with the TikZ pipeline (a TikZ feature that doesn't compile, an obscure error message, a workflow pattern that doesn't fit), file an issue at https://github.com/brandon-behring/book-scaffold-astro/issues with the `consumer:<your-workspace>` label. v4.x is the iteration window.
|
|
97
|
+
|
|
98
|
+
## See also
|
|
99
|
+
|
|
100
|
+
- `recipes/06-figures.md` — overall figure pipeline + matplotlib/svg sources
|
|
101
|
+
- `PACKAGE_DESIGN.md §7` — peer dependencies (lists `pdflatex` as optional system dep)
|
|
102
|
+
- `PACKAGE_DESIGN.md §8` — `book-scaffold` CLI reference (build-figures subcommand)
|
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
* pdftocairo, emitting under public/figures/. SVG preserves zoom/quality
|
|
7
7
|
* and stays small for matplotlib-style plots.
|
|
8
8
|
*
|
|
9
|
+
* v4.2.0 (closes #17): TikZ standalone `.tex` sources are auto-compiled
|
|
10
|
+
* to `.pdf` via pdflatex before the existing PDF→SVG pass runs. Discovery
|
|
11
|
+
* rule: if `figures/<topic>/<name>.tex` exists AND no sibling `.pdf`
|
|
12
|
+
* (or `.tex` is newer than `.pdf`), pdflatex runs. Graceful skip when
|
|
13
|
+
* pdflatex is not on PATH (only emits ERROR if .tex sources exist).
|
|
14
|
+
* See recipes/16-tikz-figures.md for the workflow + install pointers.
|
|
15
|
+
*
|
|
9
16
|
* Default source: figures/ at scaffold root. Override via BOOK_FIGURES_PATH
|
|
10
17
|
* env var (absolute path or path relative to scaffold root) — useful for
|
|
11
18
|
* books that share figures with a LaTeX sibling at e.g. ../shared/figures/.
|
|
@@ -98,6 +105,55 @@ async function listPdfsRecursive(root, prefix = '') {
|
|
|
98
105
|
return out;
|
|
99
106
|
}
|
|
100
107
|
|
|
108
|
+
/**
|
|
109
|
+
* v4.2.0 (#17): recursively collect .tex sources under FIGURES_SRC.
|
|
110
|
+
* Same shape as listPdfsRecursive but for `.tex` extension. TikZ
|
|
111
|
+
* standalone files at any subdirectory depth are picked up.
|
|
112
|
+
*/
|
|
113
|
+
async function listTexRecursive(root, prefix = '') {
|
|
114
|
+
if (!existsSync(root)) return [];
|
|
115
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
116
|
+
const out = [];
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
119
|
+
if (entry.isDirectory()) {
|
|
120
|
+
out.push(...(await listTexRecursive(resolve(root, entry.name), relPath)));
|
|
121
|
+
} else if (entry.isFile() && entry.name.endsWith('.tex')) {
|
|
122
|
+
out.push({ relPath });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* v4.2.0 (#17): compile a TikZ standalone .tex to .pdf via pdflatex.
|
|
130
|
+
* Run in the source directory so TikZ \input{} relative paths resolve
|
|
131
|
+
* + intermediate .aux/.log files land alongside the source.
|
|
132
|
+
* Returns true on success; throws Error with stderr on failure.
|
|
133
|
+
*/
|
|
134
|
+
function compileTikz(srcPath) {
|
|
135
|
+
const srcDir = dirname(srcPath);
|
|
136
|
+
const srcName = basename(srcPath);
|
|
137
|
+
const r = spawnSync(
|
|
138
|
+
'pdflatex',
|
|
139
|
+
[
|
|
140
|
+
'-halt-on-error',
|
|
141
|
+
'-interaction=nonstopmode',
|
|
142
|
+
'-output-directory=.',
|
|
143
|
+
srcName,
|
|
144
|
+
],
|
|
145
|
+
{ cwd: srcDir, stdio: 'pipe' },
|
|
146
|
+
);
|
|
147
|
+
if (r.status !== 0) {
|
|
148
|
+
const stderr = (r.stderr ?? Buffer.from('')).toString().trim();
|
|
149
|
+
const stdout = (r.stdout ?? Buffer.from('')).toString().trim();
|
|
150
|
+
throw new Error(
|
|
151
|
+
`pdflatex failed for ${srcPath}: ${stderr || stdout || `exit code ${r.status}`}`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
101
157
|
function isUpToDate(srcPath, dstPath) {
|
|
102
158
|
if (!existsSync(dstPath)) return false;
|
|
103
159
|
const srcMtime = statSync(srcPath).mtimeMs;
|
|
@@ -153,6 +209,38 @@ async function main() {
|
|
|
153
209
|
return;
|
|
154
210
|
}
|
|
155
211
|
|
|
212
|
+
// v4.2.0 (#17): stage 1 — compile any TikZ standalone .tex sources to .pdf
|
|
213
|
+
// BEFORE the PDF→SVG loop. Topic-walk discovers .tex files; compiles those
|
|
214
|
+
// where no sibling .pdf exists OR .tex is newer than .pdf.
|
|
215
|
+
const texSources = await listTexRecursive(FIGURES_SRC);
|
|
216
|
+
let tikzCompiled = 0;
|
|
217
|
+
if (texSources.length > 0) {
|
|
218
|
+
if (!check('pdflatex')) {
|
|
219
|
+
console.error(
|
|
220
|
+
`build-figures: pdflatex not on $PATH but ${texSources.length} ` +
|
|
221
|
+
`.tex source(s) detected. Install TeX Live to enable TikZ ` +
|
|
222
|
+
`compilation (see https://www.tug.org/texlive/). Skipping ` +
|
|
223
|
+
`.tex sources; pre-compiled .pdf siblings (if any) will still ` +
|
|
224
|
+
`be converted to SVG.`,
|
|
225
|
+
);
|
|
226
|
+
} else {
|
|
227
|
+
for (const { relPath } of texSources) {
|
|
228
|
+
const srcPath = resolve(FIGURES_SRC, relPath);
|
|
229
|
+
const pdfPath = srcPath.replace(/\.tex$/, '.pdf');
|
|
230
|
+
if (existsSync(pdfPath) && statSync(pdfPath).mtimeMs >= statSync(srcPath).mtimeMs) {
|
|
231
|
+
continue; // pdf is up-to-date
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
compileTikz(srcPath);
|
|
235
|
+
tikzCompiled++;
|
|
236
|
+
} catch (err) {
|
|
237
|
+
console.error(`build-figures: ${err.message ?? err}`);
|
|
238
|
+
// Continue — don't fail the whole build on one broken .tex file.
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
156
244
|
const pdfs = await listPdfsRecursive(FIGURES_SRC);
|
|
157
245
|
if (pdfs.length === 0) {
|
|
158
246
|
console.log('build-figures: no PDFs found; nothing to do.');
|
|
@@ -185,9 +273,10 @@ async function main() {
|
|
|
185
273
|
converted++;
|
|
186
274
|
}
|
|
187
275
|
|
|
276
|
+
const tikzNote = tikzCompiled > 0 ? `, ${tikzCompiled} tikz→pdf` : '';
|
|
188
277
|
console.log(
|
|
189
278
|
`build-figures: ${total} total, ${converted} converted ` +
|
|
190
|
-
`(${pngFallback} png fallback), ${skipped} cached`,
|
|
279
|
+
`(${pngFallback} png fallback), ${skipped} cached${tikzNote}`,
|
|
191
280
|
);
|
|
192
281
|
}
|
|
193
282
|
|