Skip to main content

mathra/
lib.rs

1//! mathra: equations as a tree of meaning, drawn with explicit rules.
2//!
3//! ```
4//! let svg = mathra::tex_to_svg("3.14").unwrap();
5//! assert!(svg.contains("-speech\">3.14</title>"));
6//! ```
7
8pub mod font;
9pub mod input;
10pub mod layout;
11pub mod locale;
12pub mod output;
13pub mod rules;
14pub mod tree;
15
16use std::fmt;
17
18pub use locale::Locale;
19pub use output::braille::BrailleCode;
20pub use rules::Applied;
21pub use tree::{Class, Node};
22
23pub const VERSION: &str = env!("CARGO_PKG_VERSION");
24
25/// The notation the equation is written in.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum Input {
28    /// Math LaTeX. MathML pasted here (starting with `<math`) is recognized and read as MathML (L-ENT-4).
29    #[default]
30    Latex,
31    /// Presentation MathML: what Word, Google Docs, MathType and MathJax export.
32    MathMl,
33    /// AsciiMath (asciimath.org): `sum_(i=1)^n i^2`.
34    AsciiMath,
35}
36
37impl Input {
38    pub const ALL: [Input; 3] = [Input::Latex, Input::MathMl, Input::AsciiMath];
39
40    pub fn code(self) -> &'static str {
41        match self {
42            Input::Latex => "latex",
43            Input::MathMl => "mathml",
44            Input::AsciiMath => "asciimath",
45        }
46    }
47}
48
49impl fmt::Display for Input {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        f.write_str(self.code())
52    }
53}
54
55impl std::str::FromStr for Input {
56    type Err = String;
57
58    /// `latex` (or `tex`), `mathml` (or `mml`), `asciimath` (or `am`), in any case.
59    fn from_str(s: &str) -> Result<Self, Self::Err> {
60        match s.to_ascii_lowercase().as_str() {
61            "latex" | "tex" => Ok(Input::Latex),
62            "mathml" | "mml" => Ok(Input::MathMl),
63            "asciimath" | "am" => Ok(Input::AsciiMath),
64            other => Err(format!("unknown input format: {other} (use latex, mathml or asciimath)")),
65        }
66    }
67}
68
69#[derive(Debug, Clone)]
70pub struct Options {
71    pub locale: Locale,
72    /// Display equation (like `$$…$$`), or inline with the text (like `$…$`).
73    pub display: bool,
74    /// Refuse invalid LaTeX, like TeX does. Without it, mathra recovers what it can and returns warnings.
75    pub strict: bool,
76    /// The notation of the input. LaTeX by default.
77    pub input: Input,
78    /// The braille code of [`Output::braille`]. `None`: the language's (CMU in pt-BR and es, Nemeth elsewhere).
79    pub braille: Option<BrailleCode>,
80}
81
82impl Default for Options {
83    fn default() -> Self {
84        Options { locale: Locale::default(), display: true, strict: false, input: Input::Latex, braille: None }
85    }
86}
87
88/// The tree of meaning of an equation, in the notation `options.input` says.
89fn read(source: &str, options: &Options, registry: &mut rules::Registry) -> Result<(Node, Input), Error> {
90    let input = match options.input {
91        Input::Latex if input::mathml::looks_like_mathml(source) => {
92            registry.note(rules::L_ENT_4, "MathML pasted as LaTeX: read as MathML");
93            Input::MathMl
94        }
95        other => other,
96    };
97    let tree = match input {
98        Input::Latex => input::latex::read_with(source, options.locale, registry, options.strict)?,
99        Input::MathMl => input::mathml::read_with(source, options.locale, registry, options.strict)?,
100        Input::AsciiMath => input::asciimath::read_with(source, options.locale, registry, options.strict)?,
101    };
102    Ok((tree, input))
103}
104
105#[derive(Debug, Clone)]
106pub struct Output {
107    pub svg: String,
108    pub tree: Node,
109    pub speech: String,
110    pub text: String,
111    /// The equation in braille (Unicode braille cells), in the code of `braille_code`.
112    pub braille: String,
113    pub braille_code: BrailleCode,
114    /// Rules that changed something in this equation, in the order they acted.
115    pub applied: Vec<Applied>,
116    /// Input problems the tolerant mode worked around (empty in strict mode).
117    pub warnings: Vec<Applied>,
118}
119
120impl Output {
121    /// Every output as one JSON object: `svg`, `speech`, `text`, `tree`, `applied` and `warnings`, the
122    /// last two as lists of `{"rule", "description"}`. What the JavaScript and Python packages return.
123    pub fn to_json(&self) -> String {
124        let rules = |list: &[Applied]| -> Vec<serde_json::Value> { list.iter().map(|a| serde_json::json!({ "rule": a.rule, "description": a.description })).collect() };
125        let tree: serde_json::Value = serde_json::from_str(&self.tree.to_json()).expect("the tree serializes to valid JSON");
126        serde_json::json!({
127            "svg": self.svg,
128            "speech": self.speech,
129            "text": self.text,
130            "braille": self.braille,
131            "braille_code": self.braille_code.code(),
132            "tree": tree,
133            "applied": rules(&self.applied),
134            "warnings": rules(&self.warnings),
135        })
136        .to_string()
137    }
138}
139
140#[derive(Debug, Clone, PartialEq)]
141pub enum Error {
142    Syntax { column: usize, message: String },
143    MissingGlyph { character: char },
144}
145
146impl fmt::Display for Error {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        match self {
149            Error::Syntax { column, message } => write!(f, "column {column}: {message}"),
150            Error::MissingGlyph { character } => {
151                write!(f, "the {} font has no '{character}' character (U+{:04X})", font::NAME, *character as u32)
152            }
153        }
154    }
155}
156
157impl std::error::Error for Error {}
158
159/// Converts an equation (LaTeX by default; see [`Options::input`]) to SVG, speech, text and tree.
160pub fn convert(tex: &str, options: &Options) -> Result<Output, Error> {
161    convert_in_font(tex, options, font::MathFont::default())
162}
163
164/// [`convert`], with the equation set in `font`.
165pub fn convert_in_font(tex: &str, options: &Options, font: font::MathFont) -> Result<Output, Error> {
166    let mut registry = rules::Registry::default();
167    let (tree, input) = read(tex, options, &mut registry)?;
168    let style = if options.display { layout::Style::display() } else { layout::Style::text() }.in_font(font);
169    let math_box = {
170        let mut ctx = layout::Context { locale: options.locale, registry: &mut registry, strict: options.strict };
171        layout::compose(&tree, style, &mut ctx)?
172    };
173    let speech = output::speech::speak(&tree, options.locale);
174    let text = output::text::linear(&tree, options.locale);
175    let braille_code = options.braille.unwrap_or(BrailleCode::for_locale(options.locale));
176    let braille = output::braille::transcribe(&tree, braille_code);
177    for c in &braille.missing {
178        registry.note(rules::S_BRL_1, format!("no {braille_code} braille sign for '{c}' (U+{:04X}): written as the full cell ⠿", *c as u32));
179    }
180    let tree_json = tree.to_json();
181    registry.dedup();
182    let svg = output::svg::generate(&output::svg::Content {
183        math_box: &math_box,
184        source: tex,
185        input,
186        tree_json: &tree_json,
187        speech: &speech,
188        text: &text,
189        braille: &braille.text,
190        braille_code,
191        locale: options.locale,
192        warnings: &registry.warnings,
193        font,
194    });
195    Ok(Output { svg, tree, speech, text, braille: braille.text, braille_code, applied: registry.applied, warnings: registry.warnings })
196}
197
198/// The composed box, without generating SVG: for callers that draw the equation inside something
199/// else (tikra's figure labels). Also returns the tolerant mode's warnings.
200pub fn typeset(tex: &str, options: &Options) -> Result<(layout::MathBox, Vec<Applied>), Error> {
201    typeset_in_font(tex, options, font::MathFont::default())
202}
203
204/// [`typeset`], with the equation set in `font` (its glyph ids are that font's).
205pub fn typeset_in_font(tex: &str, options: &Options, font: font::MathFont) -> Result<(layout::MathBox, Vec<Applied>), Error> {
206    let mut registry = rules::Registry::default();
207    let (tree, _) = read(tex, options, &mut registry)?;
208    let style = if options.display { layout::Style::display() } else { layout::Style::text() }.in_font(font);
209    let mut ctx = layout::Context { locale: options.locale, registry: &mut registry, strict: options.strict };
210    let math_box = layout::compose(&tree, style, &mut ctx)?;
211    registry.dedup();
212    Ok((math_box, registry.warnings))
213}
214
215/// Shortcut with the default locale (English).
216pub fn tex_to_svg(tex: &str) -> Result<String, Error> {
217    convert(tex, &Options::default()).map(|r| r.svg)
218}