use anyhow::{Context, Result, anyhow, bail};
use crate::{QueryValue, TypeInferenceOptions};
use super::{SheetData, normalize_text_headers, parse_scalar_value};
pub(super) fn load_markdown_sheet(
markdown_path: &std::path::Path,
requested_sheet: Option<&str>,
inference_options: &TypeInferenceOptions,
has_headers: bool,
) -> Result<SheetData> {
let content = std::fs::read_to_string(markdown_path)
.with_context(|| format!("failed to read {}", markdown_path.display()))?;
let tables = parse_markdown_tables(&content);
if tables.is_empty() {
bail!(
"Markdown input {} does not contain any table",
markdown_path.display()
);
}
let table_index = if let Some(key) = requested_sheet {
let index = key
.trim()
.parse::<usize>()
.map_err(|_| {
anyhow!(
"invalid Markdown table selector '{key}' for {}. Use a 1-based numeric index such as ':1' or ':2'.",
markdown_path.display()
)
})?;
if index == 0 {
bail!(
"invalid Markdown table selector '{key}' for {}. Table indexes are 1-based (:1, :2, ...).",
markdown_path.display()
);
}
index
} else {
1
};
let table_count = tables.len();
let Some(table) = tables.into_iter().nth(table_index - 1) else {
bail!(
"Markdown table {} not found in {}. Found {} table(s); choose an index between 1 and {}.",
table_index,
markdown_path.display(),
table_count,
table_count
);
};
let mut rows = table.rows;
let columns = if has_headers {
normalize_text_headers(&table.headers)
} else {
rows.insert(0, table.headers);
let width = rows.iter().map(Vec::len).max().unwrap_or(0);
(1..=width).map(|index| format!("column{index}")).collect()
};
if columns.is_empty() {
bail!("Markdown table {} is empty", table_index);
}
let data_rows = rows
.into_iter()
.map(|row| {
(0..columns.len())
.map(|index| {
let value = row.get(index).map(String::as_str).unwrap_or("");
parse_scalar_value(value, inference_options)
})
.collect::<Vec<_>>()
})
.filter(|row| row.iter().any(|value| !matches!(value, QueryValue::Null)))
.collect::<Vec<_>>();
if data_rows.is_empty() {
bail!(
"Markdown table {} in {} is empty (no data rows)",
table_index,
markdown_path.display()
);
}
Ok(SheetData {
original_name: format!("table{table_index}"),
columns,
rows: data_rows,
})
}
struct MarkdownTable {
headers: Vec<String>,
rows: Vec<Vec<String>>,
}
fn parse_markdown_tables(content: &str) -> Vec<MarkdownTable> {
let lines = content.lines().collect::<Vec<_>>();
let mut tables = Vec::new();
let mut index = 0usize;
while index + 1 < lines.len() {
if !looks_like_markdown_row(lines[index]) || !is_markdown_separator(lines[index + 1]) {
index += 1;
continue;
}
let headers = parse_markdown_row(lines[index]);
if headers.is_empty() {
index += 1;
continue;
}
let mut rows = Vec::new();
index += 2;
while index < lines.len() && looks_like_markdown_row(lines[index]) {
if is_markdown_separator(lines[index]) {
break;
}
let row = parse_markdown_row(lines[index]);
if row.iter().any(|value| !value.trim().is_empty()) {
rows.push(row);
}
index += 1;
}
tables.push(MarkdownTable { headers, rows });
}
tables
}
fn looks_like_markdown_row(line: &str) -> bool {
line.contains('|')
}
fn is_markdown_separator(line: &str) -> bool {
let parts = split_markdown_cells(line);
if parts.is_empty() {
return false;
}
parts.iter().all(|part| {
let cell = part.trim();
if cell.is_empty() {
return false;
}
let inner = cell.trim_matches(':');
!inner.is_empty() && inner.chars().all(|character| character == '-')
})
}
fn parse_markdown_row(line: &str) -> Vec<String> {
split_markdown_cells(line)
.into_iter()
.map(|cell| cell.replace("\\|", "|").trim().to_owned())
.collect()
}
fn split_markdown_cells(line: &str) -> Vec<String> {
let trimmed = line.trim();
if trimmed.is_empty() {
return Vec::new();
}
let without_prefix = trimmed.strip_prefix('|').unwrap_or(trimmed);
let content = without_prefix.strip_suffix('|').unwrap_or(without_prefix);
content.split('|').map(str::to_owned).collect()
}