@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 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.1.2",
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