Skip to main content

latexml_core/common/
glue.rs

1use std::{cmp::Ordering, fmt};
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5
6use super::dimension::fixedformat;
7use crate::{
8  Object,
9  common::{
10    dimension::attribute_format,
11    error::Result,
12    numeric_ops::{EPSILON, NumericOps, fixpoint, fixpoint_unit, kround},
13  },
14  definition::register::{RegisterType, RegisterValue},
15  digested::Digested,
16  state::*,
17};
18
19/// Positively silly enum, but it solves all kinds of issues with the Glue struct
20/// most importantly allows us to keep deriving the Copy trait, and avoids storing
21/// strings in Glue objects
22#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
23pub enum FillCode {
24  Fil,
25  Fill,
26  Filll,
27}
28
29impl fmt::Display for FillCode {
30  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.to_str()) }
31}
32
33impl FillCode {
34  pub fn new(index: usize) -> Option<FillCode> {
35    match index {
36      1 => Some(FillCode::Fil),
37      2 => Some(FillCode::Fill),
38      3 => Some(FillCode::Filll),
39      _ => None,
40    }
41  }
42  pub fn from(ftype: &str) -> Option<FillCode> {
43    match ftype {
44      "fil" => Some(FillCode::Fil),
45      "fill" => Some(FillCode::Fill),
46      "filll" => Some(FillCode::Filll),
47      _ => None,
48    }
49  }
50  pub fn to_str(&self) -> &'static str {
51    match self {
52      FillCode::Fil => "fil",
53      FillCode::Fill => "fill",
54      FillCode::Filll => "filll",
55    }
56  }
57}
58
59// Note: Regexes are not first-level objects in Rust, and neither are Strings
60//       yet we would like to have some efficient
61macro_rules! num_re_str {
62  () => {
63    r"\d*\.?\d*"
64  };
65}
66macro_rules! unit_re_str {
67  () => {
68    r"[a-zA-Z][a-zA-Z]"
69  };
70}
71macro_rules! fill_re_str {
72  () => {
73    r"fil|fill|filll|[a-zA-Z][a-zA-Z]"
74  };
75}
76
77macro_rules! plus_re_str {
78  () => {
79    concat!(r"\s+plus\s*(", num_re_str!(), ")(", fill_re_str!(), r")")
80  };
81}
82macro_rules! minus_re_str {
83  () => {
84    concat!(r"\s+minus\s*(", num_re_str!(), r")(", fill_re_str!(), r")")
85  };
86}
87
88static GLUE_RE_STR: &str = concat!(
89  r"^(\+?\-?",
90  num_re_str!(),
91  r")(",
92  unit_re_str!(),
93  r")(",
94  plus_re_str!(),
95  r")?(",
96  minus_re_str!(),
97  r")?$"
98);
99
100static _NUM_RE: Lazy<Regex> = Lazy::new(|| Regex::new(num_re_str!()).unwrap());
101static UNIT_RE: Lazy<Regex> = Lazy::new(|| Regex::new(unit_re_str!()).unwrap());
102static _FILL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(fill_re_str!()).unwrap());
103static _PLUS_RE: Lazy<Regex> = Lazy::new(|| Regex::new(plus_re_str!()).unwrap());
104static _MINUS_RE: Lazy<Regex> = Lazy::new(|| Regex::new(minus_re_str!()).unwrap());
105static GLUE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(GLUE_RE_STR).unwrap());
106
107#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
108pub struct Glue {
109  pub skip:  i64,
110  pub plus:  Option<i64>,
111  pub pfill: Option<FillCode>,
112  pub minus: Option<i64>,
113  pub mfill: Option<FillCode>,
114}
115
116impl NumericOps for Glue {
117  fn value_of(self) -> i64 { self.skip }
118  fn register_type(&self) -> RegisterType { RegisterType::Glue }
119  // identity, used to type cast in runtime
120  fn into_glue_type(self) -> Glue { self }
121  fn add<T: NumericOps>(self, other: T) -> Self
122  where Self: Sized {
123    if other.register_type() != RegisterType::Glue {
124      Glue {
125        skip:  self.skip + other.value_of(),
126        plus:  self.plus,
127        pfill: self.pfill,
128        minus: self.minus,
129        mfill: self.mfill,
130      }
131    } else {
132      // Both glues, add
133      self.add_glue(other.into_glue_type())
134    }
135  }
136  fn new(skip: i64) -> Self {
137    Glue {
138      skip,
139      plus: None,
140      pfill: None,
141      minus: None,
142      mfill: None,
143    }
144  }
145  fn new_f64(number: f64) -> Self {
146    let (skip, plus, pfill, minus, mfill) = new_setup(number, None, None, None, None);
147    Glue {
148      skip,
149      plus,
150      pfill,
151      minus,
152      mfill,
153    }
154  }
155  // Perl Glue.pm: multiply scales skip, plus, AND minus components
156  fn multiply<T: NumericOps>(self, other: T) -> Self
157  where Self: Sized {
158    let factor = other.value_f64();
159    Glue {
160      skip:  (self.skip as f64 * factor) as i64,
161      plus:  self.plus.map(|p| (p as f64 * factor) as i64),
162      pfill: self.pfill,
163      minus: self.minus.map(|m| (m as f64 * factor) as i64),
164      mfill: self.mfill,
165    }
166  }
167  // Perl Glue.pm: divide scales skip, plus, AND minus components
168  fn divide<T: NumericOps>(self, other: T) -> Self
169  where Self: Sized {
170    let mut divisor = other.value_f64();
171    if divisor == 0.0 {
172      divisor = EPSILON;
173    }
174    Glue {
175      skip:  (self.skip as f64 / divisor).trunc() as i64,
176      plus:  self.plus.map(|p| (p as f64 / divisor).trunc() as i64),
177      pfill: self.pfill,
178      minus: self.minus.map(|m| (m as f64 / divisor).trunc() as i64),
179      mfill: self.mfill,
180    }
181  }
182  fn subtract<T: NumericOps>(self, other: T) -> Self
183  where Self: Sized {
184    if other.register_type() != RegisterType::Glue {
185      Glue {
186        skip:  self.skip - other.value_of(),
187        plus:  self.plus,
188        pfill: self.pfill,
189        minus: self.minus,
190        mfill: self.mfill,
191      }
192    } else {
193      let other_glue = other.into_glue_type();
194      self.add_glue(Glue {
195        skip:  -other_glue.skip,
196        plus:  other_glue.plus.map(|p| -p),
197        pfill: other_glue.pfill,
198        minus: other_glue.minus.map(|m| -m),
199        mfill: other_glue.mfill,
200      })
201    }
202  }
203  // Negate all components (skip, plus, minus)
204  fn negate(self) -> Self {
205    Glue {
206      skip:  -self.skip,
207      plus:  self.plus.map(|p| -p),
208      pfill: self.pfill,
209      minus: self.minus.map(|m| -m),
210      mfill: self.mfill,
211    }
212  }
213  fn smaller<T: NumericOps>(self, other: T) -> Self
214  where Self: Sized {
215    let other_val = other.value_of();
216    if self.skip <= other_val {
217      self
218    } else {
219      Self::new(other_val)
220    }
221  }
222  fn larger<T: NumericOps>(self, other: T) -> Self
223  where Self: Sized {
224    let other_val = other.value_of();
225    if self.skip >= other_val {
226      self
227    } else {
228      Self::new(other_val)
229    }
230  }
231}
232
233pub fn glue_string(
234  skip: i64,
235  plus_opt: Option<i64>,
236  pfill_opt: Option<FillCode>,
237  minus_opt: Option<i64>,
238  mfill_opt: Option<FillCode>,
239  unit: &str,
240) -> String {
241  // ??? TODO: There seems to be some messy confusion about the types of the
242  // pieces of glue/dimensions -- are we consistently using i64 or f64?
243  let mut string = fixedformat(skip, Some(unit));
244  if let Some(plus) = plus_opt
245    && plus != 0
246  {
247    string.push_str(" plus ");
248    let p_fill = if let Some(fill) = pfill_opt {
249      fill.to_str()
250    } else {
251      unit
252    };
253    string.push_str(&fixedformat(plus, Some(p_fill)))
254  }
255  if let Some(minus) = minus_opt
256    && minus != 0
257  {
258    string.push_str(" minus ");
259    let p_fill = if let Some(fill) = mfill_opt {
260      fill.to_str()
261    } else {
262      unit
263    };
264    string.push_str(&fixedformat(minus, Some(p_fill)))
265  }
266  string
267}
268
269impl fmt::Display for Glue {
270  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
271    let string = glue_string(
272      self.skip, self.plus, self.pfill, self.minus, self.mfill, "pt",
273    );
274    write!(f, "{string}")
275  }
276}
277impl Object for Glue {
278  fn be_digested(self) -> Result<Digested> { Ok(RegisterValue::Glue(self).into()) }
279}
280
281pub fn new_setup(
282  skip: f64,
283  plus: Option<f64>,
284  pfill: Option<FillCode>,
285  minus: Option<f64>,
286  mfill: Option<FillCode>,
287) -> (
288  i64,
289  Option<i64>,
290  Option<FillCode>,
291  Option<i64>,
292  Option<FillCode>,
293) {
294  // See comment in Dimension for why kround rather than int
295  (
296    kround(skip),
297    plus.map(kround),
298    pfill,
299    minus.map(kround),
300    mfill,
301  )
302}
303
304pub fn spec_setup(
305  spec: &str,
306  plus: Option<f64>,
307  mut pfill: Option<FillCode>,
308  minus: Option<f64>,
309  mut mfill: Option<FillCode>,
310  unit: &str,
311) -> (
312  i64,
313  Option<i64>,
314  Option<FillCode>,
315  Option<i64>,
316  Option<FillCode>,
317) {
318  if !UNIT_RE.is_match(spec) {
319    // If no units, expect fixedpoint values
320    let skip: f64 = spec.parse::<f64>().unwrap_or_default();
321    new_setup(skip, plus, pfill, minus, mfill)
322  } else {
323    let is_mu = unit == "mu";
324    if plus.is_some() || pfill.is_some() || minus.is_some() || mfill.is_some() {
325      let msg = s!(
326        "You should not create {} with both units and stretch",
327        if is_mu { "MuGlue" } else { "Glue" }
328      );
329      Warn!("unexpected", "fill", msg);
330    }
331
332    if let Some(cs) = GLUE_RE.captures(spec) {
333      let (f, unit, p, punit, m, munit) = (
334        cs.get(1)
335          .map(|v| v.as_str().parse::<f64>().unwrap_or_default())
336          .unwrap_or_default(),
337        cs.get(2).map_or("", |m| m.as_str()),
338        cs.get(4)
339          .map(|v| v.as_str().parse::<f64>().unwrap_or_default())
340          .unwrap_or_default(),
341        cs.get(5).map_or("", |m| m.as_str()),
342        cs.get(7)
343          .map(|v| v.as_str().parse::<f64>().unwrap_or_default())
344          .unwrap_or_default(),
345        cs.get(8).map_or("", |m| m.as_str()),
346      );
347      let skip = if unit.is_empty() {
348        f.trunc() as i64
349      } else if is_mu {
350        if unit != "mu" {
351          Warn!("unexpected", unit, "Assumed mu");
352        }
353        fixpoint(f, None) // in mu
354      } else {
355        {
356          let (num, den) = convert_unit_ratio(unit);
357          fixpoint_unit(f, num, den)
358        }
359      };
360
361      let mut plus = if punit.is_empty() {
362        None // Some(0.0) ?
363      // ? punit = "0";
364      } else if let Some(code) = FillCode::from(punit) {
365        pfill = Some(code);
366        Some(fixpoint(p, None))
367      } else if is_mu {
368        pfill = None;
369        if punit != "mu" {
370          Warn!("unexpected", punit, "Assumed mu");
371        }
372        Some(fixpoint(p, None))
373      } else {
374        pfill = None; // ? 0
375        Some({
376          let (num, den) = convert_unit_ratio(punit);
377          fixpoint_unit(p, num, den)
378        })
379      };
380
381      let mut minus = if munit.is_empty() {
382        None // ? Some(0.0);
383      // munit = 0;
384      } else if let Some(code) = FillCode::from(munit) {
385        mfill = Some(code);
386        Some(fixpoint(m, None))
387      } else if is_mu {
388        mfill = None; // 0
389        if munit != "mu" {
390          Warn!("unexpected", munit, "Assumed mu");
391        }
392        Some(fixpoint(m, None))
393      } else {
394        mfill = None; // 0
395        Some({
396          let (num, den) = convert_unit_ratio(munit);
397          fixpoint_unit(m, num, den)
398        })
399      };
400
401      if punit.is_empty() {
402      } else if let Some(pfcode) = FillCode::from(punit) {
403        plus = Some(fixpoint(p, None));
404        pfill = Some(pfcode);
405      } else {
406        plus = Some({
407          let (num, den) = convert_unit_ratio(punit);
408          fixpoint_unit(p, num, den)
409        });
410        pfill = None;
411      }
412      if munit.is_empty() {
413      } else if let Some(mfcode) = FillCode::from(munit) {
414        minus = Some(fixpoint(m, None));
415        mfill = Some(mfcode);
416      } else {
417        minus = Some({
418          let (num, den) = convert_unit_ratio(munit);
419          fixpoint_unit(m, num, den)
420        });
421        mfill = None;
422      }
423      (skip, plus, pfill, minus, mfill)
424    } else {
425      let msg = s!(
426        "Missing {} specification assuming 0pt",
427        if is_mu { "MuGlue" } else { "Glue" }
428      );
429      Warn!("unexpected", spec, msg);
430      (0, None, None, None, None)
431    }
432  }
433}
434
435impl Glue {
436  pub fn new_full(
437    skip: i64,
438    plus: Option<i64>,
439    pfill: Option<FillCode>,
440    minus: Option<i64>,
441    mfill: Option<FillCode>,
442  ) -> Self {
443    Glue {
444      skip,
445      plus,
446      pfill,
447      minus,
448      mfill,
449    }
450  }
451  pub fn new_full_f64(
452    skip: f64,
453    plus: Option<f64>,
454    pfill: Option<FillCode>,
455    minus: Option<f64>,
456    mfill: Option<FillCode>,
457  ) -> Self {
458    let (skip, plus, pfill, minus, mfill) = new_setup(skip, plus, pfill, minus, mfill);
459    Glue {
460      skip,
461      plus,
462      pfill,
463      minus,
464      mfill,
465    }
466  }
467  pub fn new_spec(
468    spec: &str,
469    plus: Option<f64>,
470    pfill: Option<FillCode>,
471    minus: Option<f64>,
472    mfill: Option<FillCode>,
473  ) -> Self {
474    let (skip, plus, pfill, minus, mfill) = spec_setup(spec, plus, pfill, minus, mfill, "pt");
475    Glue {
476      skip,
477      plus,
478      pfill,
479      minus,
480      mfill,
481    }
482  }
483
484  pub fn add_glue(self, other: Glue) -> Glue {
485    // (pts, p, pf, m, mf) = @$self;
486    // if (ref $other eq 'LaTeXML::Common::Glue') {
487    // my ($pts2, $p2, $pf2, $m2, $mf2) = @$other;
488    let skip = self.skip + other.skip;
489    let mut plus = self.plus;
490    let mut minus = self.minus;
491    let mut pfill = self.pfill;
492    let mut mfill = self.mfill;
493
494    match self.pfill.cmp(&other.pfill) {
495      Ordering::Equal => {
496        if let Some(oplus) = other.plus {
497          plus = match plus {
498            Some(splus) => Some(splus + oplus),
499            None => Some(oplus),
500          };
501        }
502      },
503      Ordering::Less => {
504        plus = other.plus;
505        pfill = other.pfill;
506      },
507      _ => {},
508    };
509    match self.mfill.cmp(&other.mfill) {
510      Ordering::Equal => {
511        if let Some(ominus) = other.minus {
512          minus = match minus {
513            Some(sminus) => Some(sminus + ominus),
514            None => Some(ominus),
515          };
516        }
517      },
518      Ordering::Less => {
519        minus = other.minus;
520        mfill = other.mfill;
521      },
522      _ => {},
523    };
524
525    Glue {
526      skip,
527      plus,
528      pfill,
529      minus,
530      mfill,
531    }
532    // else {
533    // return (ref $self)->new($pts + $other->valueOf, $p, $pf, $m, $mf); }
534  }
535
536  pub fn to_attribute(&self) -> String {
537    let u = "pt";
538    let mut string = attribute_format(self.skip, Some(u));
539    if let Some(plus) = self.plus
540      && plus != 0
541    {
542      string.push_str(" plus ");
543      let fill_u = if let Some(pfill) = self.pfill {
544        pfill.to_str()
545      } else {
546        u
547      };
548      string.push_str(&attribute_format(plus, Some(fill_u)));
549    }
550    if let Some(minus) = self.minus
551      && minus != 0
552    {
553      string.push_str(" minus ");
554      let mfill_u = if let Some(mfill) = self.mfill {
555        mfill.to_str()
556      } else {
557        u
558      };
559      string.push_str(&attribute_format(minus, Some(mfill_u)));
560    }
561    string
562  }
563}
564
565#[cfg(test)]
566mod tests {
567  use super::*;
568
569  #[test]
570  fn fillcode_new_from_index() {
571    assert_eq!(FillCode::new(1), Some(FillCode::Fil));
572    assert_eq!(FillCode::new(2), Some(FillCode::Fill));
573    assert_eq!(FillCode::new(3), Some(FillCode::Filll));
574    assert_eq!(FillCode::new(0), None);
575    assert_eq!(FillCode::new(4), None);
576    assert_eq!(FillCode::new(100), None);
577  }
578
579  #[test]
580  fn fillcode_from_str_case_sensitive() {
581    assert_eq!(FillCode::from("fil"), Some(FillCode::Fil));
582    assert_eq!(FillCode::from("fill"), Some(FillCode::Fill));
583    assert_eq!(FillCode::from("filll"), Some(FillCode::Filll));
584    // Case-sensitive: uppercase is rejected.
585    assert_eq!(FillCode::from("FIL"), None);
586    assert_eq!(FillCode::from("Fil"), None);
587    assert_eq!(FillCode::from(""), None);
588    assert_eq!(FillCode::from("other"), None);
589  }
590
591  #[test]
592  fn fillcode_to_str_roundtrip() {
593    // from(to_str(c)) == c for all variants.
594    for code in [FillCode::Fil, FillCode::Fill, FillCode::Filll] {
595      let s = code.to_str();
596      assert_eq!(
597        FillCode::from(s),
598        Some(code),
599        "roundtrip broke at {code:?} via {s:?}"
600      );
601    }
602  }
603
604  #[test]
605  fn fillcode_display_matches_to_str() {
606    assert_eq!(format!("{}", FillCode::Fil), "fil");
607    assert_eq!(format!("{}", FillCode::Fill), "fill");
608    assert_eq!(format!("{}", FillCode::Filll), "filll");
609  }
610
611  #[test]
612  fn fillcode_ord_fil_lt_fill_lt_filll() {
613    // Derived Ord follows variant declaration order.
614    assert!(FillCode::Fil < FillCode::Fill);
615    assert!(FillCode::Fill < FillCode::Filll);
616  }
617
618  #[test]
619  fn glue_default_is_zero_skip_no_stretch() {
620    let g = Glue::default();
621    assert_eq!(g.skip, 0);
622    assert_eq!(g.plus, None);
623    assert_eq!(g.minus, None);
624    assert_eq!(g.pfill, None);
625    assert_eq!(g.mfill, None);
626  }
627
628  #[test]
629  fn glue_new_builds_skip_only() {
630    let g = <Glue as NumericOps>::new(65536);
631    assert_eq!(g.skip, 65536);
632    assert_eq!(g.plus, None);
633    assert_eq!(g.minus, None);
634  }
635
636  #[test]
637  fn glue_value_of_returns_skip() {
638    let g = Glue {
639      skip:  1234,
640      plus:  Some(10),
641      pfill: None,
642      minus: None,
643      mfill: None,
644    };
645    assert_eq!(
646      g.value_of(),
647      1234,
648      "value_of returns the skip, not the stretch"
649    );
650  }
651}