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::ExportPlainTextDto;
use anyhow::{Result, anyhow};
use common::database::QueryUnitOfWork;
use common::database::rope_helpers::{block_content_via_store, rope_flat_text_if_simple};
use common::entities::{Block, Document, Frame, Root};
use common::types::{EntityId, ROOT_ENTITY_ID};

pub trait ExportPlainTextUnitOfWorkFactoryTrait: Send + Sync {
    fn create(&self) -> Box<dyn ExportPlainTextUnitOfWorkTrait>;
}

#[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")]
pub trait ExportPlainTextUnitOfWorkTrait: QueryUnitOfWork {}

pub struct ExportPlainTextUseCase {
    uow_factory: Box<dyn ExportPlainTextUnitOfWorkFactoryTrait>,
}

impl ExportPlainTextUseCase {
    pub fn new(uow_factory: Box<dyn ExportPlainTextUnitOfWorkFactoryTrait>) -> Self {
        ExportPlainTextUseCase { uow_factory }
    }

    pub fn execute(&mut self) -> Result<ExportPlainTextDto> {
        let uow = self.uow_factory.create();
        uow.begin_transaction()?;

        // Step 1: Get Root (id=1) and its Document via relationship
        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"))?;

        // Step 2: Get all Frame IDs from Document.Frames relationship
        let frame_ids = uow.get_document_relationship(
            &doc_id,
            &common::direct_access::document::DocumentRelationshipField::Frames,
        )?;

        let store = uow.store();

        // Fast path: flat single-frame document with no tables has its
        // entire plain-text representation already laid out in rope
        // byte order. One allocation replaces the per-block walk.
        if let Some(plain_text) = rope_flat_text_if_simple(&store, frame_ids.len()) {
            uow.end_transaction()?;
            return Ok(ExportPlainTextDto { plain_text });
        }

        // Slow path: tables or multi-frame documents. Cell content and a blockquote's prose
        // live in their own Frames, so the whole document's blocks have to be gathered and
        // put back into reading order.
        //
        // Pool EVERY frame's blocks first, then sort ONCE, GLOBALLY, by `document_position`.
        //
        // This used to sort each frame's blocks against only *their own frame's* siblings and
        // then concatenate the frames in the order `Document.Frames` hands them back — which
        // is frame-CREATION order (the root frame is created up front; a blockquote's frame
        // when the quote is opened, a table's cell frames when the table is reached). That
        // silently assumed creation order equals document order, and it is false the instant a
        // sub-frame's content precedes sibling content in the parent's flow. `"> a0\n\na"`
        // exported as `"a\na0"`: every blockquote's prose was hoisted to the END of the
        // document. The CLI's `cat`/`convert` wrote that straight to stdout.
        //
        // A global sort is correct because `document_position` is a single counter over the
        // WHOLE parse — declared once, outside the frame-stack machinery, and advanced for
        // every block regardless of which frame it lands in (`import_djot_uc`). It is a
        // globally comparable reading-order key ACROSS frame boundaries, not a per-frame one.
        // This is exactly what `find_all` already does to build the text it searches, and why
        // search and this export used to disagree about where a blockquote sat.
        let mut all_block_ids: Vec<EntityId> = Vec::new();
        for frame_id in &frame_ids {
            all_block_ids.extend(uow.get_frame_relationship(
                frame_id,
                &common::direct_access::frame::FrameRelationshipField::Blocks,
            )?);
        }

        let mut blocks: Vec<Block> = uow
            .get_block_multi(&all_block_ids)?
            .into_iter()
            .flatten()
            .collect();
        blocks.sort_by_key(|b| b.document_position);

        let plain_text = blocks
            .iter()
            .map(|block| block_content_via_store(block, &store))
            .collect::<Vec<String>>()
            .join("\n");

        uow.end_transaction()?;

        Ok(ExportPlainTextDto { plain_text })
    }
}