Skip to main content

max / quasi-type

One call for a consumer: quasi_type::cut shop cuts its bundled face from a build.rs and Alloy's image cuts both faces into /usr/share/fonts. Neither should reassemble the steps `main` walks: a second caller writing its own version string or skipping the coverage assertion is how two builds of the same slot stop being the same face. So the pipeline is one function, `main` calls it too, and there is one path. The cache directory is a parameter rather than a constant, since a consumer's is not this repo's - a build.rs wants OUT_DIR and a checkout wants bases/cache. Also: the licence is written as `OFL-<Family>.txt`. Both slots write into the same directory and their bases are two different licence files, so `OFL.txt` had Quasi Body overwriting Quasi Mono's. goingson already names Reglo's this way.
Author: Max Johnson <me@maxj.phd> · 2026-08-17 15:02 UTC
Signed with PGP, not checked
Commit: d6994c00582f6a18909dc5e93d8d71829ebef5ac
Parent: 9288815
4 files changed, +180 insertions, -79 deletions
M README.md +19
@@ -50,6 +50,25 @@
50 50 - `out/` — built faces. Not committed: they rebuild from the checkout, and a
51 51 committed binary is a second source of truth.
52 52
53 + ## Using it from another crate
54 +
55 + A consumer cuts the face rather than vendoring it, for the same reason `out/` is
56 + not committed. `quasi_type::cut` is the whole pipeline in one call:
57 +
58 + ```rust
59 + // build.rs
60 + let out = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
61 + let cut = quasi_type::cut("quasi-mono", &out.join("bases"), false).unwrap();
62 + std::fs::write(out.join("QuasiMono.ttf"), &cut.faces[0].ttf).unwrap();
63 + ```
64 +
65 + Take it as a git dependency, and give it a cache directory: bases are downloaded
66 + and checksummed there, so a build with a warm cache needs no network.
67 +
68 + Do not reassemble the steps `build` walks. A second caller writing its own
69 + version string or skipping the coverage assertion is how two builds of the same
70 + slot stop being the same face.
71 +
53 72 ```
54 73 quasi-type build <slot> cut every face of a slot, verify, write to out/
55 74 quasi-type verify <slot> cut without writing; assert coverage only
M src/lib.rs +112
@@ -172,3 +172,115 @@
172 172 }
173 173
174 174 impl std::error::Error for Error {}
175 +
176 + /// One cut face, ready to write or to embed.
177 + pub struct Face {
178 + /// The style the pin declares, which is the base's default instance.
179 + pub style: String,
180 + /// The file name without an extension: `QuasiMono[wght]` for a variable
181 + /// face, `QuasiMono-Regular` for a static one. A face is named for its axis
182 + /// when it has one, the way upstream names the file it came from.
183 + pub stem: String,
184 + pub ttf: Vec<u8>,
185 + pub woff2: Vec<u8>,
186 + /// What the cut added to the base, as `(codepoint, glyph name)`.
187 + pub added: Vec<(u32, String)>,
188 + pub variation: Option<base::Variation>,
189 + /// The coverage verdict, already asserted. Carried so a caller can print it
190 + /// rather than re-derive it.
191 + pub verdict: String,
192 + }
193 +
194 + /// A slot, cut.
195 + pub struct Cut {
196 + pub family: String,
197 + pub base_family: String,
198 + pub base_version: String,
199 + /// How many glyphs of the house set this slot selected.
200 + pub selected: usize,
201 + pub faces: Vec<Face>,
202 + /// The base's licence text, which OFL requires to travel with the build.
203 + pub license: Vec<u8>,
204 + }
205 +
206 + /// Cut a slot from its pinned base: the whole pipeline, in one call.
207 + ///
208 + /// This is what a consumer runs. `shop` cuts its bundled face from a `build.rs`
209 + /// and Alloy's image cuts both faces into `/usr/share/fonts`, and neither should
210 + /// have to reassemble the steps `main.rs` walks — a second caller doing its own
211 + /// version string or skipping the coverage assertion is how two builds of the
212 + /// same slot stop being the same face.
213 + ///
214 + /// `cache` is where pinned bases are downloaded to and verified in. It is a
215 + /// parameter rather than a constant because a consumer's is not this repo's:
216 + /// a `build.rs` wants `OUT_DIR`, and a checkout wants `bases/cache`.
217 + pub fn cut(slot_id: &str, cache: &std::path::Path, offline: bool) -> Result<Cut, Error> {
218 + let pins = pins::Pins::parse(PINS)?;
219 + let manifest = manifest::Manifest::parse(HOUSE_SET)?;
220 + let slot = pins.slot(slot_id)?;
221 + let base_pin = pins.base(&slot.base)?;
222 +
223 + // The licence gate, before anything is built: an output name may not carry
224 + // the base's Reserved Font Name.
225 + base_pin.check_output_name(&slot.family)?;
226 +
227 + let selected: Vec<&manifest::GlyphSpec> = manifest
228 + .glyphs
229 + .iter()
230 + .filter(|g| slot.glyphs.includes(g))
231 + .collect();
232 + let house: Vec<u32> = selected.iter().map(|g| g.codepoint).collect();
233 + // The floor is the role's, and the slot's own selection is what says which
234 + // role it fills: a slot that takes no cell furniture is not the face that
235 + // will answer `monospace`, so asking it for `│ █ ▏ ░` asks it to carry
236 + // glyphs that cannot tile in it.
237 + let floor: &[u32] = if selected.iter().any(|g| g.shape.is_cell_furniture()) {
238 + &assert::ALLOY_SURFACE
239 + } else {
240 + &assert::BODY_SURFACE
241 + };
242 +
243 + let version = format!("{}.{}", manifest.set.version, base_pin.version);
244 + let mut faces = Vec::new();
245 + for face in base::load(base_pin, cache, offline)? {
246 + let id = compose::Identity {
247 + family: &slot.family,
248 + style: &face.style,
249 + version: &version,
250 + set_version: manifest.set.version,
251 + base: base_pin,
252 + };
253 + let built = compose::build(&face.bytes, &selected, &id)?;
254 + let verdict = assert::describe(&assert::check(
255 + floor,
256 + &compose::coverage(&built.bytes)?,
257 + &house,
258 + ))?;
259 + let stem = match &built.variation {
260 + Some(axis) => format!("{}[{}]", slot.family.replace(' ', ""), axis.tag),
261 + None => format!(
262 + "{}-{}",
263 + slot.family.replace(' ', ""),
264 + face.style.replace(' ', "")
265 + ),
266 + };
267 + faces.push(Face {
268 + style: face.style,
269 + stem,
270 + woff2: woff2::encode(&built.bytes)?,
271 + ttf: built.bytes,
272 + added: built.added,
273 + variation: built.variation,
274 + verdict,
275 + });
276 + }
277 +
278 + Ok(Cut {
279 + family: slot.family.clone(),
280 + base_family: base_pin.family.clone(),
281 + base_version: base_pin.version.clone(),
282 + selected: selected.len(),
283 + license: base::license_text(base_pin, cache, offline)?,
284 + faces,
285 + })
286 + }
M src/main.rs +22 -79
@@ -8,7 +8,7 @@
8 8 use quasi_type::manifest::{Manifest, format_codepoint};
9 9 use quasi_type::pins::Pins;
10 10 use quasi_type::proof;
11 - use quasi_type::{Error, HOUSE_SET, PINS, assert, woff2};
11 + use quasi_type::{Error, HOUSE_SET, PINS};
12 12
13 13 const USAGE: &str = "\
14 14 quasi-type — a pinned base plus the house glyph set in, a Quasi <Slot> face out
@@ -138,11 +138,11 @@
138 138 }
139 139 Some((&"build", tail)) => {
140 140 let slot_id = tail.first().copied().unwrap_or("quasi-mono");
141 - cut(&root, &out, &pins, &manifest, slot_id, offline, true)
141 + cut(&root, &out, slot_id, offline, true)
142 142 }
143 143 Some((&"verify", tail)) => {
144 144 let slot_id = tail.first().copied().unwrap_or("quasi-mono");
145 - cut(&root, &out, &pins, &manifest, slot_id, offline, false)
145 + cut(&root, &out, slot_id, offline, false)
146 146 }
147 147 Some((&"proof", tail)) => {
148 148 let slot_id = tail.first().copied().unwrap_or("quasi-mono");
@@ -155,86 +155,27 @@
155 155 }
156 156 }
157 157
158 - fn cut(
159 - root: &Path,
160 - out: &Path,
161 - pins: &Pins,
162 - manifest: &Manifest,
163 - slot_id: &str,
164 - offline: bool,
165 - write: bool,
166 - ) -> Result<(), Error> {
167 - let slot = pins.slot(slot_id)?;
168 - let base_pin = pins.base(&slot.base)?;
169 -
170 - // The licence gate, before anything is built: an output name may not carry
171 - // the base's Reserved Font Name.
172 - base_pin.check_output_name(&slot.family)?;
173 -
174 - let selected: Vec<&quasi_type::manifest::GlyphSpec> = manifest
175 - .glyphs
176 - .iter()
177 - .filter(|g| slot.glyphs.includes(g))
178 - .collect();
179 - let house: Vec<u32> = selected.iter().map(|g| g.codepoint).collect();
180 - // The floor is the role's, and the slot's own selection is what says which
181 - // role it fills: a slot that takes no cell furniture is not the face that
182 - // will answer `monospace`, so asking it for `│ █ ▏ ░` asks it to carry
183 - // glyphs that cannot tile in it.
184 - let floor: &[u32] = if selected.iter().any(|g| g.shape.is_cell_furniture()) {
185 - &assert::ALLOY_SURFACE
186 - } else {
187 - &assert::BODY_SURFACE
188 - };
189 -
190 - let cache = base::cache_dir(root);
191 - let faces = base::load(base_pin, &cache, offline)?;
192 - let version = format!("{}.{}", manifest.set.version, base_pin.version);
158 + fn cut(root: &Path, out: &Path, slot_id: &str, offline: bool, write: bool) -> Result<(), Error> {
159 + let cut = quasi_type::cut(slot_id, &base::cache_dir(root), offline)?;
193 160
194 161 println!(
195 162 "{} from {} {} ({} marks)",
196 - slot.family,
197 - base_pin.family,
198 - base_pin.version,
199 - selected.len()
163 + cut.family, cut.base_family, cut.base_version, cut.selected
200 164 );
201 -
202 165 if write {
203 166 std::fs::create_dir_all(out).map_err(|e| Error::Io(out.to_path_buf(), e))?;
204 167 }
205 168
206 - for face in &faces {
207 - let id = Identity {
208 - family: &slot.family,
209 - style: &face.style,
210 - version: &version,
211 - set_version: manifest.set.version,
212 - base: base_pin,
213 - };
214 - let built = compose::build(&face.bytes, &selected, &id)?;
215 - let coverage = assert::check(floor, &compose::coverage(&built.bytes)?, &house);
216 - let verdict = assert::describe(&coverage)?;
217 - let web = woff2::encode(&built.bytes)?;
218 -
219 - // A variable face is named for its axis rather than for a style, the way
220 - // upstream names the file it came from. One face covers the range, and
221 - // the name says so before anyone opens it.
222 - let stem = match &built.variation {
223 - Some(axis) => format!("{}[{}]", slot.family.replace(' ', ""), axis.tag),
224 - None => format!(
225 - "{}-{}",
226 - slot.family.replace(' ', ""),
227 - face.style.replace(' ', "")
228 - ),
229 - };
169 + for face in &cut.faces {
230 170 println!(
231 - " {:<22} ttf {:>7} woff2 {:>7} +{} marks, {verdict}",
232 - format!("{} {}", slot.family, face.style),
233 - human(built.bytes.len()),
234 - human(web.len()),
235 - built.added.len(),
171 + " {:<22} ttf {:>7} woff2 {:>7} +{} marks, {}",
172 + format!("{} {}", cut.family, face.style),
173 + human(face.ttf.len()),
174 + human(face.woff2.len()),
175 + face.added.len(),
176 + face.verdict,
236 177 );
237 - if let Some(axis) = &built.variation {
178 + if let Some(axis) = &face.variation {
238 179 println!(
239 180 " {:<22} {} {}-{}, and the marks vary with it. At rest this face is {} {} \
240 181 ({}), so a consumer names the weight it wants: `font-weight: {} {}`.",
@@ -249,10 +190,9 @@
249 190 axis.max,
250 191 );
251 192 }
252 -
253 193 if write {
254 - write_file(&out.join(format!("{stem}.ttf")), &built.bytes)?;
255 - write_file(&out.join(format!("{stem}.woff2")), &web)?;
194 + write_file(&out.join(format!("{}.ttf", face.stem)), &face.ttf)?;
195 + write_file(&out.join(format!("{}.woff2", face.stem)), &face.woff2)?;
256 196 }
257 197 }
258 198
@@ -260,9 +200,12 @@
260 200 // OFL 1.1 requires the licence to travel with a modified build, and the
261 201 // gap this closes is live: MNW serves three families with no licence
262 202 // beside them today.
263 - let license = base::license_text(base_pin, &cache, offline)?;
264 - write_file(&out.join("OFL.txt"), &license)?;
265 - println!(" {:<22} the base's licence, as OFL requires", "OFL.txt");
203 + // Named for the family rather than `OFL.txt`, because two slots write
204 + // into the same directory and their bases are two different licence
205 + // files. goingson already does it this way for Reglo.
206 + let license = format!("OFL-{}.txt", cut.family.replace(' ', ""));
207 + write_file(&out.join(&license), &cut.license)?;
208 + println!(" {license:<22} the base's licence, as OFL requires");
266 209 println!("\nwritten to {}", out.display());
267 210 }
268 211 Ok(())
@@ -149,3 +149,30 @@
149 149 "the base's advances do not vary, so this test is asserting nothing"
150 150 );
151 151 }
152 +
153 + /// The consumer entry point, which is the only path a consumer should take.
154 + ///
155 + /// `shop` cuts its bundled face from a `build.rs` and Alloy's image cuts both
156 + /// faces into `/usr/share/fonts`; both call `quasi_type::cut`. What is asserted
157 + /// here is that it produces the same face `build` writes, licence and all, so
158 + /// there is one pipeline rather than one per caller.
159 + #[test]
160 + fn the_consumer_entry_point_cuts_the_same_face() {
161 + let cache = base::cache_dir(&root());
162 + let Ok(whole) = quasi_type::cut(SLOT, &cache, true) else {
163 + eprintln!("skipped: {SLOT}'s base is not cached");
164 + return;
165 + };
166 + let Some((direct, _)) = cut() else { return };
167 +
168 + assert_eq!(whole.family, "Quasi Body");
169 + assert_eq!(whole.faces.len(), 1);
170 + let face = &whole.faces[0];
171 + assert_eq!(face.ttf, direct, "the entry point cuts a different face");
172 + assert_eq!(face.stem, "QuasiBody[wght]");
173 + assert!(!face.woff2.is_empty() && &face.woff2[0..4] == b"wOF2");
174 + assert!(
175 + String::from_utf8_lossy(&whole.license).contains("Open Font License"),
176 + "the licence has to travel with the build"
177 + );
178 + }