flowmark 0.2.7

A Markdown auto-formatter for clean diffs and semantic line breaks
Documentation
use flowmark::config::ListSpacing;
use flowmark::fill_markdown;

fn fmt(input: &str) -> String {
    fill_markdown(input, true, 88, true, false, false, false, None, ListSpacing::Preserve)
}

#[test]
fn test_basic_note_alert() {
    let input = "> [!NOTE]\n> This is a note alert.";
    let expected = "> [!NOTE]\n> This is a note alert.\n";
    assert_eq!(fmt(input), expected);
}

#[test]
fn test_all_valid_alert_types() {
    let alert_types = ["NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION"];
    for alert_type in &alert_types {
        let input =
            format!("> [!{alert_type}]\n> Content for {} alert.", alert_type.to_lowercase());
        let result = fmt(&input);
        assert!(
            result.contains(&format!("> [!{alert_type}]")),
            "Alert type {alert_type} was not preserved"
        );
        assert!(
            result.contains(&format!("{} alert", alert_type.to_lowercase())),
            "Content for {alert_type} was lost"
        );
        assert!(result.starts_with('>'), "Quote formatting lost for {alert_type}");
    }
}

#[test]
fn test_lowercase_alert_normalized_to_uppercase() {
    let input = "> [!note]\n> This lowercase alert should be normalized.";
    let result = fmt(input);
    assert!(result.contains("> [!NOTE]"));
    assert!(!result.contains("> [!note]"));
    assert!(result.contains("normalized"));
}

#[test]
fn test_misspelled_alert_preserves_quote() {
    let test_cases = [
        ("> [!NOOT]\n> Content here", "[!NOOT]"),
        ("> [!WARNNG]\n> Content here", "[!WARNNG]"),
        ("> [!WARNUNG]\n> Content here", "[!WARNUNG]"),
        ("> [!NOTEE]\n> Content here", "[!NOTEE]"),
        ("> [!HINT]\n> Content here", "[!HINT]"),
    ];
    for (input, misspelled) in &test_cases {
        let result = fmt(input);
        assert!(result.starts_with('>'), "Quote formatting lost for {misspelled}");
        assert!(result.contains(misspelled), "Content lost for {misspelled}");
        assert!(result.contains("Content here"), "Body content lost for {misspelled}");
    }
}

#[test]
fn test_unknown_alert_types_preserve_quote() {
    let test_cases = [
        "> [!FOO]\n> Foo type",
        "> [!CUSTOM]\n> Custom type",
        "> [!INFO]\n> Info type",
        "> [!DANGER]\n> Danger type",
        "> [!SUCCESS]\n> Success type",
    ];
    for input in &test_cases {
        let result = fmt(input);
        assert!(result.starts_with('>'), "Quote formatting lost for: {}", &input[..20]);
    }
}

#[test]
fn test_malformed_alert_preserves_quote() {
    let test_cases = [
        "> [NOTE]\n> Missing exclamation mark",
        "> [!NOTE\n> Missing closing bracket",
        "> ![NOTE]\n> Wrong order of symbols",
        "> [! NOTE]\n> Space after exclamation",
    ];
    for input in &test_cases {
        let result = fmt(input);
        assert!(
            result.starts_with('>'),
            "Quote formatting lost for: {}",
            &input[..30.min(input.len())]
        );
    }
}

#[test]
fn test_alert_with_multiline_content() {
    let input =
        "> [!NOTE]\n> First line of content.\n> Second line of content.\n> Third line of content.";
    let result = fmt(input);
    assert!(result.contains("> [!NOTE]"));
    assert!(result.contains("First line"));
    assert!(result.contains("content"));
}

#[test]
fn test_alert_with_multiple_paragraphs() {
    let input = "> [!TIP]\n> First paragraph.\n>\n> Second paragraph.";
    let expected = "> [!TIP]\n> First paragraph.\n> \n> Second paragraph.\n";
    assert_eq!(fmt(input), expected);
}

#[test]
fn test_alert_with_code_block() {
    let input = "> [!WARNING]\n> Be careful with this code:\n>\n> ```python\n> dangerous_operation()\n> ```";
    let result = fmt(input);
    assert!(result.contains("> [!WARNING]"));
    assert!(result.contains("```python"));
    assert!(result.contains("dangerous_operation()"));
}

#[test]
fn test_alert_with_list() {
    let input = "> [!IMPORTANT]\n> Remember:\n>\n> - First item\n> - Second item";
    let result = fmt(input);
    assert!(result.contains("> [!IMPORTANT]"));
    assert!(result.contains("First item"));
    assert!(result.contains("Second item"));
}

#[test]
fn test_multiple_alerts_in_document() {
    let input = "> [!NOTE]\n> First note.\n\nSome text between.\n\n> [!WARNING]\n> A warning.";
    let result = fmt(input);
    assert!(result.contains("> [!NOTE]"));
    assert!(result.contains("> [!WARNING]"));
    assert!(result.contains("First note"));
    assert!(result.contains("A warning"));
    assert!(result.contains("Some text between"));
}

#[test]
fn test_alert_after_heading() {
    let input = "## Section Title\n\n> [!NOTE]\n> Important note for this section.";
    let result = fmt(input);
    assert!(result.contains("## Section Title"));
    assert!(result.contains("> [!NOTE]"));
    assert!(result.contains("Important note"));
}

#[test]
fn test_regular_quote_still_works() {
    let input = "> This is a regular quote.\n> It has multiple lines.";
    let result = fmt(input);
    assert!(result.starts_with('>'));
    assert!(result.contains("regular quote"));
}

// ===== Tests ported from Python test_alerts.py =====

#[test]
fn test_empty_alert_type_preserves_quote() {
    let input = "> [!]\n> Some content";
    let result = fmt(input);
    assert!(result.starts_with('>'), "Quote formatting lost for empty alert type");
    assert!(result.contains("Some content"));
}

#[test]
fn test_quote_with_link_like_content() {
    let input = "> Check out [!this link](https://example.com) for more info.";
    let result = fmt(input);
    assert!(result.starts_with('>'));
    assert!(result.contains("this link") || result.contains("[!this link]"));
}