1use serde::{Deserialize, Serialize};
10use std::collections::{HashMap, VecDeque};
11use ucm_core::{Block, BlockId, Content, Document};
12
13use crate::error::{Error, Result};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct DeletedContent {
21 pub blocks: HashMap<BlockId, Block>,
23 pub structure: HashMap<BlockId, Vec<BlockId>>,
25 pub parent_id: BlockId,
27 pub deleted_at: chrono::DateTime<chrono::Utc>,
29}
30
31impl DeletedContent {
32 pub fn new(parent_id: BlockId) -> Self {
34 Self {
35 blocks: HashMap::new(),
36 structure: HashMap::new(),
37 parent_id,
38 deleted_at: chrono::Utc::now(),
39 }
40 }
41
42 pub fn is_empty(&self) -> bool {
44 self.blocks.is_empty()
45 }
46
47 pub fn block_count(&self) -> usize {
49 self.blocks.len()
50 }
51
52 pub fn block_ids(&self) -> Vec<BlockId> {
54 self.blocks.keys().copied().collect()
55 }
56}
57
58#[derive(Debug, Clone)]
60pub struct ClearResult {
61 pub removed_ids: Vec<BlockId>,
63 pub deleted_content: DeletedContent,
65}
66
67pub fn clear_section_content(doc: &mut Document, section_id: &BlockId) -> Result<Vec<BlockId>> {
80 let result = clear_section_content_with_undo(doc, section_id)?;
81 Ok(result.removed_ids)
82}
83
84pub fn clear_section_content_with_undo(
97 doc: &mut Document,
98 section_id: &BlockId,
99) -> Result<ClearResult> {
100 if !doc.blocks.contains_key(section_id) {
102 return Err(Error::BlockNotFound(section_id.to_string()));
103 }
104
105 let mut deleted = DeletedContent::new(*section_id);
106 let mut to_remove = Vec::new();
107 let mut queue = VecDeque::new();
108
109 if let Some(children) = doc.structure.get(section_id) {
111 deleted.structure.insert(*section_id, children.clone());
112 for child in children.clone() {
113 queue.push_back(child);
114 }
115 }
116
117 while let Some(block_id) = queue.pop_front() {
119 to_remove.push(block_id);
120
121 if let Some(block) = doc.blocks.get(&block_id) {
123 deleted.blocks.insert(block_id, block.clone());
124 }
125
126 if let Some(children) = doc.structure.get(&block_id) {
128 deleted.structure.insert(block_id, children.clone());
129 for child in children.clone() {
130 queue.push_back(child);
131 }
132 }
133 }
134
135 for block_id in &to_remove {
137 doc.blocks.remove(block_id);
138 doc.structure.remove(block_id);
139 }
140
141 if let Some(children) = doc.structure.get_mut(section_id) {
143 children.clear();
144 }
145
146 Ok(ClearResult {
147 removed_ids: to_remove,
148 deleted_content: deleted,
149 })
150}
151
152pub fn restore_deleted_content(
165 doc: &mut Document,
166 deleted: &DeletedContent,
167) -> Result<Vec<BlockId>> {
168 if !doc.blocks.contains_key(&deleted.parent_id) {
170 return Err(Error::BlockNotFound(deleted.parent_id.to_string()));
171 }
172
173 if let Some(existing_children) = doc.structure.get(&deleted.parent_id).cloned() {
175 for child in existing_children {
176 remove_subtree(doc, &child);
177 }
178 if let Some(children) = doc.structure.get_mut(&deleted.parent_id) {
179 children.clear();
180 }
181 }
182
183 let mut restored = Vec::new();
184
185 for (block_id, block) in &deleted.blocks {
187 doc.blocks.insert(*block_id, block.clone());
188 restored.push(*block_id);
189 }
190
191 for (block_id, children) in &deleted.structure {
193 if *block_id != deleted.parent_id {
194 doc.structure.insert(*block_id, children.clone());
195 }
196 }
197
198 if let Some(parent_children) = deleted.structure.get(&deleted.parent_id) {
200 if let Some(children) = doc.structure.get_mut(&deleted.parent_id) {
201 children.extend(parent_children.clone());
202 } else {
203 doc.structure
204 .insert(deleted.parent_id, parent_children.clone());
205 }
206 }
207
208 Ok(restored)
209}
210
211fn remove_subtree(doc: &mut Document, block_id: &BlockId) {
212 if let Some(children) = doc.structure.get(block_id).cloned() {
213 for child in children {
214 remove_subtree(doc, &child);
215 }
216 }
217
218 if let Some(parent) = doc.parent(block_id).cloned() {
219 if let Some(children) = doc.structure.get_mut(&parent) {
220 children.retain(|c| c != block_id);
221 }
222 }
223
224 doc.blocks.remove(block_id);
225 doc.structure.remove(block_id);
226}
227
228pub fn integrate_section_blocks(
243 doc: &mut Document,
244 target_section: &BlockId,
245 source_doc: &Document,
246 base_heading_level: Option<usize>,
247) -> Result<Vec<BlockId>> {
248 if !doc.blocks.contains_key(target_section) {
250 return Err(Error::BlockNotFound(target_section.to_string()));
251 }
252
253 let mut added_blocks = Vec::new();
254
255 let root_children = source_doc
257 .structure
258 .get(&source_doc.root)
259 .cloned()
260 .unwrap_or_default();
261
262 for child_id in root_children {
264 let integrated = integrate_subtree(
265 doc,
266 target_section,
267 source_doc,
268 &child_id,
269 base_heading_level,
270 0,
271 )?;
272 added_blocks.extend(integrated);
273 }
274
275 Ok(added_blocks)
276}
277
278fn integrate_subtree(
280 doc: &mut Document,
281 parent_id: &BlockId,
282 source_doc: &Document,
283 source_block_id: &BlockId,
284 base_heading_level: Option<usize>,
285 depth: usize,
286) -> Result<Vec<BlockId>> {
287 let mut added_blocks = Vec::new();
288
289 let source_block = source_doc
291 .get_block(source_block_id)
292 .ok_or_else(|| Error::BlockNotFound(source_block_id.to_string()))?;
293
294 let mut new_block = source_block.clone();
296
297 if let Some(base_level) = base_heading_level {
298 adjust_heading_level(&mut new_block, base_level, depth);
299 }
300
301 let new_id = regenerate_block_id(&new_block);
303 new_block.id = new_id;
304
305 doc.blocks.insert(new_id, new_block);
307 added_blocks.push(new_id);
308
309 let parent_children = doc.structure.entry(*parent_id).or_default();
311 parent_children.push(new_id);
312
313 doc.structure.entry(new_id).or_default();
315
316 if let Some(children) = source_doc.structure.get(source_block_id) {
318 for child_id in children.clone() {
319 let child_added = integrate_subtree(
320 doc,
321 &new_id,
322 source_doc,
323 &child_id,
324 base_heading_level,
325 depth + 1,
326 )?;
327 added_blocks.extend(child_added);
328 }
329 }
330
331 Ok(added_blocks)
332}
333
334fn adjust_heading_level(block: &mut Block, base_level: usize, _depth: usize) {
336 if let Some(ref mut role) = block.metadata.semantic_role {
337 let role_str = role.category.as_str();
338
339 if let Some(level_str) = role_str.strip_prefix("heading") {
341 if let Ok(current_level) = level_str.parse::<usize>() {
342 let new_level = (base_level + current_level - 1).clamp(1, 6);
344
345 if let Some(new_role) =
347 ucm_core::metadata::SemanticRole::parse(&format!("heading{}", new_level))
348 {
349 *role = new_role;
350 }
351 }
352 }
353 }
354}
355
356fn regenerate_block_id(block: &Block) -> BlockId {
358 use chrono::Utc;
359
360 let timestamp = Utc::now().timestamp_nanos_opt().unwrap_or(0) as u128;
362
363 let content_hash = ucm_core::id::compute_content_hash(&block.content);
364
365 let mut id_bytes = [0u8; 12];
367 id_bytes[0..8].copy_from_slice(×tamp.to_le_bytes()[0..8]);
368 id_bytes[8..12].copy_from_slice(&content_hash.as_bytes()[0..4]);
369
370 BlockId::from_bytes(id_bytes)
371}
372
373pub fn find_section_by_path(doc: &Document, path: &str) -> Option<BlockId> {
385 let parts: Vec<&str> = path.split(" > ").map(|s| s.trim()).collect();
386
387 if parts.is_empty() {
388 return None;
389 }
390
391 let mut current_id = doc.root;
392
393 for part in parts {
394 let children = doc.structure.get(¤t_id)?;
395
396 let found = children.iter().find(|child_id| {
397 if let Some(block) = doc.get_block(child_id) {
398 let is_heading = block
400 .metadata
401 .semantic_role
402 .as_ref()
403 .map(|r| r.category.as_str().starts_with("heading"))
404 .unwrap_or(false);
405
406 if is_heading {
407 let text = match &block.content {
409 Content::Text(t) => t.text.trim(),
410 _ => return false,
411 };
412 return text == part;
413 }
414 }
415 false
416 });
417
418 current_id = *found?;
419 }
420
421 if current_id == doc.root {
422 None
423 } else {
424 Some(current_id)
425 }
426}
427
428pub fn get_section_depth(doc: &Document, section_id: &BlockId) -> Option<usize> {
438 if *section_id == doc.root {
439 return Some(0);
440 }
441
442 let mut depth = 0;
443 let mut current = *section_id;
444
445 while let Some(parent) = doc.parent(¤t) {
446 depth += 1;
447 if *parent == doc.root {
448 return Some(depth);
449 }
450 current = *parent;
451 }
452
453 None
454}
455
456pub fn get_all_sections(doc: &Document) -> Vec<(BlockId, usize)> {
464 let mut sections = Vec::new();
465
466 for (block_id, block) in &doc.blocks {
467 if let Some(ref role) = block.metadata.semantic_role {
468 if let Some(level_str) = role.category.as_str().strip_prefix("heading") {
469 if let Ok(level) = level_str.parse::<usize>() {
470 sections.push((*block_id, level));
471 }
472 }
473 }
474 }
475
476 sections
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482 use ucm_core::{Block, Content, Document};
483
484 fn create_test_document() -> Document {
485 let mut doc = Document::create();
486 let root = doc.root;
487
488 let h1 = Block::new(Content::text("Introduction"), Some("heading1"));
490 let h1_id = doc.add_block(h1, &root).unwrap();
491
492 let h2 = Block::new(Content::text("Getting Started"), Some("heading2"));
493 let h2_id = doc.add_block(h2, &h1_id).unwrap();
494
495 let para = Block::new(Content::text("Some content here"), Some("paragraph"));
496 doc.add_block(para, &h2_id).unwrap();
497
498 doc
499 }
500
501 #[test]
502 fn test_clear_section_content() {
503 let mut doc = create_test_document();
504
505 let h1_id = find_section_by_path(&doc, "Introduction").unwrap();
507
508 let removed = clear_section_content(&mut doc, &h1_id).unwrap();
510
511 assert_eq!(removed.len(), 2);
513
514 let children = doc.structure.get(&h1_id).unwrap();
516 assert!(children.is_empty());
517 }
518
519 #[test]
520 fn test_find_section_by_path() {
521 let doc = create_test_document();
522
523 let h1_id = find_section_by_path(&doc, "Introduction");
525 assert!(h1_id.is_some());
526
527 let h2_id = find_section_by_path(&doc, "Introduction > Getting Started");
529 assert!(h2_id.is_some());
530
531 let missing = find_section_by_path(&doc, "Missing Section");
533 assert!(missing.is_none());
534 }
535
536 #[test]
537 fn test_get_all_sections() {
538 let doc = create_test_document();
539
540 let sections = get_all_sections(&doc);
541
542 assert_eq!(sections.len(), 2);
544
545 let levels: Vec<usize> = sections.iter().map(|(_, l)| *l).collect();
547 assert!(levels.contains(&1));
548 assert!(levels.contains(&2));
549 }
550
551 #[test]
552 fn test_get_section_depth() {
553 let doc = create_test_document();
554
555 let h1_id = find_section_by_path(&doc, "Introduction").unwrap();
556 let h2_id = find_section_by_path(&doc, "Introduction > Getting Started").unwrap();
557
558 assert_eq!(get_section_depth(&doc, &h1_id), Some(1));
559 assert_eq!(get_section_depth(&doc, &h2_id), Some(2));
560 }
561
562 #[test]
563 fn test_clear_with_undo_and_restore() {
564 let mut doc = create_test_document();
565 let original_count = doc.block_count();
566
567 let h1_id = find_section_by_path(&doc, "Introduction").unwrap();
569
570 let result = clear_section_content_with_undo(&mut doc, &h1_id).unwrap();
572
573 assert_eq!(result.removed_ids.len(), 2);
575 assert_eq!(result.deleted_content.block_count(), 2);
576
577 assert!(doc.block_count() < original_count);
579
580 let restored = restore_deleted_content(&mut doc, &result.deleted_content).unwrap();
582
583 assert_eq!(restored.len(), 2);
585 assert_eq!(doc.block_count(), original_count);
586
587 let children = doc.structure.get(&h1_id).unwrap();
589 assert!(!children.is_empty());
590 }
591}