use super::*;
pub(super) fn generate_table(
out: &mut String,
table: &Table,
ctx: &mut GenCtx,
) -> Result<(), ConvertError> {
ctx.table_depth += 1;
let result = match table.alignment {
Some(Alignment::Center) => {
out.push_str("#align(center)[\n");
let result = generate_table_inner(out, table, ctx);
out.push_str("]\n");
result
}
Some(Alignment::Right) => {
out.push_str("#align(right)[\n");
let result = generate_table_inner(out, table, ctx);
out.push_str("]\n");
result
}
_ => generate_table_inner(out, table, ctx),
};
ctx.table_depth -= 1;
result
}
fn generate_table_inner(
out: &mut String,
table: &Table,
ctx: &mut GenCtx,
) -> Result<(), ConvertError> {
out.push_str("#table(\n");
out.push_str(" stroke: none,\n");
if let Some(ref default_vertical_align) = table.default_vertical_align {
let align_str: &str = match default_vertical_align {
CellVerticalAlign::Top => "top",
CellVerticalAlign::Center => "horizon",
CellVerticalAlign::Bottom => "bottom",
};
let _ = writeln!(out, " align: {align_str},");
}
if let Some(padding) = table.default_cell_padding {
let _ = writeln!(out, " inset: {},", format_insets(&padding));
}
let num_cols = if !table.column_widths.is_empty() {
table.column_widths.len()
} else {
table.rows.iter().map(|r| r.cells.len()).max().unwrap_or(0)
};
if !table.column_widths.is_empty() {
out.push_str(" columns: (");
for (i, w) in table.column_widths.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
let _ = write!(out, "{}pt", format_f64(*w));
}
out.push_str("),\n");
} else if num_cols > 1 {
let _ = writeln!(out, " columns: {num_cols},");
}
if !table.use_content_driven_row_heights && table.rows.iter().any(|row| row.height.is_some()) {
out.push_str(" rows: (");
for (i, row) in table.rows.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
match row.height {
Some(height) => {
let _ = write!(out, "{}pt", format_f64(height));
}
None => out.push_str("auto"),
}
}
out.push_str("),\n");
}
let mut rowspan_remaining = vec![0usize; num_cols];
let header_row_count = table.header_row_count.min(table.rows.len());
let default_cell_padding = table.default_cell_padding.unwrap_or(Insets {
top: 5.0,
right: 5.0,
bottom: 5.0,
left: 5.0,
});
let fixed_row_heights = !table.use_content_driven_row_heights;
let lead_row_count = table
.non_repeating_header_row_count
.min(table.rows.len().saturating_sub(header_row_count));
if lead_row_count > 0 {
out.push_str(" table.header(repeat: false,\n");
generate_table_rows(
out,
&table.rows[..lead_row_count],
num_cols,
&mut rowspan_remaining,
" ",
default_cell_padding,
fixed_row_heights,
ctx,
)?;
out.push_str(" ),\n");
}
if header_row_count > 0 {
if lead_row_count > 0 {
out.push_str(" table.header(level: 2,\n");
} else {
out.push_str(" table.header(\n");
}
generate_table_rows(
out,
&table.rows[lead_row_count..lead_row_count + header_row_count],
num_cols,
&mut rowspan_remaining,
" ",
default_cell_padding,
fixed_row_heights,
ctx,
)?;
out.push_str(" ),\n");
}
generate_table_rows(
out,
&table.rows[lead_row_count + header_row_count..],
num_cols,
&mut rowspan_remaining,
" ",
default_cell_padding,
fixed_row_heights,
ctx,
)?;
out.push_str(")\n");
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn generate_table_rows(
out: &mut String,
rows: &[TableRow],
num_cols: usize,
rowspan_remaining: &mut [usize],
indent: &str,
default_cell_padding: Insets,
fixed_row_heights: bool,
ctx: &mut GenCtx,
) -> Result<(), ConvertError> {
let enclosing_row_has_east_asian_text: bool = ctx.row_has_east_asian_text;
for row in rows {
for rs in rowspan_remaining.iter_mut() {
if *rs > 0 {
*rs -= 1;
}
}
ctx.row_has_east_asian_text = row_has_east_asian_text(row);
let mut col_pos: usize = 0;
for cell in &row.cells {
if cell.col_span == 0 || cell.row_span == 0 {
continue;
}
while col_pos < num_cols && rowspan_remaining[col_pos] > 0 {
col_pos += 1;
}
if col_pos >= num_cols {
break;
}
let remaining = num_cols - col_pos;
let clamped_colspan = (cell.col_span as usize).min(remaining).max(1) as u32;
generate_table_cell(
out,
cell,
clamped_colspan,
indent,
default_cell_padding,
row.height.filter(|_| fixed_row_heights),
ctx,
)?;
if cell.row_span > 1 {
for rs in rowspan_remaining
.iter_mut()
.skip(col_pos)
.take(clamped_colspan as usize)
{
*rs = cell.row_span as usize;
}
}
col_pos += clamped_colspan as usize;
}
while col_pos < num_cols {
if rowspan_remaining[col_pos] == 0 {
let _ = writeln!(out, "{indent}[],");
}
col_pos += 1;
}
}
ctx.row_has_east_asian_text = enclosing_row_has_east_asian_text;
Ok(())
}
fn row_has_east_asian_text(row: &TableRow) -> bool {
row.cells
.iter()
.flat_map(|cell| cell.content.iter())
.any(block_has_east_asian_text)
}
fn block_has_east_asian_text(block: &Block) -> bool {
match block {
Block::Paragraph(paragraph) => paragraph
.runs
.iter()
.any(|run| run.text.chars().any(is_cjk_like)),
Block::List(list) => {
list.items
.iter()
.flat_map(|item| item.content.iter())
.any(|paragraph| {
paragraph
.runs
.iter()
.any(|run| run.text.chars().any(is_cjk_like))
})
}
_ => false,
}
}
const DATA_BAR_VERTICAL_INSET_PT: f64 = 2.0;
const DATA_BAR_MIN_HEIGHT_PT: f64 = 1.0;
const ARROW_ICON_LENGTH_PT: f64 = 10.0;
const ARROW_ICON_BREADTH_PT: f64 = 8.0;
fn arrow_icon_polygon(glyph: &str, color: Option<Color>) -> Option<String> {
let breadth: f64 = ARROW_ICON_BREADTH_PT;
let length: f64 = ARROW_ICON_LENGTH_PT;
let shaft: f64 = breadth * 0.28;
let neck: f64 = length * 0.45;
let up: Vec<(f64, f64)> = vec![
(breadth / 2.0, 0.0),
(breadth, neck),
(breadth / 2.0 + shaft, neck),
(breadth / 2.0 + shaft, length),
(breadth / 2.0 - shaft, length),
(breadth / 2.0 - shaft, neck),
(0.0, neck),
];
let flip_y = |points: &[(f64, f64)]| -> Vec<(f64, f64)> {
points.iter().map(|(x, y)| (*x, length - *y)).collect()
};
let transpose = |points: &[(f64, f64)]| -> Vec<(f64, f64)> {
points.iter().map(|(x, y)| (length - *y, *x)).collect()
};
let (points, rotation): (Vec<(f64, f64)>, Option<i32>) = match glyph {
crate::ir::ICON_ARROW_UP => (up, None),
crate::ir::ICON_ARROW_DOWN => (flip_y(&up), None),
crate::ir::ICON_ARROW_RIGHT => (transpose(&up), None),
crate::ir::ICON_ARROW_UP_RIGHT => (up, Some(45)),
crate::ir::ICON_ARROW_DOWN_RIGHT => (flip_y(&up), Some(-45)),
_ => return None,
};
let coordinates: String = points
.iter()
.map(|(x, y)| format!("({}pt, {}pt)", format_f64(*x), format_f64(*y)))
.collect::<Vec<String>>()
.join(", ");
let paint: String = color
.map(|c| rgb(&c))
.unwrap_or_else(|| "black".to_string());
let shape: String =
format!("polygon(fill: {paint}, stroke: 0.4pt + {paint}.darken(30%), {coordinates})");
Some(match rotation {
Some(degrees) => format!("rotate({degrees}deg, {shape})"),
None => shape,
})
}
fn generate_table_cell(
out: &mut String,
cell: &TableCell,
clamped_colspan: u32,
indent: &str,
default_cell_padding: Insets,
row_height: Option<f64>,
ctx: &mut GenCtx,
) -> Result<(), ConvertError> {
let needs_cell_fn = clamped_colspan > 1
|| cell.row_span > 1
|| cell.border.is_some()
|| cell.background.is_some()
|| cell.vertical_align.is_some()
|| cell.padding.is_some();
if needs_cell_fn {
out.push_str(indent);
out.push_str("table.cell(");
write_cell_params(out, cell, clamped_colspan, default_cell_padding);
out.push_str(")[");
} else {
out.push_str(indent);
out.push('[');
}
if let Some(border) = &cell.border {
write_double_border_overlays(out, border, cell.padding.unwrap_or(default_cell_padding));
}
if let Some(ref db) = cell.data_bar {
let pct = db.fill_pct.clamp(0.0, 100.0);
let bar_height: String = match row_height {
Some(height) => {
let inset_height =
(height - 2.0 * DATA_BAR_VERTICAL_INSET_PT).max(DATA_BAR_MIN_HEIGHT_PT);
format!("{}pt", format_f64(inset_height))
}
None => format!("1.2em - {}pt", format_f64(2.0 * DATA_BAR_VERTICAL_INSET_PT)),
};
let _ = write!(
out,
"#place(left + horizon, box(width: {}%, height: {}, fill: gradient.linear({}, {}.lighten(70%))))",
format_f64(pct),
bar_height,
rgb(&db.color),
rgb(&db.color),
);
}
if let Some(ref icon) = cell.icon_text {
match (arrow_icon_polygon(icon, cell.icon_color), cell.icon_color) {
(Some(polygon), _) => {
let _ = write!(out, "#place(left + horizon, {polygon})");
}
(None, Some(color)) => {
let _ = write!(
out,
"#place(left + horizon, text(fill: {}, weight: \"bold\")[{}])",
rgb(&color),
icon
);
}
(None, None) => {
let _ = write!(
out,
"#place(left + horizon, text(weight: \"bold\")[{icon}])"
);
}
}
}
if let Some(spill_width) = cell.spill_width {
let _ = write!(
out,
"#place(left + horizon, box(width: {}pt, height: 1.3em, clip: true)[",
format_f64(spill_width),
);
generate_cell_content(out, &cell.content, ctx)?;
out.push_str("])#box(width: 0pt, height: 1.3em)");
} else {
generate_cell_content(out, &cell.content, ctx)?;
}
out.push_str("],\n");
Ok(())
}
fn write_double_border_overlays(out: &mut String, border: &CellBorder, padding: Insets) {
if let Some(side) = border
.top
.as_ref()
.filter(|side| side.style == BorderLineStyle::Double)
{
write_horizontal_double_border(out, side, padding, true);
}
if let Some(side) = border
.bottom
.as_ref()
.filter(|side| side.style == BorderLineStyle::Double)
{
write_horizontal_double_border(out, side, padding, false);
}
if let Some(side) = border
.left
.as_ref()
.filter(|side| side.style == BorderLineStyle::Double)
{
write_vertical_double_border(out, side, padding, true);
}
if let Some(side) = border
.right
.as_ref()
.filter(|side| side.style == BorderLineStyle::Double)
{
write_vertical_double_border(out, side, padding, false);
}
}
fn write_horizontal_double_border(
out: &mut String,
side: &BorderSide,
padding: Insets,
is_top: bool,
) {
let align = if is_top {
"top + left"
} else {
"bottom + left"
};
let first_dy = if is_top {
-padding.top - side.width
} else {
padding.bottom - side.width
};
let second_dy = if is_top {
-padding.top + side.width
} else {
padding.bottom + side.width
};
let dx = -padding.left;
let length_extra = padding.left + padding.right;
write_double_border_line(out, align, dx, first_dy, "0deg", length_extra, side);
write_double_border_line(out, align, dx, second_dy, "0deg", length_extra, side);
}
fn write_vertical_double_border(
out: &mut String,
side: &BorderSide,
padding: Insets,
is_left: bool,
) {
let align = if is_left { "top + left" } else { "top + right" };
let first_dx = if is_left {
-padding.left - side.width
} else {
padding.right - side.width
};
let second_dx = if is_left {
-padding.left + side.width
} else {
padding.right + side.width
};
let dy = -padding.top;
let length_extra = padding.top + padding.bottom;
write_double_border_line(out, align, first_dx, dy, "90deg", length_extra, side);
write_double_border_line(out, align, second_dx, dy, "90deg", length_extra, side);
}
fn write_double_border_line(
out: &mut String,
align: &str,
dx: f64,
dy: f64,
angle: &str,
length_extra: f64,
side: &BorderSide,
) {
let _ = write!(
out,
"#place({align}, dx: {}pt, dy: {}pt, line(length: 100% + {}pt, angle: {angle}, stroke: {}pt + {}))",
format_geometry(dx),
format_geometry(dy),
format_geometry(length_extra),
format_geometry(side.width),
rgb(&side.color),
);
}
fn format_geometry(value: f64) -> String {
let rounded = (value * 1_000.0).round() / 1_000.0;
format_f64(if rounded == -0.0 { 0.0 } else { rounded })
}
fn cell_inset_with_border(cell: &TableCell, default_cell_padding: Insets) -> Insets {
let padding: Insets = cell.padding.unwrap_or(default_cell_padding);
let Some(border) = &cell.border else {
return padding;
};
let half = |side: &Option<BorderSide>| side.as_ref().map_or(0.0, |s| s.width / 2.0);
Insets {
top: padding.top + half(&border.top),
bottom: padding.bottom + half(&border.bottom),
..padding
}
}
fn write_cell_params(
out: &mut String,
cell: &TableCell,
clamped_colspan: u32,
default_cell_padding: Insets,
) {
let mut first = true;
if clamped_colspan > 1 {
write_param(out, &mut first, &format!("colspan: {clamped_colspan}"));
}
if cell.row_span > 1 {
write_param(out, &mut first, &format!("rowspan: {}", cell.row_span));
}
if let Some(ref bg) = cell.background {
write_param(out, &mut first, &format_color(bg));
}
let inset: Insets = cell_inset_with_border(cell, default_cell_padding);
if cell.padding.is_some() || cell.border.is_some() {
write_param(
out,
&mut first,
&format!("inset: {}", format_insets(&inset)),
);
}
if let Some(ref border) = cell.border {
let stroke = format_cell_stroke(border);
if !stroke.is_empty() {
write_param(out, &mut first, &stroke);
}
}
if let Some(ref va) = cell.vertical_align {
let align_str: &str = match va {
CellVerticalAlign::Top => "top",
CellVerticalAlign::Center => "horizon",
CellVerticalAlign::Bottom => "bottom",
};
write_param(out, &mut first, &format!("align: {align_str}"));
}
}
fn format_cell_stroke(border: &CellBorder) -> String {
let mut parts = Vec::with_capacity(4);
if let Some(ref side) = border.top
&& side.style != BorderLineStyle::Double
{
parts.push(format!("top: {}", format_border_side(side)));
}
if let Some(ref side) = border.bottom
&& side.style != BorderLineStyle::Double
{
parts.push(format!("bottom: {}", format_border_side(side)));
}
if let Some(ref side) = border.left
&& side.style != BorderLineStyle::Double
{
parts.push(format!("left: {}", format_border_side(side)));
}
if let Some(ref side) = border.right
&& side.style != BorderLineStyle::Double
{
parts.push(format!("right: {}", format_border_side(side)));
}
if parts.is_empty() {
String::new()
} else {
format!("stroke: ({})", parts.join(", "))
}
}
fn format_border_side(side: &BorderSide) -> String {
stroke_value(side, true)
}
fn generate_cell_content(
out: &mut String,
blocks: &[Block],
ctx: &mut GenCtx,
) -> Result<(), ConvertError> {
for (i, block) in blocks.iter().enumerate() {
if i > 0 {
out.push('\n');
}
match block {
Block::Paragraph(para) => generate_cell_paragraph(
out,
para,
ctx.default_tab_width_pt,
ctx.line_grid_pitch,
ctx.row_has_east_asian_text,
),
Block::Table(table) => {
if ctx.table_depth < MAX_TABLE_DEPTH {
generate_table(out, table, ctx)?;
}
}
Block::Image(img) => generate_image(out, img, ctx),
Block::InlineImages(images) => {
for image in images {
generate_image(out, image, ctx);
}
}
Block::FloatingImage(fi) => generate_floating_image(out, fi, ctx),
Block::FloatingTextBox(ftb) => generate_floating_text_box(out, ftb, ctx)?,
Block::FloatingShape(fs) => generate_floating_shape(out, fs),
Block::List(list) => {
if can_render_fixed_text_list_inline(list) {
generate_fixed_text_list(out, list, true, None)?;
} else {
generate_list(out, list, None)?;
}
}
Block::MathEquation(math) => generate_math_equation(out, math),
Block::Chart(chart) => generate_chart(out, chart),
Block::PageBreak | Block::ColumnBreak => {}
}
}
Ok(())
}
fn generate_cell_paragraph(
out: &mut String,
para: &Paragraph,
default_tab_width_pt: f64,
line_grid_pitch: Option<f64>,
row_has_east_asian_text: bool,
) {
let style: &ParagraphStyle = ¶.style;
let alignment = style.alignment;
let align_str: Option<&str> = match alignment {
Some(Alignment::Left) => Some("left"),
Some(Alignment::Center) => Some("center"),
Some(Alignment::Right) => Some("right"),
_ => None,
};
let line_height_settings: Option<String> =
word_cell_line_box_settings(¶.runs, style, line_grid_pitch, row_has_east_asian_text);
let has_block_wrapper = cell_paragraph_needs_block_wrapper(style)
|| align_str.is_some()
|| line_height_settings.is_some();
if has_block_wrapper {
out.push_str("#block(");
write_cell_paragraph_block_params(out, align_str.is_some());
out.push_str(")[\n");
write_line_box_settings(out, style.line_box);
write_par_settings(out, style);
if let Some(align_str) = align_str {
let _ = writeln!(out, " #set align({align_str})");
}
if let Some(ref settings) = line_height_settings {
out.push_str(settings);
}
}
if let Some(space_before) = style.space_before {
let _ = writeln!(out, "#v({}pt)", format_f64(space_before));
}
generate_runs_with_tabs(
out,
¶.runs,
style.tab_stops.as_deref(),
default_tab_width_pt,
);
if let Some(space_after) = style.space_after
&& !cell_grid_absorbs_space_after(style, line_grid_pitch, row_has_east_asian_text)
{
let _ = write!(out, "\n#v({}pt)", format_f64(space_after));
}
if has_block_wrapper {
out.push_str("\n]");
}
}
fn cell_paragraph_needs_block_wrapper(style: &ParagraphStyle) -> bool {
style.line_spacing.is_some()
|| style.line_box.is_some()
|| matches!(style.alignment, Some(Alignment::Justify))
|| matches!(style.direction, Some(TextDirection::Rtl))
}
fn write_cell_paragraph_block_params(out: &mut String, needs_full_width: bool) {
let mut first = true;
if needs_full_width {
write_param(out, &mut first, "width: 100%");
}
}