use std::fmt::Write as _;
use asciidoc_parser::blocks::{
Block,
ColumnStyle,
Frame,
Grid,
HorizontalAlignment,
IsBlock,
Stripes,
TableBlock,
TableCell,
TableCellContent,
TableColumn,
TableRow,
VerticalAlignment,
};
use crate::{
Renderer,
html::escape_attr,
};
impl<'src> Renderer<'src> {
pub(super) fn table_block(&mut self, block: &'src Block<'src>, table: &'src TableBlock<'src>) {
let mut classes = vec!["tableblock".to_string()];
classes.push(frame_class(table.frame()));
classes.push(grid_class(table.grid()));
if let Some(stripes) = stripes_class(table.stripes()) {
classes.push(stripes);
}
let width = table.width().filter(|&width| width < 100);
if width.is_none() {
classes.push(
if table.is_autowidth() {
"fit-content"
} else {
"stretch"
}
.to_string(),
);
}
if let Some(float) = block
.attrlist()
.and_then(|attrlist| attrlist.named_attribute("float"))
.map(asciidoc_parser::attributes::ElementAttribute::value)
.filter(|float| !float.is_empty())
{
classes.push(float.to_string());
}
classes.extend(block.roles().into_iter().map(str::to_string));
let mut open = String::from("<table");
if let Some(id) = block.id() {
let _ = write!(open, " id=\"{}\"", escape_attr(id));
}
let _ = write!(open, " class=\"{}\"", escape_attr(&classes.join(" ")));
if let Some(width) = width {
let _ = write!(open, " style=\"width: {width}%;\"");
}
open.push('>');
self.out.line(&open);
if let Some(title) = block.title() {
let caption = block.caption().unwrap_or_default();
self.out.line(&format!(
"<caption class=\"title\">{caption}{title}</caption>"
));
}
self.colgroup(table);
if let Some(header) = table.header_row() {
self.out.line("<thead>");
self.row(header, table.columns(), true);
self.out.line("</thead>");
}
let body = table.body_rows();
if !body.is_empty() {
self.out.line("<tbody>");
for row in body {
self.row(row, table.columns(), false);
}
self.out.line("</tbody>");
}
if let Some(footer) = table.footer_row() {
self.out.line("<tfoot>");
self.row(footer, table.columns(), false);
self.out.line("</tfoot>");
}
self.out.close("table");
}
fn colgroup(&mut self, table: &'src TableBlock<'src>) {
let columns = table.columns();
if columns.is_empty() {
return;
}
self.out.line("<colgroup>");
let total: usize = columns.iter().map(TableColumn::width).sum();
let mixed = columns.iter().any(TableColumn::is_autowidth);
let mut shares: Vec<Option<u64>> = columns
.iter()
.map(|column| {
if table.is_autowidth() || total == 0 || column.is_autowidth() {
None
} else if mixed {
Some(percentage(column.width(), 100))
} else {
Some(percentage(column.width(), total))
}
})
.collect();
if !mixed {
balance(&mut shares);
}
for share in shares {
match share {
None => self.out.line("<col>"),
Some(share) => self
.out
.line(&format!("<col style=\"width: {}%;\">", trim_percent(share))),
}
}
self.out.close("colgroup");
}
fn row(&mut self, row: &'src TableRow<'src>, columns: &'src [TableColumn], is_header: bool) {
self.out.line("<tr>");
for (index, cell) in row.cells().iter().enumerate() {
self.cell(cell, columns.get(index), is_header);
}
self.out.line("</tr>");
}
fn cell(
&mut self,
cell: &'src TableCell<'src>,
column: Option<&'src TableColumn>,
is_header: bool,
) {
let style = cell.style();
let header = is_header || style == ColumnStyle::Header;
let tag = if header { "th" } else { "td" };
let mut open = format!(
"<{tag} class=\"tableblock halign-{} valign-{}\"",
halign(cell.h_align(), column),
valign(cell.v_align(), column)
);
if cell.colspan() > 1 {
let _ = write!(open, " colspan=\"{}\"", cell.colspan());
}
if cell.rowspan() > 1 {
let _ = write!(open, " rowspan=\"{}\"", cell.rowspan());
}
open.push('>');
self.out.raw(&open);
self.cell_content(cell, is_header);
self.out.line(&format!("</{tag}>"));
}
fn cell_content(&mut self, cell: &'src TableCell<'src>, in_header_row: bool) {
match cell.content() {
TableCellContent::Simple(content) => {
let rendered = content.rendered_html();
if rendered.is_empty() && cell.style() != ColumnStyle::Literal {
return;
}
if in_header_row {
self.out.raw(rendered);
return;
}
if cell.style() == ColumnStyle::Literal {
self.out.raw(&format!(
"<div class=\"literal\"><pre>{rendered}</pre></div>"
));
return;
}
let (open, close) = match cell.style() {
ColumnStyle::Emphasis => ("<em>", "</em>"),
ColumnStyle::Strong => ("<strong>", "</strong>"),
ColumnStyle::Monospace => ("<code>", "</code>"),
_ => ("", ""),
};
let wrapped: Vec<String> = paragraphs(rendered)
.into_iter()
.map(|paragraph| {
format!("<p class=\"tableblock\">{open}{paragraph}{close}</p>")
})
.collect();
self.out.raw(&wrapped.join("\n"));
}
TableCellContent::AsciiDoc(nested) => {
self.out.raw("<div class=\"content\">");
self.blocks(nested.blocks().iter());
self.out.unline();
self.out.raw("</div>");
}
}
}
}
fn frame_class(frame: Frame) -> String {
match frame {
Frame::All => "frame-all",
Frame::Ends => "frame-ends",
Frame::Sides => "frame-sides",
Frame::None => "frame-none",
}
.to_string()
}
fn grid_class(grid: Grid) -> String {
match grid {
Grid::All => "grid-all",
Grid::Rows => "grid-rows",
Grid::Cols => "grid-cols",
Grid::None => "grid-none",
}
.to_string()
}
fn stripes_class(stripes: Stripes) -> Option<String> {
match stripes {
Stripes::None => None,
Stripes::Even => Some("stripes-even".to_string()),
Stripes::Odd => Some("stripes-odd".to_string()),
Stripes::All => Some("stripes-all".to_string()),
Stripes::Hover => Some("stripes-hover".to_string()),
}
}
fn halign(cell: HorizontalAlignment, column: Option<&TableColumn>) -> &'static str {
let alignment = if cell == HorizontalAlignment::Left {
column.map_or(cell, TableColumn::h_align)
} else {
cell
};
match alignment {
HorizontalAlignment::Center => "center",
HorizontalAlignment::Right => "right",
HorizontalAlignment::Left => "left",
}
}
fn valign(cell: VerticalAlignment, column: Option<&TableColumn>) -> &'static str {
let alignment = if cell == VerticalAlignment::Top {
column.map_or(cell, TableColumn::v_align)
} else {
cell
};
match alignment {
VerticalAlignment::Middle => "middle",
VerticalAlignment::Bottom => "bottom",
VerticalAlignment::Top => "top",
}
}
fn paragraphs(rendered: &str) -> Vec<String> {
let mut paragraphs = Vec::new();
let mut current: Vec<&str> = Vec::new();
for line in rendered.lines() {
if line.trim().is_empty() {
if !current.is_empty() {
paragraphs.push(current.join("\n"));
current.clear();
}
} else {
current.push(line);
}
}
if !current.is_empty() {
paragraphs.push(current.join("\n"));
}
paragraphs
}
const FULL: u64 = 100 * 10_000;
fn percentage(width: usize, total: usize) -> u64 {
let width = u64::try_from(width).unwrap_or(u64::MAX);
let total = u64::try_from(total).unwrap_or(u64::MAX);
if total == 0 {
return 0;
}
let scaled = width.saturating_mul(FULL);
scaled / total
}
fn balance(shares: &mut [Option<u64>]) {
let total: u64 = shares.iter().flatten().sum();
let Some(last) = shares.iter_mut().flatten().next_back() else {
return;
};
*last = last.saturating_add(FULL).saturating_sub(total);
}
fn trim_percent(share: u64) -> String {
let formatted = format!("{}.{:04}", share / 10_000, share % 10_000);
let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
trimmed.to_string()
}