use std::collections::HashMap;
use std::fmt::Write as _;
use rustyfi_backend::{
graphics_bbox, path_bbox, Color, DecoId, FrameDecoration, GraphicsElem, Length, OutlineEntry,
PathSeg, PureHorzBox, TabularBox, TabularCellBox,
};
use super::Ctx;
pub(crate) fn outline_levels(outline: &[OutlineEntry]) -> HashMap<String, i64> {
outline
.iter()
.map(|entry| (entry.dest_name.clone(), entry.level))
.collect()
}
pub(crate) fn heading_tag(level: i64) -> u8 {
(level.max(0) as u64 + 1).min(6) as u8
}
pub(crate) fn find_heading_level(bx: &PureHorzBox, ctx: &Ctx) -> Option<i64> {
match bx {
PureHorzBox::InlineFrameMarker { id, end: false, .. } => level_of_deco(id, ctx),
PureHorzBox::Frame { deco, contents, .. } => level_of_deco(deco, ctx).or_else(|| {
contents
.iter()
.find_map(|(_, inner)| find_heading_level(inner, ctx))
}),
_ => None,
}
}
fn level_of_deco(deco: &DecoId, ctx: &Ctx) -> Option<i64> {
let name = ctx.dests.get(deco)?;
ctx.outline_by_dest.get(*name).copied()
}
pub(crate) fn render_table(out: &mut String, tab: &TabularBox, extra_attrs: &str, ctx: &Ctx) {
if tab.cells.is_empty() {
return;
}
let mut rows: Vec<Vec<&TabularCellBox>> = Vec::new();
let mut last_x: Option<f64> = None;
for cell in &tab.cells {
let x = cell.x.0;
let starts_new_row = match last_x {
None => true,
Some(lx) => x <= lx,
};
if starts_new_row {
rows.push(Vec::new());
}
rows.last_mut().expect("just pushed if empty").push(cell);
last_x = Some(x);
}
let paired;
let rules: &[GraphicsElem] = if tab.rules.is_empty() {
paired = ctx
.tabular_rules
.borrow()
.iter()
.rev()
.find(|(w, h, _)| {
(w - tab.width.0).abs() < RULE_EPS_PT && (h - tab.height.0).abs() < RULE_EPS_PT
})
.map(|(_, _, r)| r.clone());
paired.as_deref().unwrap_or(&[])
} else {
&tab.rules
};
let borders = Borders::solve(&rows, rules);
let mut table = String::new();
let mut any_content = false;
let _ = write!(table, "<table class=\"tabular\"{extra_attrs}>\n");
for (r, row) in rows.iter().enumerate() {
table.push_str("<tr>\n");
for (c, cell) in row.iter().enumerate() {
let _ = write!(table, "<td{}>", borders.style_for(r, c, row.len()));
ctx.reset_flow();
let mut cell_html = String::new();
for (_, bx) in &cell.contents {
super::inline::emit_inline(&mut cell_html, bx, ctx);
}
super::inline::close_run(&mut cell_html, ctx);
ctx.reset_flow();
let trimmed = cell_html.trim();
any_content |= super::text::has_visible_content(trimmed);
table.push_str(trimmed);
table.push_str("</td>\n");
}
table.push_str("</tr>\n");
}
table.push_str("</table>\n");
if any_content {
out.push_str(&table);
}
}
struct Borders {
horizontal: Vec<Option<Rule>>,
vertical: Vec<Option<Rule>>,
}
#[derive(Clone, Copy)]
struct Rule {
width: f64,
color: Color,
}
const RULE_EPS_PT: f64 = 0.05;
impl Borders {
fn solve(rows: &[Vec<&TabularCellBox>], rules: &[GraphicsElem]) -> Self {
let ncols = rows.iter().map(Vec::len).max().unwrap_or(0);
let mut borders = Borders {
horizontal: vec![None; rows.len() + 1],
vertical: vec![None; ncols + 1],
};
let baselines: Vec<f64> = rows
.iter()
.map(|row| row.first().map_or(0.0, |c| c.baseline_y.0))
.collect();
let lefts: Vec<f64> = (0..ncols)
.map(|c| {
rows.iter()
.filter_map(|row| row.get(c).map(|cell| cell.x.0))
.fold(f64::INFINITY, f64::min)
})
.collect();
for elem in rules {
collect_rule(elem, &baselines, &lefts, &mut borders);
}
borders
}
fn style_for(&self, r: usize, c: usize, row_len: usize) -> String {
let mut decls = String::new();
let mut edge = |side: &str, rule: Option<Rule>| {
if let Some(rule) = rule {
let _ = write!(
decls,
"border-{side}:{}pt solid {};",
rule.width,
crate::svg::css_color(rule.color),
);
}
};
edge("top", self.horizontal.get(r).copied().flatten());
edge("left", self.vertical.get(c).copied().flatten());
if r + 1 == self.horizontal.len() - 1 {
edge("bottom", self.horizontal[r + 1]);
}
if c + 1 == row_len {
edge("right", self.vertical.get(c + 1).copied().flatten());
}
if decls.is_empty() {
String::new()
} else {
format!(" style=\"{decls}\"")
}
}
}
fn collect_rule(
elem: &GraphicsElem,
baselines: &[f64],
lefts: &[f64],
borders: &mut Borders,
) {
let (color, stroke_w) = match elem {
GraphicsElem::Fill(c, _) => (*c, None),
GraphicsElem::Stroke(w, c, _) | GraphicsElem::DashedStroke(w, _, c, _) => (*c, Some(w.0)),
GraphicsElem::Group(inner) | GraphicsElem::Clip(_, inner) => {
for e in inner {
collect_rule(e, baselines, lefts, borders);
}
return;
}
_ => return,
};
let Some((lo, hi)) = graphics_bbox(elem) else {
return;
};
let (Length(x0), Length(y0)) = lo;
let (Length(x1), Length(y1)) = hi;
let (w, h) = (x1 - x0, y1 - y0);
if w >= h {
let y = (y0 + y1) / 2.0;
let above = baselines.iter().filter(|b| **b > y + RULE_EPS_PT).count();
let rule = Rule {
width: stroke_w.unwrap_or(h).max(RULE_EPS_PT),
color,
};
if let Some(slot) = borders.horizontal.get_mut(above) {
*slot = Some(rule);
}
} else {
let x = (x0 + x1) / 2.0;
let left_of = lefts.iter().filter(|l| **l < x - RULE_EPS_PT).count();
let rule = Rule {
width: stroke_w.unwrap_or(w).max(RULE_EPS_PT),
color,
};
if let Some(slot) = borders.vertical.get_mut(left_of) {
*slot = Some(rule);
}
}
}
pub(crate) struct FrameRender {
pub(crate) extra_class: &'static str,
pub(crate) style: String,
pub(crate) svg: String,
}
impl FrameRender {
fn none() -> Self {
FrameRender {
extra_class: "",
style: String::new(),
svg: String::new(),
}
}
}
pub(crate) fn frame_decoration(deco: &DecoId, ctx: &Ctx) -> FrameRender {
let Some(frame) = ctx.frame_decos.get(deco) else {
return FrameRender::none();
};
if frame.elems.is_empty() || frame.width.0 <= 0.0 || frame.height.0 <= 0.0 {
return FrameRender::none();
}
if let Some(color) = solid_panel(frame) {
return FrameRender {
extra_class: " framed",
style: format!("background:{};{}", crate::svg::css_color(color), pad_right(frame)),
svg: String::new(),
};
}
let mut svg = String::new();
crate::svg::emit_graphics(
&mut svg,
&frame.elems,
frame.width.0,
frame.height.0,
0.0,
0.0,
frame.height.0,
&mut |_svg, _bx, _x, _y| {},
);
FrameRender {
extra_class: " framed",
style: pad_right(frame),
svg: retarget_svg(&svg, frame),
}
}
fn pad_right(frame: &FrameDecoration) -> String {
let r = frame.pads.1 .0;
if r > 0.5 {
format!("padding-right:{r}pt;")
} else {
String::new()
}
}
fn retarget_svg(svg: &str, frame: &FrameDecoration) -> String {
let (w, h) = (frame.width.0, frame.height.0);
let (mut x0, mut y0, mut x1, mut y1) = (0.0f64, 0.0f64, w, h);
for elem in &frame.elems {
if let Some(((Length(ex0), Length(ey0)), (Length(ex1), Length(ey1)))) = graphics_bbox(elem)
{
x0 = x0.min(ex0);
y0 = y0.min(ey0);
x1 = x1.max(ex1);
y1 = y1.max(ey1);
}
}
let view = format!(
"viewBox=\"{} {} {} {}\"",
x0,
h - y1,
(x1 - x0).max(0.01),
(y1 - y0).max(0.01),
);
let Some(end) = svg.find('>') else {
return svg.to_string();
};
format!(
"<svg class=\"frame-deco\" preserveAspectRatio=\"none\" {view}{}",
&svg[end..]
)
}
fn solid_panel(frame: &FrameDecoration) -> Option<Color> {
let [GraphicsElem::Fill(color, path)] = frame.elems.as_slice() else {
return None;
};
let ((Length(x0), Length(y0)), (Length(x1), Length(y1))) = path_bbox(path);
let covers = x0 <= RULE_EPS_PT
&& y0 <= RULE_EPS_PT
&& (x1 - frame.width.0).abs() < 1.0
&& (y1 - frame.height.0).abs() < 1.0;
let rectangular = path.subpaths.len() == 1
&& path.subpaths[0]
.segs
.iter()
.all(|s| matches!(s, PathSeg::Line(_)));
(covers && rectangular).then_some(*color)
}