Skip to main content

max / makeover-build

Write a product's font override into the generated typography layer makeover 2.8.0's layer 0 reaching the file that already carries layers 1 and 2. typography_css_from takes a Typography rather than a base URL, so a brand face is declared in the build script beside the rest of the generated frontend instead of living as a hand-maintained @font-face in the app's own stylesheet. tauri_frontend_with rather than a fourth parameter on tauri_frontend: goingson is held at an older makeover by a theming decision that has nothing to do with fonts, and a signature change would turn a font feature it cannot take into a build break it cannot avoid. The old entry point delegates through the new one and a test asserts the two produce the same bytes.
Author: Max Johnson <me@maxj.phd> · 2026-08-17 18:57 UTC
Signed with PGP, not checked
Commit: ff7858b53b2df1c4a55533bd7b3067e42bf97375
Parent: 556b7ff
2 files changed, +111 insertions, -5 deletions
M Cargo.toml +1 -1
@@ -19,7 +19,7 @@
19 19 # property whose value fails to substitute takes the whole declaration with it
20 20 # rather than degrading, so that failure is invisible until something looks
21 21 # wrong on screen.
22 - makeover = "2.7"
22 + makeover = "2.8"
23 23 # The exact patch, not the minor: the same discipline 0.13.1 was released for.
24 24 # An earlier patch in a minor can pin an older makeover-touch, which drags a
25 25 # second makeover-geometry into the graph alongside the one below and fails to
M src/lib.rs +110 -4
@@ -62,6 +62,12 @@
62 62 /// constant rather than on a string typed in two repositories.
63 63 pub use makeover::{WEBFONT_MONO_FILE, WEBFONT_SANS_FILE};
64 64
65 + /// Layer 0 of the font model, re-exported for the same one-dependency reason.
66 + ///
67 + /// A build script composing an override needs all four names and has no other
68 + /// reason to depend on `makeover` directly.
69 + pub use makeover::{FontFace, FontOverride, FontSlot, Typography};
70 +
65 71 /// Write the themes `makeover` ships into `dir`, as `<id>.toml`.
66 72 ///
67 73 /// Clears stale `.toml` files first, so a theme removed or renamed upstream
@@ -165,15 +171,45 @@
165 171 ///
166 172 /// If the file cannot be written.
167 173 pub fn typography_css(path: impl AsRef<Path>, font_url: &str) {
174 + typography_css_from(path, &makeover::Typography::house(font_url));
175 + }
176 +
177 + /// [`typography_css`], for a product that overrides a slot.
178 + ///
179 + /// Layer 0 of the font model. A product with a brand face declares it here,
180 + /// once, and the generated sheet carries both the `@font-face` and the token —
181 + /// which is what replaces the hand-maintained `@font-face` block plus a
182 + /// `--font-heading` nothing else in the tree knew about:
183 + ///
184 + /// ```no_run
185 + /// use makeover_build::{FontFace, FontOverride, FontSlot, Typography};
186 + ///
187 + /// makeover_build::typography_css_from(
188 + /// "static/typography.css",
189 + /// &Typography::house("/static/fonts").with_override(
190 + /// FontOverride::new(FontSlot::Display, "\"Young Serif\", serif")
191 + /// .with_face(FontFace::new("Young Serif", ["ysrf.woff2", "ysrf.ttf"])),
192 + /// ),
193 + /// );
194 + /// ```
195 + ///
196 + /// The product still ships the face itself, exactly as it does for the house
197 + /// two: this writes the CSS that fetches it and cannot produce a font.
198 + ///
199 + /// # Panics
200 + ///
201 + /// If the file cannot be written.
202 + pub fn typography_css_from(path: impl AsRef<Path>, typography: &makeover::Typography) {
168 203 let mut css = String::from(
169 204 "/* Generated by makeover-build from makeover. Do not edit.\n \
170 205 Two needs, two names, then a system generic. The faces are cut by\n \
171 206 quasi-type from Atkinson Hyperlegible plus the house glyph set, and\n \
172 207 both are variable over wght 200-800 in one file, which is why the\n \
173 - @font-face rules name the range. The mono face opens at ExtraLight. */\n\n",
208 + @font-face rules name the range. The mono face opens at ExtraLight.\n \
209 + A third token here is this product's own brand face, declared as an\n \
210 + override in its build script. */\n\n",
174 211 );
175 - css.push_str(&makeover::font_face_css(font_url));
176 - css.push_str(&makeover::typography_css_vars());
212 + css.push_str(&typography.css());
177 213 std::fs::write(path, css).expect("write typography css");
178 214 }
179 215
@@ -194,13 +230,42 @@
194 230 manifest_dir: impl AsRef<Path>,
195 231 opts: &makeover_webview::Emit,
196 232 explicit_touch: Option<&str>,
233 + ) {
234 + tauri_frontend_with(
235 + manifest_dir,
236 + opts,
237 + explicit_touch,
238 + &makeover::Typography::house("../fonts"),
239 + );
240 + }
241 +
242 + /// [`tauri_frontend`], for a product that overrides a font slot.
243 + ///
244 + /// Separate rather than a fourth parameter on `tauri_frontend` so the three
245 + /// consumers already calling it do not have to move: goingson is held at an
246 + /// older `makeover` by a theming decision unrelated to fonts, and a signature
247 + /// change here would make a font feature it cannot take into a build break it
248 + /// cannot avoid.
249 + ///
250 + /// The base URL is the caller's: pass `Typography::house("../fonts")` unless
251 + /// the app serves fonts from somewhere other than the one layout a Tauri
252 + /// frontend has.
253 + ///
254 + /// # Panics
255 + ///
256 + /// If any file cannot be written.
257 + pub fn tauri_frontend_with(
258 + manifest_dir: impl AsRef<Path>,
259 + opts: &makeover_webview::Emit,
260 + explicit_touch: Option<&str>,
261 + typography: &makeover::Typography,
197 262 ) {
198 263 let root = manifest_dir.as_ref();
199 264 let css = root.join("frontend").join("css");
200 265 themes(root.join("themes"));
201 266 geometry_css(css.join("geometry.css"), explicit_touch);
202 267 layout_css(css.join("layout.css"), opts);
203 - typography_css(css.join("typography.css"), "../fonts");
268 + typography_css_from(css.join("typography.css"), typography);
204 269 }
205 270
206 271 #[cfg(test)]
@@ -284,6 +349,47 @@
284 349 assert!(!css.contains("@layer"));
285 350 }
286 351
352 + #[test]
353 + fn an_overridden_slot_reaches_the_same_file_as_the_house_two() {
354 + // Layer 0's whole point: the brand face stops being a hand-maintained
355 + // `@font-face` in the app's own stylesheet and becomes a line in the
356 + // generated one, beside the slots it sits next to.
357 + let dir = scratch("typography-override");
358 + let path = dir.join("typography.css");
359 + typography_css_from(
360 + &path,
361 + &Typography::house("/static/fonts").with_override(
362 + FontOverride::new(FontSlot::Display, "\"Young Serif\", serif")
363 + .with_face(FontFace::new("Young Serif", ["ysrf.woff2", "ysrf.ttf"])),
364 + ),
365 + );
366 + let css = std::fs::read_to_string(&path).unwrap();
367 +
368 + // `@font-face {`, not `@font-face`: the header comment names the
369 + // at-rule too, and counting that would make this pass for the wrong
370 + // reason the day the comment is reworded.
371 + assert_eq!(css.matches("@font-face {").count(), 3);
372 + assert!(css.contains("--font-display: \"Young Serif\", serif;"));
373 + assert!(css.contains("--font-mono: \"Quasi Mono\", monospace;"));
374 + assert!(css.contains("url(\"/static/fonts/ysrf.ttf\") format(\"truetype\")"));
375 + assert!(!css.contains("@layer"));
376 + }
377 +
378 + #[test]
379 + fn the_default_tauri_layout_is_the_house_layer_and_nothing_else() {
380 + // `tauri_frontend` delegating through `tauri_frontend_with` must not
381 + // change a byte for the three consumers already calling it.
382 + let dir = scratch("tauri-default");
383 + let plain = dir.join("plain.css");
384 + let house = dir.join("house.css");
385 + typography_css(&plain, "../fonts");
386 + typography_css_from(&house, &Typography::house("../fonts"));
387 + assert_eq!(
388 + std::fs::read_to_string(&plain).unwrap(),
389 + std::fs::read_to_string(&house).unwrap()
390 + );
391 + }
392 +
287 393 #[test]
288 394 fn the_geometry_file_carries_the_crates_policy_and_a_banner() {
289 395 // The policy itself is tested in makeover-geometry. What is this