Skip to main content

latexml_core/common/
muglue.rs

1use std::fmt;
2
3use super::glue::{FillCode, glue_string, new_setup, spec_setup};
4use crate::{
5  Object,
6  common::{dimension::attribute_format, numeric_ops::NumericOps},
7  definition::register::RegisterType,
8  token::{Catcode, Token},
9  tokens::Tokens,
10};
11
12#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
13pub struct MuGlue {
14  pub skip:  i64,
15  pub plus:  Option<i64>,
16  pub pfill: Option<FillCode>,
17  pub minus: Option<i64>,
18  pub mfill: Option<FillCode>,
19}
20
21impl fmt::Display for MuGlue {
22  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
23    let string = glue_string(
24      self.skip, self.plus, self.pfill, self.minus, self.mfill, "mu",
25    );
26    write!(f, "{string}")
27  }
28}
29
30impl NumericOps for MuGlue {
31  fn value_of(self) -> i64 { self.skip }
32  fn register_type(&self) -> RegisterType { RegisterType::MuGlue }
33  fn new(skip: i64) -> Self {
34    MuGlue {
35      skip,
36      plus: None,
37      pfill: None,
38      minus: None,
39      mfill: None,
40    }
41  }
42  fn new_f64(number: f64) -> Self {
43    let (skip, plus, pfill, minus, mfill) = new_setup(number, None, None, None, None);
44    MuGlue {
45      skip,
46      plus,
47      pfill,
48      minus,
49      mfill,
50    }
51  }
52  // Negate all components (skip, plus, minus)
53  fn negate(self) -> Self {
54    MuGlue {
55      skip:  -self.skip,
56      plus:  self.plus.map(|p| -p),
57      pfill: self.pfill,
58      minus: self.minus.map(|m| -m),
59      mfill: self.mfill,
60    }
61  }
62}
63impl Object for MuGlue {
64  fn revert(&self) -> crate::Result<Tokens> { Ok(Tokens::new(Explode!(self.to_string()))) }
65}
66
67impl MuGlue {
68  pub fn new_full(
69    skip: i64,
70    plus: Option<i64>,
71    pfill: Option<FillCode>,
72    minus: Option<i64>,
73    mfill: Option<FillCode>,
74  ) -> Self {
75    MuGlue {
76      skip,
77      plus,
78      pfill,
79      minus,
80      mfill,
81    }
82  }
83  pub fn new_full_f64(
84    skip: f64,
85    plus: Option<f64>,
86    pfill: Option<FillCode>,
87    minus: Option<f64>,
88    mfill: Option<FillCode>,
89  ) -> Self {
90    let (skip, plus, pfill, minus, mfill) = new_setup(skip, plus, pfill, minus, mfill);
91    MuGlue {
92      skip,
93      plus,
94      pfill,
95      minus,
96      mfill,
97    }
98  }
99
100  pub fn new_spec(
101    spec: &str,
102    plus: Option<f64>,
103    pfill: Option<FillCode>,
104    minus: Option<f64>,
105    mfill: Option<FillCode>,
106  ) -> Self {
107    let (skip, plus, pfill, minus, mfill) = spec_setup(spec, plus, pfill, minus, mfill, "mu");
108    MuGlue {
109      skip,
110      plus,
111      pfill,
112      minus,
113      mfill,
114    }
115  }
116
117  pub fn to_attribute(&self) -> String {
118    // XML attribute output is pt-typed by convention (Perl
119    // `Common/Dimension::attributeformat`). Convert mu→pt via the
120    // shared `mu_to_pt` helper so XMHint width / lpadding /
121    // rpadding values come out as `1.66663pt` not `3.0mu`.
122    // Mirrors Perl `Common/MuGlue::ptValue` flow.
123    let pt_skip = mu_to_pt(self.skip);
124    let mut string = attribute_format(pt_skip, Some("pt"));
125    if let Some(plus) = self.plus
126      && plus != 0
127    {
128      string.push_str(" plus ");
129      let fill_u = if let Some(pfill) = self.pfill {
130        pfill.to_str()
131      } else {
132        "pt"
133      };
134      let plus_pt = if fill_u == "pt" { mu_to_pt(plus) } else { plus };
135      string.push_str(&attribute_format(plus_pt, Some(fill_u)));
136    }
137    if let Some(minus) = self.minus
138      && minus != 0
139    {
140      string.push_str(" minus ");
141      let mfill_u = if let Some(mfill) = self.mfill {
142        mfill.to_str()
143      } else {
144        "pt"
145      };
146      let minus_pt = if mfill_u == "pt" {
147        mu_to_pt(minus)
148      } else {
149        minus
150      };
151      string.push_str(&attribute_format(minus_pt, Some(mfill_u)));
152    }
153    string
154  }
155}
156
157fn mu_to_pt(mu_val: i64) -> i64 {
158  let fs = crate::state::lookup_font()
159    .and_then(|f| f.get_size())
160    .unwrap_or(10.0);
161  let unity = crate::common::numeric_ops::UNITY_F64;
162  let muwidth = (fs * unity / 18.0) as i64;
163  ((mu_val as f64 * muwidth as f64 / unity).trunc()) as i64
164}
165
166impl From<MuGlue> for Option<Tokens> {
167  fn from(v: MuGlue) -> Option<Tokens> { Some(v.into()) }
168}
169impl From<MuGlue> for Tokens {
170  fn from(v: MuGlue) -> Tokens {
171    v.revert()
172      .expect("MuGlue should always be revertable to Tokens.")
173  }
174}
175
176#[cfg(test)]
177mod tests {
178  use super::*;
179
180  #[test]
181  fn muglue_default_is_zero() {
182    let m = MuGlue::default();
183    assert_eq!(m.skip, 0);
184    assert_eq!(m.plus, None);
185    assert_eq!(m.minus, None);
186    assert_eq!(m.pfill, None);
187    assert_eq!(m.mfill, None);
188  }
189
190  #[test]
191  fn muglue_new_builds_skip_only() {
192    let m = <MuGlue as NumericOps>::new(65536);
193    assert_eq!(m.skip, 65536);
194    assert_eq!(m.plus, None);
195    assert_eq!(m.minus, None);
196  }
197
198  #[test]
199  fn muglue_value_of_returns_skip() {
200    let m = MuGlue {
201      skip:  1234,
202      plus:  Some(10),
203      pfill: None,
204      minus: None,
205      mfill: None,
206    };
207    assert_eq!(m.value_of(), 1234);
208  }
209
210  #[test]
211  fn muglue_register_type_is_muglue() {
212    let m = MuGlue::default();
213    assert_eq!(m.register_type(), RegisterType::MuGlue);
214  }
215
216  #[test]
217  fn muglue_negate_flips_all_components() {
218    let m = MuGlue {
219      skip:  100,
220      plus:  Some(10),
221      pfill: Some(FillCode::Fil),
222      minus: Some(5),
223      mfill: Some(FillCode::Fill),
224    };
225    let n = m.negate();
226    assert_eq!(n.skip, -100);
227    assert_eq!(n.plus, Some(-10));
228    assert_eq!(n.minus, Some(-5));
229    // FillCodes are not negated — they describe stretch/shrink kind.
230    assert_eq!(n.pfill, Some(FillCode::Fil));
231    assert_eq!(n.mfill, Some(FillCode::Fill));
232  }
233
234  #[test]
235  fn muglue_new_full_roundtrip() {
236    let m = MuGlue::new_full(
237      100,
238      Some(10),
239      Some(FillCode::Fil),
240      Some(5),
241      Some(FillCode::Fill),
242    );
243    assert_eq!(m.skip, 100);
244    assert_eq!(m.plus, Some(10));
245    assert_eq!(m.pfill, Some(FillCode::Fil));
246    assert_eq!(m.minus, Some(5));
247    assert_eq!(m.mfill, Some(FillCode::Fill));
248  }
249
250  #[test]
251  fn muglue_display_includes_mu_unit() {
252    // Display uses "mu" as the unit.
253    let m = <MuGlue as NumericOps>::new(65536); // 1mu
254    let out = format!("{m}");
255    assert!(
256      out.contains("mu"),
257      "MuGlue display should include 'mu' unit; got {out:?}"
258    );
259  }
260
261  #[test]
262  fn muglue_equality() {
263    let a = MuGlue {
264      skip:  1,
265      plus:  None,
266      pfill: None,
267      minus: None,
268      mfill: None,
269    };
270    let b = MuGlue {
271      skip:  1,
272      plus:  None,
273      pfill: None,
274      minus: None,
275      mfill: None,
276    };
277    let c = MuGlue {
278      skip:  2,
279      plus:  None,
280      pfill: None,
281      minus: None,
282      mfill: None,
283    };
284    assert_eq!(a, b);
285    assert_ne!(a, c);
286  }
287
288  #[test]
289  fn muglue_double_negate_is_identity() {
290    let m = MuGlue {
291      skip:  100,
292      plus:  Some(10),
293      pfill: Some(FillCode::Fil),
294      minus: Some(5),
295      mfill: Some(FillCode::Fill),
296    };
297    assert_eq!(m.negate().negate(), m);
298  }
299}