1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
// 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 })
}
}