use crate::ImportHtmlDto;
use crate::ImportHtmlResultDto;
use anyhow::{Result, anyhow};
use common::database::CommandUnitOfWork;
use common::database::rope_helpers::{
rope_append_block, rope_append_table_anchor, rope_insert_block_boundary, rope_reset,
};
use common::entities::{
Block, Document, Frame, FramePosition, List, Resource, Root, Table, TableCell,
};
use common::long_operation::LongOperation;
use common::parser_tools::content_parser::{
ParsedElement, format_runs_from_spans, parse_html_elements,
};
use common::parser_tools::list_grouper::ListGrouper;
use common::types::{EntityId, ROOT_ENTITY_ID};
use std::sync::Arc;
pub trait ImportHtmlUnitOfWorkFactoryTrait: Send + Sync {
fn create(&self) -> Box<dyn ImportHtmlUnitOfWorkTrait>;
}
#[macros::uow_action(entity = "Root", action = "Get", thread_safe = true)]
#[macros::uow_action(entity = "Root", action = "GetRelationship", thread_safe = true)]
#[macros::uow_action(entity = "Document", action = "Get", thread_safe = true)]
#[macros::uow_action(entity = "Document", action = "Update", thread_safe = true)]
#[macros::uow_action(entity = "Document", action = "GetRelationship", thread_safe = true)]
#[macros::uow_action(entity = "Frame", action = "Get", thread_safe = true)]
#[macros::uow_action(entity = "Frame", action = "Create", thread_safe = true)]
#[macros::uow_action(entity = "Frame", action = "Update", thread_safe = true)]
#[macros::uow_action(
entity = "Frame",
action = "UpdateWithRelationships",
thread_safe = true
)]
#[macros::uow_action(entity = "Frame", action = "Remove", thread_safe = true)]
#[macros::uow_action(entity = "Frame", action = "GetRelationship", thread_safe = true)]
#[macros::uow_action(entity = "Block", action = "Create", thread_safe = true)]
#[macros::uow_action(entity = "Block", action = "SetRelationship", thread_safe = true)]
#[macros::uow_action(entity = "List", action = "Create", thread_safe = true)]
#[macros::uow_action(entity = "Resource", action = "Create", thread_safe = true)]
#[macros::uow_action(entity = "Table", action = "Create", thread_safe = true)]
#[macros::uow_action(entity = "TableCell", action = "Create", thread_safe = true)]
pub trait ImportHtmlUnitOfWorkTrait: CommandUnitOfWork + Send + Sync {}
pub struct ImportHtmlUseCase {
uow_factory: Box<dyn ImportHtmlUnitOfWorkFactoryTrait>,
dto: ImportHtmlDto,
}
struct FrameState {
frame_id: EntityId,
child_order: Vec<i64>,
}
fn transition_bq_depth(
uow: &mut Box<dyn ImportHtmlUnitOfWorkTrait>,
doc_id: EntityId,
frame_stack: &mut Vec<FrameState>,
current_bq_depth: &mut u32,
target_depth: u32,
list_grouper: &mut ListGrouper,
) -> Result<()> {
while *current_bq_depth > target_depth && frame_stack.len() > 1 {
let finished = frame_stack.pop().unwrap();
let mut frame_entity = uow
.get_frame(&finished.frame_id)?
.ok_or_else(|| anyhow!("Blockquote frame not found"))?;
frame_entity.child_order = finished.child_order;
uow.update_frame(&frame_entity)?;
*current_bq_depth -= 1;
list_grouper.reset();
}
while *current_bq_depth < target_depth {
let parent_frame_id = frame_stack.last().unwrap().frame_id;
let bq_frame = Frame {
fmt_is_blockquote: Some(true),
fmt_position: Some(FramePosition::InFlow),
parent_frame: Some(parent_frame_id),
..Frame::default()
};
let created_bq = uow.create_frame(&bq_frame, doc_id, -1)?;
frame_stack
.last_mut()
.unwrap()
.child_order
.push(-(created_bq.id as i64));
frame_stack.push(FrameState {
frame_id: created_bq.id,
child_order: Vec::new(),
});
*current_bq_depth += 1;
list_grouper.reset();
}
Ok(())
}
impl ImportHtmlUseCase {
pub fn new(
uow_factory: Box<dyn ImportHtmlUnitOfWorkFactoryTrait>,
dto: &ImportHtmlDto,
) -> Self {
ImportHtmlUseCase {
uow_factory,
dto: dto.clone(),
}
}
}
impl LongOperation for ImportHtmlUseCase {
type Output = ImportHtmlResultDto;
fn execute(
&self,
progress_callback: Box<dyn Fn(common::long_operation::OperationProgress) + Send>,
cancel_flag: Arc<std::sync::atomic::AtomicBool>,
) -> Result<Self::Output> {
use std::sync::atomic::Ordering;
progress_callback(common::long_operation::OperationProgress::new(
0.0,
Some("Starting HTML import...".to_string()),
));
let parsed_elements = parse_html_elements(&self.dto.html_text);
progress_callback(common::long_operation::OperationProgress::new(
10.0,
Some("Parsed HTML, building document...".to_string()),
));
let mut 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,
)?;
for frame_id in &frame_ids {
uow.remove_frame(frame_id)?;
}
if cancel_flag.load(Ordering::Relaxed) {
uow.rollback()?;
return Err(anyhow!("Operation was cancelled"));
}
progress_callback(common::long_operation::OperationProgress::new(
20.0,
Some("Creating new document structure...".to_string()),
));
let new_frame = Frame::default();
let created_frame = uow.create_frame(&new_frame, doc_id, -1)?;
rope_reset(&uow.store());
let total_elements = parsed_elements.len();
let mut total_chars: i64 = 0;
let mut total_block_count: i64 = 0;
let mut document_position: i64 = 0;
let mut frame_stack: Vec<FrameState> = vec![FrameState {
frame_id: created_frame.id,
child_order: Vec::new(),
}];
let mut current_bq_depth: u32 = 0;
let mut list_grouper = ListGrouper::new();
let mut emitted_any_main_block = false;
for (i, parsed_element) in parsed_elements.iter().enumerate() {
if cancel_flag.load(Ordering::Relaxed) {
uow.rollback()?;
return Err(anyhow!("Operation was cancelled"));
}
match parsed_element {
ParsedElement::Block(parsed_block) => {
transition_bq_depth(
&mut uow,
doc_id,
&mut frame_stack,
&mut current_bq_depth,
parsed_block.blockquote_depth,
&mut list_grouper,
)?;
let (plain_text, format_runs) =
format_runs_from_spans(&parsed_block.spans, parsed_block.is_code_block);
let line_len = plain_text.chars().count() as i64;
let current_frame_id = frame_stack.last().unwrap().frame_id;
let block = Block {
document_position,
fmt_heading_level: parsed_block.heading_level,
fmt_line_height: parsed_block.line_height,
fmt_non_breakable_lines: parsed_block.non_breakable_lines,
fmt_direction: parsed_block.direction.clone(),
fmt_background_color: parsed_block.background_color.clone(),
fmt_is_code_block: if parsed_block.is_code_block {
Some(true)
} else {
None
},
fmt_code_language: parsed_block.code_language.clone(),
..Block::default()
};
let created_block = uow.create_block(&block, current_frame_id, -1)?;
if emitted_any_main_block {
rope_insert_block_boundary(&uow.store());
}
rope_append_block(&uow.store(), created_block.id, &plain_text);
emitted_any_main_block = true;
{
let store = uow.store();
let mut runs_map = store.format_runs.write();
if !format_runs.is_empty() {
runs_map.insert(created_block.id, format_runs);
} else {
runs_map.remove(&created_block.id);
}
}
if let Some(ref list_style) = parsed_block.list_style {
let list_id = if let Some(existing_id) =
list_grouper.try_reuse(list_style, parsed_block.list_indent)
{
existing_id
} else {
let list = List {
style: list_style.clone(),
indent: parsed_block.list_indent as i64,
..List::default()
};
let created_list = uow.create_list(&list, doc_id, -1)?;
list_grouper.register(
created_list.id,
list_style.clone(),
parsed_block.list_indent,
);
created_list.id
};
uow.set_block_relationship(
&created_block.id,
&common::direct_access::block::BlockRelationshipField::List,
&[list_id],
)?;
} else {
list_grouper.reset();
}
frame_stack
.last_mut()
.unwrap()
.child_order
.push(created_block.id as i64);
total_chars += line_len;
total_block_count += 1;
document_position += line_len;
if i < total_elements - 1 {
document_position += 1;
}
}
ParsedElement::Table(parsed_table) => {
transition_bq_depth(
&mut uow,
doc_id,
&mut frame_stack,
&mut current_bq_depth,
parsed_table.blockquote_depth,
&mut list_grouper,
)?;
list_grouper.reset();
let num_rows = parsed_table.rows.len() as i64;
let num_cols = parsed_table.rows.first().map_or(0, |r| r.len()) as i64;
if num_rows == 0 || num_cols == 0 {
continue;
}
let table = Table {
rows: num_rows,
columns: num_cols,
column_widths: vec![],
..Table::default()
};
let created_table = uow.create_table(&table, doc_id, -1)?;
rope_append_table_anchor(&uow.store(), created_table.id);
let current_frame_id = frame_stack.last().unwrap().frame_id;
let total_cells = num_rows * num_cols;
let mut cell_count: i64 = 0;
let mut created_cell_frame_ids: Vec<EntityId> = Vec::new();
for (r, row) in parsed_table.rows.iter().enumerate() {
for (c, cell) in row.iter().enumerate() {
let cell_frame = Frame::default();
let created_cell_frame = uow.create_frame(&cell_frame, doc_id, -1)?;
created_cell_frame_ids.push(created_cell_frame.id);
let (plain_text, format_runs) =
format_runs_from_spans(&cell.spans, false);
let block = Block {
document_position,
..Block::default()
};
let created_block =
uow.create_block(&block, created_cell_frame.id, -1)?;
{
let store = uow.store();
let mut runs_map = store.format_runs.write();
if !format_runs.is_empty() {
runs_map.insert(created_block.id, format_runs);
} else {
runs_map.remove(&created_block.id);
}
}
rope_insert_block_boundary(&uow.store());
rope_append_block(&uow.store(), created_block.id, &plain_text);
let mut updated_cell_frame = created_cell_frame.clone();
updated_cell_frame.child_order = vec![created_block.id as i64];
uow.update_frame(&updated_cell_frame)?;
let table_cell = TableCell {
row: r as i64,
column: c as i64,
row_span: 1,
column_span: 1,
cell_frame: Some(created_cell_frame.id),
..TableCell::default()
};
uow.create_table_cell(&table_cell, created_table.id, -1)?;
let text_length = plain_text.chars().count() as i64;
total_chars += text_length;
total_block_count += 1;
cell_count += 1;
document_position += text_length;
if cell_count < total_cells {
document_position += 1;
}
}
}
let anchor_frame = Frame {
parent_frame: Some(current_frame_id),
table: Some(created_table.id),
..Frame::default()
};
let created_anchor = uow.create_frame(&anchor_frame, doc_id, -1)?;
for cell_frame_id in &created_cell_frame_ids {
if let Some(cf) = uow.get_frame(cell_frame_id)? {
let mut updated = cf;
updated.parent_frame = Some(created_anchor.id);
uow.update_frame_with_relationships(&updated)?;
}
}
frame_stack
.last_mut()
.unwrap()
.child_order
.push(-(created_anchor.id as i64));
emitted_any_main_block = true;
if i < total_elements - 1 {
document_position += 1;
}
}
}
if i % 10 == 0 {
let pct = 20.0 + (i as f32 / total_elements as f32) * 70.0;
progress_callback(common::long_operation::OperationProgress::new(
pct,
Some(format!("Processing element {}/{}", i + 1, total_elements)),
));
}
}
while frame_stack.len() > 1 {
let finished = frame_stack.pop().unwrap();
let mut frame_entity = uow
.get_frame(&finished.frame_id)?
.ok_or_else(|| anyhow!("Blockquote frame not found"))?;
frame_entity.child_order = finished.child_order;
uow.update_frame(&frame_entity)?;
}
let root_state = frame_stack.pop().unwrap();
let mut updated_frame = uow
.get_frame(&root_state.frame_id)?
.ok_or_else(|| anyhow!("Created frame not found"))?;
updated_frame.child_order = root_state.child_order;
uow.update_frame(&updated_frame)?;
let mut updated_doc = uow
.get_document(&doc_id)?
.ok_or_else(|| anyhow!("Document not found after import"))?;
updated_doc.character_count = total_chars;
updated_doc.block_count = total_block_count;
uow.update_document(&updated_doc)?;
if cancel_flag.load(Ordering::Relaxed) {
uow.rollback()?;
return Err(anyhow!("Operation was cancelled"));
}
uow.commit()?;
progress_callback(common::long_operation::OperationProgress::new(
100.0,
Some("completed".to_string()),
));
Ok(ImportHtmlResultDto {
block_count: total_block_count,
})
}
}