texform 0.1.0

LaTeX formula parser, editor, and normalizer — the public TeXForm facade
Documentation
use texform::{
    ArgSpecFormInfo, ArgSpecKindInfo, DelimiterTokenInfo, RuntimeContentModeInfo, TransformEngine,
    validate_argspec,
};

#[test]
fn validate_argspec_success_returns_stable_slot_shape() {
    let result = validate_argspec("m:T? !s r() R<(,)>{x} m:D?");

    assert!(result.valid);
    assert_eq!(result.error, None);
    assert_eq!(result.arg_count, Some(5));
    let parsed = result
        .parsed
        .expect("valid argspec should include parsed slots");
    assert_eq!(parsed.len(), 5);

    assert!(parsed[0].required);
    assert!(!parsed[0].no_leading_space);
    assert!(parsed[0].nullable);
    assert_eq!(
        parsed[0].kind,
        ArgSpecKindInfo::Content {
            mode: RuntimeContentModeInfo::Text,
        }
    );
    assert_eq!(parsed[0].form, ArgSpecFormInfo::Standard);

    assert!(!parsed[1].required);
    assert!(parsed[1].no_leading_space);
    assert_eq!(parsed[1].kind, ArgSpecKindInfo::Star);
    assert_eq!(parsed[1].form, ArgSpecFormInfo::Star);

    let ArgSpecFormInfo::Delimited { open, close } = &parsed[2].form else {
        panic!("expected delimited form");
    };
    assert_eq!(open, &DelimiterTokenInfo::Char { value: '(' });
    assert_eq!(close, &DelimiterTokenInfo::Char { value: ')' });

    assert_eq!(
        parsed[3].kind,
        ArgSpecKindInfo::Content {
            mode: RuntimeContentModeInfo::Math,
        }
    );
    let ArgSpecFormInfo::Paired { pairs } = &parsed[3].form else {
        panic!("expected paired form");
    };
    assert_eq!(pairs.len(), 1);

    assert_eq!(parsed[4].kind, ArgSpecKindInfo::Delimiter);
    assert!(parsed[4].nullable);

    let star_slot = serde_json::to_value(&parsed[1]).unwrap();
    assert_eq!(star_slot["kind"], serde_json::json!({ "type": "star" }));
    assert_eq!(star_slot["form"], serde_json::json!({ "type": "star" }));
}

#[test]
fn validate_argspec_failure_keeps_all_fields_present() {
    let result = validate_argspec("m?");

    assert!(!result.valid);
    assert!(
        result
            .error
            .as_deref()
            .is_some_and(|message| message.contains("invalid argspec"))
    );
    assert_eq!(result.arg_count, None);
    assert_eq!(result.parsed, None);
}

#[test]
fn transform_report_dto_serializes_as_snake_case() {
    let mut report = texform::TransformReport::default();
    report
        .finalize_ast
        .steps
        .merge_adjacent_primes
        .applied_count = 1;

    let value = serde_json::to_value(texform::bindings::transform_report_to_dto(&report)).unwrap();

    assert!(value.get("finalize_ast").is_some());
    assert!(value.get("finalizeAst").is_none());
    assert_eq!(
        value["finalize_ast"]["steps"]["merge_adjacent_primes"]["applied_count"],
        1
    );
}

#[test]
fn lookup_info_dto_reuses_stable_argspec_slots() {
    let parser = texform::Parser::builder().build().unwrap();
    let record = parser
        .lookup_command("frac", texform::ContentMode::Math)
        .expect("default parser should know frac");

    let value = serde_json::to_value(texform::bindings::command_info_to_dto(record)).unwrap();

    assert_eq!(value["name"], "frac");
    assert_eq!(value["allowed_mode"], "math");
    assert!(
        value["spec_string"]
            .as_str()
            .is_some_and(|spec| !spec.is_empty())
    );
    assert!(
        value["from_packages"]
            .as_array()
            .is_some_and(|items| !items.is_empty())
    );
    assert!(
        value["args"]
            .as_array()
            .is_some_and(|items| !items.is_empty())
    );
    assert!(value["args"][0].get("no_leading_space").is_some());
}

#[test]
fn normalize_error_maps_parse_failure_to_binding_error_parts() {
    let engine = TransformEngine::builder()
        .profile(texform::Profile::Authoring)
        .build()
        .unwrap();

    let error = match engine.normalize("\\unknown") {
        Ok(_) => panic!("strict normalize should reject unknown commands"),
        Err(error) => error,
    };
    let parts = texform::bindings::normalize_error_to_parts(error);

    assert_eq!(parts.error.kind, "parse");
    assert!(!parts.error.diagnostics.is_empty());
}

#[test]
fn list_packages_reports_known_packages_with_counts() {
    let packages = texform::list_packages();
    assert!(!packages.is_empty());

    for expected in ["base", "ams", "physics"] {
        let info = packages
            .iter()
            .find(|info| info.name == expected)
            .unwrap_or_else(|| panic!("package {expected} should be listed"));
        assert!(info.commands > 0, "{expected} should have commands");
    }

    let base = packages.iter().find(|info| info.name == "base").unwrap();
    assert!(base.environments > 0, "base should have environments");
}