use crate::ExportPlainTextDto;
use anyhow::{Result, anyhow};
use common::database::QueryUnitOfWork;
use common::database::Store;
use common::database::rope_helpers::{block_content_via_store, rope_flat_text_if_simple};
use common::entities::{Block, Document, Frame, Root};
use common::format_runs::InlineContent;
use common::format_runs_query::inline_segments_for_block;
use common::parser_tools::{FORM_FEED, PlainTextExportOptions};
use common::types::{EntityId, ROOT_ENTITY_ID};
use std::collections::HashMap;
const QUOTE_INDENT: &str = " ";
fn blockquote_depth(frame_id: EntityId, frames: &HashMap<EntityId, Frame>) -> usize {
let mut depth = 0;
let mut current = Some(frame_id);
let mut guard = frames.len() + 1;
while let Some(id) = current {
if guard == 0 {
break;
}
guard -= 1;
let Some(frame) = frames.get(&id) else { break };
if frame.fmt_is_blockquote == Some(true) {
depth += 1;
}
current = frame.parent_frame;
}
depth
}
fn indent_quoted(text: &str, depth: usize) -> String {
let prefix = QUOTE_INDENT.repeat(depth);
text.split('\n')
.map(|line| {
if line.trim().is_empty() {
line.to_string()
} else {
format!("{prefix}{line}")
}
})
.collect::<Vec<String>>()
.join("\n")
}
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 {}
fn strip_image_sentinels(text: &str, strip: bool) -> String {
if strip && text.contains('\u{FFFC}') {
text.replace('\u{FFFC}', "")
} else {
text.to_string()
}
}
fn render_block_plain_text(
store: &Store,
block: &Block,
block_text: &str,
strip_images: bool,
mark_footnotes: bool,
notes: &crate::footnotes::Footnotes,
) -> String {
let elements = inline_segments_for_block(store, block.id, block_text);
let mut out = String::with_capacity(block_text.len());
for elem in &elements {
match &elem.content {
InlineContent::Text(t) => out.push_str(t),
InlineContent::Image { .. } => {
if !strip_images {
out.push('\u{FFFC}');
}
}
InlineContent::FootnoteRef { label } => {
if mark_footnotes {
out.push_str(&format!("[{}]", notes.marker(label)));
} else {
out.push('\u{FFFC}');
}
}
InlineContent::Empty => {}
}
}
out
}
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, options: PlainTextExportOptions) -> Result<ExportPlainTextDto> {
let PlainTextExportOptions {
quote_indent,
page_breaks,
strip_images,
endnote_footnotes,
} = options;
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 store = uow.store();
let footnote_sentinel_at_risk = (strip_images || endnote_footnotes)
&& !crate::footnotes::Footnotes::build(&store).is_empty();
if !page_breaks
&& !footnote_sentinel_at_risk
&& let Some(plain_text) = rope_flat_text_if_simple(&store, frame_ids.len())
{
uow.end_transaction()?;
return Ok(ExportPlainTextDto {
plain_text: strip_image_sentinels(&plain_text, strip_images),
});
}
let mut frames_by_id: HashMap<EntityId, Frame> = HashMap::new();
if quote_indent {
for frame_id in &frame_ids {
if let Some(frame) = uow.get_frame(frame_id)? {
frames_by_id.insert(*frame_id, frame);
}
}
}
let notes = crate::footnotes::Footnotes::build(&store);
let mut all_block_ids: Vec<EntityId> = Vec::new();
let mut quote_depth: HashMap<EntityId, usize> = HashMap::new();
for frame_id in &frame_ids {
if notes.is_definition(*frame_id) {
continue;
}
let depth = if quote_indent {
blockquote_depth(*frame_id, &frames_by_id)
} else {
0
};
for block_id in uow.get_frame_relationship(
frame_id,
&common::direct_access::frame::FrameRelationshipField::Blocks,
)? {
if depth > 0 {
quote_depth.insert(block_id, depth);
}
all_block_ids.push(block_id);
}
}
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| {
let block_text = block_content_via_store(block, &store);
let text = render_block_plain_text(
&store,
block,
&block_text,
strip_images,
endnote_footnotes,
¬es,
);
let text = match quote_depth.get(&block.id) {
Some(&depth) => indent_quoted(&text, depth),
None => text,
};
if page_breaks && block.fmt_page_break_before == Some(true) {
format!("{FORM_FEED}{text}")
} else {
text
}
})
.collect::<Vec<String>>()
.join("\n");
let mut plain_text = plain_text;
let printed = if endnote_footnotes {
notes.in_print_order()
} else {
Vec::new()
};
if !printed.is_empty() {
plain_text.push('\n');
for (number, _, frame_id) in printed {
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 mut blocks: Vec<Block> = blocks_opt.into_iter().flatten().collect();
blocks.sort_by_key(|b| b.document_position);
let body = blocks
.iter()
.map(|b| block_content_via_store(b, &store))
.collect::<Vec<_>>()
.join(" ");
let body = body.trim();
if body.is_empty() {
continue;
}
plain_text.push_str(&format!("\n{number}. {body}"));
}
}
uow.end_transaction()?;
Ok(ExportPlainTextDto { plain_text })
}
}