use std::fmt::Write as _;
use rustyfi_backend::{
AnnotAction, Color, GraphicsElem, HorzStringInfo, InlineMarkKind, MathGlyph, PureHorzBox,
};
use super::Ctx;
use crate::image;
pub(crate) fn emit_inline(out: &mut String, bx: &PureHorzBox, ctx: &Ctx) {
match bx {
PureHorzBox::InlineMark(kind) => match kind {
InlineMarkKind::EmphStart { strong } => {
close_run(out, ctx);
ctx.emph_stack.borrow_mut().push(*strong);
out.push_str(if *strong { "<strong>" } else { "<em>" });
}
InlineMarkKind::EmphEnd => {
close_run(out, ctx);
let strong = ctx.emph_stack.borrow_mut().pop().unwrap_or(false);
out.push_str(if strong { "</strong>" } else { "</em>" });
}
InlineMarkKind::BulletStart => {
*ctx.bullet_suppress.borrow_mut() += 1;
}
InlineMarkKind::BulletEnd => {
let mut n = ctx.bullet_suppress.borrow_mut();
*n = n.saturating_sub(1);
}
},
PureHorzBox::InlineFrameMarker { id, end, .. } => {
let suppressed = *ctx.bullet_suppress.borrow() > 0;
close_run(out, ctx);
if *end {
let close = ctx
.iframe_stack
.borrow_mut()
.pop()
.map_or("</span>", |(_, close)| close);
if !suppressed {
out.push_str(close);
}
} else {
let (open, reopen, close) = wrapper_tags(id, ctx);
if !suppressed {
out.push_str(&open);
}
ctx.iframe_stack.borrow_mut().push((reopen, close));
}
}
_ if *ctx.bullet_suppress.borrow() > 0 => {}
PureHorzBox::InnerString { info, text, .. } => emit_run(out, info, text, ctx),
PureHorzBox::OuterEmpty { natural, .. } => ctx.note_glue(natural.0),
PureHorzBox::OuterFil => ctx.note_glue(0.0),
PureHorzBox::FixedEmpty { width } => {
if width.0 >= HSKIP_MIN_PT {
ctx.resolve_glue(out, None);
close_run(out, ctx);
let _ = write!(
out,
"<span class=\"hskip\" style=\"width:{}pt;\"></span>",
width.0
);
} else {
ctx.note_glue(0.0);
}
}
PureHorzBox::Discretionary { pre_break, .. } => {
if pre_break_carries_text(pre_break) {
out.push_str("­");
} else {
ctx.note_glue(glue_width(pre_break));
}
}
PureHorzBox::Frame { deco, contents, .. } => {
let (open, _, close) = wrapper_tags(deco, ctx);
close_run(out, ctx);
out.push_str(&open);
for (_, cbx) in contents {
emit_inline(out, cbx, ctx);
}
close_run(out, ctx);
out.push_str(close);
}
PureHorzBox::Math {
width,
height,
depth,
glyphs,
rules,
} => emit_math_svg(out, width.0, height.0, depth.0, glyphs, rules, ctx),
PureHorzBox::Graphics {
width,
height,
depth,
elems,
origin_independent: _,
} => emit_graphics_box(out, width.0, height.0, depth.0, elems, ctx),
PureHorzBox::GraphicsOuter { .. } => {
open_opaque(out, ctx);
out.push_str(
"<span class=\"gfx-placeholder\" title=\"unresolved inline-graphics-outer (lang-side callback)\"></span>",
);
}
PureHorzBox::Image {
width,
height,
image,
} => {
open_opaque(out, ctx);
match ctx.images.get(image.0) {
Some(res) if res.pdf.is_some() => {
let _ = write!(
out,
"<span class=\"pdf-image\" style=\"width:{}pt; height:{}pt;\" \
title=\"embedded PDF page (not rasterized)\"></span>",
width.0, height.0,
);
}
Some(_) if ctx.image_sharing(image.0).1 => {
let canon = ctx.image_sharing(image.0).0;
let mut shared = ctx.shared_images.borrow_mut();
if !shared.contains(&canon) {
shared.push(canon);
}
drop(shared);
let _ = write!(
out,
"<span class=\"img shared-img-{canon}\" style=\"width:{}pt; height:{}pt;\" \
aria-hidden=\"true\"></span>",
width.0, height.0,
);
}
Some(res) => {
let _ = write!(
out,
"<img class=\"img\" src=\"{}\" style=\"width:{}pt; height:{}pt;\" alt=\"\">",
image::data_uri(res),
width.0,
height.0,
);
}
None => {}
}
}
PureHorzBox::Tabular(tab) => {
open_opaque(out, ctx);
super::structure::render_table(out, tab, "", ctx)
}
PureHorzBox::Footnote { block } => {
open_opaque(out, ctx);
let n = ctx.footnote_seq.get() + 1;
ctx.footnote_seq.set(n);
let saved = (ctx.pending_glue.take(), ctx.last_char.take());
let parked = std::mem::take(&mut *ctx.footnotes.borrow_mut());
let mut body = String::new();
super::block::walk_vboxes(&mut body, block, ctx);
ctx.pending_glue.set(saved.0);
ctx.last_char.set(saved.1);
let mut queue = ctx.footnotes.borrow_mut();
*queue = parked;
queue.push((n, body));
drop(queue);
let _ = write!(out, "<span class=\"fnref\" id=\"fnref-{n}\"></span>");
}
PureHorzBox::EmbeddedBlock { .. } => {}
PureHorzBox::HookPageBreak { .. } | PureHorzBox::FrameMarker { .. } => {}
}
}
const HSKIP_MIN_PT: f64 = 2.0;
fn emit_run(out: &mut String, info: &HorzStringInfo, text: &str, ctx: &Ctx) {
if text.is_empty() {
return;
}
ctx.resolve_glue(out, text.chars().next());
ctx.last_char.set(text.chars().next_back());
ctx.mono_run.set(ctx.is_monospace(Some(info.font)));
let mut style = String::new();
if !ctx.body.matches(info.font, info.size.0) {
if Some(info.font) != ctx.body.font {
if let Some(stack) = ctx.font_family_for(info.font) {
let _ = write!(style, "font-family:{stack};");
}
} else {
let _ = ctx.font_family_for(info.font);
}
if (info.size.0 - ctx.body.size).abs() >= 0.005 {
let _ = write!(style, "font-size:{:.4}em;", info.size.0 / ctx.body.size);
}
} else {
let _ = ctx.font_family_for(info.font);
}
if info.color != Color::Gray(0.0) {
let _ = write!(style, "color:{};", crate::svg::css_color(info.color));
}
if info.rising.0 != 0.0 {
let _ = write!(style, "vertical-align:{}pt;", info.rising.0);
}
let escaped = crate::escape_html(text);
if style.is_empty() {
close_run(out, ctx);
out.push_str(&escaped);
return;
}
let mut open = ctx.open_run.borrow_mut();
match open.as_deref() {
Some(current) if current == style => {}
_ => {
if open.is_some() {
out.push_str("</span>");
}
let _ = write!(out, "<span class=\"run\" style=\"{style}\">");
*open = Some(style);
}
}
out.push_str(&escaped);
}
pub(crate) fn close_run(out: &mut String, ctx: &Ctx) {
if ctx.open_run.borrow_mut().take().is_some() {
out.push_str("</span>");
}
}
fn wrapper_tags(deco: &rustyfi_backend::DecoId, ctx: &Ctx) -> (String, String, &'static str) {
if let Some(action) = ctx.links.get(deco) {
let href = match action {
AnnotAction::Uri(uri) => crate::escape_html(uri),
AnnotAction::GotoName(name) => format!("#{}", crate::escape_html(name)),
};
let tag = format!("<a class=\"link\" href=\"{href}\">");
(tag.clone(), tag, "</a>")
} else if let Some(name) = ctx.dests.get(deco) {
(
format!(
"<span class=\"iframe\" id=\"{}\">",
crate::escape_html(name)
),
"<span class=\"iframe\">".to_string(),
"</span>",
)
} else {
(
"<span class=\"iframe\">".to_string(),
"<span class=\"iframe\">".to_string(),
"</span>",
)
}
}
pub(crate) fn close_open_wrappers(out: &mut String, ctx: &Ctx, base: usize) {
close_run(out, ctx);
for (_, close) in ctx.iframe_stack.borrow().iter().skip(base).rev() {
out.push_str(close);
}
}
pub(crate) fn reopen_wrappers(out: &mut String, ctx: &Ctx, base: usize) {
for (reopen, _) in ctx.iframe_stack.borrow().iter().skip(base) {
out.push_str(reopen);
}
}
fn pre_break_carries_text(pre_break: &[PureHorzBox]) -> bool {
pre_break.iter().any(|b| match b {
PureHorzBox::InnerString { text, .. } => !text.is_empty(),
_ => false,
})
}
fn glue_width(pre_break: &[PureHorzBox]) -> f64 {
pre_break
.iter()
.map(|b| match b {
PureHorzBox::OuterEmpty { natural, .. } => natural.0,
PureHorzBox::FixedEmpty { width } => width.0,
_ => 0.0,
})
.sum()
}
fn open_opaque(out: &mut String, ctx: &Ctx) {
ctx.resolve_glue(out, None);
close_run(out, ctx);
ctx.last_char.set(None);
}
fn emit_graphics_box(
out: &mut String,
width: f64,
height: f64,
depth: f64,
elems: &[GraphicsElem],
ctx: &Ctx,
) {
if elems.is_empty() {
return;
}
if elems.iter().all(is_pure_text) {
let pushed = collect_overlaid_rules(elems, ctx);
let mut nested = String::new();
emit_text_only(&mut nested, elems, ctx);
ctx.tabular_rules.borrow_mut().truncate(pushed);
out.push_str(&nested);
return;
}
open_opaque(out, ctx);
let total_h = height + depth;
let _ = write!(
out,
"<span class=\"gfx\" style=\"position:relative; display:inline-block; \
width:{width}pt; height:{total_h}pt; vertical-align:{}pt;\">\n",
-depth,
);
let mut nested = String::new();
crate::svg::emit_graphics(
out,
elems,
width,
height,
depth,
0.0,
height,
&mut |_svg, cbx, _x, _y| emit_nested_text(&mut nested, cbx, ctx),
);
out.push_str(&nested);
out.push_str("</span>\n");
}
fn is_pure_text(elem: &GraphicsElem) -> bool {
match elem {
GraphicsElem::Text { .. } => true,
GraphicsElem::Group(inner) | GraphicsElem::Clip(_, inner) => inner.iter().all(is_pure_text),
_ => false,
}
}
fn collect_overlaid_rules(elems: &[GraphicsElem], ctx: &Ctx) -> usize {
let base = ctx.tabular_rules.borrow().len();
walk_tabulars(elems, &mut |tab| {
if !tab.rules.is_empty() {
ctx.tabular_rules.borrow_mut().push((
tab.width.0,
tab.height.0,
tab.rules.clone(),
));
}
});
base
}
fn walk_tabulars(elems: &[GraphicsElem], f: &mut impl FnMut(&rustyfi_backend::TabularBox)) {
for elem in elems {
match elem {
GraphicsElem::Text { contents, .. } => {
for (_, bx) in contents {
if let PureHorzBox::Tabular(tab) = bx {
f(tab);
}
}
}
GraphicsElem::Group(inner) | GraphicsElem::Clip(_, inner) => walk_tabulars(inner, f),
_ => {}
}
}
}
fn emit_text_only(out: &mut String, elems: &[GraphicsElem], ctx: &Ctx) {
for elem in elems {
match elem {
GraphicsElem::Text { contents, .. } => {
for (_, cbx) in contents {
emit_nested_text(out, cbx, ctx);
}
}
GraphicsElem::Group(inner) | GraphicsElem::Clip(_, inner) => {
emit_text_only(out, inner, ctx)
}
_ => {}
}
}
}
fn emit_nested_text(nested: &mut String, bx: &PureHorzBox, ctx: &Ctx) {
emit_inline(nested, bx, ctx);
close_run(nested, ctx);
}
fn emit_math_svg(
out: &mut String,
width: f64,
height: f64,
depth: f64,
glyphs: &[MathGlyph],
rules: &[GraphicsElem],
ctx: &Ctx,
) {
if glyphs.is_empty() && rules.is_empty() {
return;
}
open_opaque(out, ctx);
let total_h = height + depth;
let _ = write!(
out,
"<span class=\"math\" style=\"position:relative; display:inline-block; \
width:{width}pt; height:{total_h}pt; vertical-align:{}pt;\">\n\
<svg class=\"math-glyphs\" style=\"position:absolute; left:0; top:0; overflow:visible;\" \
width=\"{width}pt\" height=\"{total_h}pt\" viewBox=\"0 0 {width} {total_h}\">\n",
-depth,
);
for g in glyphs {
let x = g.dx.0;
let y = height - g.dy.0 - g.info.rising.0;
let mut style = format!("font-size:{}pt;", g.info.size.0);
if let Some(stack) = ctx.font_family_for(g.info.font) {
style.push_str(&format!("font-family:{stack};"));
}
if g.info.color != Color::Gray(0.0) {
style.push_str(&format!("fill:{};", crate::svg::css_color(g.info.color)));
}
let _ = write!(
out,
"<text x=\"{x}\" y=\"{y}\" style=\"{style}\">{}</text>\n",
crate::escape_html(&g.text),
);
}
out.push_str("</svg>\n");
let mut nested = String::new();
if !rules.is_empty() {
crate::svg::emit_graphics(
out,
rules,
width,
height,
depth,
0.0,
height,
&mut |_svg, cbx, _x, _y| emit_nested_text(&mut nested, cbx, ctx),
);
}
out.push_str(&nested);
out.push_str("</span>\n");
}