use crate::ast::{ErrorLevel, Node, NodeType, ValidationError};
pub const DEFAULT_CONDITIONAL_TAGS: &[&str] = &["if"];
pub fn apply(document: &mut Node<'_>, conditional_tags: &[&str]) {
let mut stack: Vec<&mut Node<'_>> = vec![document];
while let Some(node) = stack.pop() {
if node.node_type == NodeType::Tag && node.tag.as_deref() == Some("table") {
rewrite(node, conditional_tags);
}
let mut group: Vec<&mut Node<'_>> = Vec::new();
for slot in node.slots.values_mut() {
group.push(slot);
}
for child in &mut node.children {
group.push(child);
}
group.reverse();
stack.append(&mut group);
}
}
fn rewrite(node: &mut Node<'_>, conditional_tags: &[&str]) {
match node.children.first() {
None => return,
Some(first) if first.node_type == NodeType::Table => return,
Some(_) => {}
}
let mut children = std::mem::take(&mut node.children);
let mut rest = children.split_off(1);
let Some(first) = children.pop() else { return };
let mut thead = Node::new(NodeType::Thead);
let mut tbody = Node::new(NodeType::Tbody);
if first.node_type == NodeType::List {
thead.push(convert_to_row(first, NodeType::Th));
}
for row in rest.drain(..) {
if let Some(row) = accept_row(node, row, conditional_tags) {
tbody.push(row);
}
}
let table = Node::with(
NodeType::Table,
node.attributes.clone(),
vec![thead, tbody],
None,
);
node.children = vec![table];
}
fn accept_row<'a>(
table: &mut Node<'a>,
mut row: Node<'a>,
conditional_tags: &[&str],
) -> Option<Node<'a>> {
if row.node_type == NodeType::List {
return Some(convert_to_row(row, NodeType::Td));
}
if is_conditional_tag(&row, conditional_tags) {
rewrite_conditional_row(&mut row, conditional_tags);
return Some(row);
}
if row.node_type != NodeType::Hr && !is_comment(&row) {
table.errors.push(unexpected_node_error(&row));
}
None
}
fn rewrite_conditional_row(row: &mut Node<'_>, conditional_tags: &[&str]) {
let children = std::mem::take(&mut row.children);
let mut kept = Vec::with_capacity(children.len());
for child in children {
if child.node_type == NodeType::Hr {
continue;
}
if child.node_type == NodeType::List {
kept.push(convert_to_row(child, NodeType::Td));
continue;
}
let structural = is_comment(&child)
|| child.tag.as_deref() == Some("else")
|| is_conditional_tag(&child, conditional_tags);
if structural {
kept.push(child);
} else {
row.errors.push(unexpected_node_error(&child));
}
}
row.children = kept;
}
fn convert_to_row(mut node: Node<'_>, cell: NodeType) -> Node<'_> {
node.node_type = NodeType::Tr;
node.attributes.clear();
for child in &mut node.children {
child.node_type = cell;
}
node
}
fn is_conditional_tag(node: &Node<'_>, conditional_tags: &[&str]) -> bool {
node.node_type == NodeType::Tag
&& node
.tag
.as_deref()
.is_some_and(|tag| conditional_tags.contains(&tag))
}
fn is_comment(node: &Node<'_>) -> bool {
node.node_type == NodeType::Comment
|| (node.node_type == NodeType::Tag && node.tag.as_deref() == Some("comment"))
}
fn unexpected_node_error<'a>(node: &Node<'a>) -> ValidationError<'a> {
let what = match &node.tag {
Some(tag) => format!("{} {tag}", node.node_type),
None => node.node_type.to_string(),
};
let error = ValidationError::new(
"table-syntax",
ErrorLevel::Critical,
format!(
"Found {what} where a list was expected. \
Make sure all content inside table cells is indented."
),
);
match node.location {
Some(location) => error.at(location),
None => error,
}
}