use std::collections::HashMap;
use unicode_segmentation::UnicodeSegmentation;
use crate::Stroke;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Glyph(String);
impl Glyph {
#[must_use]
pub fn new(text: &str) -> Option<Self> {
let mut clusters = text.graphemes(true);
clusters.next()?;
if clusters.next().is_some() || text.chars().any(char::is_control) {
return None;
}
Some(Self(text.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Symbol {
DirectionUp,
DirectionRight,
DirectionDown,
DirectionLeft,
Diamond,
Hexagon,
Circle,
}
impl Symbol {
pub const ALL: [Symbol; 7] = [
Symbol::DirectionUp,
Symbol::DirectionRight,
Symbol::DirectionDown,
Symbol::DirectionLeft,
Symbol::Diamond,
Symbol::Hexagon,
Symbol::Circle,
];
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SymbolStyleName(pub String);
impl From<&str> for SymbolStyleName {
fn from(name: &str) -> Self {
Self(name.to_owned())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SymbolCell {
pub symbol: Symbol,
pub style: SymbolStyleName,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GlyphKey {
pub top: Option<Stroke>,
pub right: Option<Stroke>,
pub bottom: Option<Stroke>,
pub left: Option<Stroke>,
}
pub struct GlyphCatalog {
arms: HashMap<GlyphKey, Glyph>,
symbols: HashMap<SymbolCell, Glyph>,
}
type Row = (
Option<&'static str>,
Option<&'static str>,
Option<&'static str>,
Option<&'static str>,
&'static str,
);
const LIGHT: &[Row] = &[
(None, None, Some("light"), None, "│"),
(None, None, Some("light"), Some("light"), "┐"),
(None, Some("light"), Some("light"), Some("light"), "┬"),
(
Some("light"),
Some("light"),
Some("light"),
Some("light"),
"┼",
),
(Some("light"), None, Some("light"), Some("light"), "┤"),
(None, Some("light"), Some("light"), None, "┌"),
(Some("light"), Some("light"), Some("light"), None, "├"),
(Some("light"), None, Some("light"), None, "│"),
(None, None, None, Some("light"), "─"),
(None, Some("light"), None, Some("light"), "─"),
(Some("light"), Some("light"), None, Some("light"), "┴"),
(Some("light"), None, None, Some("light"), "┘"),
(None, Some("light"), None, None, "─"),
(Some("light"), Some("light"), None, None, "└"),
(Some("light"), None, None, None, "│"),
];
const SOLID: &[(Symbol, &str)] = &[
(Symbol::DirectionUp, "▲"),
(Symbol::DirectionRight, "▶"),
(Symbol::DirectionDown, "▼"),
(Symbol::DirectionLeft, "◀"),
(Symbol::Diamond, "◆"),
(Symbol::Hexagon, "⬢"),
(Symbol::Circle, "●"),
];
impl GlyphCatalog {
#[must_use]
pub fn from_rules(rules: impl IntoIterator<Item = (GlyphKey, Glyph)>) -> Self {
let mut map = HashMap::new();
for (key, glyph) in rules {
map.entry(key).or_insert(glyph);
}
Self {
arms: map,
symbols: HashMap::new(),
}
}
#[must_use]
pub fn from_symbol_rules(rules: impl IntoIterator<Item = (SymbolCell, Glyph)>) -> Self {
let mut map = HashMap::new();
for (key, glyph) in rules {
map.entry(key).or_insert(glyph);
}
Self {
arms: HashMap::new(),
symbols: map,
}
}
#[must_use]
pub fn union(catalogs: impl IntoIterator<Item = Self>) -> Self {
let mut arms = HashMap::new();
let mut symbols = HashMap::new();
for catalog in catalogs {
for (key, glyph) in catalog.arms {
arms.entry(key).or_insert(glyph);
}
for (key, glyph) in catalog.symbols {
symbols.entry(key).or_insert(glyph);
}
}
Self { arms, symbols }
}
#[must_use]
pub fn light() -> Self {
Self::from_rules(LIGHT.iter().map(|&(top, right, bottom, left, glyph)| {
let key = GlyphKey {
top: top.map(Stroke::from),
right: right.map(Stroke::from),
bottom: bottom.map(Stroke::from),
left: left.map(Stroke::from),
};
let glyph = Glyph::new(glyph)
.unwrap_or_else(|| panic!("Light table row {key:?} is not a valid glyph"));
(key, glyph)
}))
}
#[must_use]
pub fn solid() -> Self {
Self::from_symbol_rules(SOLID.iter().map(|&(symbol, glyph)| {
let key = SymbolCell {
symbol,
style: SymbolStyleName::from("solid"),
};
let glyph = Glyph::new(glyph)
.unwrap_or_else(|| panic!("Solid table row {key:?} is not a valid glyph"));
(key, glyph)
}))
}
#[must_use]
pub fn glyph(&self, key: &GlyphKey) -> Option<&Glyph> {
self.arms.get(key)
}
#[must_use]
pub fn symbol(&self, key: &SymbolCell) -> Option<&Glyph> {
self.symbols.get(key)
}
#[must_use]
pub fn symbol_styles(&self) -> Vec<&SymbolStyleName> {
let mut styles: Vec<&SymbolStyleName> = self.symbols.keys().map(|key| &key.style).collect();
styles.sort();
styles.dedup();
styles
}
}
#[cfg(test)]
mod tests {
use super::{Glyph, GlyphCatalog, GlyphKey, Symbol, SymbolCell, SymbolStyleName};
use crate::Stroke;
#[test]
fn a_single_character_is_accepted_and_reads_back_unchanged() {
assert_eq!(Glyph::new("│").as_ref().map(Glyph::as_str), Some("│"));
}
#[test]
fn empty_text_is_refused() {
assert_eq!(Glyph::new(""), None);
}
#[test]
fn more_than_one_character_is_refused() {
assert_eq!(Glyph::new("ab"), None);
}
#[test]
fn a_control_character_is_refused() {
assert_eq!(Glyph::new("\n"), None);
}
#[test]
fn a_lone_format_character_is_accepted() {
assert!(Glyph::new("\u{200D}").is_some());
}
#[test]
fn a_cluster_built_from_several_code_points_is_accepted() {
assert!(Glyph::new("e\u{301}").is_some());
}
#[test]
fn a_regional_indicator_pair_is_accepted() {
assert!(Glyph::new("🇦🇷").is_some());
}
#[test]
fn a_control_character_is_refused_even_inside_one_cluster() {
assert_eq!(Glyph::new("\r\n"), None);
}
#[test]
fn two_normal_forms_of_the_same_letter_compare_unequal() {
let composed = Glyph::new("é").expect("é is one glyph");
let decomposed = Glyph::new("e\u{301}").expect("e followed by U+0301 is one glyph");
assert_ne!(composed, decomposed);
}
#[test]
fn an_exact_key_answers_the_light_table() {
let catalog = GlyphCatalog::light();
let key = GlyphKey {
top: Some(Stroke::from("light")),
right: None,
bottom: Some(Stroke::from("light")),
left: None,
};
assert_eq!(catalog.glyph(&key).map(Glyph::as_str), Some("│"));
}
#[test]
fn light_answers_every_non_empty_combination_of_its_own_stroke() {
let catalog = GlyphCatalog::light();
for mask in 1u8..16 {
let side = |bit: u8| {
if mask & bit == 0 {
None
} else {
Some(Stroke::from("light"))
}
};
let key = GlyphKey {
top: side(0b0001),
right: side(0b0010),
bottom: side(0b0100),
left: side(0b1000),
};
assert!(catalog.glyph(&key).is_some(), "no glyph for {key:?}");
}
}
#[test]
fn the_empty_key_has_no_answer_in_the_light_table() {
let catalog = GlyphCatalog::light();
let key = GlyphKey {
top: None,
right: None,
bottom: None,
left: None,
};
assert_eq!(catalog.glyph(&key), None);
}
fn key_with_top(stroke: &str) -> GlyphKey {
GlyphKey {
top: Some(Stroke::from(stroke)),
right: None,
bottom: None,
left: None,
}
}
#[test]
fn a_key_only_one_catalog_holds_answers_the_same_regardless_of_union_order() {
let mine_key = key_with_top("mine");
let mine = Glyph::new("m").expect("\"m\" is one glyph");
let other_key = key_with_top("other");
let other = Glyph::new("o").expect("\"o\" is one glyph");
let mine_catalog = GlyphCatalog::from_rules([(mine_key.clone(), mine.clone())]);
let other_catalog = GlyphCatalog::from_rules([(other_key.clone(), other.clone())]);
let mine_first = GlyphCatalog::union([
GlyphCatalog::from_rules([(mine_key.clone(), mine.clone())]),
GlyphCatalog::from_rules([(other_key.clone(), other.clone())]),
]);
let other_first = GlyphCatalog::union([other_catalog, mine_catalog]);
assert_eq!(mine_first.glyph(&mine_key), Some(&mine));
assert_eq!(mine_first.glyph(&other_key), Some(&other));
assert_eq!(other_first.glyph(&mine_key), Some(&mine));
assert_eq!(other_first.glyph(&other_key), Some(&other));
}
#[test]
fn a_key_two_catalogs_both_claim_answers_from_whichever_came_first() {
let key = key_with_top("shared");
let first = Glyph::new("1").expect("\"1\" is one glyph");
let second = Glyph::new("2").expect("\"2\" is one glyph");
let first_wins = GlyphCatalog::union([
GlyphCatalog::from_rules([(key.clone(), first.clone())]),
GlyphCatalog::from_rules([(key.clone(), second.clone())]),
]);
let second_wins = GlyphCatalog::union([
GlyphCatalog::from_rules([(key.clone(), second.clone())]),
GlyphCatalog::from_rules([(key.clone(), first.clone())]),
]);
assert_eq!(first_wins.glyph(&key), Some(&first));
assert_eq!(second_wins.glyph(&key), Some(&second));
}
#[test]
fn an_empty_rules_iterator_produces_a_catalog_with_no_answers() {
let catalog = GlyphCatalog::from_rules(std::iter::empty());
assert_eq!(catalog.glyph(&key_with_top("anything")), None);
}
#[test]
fn a_union_of_no_catalogs_produces_a_catalog_with_no_answers() {
let catalog = GlyphCatalog::union(std::iter::empty());
assert_eq!(catalog.glyph(&key_with_top("anything")), None);
}
#[test]
fn a_union_of_a_single_catalog_answers_the_same_as_that_catalog_alone() {
let key = key_with_top("solo");
let glyph = Glyph::new("s").expect("\"s\" is one glyph");
let alone = GlyphCatalog::from_rules([(key.clone(), glyph.clone())]);
let merged =
GlyphCatalog::union([GlyphCatalog::from_rules([(key.clone(), glyph.clone())])]);
assert_eq!(alone.glyph(&key), merged.glyph(&key));
assert_eq!(merged.glyph(&key), Some(&glyph));
}
fn a_symbol(symbol: Symbol, style: &str) -> SymbolCell {
SymbolCell {
symbol,
style: SymbolStyleName::from(style),
}
}
#[test]
fn union_merges_both_maps_and_holds_two_styles_of_one_symbol() {
let arm_key = key_with_top("shared-arm");
let arm = Glyph::new("│").expect("\"│\" is one glyph");
let solid = a_symbol(Symbol::DirectionUp, "solid");
let outline = a_symbol(Symbol::DirectionUp, "outline");
let solid_glyph = Glyph::new("▲").expect("\"▲\" is one glyph");
let outline_glyph = Glyph::new("△").expect("\"△\" is one glyph");
let arms_only = GlyphCatalog::from_rules([(arm_key.clone(), arm.clone())]);
let symbols_only = GlyphCatalog::from_symbol_rules([
(solid.clone(), solid_glyph.clone()),
(outline.clone(), outline_glyph.clone()),
]);
assert_eq!(arms_only.symbol(&solid), None, "an arm rule is not a pair");
assert_eq!(
symbols_only.glyph(&arm_key),
None,
"a symbol rule is not a key of arms"
);
let merged = GlyphCatalog::union([arms_only, symbols_only]);
assert_eq!(merged.glyph(&arm_key), Some(&arm));
assert_eq!(merged.symbol(&solid), Some(&solid_glyph));
assert_eq!(merged.symbol(&outline), Some(&outline_glyph));
}
#[test]
fn a_pair_two_catalogs_both_claim_answers_from_whichever_came_first() {
let key = a_symbol(Symbol::Diamond, "shared");
let first = Glyph::new("1").expect("\"1\" is one glyph");
let second = Glyph::new("2").expect("\"2\" is one glyph");
let first_wins = GlyphCatalog::union([
GlyphCatalog::from_symbol_rules([(key.clone(), first.clone())]),
GlyphCatalog::from_symbol_rules([(key.clone(), second.clone())]),
]);
let second_wins = GlyphCatalog::union([
GlyphCatalog::from_symbol_rules([(key.clone(), second.clone())]),
GlyphCatalog::from_symbol_rules([(key.clone(), first.clone())]),
]);
assert_eq!(first_wins.symbol(&key), Some(&first));
assert_eq!(second_wins.symbol(&key), Some(&second));
}
#[test]
fn solid_answers_all_seven_symbols() {
let catalog = GlyphCatalog::solid();
let drawn: String = Symbol::ALL
.iter()
.filter_map(|symbol| catalog.symbol(&a_symbol(*symbol, "solid")))
.map(Glyph::as_str)
.collect();
assert_eq!(drawn, "▲▶▼◀◆⬢●");
}
#[test]
fn symbol_styles_reports_each_style_once_in_name_order() {
let catalog = GlyphCatalog::from_symbol_rules([
(
a_symbol(Symbol::Diamond, "zulu"),
Glyph::new("z").expect("\"z\" is one glyph"),
),
(
a_symbol(Symbol::Diamond, "alpha"),
Glyph::new("a").expect("\"a\" is one glyph"),
),
(
a_symbol(Symbol::Circle, "alpha"),
Glyph::new("A").expect("\"A\" is one glyph"),
),
(
a_symbol(Symbol::Circle, "mike"),
Glyph::new("m").expect("\"m\" is one glyph"),
),
]);
let styles: Vec<&str> = catalog
.symbol_styles()
.iter()
.map(|style| style.0.as_str())
.collect();
assert_eq!(styles, ["alpha", "mike", "zulu"]);
}
}