use crate::NodeId;
use crate::adapter::AstAdapter;
use crate::value::Value;
pub const TABLE_CSS: &str = include_str!("table.css");
pub fn table_columns(values: &[Value], named: Option<&[String]>) -> Vec<String> {
match named {
Some(names) => names.to_vec(),
None => {
let mut columns: Vec<String> = Vec::new();
let mut has_scalar = false;
for v in values {
match v {
Value::Record(fields) => {
for (k, _) in fields {
if !columns.contains(k) {
columns.push(k.clone());
}
}
}
_ => has_scalar = true,
}
}
if has_scalar || columns.is_empty() {
columns.push("value".to_string());
}
columns
}
}
}
pub fn table_cells(values: &[Value], columns: &[String]) -> Vec<Vec<Value>> {
values
.iter()
.map(|v| match v {
Value::Record(fields) => columns
.iter()
.map(|c| {
fields
.iter()
.find(|(k, _)| k == c)
.map(|(_, v)| v.clone())
.unwrap_or(Value::Null)
})
.collect(),
other => columns
.iter()
.map(|c| {
if c == "value" {
other.clone()
} else {
Value::Null
}
})
.collect(),
})
.collect()
}
pub fn table_html(
values: &[Value],
named: Option<&[String]>,
cell: &dyn Fn(&Value) -> String,
) -> String {
let columns = table_columns(values, named);
let rows = table_cells(values, &columns);
let mut out = String::with_capacity(64 + rows.len() * columns.len() * 24);
out.push_str("<table class=\"quarb-table\">\n<thead>\n<tr>");
for (i, c) in columns.iter().enumerate() {
let numeric = rows.iter().filter_map(|r| r.get(i)).any(is_number)
&& rows
.iter()
.filter_map(|r| r.get(i))
.all(|v| is_number(v) || matches!(v, Value::Null));
if numeric {
out.push_str("<th class=\"num\">");
} else {
out.push_str("<th>");
}
out.push_str(&escape(c));
out.push_str("</th>");
}
out.push_str("</tr>\n</thead>\n<tbody>\n");
for row in &rows {
out.push_str("<tr>");
for v in row {
match v {
Value::Null => out.push_str("<td class=\"null\"></td>"),
_ => {
let class = cell_class(v);
if class.is_empty() {
out.push_str("<td>");
} else {
out.push_str("<td class=\"");
out.push_str(class);
out.push_str("\">");
}
out.push_str(&escape(&cell(v)));
out.push_str("</td>");
}
}
}
out.push_str("</tr>\n");
}
out.push_str("</tbody>\n</table>");
out
}
pub fn table_html_plain(values: &[Value], named: Option<&[String]>) -> String {
table_html(values, named, &|v| match v {
Value::Str(s) => s.clone(),
other => other.display_form(),
})
}
fn is_number(v: &Value) -> bool {
matches!(v, Value::Int(_) | Value::Float(_) | Value::Quantity { .. })
}
fn cell_class(v: &Value) -> &'static str {
match v {
Value::Int(_) | Value::Float(_) | Value::Quantity { .. } => "num",
Value::Bool(_) => "bool",
Value::List(_) | Value::Record(_) => "nested",
Value::Instant { .. } | Value::Duration { .. } => "time",
_ => "",
}
}
pub fn escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
c => out.push(c),
}
}
out
}
pub fn table_rows_of(
adapter: &dyn AstAdapter,
result: crate::QueryResult,
columns: &[String],
render: &dyn Fn(NodeId) -> String,
) -> Vec<Value> {
match result {
crate::QueryResult::Values(values) => values,
crate::QueryResult::Nodes(nodes) => {
let mut rows = Vec::with_capacity(nodes.len());
for n in nodes {
if let Some(records) = table_node_records(adapter, n) {
rows.extend(records);
} else if !columns.is_empty() {
rows.push(Value::Record(
columns
.iter()
.map(|c| (c.clone(), adapter.property(n, c).unwrap_or(Value::Null)))
.collect(),
));
} else {
rows.push(Value::Str(render(n)));
}
}
rows
}
}
}
pub fn table_node_records(adapter: &dyn AstAdapter, node: NodeId) -> Option<Vec<Value>> {
if !adapter.has_trait(node, "tabula") {
return None;
}
let text = |n: NodeId, key: &str| -> Value {
adapter
.property(n, key)
.or_else(|| adapter.default_value(n))
.unwrap_or(Value::Null)
};
let mut records = Vec::new();
for row in adapter.children(node) {
if !adapter.has_trait(row, "row") || adapter.has_trait(row, "header-row") {
continue;
}
let mut fields: Vec<(String, Value)> = Vec::new();
for (i, cell) in cells_of(adapter, row).into_iter().enumerate() {
let key = match adapter.property(cell, "lemma") {
Some(Value::Str(s)) if !s.is_empty() => s,
_ => (i + 1).to_string(),
};
fields.push((key, text(cell, "body")));
}
if !fields.is_empty() {
records.push(Value::Record(fields));
}
}
Some(records)
}
fn cells_of(adapter: &dyn AstAdapter, row: NodeId) -> Vec<NodeId> {
let mut out = Vec::new();
let mut stack: Vec<NodeId> = adapter.children(row);
stack.reverse();
while let Some(n) = stack.pop() {
if adapter.has_trait(n, "cell") {
out.push(n);
continue;
}
let kids = adapter.children(n);
stack.extend(kids.into_iter().rev());
}
out
}