1pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum Input {
28 #[default]
30 Latex,
31 MathMl,
33 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 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 pub display: bool,
74 pub strict: bool,
76 pub input: Input,
78 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
88fn 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 pub braille: String,
113 pub braille_code: BrailleCode,
114 pub applied: Vec<Applied>,
116 pub warnings: Vec<Applied>,
118}
119
120impl Output {
121 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
159pub fn convert(tex: &str, options: &Options) -> Result<Output, Error> {
161 convert_in_font(tex, options, font::MathFont::default())
162}
163
164pub 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: ®istry.warnings,
193 font,
194 });
195 Ok(Output { svg, tree, speech, text, braille: braille.text, braille_code, applied: registry.applied, warnings: registry.warnings })
196}
197
198pub fn typeset(tex: &str, options: &Options) -> Result<(layout::MathBox, Vec<Applied>), Error> {
201 typeset_in_font(tex, options, font::MathFont::default())
202}
203
204pub 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
215pub fn tex_to_svg(tex: &str) -> Result<String, Error> {
217 convert(tex, &Options::default()).map(|r| r.svg)
218}