From 3ae6b6124276b02542e56360982879e72fac2e22 Mon Sep 17 00:00:00 2001 From: Jany Belluz Date: Mon, 17 Nov 2025 19:05:23 +0000 Subject: [PATCH 1/5] Sort words in the report so the highest and lowest overall are first for each script + timestamp the report --- Cargo.lock | 175 +++++++++++++++++++++++++++++++++++++++++++- cli/Cargo.toml | 1 + cli/src/fmt/html.rs | 131 +++++++++++++++++++++++++-------- core/src/lib.rs | 82 +++++++++++++++++++++ 4 files changed, 357 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 10cd060..023c241 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,15 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.18" @@ -168,6 +177,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link 0.2.1", +] + [[package]] name = "clap" version = "4.5.27" @@ -225,6 +247,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "core_maths" version = "0.1.0" @@ -370,6 +398,7 @@ name = "fontheight-cli" version = "0.1.2" dependencies = [ "anyhow", + "chrono", "clap", "clap-verbosity-flag", "env_logger", @@ -433,6 +462,30 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "indexmap" version = "2.12.0" @@ -464,6 +517,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "kurbo" version = "0.12.0" @@ -714,6 +777,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "same-file" version = "1.0.6" @@ -939,6 +1008,51 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + [[package]] name = "webpki-roots" version = "0.25.4" @@ -954,12 +1068,71 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -1009,7 +1182,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index b57f40b..f012508 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -18,6 +18,7 @@ path = "src/main.rs" [dependencies] anyhow = "1" +chrono = "0.4.42" clap-verbosity-flag = { version = "3", features = ["log"] } fontheight = { version = "0.2", path = "../core" } harfrust.workspace = true diff --git a/cli/src/fmt/html.rs b/cli/src/fmt/html.rs index 8e09d2e..ede547f 100644 --- a/cli/src/fmt/html.rs +++ b/cli/src/fmt/html.rs @@ -1,6 +1,6 @@ use std::{ cell::RefCell, - collections::{BTreeMap, HashMap, hash_map::Entry}, + collections::{BTreeMap, HashMap, HashSet, hash_map::Entry}, fmt, fmt::Write, ops::Neg, @@ -8,7 +8,8 @@ use std::{ }; use anyhow::{Context, bail}; -use fontheight::{Location, Report, VerticalExtremes}; +use chrono::Utc; +use fontheight::{FlatReport, Location, Report, VerticalExtremes}; use harfrust::{ShaperData, ShaperInstance, UnicodeBuffer}; use harfshapedfa::{ HarfRustShaperExt, ShapingMeta, @@ -34,12 +35,28 @@ body { font-family: sans-serif; } +#timestamp { + position: fixed; + top: 0; + right: 0; + margin: 0; + background: white; + z-index: 1; +} + h1 { text-align: center; } details { margin: 4rem 0; + position: relative; +} + +summary { + position: sticky; + top: 0; + background: white; } summary h2 { @@ -554,33 +571,24 @@ fn draw_exemplar<'a>( fn format_script_reports<'a>( font_cache: Rc>>, script: &str, - reports: &[&Report<'a>], + reports: &[FlatReport<'a>], ) -> Markup { html! { details open { summary { h2 { (script) } } - @for report in reports { - @let location_cache = - Rc::new(RefCell::new(LocationCache::new(font_cache.borrow().font, report.location))); - ul.drawn { - @for high_exemplar in report.exemplars.highest() { - (draw_exemplar( - font_cache.clone(), - location_cache.clone(), - high_exemplar.word, - report.word_list, - report.location, - )) - } - @for low_exemplar in report.exemplars.lowest() { - (draw_exemplar( - font_cache.clone(), - location_cache.clone(), - low_exemplar.word, - report.word_list, - report.location, - )) - } + ul.drawn { + @for report in reports { + // TODO: make location_cache be a hash by location? + // Generating the HTML report is fast enough like this though. + @let location_cache = + Rc::new(RefCell::new(LocationCache::new(font_cache.borrow().font, report.location))); + (draw_exemplar( + font_cache.clone(), + location_cache.clone(), + report.extremes.word, + report.word_list, + report.location, + )) } } } @@ -591,18 +599,58 @@ pub fn format_all_reports( reports: &[Report], font: &FontRef, ) -> anyhow::Result { - // Group on script and then present exemplars from word lists in order by - // name - let mut script_exemplars = BTreeMap::<&str, Vec<&Report>>::new(); - reports.iter().for_each(|report| { + // Group on script and then present examplars by decreasing badness, with + // all locations and word lists of origin mixed together. + let mut script_exemplars = BTreeMap::<&str, Vec>::new(); + reports.iter().flat_map(Report::flatten).for_each(|report| { // ZWSP at the start of Unknown so it gets sorted last let script = report.word_list.script().unwrap_or("\u{200B}Unknown"); script_exemplars.entry(script).or_default().push(report); }); // Sort reports by name, then by location script_exemplars.values_mut().for_each(|reports| { - reports.sort_unstable_by(|report_a, report_b| { - Ord::cmp(report_a.word_list.name(), report_b.word_list.name()) + // Here we'll also be deduplicating words from different locations, + // keeping the badest one. Here the same word can show at most twice, + // once as demonstrating a highest extreme, and a second time for + // lowest. I was concerned that using the same dedup for high and low + // could miss a problem, if a word is highest in one location but not + // very low there, and lowest in another location (later in the list). + let mut dedup_high = HashSet::new(); + let mut dedup_low = HashSet::new(); + let mut highest = reports.clone(); + highest.sort_unstable_by(|report_a, report_b| { + // b cmp a, because above we want the biggest values first (desc) + report_b + .extremes + .highest_not_nan() + .cmp(&report_a.extremes.highest_not_nan()) + .then_with(|| { + Ord::cmp( + report_a.word_list.name(), + report_b.word_list.name(), + ) + }) + .then_with(|| { + PartialOrd::partial_cmp( + &report_a.location, + &report_b.location, + ) + .expect("fontheight produced unsortable locations") + }) + }); + let mut lowest = reports.clone(); + lowest.sort_unstable_by(|report_a, report_b| { + // a cmp b, because below we want the smallest values first (asc) + report_a + .extremes + .lowest_not_nan() + .cmp(&report_b.extremes.lowest_not_nan()) + .then_with(|| { + Ord::cmp( + report_a.word_list.name(), + report_b.word_list.name(), + ) + }) .then_with(|| { PartialOrd::partial_cmp( &report_a.location, @@ -611,6 +659,22 @@ pub fn format_all_reports( .expect("fontheight produced unsortable locations") }) }); + // Report high, low, high, low so that the first 2 words from each + // script are the worst high and low = makes skimming the report easy. + *reports = highest + .into_iter() + .zip(lowest) + .flat_map(|(high, low)| { + let mut res = vec![]; + if dedup_high.insert(high.extremes.word) { + res.push(high); + } + if dedup_low.insert(low.extremes.word) { + res.push(low) + } + res + }) + .collect(); }); let font_cache = Rc::new(RefCell::new(FontCache::new(font)?)); @@ -624,6 +688,11 @@ pub fn format_all_reports( style { (CSS) } } body { + // Timestamp the report to allow checking quickly that we're + // looking at the latest + p id="timestamp" { + (Utc::now().format("%Y-%m-%d %H:%M:%S").to_string()) + } h1 { "Font Height report" } h3 { "Lines legend" } p { diff --git a/core/src/lib.rs b/core/src/lib.rs index 9951820..a7f204f 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -470,6 +470,24 @@ impl WordExtremes<'_> { self.extremes.highest() } + /// The lowest/smaller extreme, in font units. NotNan for cmp. + /// + /// Sugar for [`VerticalExtremes::lowest_not_nan`]. + #[inline] + #[must_use] + pub fn lowest_not_nan(&self) -> NotNan { + self.extremes.lowest_not_nan() + } + + /// The highest/bigger extreme, in font units. NotNan for cmp. + /// + /// Sugar for [`VerticalExtremes::highest_not_nan`]. + #[inline] + #[must_use] + pub fn highest_not_nan(&self) -> NotNan { + self.extremes.highest_not_nan() + } + /// Get the `WordExtremes` that reaches the lowest. #[inline] #[must_use] @@ -576,6 +594,20 @@ impl VerticalExtremes { *self.highest } + /// The lowest/smaller extreme, in font units. NotNan for cmp. + #[inline] + #[must_use] + pub fn lowest_not_nan(&self) -> NotNan { + self.lowest + } + + /// The highest/bigger extreme, in font units. NotNan for cmp. + #[inline] + #[must_use] + pub fn highest_not_nan(&self) -> NotNan { + self.highest + } + /// Combine two `VerticalExtremes`, taking the higher `highest` value, and /// lower `lowest` value. #[inline] @@ -618,4 +650,54 @@ impl<'a> Report<'a> { exemplars, } } + + /// Produce a list of flat reports, each about a single word, for easy + /// sorting for HTML reporting. + pub fn flatten(&'a self) -> Vec> { + // Deduplicate words at this location and word_list (the same word might + // be both hitting low and high extremes) + self.exemplars + .lowest() + .iter() + .chain(self.exemplars.highest()) + .map(|word_extremes| (word_extremes.word, word_extremes)) + .collect::>() + .values() + .map(|word_extremes| { + FlatReport::new(self.location, self.word_list, **word_extremes) + }) + .collect() + } +} + +/// For reporting purposes, the user might want to see the lowest and highest +/// words regardless of their location or word list of origin. +#[derive(Debug, Clone)] +pub struct FlatReport<'a> { + /// The [`Location`] the exemplars were found at. + pub location: &'a Location, + /// The [`WordList`] that was shaped. + /// + /// This will always be the full word list, even if only part of it was + /// tested. + pub word_list: &'a WordList, + /// One word that reached an extreme + pub extremes: WordExtremes<'a>, +} + +impl<'a> FlatReport<'a> { + /// Create a new report from its fields. + #[inline] + #[must_use] + pub const fn new( + location: &'a Location, + word_list: &'a WordList, + extremes: WordExtremes<'a>, + ) -> Self { + FlatReport { + location, + word_list, + extremes, + } + } } From a4b2dc669134f796cd79ae7517b068e27f9cf04f Mon Sep 17 00:00:00 2001 From: Ricky Atkins Date: Wed, 19 Nov 2025 10:29:06 +0000 Subject: [PATCH 2/5] Golf --- cli/src/fmt/html.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/cli/src/fmt/html.rs b/cli/src/fmt/html.rs index ede547f..3ce8c7f 100644 --- a/cli/src/fmt/html.rs +++ b/cli/src/fmt/html.rs @@ -661,20 +661,18 @@ pub fn format_all_reports( }); // Report high, low, high, low so that the first 2 words from each // script are the worst high and low = makes skimming the report easy. - *reports = highest - .into_iter() - .zip(lowest) - .flat_map(|(high, low)| { - let mut res = vec![]; + *reports = highest.into_iter().zip(lowest).fold( + Vec::new(), + |mut acc, (high, low)| { if dedup_high.insert(high.extremes.word) { - res.push(high); + acc.push(high); } if dedup_low.insert(low.extremes.word) { - res.push(low) + acc.push(low) } - res - }) - .collect(); + acc + }, + ) }); let font_cache = Rc::new(RefCell::new(FontCache::new(font)?)); From ea0a199d5fa1dd48ddbdc27dfb318623e2b02a0d Mon Sep 17 00:00:00 2001 From: Ricky Atkins Date: Mon, 26 Jan 2026 12:56:18 +0000 Subject: [PATCH 3/5] Move FlatReport into CLI as WordReport --- cli/src/fmt/html.rs | 47 ++++++++++++++++++++++++++++++++++++++---- core/src/lib.rs | 50 --------------------------------------------- 2 files changed, 43 insertions(+), 54 deletions(-) diff --git a/cli/src/fmt/html.rs b/cli/src/fmt/html.rs index 3ce8c7f..7381136 100644 --- a/cli/src/fmt/html.rs +++ b/cli/src/fmt/html.rs @@ -9,7 +9,7 @@ use std::{ use anyhow::{Context, bail}; use chrono::Utc; -use fontheight::{FlatReport, Location, Report, VerticalExtremes}; +use fontheight::{Location, Report, VerticalExtremes, WordExtremes}; use harfrust::{ShaperData, ShaperInstance, UnicodeBuffer}; use harfshapedfa::{ HarfRustShaperExt, ShapingMeta, @@ -380,6 +380,45 @@ impl ShapingAccumulator { } } +/// A report about a single word, for easier sorting +#[derive(Debug, Clone)] +struct WordReport<'a> { + /// The [`Location`] the exemplars were found at. + location: &'a Location, + /// The [`WordList`] that was shaped. + /// + /// This will always be the full word list, even if only part of it was + /// tested. + word_list: &'a WordList, + /// One word that reached an extreme + extremes: WordExtremes<'a>, +} + +impl<'a> WordReport<'a> { + /// Produce a list of flat reports, each about a single word, for easy + /// sorting for HTML reporting. + fn new(report: &'a Report<'a>) -> Vec { + // Deduplicate words at this location and word_list (the same word might + // be both hitting low and high extremes) + report + .exemplars + .lowest() + .iter() + .chain(report.exemplars.highest()) + .copied() + .map(|word_extremes| (word_extremes.word, word_extremes)) + .collect::>() + .values() + .copied() + .map(|word_extremes| WordReport { + location: report.location, + word_list: report.word_list, + extremes: word_extremes, + }) + .collect() + } +} + fn draw_svg<'a>( font_cache: Rc>>, location_cache: Rc>, @@ -571,7 +610,7 @@ fn draw_exemplar<'a>( fn format_script_reports<'a>( font_cache: Rc>>, script: &str, - reports: &[FlatReport<'a>], + reports: &[WordReport<'a>], ) -> Markup { html! { details open { @@ -601,8 +640,8 @@ pub fn format_all_reports( ) -> anyhow::Result { // Group on script and then present examplars by decreasing badness, with // all locations and word lists of origin mixed together. - let mut script_exemplars = BTreeMap::<&str, Vec>::new(); - reports.iter().flat_map(Report::flatten).for_each(|report| { + let mut script_exemplars = BTreeMap::<&str, Vec>::new(); + reports.iter().flat_map(WordReport::new).for_each(|report| { // ZWSP at the start of Unknown so it gets sorted last let script = report.word_list.script().unwrap_or("\u{200B}Unknown"); script_exemplars.entry(script).or_default().push(report); diff --git a/core/src/lib.rs b/core/src/lib.rs index a7f204f..6a48935 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -650,54 +650,4 @@ impl<'a> Report<'a> { exemplars, } } - - /// Produce a list of flat reports, each about a single word, for easy - /// sorting for HTML reporting. - pub fn flatten(&'a self) -> Vec> { - // Deduplicate words at this location and word_list (the same word might - // be both hitting low and high extremes) - self.exemplars - .lowest() - .iter() - .chain(self.exemplars.highest()) - .map(|word_extremes| (word_extremes.word, word_extremes)) - .collect::>() - .values() - .map(|word_extremes| { - FlatReport::new(self.location, self.word_list, **word_extremes) - }) - .collect() - } -} - -/// For reporting purposes, the user might want to see the lowest and highest -/// words regardless of their location or word list of origin. -#[derive(Debug, Clone)] -pub struct FlatReport<'a> { - /// The [`Location`] the exemplars were found at. - pub location: &'a Location, - /// The [`WordList`] that was shaped. - /// - /// This will always be the full word list, even if only part of it was - /// tested. - pub word_list: &'a WordList, - /// One word that reached an extreme - pub extremes: WordExtremes<'a>, -} - -impl<'a> FlatReport<'a> { - /// Create a new report from its fields. - #[inline] - #[must_use] - pub const fn new( - location: &'a Location, - word_list: &'a WordList, - extremes: WordExtremes<'a>, - ) -> Self { - FlatReport { - location, - word_list, - extremes, - } - } } From 947f39e4ea62a4a4f43f63c6df5544f0b3d18a22 Mon Sep 17 00:00:00 2001 From: Ricky Atkins Date: Mon, 26 Jan 2026 13:02:07 +0000 Subject: [PATCH 4/5] Avoid exposing ordered-float in public API --- cli/src/fmt/html.rs | 54 ++++++++++++++++++--------------------------- core/src/lib.rs | 32 --------------------------- 2 files changed, 22 insertions(+), 64 deletions(-) diff --git a/cli/src/fmt/html.rs b/cli/src/fmt/html.rs index 7381136..be8f9fa 100644 --- a/cli/src/fmt/html.rs +++ b/cli/src/fmt/html.rs @@ -659,44 +659,34 @@ pub fn format_all_reports( let mut highest = reports.clone(); highest.sort_unstable_by(|report_a, report_b| { // b cmp a, because above we want the biggest values first (desc) - report_b - .extremes - .highest_not_nan() - .cmp(&report_a.extremes.highest_not_nan()) - .then_with(|| { - Ord::cmp( - report_a.word_list.name(), - report_b.word_list.name(), - ) - }) - .then_with(|| { - PartialOrd::partial_cmp( - &report_a.location, - &report_b.location, - ) + f64::partial_cmp( + &report_b.extremes.highest(), + &report_a.extremes.highest(), + ) + .unwrap() // unwrap is safe because we know there are no NaNs + .then_with(|| { + Ord::cmp(report_a.word_list.name(), report_b.word_list.name()) + }) + .then_with(|| { + PartialOrd::partial_cmp(&report_a.location, &report_b.location) .expect("fontheight produced unsortable locations") - }) + }) }); let mut lowest = reports.clone(); lowest.sort_unstable_by(|report_a, report_b| { // a cmp b, because below we want the smallest values first (asc) - report_a - .extremes - .lowest_not_nan() - .cmp(&report_b.extremes.lowest_not_nan()) - .then_with(|| { - Ord::cmp( - report_a.word_list.name(), - report_b.word_list.name(), - ) - }) - .then_with(|| { - PartialOrd::partial_cmp( - &report_a.location, - &report_b.location, - ) + f64::partial_cmp( + &report_a.extremes.lowest(), + &report_b.extremes.lowest(), + ) + .unwrap() // unwrap is safe because we know there are no NaNs + .then_with(|| { + Ord::cmp(report_a.word_list.name(), report_b.word_list.name()) + }) + .then_with(|| { + PartialOrd::partial_cmp(&report_a.location, &report_b.location) .expect("fontheight produced unsortable locations") - }) + }) }); // Report high, low, high, low so that the first 2 words from each // script are the worst high and low = makes skimming the report easy. diff --git a/core/src/lib.rs b/core/src/lib.rs index 6a48935..9951820 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -470,24 +470,6 @@ impl WordExtremes<'_> { self.extremes.highest() } - /// The lowest/smaller extreme, in font units. NotNan for cmp. - /// - /// Sugar for [`VerticalExtremes::lowest_not_nan`]. - #[inline] - #[must_use] - pub fn lowest_not_nan(&self) -> NotNan { - self.extremes.lowest_not_nan() - } - - /// The highest/bigger extreme, in font units. NotNan for cmp. - /// - /// Sugar for [`VerticalExtremes::highest_not_nan`]. - #[inline] - #[must_use] - pub fn highest_not_nan(&self) -> NotNan { - self.extremes.highest_not_nan() - } - /// Get the `WordExtremes` that reaches the lowest. #[inline] #[must_use] @@ -594,20 +576,6 @@ impl VerticalExtremes { *self.highest } - /// The lowest/smaller extreme, in font units. NotNan for cmp. - #[inline] - #[must_use] - pub fn lowest_not_nan(&self) -> NotNan { - self.lowest - } - - /// The highest/bigger extreme, in font units. NotNan for cmp. - #[inline] - #[must_use] - pub fn highest_not_nan(&self) -> NotNan { - self.highest - } - /// Combine two `VerticalExtremes`, taking the higher `highest` value, and /// lower `lowest` value. #[inline] From 04b5fecde7b195cb75863f9d1c4d741578d66d63 Mon Sep 17 00:00:00 2001 From: Ricky Atkins Date: Mon, 26 Jan 2026 13:08:32 +0000 Subject: [PATCH 5/5] Use humantime instead of chrono --- Cargo.lock | 176 ++------------------------------------------ cli/Cargo.toml | 2 +- cli/src/fmt/html.rs | 5 +- 3 files changed, 9 insertions(+), 174 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 023c241..8a8d8d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,15 +23,6 @@ dependencies = [ "alloc-no-stdlib", ] -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anstream" version = "0.6.18" @@ -177,19 +168,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link 0.2.1", -] - [[package]] name = "clap" version = "4.5.27" @@ -247,12 +225,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "core_maths" version = "0.1.0" @@ -398,13 +370,13 @@ name = "fontheight-cli" version = "0.1.2" dependencies = [ "anyhow", - "chrono", "clap", "clap-verbosity-flag", "env_logger", "fontheight", "harfrust", "harfshapedfa", + "humantime", "log", "maud", "ordered-float", @@ -463,28 +435,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" +name = "humantime" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "indexmap" @@ -517,16 +471,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" -[[package]] -name = "js-sys" -version = "0.3.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - [[package]] name = "kurbo" version = "0.12.0" @@ -777,12 +721,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - [[package]] name = "same-file" version = "1.0.6" @@ -1008,51 +946,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "wasm-bindgen" -version = "0.2.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" -dependencies = [ - "unicode-ident", -] - [[package]] name = "webpki-roots" version = "0.25.4" @@ -1068,71 +961,12 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.2.1", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-link" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -1182,7 +1016,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link 0.1.3", + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index f012508..04616d5 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -18,11 +18,11 @@ path = "src/main.rs" [dependencies] anyhow = "1" -chrono = "0.4.42" clap-verbosity-flag = { version = "3", features = ["log"] } fontheight = { version = "0.2", path = "../core" } harfrust.workspace = true harfshapedfa.workspace = true +humantime = "2.3.0" log.workspace = true maud = "0.27.0" ordered-float.workspace = true diff --git a/cli/src/fmt/html.rs b/cli/src/fmt/html.rs index be8f9fa..c4af664 100644 --- a/cli/src/fmt/html.rs +++ b/cli/src/fmt/html.rs @@ -5,10 +5,10 @@ use std::{ fmt::Write, ops::Neg, rc::Rc, + time::SystemTime, }; use anyhow::{Context, bail}; -use chrono::Utc; use fontheight::{Location, Report, VerticalExtremes, WordExtremes}; use harfrust::{ShaperData, ShaperInstance, UnicodeBuffer}; use harfshapedfa::{ @@ -16,6 +16,7 @@ use harfshapedfa::{ convert::{iso639_to_opentype, iso15924_to_opentype}, pens::BoundsPen, }; +use humantime::format_rfc3339; use log::{debug, error}; use maud::{DOCTYPE, Escaper, Markup, PreEscaped, Render, html}; use ordered_float::NotNan; @@ -718,7 +719,7 @@ pub fn format_all_reports( // Timestamp the report to allow checking quickly that we're // looking at the latest p id="timestamp" { - (Utc::now().format("%Y-%m-%d %H:%M:%S").to_string()) + (format_rfc3339(SystemTime::now())) } h1 { "Font Height report" } h3 { "Lines legend" }