Skip to main content

mdwright_mathrender/
profile.rs

1//! Renderer profile.
2//!
3//! A profile records *which renderer* a math body is checked against, which
4//! package set that renderer has loaded, and which user macros are in scope.
5//! Compatibility tables live in `tables`; the profile is the *consumer* of
6//! those tables.
7
8use std::collections::HashMap;
9
10/// Math renderer that consumes the TeX-shaped source.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum Renderer {
13    /// MathJax v3 (mjs3, the current stable line).
14    MathJaxV3,
15    /// KaTeX (any release in the 0.16+ feature window).
16    Katex,
17}
18
19impl Renderer {
20    /// User-facing name. Goes into lint diagnostic messages.
21    #[must_use]
22    pub const fn name(self) -> &'static str {
23        match self {
24            Self::MathJaxV3 => "MathJax v3",
25            Self::Katex => "KaTeX",
26        }
27    }
28
29    /// User-facing noun for "package" when speaking about this renderer:
30    /// MathJax says *package*, KaTeX says *extension*. Used by lint
31    /// diagnostic text so the message reads naturally for the active
32    /// renderer.
33    #[must_use]
34    pub const fn package_noun(self) -> &'static str {
35        match self {
36            Self::MathJaxV3 => "package",
37            Self::Katex => "extension",
38        }
39    }
40}
41
42/// Package bitmask, shared across renderers. Each renderer's table maps its
43/// commands to one of these bits; the same bit (e.g. `MHCHEM`) means
44/// "MathJax's mhchem extension" or "KaTeX's mhchem extension" depending on
45/// the active profile.
46#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
47pub(crate) struct PackageMask(u32);
48
49impl PackageMask {
50    pub(crate) const BASE: Self = Self(1 << 0);
51    pub(crate) const AMS: Self = Self(1 << 1);
52    pub(crate) const NEWCOMMAND: Self = Self(1 << 2);
53    pub(crate) const CONFIGMACROS: Self = Self(1 << 3);
54    pub(crate) const BOLDSYMBOL: Self = Self(1 << 4);
55    pub(crate) const REQUIRE: Self = Self(1 << 5);
56    pub(crate) const NOUNDEFINED: Self = Self(1 << 6);
57    pub(crate) const COLOR: Self = Self(1 << 7);
58    pub(crate) const CANCEL: Self = Self(1 << 8);
59    pub(crate) const ENCLOSE: Self = Self(1 << 9);
60    pub(crate) const MHCHEM: Self = Self(1 << 10);
61    pub(crate) const PHYSICS: Self = Self(1 << 11);
62    pub(crate) const AMSCD: Self = Self(1 << 12);
63    pub(crate) const BRACEMATCH: Self = Self(1 << 13);
64    pub(crate) const TEXTMACROS: Self = Self(1 << 14);
65    pub(crate) const MATHTOOLS: Self = Self(1 << 15);
66
67    pub(crate) const fn contains(self, other: Self) -> bool {
68        (self.0 & other.0) != 0
69    }
70
71    pub(crate) const fn union(self, other: Self) -> Self {
72        Self(self.0 | other.0)
73    }
74}
75
76/// Resolve a package name (as users would write in config) to its mask bit.
77/// Returns `None` for unknown names so callers can surface them. Names follow
78/// the renderer's own conventions; both MathJax and KaTeX use the same names
79/// (`mhchem`, `physics`, `color`, `cancel`, …) so one resolver works for both.
80pub(crate) fn package_from_name(name: &str) -> Option<PackageMask> {
81    match name {
82        "base" => Some(PackageMask::BASE),
83        "ams" => Some(PackageMask::AMS),
84        "newcommand" => Some(PackageMask::NEWCOMMAND),
85        "configmacros" => Some(PackageMask::CONFIGMACROS),
86        "boldsymbol" => Some(PackageMask::BOLDSYMBOL),
87        "require" => Some(PackageMask::REQUIRE),
88        "noundefined" => Some(PackageMask::NOUNDEFINED),
89        "color" => Some(PackageMask::COLOR),
90        "cancel" => Some(PackageMask::CANCEL),
91        "enclose" => Some(PackageMask::ENCLOSE),
92        "mhchem" => Some(PackageMask::MHCHEM),
93        "physics" => Some(PackageMask::PHYSICS),
94        "amscd" => Some(PackageMask::AMSCD),
95        "bracematch" => Some(PackageMask::BRACEMATCH),
96        "textmacros" => Some(PackageMask::TEXTMACROS),
97        "mathtools" => Some(PackageMask::MATHTOOLS),
98        _ => None,
99    }
100}
101
102/// Canonical user-facing name for a single package mask. Used in diagnostic text.
103pub(crate) fn package_name(mask: PackageMask) -> &'static str {
104    if mask.contains(PackageMask::BASE) {
105        "base"
106    } else if mask.contains(PackageMask::AMS) {
107        "ams"
108    } else if mask.contains(PackageMask::MHCHEM) {
109        "mhchem"
110    } else if mask.contains(PackageMask::PHYSICS) {
111        "physics"
112    } else if mask.contains(PackageMask::COLOR) {
113        "color"
114    } else if mask.contains(PackageMask::CANCEL) {
115        "cancel"
116    } else if mask.contains(PackageMask::ENCLOSE) {
117        "enclose"
118    } else if mask.contains(PackageMask::AMSCD) {
119        "amscd"
120    } else if mask.contains(PackageMask::BOLDSYMBOL) {
121        "boldsymbol"
122    } else if mask.contains(PackageMask::NEWCOMMAND) {
123        "newcommand"
124    } else if mask.contains(PackageMask::CONFIGMACROS) {
125        "configmacros"
126    } else if mask.contains(PackageMask::REQUIRE) {
127        "require"
128    } else if mask.contains(PackageMask::NOUNDEFINED) {
129        "noundefined"
130    } else if mask.contains(PackageMask::BRACEMATCH) {
131        "bracematch"
132    } else if mask.contains(PackageMask::TEXTMACROS) {
133        "textmacros"
134    } else if mask.contains(PackageMask::MATHTOOLS) {
135        "mathtools"
136    } else {
137        "unknown"
138    }
139}
140
141/// A configured renderer profile: which renderer, which packages it has
142/// loaded, and which user-declared macros are in scope.
143#[derive(Clone, Debug)]
144pub struct RenderProfile {
145    pub(crate) renderer: Renderer,
146    pub(crate) packages: PackageMask,
147    pub(crate) macros: HashMap<String, u8>,
148}
149
150impl Default for RenderProfile {
151    fn default() -> Self {
152        Self::mathjax_v3()
153    }
154}
155
156impl RenderProfile {
157    /// MathJax v3 with the default autoload set (`base`, `ams`, `newcommand`,
158    /// `noundefined`, `require`, `configmacros`, `boldsymbol`).
159    #[must_use]
160    pub fn mathjax_v3() -> Self {
161        let packages = PackageMask::BASE
162            .union(PackageMask::AMS)
163            .union(PackageMask::NEWCOMMAND)
164            .union(PackageMask::NOUNDEFINED)
165            .union(PackageMask::REQUIRE)
166            .union(PackageMask::CONFIGMACROS)
167            .union(PackageMask::BOLDSYMBOL);
168        Self {
169            renderer: Renderer::MathJaxV3,
170            packages,
171            macros: HashMap::new(),
172        }
173    }
174
175    /// KaTeX with its always-available core (KaTeX core covers what MathJax
176    /// splits between `base` and `ams`, so both bits are set). Extensions
177    /// (`mhchem`, `physics`, `cancel`, `color`, `mathtools`) are opt-in via
178    /// `with_package`.
179    #[must_use]
180    pub fn katex() -> Self {
181        let packages = PackageMask::BASE
182            .union(PackageMask::AMS)
183            .union(PackageMask::NEWCOMMAND)
184            .union(PackageMask::CONFIGMACROS);
185        Self {
186            renderer: Renderer::Katex,
187            packages,
188            macros: HashMap::new(),
189        }
190    }
191
192    /// Which renderer this profile targets.
193    #[must_use]
194    pub const fn renderer(&self) -> Renderer {
195        self.renderer
196    }
197
198    /// Load a package/extension by name (e.g. `"mhchem"`, `"physics"`).
199    /// Unknown names are silently ignored; users learn about missing packages
200    /// through check diagnostics, not the profile builder.
201    #[must_use]
202    pub fn with_package(mut self, package: &str) -> Self {
203        if let Some(mask) = package_from_name(package) {
204            self.packages = self.packages.union(mask);
205        }
206        self
207    }
208
209    /// Declare a user-defined macro known to be available at render time.
210    /// The arity is informational only; the checker treats the name as defined
211    /// and does not validate argument counts.
212    #[must_use]
213    pub fn with_macro(mut self, name: impl Into<String>, arity: u8) -> Self {
214        self.macros.insert(name.into(), arity);
215        self
216    }
217
218    pub(crate) fn has_package(&self, mask: PackageMask) -> bool {
219        self.packages.contains(mask)
220    }
221
222    pub(crate) fn has_macro(&self, name: &str) -> bool {
223        self.macros.contains_key(name)
224    }
225}