pub mod font;
pub mod input;
pub mod layout;
pub mod locale;
pub mod output;
pub mod rules;
pub mod tree;
use std::fmt;
pub use locale::Locale;
pub use output::braille::BrailleCode;
pub use rules::Applied;
pub use tree::{Class, Node};
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Input {
#[default]
Latex,
MathMl,
AsciiMath,
}
impl Input {
pub const ALL: [Input; 3] = [Input::Latex, Input::MathMl, Input::AsciiMath];
pub fn code(self) -> &'static str {
match self {
Input::Latex => "latex",
Input::MathMl => "mathml",
Input::AsciiMath => "asciimath",
}
}
}
impl fmt::Display for Input {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.code())
}
}
impl std::str::FromStr for Input {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"latex" | "tex" => Ok(Input::Latex),
"mathml" | "mml" => Ok(Input::MathMl),
"asciimath" | "am" => Ok(Input::AsciiMath),
other => Err(format!("unknown input format: {other} (use latex, mathml or asciimath)")),
}
}
}
#[derive(Debug, Clone)]
pub struct Options {
pub locale: Locale,
pub display: bool,
pub strict: bool,
pub input: Input,
pub braille: Option<BrailleCode>,
}
impl Default for Options {
fn default() -> Self {
Options { locale: Locale::default(), display: true, strict: false, input: Input::Latex, braille: None }
}
}
fn read(source: &str, options: &Options, registry: &mut rules::Registry) -> Result<(Node, Input), Error> {
let input = match options.input {
Input::Latex if input::mathml::looks_like_mathml(source) => {
registry.note(rules::L_ENT_4, "MathML pasted as LaTeX: read as MathML");
Input::MathMl
}
other => other,
};
let tree = match input {
Input::Latex => input::latex::read_with(source, options.locale, registry, options.strict)?,
Input::MathMl => input::mathml::read_with(source, options.locale, registry, options.strict)?,
Input::AsciiMath => input::asciimath::read_with(source, options.locale, registry, options.strict)?,
};
Ok((tree, input))
}
#[derive(Debug, Clone)]
pub struct Output {
pub svg: String,
pub tree: Node,
pub speech: String,
pub text: String,
pub braille: String,
pub braille_code: BrailleCode,
pub applied: Vec<Applied>,
pub warnings: Vec<Applied>,
}
impl Output {
pub fn to_json(&self) -> String {
let rules = |list: &[Applied]| -> Vec<serde_json::Value> { list.iter().map(|a| serde_json::json!({ "rule": a.rule, "description": a.description })).collect() };
let tree: serde_json::Value = serde_json::from_str(&self.tree.to_json()).expect("the tree serializes to valid JSON");
serde_json::json!({
"svg": self.svg,
"speech": self.speech,
"text": self.text,
"braille": self.braille,
"braille_code": self.braille_code.code(),
"tree": tree,
"applied": rules(&self.applied),
"warnings": rules(&self.warnings),
})
.to_string()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Error {
Syntax { column: usize, message: String },
MissingGlyph { character: char },
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Syntax { column, message } => write!(f, "column {column}: {message}"),
Error::MissingGlyph { character } => {
write!(f, "the {} font has no '{character}' character (U+{:04X})", font::NAME, *character as u32)
}
}
}
}
impl std::error::Error for Error {}
pub fn convert(tex: &str, options: &Options) -> Result<Output, Error> {
convert_in_font(tex, options, font::MathFont::default())
}
pub fn convert_in_font(tex: &str, options: &Options, font: font::MathFont) -> Result<Output, Error> {
let mut registry = rules::Registry::default();
let (tree, input) = read(tex, options, &mut registry)?;
let style = if options.display { layout::Style::display() } else { layout::Style::text() }.in_font(font);
let math_box = {
let mut ctx = layout::Context { locale: options.locale, registry: &mut registry, strict: options.strict };
layout::compose(&tree, style, &mut ctx)?
};
let speech = output::speech::speak(&tree, options.locale);
let text = output::text::linear(&tree, options.locale);
let braille_code = options.braille.unwrap_or(BrailleCode::for_locale(options.locale));
let braille = output::braille::transcribe(&tree, braille_code);
for c in &braille.missing {
registry.note(rules::S_BRL_1, format!("no {braille_code} braille sign for '{c}' (U+{:04X}): written as the full cell ⠿", *c as u32));
}
let tree_json = tree.to_json();
registry.dedup();
let svg = output::svg::generate(&output::svg::Content {
math_box: &math_box,
source: tex,
input,
tree_json: &tree_json,
speech: &speech,
text: &text,
braille: &braille.text,
braille_code,
locale: options.locale,
warnings: ®istry.warnings,
font,
});
Ok(Output { svg, tree, speech, text, braille: braille.text, braille_code, applied: registry.applied, warnings: registry.warnings })
}
pub fn typeset(tex: &str, options: &Options) -> Result<(layout::MathBox, Vec<Applied>), Error> {
typeset_in_font(tex, options, font::MathFont::default())
}
pub fn typeset_in_font(tex: &str, options: &Options, font: font::MathFont) -> Result<(layout::MathBox, Vec<Applied>), Error> {
let mut registry = rules::Registry::default();
let (tree, _) = read(tex, options, &mut registry)?;
let style = if options.display { layout::Style::display() } else { layout::Style::text() }.in_font(font);
let mut ctx = layout::Context { locale: options.locale, registry: &mut registry, strict: options.strict };
let math_box = layout::compose(&tree, style, &mut ctx)?;
registry.dedup();
Ok((math_box, registry.warnings))
}
pub fn tex_to_svg(tex: &str) -> Result<String, Error> {
convert(tex, &Options::default()).map(|r| r.svg)
}