Skip to main content

max / quasi-type

cut_web: put the slots straight into a directory a server serves Three consumers need the same fifteen lines -- cut both slots, write the woff2 under the name makeover's @font-face fetches, carry the licence text -- and makeover-build cannot hold them, since this crate is publish = false and that one is on crates.io. So they live here, on the side that can be depended on. The filename stays the caller's: it is half of an agreement makeover holds the other half of, and inventing it here would make a third answer.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-17 16:19 UTC
Signed with PGP, not checked
Commit: e40330602e68fbdd2a9d2917bdec33a7ce573a1f
Parent: cbbb9e3
1 file changed, +53 insertions, -0 deletions
M src/lib.rs +53
@@ -284,3 +284,56 @@
284 284 faces,
285 285 })
286 286 }
287 +
288 + /// Cut slots straight into a directory a web server serves, as woff2.
289 + ///
290 + /// `slots` pairs a slot id with the filename it lands under, and the filename
291 + /// is the caller's because it is half of an agreement this crate cannot see:
292 + /// `makeover` emits the `@font-face` that fetches these, and its
293 + /// `WEBFONT_MONO_FILE` / `WEBFONT_SANS_FILE` are the other half. makeover
294 + /// cannot call this — it is on crates.io and this crate is `publish = false` —
295 + /// so the two meet in a consumer's build script, and this is the part of that
296 + /// meeting worth having once instead of three times.
297 + ///
298 + /// woff2 only. Every browser that can run a Make Creative frontend has
299 + /// supported it since 2018, so a ttf beside it is two to three times the bytes
300 + /// for a case that does not arrive.
301 + ///
302 + /// The licence text travels as `OFL-<Family>.txt` beside each face, which is
303 + /// what OFL requires of a build that ships the face.
304 + ///
305 + /// Nothing is rewritten if it is already byte-identical. A build script that
306 + /// rewrites its outputs every run bumps their mtimes, and everything watching
307 + /// those files then believes the fonts moved.
308 + ///
309 + /// # Errors
310 + ///
311 + /// If a slot cannot be cut, or a file cannot be written.
312 + pub fn cut_web(
313 + dir: &std::path::Path,
314 + cache: &std::path::Path,
315 + offline: bool,
316 + slots: &[(&str, &str)],
317 + ) -> Result<(), Error> {
318 + std::fs::create_dir_all(dir).map_err(|e| Error::Io(dir.to_path_buf(), e))?;
319 + for (slot_id, filename) in slots {
320 + let cut = cut(slot_id, cache, offline)?;
321 + let face = cut
322 + .faces
323 + .first()
324 + .ok_or_else(|| Error::Pins(format!("slot `{slot_id}` cut no faces")))?;
325 + write_if_changed(&dir.join(filename), &face.woff2)?;
326 + write_if_changed(
327 + &dir.join(format!("OFL-{}.txt", cut.family.replace(' ', ""))),
328 + &cut.license,
329 + )?;
330 + }
331 + Ok(())
332 + }
333 +
334 + fn write_if_changed(path: &std::path::Path, bytes: &[u8]) -> Result<(), Error> {
335 + if std::fs::read(path).is_ok_and(|existing| existing == bytes) {
336 + return Ok(());
337 + }
338 + std::fs::write(path, bytes).map_err(|e| Error::Io(path.to_path_buf(), e))
339 + }