Skip to main content

latexml_core/alignment/
template.rs

1//! Support for tabular/array environments
2use std::{
3  collections::VecDeque,
4  fmt::{self, Debug, Display},
5};
6
7use rustc_hash::FxHashMap as HashMap;
8
9use super::cell::Cell;
10use crate::{Digested, common::dimension::Dimension, state::Stored, token::Token, tokens::Tokens};
11
12// ??
13pub type Row = Template;
14#[derive(Debug, Clone, Default, PartialEq)]
15pub enum Align {
16  #[default]
17  Left,
18  Center,
19  Right,
20  Justify,
21  /// Perl: align => 'char:X' — decimal-aligned column (dcolumn.sty)
22  Char(String),
23}
24impl Align {
25  pub fn char_code(&self) -> char {
26    match self {
27      Align::Right => 'r',
28      Align::Left => 'l',
29      Align::Center => 'c',
30      Align::Justify => 'p',
31      Align::Char(_) => 'c', // fallback for sizing
32    }
33  }
34  pub fn name(&self) -> String {
35    match self {
36      Align::Right => "right".to_string(),
37      Align::Left => "left".to_string(),
38      Align::Center => "center".to_string(),
39      Align::Justify => "justify".to_string(),
40      Align::Char(ch) => format!("char:{ch}"),
41    }
42  }
43}
44impl From<char> for Align {
45  fn from(c: char) -> Align {
46    match c {
47      'l' => Align::Left,
48      'r' => Align::Right,
49      'c' => Align::Center,
50      'p' => Align::Justify,
51      _ => Align::default(), // fallback
52    }
53  }
54}
55
56/// Two axes of tabular orientation
57#[derive(Debug, Copy, Clone, PartialEq)]
58pub enum Axis {
59  Column,
60  Row,
61}
62impl Axis {
63  /// The string name of a tabular axis
64  pub fn name(&self) -> &'static str {
65    match self {
66      Axis::Column => "column",
67      Axis::Row => "row",
68    }
69  }
70  /// Maybe these may have been better named as "horizontal_group" and "vertical_group" in latexml?
71  pub fn marker_name(&self) -> &'static str {
72    match self {
73      Axis::Column => "row",
74      Axis::Row => "column",
75    }
76  }
77}
78
79#[derive(Debug, Copy, Clone, PartialEq)]
80pub enum ColumnSpec {
81  Integer,   // 'i'
82  Empty,     // '_'
83  Unknown,   // '?'
84  Text,      // 't'
85  MultiText, // 'tt' — multiple text elements (e.g. colorbox + text)
86  Math,      // 'm'
87  /// Math *and* Text, alternating
88  MathAltText, // 'mx'
89  D,         // 'd'
90  Graphics,  // 'g'
91}
92impl ColumnSpec {
93  /// The cell comparator.
94  pub fn difference_heuristic(&self, other: &ColumnSpec) -> f64 {
95    use ColumnSpec::*;
96    match self {
97      Empty => match other {
98        Empty => 0.0,
99        Math => 0.05,
100        Integer => 0.05,
101        Text => 0.05,
102        Unknown => 0.05,
103        MathAltText => 0.05,
104        _ => 0.75,
105      },
106      Math => match other {
107        Empty => 0.05,
108        Math => 0.0,
109        Integer => 0.1,
110        MathAltText => 0.2,
111        _ => 0.75,
112      },
113      Integer => match other {
114        Empty => 0.05,
115        Math => 0.1,
116        Integer => 0.0,
117        MathAltText => 0.2,
118        _ => 0.75,
119      },
120      Text => match other {
121        Empty => 0.05,
122        Text => 0.0,
123        MathAltText => 0.2,
124        _ => 0.75, // includes MultiText — Perl fallthrough 0.75
125      },
126      MultiText => match other {
127        Empty => 0.05,
128        MultiText => 0.0,
129        MathAltText => 0.2,
130        _ => 0.75, // Perl: "tt" not in diff table → 0.75 fallthrough
131      },
132      Unknown => match other {
133        Empty => 0.05,
134        Unknown => 0.0,
135        MathAltText => 0.2,
136        _ => 0.75,
137      },
138      MathAltText => match other {
139        Empty => 0.05,
140        Math => 0.2,
141        Integer => 0.2,
142        Text => 0.2,
143        Unknown => 0.2,
144        MathAltText => 0.0,
145        _ => 0.75,
146      },
147      D => match other {
148        D => 0.0,
149        _ => 0.75,
150      },
151      Graphics => match other {
152        Graphics => 0.0,
153        _ => 0.75,
154      },
155    }
156  }
157}
158impl Display for ColumnSpec {
159  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160    match self {
161      ColumnSpec::Integer => write!(f, "i"),
162      ColumnSpec::Empty => write!(f, "_"),
163      ColumnSpec::Unknown => write!(f, "?"),
164      ColumnSpec::Text => write!(f, "t"),
165      ColumnSpec::MultiText => write!(f, "tt"),
166      ColumnSpec::Math => write!(f, "m"),
167      ColumnSpec::MathAltText => write!(f, "mx"),
168      ColumnSpec::D => write!(f, "d"),
169      ColumnSpec::Graphics => write!(f, "g"),
170    }
171  }
172}
173
174#[derive(Debug, Copy, Clone, PartialEq)]
175pub enum BorderSpec {
176  Top,
177  Bottom,
178  Left,
179  Right,
180}
181
182#[derive(Debug, Clone, Default)]
183pub struct TemplateConfig {
184  pub repeating:     Option<bool>,
185  pub pseudorow:     Option<bool>,
186  pub non_repeating: usize,
187  pub repeated:      Vec<Cell>,
188  pub reversion:     Option<Tokens>,
189  pub columns:       Option<Vec<Cell>>,
190  pub tokens:        Option<Vec<Token>>,
191  pub save_before:   Option<VecDeque<Token>>,
192  pub save_between:  Option<VecDeque<Token>>,
193}
194
195#[derive(Debug, Clone, Default, PartialEq)]
196pub struct Template {
197  repeating:            bool,
198  pseudorow:            bool,
199  non_repeating:        usize,
200  repeated:             Vec<Cell>,
201  reversion:            Option<Tokens>,
202  columns:              Vec<Cell>,
203  pub tokens:           Vec<Token>,
204  padding:              Option<Dimension>,
205  pub top_padding:      Option<Dimension>,
206  pub bottom_padding:   Option<Dimension>,
207  pub before:           VecDeque<Digested>,
208  pub after:            VecDeque<Digested>,
209  save_before:          VecDeque<Token>,
210  save_between:         VecDeque<Token>,
211  disabled_intercolumn: bool,
212  pub cached_width:     Option<Dimension>,
213  pub cached_height:    Option<Dimension>,
214  pub cached_depth:     Option<Dimension>,
215  pub x:                Option<Dimension>,
216  pub y:                Option<Dimension>,
217  /// Per-row properties (e.g. xml:id, tags) set during digestion
218  /// and consumed during construction. Perl: $$row{id}, $$row{tags}.
219  /// Uses Stored to preserve typed values (esp. Digested for tags).
220  pub properties:       HashMap<String, Stored>,
221}
222
223impl Display for Template {
224  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Alignment[]",) }
225}
226impl Template {
227  pub fn new(config: TemplateConfig) -> Self {
228    let repeating = config.repeating.unwrap_or(false) || !config.repeated.is_empty();
229    let pseudorow = config.pseudorow.unwrap_or(false);
230    let mut columns = config.columns.unwrap_or_default();
231    let mut repeated = config.repeated;
232    let non_repeating = columns.len();
233    let save_before = config.save_before.unwrap_or_default();
234    let save_between = config.save_between.unwrap_or_default(); // `between` comes before `before`!
235    for column in columns.iter_mut() {
236      column.empty = true;
237    }
238    for v in repeated.iter_mut() {
239      v.empty = true;
240    }
241
242    Template {
243      columns,
244      pseudorow,
245      repeating,
246      repeated,
247      non_repeating,
248      save_before,
249      save_between,
250      disabled_intercolumn: false,
251      before: VecDeque::new(),
252      after: VecDeque::new(),
253      padding: None,
254      top_padding: None,
255      bottom_padding: None,
256      cached_width: None,
257      cached_height: None,
258      cached_depth: None,
259      x: None,
260      y: None,
261      reversion: config.reversion,
262      tokens: config.tokens.unwrap_or_default(),
263      properties: rustc_hash::FxHashMap::default(),
264    }
265  }
266  pub fn set_reversion(&mut self, tks: Tokens) { self.reversion = Some(tks); }
267  pub fn set_repeating(&mut self) { self.repeating = true; }
268  pub fn set_padding(&mut self, d: Dimension) { self.padding = Some(d); }
269  pub fn get_padding(&self) -> Option<&Dimension> { self.padding.as_ref() }
270
271  /// Perl Template.pm L76-80: disableIntercolumn
272  /// Only sets the flag when there is a current column.
273  /// Perl: `if (my $col = $$self{current_column}) { $$self{disabled_intercolumn} = 1; }`
274  pub fn disable_intercolumn(&mut self) {
275    if !self.columns.is_empty() || !self.repeated.is_empty() {
276      self.disabled_intercolumn = true;
277    }
278  }
279
280  /// Perl Template.pm L113-118: finish
281  /// Appends \lx@intercol to last column's after unless disabled_intercolumn
282  pub fn finish(&mut self) {
283    let last = if self.repeating {
284      self.repeated.last_mut()
285    } else {
286      self.columns.last_mut()
287    };
288    if let Some(prev) = last
289      && !self.disabled_intercolumn
290    {
291      // `take()` moves out the current Option<Tokens> (replacing with
292      // None) — we immediately re-assign, so the clone in the old
293      // `.clone().unwrap_or_default().unlist()` was redundant.
294      let mut after = prev.after.take().unwrap_or_default().unlist();
295      after.push(T_CS!("\\lx@intercol"));
296      prev.after = Some(Tokens::new(after));
297      prev.has_intercol_after = true;
298    }
299  }
300
301  // These add material before & after the current column
302  pub fn add_before_column(&mut self, mut new: VecDeque<Token>) {
303    let current_sb = self.save_before.drain(..);
304    new.extend(current_sb);
305    self.save_before = new; // NOTE: goes all the way to front!
306  }
307  // NOTE: \lx@column@trimright should ONLY be added to LaTeX tabular style templates!!!!
308  // NOT \halign style templates!
309  pub fn add_after_column(&mut self, new: Vec<Token>) {
310    if let Some(current_column) = self.columns.last_mut() {
311      let current_after = current_column.after.take().unwrap_or_default().unlist();
312      current_column.after = Some(Tokens!(T_CS!("\\lx@column@trimright"), new, current_after));
313    }
314  }
315
316  // Perl Template.pm L65-74: addBetweenColumn
317  pub fn add_between_column(&mut self, tokens: Vec<Token>) {
318    if let Some(current_column) = self.columns.last_mut() {
319      let mut combined = Vec::new();
320      let current_after = current_column.after.take().unwrap_or_default().unlist();
321      combined.extend(current_after);
322      // Perl L69-70: prepend \lx@intercol unless disabled_intercolumn
323      if !self.disabled_intercolumn {
324        combined.push(T_CS!("\\lx@intercol"));
325      }
326      combined.extend(tokens);
327      current_column.after = Some(Tokens::new(combined));
328    } else {
329      self.save_between.extend(tokens);
330    }
331  }
332
333  // Perl Template.pm L82-110: addColumn
334  pub fn add_column(&mut self, mut col: Cell) {
335    // Perl L85-87: append \lx@intercol to previous column's after unless disabled_intercolumn
336    if let Some(prev) = if self.repeating {
337      self.repeated.last_mut()
338    } else {
339      self.columns.last_mut()
340    } && !self.disabled_intercolumn
341    {
342      let mut after = prev.after.take().unwrap_or_default().unlist();
343      after.push(T_CS!("\\lx@intercol"));
344      prev.after = Some(Tokens::new(after));
345      prev.has_intercol_after = true;
346    }
347    // Perl L88-95: build before from save_between + \lx@intercol + properties before + save_before
348    let mut before = Vec::new();
349    if !self.save_between.is_empty() {
350      before.extend(self.save_between.clone());
351    }
352    // Perl L90: push \lx@intercol unless disabled_intercolumn
353    let has_intercol_before = !self.disabled_intercolumn;
354    if has_intercol_before {
355      before.push(T_CS!("\\lx@intercol"));
356    }
357    // Perl L91: delete disabled_intercolumn
358    self.disabled_intercolumn = false;
359
360    if let Some(prop_before) = col.before {
361      before.extend(prop_before.unlist());
362    }
363    if !self.save_before.is_empty() {
364      before.extend(self.save_before.clone());
365    }
366    col.before = if !before.is_empty() {
367      Some(Tokens::new(before))
368    } else {
369      None
370    };
371    col.has_intercol_before = has_intercol_before;
372    let mut after = vec![T_CS!("\\lx@column@trimright")];
373    if let Some(prop_after) = col.after {
374      after.extend(prop_after.unlist());
375    }
376    col.after = if after.is_empty() {
377      None
378    } else {
379      Some(Tokens::new(after))
380    };
381    col.empty = true;
382    self.save_between = VecDeque::new();
383    self.save_before = VecDeque::new();
384
385    if self.repeating {
386      self.non_repeating = self.columns.len();
387      self.repeated.push(col);
388    } else {
389      self.columns.push(col);
390    }
391  }
392
393  pub fn get_column_mut(&mut self, n: usize) -> Option<&mut Cell> {
394    let all_columns = self.columns.len();
395    if (n > all_columns) && self.repeating {
396      let rep = &self.repeated;
397      let m = rep.len();
398      if m > 0 {
399        for i in all_columns..n {
400          let dup = rep[(i - self.non_repeating) % m].clone();
401          self.columns.push(dup);
402        }
403      }
404    }
405    if n > 0 {
406      self.columns.get_mut(n - 1)
407    } else {
408      None
409    }
410  }
411
412  pub fn get_columns(&self) -> &[Cell] { &self.columns }
413  pub fn get_columns_mut(&mut self) -> &mut Vec<Cell> { &mut self.columns }
414  pub fn get_repeated_mut(&mut self) -> &mut Vec<Cell> { &mut self.repeated }
415  pub fn set_pseudo(&mut self) { self.pseudorow = true; }
416  pub fn unset_pseudo(&mut self) { self.pseudorow = false; }
417  pub fn is_pseudo(&self) -> bool { self.pseudorow }
418}
419
420#[cfg(test)]
421mod tests {
422  use super::*;
423
424  #[test]
425  fn align_default_is_left() {
426    assert_eq!(Align::default(), Align::Left);
427  }
428
429  #[test]
430  fn align_char_code() {
431    assert_eq!(Align::Left.char_code(), 'l');
432    assert_eq!(Align::Right.char_code(), 'r');
433    assert_eq!(Align::Center.char_code(), 'c');
434    assert_eq!(Align::Justify.char_code(), 'p');
435    // Char variant falls back to 'c' for sizing.
436    assert_eq!(Align::Char(".".to_string()).char_code(), 'c');
437  }
438
439  #[test]
440  fn align_name() {
441    assert_eq!(Align::Left.name(), "left");
442    assert_eq!(Align::Right.name(), "right");
443    assert_eq!(Align::Center.name(), "center");
444    assert_eq!(Align::Justify.name(), "justify");
445    assert_eq!(Align::Char(".".to_string()).name(), "char:.");
446  }
447
448  #[test]
449  fn align_from_char_basic() {
450    assert_eq!(Align::from('l'), Align::Left);
451    assert_eq!(Align::from('r'), Align::Right);
452    assert_eq!(Align::from('c'), Align::Center);
453    assert_eq!(Align::from('p'), Align::Justify);
454  }
455
456  #[test]
457  fn align_from_char_unknown_is_default() {
458    // Unknown char falls back to Default (Left).
459    assert_eq!(Align::from('x'), Align::Left);
460    assert_eq!(Align::from('?'), Align::Left);
461  }
462
463  #[test]
464  fn axis_name() {
465    assert_eq!(Axis::Column.name(), "column");
466    assert_eq!(Axis::Row.name(), "row");
467  }
468
469  #[test]
470  fn axis_marker_name_is_inverse() {
471    // marker_name is intentionally "the other axis" — column's
472    // marker is a row, row's marker is a column (possibly a naming
473    // artifact from the Perl side).
474    assert_eq!(Axis::Column.marker_name(), "row");
475    assert_eq!(Axis::Row.marker_name(), "column");
476  }
477
478  #[test]
479  fn column_spec_display_chars() {
480    assert_eq!(format!("{}", ColumnSpec::Integer), "i");
481    assert_eq!(format!("{}", ColumnSpec::Empty), "_");
482    assert_eq!(format!("{}", ColumnSpec::Unknown), "?");
483    assert_eq!(format!("{}", ColumnSpec::Text), "t");
484    assert_eq!(format!("{}", ColumnSpec::MultiText), "tt");
485    assert_eq!(format!("{}", ColumnSpec::Math), "m");
486    assert_eq!(format!("{}", ColumnSpec::MathAltText), "mx");
487    assert_eq!(format!("{}", ColumnSpec::D), "d");
488    assert_eq!(format!("{}", ColumnSpec::Graphics), "g");
489  }
490
491  #[test]
492  fn column_spec_difference_heuristic_self_is_zero() {
493    // Like-to-like distances are 0 for each non-generic variant.
494    assert_eq!(
495      ColumnSpec::Empty.difference_heuristic(&ColumnSpec::Empty),
496      0.0
497    );
498    assert_eq!(
499      ColumnSpec::Math.difference_heuristic(&ColumnSpec::Math),
500      0.0
501    );
502    assert_eq!(
503      ColumnSpec::Integer.difference_heuristic(&ColumnSpec::Integer),
504      0.0
505    );
506    assert_eq!(
507      ColumnSpec::Text.difference_heuristic(&ColumnSpec::Text),
508      0.0
509    );
510    assert_eq!(ColumnSpec::D.difference_heuristic(&ColumnSpec::D), 0.0);
511    assert_eq!(
512      ColumnSpec::Graphics.difference_heuristic(&ColumnSpec::Graphics),
513      0.0
514    );
515  }
516
517  #[test]
518  fn column_spec_difference_heuristic_incompatible_is_large() {
519    // Graphics vs anything else → 0.75 (Perl's "strong difference").
520    assert_eq!(
521      ColumnSpec::Graphics.difference_heuristic(&ColumnSpec::Math),
522      0.75
523    );
524    assert_eq!(ColumnSpec::D.difference_heuristic(&ColumnSpec::Text), 0.75);
525  }
526}