use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct ProtrusionFactors {
pub start: f64,
pub end: f64,
}
impl ProtrusionFactors {
pub const fn new(start: f64, end: f64) -> Self {
Self { start, end }
}
pub const fn end_only(end: f64) -> Self {
Self { start: 0.0, end }
}
pub const fn start_only(start: f64) -> Self {
Self { start, end: 0.0 }
}
pub const fn symmetric(factor: f64) -> Self {
Self { start: factor, end: factor }
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ProtrusionTable {
entries: HashMap<char, ProtrusionFactors>,
}
impl ProtrusionTable {
pub fn new() -> Self {
Self::default()
}
pub fn with_entry(mut self, ch: char, factors: ProtrusionFactors) -> Self {
self.entries.insert(ch, factors);
self
}
pub fn get(&self, ch: char) -> ProtrusionFactors {
self.entries.get(&ch).copied().unwrap_or_default()
}
pub fn default_punctuation() -> Self {
Self::new()
.with_entry('.', ProtrusionFactors::end_only(1.0))
.with_entry(',', ProtrusionFactors::end_only(1.0))
.with_entry('-', ProtrusionFactors::symmetric(0.5))
.with_entry(':', ProtrusionFactors::end_only(0.2))
.with_entry(';', ProtrusionFactors::end_only(0.2))
.with_entry('\u{0027}', ProtrusionFactors::symmetric(0.5)) .with_entry('\u{0022}', ProtrusionFactors::symmetric(0.5)) .with_entry('\u{2018}', ProtrusionFactors::start_only(0.5)) .with_entry('\u{2019}', ProtrusionFactors::end_only(0.5)) .with_entry('\u{201C}', ProtrusionFactors::start_only(0.5)) .with_entry('\u{201D}', ProtrusionFactors::end_only(0.5)) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_table_protrudes_nothing() {
let table = ProtrusionTable::new();
assert_eq!(table.get('.'), ProtrusionFactors::default());
assert_eq!(table.get('.').start, 0.0);
assert_eq!(table.get('.').end, 0.0);
}
#[test]
fn with_entry_overrides_a_single_character() {
let table = ProtrusionTable::new().with_entry('!', ProtrusionFactors::end_only(0.75));
assert_eq!(table.get('!'), ProtrusionFactors::end_only(0.75));
assert_eq!(table.get('?'), ProtrusionFactors::default(), "an unset character stays at zero protrusion");
}
#[test]
fn factors_constructors_set_exactly_the_named_side() {
assert_eq!(ProtrusionFactors::end_only(0.5), ProtrusionFactors { start: 0.0, end: 0.5 });
assert_eq!(ProtrusionFactors::start_only(0.5), ProtrusionFactors { start: 0.5, end: 0.0 });
assert_eq!(ProtrusionFactors::symmetric(0.5), ProtrusionFactors { start: 0.5, end: 0.5 });
}
#[test]
fn default_punctuation_gives_period_and_comma_full_end_hang() {
let table = ProtrusionTable::default_punctuation();
assert_eq!(table.get('.'), ProtrusionFactors::end_only(1.0));
assert_eq!(table.get(','), ProtrusionFactors::end_only(1.0));
}
#[test]
fn default_punctuation_gives_directional_curly_quotes_the_correct_side() {
let table = ProtrusionTable::default_punctuation();
assert!(table.get('\u{2018}').start > 0.0 && table.get('\u{2018}').end == 0.0, "opening single quote hangs only at start");
assert!(table.get('\u{2019}').end > 0.0 && table.get('\u{2019}').start == 0.0, "closing single quote hangs only at end");
assert!(table.get('\u{201C}').start > 0.0 && table.get('\u{201C}').end == 0.0, "opening double quote hangs only at start");
assert!(table.get('\u{201D}').end > 0.0 && table.get('\u{201D}').start == 0.0, "closing double quote hangs only at end");
}
#[test]
fn default_punctuation_ascii_quotes_are_symmetric() {
let table = ProtrusionTable::default_punctuation();
let dq = table.get('"');
let sq = table.get('\'');
assert_eq!(dq.start, dq.end, "ASCII double quote is glyph-ambiguous — both sides get the same factor");
assert_eq!(sq.start, sq.end, "ASCII apostrophe/quote is glyph-ambiguous — both sides get the same factor");
assert!(dq.start > 0.0 && sq.start > 0.0);
}
#[test]
fn default_punctuation_covers_every_character_this_task_named() {
let table = ProtrusionTable::default_punctuation();
for ch in ['.', ',', '-', ':', ';', '\'', '"'] {
assert!(table.get(ch) != ProtrusionFactors::default(), "'{ch}' must have a non-zero protrusion entry");
}
}
}