use crate::ExportLatexDto;
use crate::ExportLatexResultDto;
use anyhow::{Result, anyhow};
use common::database::QueryUnitOfWork;
use common::entities::{
Block, Document, Frame, InlineContent, InlineElement, List, ListStyle, Root, Table, TableCell,
TextDirection,
};
use common::types::{EntityId, ROOT_ENTITY_ID};
use std::collections::HashMap;
use std::collections::HashSet;
pub trait ExportLatexUnitOfWorkFactoryTrait: Send + Sync {
fn create(&self) -> Box<dyn ExportLatexUnitOfWorkTrait>;
}
#[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 = "GetMultiRO")]
#[macros::uow_action(entity = "Block", action = "GetRelationshipRO")]
#[macros::uow_action(entity = "InlineElement", action = "GetMultiRO")]
#[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 ExportLatexUnitOfWorkTrait: QueryUnitOfWork {}
pub struct ExportLatexUseCase {
uow_factory: Box<dyn ExportLatexUnitOfWorkFactoryTrait>,
}
impl ExportLatexUseCase {
pub fn new(uow_factory: Box<dyn ExportLatexUnitOfWorkFactoryTrait>) -> Self {
ExportLatexUseCase { uow_factory }
}
pub fn execute(&mut self, dto: &ExportLatexDto) -> Result<ExportLatexResultDto> {
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_latex = self.render_frame_latex(&*uow, frame_id, &cell_frame_ids)?;
if !frame_latex.is_empty() {
body_parts.push(frame_latex);
}
}
uow.end_transaction()?;
let body = body_parts.join("\n\n");
let latex_text = if dto.include_preamble {
let doc_class = if dto.document_class.is_empty() {
"article"
} else {
&dto.document_class
};
format!(
"\\documentclass{{{}}}\n\\usepackage{{hyperref}}\n\\usepackage{{ulem}}\n\\usepackage{{graphicx}}\n\\usepackage{{setspace}}\n\\usepackage{{xcolor}}\n\\begin{{document}}\n\n{}\n\n\\end{{document}}",
doc_class, body
)
} else {
body
};
Ok(ExportLatexResultDto { latex_text })
}
fn render_frame_latex(
&self,
uow: &dyn ExportLatexUnitOfWorkTrait,
frame_id: &EntityId,
cell_frame_ids: &HashSet<EntityId>,
) -> Result<String> {
let frame = uow.get_frame(frame_id)?;
let frame = match frame {
Some(f) => f,
None => return Ok(String::new()),
};
if let Some(table_id) = frame.table {
return self.render_table_latex(uow, &table_id);
}
let block_ids = uow.get_frame_relationship(
frame_id,
&common::direct_access::frame::FrameRelationshipField::Blocks,
)?;
let blocks_opt = uow.get_block_multi(&block_ids)?;
let block_map: HashMap<EntityId, Block> = blocks_opt
.into_iter()
.flatten()
.map(|b| (b.id, b))
.collect();
let mut parts: Vec<String> = Vec::new();
if frame.child_order.is_empty() {
let mut blocks: Vec<&Block> = block_map.values().collect();
blocks.sort_by_key(|b| b.document_position);
self.render_blocks_latex(uow, &blocks, &mut parts)?;
} else {
let mut pending_blocks: Vec<&Block> = Vec::new();
for &order_val in &frame.child_order {
if order_val > 0 {
let block_id: EntityId = order_val as u64;
if let Some(block) = block_map.get(&block_id) {
pending_blocks.push(block);
}
} else {
if !pending_blocks.is_empty() {
self.render_blocks_latex(uow, &pending_blocks, &mut parts)?;
pending_blocks.clear();
}
let sub_frame_id: EntityId = (-order_val) as u64;
if cell_frame_ids.contains(&sub_frame_id) {
continue;
}
let sub_latex = self.render_frame_latex(uow, &sub_frame_id, cell_frame_ids)?;
if !sub_latex.is_empty() {
parts.push(sub_latex);
}
}
}
if !pending_blocks.is_empty() {
self.render_blocks_latex(uow, &pending_blocks, &mut parts)?;
}
}
if parts.is_empty() {
return Ok(String::new());
}
let content = parts.join("\n\n");
if frame.fmt_is_blockquote == Some(true) {
Ok(format!("\\begin{{quote}}\n{}\n\\end{{quote}}", content))
} else {
Ok(content)
}
}
fn render_blocks_latex(
&self,
uow: &dyn ExportLatexUnitOfWorkTrait,
blocks: &[&Block],
parts: &mut Vec<String>,
) -> Result<()> {
let mut i = 0;
while i < blocks.len() {
let block = blocks[i];
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 env = if is_ordered { "enumerate" } else { "itemize" };
let mut 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_latex = self.render_inline_latex(uow, b)?;
items.push(format!("\\item {}", inline_latex));
i += 1;
} else {
break;
}
}
parts.push(format!(
"\\begin{{{}}}\n{}\n\\end{{{}}}",
env,
items.join("\n"),
env
));
} else if block.fmt_is_code_block == Some(true) {
let raw_text = self.render_raw_text(uow, block)?;
parts.push(format!(
"\\begin{{verbatim}}\n{}\n\\end{{verbatim}}",
raw_text
));
i += 1;
} else {
let inline_latex = self.render_inline_latex(uow, block)?;
let mut content = if let Some(level) = block.fmt_heading_level {
let cmd = match level {
1 => "section",
2 => "subsection",
3 => "subsubsection",
_ => "paragraph",
};
format!("\\{}{{{}}}", cmd, inline_latex)
} else {
inline_latex
};
if let Some(lh) = block.fmt_line_height {
let spacing = lh as f64 / 1000.0;
content = format!("{{\\setstretch{{{:.2}}}{}}}", spacing, content);
}
if block.fmt_direction == Some(TextDirection::RightToLeft) {
content = format!("\\RL{{{}}}", content);
}
if let Some(ref c) = block.fmt_background_color {
content = format!(
"\\colorbox{{{}}}{{\\parbox{{\\linewidth}}{{{}}}}}",
c, content
);
}
if block.fmt_non_breakable_lines == Some(true) {
content = format!("\\mbox{{{}}}", content);
}
parts.push(content);
i += 1;
}
}
Ok(())
}
fn render_raw_text(
&self,
uow: &dyn ExportLatexUnitOfWorkTrait,
block: &Block,
) -> Result<String> {
let element_ids = uow.get_block_relationship(
&block.id,
&common::direct_access::block::BlockRelationshipField::Elements,
)?;
let elements_opt = uow.get_inline_element_multi(&element_ids)?;
let elements: Vec<InlineElement> = elements_opt.into_iter().flatten().collect();
let mut text = String::new();
for elem in &elements {
match &elem.content {
InlineContent::Text(t) => text.push_str(t),
InlineContent::Image { name, .. } => text.push_str(name),
InlineContent::Empty => {}
}
}
Ok(text)
}
fn render_inline_latex(
&self,
uow: &dyn ExportLatexUnitOfWorkTrait,
block: &Block,
) -> Result<String> {
let element_ids = uow.get_block_relationship(
&block.id,
&common::direct_access::block::BlockRelationshipField::Elements,
)?;
let elements_opt = uow.get_inline_element_multi(&element_ids)?;
let elements: Vec<InlineElement> = elements_opt.into_iter().flatten().collect();
let mut latex = String::new();
for elem in &elements {
let text = match &elem.content {
InlineContent::Text(t) => escape_latex(t),
InlineContent::Image { name, .. } => {
format!("\\includegraphics{{{}}}", escape_latex(name))
}
InlineContent::Empty => String::new(),
};
if text.is_empty() {
continue;
}
if text.starts_with("\\includegraphics") {
latex.push_str(&text);
continue;
}
let mut formatted = text;
if elem.fmt_font_family.as_deref() == Some("monospace") {
formatted = format!("\\texttt{{{}}}", formatted);
}
if elem.fmt_font_bold == Some(true) {
formatted = format!("\\textbf{{{}}}", formatted);
}
if elem.fmt_font_italic == Some(true) {
formatted = format!("\\textit{{{}}}", formatted);
}
if elem.fmt_font_underline == Some(true) {
formatted = format!("\\underline{{{}}}", formatted);
}
if elem.fmt_font_strikeout == Some(true) {
formatted = format!("\\sout{{{}}}", formatted);
}
if let Some(ref href) = elem.fmt_anchor_href {
formatted = format!("\\href{{{}}}{{{}}}", escape_latex(href), formatted);
}
latex.push_str(&formatted);
}
Ok(latex)
}
fn render_table_latex(
&self,
uow: &dyn ExportLatexUnitOfWorkTrait,
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 col_spec = format!("|{}|", vec!["l"; cols].join("|"));
let mut latex = format!("\\begin{{tabular}}{{{}}}\n\\hline", col_spec);
for r in 0..rows {
let mut row_parts: Vec<String> = Vec::new();
let mut c = 0;
while c < cols {
if covered[r][c] {
c += 1;
continue;
}
let cell = cells
.iter()
.find(|cell| cell.row == r as i64 && cell.column == c as i64);
if let Some(cell) = cell {
let content = 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_latex = self.render_inline_latex(uow, block)?;
if !inline_latex.is_empty() {
cell_parts.push(inline_latex);
}
}
cell_parts.join(" ")
} else {
String::new()
};
let wrapped = if cell.row_span > 1 && cell.column_span > 1 {
let cs = cell.column_span as usize;
let rs = cell.row_span as usize;
for sc in 1..cs {
if c + sc < cols {
covered[r][c + sc] = true;
}
}
format!(
"\\multicolumn{{{}}}{{|l|}}{{\\multirow{{{}}}{{*}}{{{}}}}}",
cs, rs, content
)
} else if cell.column_span > 1 {
let cs = cell.column_span as usize;
for sc in 1..cs {
if c + sc < cols {
covered[r][c + sc] = true;
}
}
format!("\\multicolumn{{{}}}{{|l|}}{{{}}}", cs, content)
} else if cell.row_span > 1 {
let rs = cell.row_span as usize;
format!("\\multirow{{{}}}{{*}}{{{}}}", rs, content)
} else {
content
};
row_parts.push(wrapped);
for sr in 1..cell.row_span as usize {
for sc in 0..cell.column_span as usize {
if r + sr < rows && c + sc < cols {
covered[r + sr][c + sc] = true;
}
}
}
c += cell.column_span as usize;
} else {
row_parts.push(String::new());
c += 1;
}
}
latex.push_str(&format!("\n{} \\\\", row_parts.join(" & ")));
latex.push_str("\n\\hline");
}
latex.push_str("\n\\end{tabular}");
Ok(latex)
}
}
fn escape_latex(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'\\' => result.push_str("\\textbackslash{}"),
'&' => result.push_str("\\&"),
'%' => result.push_str("\\%"),
'$' => result.push_str("\\$"),
'#' => result.push_str("\\#"),
'_' => result.push_str("\\_"),
'{' => result.push_str("\\{"),
'}' => result.push_str("\\}"),
'~' => result.push_str("\\textasciitilde{}"),
'^' => result.push_str("\\textasciicircum{}"),
_ => result.push(ch),
}
}
result
}