use crate::adapter::AstAdapter;
use crate::value::{Table, Value};
use crate::NodeId;
fn has_trait(adapter: &impl AstAdapter, node: NodeId, t: &str) -> bool {
adapter.traits(node).iter().any(|x| x == t)
}
fn local_name(adapter: &impl AstAdapter, node: NodeId) -> String {
adapter
.name(node)
.map(|n| n.rsplit(':').next().unwrap_or("").to_ascii_lowercase())
.unwrap_or_default()
}
fn is_row(adapter: &impl AstAdapter, node: NodeId) -> bool {
has_trait(adapter, node, "row") || matches!(local_name(adapter, node).as_str(), "tr" | "row")
}
fn is_cell(adapter: &impl AstAdapter, node: NodeId) -> bool {
has_trait(adapter, node, "cell")
|| matches!(local_name(adapter, node).as_str(), "th" | "td" | "cell")
}
fn cell_value(text: &str) -> Value {
let t = text.trim();
if t.is_empty() {
return Value::Null;
}
crate::number::read(t, crate::number::Decimal::Point).unwrap_or_else(|| Value::Str(t.to_string()))
}
fn span_of(adapter: &impl AstAdapter, node: NodeId, names: &[&str]) -> usize {
names
.iter()
.find_map(|n| adapter.property(node, n))
.and_then(|v| v.to_string().trim().parse::<usize>().ok())
.filter(|n| *n >= 1)
.unwrap_or(1)
}
struct Cell {
label: Option<String>,
head: bool,
value: Value,
colspan: usize,
rowspan: usize,
}
fn rows_under(
adapter: &impl AstAdapter,
node: NodeId,
root: NodeId,
rows: &mut Vec<(NodeId, bool)>,
in_head: bool,
) -> Result<(), String> {
for c in adapter.children(node) {
if c != root && has_trait(adapter, c, "tabula") {
return Err(
"a nested table is not read with its outer one: address it on its own ('//*<tabula>')".into(),
);
}
if is_row(adapter, c) {
let labelled = adapter
.property(c, "role")
.is_some_and(|v| v.to_string() == "label");
rows.push((c, in_head || labelled));
} else {
let head = in_head || local_name(adapter, c) == "thead";
rows_under(adapter, c, root, rows, head)?;
}
}
Ok(())
}
fn cells_under(adapter: &impl AstAdapter, node: NodeId, out: &mut Vec<Cell>) {
for c in adapter.children(node) {
if is_cell(adapter, c) {
let name = local_name(adapter, c);
let label = adapter
.property(c, "lemma")
.map(|v| v.to_string())
.filter(|s| !s.is_empty());
let role_label = adapter
.property(c, "role")
.is_some_and(|v| v.to_string() == "label");
let mut text = adapter
.property(c, "body")
.or_else(|| adapter.default_value(c))
.map(|v| v.to_string())
.unwrap_or_default();
if let Some(l) = &label
&& let Some(rest) = text.strip_prefix(&format!("{l}: "))
{
text = rest.to_string();
}
out.push(Cell {
label,
head: name == "th" || role_label,
value: cell_value(&text),
colspan: span_of(adapter, c, &["colspan", "cols"]),
rowspan: span_of(adapter, c, &["rowspan", "rows"]),
});
} else if !is_row(adapter, c) {
cells_under(adapter, c, out);
}
}
}
pub(super) fn node_table(adapter: &impl AstAdapter, node: NodeId) -> Result<Table, String> {
if !has_trait(adapter, node, "tabula") {
return Err(format!(
"'{}' is not a table: '//*<tabula>' finds the tables of a document, '@| #' reads a stream of rows as one",
adapter.name(node).unwrap_or_else(|| "the node".into())
));
}
let mut rows: Vec<(NodeId, bool)> = Vec::new();
rows_under(adapter, node, node, &mut rows, false)?;
let rows: Vec<(Vec<Cell>, bool)> = rows
.into_iter()
.map(|(r, head)| {
let mut cells = Vec::new();
cells_under(adapter, r, &mut cells);
(cells, head)
})
.collect();
if rows.iter().any(|(cells, _)| cells.iter().any(|c| c.label.is_some())) {
let mut header: Vec<String> = Vec::new();
for (cells, _) in &rows {
for c in cells {
if let Some(l) = &c.label
&& !header.contains(l)
{
header.push(l.clone());
}
}
}
let body = rows
.iter()
.map(|(cells, _)| {
let mut row = vec![Value::Null; header.len()];
for c in cells {
if let Some(l) = &c.label
&& let Some(j) = header.iter().position(|h| h == l)
{
row[j] = c.value.clone();
}
}
row
})
.collect();
return Ok(Table {
header: Some(header),
rows: body,
});
}
let mut grid: Vec<Vec<Value>> = Vec::new();
let mut carried: Vec<(usize, Value)> = Vec::new(); let mut header_rows = 0usize;
for (i, (cells, in_head)) in rows.iter().enumerate() {
let mut row: Vec<Value> = Vec::new();
let mut spans: Vec<(usize, Value)> = Vec::new();
let mut it = cells.iter();
let mut next = it.next();
loop {
let j = row.len();
if j < carried.len() && carried[j].0 > 0 {
carried[j].0 -= 1;
row.push(carried[j].1.clone());
spans.push((0, Value::Null));
continue;
}
let Some(c) = next else { break };
for _ in 0..c.colspan {
row.push(c.value.clone());
spans.push((c.rowspan - 1, c.value.clone()));
}
next = it.next();
}
while row.len() < carried.len() && carried[row.len()].0 > 0 {
let j = row.len();
carried[j].0 -= 1;
row.push(carried[j].1.clone());
spans.push((0, Value::Null));
}
for (j, (left, v)) in spans.into_iter().enumerate() {
if j >= carried.len() {
carried.push((0, Value::Null));
}
if left > 0 {
carried[j] = (left, v);
}
}
let all_head = !cells.is_empty() && cells.iter().all(|c| c.head);
if i == header_rows && (all_head || *in_head) {
header_rows += 1;
}
grid.push(row);
}
let width = grid.iter().map(Vec::len).max().unwrap_or(0);
for r in &mut grid {
r.resize(width, Value::Null);
}
if header_rows > 0 {
let header: Vec<String> = grid[header_rows - 1].iter().map(Value::to_string).collect();
return Ok(Table {
header: Some(header),
rows: grid.split_off(header_rows),
});
}
Ok(Table {
header: None,
rows: grid,
})
}