use anyhow::{Result, anyhow};
use common::database::Store;
use common::database::rope_helpers::block_content_via_store;
use common::entities::{Alignment, Block, ListStyle, TableCell, TextDirection};
use common::format_runs::InlineContent;
use common::format_runs_query::inline_segments_for_block;
use common::parser_tools::image_options::{ExportImages, base64_encode};
use common::types::EntityId;
#[derive(Debug, Clone, Copy, Default)]
pub enum HtmlImagePolicy<'a> {
#[default]
Reference,
Rewrite(&'a std::collections::BTreeMap<String, String>),
DataUri(&'a ExportImages),
Omit,
}
impl HtmlImagePolicy<'_> {
fn resolve(&self, name: &str) -> Option<String> {
match self {
Self::Reference => Some(name.to_string()),
Self::Rewrite(map) => Some(map.get(name).cloned().unwrap_or_else(|| name.to_string())),
Self::DataUri(images) => images.get(name).map(|img| {
format!(
"data:{};base64,{}",
img.mime_type,
base64_encode(&img.bytes)
)
}),
Self::Omit => None,
}
}
}
pub fn render_blocks_html(
store: &Store,
blocks: &[Block],
images: HtmlImagePolicy<'_>,
notes: &crate::footnotes::Footnotes,
) -> String {
let mut parts: Vec<String> = Vec::new();
let mut i = 0;
while i < blocks.len() {
let block = &blocks[i];
if block.fmt_is_code_block == Some(true) {
let raw_text = block_plain_text(store, block);
let escaped = escape_html(&raw_text);
let code_open = if let Some(ref lang) = block.fmt_code_language {
if !lang.is_empty() {
format!("<code class=\"language-{}\">", escape_html(lang))
} else {
"<code>".to_string()
}
} else {
"<code>".to_string()
};
parts.push(format!("<pre>{}{}</code></pre>", code_open, escaped));
i += 1;
continue;
}
let list = block
.list
.and_then(|list_id| store.lists.read().get(&list_id).cloned());
if let Some(list_entity) = list {
let is_ordered = matches!(
list_entity.style,
ListStyle::Decimal
| ListStyle::LowerAlpha
| ListStyle::UpperAlpha
| ListStyle::LowerRoman
| ListStyle::UpperRoman
);
let list_tag = if is_ordered { "ol" } else { "ul" };
let mut list_items = Vec::new();
while i < blocks.len() {
let b = &blocks[i];
let b_is_listed = b
.list
.is_some_and(|list_id| store.lists.read().contains_key(&list_id));
if b_is_listed {
let inline_html = render_inline_html(store, b, images, notes);
list_items.push(format!("<li>{}</li>", inline_html));
i += 1;
} else {
break;
}
}
parts.push(format!(
"<{}>{}</{}>",
list_tag,
list_items.join(""),
list_tag
));
} else {
let inline_html = render_inline_html(store, block, images, notes);
let mut styles: Vec<String> = Vec::new();
match block.fmt_alignment {
Some(Alignment::Left) => styles.push("text-align: left".into()),
Some(Alignment::Right) => styles.push("text-align: right".into()),
Some(Alignment::Center) => styles.push("text-align: center".into()),
Some(Alignment::Justify) => styles.push("text-align: justify".into()),
None => {}
}
if let Some(lh) = block.fmt_line_height {
styles.push(format!("line-height: {}", lh as f64 / 1000.0));
}
if block.fmt_non_breakable_lines == Some(true) {
styles.push("white-space: pre".into());
}
if block.fmt_page_break_before == Some(true) {
styles.push("break-before: page".into());
styles.push("page-break-before: always".into());
}
if block.fmt_direction == Some(TextDirection::RightToLeft) {
styles.push("direction: rtl".into());
}
if let Some(ref c) = block.fmt_background_color {
styles.push(format!("background-color: {}", c));
}
if let Some(tm) = block.fmt_top_margin {
styles.push(format!("margin-top: {tm}px"));
}
if let Some(ti) = block.fmt_text_indent {
styles.push(format!("text-indent: {ti}px"));
}
let style_attr = if styles.is_empty() {
String::new()
} else {
format!(" style=\"{}\"", styles.join("; "))
};
if let Some(level) = block.fmt_heading_level {
let level = level.clamp(1, 6);
parts.push(format!(
"<h{}{}>{}</h{}>",
level, style_attr, inline_html, level
));
} else {
parts.push(format!("<p{}>{}</p>", style_attr, inline_html));
}
i += 1;
}
}
parts.join("")
}
pub fn render_inline_html(
store: &Store,
block: &Block,
images: HtmlImagePolicy<'_>,
notes: &crate::footnotes::Footnotes,
) -> String {
let block_text = block_content_via_store(block, store);
let elements = inline_segments_for_block(store, block.id, &block_text);
let mut html = String::new();
for elem in &elements {
let text = match &elem.content {
InlineContent::Text(t) => escape_html(t),
InlineContent::FootnoteRef { label } => {
let marker = escape_html(¬es.marker(label));
if notes.is_nested_reference(label) {
html.push_str(&format!("<sup>{marker}</sup>"));
} else {
let id = escape_html(label);
html.push_str(&format!(
"<a epub:type=\"noteref\" role=\"doc-noteref\" href=\"#fn-{id}\" \
id=\"fnref-{id}\"><sup>{marker}</sup></a>"
));
}
continue;
}
InlineContent::Image {
name,
alt,
width,
height,
..
} => match images.resolve(name) {
Some(src) => {
let mut tag = format!(
"<img src=\"{}\" alt=\"{}\"",
escape_html(&src),
escape_html(alt)
);
if *width > 0 {
tag.push_str(&format!(" width=\"{width}\""));
}
if *height > 0 {
tag.push_str(&format!(" height=\"{height}\""));
}
tag.push_str(" />");
tag
}
None => escape_html(alt),
},
InlineContent::Empty => String::new(),
};
if text.is_empty() {
continue;
}
if text.starts_with("<img ") {
html.push_str(&text);
continue;
}
let mut formatted = text;
if elem.fmt_font_family.as_deref() == Some("monospace") {
formatted = format!("<code>{}</code>", formatted);
}
if elem.fmt_font_bold == Some(true) {
formatted = format!("<strong>{}</strong>", formatted);
}
if elem.fmt_font_italic == Some(true) {
formatted = format!("<em>{}</em>", formatted);
}
if elem.fmt_font_underline == Some(true) {
formatted = format!("<u>{}</u>", formatted);
}
if elem.fmt_font_strikeout == Some(true) {
formatted = format!("<s>{}</s>", formatted);
}
if let Some(ref href) = elem.fmt_anchor_href {
formatted = format!("<a href=\"{}\">{}</a>", escape_html(href), formatted);
}
html.push_str(&formatted);
}
html
}
pub fn block_plain_text(store: &Store, block: &Block) -> String {
let block_text = block_content_via_store(block, store);
let elements = inline_segments_for_block(store, block.id, &block_text);
let mut raw_text = String::new();
for elem in &elements {
if let InlineContent::Text(t) = &elem.content {
raw_text.push_str(t);
}
}
raw_text
}
pub fn render_table_html(
store: &Store,
table_id: EntityId,
images: HtmlImagePolicy<'_>,
notes: &crate::footnotes::Footnotes,
) -> Result<String> {
let table = store
.tables
.read()
.get(&table_id)
.cloned()
.ok_or_else(|| anyhow!("Table not found"))?;
let mut cells: Vec<TableCell> = table
.cells
.iter()
.filter_map(|cid| store.table_cells.read().get(cid).cloned())
.collect();
cells.sort_by(|a, b| a.row.cmp(&b.row).then(a.column.cmp(&b.column)));
let rows = table.rows as usize;
let cols = table.columns as usize;
let mut covered = vec![vec![false; cols]; rows];
let mut html = String::from("<table");
if let Some(border) = table.fmt_border {
html.push_str(&format!(" border=\"{}\"", border));
}
html.push('>');
for r in 0..rows {
html.push_str("<tr>");
for c in 0..cols {
if covered[r][c] {
continue;
}
let cell = cells
.iter()
.find(|cell| cell.row == r as i64 && cell.column == c as i64);
if let Some(cell) = cell {
let mut td = String::from("<td");
if cell.row_span > 1 {
td.push_str(&format!(" rowspan=\"{}\"", cell.row_span));
}
if cell.column_span > 1 {
td.push_str(&format!(" colspan=\"{}\"", cell.column_span));
}
td.push('>');
if let Some(cf_id) = cell.cell_frame {
let block_ids = store
.frames
.read()
.get(&cf_id)
.map(|f| f.blocks.clone())
.unwrap_or_default();
let blocks: Vec<Block> = block_ids
.iter()
.filter_map(|bid| store.blocks.read().get(bid).cloned())
.collect();
let mut cell_parts: Vec<String> = Vec::new();
for block in &blocks {
let inline_html = render_inline_html(store, block, images, notes);
if !inline_html.is_empty() {
cell_parts.push(inline_html);
}
}
td.push_str(&cell_parts.join("<br/>"));
}
td.push_str("</td>");
html.push_str(&td);
for sr in 0..cell.row_span as usize {
for sc in 0..cell.column_span as usize {
if sr == 0 && sc == 0 {
continue;
}
if r + sr < rows && c + sc < cols {
covered[r + sr][c + sc] = true;
}
}
}
} else {
html.push_str("<td></td>");
}
}
html.push_str("</tr>");
}
html.push_str("</table>");
Ok(html)
}
pub fn escape_html(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
.replace('\r', " ")
}