texform-transform 0.1.0

Profile-based AST transform engine for TeXForm (internal; use the texform crate)
Documentation
use texform_core::ast::Ast;
use texform_interface::syntax_node::{
    Argument, ArgumentKind, ArgumentValue, ContentMode, GroupKind, SyntaxNode,
};
use texform_transform::{FinalizeAstConfig, FinalizeAstReport, finalize_ast};

fn run_finalize(ast: &mut Ast, enabled: bool) -> FinalizeAstReport {
    let mut report = FinalizeAstReport::default();
    let config = FinalizeAstConfig { enabled };
    finalize_ast::run(ast, &config, &mut report);
    ast.assert_invariants();
    report
}

fn ast_from(children: Vec<SyntaxNode>) -> Ast {
    Ast::from_syntax_root(&SyntaxNode::Root {
        mode: ContentMode::Math,
        children,
    })
}

fn math_group(children: Vec<SyntaxNode>) -> SyntaxNode {
    SyntaxNode::Group {
        mode: ContentMode::Math,
        kind: GroupKind::Explicit,
        children,
    }
}

fn text_group(children: Vec<SyntaxNode>) -> SyntaxNode {
    SyntaxNode::Group {
        mode: ContentMode::Text,
        kind: GroupKind::Explicit,
        children,
    }
}

fn math_arg(node: SyntaxNode) -> Option<Argument> {
    Some(Argument {
        kind: ArgumentKind::Mandatory,
        value: ArgumentValue::MathContent(node),
    })
}

fn root_children(ast: &Ast) -> Vec<SyntaxNode> {
    match ast.to_syntax_root() {
        SyntaxNode::Root { children, .. } => children,
        other => panic!("expected root node, got {other:?}"),
    }
}

#[test]
fn disabled_config_leaves_adjacent_primes_unchanged() {
    let mut ast = ast_from(vec![
        SyntaxNode::Prime { count: 1 },
        SyntaxNode::Prime { count: 2 },
    ]);

    let report = run_finalize(&mut ast, false);

    assert_eq!(
        root_children(&ast),
        vec![
            SyntaxNode::Prime { count: 1 },
            SyntaxNode::Prime { count: 2 },
        ]
    );
    assert_eq!(report.steps.merge_adjacent_primes.applied_count, 0);
}

#[test]
fn enabled_config_merges_adjacent_primes_in_same_sequence() {
    let mut ast = ast_from(vec![
        SyntaxNode::Char('a'),
        SyntaxNode::Prime { count: 1 },
        SyntaxNode::Prime { count: 2 },
        SyntaxNode::Char('b'),
        SyntaxNode::Prime { count: 1 },
        SyntaxNode::Prime { count: 1 },
        SyntaxNode::Prime { count: 1 },
    ]);

    let report = run_finalize(&mut ast, true);

    assert_eq!(
        root_children(&ast),
        vec![
            SyntaxNode::Char('a'),
            SyntaxNode::Prime { count: 3 },
            SyntaxNode::Char('b'),
            SyntaxNode::Prime { count: 3 },
        ]
    );
    assert_eq!(report.steps.merge_adjacent_primes.applied_count, 2);
}

#[test]
fn one_contiguous_prime_run_counts_as_one_action() {
    let mut ast = ast_from(vec![
        SyntaxNode::Prime { count: 1 },
        SyntaxNode::Prime { count: 1 },
        SyntaxNode::Prime { count: 3 },
    ]);

    let report = run_finalize(&mut ast, true);

    assert_eq!(root_children(&ast), vec![SyntaxNode::Prime { count: 5 }]);
    assert_eq!(report.steps.merge_adjacent_primes.applied_count, 1);
}

#[test]
fn recursively_enters_math_arguments_scripts_and_environment_body() {
    let command = SyntaxNode::Command {
        name: "wrap".to_string(),
        args: vec![math_arg(math_group(vec![
            SyntaxNode::Prime { count: 1 },
            SyntaxNode::Prime { count: 1 },
        ]))],
        known: true,
    };
    let scripted = SyntaxNode::Scripted {
        base: Box::new(SyntaxNode::Char('x')),
        subscript: None,
        superscript: Some(Box::new(math_group(vec![
            SyntaxNode::Prime { count: 2 },
            SyntaxNode::Prime { count: 1 },
        ]))),
    };
    let environment = SyntaxNode::Environment {
        name: "matrix".to_string(),
        args: vec![],
        known: true,
        body: Box::new(math_group(vec![
            SyntaxNode::Prime { count: 1 },
            SyntaxNode::Prime { count: 2 },
        ])),
    };
    let mut ast = ast_from(vec![command, scripted, environment]);

    let report = run_finalize(&mut ast, true);

    let children = root_children(&ast);
    match &children[0] {
        SyntaxNode::Command { args, .. } => match &args[0].as_ref().unwrap().value {
            ArgumentValue::MathContent(node) => {
                assert_eq!(node, &math_group(vec![SyntaxNode::Prime { count: 2 }]))
            }
            other => panic!("expected math content argument, got {other:?}"),
        },
        other => panic!("expected command node, got {other:?}"),
    }
    match &children[1] {
        SyntaxNode::Scripted { superscript, .. } => assert_eq!(
            superscript.as_deref(),
            Some(&math_group(vec![SyntaxNode::Prime { count: 3 }]))
        ),
        other => panic!("expected scripted node, got {other:?}"),
    }
    match &children[2] {
        SyntaxNode::Environment { body, .. } => assert_eq!(
            body.as_ref(),
            &math_group(vec![SyntaxNode::Prime { count: 3 }])
        ),
        other => panic!("expected environment node, got {other:?}"),
    }
    assert_eq!(report.steps.merge_adjacent_primes.applied_count, 3);
}

#[test]
fn does_not_merge_across_containers_or_slots() {
    let scripted = SyntaxNode::Scripted {
        base: Box::new(SyntaxNode::Prime { count: 1 }),
        subscript: Some(Box::new(SyntaxNode::Prime { count: 1 })),
        superscript: Some(Box::new(SyntaxNode::Prime { count: 1 })),
    };
    let mut ast = ast_from(vec![
        SyntaxNode::Prime { count: 1 },
        math_group(vec![SyntaxNode::Prime { count: 1 }]),
        SyntaxNode::Prime { count: 1 },
        scripted,
    ]);

    let report = run_finalize(&mut ast, true);

    assert_eq!(
        root_children(&ast),
        vec![
            SyntaxNode::Prime { count: 1 },
            math_group(vec![SyntaxNode::Prime { count: 1 }]),
            SyntaxNode::Prime { count: 1 },
            SyntaxNode::Scripted {
                base: Box::new(SyntaxNode::Prime { count: 1 }),
                subscript: Some(Box::new(SyntaxNode::Prime { count: 1 })),
                superscript: Some(Box::new(SyntaxNode::Prime { count: 1 })),
            },
        ]
    );
    assert_eq!(report.steps.merge_adjacent_primes.applied_count, 0);
}

#[test]
fn ignores_text_mode_sequences_and_prime_commands() {
    let mut ast = ast_from(vec![
        text_group(vec![
            SyntaxNode::Prime { count: 1 },
            SyntaxNode::Prime { count: 1 },
        ]),
        SyntaxNode::Command {
            name: "prime".to_string(),
            args: vec![],
            known: true,
        },
        SyntaxNode::Command {
            name: "prime".to_string(),
            args: vec![],
            known: true,
        },
    ]);

    let report = run_finalize(&mut ast, true);

    assert_eq!(
        root_children(&ast),
        vec![
            text_group(vec![
                SyntaxNode::Prime { count: 1 },
                SyntaxNode::Prime { count: 1 },
            ]),
            SyntaxNode::Command {
                name: "prime".to_string(),
                args: vec![],
                known: true,
            },
            SyntaxNode::Command {
                name: "prime".to_string(),
                args: vec![],
                known: true,
            },
        ]
    );
    assert_eq!(report.steps.merge_adjacent_primes.applied_count, 0);
}