use crate::ExportHtmlDto;
use anyhow::{Result, anyhow};
use common::database::QueryUnitOfWork;
use common::database::rope_helpers::block_content_via_store;
use common::entities::{
Alignment, Block, Document, Frame, List, ListStyle, Root, Table, TableCell, TextDirection,
};
use common::format_runs::InlineContent;
use common::types::{EntityId, ROOT_ENTITY_ID};
use std::collections::HashSet;
pub trait ExportHtmlUnitOfWorkFactoryTrait: Send + Sync {
fn create(&self) -> Box<dyn ExportHtmlUnitOfWorkTrait>;
}
#[macros::uow_action(entity = "Root", action = "GetRO")]
#[macros::uow_action(entity = "Root", action = "GetRelationshipRO")]
#[macros::uow_action(entity = "Document", action = "GetRO")]
#[macros::uow_action(entity = "Document", action = "GetRelationshipRO")]
#[macros::uow_action(entity = "Frame", action = "GetRO")]
#[macros::uow_action(entity = "Frame", action = "GetRelationshipRO")]
#[macros::uow_action(entity = "Block", action = "GetRO")]
#[macros::uow_action(entity = "Block", action = "GetMultiRO")]
#[macros::uow_action(entity = "Block", action = "GetRelationshipRO")]
#[macros::uow_action(entity = "List", action = "GetRO")]
#[macros::uow_action(entity = "Table", action = "GetRO")]
#[macros::uow_action(entity = "Table", action = "GetRelationshipRO")]
#[macros::uow_action(entity = "TableCell", action = "GetMultiRO")]
pub trait ExportHtmlUnitOfWorkTrait: QueryUnitOfWork {}
pub struct ExportHtmlUseCase {
uow_factory: Box<dyn ExportHtmlUnitOfWorkFactoryTrait>,
}
impl ExportHtmlUseCase {
pub fn new(uow_factory: Box<dyn ExportHtmlUnitOfWorkFactoryTrait>) -> Self {
ExportHtmlUseCase { uow_factory }
}
pub fn execute(&mut self) -> Result<ExportHtmlDto> {
let uow = self.uow_factory.create();
uow.begin_transaction()?;
let root = uow
.get_root(&ROOT_ENTITY_ID)?
.ok_or_else(|| anyhow!("Root entity not found"))?;
let doc_ids = uow.get_root_relationship(
&root.id,
&common::direct_access::root::RootRelationshipField::Document,
)?;
let doc_id = *doc_ids
.first()
.ok_or_else(|| anyhow!("Root has no associated Document"))?;
let frame_ids = uow.get_document_relationship(
&doc_id,
&common::direct_access::document::DocumentRelationshipField::Frames,
)?;
let table_ids = uow.get_document_relationship(
&doc_id,
&common::direct_access::document::DocumentRelationshipField::Tables,
)?;
let mut cell_frame_ids: HashSet<EntityId> = HashSet::new();
for tid in &table_ids {
let cell_ids = uow.get_table_relationship(
tid,
&common::direct_access::table::TableRelationshipField::Cells,
)?;
let cells_opt = uow.get_table_cell_multi(&cell_ids)?;
for cell in cells_opt.into_iter().flatten() {
if let Some(cf_id) = cell.cell_frame {
cell_frame_ids.insert(cf_id);
}
}
}
let mut body_parts: Vec<String> = Vec::new();
for frame_id in &frame_ids {
if cell_frame_ids.contains(frame_id) {
continue;
}
let frame_html = self.render_frame_html(&*uow, frame_id, &cell_frame_ids)?;
if !frame_html.is_empty() {
body_parts.push(frame_html);
}
}
uow.end_transaction()?;
let html_text = format!(
"<html><head><meta charset=\"utf-8\"></head><body>{}</body></html>",
body_parts.join("")
);
Ok(ExportHtmlDto { html_text })
}
fn render_frame_html(
&self,
uow: &dyn ExportHtmlUnitOfWorkTrait,
frame_id: &EntityId,
cell_frame_ids: &HashSet<EntityId>,
) -> Result<String> {
let frame = uow
.get_frame(frame_id)?
.ok_or_else(|| anyhow!("Frame not found"))?;
if let Some(table_id) = frame.table {
return self.render_table_html(uow, &table_id);
}
if !frame.child_order.is_empty() {
return self.render_frame_by_child_order(uow, &frame, cell_frame_ids);
}
let block_ids = uow.get_frame_relationship(
frame_id,
&common::direct_access::frame::FrameRelationshipField::Blocks,
)?;
if block_ids.is_empty() {
return Ok(String::new());
}
let blocks_opt = uow.get_block_multi(&block_ids)?;
let mut blocks: Vec<Block> = blocks_opt.into_iter().flatten().collect();
blocks.sort_by_key(|b| b.document_position);
self.render_blocks_html(uow, &blocks)
}
fn render_frame_by_child_order(
&self,
uow: &dyn ExportHtmlUnitOfWorkTrait,
frame: &Frame,
cell_frame_ids: &HashSet<EntityId>,
) -> Result<String> {
let mut parts: Vec<String> = Vec::new();
let mut pending_blocks: Vec<Block> = Vec::new();
for &entry in &frame.child_order {
if entry > 0 {
let block_id = entry as u64;
if let Some(block) = uow.get_block(&block_id)? {
pending_blocks.push(block);
}
} else {
if !pending_blocks.is_empty() {
let html = self.render_blocks_html(uow, &pending_blocks)?;
if !html.is_empty() {
parts.push(html);
}
pending_blocks.clear();
}
let sub_frame_id = (-entry) as u64;
if cell_frame_ids.contains(&sub_frame_id) {
continue;
}
let sub_frame = uow.get_frame(&sub_frame_id)?;
if let Some(ref sf) = sub_frame {
if sf.fmt_is_blockquote == Some(true) {
let inner = self.render_frame_html(uow, &sub_frame_id, cell_frame_ids)?;
if !inner.is_empty() {
parts.push(format!("<blockquote>{}</blockquote>", inner));
}
} else {
let inner = self.render_frame_html(uow, &sub_frame_id, cell_frame_ids)?;
if !inner.is_empty() {
parts.push(inner);
}
}
}
}
}
if !pending_blocks.is_empty() {
let html = self.render_blocks_html(uow, &pending_blocks)?;
if !html.is_empty() {
parts.push(html);
}
}
Ok(parts.join(""))
}
fn render_blocks_html(
&self,
uow: &dyn ExportHtmlUnitOfWorkTrait,
blocks: &[Block],
) -> Result<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 block_text = block_content_via_store(block, &uow.store());
let elements = common::format_runs_query::inline_segments_for_block(
&uow.store(),
block.id,
&block_text,
);
let mut raw_text = String::new();
for elem in &elements {
match &elem.content {
InlineContent::Text(t) => raw_text.push_str(t),
InlineContent::Image { .. } | InlineContent::Empty => {}
}
}
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_ids = uow.get_block_relationship(
&block.id,
&common::direct_access::block::BlockRelationshipField::List,
)?;
let list = if let Some(list_id) = list_ids.first() {
uow.get_list(list_id)?
} else {
None
};
if let Some(ref 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_list_ids = uow.get_block_relationship(
&b.id,
&common::direct_access::block::BlockRelationshipField::List,
)?;
let b_list = if let Some(lid) = b_list_ids.first() {
uow.get_list(lid)?
} else {
None
};
if b_list.is_some() {
let inline_html = self.render_inline_html(uow, b)?;
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 = self.render_inline_html(uow, block)?;
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_direction == Some(TextDirection::RightToLeft) {
styles.push("direction: rtl".into());
}
if let Some(ref c) = block.fmt_background_color {
styles.push(format!("background-color: {}", c));
}
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;
}
}
Ok(parts.join(""))
}
fn render_table_html(
&self,
uow: &dyn ExportHtmlUnitOfWorkTrait,
table_id: &EntityId,
) -> Result<String> {
let table = uow
.get_table(table_id)?
.ok_or_else(|| anyhow!("Table not found"))?;
let cell_ids = uow.get_table_relationship(
table_id,
&common::direct_access::table::TableRelationshipField::Cells,
)?;
let cells_opt = uow.get_table_cell_multi(&cell_ids)?;
let mut cells: Vec<TableCell> = cells_opt.into_iter().flatten().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 = uow.get_frame_relationship(
&cf_id,
&common::direct_access::frame::FrameRelationshipField::Blocks,
)?;
let blocks_opt = uow.get_block_multi(&block_ids)?;
let blocks: Vec<Block> = blocks_opt.into_iter().flatten().collect();
let mut cell_parts: Vec<String> = Vec::new();
for block in &blocks {
let inline_html = self.render_inline_html(uow, block)?;
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)
}
fn render_inline_html(
&self,
uow: &dyn ExportHtmlUnitOfWorkTrait,
block: &Block,
) -> Result<String> {
let block_text = block_content_via_store(block, &uow.store());
let elements = common::format_runs_query::inline_segments_for_block(
&uow.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::Image {
name,
width,
height,
..
} => {
format!(
"<img src=\"{}\" width=\"{}\" height=\"{}\" />",
escape_html(name),
width,
height
)
}
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);
}
Ok(html)
}
}
fn escape_html(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
.replace('\r', " ")
}