use std::fmt::Write as _;
use rustyfi_backend::{ListMarkKind, PureHorzBox, VertBox};
use super::{inline, structure, Ctx};
struct Para {
text: String,
open: bool,
heading_level: Option<i64>,
leading_fil: bool,
trailing_fil: bool,
wrapper_base: usize,
indent: Option<f64>,
mono: bool,
mixed: bool,
}
impl Para {
fn new(wrapper_base: usize) -> Self {
Para {
text: String::new(),
open: false,
heading_level: None,
leading_fil: false,
trailing_fil: false,
wrapper_base,
indent: None,
mono: false,
mixed: false,
}
}
fn indent_decl(&self, in_list: bool) -> String {
match self.indent {
Some(x) if x >= INDENT_MIN_PT && !in_list => format!("margin-left:{x}pt;"),
_ => String::new(),
}
}
fn alignment(&self) -> Option<&'static str> {
match (self.leading_fil, self.trailing_fil) {
(true, true) => Some("center"),
(true, false) => Some("right"),
_ => None,
}
}
}
pub(crate) fn walk_vboxes(out: &mut String, vboxes: &[VertBox], ctx: &Ctx) {
let mut para = Para::new(ctx.iframe_stack.borrow().len());
let mut pending_margin: f64 = 0.0;
let mut list_stack: Vec<bool> = Vec::new();
for vb in vboxes {
match vb {
VertBox::Line { contents, .. } => {
if !matches!(contents.first(), Some((_, PureHorzBox::OuterFil))) {
if let Some((x, _)) = contents.first() {
let x = x.0;
para.indent = Some(para.indent.map_or(x, |cur: f64| cur.min(x)));
}
}
for (_, bx) in contents {
match bx {
PureHorzBox::EmbeddedBlock { block, .. } => {
flush_para(out, &mut para, &mut pending_margin, ctx, !list_stack.is_empty());
let margin = take_margin(&mut pending_margin);
let _ = write!(out, "<div class=\"embed\"{margin}>\n");
walk_vboxes(out, block, ctx);
out.push_str("</div>\n");
}
PureHorzBox::Tabular(tab) => {
flush_para(out, &mut para, &mut pending_margin, ctx, !list_stack.is_empty());
let margin = take_margin(&mut pending_margin);
structure::render_table(out, tab, &margin, ctx);
}
PureHorzBox::OuterFil => {
if para.text.trim().is_empty() {
para.leading_fil = true;
}
para.trailing_fil = true;
inline::emit_inline(&mut para.text, bx, ctx);
}
other => {
if para.heading_level.is_none() {
para.heading_level = structure::find_heading_level(other, ctx);
}
if !para.open {
inline::reopen_wrappers(&mut para.text, ctx, para.wrapper_base);
}
para.open = true;
let before = para.text.len();
inline::emit_inline(&mut para.text, other, ctx);
if para.text.len() != before {
para.trailing_fil = false;
}
if matches!(other, PureHorzBox::InnerString { .. }) {
if ctx.mono_run.get() {
para.mono = !para.mixed;
} else {
para.mono = false;
para.mixed = true;
}
}
}
}
}
if para.open {
if para.mono {
inline::close_run(&mut para.text, ctx);
para.text.push_str("<br>\n");
ctx.break_hyphen.set(false);
ctx.reset_flow();
} else if ctx.break_hyphen.replace(false) {
drop_break_hyphen(&mut para.text);
} else if ends_with_hyphen(¶.text) {
} else {
ctx.note_glue(WORD_SPACE_PT);
}
}
}
VertBox::Skip(len) | VertBox::ParagTop(len) | VertBox::FramePad(len) => {
flush_para(out, &mut para, &mut pending_margin, ctx, !list_stack.is_empty());
pending_margin = pending_margin.max(len.0);
}
VertBox::ClearPage => {
flush_para(out, &mut para, &mut pending_margin, ctx, !list_stack.is_empty());
let margin = take_margin(&mut pending_margin);
let _ = write!(out, "<hr class=\"clearpage\"{margin}>\n");
}
VertBox::HookPageBreak(_) => {}
VertBox::FrameStart(deco) => {
flush_para(out, &mut para, &mut pending_margin, ctx, !list_stack.is_empty());
let margin = margin_decl(&mut pending_margin);
let id_attr = match ctx.dests.get(deco) {
Some(name) => format!(" id=\"{}\"", crate::escape_html(name)),
None => String::new(),
};
let deco_render = structure::frame_decoration(deco, ctx);
let style = style_attr(&[&margin, &deco_render.style]);
let _ = write!(
out,
"<div class=\"frame{}\"{id_attr}{style}>\n{}",
deco_render.extra_class, deco_render.svg,
);
}
VertBox::FrameEnd(_deco) => {
flush_para(out, &mut para, &mut pending_margin, ctx, !list_stack.is_empty());
out.push_str("</div>\n");
}
VertBox::ListMark(kind) => {
flush_para(out, &mut para, &mut pending_margin, ctx, !list_stack.is_empty());
let margin = take_margin(&mut pending_margin);
match kind {
ListMarkKind::ListStart { ordered } => {
let tag = if *ordered { "ol" } else { "ul" };
let _ = write!(out, "<{tag} class=\"list\"{margin}>\n");
list_stack.push(*ordered);
}
ListMarkKind::ListEnd => {
let ordered = list_stack.pop().unwrap_or(false);
let tag = if ordered { "ol" } else { "ul" };
let _ = write!(out, "</{tag}>\n");
}
ListMarkKind::ItemStart => {
let _ = write!(out, "<li{margin}>\n");
}
ListMarkKind::ItemEnd => {
out.push_str("</li>\n");
}
}
}
}
}
flush_para(out, &mut para, &mut pending_margin, ctx, !list_stack.is_empty());
drain_footnotes(out, ctx);
}
const WORD_SPACE_PT: f64 = 3.0;
fn ends_with_hyphen(text: &str) -> bool {
let body = text.strip_suffix("</span>").unwrap_or(text);
matches!(body.chars().next_back(), Some('-' | '\u{2010}'))
}
fn drop_break_hyphen(text: &mut String) {
let closed = text.ends_with("</span>");
let body = if closed {
&text[..text.len() - "</span>".len()]
} else {
&text[..]
};
if !matches!(body.chars().next_back(), Some('-' | '\u{2010}')) {
return;
}
let hyphen_len = body.chars().next_back().map_or(0, char::len_utf8);
let cut = body.len() - hyphen_len;
let tail = text[body.len()..].to_string();
text.truncate(cut);
text.push_str(&tail);
}
fn flush_para(
out: &mut String,
para: &mut Para,
pending_margin: &mut f64,
ctx: &Ctx,
in_list: bool,
) {
if para.open {
inline::close_open_wrappers(&mut para.text, ctx, para.wrapper_base);
let trimmed = para.text.trim();
if super::text::has_visible_content(trimmed) {
let margin = margin_decl(pending_margin);
match para.heading_level {
Some(level) => {
let tag = structure::heading_tag(level);
let style = style_attr(&[&margin]);
let _ = write!(
out,
"<h{tag} class=\"heading\" data-outline-level=\"{level}\"{style}>{trimmed}</h{tag}>\n"
);
}
None => {
let align = match para.alignment() {
Some(a) => format!(" data-align=\"{a}\""),
None => String::new(),
};
let class = if para.mono { "para code" } else { "para" };
let style = style_attr(&[&margin, ¶.indent_decl(in_list)]);
let _ = write!(out, "<p class=\"{class}\"{align}{style}>{trimmed}</p>\n");
}
}
drain_footnotes(out, ctx);
}
}
para.text.clear();
para.open = false;
para.heading_level = None;
para.leading_fil = false;
para.trailing_fil = false;
para.indent = None;
para.mono = false;
para.mixed = false;
ctx.reset_flow();
}
fn drain_footnotes(out: &mut String, ctx: &Ctx) {
let pending: Vec<(usize, String)> = std::mem::take(&mut *ctx.footnotes.borrow_mut());
for (n, body) in pending {
let _ = write!(
out,
"<aside class=\"footnote\" id=\"fn-{n}\" role=\"doc-footnote\">\n{}\
<a class=\"fnback\" href=\"#fnref-{n}\" aria-label=\"back to reference\">\u{21A9}</a>\
</aside>\n",
body.trim(),
);
}
}
fn take_margin(pending_margin: &mut f64) -> String {
style_attr(&[&margin_decl(pending_margin)])
}
fn margin_decl(pending_margin: &mut f64) -> String {
let m = *pending_margin;
*pending_margin = 0.0;
if m > 0.0 {
format!("margin-top:{m}pt;")
} else {
String::new()
}
}
fn style_attr(decls: &[&str]) -> String {
let joined: String = decls.concat();
if joined.is_empty() {
String::new()
} else {
format!(" style=\"{joined}\"")
}
}
const INDENT_MIN_PT: f64 = 1.0;