use super::ast::{Align, Inline, Table};
use super::inline::parse_inlines;
use regex::Regex;
use std::sync::LazyLock;
static COLLAPSED_ROW_BOUNDARY: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\|[ \t]*\|").expect("collapsed-table boundary regex"));
pub(crate) fn reflow_collapsed_tables(text: &str) -> String {
if !text.contains('|') {
return text.to_string();
}
let mut out: Vec<String> = Vec::new();
for line in text.lines() {
if is_collapsed_table_line(line) {
let pipe = line.find('|').unwrap_or(0);
let (prefix, rest) = line.split_at(pipe);
if !prefix.trim().is_empty() {
out.push(prefix.trim_end().to_string());
}
out.push(
COLLAPSED_ROW_BOUNDARY
.replace_all(rest, "|\n|")
.into_owned(),
);
} else {
out.push(line.to_string());
}
}
out.join("\n")
}
fn is_collapsed_table_line(line: &str) -> bool {
let mut has_dash = false;
let mut has_content = false;
for cell in line.split('|') {
let c = cell.trim();
if c.is_empty() {
continue;
}
let core = c.trim_start_matches(':').trim_end_matches(':');
if core.len() >= 2 && core.chars().all(|ch| ch == '-') {
has_dash = true;
} else {
has_content = true;
}
if has_dash && has_content {
return true;
}
}
false
}
pub(crate) fn ensure_blank_line_before_tables(text: &str) -> String {
if !text.contains('|') {
return text.to_string();
}
let lines: Vec<String> = text.lines().map(|l| l.to_string()).collect();
let mut out: Vec<String> = Vec::with_capacity(lines.len() + 1);
let mut in_fence = false;
let mut i = 0;
while i < lines.len() {
let line = &lines[i];
let trimmed = line.trim_start();
if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
in_fence = !in_fence;
out.push(line.clone());
i += 1;
continue;
}
if !in_fence && let Some((_, next)) = try_parse(&lines, i) {
let prev_is_text = out.last().is_some_and(|prev| !prev.trim().is_empty());
if prev_is_text {
out.push(String::new());
}
while i < next {
out.push(lines[i].clone());
i += 1;
}
continue;
}
out.push(line.clone());
i += 1;
}
let mut result = out.join("\n");
if text.ends_with('\n') {
result.push('\n');
}
result
}
pub(super) fn try_parse(lines: &[String], start: usize) -> Option<(Table, usize)> {
let header_line = lines.get(start)?;
let sep_line = lines.get(start + 1)?;
if !looks_like_row(header_line) || !is_separator(sep_line) {
return None;
}
let header: Vec<Vec<Inline>> = split_cells(header_line)
.into_iter()
.map(|c| parse_inlines(c.trim()))
.collect();
let align = parse_alignment(sep_line, header.len());
let mut rows = Vec::new();
let mut i = start + 2;
while i < lines.len() && looks_like_row(&lines[i]) {
let row: Vec<Vec<Inline>> = split_cells(&lines[i])
.into_iter()
.map(|c| parse_inlines(c.trim()))
.collect();
rows.push(row);
i += 1;
}
Some((
Table {
align,
header,
rows,
},
i,
))
}
fn looks_like_row(line: &str) -> bool {
let t = line.trim();
t.contains('|') && !t.is_empty()
}
fn is_separator(line: &str) -> bool {
let cells = split_cells(line);
if cells.is_empty() {
return false;
}
cells.iter().all(|c| {
let c = c.trim();
let core = c.trim_start_matches(':').trim_end_matches(':');
!core.is_empty() && core.chars().all(|ch| ch == '-')
})
}
fn split_cells(line: &str) -> Vec<&str> {
let t = line.trim();
let t = t.strip_prefix('|').unwrap_or(t);
let t = t.strip_suffix('|').unwrap_or(t);
t.split('|').collect()
}
fn parse_alignment(sep: &str, cols: usize) -> Vec<Align> {
let mut align: Vec<Align> = split_cells(sep)
.into_iter()
.map(|c| {
let c = c.trim();
let left = c.starts_with(':');
let right = c.ends_with(':');
match (left, right) {
(true, true) => Align::Center,
(true, false) => Align::Left,
(false, true) => Align::Right,
(false, false) => Align::None,
}
})
.collect();
align.resize(cols, Align::None);
align
}
pub(crate) fn normalize_tables(text: &str) -> String {
ensure_blank_line_before_tables(&reflow_collapsed_tables(text))
}