Skip to main content

mdwright_math/
env.rs

1//! LaTeX environment classification.
2//!
3//! Each `\begin{name} … \end{name}` region scanned in
4//! [`super::scan::scan_math_regions`] resolves its `name` to one of
5//! the variants here. Known environments drive canonicalisation
6//! decisions such as aligning vs non-aligning bodies; unknown names
7//! fall through as [`EnvKind::Custom`] and the body is preserved
8//! verbatim.
9
10use std::ops::Range;
11
12/// One of the standard LaTeX / `amsmath` environments mdwright knows
13/// how to classify, or an unrecognised name.
14///
15/// The `Custom` variant carries the byte range of the name inside the
16/// source so callers can recover the original spelling without
17/// allocating; see [`super::span::MathSpan`] for the consumer side.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub enum EnvKind {
20    Known(KnownEnv),
21    /// Byte range of the environment name in source. The matching
22    /// `\end{name}` carries the same name; we store the opening one
23    /// because it is reached first.
24    Custom(Range<usize>),
25}
26
27/// Standard environments recognised by math canonicalisation. Names
28/// follow `amsmath` conventions; the starred variants are the
29/// unnumbered forms.
30#[derive(Copy, Clone, Debug, PartialEq, Eq)]
31pub enum KnownEnv {
32    Align,
33    AlignStar,
34    Aligned,
35    Equation,
36    EquationStar,
37    Gather,
38    GatherStar,
39    Matrix,
40    Pmatrix,
41    Bmatrix,
42    Vmatrix,
43    Smallmatrix,
44    Cases,
45    Array,
46    Split,
47    Multline,
48    MultlineStar,
49}
50
51impl KnownEnv {
52    /// Resolve an environment name to a known variant. Returns `None`
53    /// for any name outside the standard set; the caller falls back to
54    /// [`EnvKind::Custom`].
55    pub fn from_name(name: &str) -> Option<Self> {
56        Some(match name {
57            "align" => Self::Align,
58            "align*" => Self::AlignStar,
59            "aligned" => Self::Aligned,
60            "equation" => Self::Equation,
61            "equation*" => Self::EquationStar,
62            "gather" => Self::Gather,
63            "gather*" => Self::GatherStar,
64            "matrix" => Self::Matrix,
65            "pmatrix" => Self::Pmatrix,
66            "bmatrix" => Self::Bmatrix,
67            "vmatrix" => Self::Vmatrix,
68            "smallmatrix" => Self::Smallmatrix,
69            "cases" => Self::Cases,
70            "array" => Self::Array,
71            "split" => Self::Split,
72            "multline" => Self::Multline,
73            "multline*" => Self::MultlineStar,
74            _ => return None,
75        })
76    }
77
78    /// The canonical name as it appears between `\begin{…}` braces.
79    pub const fn name(self) -> &'static str {
80        match self {
81            Self::Align => "align",
82            Self::AlignStar => "align*",
83            Self::Aligned => "aligned",
84            Self::Equation => "equation",
85            Self::EquationStar => "equation*",
86            Self::Gather => "gather",
87            Self::GatherStar => "gather*",
88            Self::Matrix => "matrix",
89            Self::Pmatrix => "pmatrix",
90            Self::Bmatrix => "bmatrix",
91            Self::Vmatrix => "vmatrix",
92            Self::Smallmatrix => "smallmatrix",
93            Self::Cases => "cases",
94            Self::Array => "array",
95            Self::Split => "split",
96            Self::Multline => "multline",
97            Self::MultlineStar => "multline*",
98        }
99    }
100
101    /// Whether rows in this environment are organised by `&` column
102    /// separators that should be aligned by canonicalisation.
103    /// `equation`, `gather`, and `multline` are vertical-list
104    /// environments without column alignment.
105    pub const fn is_aligning(self) -> bool {
106        matches!(
107            self,
108            Self::Align
109                | Self::AlignStar
110                | Self::Aligned
111                | Self::Matrix
112                | Self::Pmatrix
113                | Self::Bmatrix
114                | Self::Vmatrix
115                | Self::Smallmatrix
116                | Self::Cases
117                | Self::Array
118                | Self::Split
119        )
120    }
121}
122
123impl EnvKind {
124    /// `true` when this environment's rows use `&` for column
125    /// alignment; `Custom` environments are never aligned (we don't
126    /// know their grammar).
127    pub fn is_aligning(&self) -> bool {
128        matches!(self, Self::Known(k) if k.is_aligning())
129    }
130
131    /// The environment name as it appears in source, borrowed from
132    /// `source` for `Custom` and `&'static` for `Known`.
133    pub fn name<'a>(&self, source: &'a str) -> &'a str {
134        match self {
135            Self::Known(k) => k.name(),
136            Self::Custom(range) => source.get(range.clone()).unwrap_or(""),
137        }
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn known_from_name_round_trips() {
147        for k in [
148            KnownEnv::Align,
149            KnownEnv::AlignStar,
150            KnownEnv::Pmatrix,
151            KnownEnv::Cases,
152            KnownEnv::MultlineStar,
153        ] {
154            assert_eq!(KnownEnv::from_name(k.name()), Some(k));
155        }
156    }
157
158    #[test]
159    fn unknown_name_returns_none() {
160        assert_eq!(KnownEnv::from_name("tikzcd"), None);
161        assert_eq!(KnownEnv::from_name("widget"), None);
162    }
163
164    #[test]
165    fn aligning_split() {
166        assert!(KnownEnv::Align.is_aligning());
167        assert!(KnownEnv::Pmatrix.is_aligning());
168        assert!(KnownEnv::Cases.is_aligning());
169        assert!(!KnownEnv::Equation.is_aligning());
170        assert!(!KnownEnv::Gather.is_aligning());
171        assert!(!KnownEnv::Multline.is_aligning());
172    }
173
174    #[test]
175    fn custom_is_never_aligning() {
176        let env = EnvKind::Custom(0..6);
177        assert!(!env.is_aligning());
178    }
179
180    #[test]
181    fn custom_name_resolves_from_source() {
182        let s = "tikzcd";
183        let env = EnvKind::Custom(0..6);
184        assert_eq!(env.name(s), "tikzcd");
185    }
186}