use serde::{Deserialize, Serialize}; use std::ops::RangeInclusive; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum BlockType { Paragraph, Heading, BulletListItem, NumberedListItem, Todo, Quote, Divider, CodeBlock, PageReference, BlockReference, MediaPlaceholder, ProgressPlaceholder, } impl BlockType { pub fn from_editor_label(label: &str) -> Option { match label { "paragraph" => Some(Self::Paragraph), "heading" => Some(Self::Heading), "bullet_list_item" => Some(Self::BulletListItem), "numbered_list_item" => Some(Self::NumberedListItem), "todo" => Some(Self::Todo), "quote" => Some(Self::Quote), "divider" => Some(Self::Divider), "code_block" => Some(Self::CodeBlock), "page_reference" => Some(Self::PageReference), "block_reference" => Some(Self::BlockReference), "media_placeholder" => Some(Self::MediaPlaceholder), "progress_placeholder" => Some(Self::ProgressPlaceholder), _ => None, } } pub fn as_editor_label(&self) -> &'static str { match self { Self::Paragraph => "paragraph", Self::Heading => "heading", Self::BulletListItem => "bullet_list_item", Self::NumberedListItem => "numbered_list_item", Self::Todo => "todo", Self::Quote => "quote", Self::Divider => "divider", Self::CodeBlock => "code_block", Self::PageReference => "page_reference", Self::BlockReference => "block_reference", Self::MediaPlaceholder => "media_placeholder", Self::ProgressPlaceholder => "progress_placeholder", } } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ReferenceKind { Page, Block, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BlockReference { pub kind: ReferenceKind, pub target_id: String, pub label: Option, } #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct BlockContent { pub text: String, pub language: Option, pub reference: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct DocumentBlock { pub id: String, pub block_type: BlockType, pub parent_id: Option, pub indent: u16, pub collapsed: bool, pub heading_level: Option, pub checked: Option, pub content: BlockContent, } impl DocumentBlock { pub fn new(id: impl Into, block_type: BlockType) -> Self { Self { id: id.into(), block_type, parent_id: None, indent: 0, collapsed: false, heading_level: None, checked: None, content: BlockContent::default(), } } pub fn with_parent(mut self, parent_id: impl Into, indent: u16) -> Self { self.parent_id = Some(parent_id.into()); self.indent = indent; self } pub fn with_heading_level(mut self, level: u8) -> Self { self.heading_level = Some(level); self } pub fn with_text(mut self, text: impl Into) -> Self { self.content.text = text.into(); self } pub fn with_checked(mut self, checked: bool) -> Self { self.checked = Some(checked); self } pub fn with_language(mut self, language: impl Into) -> Self { self.content.language = Some(language.into()); self } pub fn with_reference( mut self, kind: ReferenceKind, target_id: impl Into, label: Option, ) -> Self { self.content.reference = Some(BlockReference { kind, target_id: target_id.into(), label, }); self } pub fn with_collapsed(mut self, collapsed: bool) -> Self { self.collapsed = collapsed; self } pub fn set_block_type(mut self, block_type: BlockType) -> Self { self.block_type = block_type; self } } #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct DocumentModel { blocks: Vec, } impl DocumentModel { pub fn new(blocks: Vec) -> Self { Self { blocks } } pub fn empty() -> Self { Self::default() } pub fn blocks(&self) -> &[DocumentBlock] { &self.blocks } pub(crate) fn blocks_mut(&mut self) -> &mut Vec { &mut self.blocks } pub fn push(&mut self, block: DocumentBlock) { self.blocks.push(block); } pub fn block(&self, id: &str) -> Option<&DocumentBlock> { self.blocks.iter().find(|block| block.id == id) } pub(crate) fn block_mut(&mut self, id: &str) -> Option<&mut DocumentBlock> { self.blocks.iter_mut().find(|block| block.id == id) } pub fn contains_id(&self, id: &str) -> bool { self.block(id).is_some() } pub fn index_of(&self, id: &str) -> Option { self.blocks.iter().position(|block| block.id == id) } pub fn children_of<'a>(&'a self, parent_id: Option<&str>) -> Vec<&'a DocumentBlock> { self.blocks .iter() .filter(|block| block.parent_id.as_deref() == parent_id) .collect() } pub fn is_descendant_of(&self, block_id: &str, ancestor_id: &str) -> bool { let mut current_parent = self .block(block_id) .and_then(|block| block.parent_id.as_deref()); while let Some(parent_id) = current_parent { if parent_id == ancestor_id { return true; } current_parent = self .block(parent_id) .and_then(|parent| parent.parent_id.as_deref()); } false } pub fn subtree_range(&self, block_id: &str) -> Option> { let start = self.index_of(block_id)?; let mut end = start; for next_index in (start + 1)..self.blocks.len() { if self.is_descendant_of(&self.blocks[next_index].id, block_id) { end = next_index; } else { break; } } Some(start..=end) } }