use crate::tables::{has_wrapped_cells, unwrap_table_rows};
use super::table::{is_table_row, is_table_separator, normalize_table};
pub fn process_safe_mode(content: &str) -> String {
let content = crate::lists::normalize_lists(content);
let content = crate::lists::normalize_loose_lists(&content);
let lines: Vec<&str> = content.lines().collect();
let mut result = Vec::new();
let mut i = 0;
while i < lines.len() {
if i + 1 < lines.len() && is_table_row(lines[i]) && is_table_separator(lines[i + 1]) {
let header = lines[i];
let table_indent = header.len() - header.trim_start().len();
let indent_str = " ".repeat(table_indent);
let separator = lines[i + 1];
i += 2;
let mut table_rows = vec![];
while i < lines.len() && is_table_row(lines[i]) {
table_rows.push(lines[i]);
i += 1;
}
let reindent = |normalized: String| -> String {
if table_indent == 0 {
return normalized;
}
normalized
.lines()
.map(|l| format!("{indent_str}{l}"))
.collect::<Vec<_>>()
.join("\n")
};
let all_table_lines: Vec<&str> = std::iter::once(header)
.chain(std::iter::once(separator))
.chain(table_rows.iter().copied())
.collect();
let table_content = all_table_lines.join("\n");
if has_wrapped_cells(&table_content) {
let unwrapped_rows = unwrap_table_rows(&table_rows);
let unwrapped_refs: Vec<&str> = unwrapped_rows.iter().map(String::as_str).collect();
if let Some(normalized) = normalize_table(header, separator, &unwrapped_refs) {
result.push(reindent(normalized));
} else {
result.push(header.to_string());
result.push(separator.to_string());
for row in unwrapped_rows {
result.push(row);
}
}
} else {
if let Some(normalized) = normalize_table(header, separator, &table_rows) {
result.push(reindent(normalized));
} else {
result.push(header.to_string());
result.push(separator.to_string());
for &row in &table_rows {
result.push(row.to_string());
}
i -= table_rows.len();
i += 2;
}
}
} else {
result.push(lines[i].to_string());
i += 1;
}
}
result.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_safe_mode_preserves_content() {
let content = "# Test\n\nSome content";
let result = process_safe_mode(content);
assert_eq!(result, content);
}
#[test]
fn test_safe_mode_normalizes_table() {
let content = "| Name | Age |\n|------|-----|\n| Alice | 30 |\n| Bob | 25 |";
let result = process_safe_mode(content);
assert!(result.contains("| Name"));
assert!(result.contains("| Age"));
assert!(result.contains("| Alice"));
assert!(result.contains("| Bob"));
}
#[test]
fn test_safe_mode_preserves_non_tables() {
let content = "# Title\n\nSome paragraph.\n\nMore text.";
let result = process_safe_mode(content);
assert_eq!(result, content);
}
#[test]
fn test_safe_mode_misaligned_table() {
let content = "| A | B |\n|---|---|\n| x| y |";
let result = process_safe_mode(content);
assert!(result.contains("| A"));
assert!(result.contains("| B"));
}
#[test]
fn test_safe_mode_multiple_tables() {
let content =
"| H1 | H2 |\n|---|---|\n| a | b |\n\nText\n\n| C | D |\n|---|---|\n| c | d |";
let result = process_safe_mode(content);
assert!(result.contains("| H1"));
assert!(result.contains("| C"));
}
#[test]
fn test_link_in_table_cell_preserved() {
let content = "| [API](https://example.com/api(v2)) | Description |\n|-----------------------------------|-------------|\n| Link | Value |";
let result = process_safe_mode(content);
assert!(
result.contains("https://example.com/api(v2)"),
"Link URL with parentheses should be preserved in table cell. Result:\n{result}"
);
}
#[test]
fn test_link_with_pipe_in_table_cell() {
let content = "| [Docs](https://example.com/doc|section) | Description |\n|------------------------------------------|-------------|\n| Link | Value |";
let result = process_safe_mode(content);
assert!(
result.contains("https://example.com/doc|section"),
"Link URL with pipe should be preserved. Result:\n{result}"
);
}
#[test]
fn test_safe_mode_unwraps_wrapped_table_cells() {
let content = "| Name | Description |\n|------|-------------|\n| Item | This is a very |\n| | long description |";
let result = process_safe_mode(content);
assert!(
result.contains("This is a very long description"),
"Wrapped cell should be unwrapped. Result:\n{result}"
);
assert!(
!result.contains("| | long description |"),
"Continuation row should be removed. Result:\n{result}"
);
}
#[test]
fn test_safe_mode_preserves_multiline_code_in_tables() {
let content = "| Code | Example |\n|------|---------|\n| ```python | of code |\n| def hello(): | inside |\n| ``` | cell |";
let result = process_safe_mode(content);
assert!(
result.contains("```python"),
"Code fence should be preserved. Result:\n{result}"
);
assert!(
result.contains("def hello():"),
"Code content should be preserved. Result:\n{result}"
);
}
#[test]
fn test_safe_mode_normalizes_list_indentation() {
let content = "- Item 1\n - Nested with 4 spaces\n- Item 2";
let result = process_safe_mode(content);
assert!(
result.contains(" - Nested with 4 spaces"),
"List indentation should be normalized to 2 spaces. Result:\n{result}"
);
assert!(
!result.contains(" - Nested"),
"4-space indentation should be removed. Result:\n{result}"
);
}
#[test]
fn test_safe_mode_normalizes_bullet_styles() {
let content = "- Item 1\n* Item 2\n+ Item 3";
let result = process_safe_mode(content);
assert!(
result.contains("- Item 1"),
"Dash bullet should remain. Result:\n{result}"
);
assert!(
result.contains("- Item 2"),
"Asterisk should become dash. Result:\n{result}"
);
assert!(
result.contains("- Item 3"),
"Plus should become dash. Result:\n{result}"
);
}
#[test]
fn test_safe_mode_preserves_task_lists() {
let content = "- [ ] Buy milk\n- [x] Done item\n- [X] Also done";
let result = process_safe_mode(content);
assert!(
result.contains("- [ ] Buy milk"),
"Unchecked task should be preserved. Result:\n{result}"
);
assert!(
result.contains("- [x] Done item"),
"Checked task should be preserved. Result:\n{result}"
);
}
#[test]
fn test_safe_mode_preserves_ordered_list_child_indentation() {
let content = "1. **First item**\n - sub-item a\n - sub-item b";
let result = process_safe_mode(content);
assert!(
result.contains(" - sub-item a"),
"3-space indent under ordered list must be preserved. Got:\n{result}"
);
assert!(
!result.contains("\n - sub-item"),
"2-space indent must not appear under ordered list parent. Got:\n{result}"
);
}
#[test]
fn test_safe_mode_preserves_table_indentation_inside_ordered_list() {
let content = "2. **Map endpoints**\n\n | Column A | Column B |\n |----------|----------|\n | foo | bar |";
let result = process_safe_mode(content);
assert!(
result.contains(" | Column A"),
"Table inside ordered list must keep 3-space indent. Got:\n{result}"
);
assert!(
result.contains(" | foo"),
"Table data row must keep 3-space indent. Got:\n{result}"
);
assert!(
!result.contains("\n| Column A"),
"Table must not be moved to column 0. Got:\n{result}"
);
}
#[test]
fn test_safe_mode_preserves_lists_in_code_blocks() {
let content =
"```markdown\n- Item in code block\n* Another item\n```\n\n- Real item outside";
let result = process_safe_mode(content);
assert!(
result.contains("- Item in code block"),
"List in code block should be preserved. Result:\n{result}"
);
assert!(
result.contains("* Another item"),
"Asterisk in code block should be preserved. Result:\n{result}"
);
assert!(
result.contains("- Real item outside"),
"Outside list should be normalized. Result:\n{result}"
);
}
}