text-document-io 1.8.0

Import/export for text-document: plain text, Markdown, HTML, LaTeX, DOCX
Documentation
// Generated by Qleany v1.4.8 from feature_use_case.tera
use crate::ExportHtmlDto;
use crate::html_render;
use anyhow::{Result, anyhow};
use common::database::QueryUnitOfWork;
use common::entities::{Block, Document, Frame, List, Root, Table, TableCell};
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()?;

        // Step 1: Get Root and Document
        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,
        )?;

        // Collect all cell frame IDs so we can skip them in the main loop
        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 {
            // Skip cell frames — they're rendered as part of their table
            if cell_frame_ids.contains(frame_id) {
                continue;
            }
            // Skip sub-frames (parent_frame != None) — recursively rendered
            // by their parent's render_frame_html walk; rendering at the
            // top level again would duplicate their content.
            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)?;
            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 })
    }

    /// Render a frame's content as HTML, walking its `child_order` to interleave
    /// blocks and sub-frames (blockquotes). Falls back to sorted blocks when
    /// `child_order` is empty.
    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"))?;

        // Table anchor frame — render the table instead of blocks
        if let Some(table_id) = frame.table {
            return html_render::render_table_html(&uow.store(), table_id);
        }

        // If child_order is populated, use it to interleave blocks and sub-frames
        if !frame.child_order.is_empty() {
            return self.render_frame_by_child_order(uow, &frame, cell_frame_ids);
        }

        // Fallback: render all blocks in document_position order (original behaviour)
        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))
    }

    /// Walk `child_order` entries: positive values are block IDs, negative values
    /// are negated sub-frame IDs.
    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();
        // Accumulate consecutive blocks so we can group list items
        let mut pending_blocks: Vec<Block> = Vec::new();

        for &entry in &frame.child_order {
            if entry > 0 {
                // Positive: block ID
                let block_id = entry as u64;
                if let Some(block) = uow.get_block(&block_id)? {
                    pending_blocks.push(block);
                }
            } else {
                // Negative: negated sub-frame ID
                // First, flush any accumulated blocks
                if !pending_blocks.is_empty() {
                    let html = html_render::render_blocks_html(&uow.store(), &pending_blocks);
                    if !html.is_empty() {
                        parts.push(html);
                    }
                    pending_blocks.clear();
                }

                let sub_frame_id = (-entry) as u64;

                // Skip cell frames
                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) {
                        // Recursively render the blockquote frame content
                        let inner = self.render_frame_html(uow, &sub_frame_id, cell_frame_ids)?;
                        if !inner.is_empty() {
                            parts.push(format!("<blockquote>{}</blockquote>", inner));
                        }
                    } else {
                        // Non-blockquote sub-frame: render normally
                        let inner = self.render_frame_html(uow, &sub_frame_id, cell_frame_ids)?;
                        if !inner.is_empty() {
                            parts.push(inner);
                        }
                    }
                }
            }
        }

        // Flush remaining blocks
        if !pending_blocks.is_empty() {
            let html = html_render::render_blocks_html(&uow.store(), &pending_blocks);
            if !html.is_empty() {
                parts.push(html);
            }
        }

        Ok(parts.join(""))
    }
}