1use std::{fmt, rc::Rc};
2
3use libxml::tree::Node;
4
5use crate::{
6 common::{error::Result, font::Font},
7 document::Document,
8};
9
10pub type LigatureClosure = Rc<dyn Fn(&str) -> String>;
11pub type FontTestClosure = Rc<dyn Fn(&Font) -> bool>;
12pub type LigatureMatcher =
13 Rc<dyn Fn(&mut Document, &mut Node) -> Result<Option<(usize, String, MathLigatureOptions)>>>;
14
15#[derive(Debug, Default, Clone, PartialEq, Eq)]
16pub struct MathLigatureOptions {
17 pub role: Option<String>,
18 pub name: Option<String>,
19 pub meaning: Option<String>,
20}
21
22impl MathLigatureOptions {
23 pub fn sorted_each(&self) -> [(&str, Option<&String>); 3] {
24 [
25 ("meaning", self.meaning.as_ref()),
26 ("name", self.name.as_ref()),
27 ("role", self.role.as_ref()),
28 ]
29 }
30}
31
32#[derive(Clone, Default)]
33pub struct Ligature {
34 pub id: usize,
35 pub regex: Option<String>,
36 pub code: Option<LigatureClosure>,
37 pub font_test: Option<FontTestClosure>,
38 pub matcher: Option<LigatureMatcher>,
39}
40
41impl fmt::Debug for Ligature {
42 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self.regex) }
43}
44
45impl PartialEq for Ligature {
46 fn eq(&self, other: &Ligature) -> bool { self.id == other.id }
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52
53 #[test]
54 fn math_ligature_options_default_is_none_triple() {
55 let m = MathLigatureOptions::default();
56 assert!(m.role.is_none());
57 assert!(m.name.is_none());
58 assert!(m.meaning.is_none());
59 }
60
61 #[test]
62 fn math_ligature_options_equality() {
63 let a = MathLigatureOptions {
64 role: Some("ADDOP".into()),
65 name: None,
66 meaning: Some("plus".into()),
67 };
68 let b = MathLigatureOptions {
69 role: Some("ADDOP".into()),
70 name: None,
71 meaning: Some("plus".into()),
72 };
73 let c = MathLigatureOptions {
74 role: Some("RELOP".into()), name: None,
76 meaning: Some("plus".into()),
77 };
78 assert_eq!(a, b);
79 assert_ne!(a, c);
80 }
81
82 #[test]
83 fn math_ligature_sorted_each_fixed_order() {
84 let m = MathLigatureOptions {
86 role: Some("r".into()),
87 name: Some("n".into()),
88 meaning: Some("m".into()),
89 };
90 let ordered = m.sorted_each();
91 assert_eq!(ordered[0].0, "meaning");
92 assert_eq!(ordered[1].0, "name");
93 assert_eq!(ordered[2].0, "role");
94 }
95
96 #[test]
97 fn math_ligature_sorted_each_none_values_preserved() {
98 let m = MathLigatureOptions::default();
100 let ordered = m.sorted_each();
101 for (_, v) in ordered {
102 assert!(v.is_none());
103 }
104 }
105
106 #[test]
107 fn ligature_default_has_zero_id_and_none_fields() {
108 let l = Ligature::default();
109 assert_eq!(l.id, 0);
110 assert!(l.regex.is_none());
111 assert!(l.code.is_none());
112 assert!(l.font_test.is_none());
113 assert!(l.matcher.is_none());
114 }
115
116 #[test]
117 fn ligature_equality_by_id_only() {
118 let mut a = Ligature::default();
119 let mut b = Ligature::default();
120 a.id = 1;
121 b.id = 1;
122 assert_eq!(a, b);
125 b.id = 2;
126 assert_ne!(a, b);
127 }
128
129 #[test]
130 fn ligature_debug_format_uses_regex() {
131 let l = Ligature {
134 regex: Some("test_regex".to_string()),
135 ..Default::default()
136 };
137 let out = format!("{l:?}");
138 assert!(out.contains("test_regex"), "got {out:?}");
139 }
140}