File size: 3,727 Bytes
1851bae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
//! Repair malformed markdown before parsing.
//!
//! This module handles common markdown issues that the parser doesn't handle
//! well, such as closing code fences on the same line as content.

use streamdown_core::ParseState;

/// Repair a line of markdown, returning one or more normalized lines.
///
/// Handles:
/// - Embedded closing fences: `}```\n` becomes `}\n` + ```` ``` ```` (only when
///   in code block)
pub fn repair_line(line: &str, state: &ParseState) -> Vec<String> {
    // Only check for embedded closing fence when we're inside a code block
    if state.is_in_code()
        && let Some(lines) = split_embedded_fence(line)
    {
        return lines;
    }

    vec![line.to_string()]
}

/// Split a line if it contains an embedded closing fence at the end.
/// e.g., `}``` ` becomes Some(vec![`}`, ```` ``` ````])
fn split_embedded_fence(line: &str) -> Option<Vec<String>> {
    let trimmed = line.trim_end();

    // Check for ``` at the end
    if let Some(stripped) = trimmed.strip_suffix("```")
        && !stripped.trim().is_empty()
    {
        return Some(vec![stripped.to_string(), "```".to_string()]);
    }

    // Check for ~~~ at the end
    if let Some(stripped) = trimmed.strip_suffix("~~~")
        && !stripped.trim().is_empty()
    {
        return Some(vec![stripped.to_string(), "~~~".to_string()]);
    }

    None
}

#[cfg(test)]
mod tests {
    use streamdown_core::Code;

    use super::*;

    fn state_outside_code() -> ParseState {
        ParseState::new()
    }

    fn state_inside_code() -> ParseState {
        let mut state = ParseState::new();
        state.enter_code_block(Code::Backtick, Some("rust".to_string()));
        state
    }

    #[test]
    fn test_normal_line_unchanged() {
        assert_eq!(
            repair_line("hello world", &state_outside_code()),
            vec!["hello world"]
        );
        assert_eq!(
            repair_line("hello world", &state_inside_code()),
            vec!["hello world"]
        );
    }

    #[test]
    fn test_valid_fence_unchanged() {
        assert_eq!(repair_line("```", &state_outside_code()), vec!["```"]);
        assert_eq!(repair_line("   ```", &state_outside_code()), vec!["   ```"]);
        assert_eq!(
            repair_line("```rust", &state_outside_code()),
            vec!["```rust"]
        );
    }

    #[test]
    fn test_embedded_fence_not_split_outside_code_block() {
        // Outside code block, don't split
        assert_eq!(repair_line("}```", &state_outside_code()), vec!["}```"]);
        assert_eq!(
            repair_line("return x;```", &state_outside_code()),
            vec!["return x;```"]
        );
    }

    #[test]
    fn test_embedded_backtick_fence_split_in_code_block() {
        // Inside code block, split embedded fences
        assert_eq!(repair_line("}```", &state_inside_code()), vec!["}", "```"]);
        assert_eq!(
            repair_line("     }```", &state_inside_code()),
            vec!["     }", "```"]
        );
        assert_eq!(
            repair_line("return x;```", &state_inside_code()),
            vec!["return x;", "```"]
        );
    }

    #[test]
    fn test_embedded_tilde_fence_split_in_code_block() {
        assert_eq!(repair_line("}~~~", &state_inside_code()), vec!["}", "~~~"]);
        assert_eq!(
            repair_line("return x;~~~", &state_inside_code()),
            vec!["return x;", "~~~"]
        );
    }

    #[test]
    fn test_whitespace_only_before_fence_unchanged() {
        // Just whitespace before fence is a valid fence, don't split
        assert_eq!(repair_line("   ```", &state_inside_code()), vec!["   ```"]);
        assert_eq!(repair_line("\t```", &state_inside_code()), vec!["\t```"]);
    }
}