use std::{
fs,
io::{Read, Write},
path::{Path, PathBuf},
};
use clap::Parser;
use math_core::{LatexError, LatexToMathML, MathDisplay};
mod config_file;
mod html_entities;
mod replace;
use replace::{ConversionError, Replacer};
static DEFAULT_CONFIG_FILE: &str = "mathcore.toml";
#[derive(Parser, Debug)]
#[command(version, about = "Converts LaTeX formulas to MathML", long_about = None)]
struct Args {
#[arg(conflicts_with = "formula", value_name = "FILE")]
file: Option<PathBuf>,
#[arg(
long,
default_value = "$",
conflicts_with = "formula",
value_name = "STR"
)]
inline_del: String,
#[arg(
long,
default_value = "$$",
conflicts_with = "formula",
value_name = "STR"
)]
block_del: String,
#[arg(
long,
conflicts_with = "inline_del",
requires = "inline_close",
value_name = "STR"
)]
inline_open: Option<String>,
#[arg(
long,
conflicts_with = "inline_del",
requires = "inline_open",
value_name = "STR"
)]
inline_close: Option<String>,
#[arg(
long,
conflicts_with = "block_del",
requires = "block_close",
value_name = "STR"
)]
block_open: Option<String>,
#[arg(
long,
conflicts_with = "block_del",
requires = "block_open",
value_name = "STR"
)]
block_close: Option<String>,
#[arg(short, long, conflicts_with = "formula")]
recursive: bool,
#[arg(short, long, conflicts_with_all = ["formula", "recursive"])]
write: bool,
#[arg(long, conflicts_with = "formula")]
dry_run: bool,
#[arg(long, conflicts_with = "formula")]
ignore_escaped_delim: bool,
#[arg(long, conflicts_with = "formula")]
continue_on_error: bool,
#[arg(short, long, conflicts_with = "file")]
formula: Option<String>,
#[arg(short, long, conflicts_with = "file", group = "mode")]
inline: bool,
#[arg(short, long, conflicts_with = "file", group = "mode")]
block: bool,
#[arg(short, long, value_name = "FILE")]
config_file: Option<PathBuf>,
}
fn main() {
let args = Args::parse();
let config_path = args
.config_file
.as_deref()
.unwrap_or_else(|| Path::new(DEFAULT_CONFIG_FILE));
let config = match config_file::load_config_file(config_path) {
Ok(config) => config,
Err(config_file::ConfigError::Io(ref io_err))
if io_err.kind() == std::io::ErrorKind::NotFound =>
{
if args.config_file.is_none() {
config_file::Config::default()
} else {
eprintln!("Config file '{}' not found", config_path.display());
std::process::exit(3);
}
}
Err(err) => {
eprintln!(
"Failed to load config file '{}': {}",
config_path.display(),
err
);
std::process::exit(4);
}
};
let mut converter = LatexToMathML::new(config.math_core).unwrap_or_else(|err| {
render_ariadne_report(&err.0, &format!("macro {}", err.1), &err.2);
std::process::exit(2);
});
if let Some(fpath) = &args.file {
let inline_delim: (&str, &str) = if let Some(open) = &args.inline_open {
(open, args.inline_close.as_ref().unwrap())
} else {
(&args.inline_del, &args.inline_del)
};
let block_delim: (&str, &str) = if let Some(open) = &args.block_open {
(open, args.block_close.as_ref().unwrap())
} else {
(&args.block_del, &args.block_del)
};
let mut replacer = Replacer::new(inline_delim, block_delim, args.ignore_escaped_delim);
if fpath == &PathBuf::from("-") {
let input = read_stdin();
match replace(
&mut replacer,
&input,
&mut converter,
args.continue_on_error,
) {
Ok(mathml) => {
println!("{mathml}");
}
Err(e) => exit_conversion_error(e, None),
}
} else if args.recursive {
convert_html_recursive(&args, fpath, &mut replacer, &mut converter);
} else {
convert_html(&args, fpath, args.write, &mut replacer, &mut converter);
}
} else if let Some(formula) = &args.formula {
convert_and_exit(&args, formula, &mut converter);
} else {
convert_and_exit(&args, &read_stdin(), &mut converter);
}
}
fn read_stdin() -> String {
let mut buffer = String::new();
if let Err(e) = std::io::stdin().read_to_string(&mut buffer) {
exit_io_error(&e);
}
buffer
}
fn convert_and_exit(args: &Args, latex: &str, converter: &mut LatexToMathML) {
let display = if args.block {
MathDisplay::Block
} else {
MathDisplay::Inline
};
match converter.convert_with_global_state(latex, display) {
Ok(mathml) => println!("{}", mathml.mathml),
Err(e) => {
render_ariadne_report(&e, "<input>", latex);
std::process::exit(2);
}
}
}
fn replace<'source, 'buf>(
replacer: &'buf mut Replacer,
input: &'source str,
converter: &'buf mut LatexToMathML,
continue_on_error: bool,
) -> Result<String, ConversionError<'source, 'buf>>
where
'source: 'buf,
{
replacer.replace(input, converter, |converter, buf, latex, display| {
let result = converter
.convert_with_global_state(latex, display)
.map(|mathml| mathml.mathml);
let result = if continue_on_error {
result.unwrap_or_else(|err| err.to_html(latex, display, None))
} else {
result?
};
buf.push_str(result.as_str());
Ok(())
})
}
fn convert_html_recursive(
args: &Args,
path: &Path,
replacer: &mut Replacer,
converter: &mut LatexToMathML,
) {
if path.is_dir() {
let dir = fs::read_dir(path).unwrap_or_else(|e| exit_io_error(&e));
for entry in dir.filter_map(Result::ok) {
convert_html_recursive(args, entry.path().as_ref(), replacer, converter)
}
} else if path.is_file()
&& let Some(ext) = path.extension()
&& ext == "html"
{
convert_html(args, path, true, replacer, converter);
}
}
fn convert_html(
args: &Args,
fp: &Path,
write: bool,
replacer: &mut Replacer,
converter: &mut LatexToMathML,
) {
let original = fs::read_to_string(fp).unwrap_or_else(|e| exit_io_error(&e));
let converted = replace(replacer, &original, converter, args.continue_on_error)
.unwrap_or_else(|e| exit_conversion_error(e, Some(fp)));
if args.dry_run {
return;
}
if write {
if original != converted {
let mut fp = fs::File::create(fp).unwrap_or_else(|e| exit_io_error(&e));
fp.write_all(converted.as_bytes())
.unwrap_or_else(|e| exit_io_error(&e));
}
} else {
print!("{converted}");
}
}
fn render_ariadne_report(error: &LatexError, source_name: &str, input: &str) {
let report = error.to_report(source_name, true);
report
.eprint((source_name, ariadne::Source::from(input)))
.expect("failed to write report");
}
fn exit_conversion_error<E: std::error::Error>(e: E, fp: Option<&Path>) -> ! {
eprint!("Conversion error");
if let Some(fp) = fp {
eprint!(" in '{}'", fp.display());
}
eprintln!(": {e}");
std::process::exit(2);
}
fn exit_io_error(e: &std::io::Error) -> ! {
eprintln!("IO Error: {e}");
std::process::exit(1);
}
#[cfg(test)]
mod tests {
#[test]
fn full_test() {
let text = r#"
Let us consider a rigid sphere (i.e., one having a spherical figure when tested in the stationary system) of radius $R$
which is at rest relative to the system ($K$), and whose centre coincides with the origin of $K$ then the equation of the
surface of this sphere, which is moving with a velocity $v$ relative to $K$, is
$$\xi^2 + \eta^2 + \zeta^2 = R^2$$
At time $t = 0$ the equation is expressed by means of $(x, y, z, t)$ as
$$\frac{ x^2 }{ \left( \sqrt{ 1 - \frac{ v^2 }{ c^2 } } \right)^2 } + y^2 + z^2 = R^2 .$$
A rigid body which has the figure of a sphere when measured in the moving system, has therefore in the moving
condition — when considered from the stationary system, the figure of a rotational ellipsoid with semi-axes
$$R {\sqrt{1-{\frac {v^{2}}{c^{2}}}}}, \ R, \ R .$$
"#;
let mut converter =
math_core::LatexToMathML::new(math_core::MathCoreConfig::default()).unwrap();
let mut replacer = crate::Replacer::new(("$", "$"), ("$$", "$$"), false);
let mathml = crate::replace(&mut replacer, text, &mut converter, false).unwrap();
println!("{}", mathml);
}
}