1use std::collections::HashMap;
12
13use unicode_segmentation::UnicodeSegmentation;
14
15use crate::Stroke;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct Glyph(String);
32
33impl Glyph {
34 #[must_use]
37 pub fn new(text: &str) -> Option<Self> {
38 let mut clusters = text.graphemes(true);
39 clusters.next()?;
40 if clusters.next().is_some() || text.chars().any(char::is_control) {
41 return None;
42 }
43
44 Some(Self(text.to_owned()))
45 }
46
47 #[must_use]
49 pub fn as_str(&self) -> &str {
50 &self.0
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
60pub enum Symbol {
61 DirectionUp,
63 DirectionRight,
65 DirectionDown,
67 DirectionLeft,
69 Diamond,
71 Hexagon,
73 Circle,
75}
76
77impl Symbol {
78 pub const ALL: [Symbol; 7] = [
80 Symbol::DirectionUp,
81 Symbol::DirectionRight,
82 Symbol::DirectionDown,
83 Symbol::DirectionLeft,
84 Symbol::Diamond,
85 Symbol::Hexagon,
86 Symbol::Circle,
87 ];
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
96pub struct SymbolStyleName(pub String);
97
98impl From<&str> for SymbolStyleName {
99 fn from(name: &str) -> Self {
100 Self(name.to_owned())
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Hash)]
111pub struct SymbolCell {
112 pub symbol: Symbol,
114 pub style: SymbolStyleName,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Hash)]
120pub struct GlyphKey {
121 pub top: Option<Stroke>,
123 pub right: Option<Stroke>,
125 pub bottom: Option<Stroke>,
127 pub left: Option<Stroke>,
129}
130
131pub struct GlyphCatalog {
139 arms: HashMap<GlyphKey, Glyph>,
141 symbols: HashMap<SymbolCell, Glyph>,
143}
144
145type Row = (
150 Option<&'static str>,
151 Option<&'static str>,
152 Option<&'static str>,
153 Option<&'static str>,
154 &'static str,
155);
156
157const LIGHT: &[Row] = &[
160 (None, None, Some("light"), None, "│"),
161 (None, None, Some("light"), Some("light"), "┐"),
162 (None, Some("light"), Some("light"), Some("light"), "┬"),
163 (
164 Some("light"),
165 Some("light"),
166 Some("light"),
167 Some("light"),
168 "┼",
169 ),
170 (Some("light"), None, Some("light"), Some("light"), "┤"),
171 (None, Some("light"), Some("light"), None, "┌"),
172 (Some("light"), Some("light"), Some("light"), None, "├"),
173 (Some("light"), None, Some("light"), None, "│"),
174 (None, None, None, Some("light"), "─"),
175 (None, Some("light"), None, Some("light"), "─"),
176 (Some("light"), Some("light"), None, Some("light"), "┴"),
177 (Some("light"), None, None, Some("light"), "┘"),
178 (None, Some("light"), None, None, "─"),
179 (Some("light"), Some("light"), None, None, "└"),
180 (Some("light"), None, None, None, "│"),
181];
182
183const SOLID: &[(Symbol, &str)] = &[
186 (Symbol::DirectionUp, "▲"),
187 (Symbol::DirectionRight, "▶"),
188 (Symbol::DirectionDown, "▼"),
189 (Symbol::DirectionLeft, "◀"),
190 (Symbol::Diamond, "◆"),
191 (Symbol::Hexagon, "⬢"),
192 (Symbol::Circle, "●"),
193];
194
195impl GlyphCatalog {
196 #[must_use]
199 pub fn from_rules(rules: impl IntoIterator<Item = (GlyphKey, Glyph)>) -> Self {
200 let mut map = HashMap::new();
201 for (key, glyph) in rules {
202 map.entry(key).or_insert(glyph);
203 }
204
205 Self {
206 arms: map,
207 symbols: HashMap::new(),
208 }
209 }
210
211 #[must_use]
214 pub fn from_symbol_rules(rules: impl IntoIterator<Item = (SymbolCell, Glyph)>) -> Self {
215 let mut map = HashMap::new();
216 for (key, glyph) in rules {
217 map.entry(key).or_insert(glyph);
218 }
219
220 Self {
221 arms: HashMap::new(),
222 symbols: map,
223 }
224 }
225
226 #[must_use]
229 pub fn union(catalogs: impl IntoIterator<Item = Self>) -> Self {
230 let mut arms = HashMap::new();
231 let mut symbols = HashMap::new();
232 for catalog in catalogs {
233 for (key, glyph) in catalog.arms {
234 arms.entry(key).or_insert(glyph);
235 }
236 for (key, glyph) in catalog.symbols {
237 symbols.entry(key).or_insert(glyph);
238 }
239 }
240
241 Self { arms, symbols }
242 }
243
244 #[must_use]
254 pub fn light() -> Self {
255 Self::from_rules(LIGHT.iter().map(|&(top, right, bottom, left, glyph)| {
256 let key = GlyphKey {
257 top: top.map(Stroke::from),
258 right: right.map(Stroke::from),
259 bottom: bottom.map(Stroke::from),
260 left: left.map(Stroke::from),
261 };
262 let glyph = Glyph::new(glyph)
263 .unwrap_or_else(|| panic!("Light table row {key:?} is not a valid glyph"));
264 (key, glyph)
265 }))
266 }
267
268 #[must_use]
278 pub fn solid() -> Self {
279 Self::from_symbol_rules(SOLID.iter().map(|&(symbol, glyph)| {
280 let key = SymbolCell {
281 symbol,
282 style: SymbolStyleName::from("solid"),
283 };
284 let glyph = Glyph::new(glyph)
285 .unwrap_or_else(|| panic!("Solid table row {key:?} is not a valid glyph"));
286 (key, glyph)
287 }))
288 }
289
290 #[must_use]
292 pub fn glyph(&self, key: &GlyphKey) -> Option<&Glyph> {
293 self.arms.get(key)
294 }
295
296 #[must_use]
302 pub fn symbol(&self, key: &SymbolCell) -> Option<&Glyph> {
303 self.symbols.get(key)
304 }
305
306 #[must_use]
311 pub fn symbol_styles(&self) -> Vec<&SymbolStyleName> {
312 let mut styles: Vec<&SymbolStyleName> = self.symbols.keys().map(|key| &key.style).collect();
313 styles.sort();
314 styles.dedup();
315
316 styles
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::{Glyph, GlyphCatalog, GlyphKey, Symbol, SymbolCell, SymbolStyleName};
323 use crate::Stroke;
324
325 #[test]
328 fn a_single_character_is_accepted_and_reads_back_unchanged() {
329 assert_eq!(Glyph::new("│").as_ref().map(Glyph::as_str), Some("│"));
330 }
331
332 #[test]
334 fn empty_text_is_refused() {
335 assert_eq!(Glyph::new(""), None);
336 }
337
338 #[test]
341 fn more_than_one_character_is_refused() {
342 assert_eq!(Glyph::new("ab"), None);
343 }
344
345 #[test]
347 fn a_control_character_is_refused() {
348 assert_eq!(Glyph::new("\n"), None);
349 }
350
351 #[test]
354 fn a_lone_format_character_is_accepted() {
355 assert!(Glyph::new("\u{200D}").is_some());
356 }
357
358 #[test]
361 fn a_cluster_built_from_several_code_points_is_accepted() {
362 assert!(Glyph::new("e\u{301}").is_some());
363 }
364
365 #[test]
368 fn a_regional_indicator_pair_is_accepted() {
369 assert!(Glyph::new("🇦🇷").is_some());
370 }
371
372 #[test]
375 fn a_control_character_is_refused_even_inside_one_cluster() {
376 assert_eq!(Glyph::new("\r\n"), None);
377 }
378
379 #[test]
382 fn two_normal_forms_of_the_same_letter_compare_unequal() {
383 let composed = Glyph::new("é").expect("é is one glyph");
384 let decomposed = Glyph::new("e\u{301}").expect("e followed by U+0301 is one glyph");
385
386 assert_ne!(composed, decomposed);
387 }
388
389 #[test]
392 fn an_exact_key_answers_the_light_table() {
393 let catalog = GlyphCatalog::light();
394 let key = GlyphKey {
395 top: Some(Stroke::from("light")),
396 right: None,
397 bottom: Some(Stroke::from("light")),
398 left: None,
399 };
400
401 assert_eq!(catalog.glyph(&key).map(Glyph::as_str), Some("│"));
402 }
403
404 #[test]
407 fn light_answers_every_non_empty_combination_of_its_own_stroke() {
408 let catalog = GlyphCatalog::light();
409
410 for mask in 1u8..16 {
411 let side = |bit: u8| {
412 if mask & bit == 0 {
413 None
414 } else {
415 Some(Stroke::from("light"))
416 }
417 };
418 let key = GlyphKey {
419 top: side(0b0001),
420 right: side(0b0010),
421 bottom: side(0b0100),
422 left: side(0b1000),
423 };
424
425 assert!(catalog.glyph(&key).is_some(), "no glyph for {key:?}");
426 }
427 }
428
429 #[test]
430 fn the_empty_key_has_no_answer_in_the_light_table() {
431 let catalog = GlyphCatalog::light();
432 let key = GlyphKey {
433 top: None,
434 right: None,
435 bottom: None,
436 left: None,
437 };
438
439 assert_eq!(catalog.glyph(&key), None);
440 }
441
442 fn key_with_top(stroke: &str) -> GlyphKey {
445 GlyphKey {
446 top: Some(Stroke::from(stroke)),
447 right: None,
448 bottom: None,
449 left: None,
450 }
451 }
452
453 #[test]
456 fn a_key_only_one_catalog_holds_answers_the_same_regardless_of_union_order() {
457 let mine_key = key_with_top("mine");
458 let mine = Glyph::new("m").expect("\"m\" is one glyph");
459 let other_key = key_with_top("other");
460 let other = Glyph::new("o").expect("\"o\" is one glyph");
461 let mine_catalog = GlyphCatalog::from_rules([(mine_key.clone(), mine.clone())]);
462 let other_catalog = GlyphCatalog::from_rules([(other_key.clone(), other.clone())]);
463
464 let mine_first = GlyphCatalog::union([
465 GlyphCatalog::from_rules([(mine_key.clone(), mine.clone())]),
466 GlyphCatalog::from_rules([(other_key.clone(), other.clone())]),
467 ]);
468 let other_first = GlyphCatalog::union([other_catalog, mine_catalog]);
469
470 assert_eq!(mine_first.glyph(&mine_key), Some(&mine));
471 assert_eq!(mine_first.glyph(&other_key), Some(&other));
472 assert_eq!(other_first.glyph(&mine_key), Some(&mine));
473 assert_eq!(other_first.glyph(&other_key), Some(&other));
474 }
475
476 #[test]
479 fn a_key_two_catalogs_both_claim_answers_from_whichever_came_first() {
480 let key = key_with_top("shared");
481 let first = Glyph::new("1").expect("\"1\" is one glyph");
482 let second = Glyph::new("2").expect("\"2\" is one glyph");
483
484 let first_wins = GlyphCatalog::union([
485 GlyphCatalog::from_rules([(key.clone(), first.clone())]),
486 GlyphCatalog::from_rules([(key.clone(), second.clone())]),
487 ]);
488 let second_wins = GlyphCatalog::union([
489 GlyphCatalog::from_rules([(key.clone(), second.clone())]),
490 GlyphCatalog::from_rules([(key.clone(), first.clone())]),
491 ]);
492
493 assert_eq!(first_wins.glyph(&key), Some(&first));
494 assert_eq!(second_wins.glyph(&key), Some(&second));
495 }
496
497 #[test]
499 fn an_empty_rules_iterator_produces_a_catalog_with_no_answers() {
500 let catalog = GlyphCatalog::from_rules(std::iter::empty());
501
502 assert_eq!(catalog.glyph(&key_with_top("anything")), None);
503 }
504
505 #[test]
507 fn a_union_of_no_catalogs_produces_a_catalog_with_no_answers() {
508 let catalog = GlyphCatalog::union(std::iter::empty());
509
510 assert_eq!(catalog.glyph(&key_with_top("anything")), None);
511 }
512
513 #[test]
515 fn a_union_of_a_single_catalog_answers_the_same_as_that_catalog_alone() {
516 let key = key_with_top("solo");
517 let glyph = Glyph::new("s").expect("\"s\" is one glyph");
518 let alone = GlyphCatalog::from_rules([(key.clone(), glyph.clone())]);
519 let merged =
520 GlyphCatalog::union([GlyphCatalog::from_rules([(key.clone(), glyph.clone())])]);
521
522 assert_eq!(alone.glyph(&key), merged.glyph(&key));
523 assert_eq!(merged.glyph(&key), Some(&glyph));
524 }
525
526 fn a_symbol(symbol: Symbol, style: &str) -> SymbolCell {
529 SymbolCell {
530 symbol,
531 style: SymbolStyleName::from(style),
532 }
533 }
534
535 #[test]
538 fn union_merges_both_maps_and_holds_two_styles_of_one_symbol() {
539 let arm_key = key_with_top("shared-arm");
540 let arm = Glyph::new("│").expect("\"│\" is one glyph");
541 let solid = a_symbol(Symbol::DirectionUp, "solid");
542 let outline = a_symbol(Symbol::DirectionUp, "outline");
543 let solid_glyph = Glyph::new("▲").expect("\"▲\" is one glyph");
544 let outline_glyph = Glyph::new("△").expect("\"△\" is one glyph");
545
546 let arms_only = GlyphCatalog::from_rules([(arm_key.clone(), arm.clone())]);
547 let symbols_only = GlyphCatalog::from_symbol_rules([
548 (solid.clone(), solid_glyph.clone()),
549 (outline.clone(), outline_glyph.clone()),
550 ]);
551 assert_eq!(arms_only.symbol(&solid), None, "an arm rule is not a pair");
552 assert_eq!(
553 symbols_only.glyph(&arm_key),
554 None,
555 "a symbol rule is not a key of arms"
556 );
557
558 let merged = GlyphCatalog::union([arms_only, symbols_only]);
559
560 assert_eq!(merged.glyph(&arm_key), Some(&arm));
561 assert_eq!(merged.symbol(&solid), Some(&solid_glyph));
562 assert_eq!(merged.symbol(&outline), Some(&outline_glyph));
563 }
564
565 #[test]
568 fn a_pair_two_catalogs_both_claim_answers_from_whichever_came_first() {
569 let key = a_symbol(Symbol::Diamond, "shared");
570 let first = Glyph::new("1").expect("\"1\" is one glyph");
571 let second = Glyph::new("2").expect("\"2\" is one glyph");
572
573 let first_wins = GlyphCatalog::union([
574 GlyphCatalog::from_symbol_rules([(key.clone(), first.clone())]),
575 GlyphCatalog::from_symbol_rules([(key.clone(), second.clone())]),
576 ]);
577 let second_wins = GlyphCatalog::union([
578 GlyphCatalog::from_symbol_rules([(key.clone(), second.clone())]),
579 GlyphCatalog::from_symbol_rules([(key.clone(), first.clone())]),
580 ]);
581
582 assert_eq!(first_wins.symbol(&key), Some(&first));
583 assert_eq!(second_wins.symbol(&key), Some(&second));
584 }
585
586 #[test]
588 fn solid_answers_all_seven_symbols() {
589 let catalog = GlyphCatalog::solid();
590
591 let drawn: String = Symbol::ALL
592 .iter()
593 .filter_map(|symbol| catalog.symbol(&a_symbol(*symbol, "solid")))
594 .map(Glyph::as_str)
595 .collect();
596
597 assert_eq!(drawn, "▲▶▼◀◆⬢●");
598 }
599
600 #[test]
603 fn symbol_styles_reports_each_style_once_in_name_order() {
604 let catalog = GlyphCatalog::from_symbol_rules([
605 (
606 a_symbol(Symbol::Diamond, "zulu"),
607 Glyph::new("z").expect("\"z\" is one glyph"),
608 ),
609 (
610 a_symbol(Symbol::Diamond, "alpha"),
611 Glyph::new("a").expect("\"a\" is one glyph"),
612 ),
613 (
614 a_symbol(Symbol::Circle, "alpha"),
615 Glyph::new("A").expect("\"A\" is one glyph"),
616 ),
617 (
618 a_symbol(Symbol::Circle, "mike"),
619 Glyph::new("m").expect("\"m\" is one glyph"),
620 ),
621 ]);
622
623 let styles: Vec<&str> = catalog
624 .symbol_styles()
625 .iter()
626 .map(|style| style.0.as_str())
627 .collect();
628
629 assert_eq!(styles, ["alpha", "mike", "zulu"]);
630 }
631}