Skip to main content

yui_core/util/
tex.rs

1//! LaTeX rendering helpers, gated on the `tex` feature.
2//!
3//! See: <https://en.wikipedia.org/wiki/LaTeX>
4
5
6use itertools::Itertools;
7use std::fmt::Display;
8
9/// Types that can be rendered as LaTeX math.
10pub trait TeX {
11    fn tex_math_symbol() -> String;
12    fn tex_string(&self) -> String;
13}
14
15/// Render a 2D table as a LaTeX `\begin{tabular}` environment.
16pub fn tex_table<S, I, J, I1, I2, D, F>(caption: &str, head: S, rows: I1, cols: I2, entry: F, math_mode: bool, hor_at_top: bool) -> String
17where
18    S: Display,
19    I: Display,
20    J: Display,
21    I1: IntoIterator<Item = I>,
22    I2: IntoIterator<Item = J>,
23    D: Display,
24    F: Fn(&I, &J) -> D
25{
26    fn disp<S>(s: S, math_mode: bool) -> String where S: Display {
27        if math_mode {
28            let s = s.to_string();
29            if s.is_empty() {
30                "$ $".to_string()
31            } else {
32                format!("${}$", s)
33            }
34        } else {
35            s.to_string()
36        }
37    }
38
39    let cols = cols.into_iter().collect_vec();
40    let mut res = String::new();
41
42    res += r#"\begin{table}
43\centering
44\begin{tabular}"#;
45
46    res += &format!("{{r|{}}}\n", "l".repeat(cols.len()));
47
48    // one cell per column, so a table with no columns emits no `&` and stays valid.
49    let row = |head: String, cells: Vec<String>|
50        std::iter::once(head).chain(cells).join(" & ") + " \\\\\n";
51
52    let hor = row(
53        disp(head, math_mode),
54        cols.iter().map(|c| disp(c, math_mode)).collect_vec()
55    );
56
57    if hor_at_top {
58        res += &hor;
59        res += "\\hline\n";
60    }
61
62    for i in rows {
63        res += &row(
64            disp(&i, math_mode),
65            cols.iter().map(|j| disp(entry(&i, j), math_mode)).collect_vec()
66        );
67    }
68
69    if !hor_at_top {
70        res += "\\hline\n";
71        res += &hor;
72    }
73
74    res += "\\end{tabular}\n";
75    res += &format!("\\caption{{{caption}}}\n");
76    res += "\\end{table}\n";
77    res
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn test_tex_table() {
86        let _table = tex_table("Caption", "i, j", [1, 2, 3], [4, 5, 6, 7], |i, j| i * 10 + j, true, false);
87        // println!("{_table}");
88    }
89
90    #[test]
91    fn table_without_columns() {
92        // the row must carry one cell per column, or it outruns the `{r|}` spec.
93        let entry = |i: &i32, j: &i32| i * 10 + j;
94        let none: [i32; 0] = [];
95
96        let t = tex_table("Caption", "i", none, none, entry, true, true);
97        assert!(t.contains("{r|}"));
98        assert!(!t.contains('&'), "empty table has a stray `&`:\n{t}");
99
100        let t = tex_table("Caption", "i", [1, 2], none, entry, true, true);
101        assert!(!t.contains('&'), "column-less rows have a stray `&`:\n{t}");
102
103        // and a normal table is unaffected: head + 2 columns = 2 separators per row.
104        let t = tex_table("Caption", "i", [1], [4, 5], entry, true, true);
105        assert!(t.lines().all(|l| !l.ends_with("\\\\") || l.matches('&').count() == 2), "{t}");
106    }
107}