pulldown-latex 0.8.0

Pull parser to convert LaTeX equations to MathML
Documentation
use heck::ToTitleCase;
use inventory::collect;
use libtest_mimic::{Arguments, Conclusion, Failed, Trial};
use pulldown_latex::{config::RenderConfig, mathml::push_mathml, Parser, Storage};
use std::{io::Write, sync::Mutex};

#[allow(clippy::type_complexity)]
pub static RENDERED: Mutex<Vec<(&str, Vec<(&str, String)>)>> = Mutex::new(Vec::new());
pub const OUTPUT_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/out");

pub struct TestCase {
    pub name: &'static str,
    pub test: fn() -> Result<(), Failed>,
}
collect!(TestCase);

pub fn test() -> Conclusion {
    let args = Arguments::from_args();
    let tests = inventory::iter::<TestCase>
        .into_iter()
        .map(|TestCase { name, test }| Trial::test(*name, test))
        .collect::<Vec<_>>();
    libtest_mimic::run(&args, tests)
}

pub fn round_trip(
    fn_name: &'static str,
    inputs: &[&'static str],
    config: RenderConfig,
) -> Result<(), Failed> {
    let mut storage = Storage::new();
    let rendered: Vec<_> = inputs
        .iter()
        .map(|input| -> Result<_, Failed> {
            show_errors(Parser::new(input, &storage))?;
            let parser = Parser::new(input, &storage);
            let mut output = String::new();
            push_mathml(&mut output, parser, config)?;
            storage.reset();
            Ok((*input, output))
        })
        .collect::<Result<_, Failed>>()?;

    RENDERED.lock()?.push((fn_name, rendered));

    Ok(())
}

pub fn show_errors(parser: Parser) -> Result<(), usize> {
    let mut error_count = 0;

    parser.for_each(|event| {
        if let Err(e) = event {
            eprintln!("{:?}", e);
            error_count += 1;
        }
    });

    if error_count > 0 {
        Err(error_count)
    } else {
        Ok(())
    }
}

// This shared module is also compiled into the cross-browser test, which uses
// its own table renderer.
#[allow(dead_code)]
pub fn tabled(file: &mut std::fs::File) -> anyhow::Result<()> {
    file.write_all(br#"<table style="max-width: 60vw; margin: auto;">"#)?;
    let mut rendered = RENDERED.lock().unwrap();
    rendered.sort();
    rendered
        .iter()
        .try_for_each(|(table_name, rows)| -> std::io::Result<()> {
            file.write_fmt(format_args!(
                r#"<tr><th colspan="2">{table_name}</th></tr>"#,
                table_name = table_name.to_title_case()
            ))?;

            rows.iter().try_for_each(|(input, output)| {
                file.write_fmt(format_args!(
                    r#"<tr><td>{input}</td><td style="position: relative">{output}</td></tr>"#,
                    input = input,
                    output = output
                ))
            })
        })?;
    file.write_all(b"</table>")?;
    Ok(())
}

pub fn html_template(
    file: &mut std::fs::File,
    title: &str,
    stylesheet: Option<&str>,
    render: impl FnOnce(&mut std::fs::File) -> anyhow::Result<()>,
) -> anyhow::Result<()> {
    let styles = match stylesheet {
        Some(stylesheet) => {
            format!(r#"<link rel="stylesheet" type="text/css" href="{OUTPUT_DIR}/{stylesheet}">"#,)
        }
        None => "".to_string(),
    };

    file.write_fmt(format_args!(
        r#"<!DOCTYPE html>
<html>
<head>
<title>{title}</title>
<link rel="stylesheet" type="text/css" href="{}/styles.css">
<meta charset="UTF-8">
{styles}</head>
<body>
"#,
        env!("CARGO_MANIFEST_DIR")
    ))?;

    render(file)?;

    file.write_fmt(format_args!(
        r#"</body>
</html>"#
    ))?;
    Ok(())
}

#[macro_export]
macro_rules! round_trip_display {
    ($name:ident, $($input:literal),+ $(,)?) => {
        $crate::round_trip!(
            $name,
            $($input),+,
            display_mode = pulldown_latex::config::DisplayMode::Block
        );
    };
    (should_panic, $name:ident, $($input:literal),+ $(,)?) => {
        $crate::round_trip!(
            should_panic,
            $name,
            $($input),+
        );
    }
}

#[macro_export]
macro_rules! round_trip {
    (should_panic, $name:ident, $($input:literal),+ $(,)?) => {
        pub fn $name() -> Result<(), libtest_mimic::Failed> {
            let inputs = &[$($input),*];
            let mut storage = pulldown_latex::Storage::new();
            for input in inputs {
                let mut parser = pulldown_latex::parser::Parser::new(input, &storage);
                if !parser.all(|event| event.is_err()) {
                    return Err(libtest_mimic::Failed::without_message());
                };
                storage.reset();
            }
            Ok(())
        }

        inventory::submit! {
            $crate::common::TestCase {
                name: stringify!($name),
                test: $name
            }
        }
    };
    ($name:ident, $($input:literal),+ $(, $field:ident = $value:expr)* $(,)?) => {
        pub fn $name() -> Result<(), libtest_mimic::Failed> {
            let inputs = &[$($input),*];
            $crate::common::round_trip(stringify!($name), inputs, pulldown_latex::config::RenderConfig {
                $($field: $value,)*
                ..Default::default()
            })
        }

        inventory::submit! {
            $crate::common::TestCase {
                name: stringify!($name),
                test: $name
            }
        }
    };
}