texform-core 0.1.0

Parser, document tree, and serializer for TeXForm (internal; use the texform crate)
Documentation
mod support;

use support::parser::*;
use texform_core::parse::{AllowedMode, CommandKind, ParseConfig};
use texform_interface::syntax_node::{ArgumentValue, ContentMode, SyntaxNode};

#[test]
fn underline_uses_math_and_text_variants_in_matching_modes() {
    let ctx = test_context_with_items([
        command_item("underline", CommandKind::Prefix, AllowedMode::Math, "m"),
        command_item("underline", CommandKind::Prefix, AllowedMode::Text, "m:T"),
    ]);

    let math = ctx
        .parse(r"\underline{x}", &ParseConfig::LENIENT)
        .try_into_document()
        .expect("expected math parse result")
        .0
        .to_syntax();
    let text = ctx
        .parse(r"\text{a \underline{b}}", &ParseConfig::LENIENT)
        .try_into_document()
        .expect("expected text parse result")
        .0
        .to_syntax();

    match math {
        SyntaxNode::Root { children, .. } => match &children[0] {
            SyntaxNode::Command { args, .. } => {
                assert_eq!(unwrap_content(&args[0]), &SyntaxNode::Char('x'));
            }
            other => panic!("expected underline command, got {:?}", other),
        },
        other => panic!("expected root node, got {:?}", other),
    }

    match text {
        SyntaxNode::Root { children, .. } => match &children[0] {
            SyntaxNode::Command { args, .. } => match unwrap_content(&args[0]) {
                SyntaxNode::Group {
                    mode: ContentMode::Text,
                    children,
                    ..
                } => {
                    assert_eq!(children.len(), 2);
                    assert_eq!(children[0], SyntaxNode::Text("a ".to_string()));
                    match &children[1] {
                        SyntaxNode::Command { args, .. } => match &expect_arg(&args[0]).value {
                            ArgumentValue::TextContent(node) => {
                                assert_eq!(node, &SyntaxNode::Text("b".to_string()));
                            }
                            other => panic!("expected text content argument, got {:?}", other),
                        },
                        other => panic!("expected nested underline command, got {:?}", other),
                    }
                }
                other => panic!("expected text content group, got {:?}", other),
            },
            other => panic!("expected text command, got {:?}", other),
        },
        other => panic!("expected root node, got {:?}", other),
    }
}

#[test]
fn known_but_disallowed_command_is_mode_error_even_when_non_strict() {
    let ctx = test_context_with_items([command_item(
        "textonly",
        CommandKind::Prefix,
        AllowedMode::Text,
        "m:T",
    )]);

    let output = ctx.parse(r"\textonly{x}", &ParseConfig::LENIENT);
    let messages = output
        .diagnostics
        .into_iter()
        .map(|diag| diag.message)
        .collect::<Vec<_>>();

    assert_eq!(
        messages,
        vec!["Command \\textonly is not allowed in math mode"]
    );
}

#[test]
fn known_but_disallowed_environment_is_mode_error_in_both_strictness_modes() {
    let ctx = test_context_with_items([environment_item(
        "textenv",
        AllowedMode::Text,
        ContentMode::Text,
        "",
    )]);

    for strict in [false, true] {
        let config = if strict {
            ParseConfig::STRICT
        } else {
            ParseConfig::LENIENT
        };
        let output = ctx.parse(r"a \begin{textenv}b\end{textenv} c", &config);
        let messages = output
            .diagnostics
            .into_iter()
            .map(|diag| diag.message)
            .collect::<Vec<_>>();

        assert_eq!(
            messages,
            vec!["Environment textenv is not allowed in math mode"],
            "strict={strict}"
        );
    }
}

#[test]
fn disallowed_environment_does_not_rewrite_unrelated_generic_error() {
    let ctx = test_context_with_items([environment_item(
        "textenv",
        AllowedMode::Text,
        ContentMode::Text,
        "",
    )]);

    let output = ctx.parse(r"a \begin{textenv}b\end{textenv} }", &ParseConfig::LENIENT);
    let messages = output
        .diagnostics
        .into_iter()
        .map(|diag| diag.message)
        .collect::<Vec<_>>();

    assert_eq!(
        messages,
        vec![
            "Environment textenv is not allowed in math mode",
            "found '}' expected something else, or end of input",
        ]
    );
}

#[test]
fn test_parser_isolation_for_custom_commands() {
    let ctx1 = test_context_with_items([command_item(
        "fooisolated",
        CommandKind::Prefix,
        AllowedMode::Math,
        "m",
    )]);

    let out1_foo = ctx1.parse(r"\fooisolated{a}", &ParseConfig::STRICT);
    assert!(out1_foo.diagnostics.is_empty());
    assert!(out1_foo.document().is_some());

    let out1_bar = ctx1.parse(r"\barisolated{a}", &ParseConfig::STRICT);
    assert!(!out1_bar.diagnostics.is_empty());
    assert!(out1_bar.document().is_none());

    let ctx2 = test_context_with_items([command_item(
        "barisolated",
        CommandKind::Prefix,
        AllowedMode::Math,
        "m",
    )]);

    let out2_bar = ctx2.parse(r"\barisolated{a}", &ParseConfig::STRICT);
    assert!(out2_bar.diagnostics.is_empty());
    assert!(out2_bar.document().is_some());

    let out2_foo = ctx2.parse(r"\fooisolated{a}", &ParseConfig::STRICT);
    assert!(!out2_foo.diagnostics.is_empty());
    assert!(out2_foo.document().is_none());
}