1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
//! Theme registry for `ferrocv`.
//!
//! A [`Theme`] is a bundle of Typst source files ([`include_bytes!`]'d
//! at compile time) plus the virtual path Typst should start compiling
//! from. Theme files are served by [`crate::render::FerrocvWorld`]
//! through its in-memory file map; there is no filesystem access at
//! render time (CONSTITUTION §6.1, §6.4).
//!
//! # Adapters vs. native themes — CONSTITUTION §4
//!
//! This module's registry currently holds a mix:
//!
//! - **Adapters** wrap an upstream Typst template (e.g.
//! `typst-jsonresume-cv`) and hand it a JSON Resume structure
//! through the conventional `/resume.json` virtual file. Adapters
//! accept that upstream layout changes may break them; in return
//! they give `ferrocv` visual variety without re-implementing a
//! full resume renderer.
//! - **Native themes** implement a `render(data) -> content` contract
//! directly against parsed JSON Resume data. The `text-minimal`
//! theme below is the first native theme — it exists to feed clean
//! plain-text output to [`crate::render::compile_text`].
//!
//! Per CONSTITUTION §4 the two layers are kept separable: adapter
//! code does not leak into native themes, and native themes do not
//! depend on adapter internals. §4 also promises that native themes
//! will eventually live in a dedicated module. For Phase 2, with one
//! native theme to ship, that split would be premature abstraction
//! (CONSTITUTION §5: "simple now, iterate later"); the split is
//! deferred until a second native theme materializes.
//!
//! # Why a static slice, not a `HashMap` or `ThemeRegistry`
//!
//! Phase 2 ships four themes total: three adapters
//! (`typst-jsonresume-cv`, `fantastic-cv`, `modern-cv`) and one native
//! theme (`text-minimal`). A linear scan over `THEMES` is O(n) for
//! small n; CONSTITUTION §5 ("simple now, iterate later") calls for
//! the narrower solution here. Generalizing to a hashed lookup or a
//! builder pattern should wait for a caller that actually needs it.
/// A themed Typst source bundle that [`crate::render::compile_theme`]
/// can compile against a JSON Resume document.
///
/// `name` is the registry key (the string a CLI `--theme <name>`
/// argument will eventually match against). `files` is the set of
/// Typst source files the theme needs, each keyed by the virtual
/// path it will resolve under inside [`crate::render::FerrocvWorld`].
/// `entrypoint` is the virtual path of the file `typst::compile`
/// starts from.
///
/// All fields are `'static` because themes are defined as `const`s
/// and their contents come from [`include_bytes!`].
/// Virtual-path prefix for this theme's files inside the World.
///
/// Centralized as a private `const` so the `files` and `entrypoint`
/// fields stay in lockstep. If this prefix changes, every file path
/// in [`TYPST_JSONRESUME_CV`] updates in one place.
const TYPST_JSONRESUME_CV_PREFIX: &str = "/themes/typst-jsonresume-cv";
/// Virtual-path prefix for this theme's files inside the World.
///
/// Centralized as a private `const` so the `files` and `entrypoint`
/// fields stay in lockstep. If this prefix changes, every file path
/// in [`FANTASTIC_CV`] updates in one place.
const FANTASTIC_CV_PREFIX: &str = "/themes/fantastic-cv";
/// Virtual-path prefix for this theme's files inside the World.
///
/// Centralized as a private `const` so the `files` and `entrypoint`
/// fields stay in lockstep. If this prefix changes, every file path
/// in [`MODERN_CV`] updates in one place.
const MODERN_CV_PREFIX: &str = "/themes/modern-cv";
/// Adapter for [`fruggiero/typst-jsonresume-cv`]'s `basic-resume`
/// theme, vendored under `assets/themes/typst-jsonresume-cv/`.
///
/// The entrypoint is the patched `resume.typ`. It does
/// `#import "base.typ": *`, which Typst resolves relative to the
/// entrypoint's virtual directory — hence both files sit side-by-side
/// under the same prefix. See `assets/themes/typst-jsonresume-cv/VENDORING.md`
/// for the patch record and upstream commit SHA.
///
/// [`fruggiero/typst-jsonresume-cv`]: https://github.com/fruggiero/typst-jsonresume-cv
pub const TYPST_JSONRESUME_CV: Theme = Theme ;
// Compile-time sanity check: the entrypoint matches the prefix we
// centralized above. Kept as a `const _` so a typo in either string
// literal becomes a build error rather than a runtime mystery.
const _: = ;
/// Adapter for [`austinyu/fantastic-cv`], vendored under
/// `assets/themes/fantastic-cv/`.
///
/// The entrypoint is our authored glue `resume.typ`, which
/// `#import`s the byte-for-byte vendored `fantastic-cv.typ` from the
/// same virtual directory. All JSON-Resume → fantastic-cv field
/// mapping lives in the glue; the vendored source is untouched. See
/// `assets/themes/fantastic-cv/VENDORING.md` for the provenance record
/// and the glue-not-patch rationale.
///
/// [`austinyu/fantastic-cv`]: https://github.com/austinyu/fantastic-cv
pub const FANTASTIC_CV: Theme = Theme ;
// Compile-time sanity check: same shape as for TYPST_JSONRESUME_CV.
const _: = ;
/// Adapter for [`DeveloperPaul123/modern-cv`] (canonical:
/// `ptsouchlos/modern-cv`), vendored under `assets/themes/modern-cv/`.
///
/// Unlike [`FANTASTIC_CV`] (which is a pure glue-only vendor — the
/// upstream source is byte-for-byte unchanged), this adapter ships a
/// **patched** `lib.typ`: the upstream pulls `@preview/fontawesome`
/// and `@preview/linguify` at compile time, which CONSTITUTION §6.1
/// forbids. All icon and i18n call sites were rewritten; see
/// `assets/themes/modern-cv/VENDORING.md` for the patch record.
/// The entrypoint is our authored glue `resume.typ`, which imports
/// the patched `lib.typ` from the same virtual directory.
///
/// [`DeveloperPaul123/modern-cv`]: https://github.com/DeveloperPaul123/modern-cv
pub const MODERN_CV: Theme = Theme ;
// Compile-time sanity check: same shape as for TYPST_JSONRESUME_CV.
const _: = ;
/// Virtual path of the `text-minimal` theme's entrypoint.
///
/// Single per-file constant used by both the [`Theme::files`] key and
/// the [`Theme::entrypoint`] field below, so the two cannot drift out
/// of sync. This is the cleanup CodeRabbit flagged on the original PR
/// — the previous "prefix" constant was declared but unused (the
/// `concat!` calls hardcoded the literal), making the centralization
/// claim cosmetic. The adapter above still uses the older
/// prefix-as-const pattern; tightening it the same way is its own
/// scope.
const TEXT_MINIMAL_RESUME_PATH: &str = "/themes/text-minimal/resume.typ";
/// `text-minimal` — a **native theme** (per CONSTITUTION §4) authored
/// directly against the JSON Resume v1.0.0 schema, with no upstream
/// template to wrap.
///
/// It exists to produce clean output for
/// [`crate::render::compile_text`]. The Frame-walk extractor sorts
/// glyph runs by `(page, y, x)` and joins same-line items with a
/// space; multi-column or floated layouts therefore produce zig-zag
/// reading order. `text-minimal` is single-column, uses explicit
/// `linebreak()` and `parbreak()` for line and paragraph boundaries,
/// avoids decorative glyphs (no bullets, arrows, dingbats — those
/// survive frame extraction and add ATS noise), and sticks with the
/// default font for cross-host reproducibility (CONSTITUTION §6).
///
/// Every field access in the theme source is wrapped in
/// `dict.at(k, default: none)` so any schema-valid JSON Resume
/// document compiles, including documents that exercise only
/// `basics.name` (the `render_sparse.json` fixture is the lower
/// bound).
///
/// The MIT-licensed source under `assets/themes/text-minimal/` is
/// also redistributable under the `ferrocv` crate's MIT-or-Apache-2.0
/// dual license; the file-level `LICENSE` is duplicated so the theme
/// remains self-contained if it is ever extracted into its own
/// package.
///
/// CONSTITUTION §4 promises a separate native-themes module
/// eventually. For Phase 2, with one native theme registered,
/// splitting is premature abstraction (§5: "simple now, iterate
/// later"); the split is deferred until a second native theme exists.
pub const TEXT_MINIMAL: Theme = Theme ;
/// All themes registered with this build of `ferrocv`.
///
/// Phase 2 ships three adapters (`typst-jsonresume-cv`, `fantastic-cv`,
/// `modern-cv`) and one native theme (`text-minimal`). See the module
/// doc for why this is a `&[&Theme]` rather than a `HashMap` or a
/// builder pattern — a linear scan over a handful of entries is fine,
/// and CONSTITUTION §5 calls for the narrower solution until a caller
/// actually needs more. See the module doc as well for the §4
/// deferral on splitting native themes into their own module.
pub const THEMES: & = &;
/// Look up a [`Theme`] by name. Returns `None` for unknown names.
///
/// Linear scan over [`THEMES`]; O(n) for n themes. Acceptable for the
/// current handful of entries (CONSTITUTION §5).