lib.rs

4.8 kB · rust · 160 lines

1#![doc = include_str!("../README.md")]2#![deny(missing_docs)]34/// The stroke-order writing animations of text.5pub mod animate;6/// The built font files the crate ships: json, ttf, woff, woff2.7pub mod assets;8/// The glyph well this crate pours.9pub mod data;10/// The raw bitmap tables of the font.11pub mod glyphs;12/// The glyph builders for uppers, lowers, digits, extras and specials.13pub mod letters;14mod models;15/// The Unicode names of the font's characters.16pub mod names;17/// The stroke orders that write each character.18pub mod paths;19/// The hand-penned stroke tables, one per glyph.20pub mod pens;21/// The 0/1 grid a text renders to.22pub mod raster;23/// The string, list and JSON forms of glyphs.24pub mod serializer;25/// The descenders and the trimming of bitmaps.26pub mod shape;2728use std::collections::BTreeMap;29use std::sync::OnceLock;3031pub use animate::{animate, cycle, merge, Anim, FPS, HOLD};32pub use letters::{all, digits, extras, lowers, specials, uppers};33pub use models::Glyph;34pub use names::name_of;35pub use paths::{draft, floor, path, strokes};36pub use raster::raster;37pub use serializer::{to_json, to_lists, to_strings};38pub use shape::{descends, trim, DESCENDERS};3940fn book() -> &'static BTreeMap<char, Glyph> {41    static BOOK: OnceLock<BTreeMap<char, Glyph>> = OnceLock::new();42    BOOK.get_or_init(|| all().into_iter().map(|g| (g.char, g)).collect())43}4445fn order() -> &'static Vec<char> {46    static ORDER: OnceLock<Vec<char>> = OnceLock::new();47    ORDER.get_or_init(|| all().iter().map(|g| g.char).collect())48}4950/// Returns an owned copy of the character's glyph, or None outside the font.51///52/// ```53/// let a = mrlyfont::glyph('a').unwrap();54/// assert_eq!(a.rows[0], "01110");55/// assert_eq!(mrlyfont::glyph('\u{6f22}'), None);56/// ```57pub fn glyph(c: char) -> Option<Glyph> {58    book().get(&c).cloned()59}6061/// Returns a borrowed glyph from the static book, or None outside the font.62pub fn look(c: char) -> Option<&'static Glyph> {63    book().get(&c)64}6566/// Returns every character in the font, in font order.67pub fn supported() -> Vec<char> {68    order().clone()69}7071/// Returns the whole font as a map from character to bitmap rows.72pub fn map() -> BTreeMap<char, Vec<String>> {73    all().into_iter().map(|g| (g.char, g.rows)).collect()74}7576#[cfg(test)]77mod tests {78    use super::*;79    const WORDMARK: &str = "MRLYPROD";8081    #[test]82    fn the_book_never_reorders_the_font() {83        let straight: Vec<char> = all().iter().map(|g| g.char).collect();84        assert_eq!(85            supported(),86            straight,87            "the font app seeds its order from supported()"88        );89        assert_eq!(supported().first(), Some(&'A'), "uppers still lead");90    }9192    #[test]93    fn the_cached_glyph_is_the_built_glyph() {94        for want in all() {95            assert_eq!(96                glyph(want.char).as_ref(),97                Some(&want),98                "{} drifted",99                want.char100            );101        }102        assert_eq!(glyph('\u{6f22}'), None);103    }104    #[test]105    fn mrlyprod_union_is_x() {106        let mut union = vec![vec!['0'; 5]; 5];107        for c in WORDMARK.chars() {108            let g = glyph(c).unwrap();109            for (y, row) in g.rows.iter().enumerate() {110                for (x, ch) in row.chars().enumerate() {111                    if ch == '1' {112                        union[y][x] = '1';113                    }114                }115            }116        }117        let folded: Vec<String> = union118            .into_iter()119            .map(|row| row.into_iter().collect())120            .collect();121        assert_eq!(folded, glyph('X').unwrap().rows);122    }123    #[test]124    fn lowercase_rounds_corners() {125        let a = glyph('a').unwrap();126        assert_eq!(a.rows, vec!["01110", "10001", "11111", "10001", "00000"]);127        assert_eq!(glyph('A').unwrap().rows[0], "11111");128    }129    #[test]130    fn trim_collapses_blank() {131        let space = glyph(' ').unwrap();132        assert_eq!(trim(&space.rows), vec!["0"; 5]);133    }134    #[test]135    fn trim_drops_edge_columns() {136        let rows = vec!["00100".to_string(), "00100".to_string()];137        assert_eq!(trim(&rows), vec!["1".to_string(), "1".to_string()]);138    }139    #[test]140    fn count_matches_layout() {141        assert_eq!(supported().len(), 108);142        assert_eq!(uppers().len(), 26);143        assert_eq!(lowers().len(), 26);144        assert_eq!(digits().len(), 10);145        assert_eq!(extras().len(), 42);146        assert_eq!(specials().len(), 4);147    }148    #[test]149    fn descenders_flagged() {150        assert!(descends('$'));151        assert!(descends('('));152        assert!(!descends('A'));153    }154    #[test]155    fn json_is_multiline_and_named() {156        let json = to_json(&all());157        assert!(json.contains("\"name\": \"LATIN CAPITAL LETTER A\""));158        assert!(json.contains("\"rows\": [\n"));159    }160}