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```"]);
}
}
|