use crate::ExportMarkdownDto;
use anyhow::{Result, anyhow};
use common::database::QueryUnitOfWork;
use common::database::rope_helpers::block_content_via_store;
use common::entities::{Block, Document, Frame, List, ListStyle, Root, Table, TableCell};
use common::format_runs::{InlineContent, InlineSegment};
use common::types::{EntityId, ROOT_ENTITY_ID};
use std::collections::HashSet;
pub trait ExportMarkdownUnitOfWorkFactoryTrait: Send + Sync {
fn create(&self) -> Box<dyn ExportMarkdownUnitOfWorkTrait>;
}
#[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 = "GetMultiRO")]
#[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 ExportMarkdownUnitOfWorkTrait: QueryUnitOfWork {}
pub struct ExportMarkdownUseCase {
uow_factory: Box<dyn ExportMarkdownUnitOfWorkFactoryTrait>,
}
impl ExportMarkdownUseCase {
pub fn new(uow_factory: Box<dyn ExportMarkdownUnitOfWorkFactoryTrait>) -> Self {
ExportMarkdownUseCase { uow_factory }
}
pub fn execute(&mut self) -> Result<ExportMarkdownDto> {
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 output_parts: Vec<String> = Vec::new();
for frame_id in &frame_ids {
if cell_frame_ids.contains(frame_id) {
continue;
}
let frame = uow.get_frame(frame_id)?;
if let Some(ref f) = frame
&& let Some(table_id) = f.table
{
let table_md = self.render_table_markdown(&*uow, &table_id)?;
if !output_parts.is_empty() {
output_parts.push("\n\n".to_string());
}
output_parts.push(table_md);
continue;
}
if let Some(ref f) = frame {
let frame_lines = self.render_frame_content(&*uow, f, &cell_frame_ids, "")?;
for line in frame_lines {
if !output_parts.is_empty() {
output_parts.push("\n\n".to_string());
}
output_parts.push(line);
}
}
}
uow.end_transaction()?;
let markdown_text = output_parts.concat();
Ok(ExportMarkdownDto { markdown_text })
}
fn render_frame_content(
&self,
uow: &dyn ExportMarkdownUnitOfWorkTrait,
frame: &Frame,
cell_frame_ids: &HashSet<EntityId>,
quote_prefix: &str,
) -> Result<Vec<String>> {
let mut result: Vec<String> = Vec::new();
let mut prev_was_list = false;
let mut ordered_list_counter: i64 = 0;
let mut current_list_id: Option<EntityId> = None;
let use_child_order = !frame.child_order.is_empty();
if use_child_order {
for &entry in &frame.child_order {
if entry > 0 {
let block_id = entry as EntityId;
let block = uow.get_block(&block_id)?;
if let Some(ref b) = block {
let (line, is_list_item) = self.render_block_line(
uow,
b,
quote_prefix,
&mut ordered_list_counter,
&mut current_list_id,
)?;
if !result.is_empty() {
if is_list_item && prev_was_list {
result.push("\n".to_string());
} else {
result.push("\n\n".to_string());
}
}
result.push(line);
prev_was_list = is_list_item;
}
} else {
let sub_frame_id = (-entry) as EntityId;
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 let Some(table_id) = sf.table {
let table_md = self.render_table_markdown(uow, &table_id)?;
let prefixed = if !quote_prefix.is_empty() {
prefix_lines(&table_md, quote_prefix)
} else {
table_md
};
if !result.is_empty() {
result.push("\n\n".to_string());
}
result.push(prefixed);
prev_was_list = false;
current_list_id = None;
ordered_list_counter = 0;
continue;
}
let sub_prefix = if sf.fmt_is_blockquote == Some(true) {
format!("{}> ", quote_prefix)
} else {
quote_prefix.to_string()
};
let sub_lines =
self.render_frame_content(uow, sf, cell_frame_ids, &sub_prefix)?;
for sub_line in sub_lines {
if !result.is_empty() {
result.push("\n\n".to_string());
}
result.push(sub_line);
}
prev_was_list = false;
current_list_id = None;
ordered_list_counter = 0;
}
}
}
} else {
let block_ids = uow.get_frame_relationship(
&frame.id,
&common::direct_access::frame::FrameRelationshipField::Blocks,
)?;
if block_ids.is_empty() {
return Ok(result);
}
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);
for block in &blocks {
let (line, is_list_item) = self.render_block_line(
uow,
block,
quote_prefix,
&mut ordered_list_counter,
&mut current_list_id,
)?;
if !result.is_empty() {
if is_list_item && prev_was_list {
result.push("\n".to_string());
} else {
result.push("\n\n".to_string());
}
}
result.push(line);
prev_was_list = is_list_item;
}
}
Ok(result)
}
fn render_block_line(
&self,
uow: &dyn ExportMarkdownUnitOfWorkTrait,
block: &Block,
quote_prefix: &str,
ordered_list_counter: &mut i64,
current_list_id: &mut Option<EntityId>,
) -> Result<(String, bool)> {
if block.fmt_is_code_block == Some(true) {
let lang = block.fmt_code_language.as_deref().unwrap_or("");
let block_text = block_content_via_store(block, &uow.store());
let elements = common::format_runs_query::inline_segments_for_block(
&uow.store(),
block.id,
&block_text,
);
let mut raw_text = String::new();
for elem in &elements {
match &elem.content {
InlineContent::Text(t) => raw_text.push_str(t),
InlineContent::Empty => {}
InlineContent::Image { .. } => {}
}
}
let code_block = if quote_prefix.is_empty() {
format!("```{}\n{}\n```", lang, raw_text)
} else {
let mut lines = Vec::new();
lines.push(format!("{}```{}", quote_prefix, lang));
for line in raw_text.lines() {
lines.push(format!("{}{}", quote_prefix, line));
}
if raw_text.is_empty() {
lines.push(quote_prefix.to_string());
}
lines.push(format!("{}```", quote_prefix));
lines.join("\n")
};
*current_list_id = None;
*ordered_list_counter = 0;
return Ok((code_block, false));
}
let block_text = block_content_via_store(block, &uow.store());
let elements = common::format_runs_query::inline_segments_for_block(
&uow.store(),
block.id,
&block_text,
);
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
};
let is_list_item = list.is_some();
let inline_md = self.render_inline_segments(&elements)?;
let block_line = if let Some(level) = block.fmt_heading_level {
let prefix = "#".repeat(level as usize);
format!("{}{} {}", quote_prefix, prefix, inline_md)
} else if let Some(ref list_entity) = list {
let indent_prefix = " ".repeat(list_entity.indent as usize);
match list_entity.style {
ListStyle::Decimal
| ListStyle::LowerAlpha
| ListStyle::UpperAlpha
| ListStyle::LowerRoman
| ListStyle::UpperRoman => {
let this_list_id = list_ids.first().copied();
if this_list_id != *current_list_id {
*ordered_list_counter = 1;
*current_list_id = this_list_id;
} else {
*ordered_list_counter += 1;
}
format!(
"{}{}{}. {}",
quote_prefix, indent_prefix, ordered_list_counter, inline_md
)
}
_ => format!("{}{}- {}", quote_prefix, indent_prefix, inline_md),
}
} else {
*current_list_id = None;
*ordered_list_counter = 0;
format!("{}{}", quote_prefix, inline_md)
};
Ok((block_line, is_list_item))
}
fn render_inline_segments(&self, elements: &[InlineSegment]) -> Result<String> {
let mut inline_md = String::new();
for elem in elements {
let is_code = elem.fmt_font_family.as_deref() == Some("monospace");
let text = match &elem.content {
InlineContent::Text(t) => {
if is_code {
t.clone()
} else {
escape_markdown(t)
}
}
InlineContent::Image { name, .. } => {
format!("", name, name)
}
InlineContent::Empty => String::new(),
};
if text.is_empty() {
continue;
}
let mut formatted = text.clone();
if elem.fmt_font_family.as_deref() == Some("monospace") {
formatted = format!("`{}`", formatted);
}
if elem.fmt_font_strikeout == Some(true) {
formatted = format!("~~{}~~", formatted);
}
if elem.fmt_font_bold == Some(true) && elem.fmt_font_italic == Some(true) {
formatted = format!("***{}***", formatted);
} else if elem.fmt_font_bold == Some(true) {
formatted = format!("**{}**", formatted);
} else if elem.fmt_font_italic == Some(true) {
formatted = format!("*{}*", formatted);
}
if let Some(ref href) = elem.fmt_anchor_href {
formatted = format!("[{}]({})", formatted, href);
}
inline_md.push_str(&formatted);
}
Ok(inline_md)
}
fn render_table_markdown(
&self,
uow: &dyn ExportMarkdownUnitOfWorkTrait,
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 grid: Vec<Vec<String>> = vec![vec![String::new(); cols]; rows];
for cell in &cells {
let r = cell.row as usize;
let c = cell.column as usize;
if r >= rows || c >= cols {
continue;
}
let mut cell_text = String::new();
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 mut blocks: Vec<Block> = blocks_opt.into_iter().flatten().collect();
blocks.sort_by_key(|b| b.document_position);
let mut parts: Vec<String> = Vec::new();
for block in &blocks {
let inline_md = self.render_inline_markdown(uow, block)?;
if !inline_md.is_empty() {
parts.push(inline_md);
}
}
cell_text = parts.join(" ");
}
grid[r][c] = cell_text;
}
let mut md = String::new();
for (r, row) in grid.iter().enumerate() {
md.push('|');
for cell_text in row {
md.push(' ');
md.push_str(cell_text);
md.push_str(" |");
}
md.push('\n');
if r == 0 {
md.push('|');
for _ in 0..cols {
md.push_str("---|");
}
md.push('\n');
}
}
if md.ends_with('\n') {
md.pop();
}
Ok(md)
}
fn render_inline_markdown(
&self,
uow: &dyn ExportMarkdownUnitOfWorkTrait,
block: &Block,
) -> Result<String> {
let block_text = block_content_via_store(block, &uow.store());
let elements = common::format_runs_query::inline_segments_for_block(
&uow.store(),
block.id,
&block_text,
);
self.render_inline_segments(&elements)
}
}
fn prefix_lines(text: &str, prefix: &str) -> String {
text.lines()
.map(|line| format!("{}{}", prefix, line))
.collect::<Vec<_>>()
.join("\n")
}
fn escape_markdown(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\\' | '*' | '_' | '{' | '}' | '[' | ']' | '(' | ')' | '#' | '+' | '-' | '.' | '!'
| '|' | '~' | '>' => {
result.push('\\');
result.push(c);
}
_ => result.push(c),
}
}
result
}