@brandon_m_behring/book-scaffold-astro 4.6.0 → 4.6.1
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 +29 -0
- package/dist/index.mjs +8 -7
- package/dist/schemas.mjs +8 -7
- package/examples/chapter-template-research-portfolio.mdx +18 -10
- package/package.json +1 -1
- package/recipes/07-chapter-shapes.md +2 -0
- package/recipes/13-research-portfolio-getting-started.md +49 -11
- package/recipes/20-anki-export.md +175 -0
- package/recipes/README.md +6 -0
- package/src/profiles/academic.ts +1 -1
- package/src/profiles/course-notes.ts +1 -1
- package/src/profiles/minimal.ts +1 -1
- package/src/profiles/research-portfolio.ts +1 -1
package/CLAUDE.md
CHANGED
|
@@ -58,6 +58,35 @@ sources: [] # array of source-manifest keys
|
|
|
58
58
|
---
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
### Research-portfolio profile (`src/schemas.ts:researchPortfolioChapterSchema`)
|
|
62
|
+
|
|
63
|
+
Hybrid of academic + tools provenance with research-paper-style inline sources. Only `title` + `last_verified` are required; all hierarchy and classification fields are optional.
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
---
|
|
67
|
+
title: "..." # required
|
|
68
|
+
last_verified: 2026-05-19 # date, required
|
|
69
|
+
# optional — hierarchy (use whichever fits; all may be omitted)
|
|
70
|
+
slug: ch01-introduction # defaults to filename
|
|
71
|
+
chapter: 1
|
|
72
|
+
part: 1 # number OR academic-style string enum
|
|
73
|
+
week: 1
|
|
74
|
+
# optional — status (AUTHORING state) vs freshness (EPISTEMIC type) are ORTHOGONAL
|
|
75
|
+
status: prose_only # 'scaffolded'|'prose_only'|'code_only'|'chapter_only'|'reading_only'|'implemented'|'planned'
|
|
76
|
+
freshness: experimental-result # 'experimental-result'|'literature-survey'|'theoretical'|'reference'
|
|
77
|
+
# optional — provenance + inline sources (T1-T4 tiers)
|
|
78
|
+
volatility: feature-surface # 'stable-principle'|'architectural-pattern'|'feature-surface'
|
|
79
|
+
tags: [prompt-injection, ...] # freeform string array
|
|
80
|
+
sources:
|
|
81
|
+
- tier: T1
|
|
82
|
+
url: https://...
|
|
83
|
+
label: Primary source
|
|
84
|
+
# optional: description, draft, updated, author, published, image (SEO/og:*)
|
|
85
|
+
---
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**`status` vs `freshness` is the #1 author gotcha.** `status` = authoring state (have I written it?). `freshness` = epistemic type (what kind of evidence?). A chapter can be `status: scaffolded` (not written yet) AND `freshness: theoretical` (will be a math argument). See Recipe 13 for the full table.
|
|
89
|
+
|
|
61
90
|
## Component reference
|
|
62
91
|
|
|
63
92
|
Two callout families coexist. Authors import what they need.
|
package/dist/index.mjs
CHANGED
|
@@ -398,8 +398,8 @@ var academicProfile = defineProfile({
|
|
|
398
398
|
references: true,
|
|
399
399
|
search: true,
|
|
400
400
|
print: true,
|
|
401
|
-
chapters:
|
|
402
|
-
// academic
|
|
401
|
+
chapters: true,
|
|
402
|
+
// v4.6.1 (#75 follow-up): auto-injected /chapters/[...slug]/ + /chapters/ index. Pre-v4.3.0 academic books shipped their own listing; v4.6.0 (#76 Layer 3c) removed the consumer template assuming auto-injection. Default flipped here to close the gap. Consumers wanting their own listing override via `routes: { chapters: false }` + their own src/pages/chapters/* — see recipe 18.
|
|
403
403
|
convergence: false,
|
|
404
404
|
// tools-profile-specific
|
|
405
405
|
frontmatter: false,
|
|
@@ -607,7 +607,8 @@ var minimalProfile = defineProfile({
|
|
|
607
607
|
references: true,
|
|
608
608
|
search: true,
|
|
609
609
|
print: true,
|
|
610
|
-
chapters:
|
|
610
|
+
chapters: true,
|
|
611
|
+
// v4.6.1 (#75 follow-up): default-on across all profiles. Consumer override via routes: { chapters: false }.
|
|
611
612
|
convergence: false,
|
|
612
613
|
frontmatter: false,
|
|
613
614
|
// opt-in per book; see #7
|
|
@@ -631,8 +632,8 @@ var courseNotesProfile = defineProfile({
|
|
|
631
632
|
references: true,
|
|
632
633
|
search: true,
|
|
633
634
|
print: true,
|
|
634
|
-
chapters:
|
|
635
|
-
//
|
|
635
|
+
chapters: true,
|
|
636
|
+
// v4.6.1 (#75 follow-up): default-on. Multi-book consumers (DLAI-style) override via routes: { chapters: false } + own [book]/[slug] routes — see #15 deferred.
|
|
636
637
|
convergence: false,
|
|
637
638
|
frontmatter: false,
|
|
638
639
|
// opt-in per book; see #7
|
|
@@ -659,8 +660,8 @@ var researchPortfolioProfile = defineProfile({
|
|
|
659
660
|
references: true,
|
|
660
661
|
search: true,
|
|
661
662
|
print: true,
|
|
662
|
-
chapters:
|
|
663
|
-
//
|
|
663
|
+
chapters: true,
|
|
664
|
+
// v4.6.1 (#75 follow-up): default-on. Portfolios still ship their own /frontmatter/* + landing; /chapters/* renders the underlying chapter list.
|
|
664
665
|
convergence: false,
|
|
665
666
|
// tools-profile-specific
|
|
666
667
|
frontmatter: true,
|
package/dist/schemas.mjs
CHANGED
|
@@ -282,8 +282,8 @@ var academicProfile = defineProfile({
|
|
|
282
282
|
references: true,
|
|
283
283
|
search: true,
|
|
284
284
|
print: true,
|
|
285
|
-
chapters:
|
|
286
|
-
// academic
|
|
285
|
+
chapters: true,
|
|
286
|
+
// v4.6.1 (#75 follow-up): auto-injected /chapters/[...slug]/ + /chapters/ index. Pre-v4.3.0 academic books shipped their own listing; v4.6.0 (#76 Layer 3c) removed the consumer template assuming auto-injection. Default flipped here to close the gap. Consumers wanting their own listing override via `routes: { chapters: false }` + their own src/pages/chapters/* — see recipe 18.
|
|
287
287
|
convergence: false,
|
|
288
288
|
// tools-profile-specific
|
|
289
289
|
frontmatter: false,
|
|
@@ -491,7 +491,8 @@ var minimalProfile = defineProfile({
|
|
|
491
491
|
references: true,
|
|
492
492
|
search: true,
|
|
493
493
|
print: true,
|
|
494
|
-
chapters:
|
|
494
|
+
chapters: true,
|
|
495
|
+
// v4.6.1 (#75 follow-up): default-on across all profiles. Consumer override via routes: { chapters: false }.
|
|
495
496
|
convergence: false,
|
|
496
497
|
frontmatter: false,
|
|
497
498
|
// opt-in per book; see #7
|
|
@@ -515,8 +516,8 @@ var courseNotesProfile = defineProfile({
|
|
|
515
516
|
references: true,
|
|
516
517
|
search: true,
|
|
517
518
|
print: true,
|
|
518
|
-
chapters:
|
|
519
|
-
//
|
|
519
|
+
chapters: true,
|
|
520
|
+
// v4.6.1 (#75 follow-up): default-on. Multi-book consumers (DLAI-style) override via routes: { chapters: false } + own [book]/[slug] routes — see #15 deferred.
|
|
520
521
|
convergence: false,
|
|
521
522
|
frontmatter: false,
|
|
522
523
|
// opt-in per book; see #7
|
|
@@ -543,8 +544,8 @@ var researchPortfolioProfile = defineProfile({
|
|
|
543
544
|
references: true,
|
|
544
545
|
search: true,
|
|
545
546
|
print: true,
|
|
546
|
-
chapters:
|
|
547
|
-
//
|
|
547
|
+
chapters: true,
|
|
548
|
+
// v4.6.1 (#75 follow-up): default-on. Portfolios still ship their own /frontmatter/* + landing; /chapters/* renders the underlying chapter list.
|
|
548
549
|
convergence: false,
|
|
549
550
|
// tools-profile-specific
|
|
550
551
|
frontmatter: true,
|
|
@@ -1,25 +1,33 @@
|
|
|
1
1
|
---
|
|
2
|
+
# required fields — schema will reject the chapter if missing
|
|
2
3
|
title: "Chapter N — Title goes here"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
last_verified: 2026-05-19 # required; YAML date (no quotes)
|
|
5
|
+
|
|
6
|
+
# optional — hierarchy (use whichever fits; all may be omitted)
|
|
7
|
+
slug: chN-short-slug # optional; defaults to filename
|
|
8
|
+
chapter: 1 # optional; tools-style numeric
|
|
9
|
+
part: 1 # optional; number OR academic-style string enum
|
|
6
10
|
week: 1 # optional; omit if not on a weekly cadence
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
# optional — status (AUTHORING state) vs freshness (EPISTEMIC type); see Recipe 13
|
|
13
|
+
status: prose_only # 'scaffolded'|'prose_only'|'code_only'|'chapter_only'|'reading_only'|'implemented'|'planned'
|
|
14
|
+
freshness: experimental-result # 'experimental-result'|'literature-survey'|'theoretical'|'reference'
|
|
15
|
+
|
|
16
|
+
# optional — provenance
|
|
17
|
+
volatility: feature-surface # 'stable-principle'|'architectural-pattern'|'feature-surface'
|
|
18
|
+
tags: # optional; freeform string array
|
|
11
19
|
- replace-me
|
|
12
20
|
- with
|
|
13
21
|
- real-tags
|
|
14
|
-
sources:
|
|
22
|
+
sources: # optional; structured inline T1-T4
|
|
15
23
|
- tier: T1
|
|
16
24
|
url: https://example.invalid/primary-source
|
|
17
25
|
label: Primary source (e.g., NVD CVE / arXiv paper / official spec)
|
|
18
26
|
- tier: T2
|
|
19
27
|
url: https://example.invalid/secondary
|
|
20
28
|
label: Secondary corroboration
|
|
21
|
-
|
|
22
|
-
draft: true
|
|
29
|
+
|
|
30
|
+
draft: true # optional; defaults to false
|
|
23
31
|
---
|
|
24
32
|
|
|
25
33
|
import PreReleaseBanner from '@brandon_m_behring/book-scaffold-astro/components/PreReleaseBanner.astro';
|
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.6.
|
|
4
|
+
"version": "4.6.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
|
@@ -47,6 +47,8 @@ Source: Koller & Friedman, *Probabilistic Graphical Models*, 2009 — chapter st
|
|
|
47
47
|
| Research synthesis (one paper or theorem per chapter) | Academic | `academic` |
|
|
48
48
|
| A practitioner field-guide across multiple tools | Tools | `tools` |
|
|
49
49
|
| A versioned tech survey with convergence tracking | Tools | `tools` |
|
|
50
|
+
| A research-portfolio with mixed evidence types + AI disclosure | Hybrid | `research-portfolio` (see [Recipe 13](13-research-portfolio-getting-started.md)) |
|
|
51
|
+
| A course-notes / study-derived corpus | Hybrid | `course-notes` |
|
|
50
52
|
| A solo essay collection | either, lean Academic | `minimal` (uses tools schema) |
|
|
51
53
|
|
|
52
54
|
## Hybrid books
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
The `research-portfolio` preset (v3.5.0+) is for books that combine:
|
|
4
4
|
|
|
5
5
|
- **Academic structure**: week/part/status, KaTeX math, BibTeX citations, Theorem family
|
|
6
|
-
- **Tools-style provenance**: volatility class, T1–T4 tier-tagged sources, `last_verified`
|
|
6
|
+
- **Tools-style provenance**: volatility class, T1–T4 tier-tagged sources, required `last_verified` date
|
|
7
7
|
- **Portfolio-specific affordances**: pre-release banner, AI collaboration disclosure, blocked-by-upstream callouts, structured ethics/policy references
|
|
8
8
|
|
|
9
9
|
If your book is primarily a weekly curriculum, use [`academic`](07-chapter-shapes.md#academic). If primarily AI-CLI comparison content, use [`tools`](07-chapter-shapes.md#tools). If a course-derived study notebook, use [`course-notes`](07-chapter-shapes.md#course-notes). Research portfolios sit at the intersection of all three and get their own preset.
|
|
@@ -39,28 +39,66 @@ This scaffolds:
|
|
|
39
39
|
|
|
40
40
|
## Chapter frontmatter shape
|
|
41
41
|
|
|
42
|
+
Two fields are **required** by the schema; everything else is optional.
|
|
43
|
+
|
|
44
|
+
| Field | Required? | Notes |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `title` | **required** | Non-empty string |
|
|
47
|
+
| `last_verified` | **required** | YAML date (`2026-05-19`); used by freshness reports + the v4.6 prevalidate hook |
|
|
48
|
+
| All other fields | optional | See annotations in the template below |
|
|
49
|
+
|
|
50
|
+
### `status` vs `freshness` — two distinct axes
|
|
51
|
+
|
|
52
|
+
These look similar but mean different things. Authors often confuse them — getting `freshness` wrong fails the schema with `InvalidContentEntryDataError`.
|
|
53
|
+
|
|
54
|
+
| Field | Concept | Enum values | Mental check |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| `status` | **Authoring state** — where am I in writing this chapter? | `scaffolded`, `prose_only`, `code_only`, `chapter_only`, `reading_only`, `implemented`, `planned` | "Have I written it?" |
|
|
57
|
+
| `freshness` | **Epistemic type** — what kind of evidence does this chapter rest on? | `experimental-result`, `literature-survey`, `theoretical`, `reference` | "What kind of knowledge is this?" |
|
|
58
|
+
|
|
59
|
+
A chapter can be `status: scaffolded` (not yet written) AND `freshness: theoretical` (will be a mathematical argument). They're orthogonal.
|
|
60
|
+
|
|
61
|
+
If you want to mark a chapter as "not written yet", use `status: scaffolded` or `status: planned`. `freshness` has no value for that — it describes the chapter's content type, not its progress.
|
|
62
|
+
|
|
63
|
+
### Template
|
|
64
|
+
|
|
42
65
|
```yaml
|
|
43
66
|
---
|
|
67
|
+
# required
|
|
44
68
|
title: "Chapter title"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
69
|
+
last_verified: 2026-05-19 # YAML date (no quotes); becomes a JS Date
|
|
70
|
+
|
|
71
|
+
# optional — hierarchy (use whichever fits; all three may be omitted)
|
|
72
|
+
slug: ch01-introduction # defaults to filename
|
|
73
|
+
chapter: 1 # tools-style numeric
|
|
74
|
+
part: 1 # either number OR academic-style string enum
|
|
75
|
+
week: 1 # only if you use weekly cadence
|
|
76
|
+
|
|
77
|
+
# optional — authoring state + epistemic type
|
|
78
|
+
status: prose_only # 'scaffolded'|'prose_only'|'code_only'|'chapter_only'|'reading_only'|'implemented'|'planned'
|
|
79
|
+
freshness: experimental-result # 'experimental-result'|'literature-survey'|'theoretical'|'reference'
|
|
80
|
+
|
|
81
|
+
# optional — provenance
|
|
82
|
+
volatility: feature-surface # 'stable-principle'|'architectural-pattern'|'feature-surface'
|
|
83
|
+
tags: # freeform string array (NOT the tools_compared enum)
|
|
53
84
|
- prompt-injection
|
|
54
85
|
- red-team
|
|
55
86
|
- CVE-2025-32711
|
|
56
|
-
sources:
|
|
87
|
+
sources: # structured inline; tier ∈ {T1, T2, T3, T4}
|
|
57
88
|
- tier: T1
|
|
58
89
|
url: https://nvd.nist.gov/vuln/detail/CVE-2025-32711
|
|
59
90
|
label: NVD CVE-2025-32711 (primary advisory)
|
|
60
91
|
- tier: T2
|
|
61
92
|
url: https://arxiv.org/abs/2406.00799
|
|
62
93
|
label: TaskTracker (Wallace et al. 2024)
|
|
63
|
-
|
|
94
|
+
|
|
95
|
+
# optional — SEO / OpenGraph (v4.6+)
|
|
96
|
+
description: "..." # used by Base.astro meta tags
|
|
97
|
+
author: "Brandon Behring"
|
|
98
|
+
published: 2026-05-01
|
|
99
|
+
updated: 2026-05-19
|
|
100
|
+
image: "/og/ch01.png"
|
|
101
|
+
|
|
64
102
|
draft: false
|
|
65
103
|
---
|
|
66
104
|
```
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Recipe 20 — Anki deck export (consumer-side pattern)
|
|
2
|
+
|
|
3
|
+
**Profile**: any (most useful for `course-notes` and `research-portfolio`).
|
|
4
|
+
|
|
5
|
+
**TL;DR**: The scaffold does **not** ship an `<AnkiCard>` component or an `extract-cards` CLI (see [PACKAGE_DESIGN.md §15a](../../PACKAGE_DESIGN.md#15a-deferred-scope-post-v4x) for why). This recipe shows how a consumer can roll their own — a small `<AnkiCard>` component plus a `scripts/extract-anki.mjs` extractor that walks the chapters collection and emits an Anki deck. ~120 lines of consumer-side code; no scaffold changes required.
|
|
6
|
+
|
|
7
|
+
If your book is the **third** independent consumer to want this, please open an issue at [book-scaffold-astro](https://github.com/brandon-behring/book-scaffold-astro/issues) — that's the signal we need to consider promoting this to scaffold-level surface.
|
|
8
|
+
|
|
9
|
+
## When to use this pattern
|
|
10
|
+
|
|
11
|
+
Use it if your book:
|
|
12
|
+
|
|
13
|
+
- Has discrete reviewable facts that benefit from spaced repetition (course notes, foundational reference material).
|
|
14
|
+
- Already authors chapter content in MDX and wants flashcards as a *byproduct*, not a parallel deck file.
|
|
15
|
+
- Wants `<AnkiCard>` to render inline as a "study widget" on the chapter page AND export to `.apkg` for offline review.
|
|
16
|
+
|
|
17
|
+
Do **not** use it if your book is essay-style prose, narrative arguments, or working through proofs — the per-card "front/back" shape fights that content.
|
|
18
|
+
|
|
19
|
+
## Step 1 — Add the component
|
|
20
|
+
|
|
21
|
+
Create `src/components/AnkiCard.astro`:
|
|
22
|
+
|
|
23
|
+
```astro
|
|
24
|
+
---
|
|
25
|
+
export interface Props {
|
|
26
|
+
id?: string; // Stable Anki note GUID (recommended)
|
|
27
|
+
front: string; // Card front (HTML / Markdown allowed in slot)
|
|
28
|
+
back?: string; // Optional one-line back; otherwise use slot
|
|
29
|
+
tags?: string[]; // Anki tags (in addition to chapter slug)
|
|
30
|
+
}
|
|
31
|
+
const { id, front, back, tags = [] } = Astro.props;
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
<aside class="anki-card" data-id={id} data-front={front} data-tags={tags.join(',')}>
|
|
35
|
+
<header><strong>Q.</strong> {front}</header>
|
|
36
|
+
{back ? <p><strong>A.</strong> {back}</p> : <slot />}
|
|
37
|
+
</aside>
|
|
38
|
+
|
|
39
|
+
<style>
|
|
40
|
+
.anki-card {
|
|
41
|
+
border-left: 4px solid var(--color-accent, #6366f1);
|
|
42
|
+
padding: 0.75rem 1rem;
|
|
43
|
+
margin: 1rem 0;
|
|
44
|
+
background: var(--color-surface-2, #fafafa);
|
|
45
|
+
border-radius: 4px;
|
|
46
|
+
}
|
|
47
|
+
.anki-card header { margin-bottom: 0.5rem; }
|
|
48
|
+
</style>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Import in chapters:
|
|
52
|
+
|
|
53
|
+
```mdx
|
|
54
|
+
import AnkiCard from '../components/AnkiCard.astro';
|
|
55
|
+
|
|
56
|
+
<AnkiCard id="ch01-q01" front="What does the central limit theorem state?">
|
|
57
|
+
The distribution of sample means approaches a normal distribution as $n \to \infty$,
|
|
58
|
+
regardless of the underlying population distribution (provided finite variance).
|
|
59
|
+
</AnkiCard>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Step 2 — Add the extractor
|
|
63
|
+
|
|
64
|
+
Create `scripts/extract-anki.mjs` at the project root:
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
#!/usr/bin/env node
|
|
68
|
+
/**
|
|
69
|
+
* scripts/extract-anki.mjs — walk chapters, find <AnkiCard> instances, emit JSON.
|
|
70
|
+
*
|
|
71
|
+
* Pairs with src/components/AnkiCard.astro. For .apkg generation, pipe the
|
|
72
|
+
* JSON through a separate tool (e.g., genanki Python lib) — keeping that
|
|
73
|
+
* outside this script avoids a runtime dependency on an Anki package builder.
|
|
74
|
+
*/
|
|
75
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
76
|
+
import { glob } from 'node:fs/promises';
|
|
77
|
+
import { resolve, dirname } from 'node:path';
|
|
78
|
+
|
|
79
|
+
const CHAPTERS_GLOB = 'src/content/chapters/**/*.{md,mdx}';
|
|
80
|
+
const OUT_PATH = 'dist-anki/cards.json';
|
|
81
|
+
|
|
82
|
+
// Match <AnkiCard ...> ... </AnkiCard> OR self-closing <AnkiCard ... />.
|
|
83
|
+
// Captures attribute block + optional slot body.
|
|
84
|
+
const ANKI_RE = /<AnkiCard\s+([^>]*?)(?:\/>|>([\s\S]*?)<\/AnkiCard>)/g;
|
|
85
|
+
const ATTR_RE = /(\w+)\s*=\s*(?:"([^"]*)"|\{([^}]*)\}|'([^']*)')/g;
|
|
86
|
+
|
|
87
|
+
function parseAttrs(attrStr) {
|
|
88
|
+
const out = {};
|
|
89
|
+
let m;
|
|
90
|
+
while ((m = ATTR_RE.exec(attrStr)) !== null) {
|
|
91
|
+
out[m[1]] = m[2] ?? m[3] ?? m[4];
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function main() {
|
|
97
|
+
const cards = [];
|
|
98
|
+
for await (const file of glob(CHAPTERS_GLOB)) {
|
|
99
|
+
const src = await readFile(file, 'utf8');
|
|
100
|
+
const slug = file.replace(/^.*\/chapters\//, '').replace(/\.mdx?$/, '');
|
|
101
|
+
let m;
|
|
102
|
+
while ((m = ANKI_RE.exec(src)) !== null) {
|
|
103
|
+
const attrs = parseAttrs(m[1]);
|
|
104
|
+
cards.push({
|
|
105
|
+
guid: attrs.id ?? `${slug}-${cards.length}`,
|
|
106
|
+
chapter: slug,
|
|
107
|
+
front: attrs.front,
|
|
108
|
+
back: attrs.back ?? (m[2] ?? '').trim(),
|
|
109
|
+
tags: (attrs.tags ?? '').split(',').filter(Boolean).concat([slug]),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await mkdir(dirname(OUT_PATH), { recursive: true });
|
|
115
|
+
await writeFile(OUT_PATH, JSON.stringify(cards, null, 2) + '\n');
|
|
116
|
+
console.log(`extract-anki: ${cards.length} cards → ${OUT_PATH}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
main().catch((e) => { console.error(e); process.exit(1); });
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Wire it into `package.json`:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"scripts": {
|
|
127
|
+
"build:anki": "node scripts/extract-anki.mjs"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Step 3 — Generate the `.apkg`
|
|
133
|
+
|
|
134
|
+
The JSON output is intentionally tool-neutral. Pick whichever Anki builder fits your stack:
|
|
135
|
+
|
|
136
|
+
- **Python `genanki`** (most common): wrap the JSON read in a 20-line Python script that emits `.apkg` via `genanki.Deck` + `genanki.Note`. Stable note GUIDs from the `guid` field keep your review history when the source updates.
|
|
137
|
+
- **Node `anki-apkg-export`** (npm): direct from Node if you want to stay in one runtime.
|
|
138
|
+
- **Plain CSV import**: Anki's CSV importer reads the JSON directly enough that you can convert with `jq -r '.[] | [.guid, .front, .back, (.tags | join(" "))] | @tsv'`.
|
|
139
|
+
|
|
140
|
+
The scaffold deliberately does **not** opine on the choice — the JSON is the contract.
|
|
141
|
+
|
|
142
|
+
## Step 4 — Per-book grouping (if needed)
|
|
143
|
+
|
|
144
|
+
The example above emits one deck per book. If you have a multi-book corpus (per the DLAI Study Notes pattern), gate emission on a `book` discriminator in chapter frontmatter:
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
const byBook = new Map();
|
|
148
|
+
// ... inside the loop:
|
|
149
|
+
const fm = parseFrontmatter(src); // bring your own YAML parser
|
|
150
|
+
const book = fm.book ?? 'main';
|
|
151
|
+
if (!byBook.has(book)) byBook.set(book, []);
|
|
152
|
+
byBook.get(book).push({ ... });
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Then write one file per book key. Multi-book corpus routing is itself out of scope at v4.x ([deferred, see #15](../../PACKAGE_DESIGN.md#15a-deferred-scope-post-v4x)) — if you need it, the same consumer-side pattern applies.
|
|
156
|
+
|
|
157
|
+
## Common gotchas
|
|
158
|
+
|
|
159
|
+
- **Stable GUIDs**: if you omit `id`, the script falls back to `${slug}-${index}`. Adding/removing cards above an existing card will shift indices and break review history. **Always set explicit `id` props** for cards you want to survive content edits.
|
|
160
|
+
- **Markdown in slots**: Anki accepts HTML; MDX renders the slot content to HTML before this extractor sees it, so most formatting carries through. KaTeX math is a special case — the extractor sees raw `$...$` LaTeX; either pre-render with KaTeX server-side before extraction, or use Anki's MathJax integration.
|
|
161
|
+
- **Pagefind**: `<AnkiCard>` instances are indexed by Pagefront like any prose. If you don't want flashcard fronts in search results, wrap in `<aside data-pagefind-ignore>`.
|
|
162
|
+
- **Visual regression**: the component adds a styled block to every chapter that has cards. If you maintain visual baselines, regenerate them after first adoption.
|
|
163
|
+
|
|
164
|
+
## Why this isn't in the scaffold
|
|
165
|
+
|
|
166
|
+
Per [PACKAGE_DESIGN.md §15a](../../PACKAGE_DESIGN.md#15a-deferred-scope-post-v4x): single consumer signal so far (DLAI), runtime dep concerns (`.apkg` is a SQLite-backed zip — needs an external builder), and the design space for per-book grouping is entangled with multi-book corpus routing (also deferred). When 2-3 consumers independently want this, it gets promoted.
|
|
167
|
+
|
|
168
|
+
## Canonical files
|
|
169
|
+
|
|
170
|
+
- This recipe (consumer pattern)
|
|
171
|
+
- [PACKAGE_DESIGN.md §15a](../../PACKAGE_DESIGN.md#15a-deferred-scope-post-v4x) — deferral rationale
|
|
172
|
+
|
|
173
|
+
## Reference implementation
|
|
174
|
+
|
|
175
|
+
- [`dlai-study-notes`](https://github.com/brandon-behring/dlai-study-notes) — the DLAI Study Notes pilot that prototyped this pattern (originator of issue #16).
|
package/recipes/README.md
CHANGED
|
@@ -20,6 +20,12 @@ Terse pointers into canonical code for the most common book-authoring workflows.
|
|
|
20
20
|
| 12 | [Where to file issues](12-where-to-file-issues.md) | any | Consumer-pilot issue template, label conventions |
|
|
21
21
|
| 13 | [Research-portfolio getting started](13-research-portfolio-getting-started.md) | research-portfolio | When to use the preset, frontmatter shape, the 4 new components |
|
|
22
22
|
| 14 | [Port a LaTeX book](14-port-latex-book.md) | typically academic | Operational playbook for porting an existing LaTeX manuscript — bib sharing, inline-upstream-PR loop, common pitfalls |
|
|
23
|
+
| 15 | [Defining styles](15-defining-styles.md) | any | The `defineStyle` API (v4.0+): compose styles per-key, override CSS, share between books |
|
|
24
|
+
| 16 | [TikZ figures](16-tikz-figures.md) | typically academic | `build-figures` TikZ standalone → SVG pipeline |
|
|
25
|
+
| 17 | [Draft chapter workflow](17-draft-chapter-workflow.md) | any | `draft: true` filtering, in-flight chapters, prerequisite gating |
|
|
26
|
+
| 18 | [Chapter route ownership](18-chapter-route-ownership.md) | any | When to override the auto-injected `/chapters/[...slug]/` route |
|
|
27
|
+
| 19 | [Prevalidate hook](19-prevalidate-hook.md) | any | Wire `prevalidate` to run `build:bib` + `build:labels` before `validate` |
|
|
28
|
+
| 20 | [Anki deck export (consumer-side)](20-anki-export.md) | any (esp. course-notes, research-portfolio) | Roll-your-own `<AnkiCard>` + extractor; scaffold deliberately doesn't ship this |
|
|
23
29
|
|
|
24
30
|
## How to read recipes
|
|
25
31
|
|
package/src/profiles/academic.ts
CHANGED
|
@@ -20,7 +20,7 @@ export const academicProfile = defineProfile({
|
|
|
20
20
|
references: true,
|
|
21
21
|
search: true,
|
|
22
22
|
print: true,
|
|
23
|
-
chapters:
|
|
23
|
+
chapters: true, // v4.6.1 (#75 follow-up): auto-injected /chapters/[...slug]/ + /chapters/ index. Pre-v4.3.0 academic books shipped their own listing; v4.6.0 (#76 Layer 3c) removed the consumer template assuming auto-injection. Default flipped here to close the gap. Consumers wanting their own listing override via `routes: { chapters: false }` + their own src/pages/chapters/* — see recipe 18.
|
|
24
24
|
convergence: false, // tools-profile-specific
|
|
25
25
|
frontmatter: false, // opt-in per book; see #7
|
|
26
26
|
tips: false, // v4.3.0 #70: opt-in per book; requires build-tips
|
|
@@ -22,7 +22,7 @@ export const courseNotesProfile = defineProfile({
|
|
|
22
22
|
references: true,
|
|
23
23
|
search: true,
|
|
24
24
|
print: true,
|
|
25
|
-
chapters:
|
|
25
|
+
chapters: true, // v4.6.1 (#75 follow-up): default-on. Multi-book consumers (DLAI-style) override via routes: { chapters: false } + own [book]/[slug] routes — see #15 deferred.
|
|
26
26
|
convergence: false,
|
|
27
27
|
frontmatter: false, // opt-in per book; see #7
|
|
28
28
|
tips: false, // v4.3.0 #70: opt-in per book
|
package/src/profiles/minimal.ts
CHANGED
|
@@ -17,7 +17,7 @@ export const minimalProfile = defineProfile({
|
|
|
17
17
|
references: true,
|
|
18
18
|
search: true,
|
|
19
19
|
print: true,
|
|
20
|
-
chapters: false
|
|
20
|
+
chapters: true, // v4.6.1 (#75 follow-up): default-on across all profiles. Consumer override via routes: { chapters: false }.
|
|
21
21
|
convergence: false,
|
|
22
22
|
frontmatter: false, // opt-in per book; see #7
|
|
23
23
|
tips: false, // v4.3.0 #70: opt-in per book
|
|
@@ -33,7 +33,7 @@ export const researchPortfolioProfile = defineProfile({
|
|
|
33
33
|
references: true,
|
|
34
34
|
search: true,
|
|
35
35
|
print: true,
|
|
36
|
-
chapters:
|
|
36
|
+
chapters: true, // v4.6.1 (#75 follow-up): default-on. Portfolios still ship their own /frontmatter/* + landing; /chapters/* renders the underlying chapter list.
|
|
37
37
|
convergence: false, // tools-profile-specific
|
|
38
38
|
frontmatter: true, // portfolios universally need title/disclosure/banner pages
|
|
39
39
|
tips: false, // v4.3.0 #70: opt-in per book
|