use crate::ExportHtmlDto;
use crate::html_render;
use anyhow::{Result, anyhow};
use common::database::QueryUnitOfWork;
use common::entities::{Block, Document, Frame, List, Root, SemanticRole, Table, TableCell};
use common::parser_tools::{HtmlExportOptions, HtmlImageMode};
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>,
options: HtmlExportOptions,
}
impl ExportHtmlUseCase {
pub fn new(
uow_factory: Box<dyn ExportHtmlUnitOfWorkFactoryTrait>,
options: HtmlExportOptions,
) -> Self {
ExportHtmlUseCase {
uow_factory,
options,
}
}
fn image_policy(&self) -> html_render::HtmlImagePolicy<'_> {
match self.options.image_mode {
HtmlImageMode::Reference => html_render::HtmlImagePolicy::Reference,
HtmlImageMode::DataUri => html_render::HtmlImagePolicy::DataUri(&self.options.images),
HtmlImageMode::Omit => html_render::HtmlImagePolicy::Omit,
}
}
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 notes = crate::footnotes::Footnotes::build(&uow.store());
let mut body_parts: Vec<String> = Vec::new();
for frame_id in &frame_ids {
if cell_frame_ids.contains(frame_id) {
continue;
}
if notes.is_definition(*frame_id) {
continue;
}
if let Some(f) = uow.get_frame(frame_id)?
&& f.parent_frame.is_some()
{
continue;
}
let frame_html = self.render_frame_html(&*uow, frame_id, &cell_frame_ids, ¬es)?;
if !frame_html.is_empty() {
body_parts.push(frame_html);
}
}
for (number, label, frame_id) in notes.in_print_order() {
let body = self.render_frame_html(&*uow, &frame_id, &cell_frame_ids, ¬es)?;
let id = crate::html_render::escape_html(&label);
body_parts.push(format!(
"<aside epub:type=\"footnote\" role=\"doc-footnote\" id=\"fn-{id}\">\
<a href=\"#fnref-{id}\" role=\"doc-backlink\">{number}</a>. {body}</aside>"
));
}
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>,
notes: &crate::footnotes::Footnotes,
) -> Result<String> {
let image_policy = self.image_policy();
let frame = uow
.get_frame(frame_id)?
.ok_or_else(|| anyhow!("Frame not found"))?;
if let Some(table_id) = frame.table {
return html_render::render_table_html(&uow.store(), table_id, image_policy, notes);
}
if !frame.child_order.is_empty() {
return self.render_frame_by_child_order(uow, &frame, cell_frame_ids, notes);
}
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);
Ok(html_render::render_blocks_html(
&uow.store(),
&blocks,
image_policy,
notes,
))
}
fn render_frame_by_child_order(
&self,
uow: &dyn ExportHtmlUnitOfWorkTrait,
frame: &Frame,
cell_frame_ids: &HashSet<EntityId>,
notes: &crate::footnotes::Footnotes,
) -> Result<String> {
let image_policy = self.image_policy();
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 = html_render::render_blocks_html(
&uow.store(),
&pending_blocks,
image_policy,
notes,
);
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, notes)?;
if !inner.is_empty() {
let semantics = match &sf.fmt_semantic_role {
Some(SemanticRole::Epigraph) => {
r#" epub:type="epigraph" role="doc-epigraph""#
}
None => "",
};
parts.push(format!("<blockquote{}>{}</blockquote>", semantics, inner));
}
} else {
let inner =
self.render_frame_html(uow, &sub_frame_id, cell_frame_ids, notes)?;
if !inner.is_empty() {
parts.push(inner);
}
}
}
}
}
if !pending_blocks.is_empty() {
let html =
html_render::render_blocks_html(&uow.store(), &pending_blocks, image_policy, notes);
if !html.is_empty() {
parts.push(html);
}
}
Ok(parts.join(""))
}
}