//! 书签与标注模块 //! //! 支持高亮、下划线、波浪线、边注等标注类型 use anyhow::Result; use serde::{Deserialize, Serialize}; use chrono::{DateTime, Utc}; /// 标注类型 #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum HighlightType { /// 高亮线 Highlight, /// 下划线 Underline, /// 波浪线 Wavy, /// 边注 MarginNote, } /// 书签/标注记录 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Bookmark { /// 唯一标识 pub id: String, /// 文档路径 pub document_path: String, /// 页码 pub page_number: usize, /// 位置偏移 pub position: usize, /// 标注类型 pub highlight_type: HighlightType, /// 标注的原文内容 pub text: String, /// 用户笔记 pub note: Option, /// 颜色 (HEX) pub color: Option, /// 创建时间 pub created_at: DateTime, /// 更新时间 pub updated_at: DateTime, } impl Bookmark { pub fn new( document_path: String, page_number: usize, position: usize, highlight_type: HighlightType, text: String, ) -> Self { let now = Utc::now(); Self { id: uuid::Uuid::new_v4().to_string(), document_path, page_number, position, highlight_type, text, note: None, color: None, created_at: now, updated_at: now, } } /// 设置笔记内容 pub fn with_note(mut self, note: String) -> Self { self.note = Some(note); self.updated_at = Utc::now(); self } /// 设置颜色 pub fn with_color(mut self, color: String) -> Self { self.color = Some(color); self.updated_at = Utc::now(); self } } /// 书签管理器 pub struct BookmarkManager { db: sled::Db, } impl BookmarkManager { /// 创建书签管理器 pub fn new(db_path: &str) -> Result { let db = sled::open(db_path)?; Ok(Self { db }) } /// 添加书签 pub fn add(&self, bookmark: &Bookmark) -> Result<()> { let key = format!("bookmark:{}", bookmark.id); let value = serde_json::to_vec(bookmark)?; self.db.insert(key, value)?; Ok(()) } /// 删除书签 pub fn remove(&self, id: &str) -> Result<()> { let key = format!("bookmark:{}", id); self.db.remove(key)?; Ok(()) } /// 获取文档的所有书签 pub fn get_by_document(&self, document_path: &str) -> Result> { let prefix = format!("bookmark:"); let mut bookmarks = Vec::new(); for item in self.db.scan_prefix(prefix) { let (_, value) = item?; let bookmark: Bookmark = serde_json::from_slice(&value)?; if bookmark.document_path == document_path { bookmarks.push(bookmark); } } // 按页码和位置排序 bookmarks.sort_by(|a, b| { a.page_number.cmp(&b.page_number) .then(a.position.cmp(&b.position)) }); Ok(bookmarks) } /// 获取所有书签 pub fn get_all(&self) -> Result> { let mut bookmarks = Vec::new(); for item in self.db.scan_prefix("bookmark:") { let (_, value) = item?; let bookmark: Bookmark = serde_json::from_slice(&value)?; bookmarks.push(bookmark); } Ok(bookmarks) } /// 导出书签为 Markdown pub fn export_markdown(&self, document_path: &str) -> Result { let bookmarks = self.get_by_document(document_path)?; let mut md = String::new(); md.push_str(&format!("# {} 标注导出\n\n", document_path)); md.push_str(&format!("导出时间:{}\n\n", Utc::now().format("%Y-%m-%d %H:%M:%S"))); md.push_str(&format!("共 {} 处标注\n\n", bookmarks.len())); md.push_str("---\n\n"); let mut current_page = 0; for bookmark in bookmarks { if bookmark.page_number != current_page { current_page = bookmark.page_number; md.push_str(&format!("## 第 {} 页\n\n", current_page)); } // 标注类型图标 let icon = match bookmark.highlight_type { HighlightType::Highlight => "🟨", HighlightType::Underline => "📏", HighlightType::Wavy => "〰️", HighlightType::MarginNote => "📝", }; md.push_str(&format!("{} **{}**\n\n", icon, bookmark.text)); if let Some(note) = &bookmark.note { md.push_str(&format!("> 💬 {}\n\n", note)); } md.push_str("---\n\n"); } Ok(md) } /// 导出书签为 CSV pub fn export_csv(&self, document_path: &str) -> Result { let bookmarks = self.get_by_document(document_path)?; let mut csv = String::new(); // CSV 头部 csv.push_str("id,page,position,type,text,note,color,created_at\n"); for bookmark in bookmarks { let highlight_type_str = match bookmark.highlight_type { HighlightType::Highlight => "highlight", HighlightType::Underline => "underline", HighlightType::Wavy => "wavy", HighlightType::MarginNote => "margin_note", }; let note = bookmark.note.as_deref().unwrap_or(""); let color = bookmark.color.as_deref().unwrap_or(""); // CSV 转义 let text_escaped = bookmark.text.replace('"', "\"\""); let note_escaped = note.replace('"', "\"\""); csv.push_str(&format!( "{},{},{},{},\"{}\",\"{}\",\"{}\",{}\n", bookmark.id, bookmark.page_number, bookmark.position, highlight_type_str, text_escaped, note_escaped, color, bookmark.created_at.to_rfc3339() )); } Ok(csv) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_bookmark_creation() { let bookmark = Bookmark::new( "/path/to/doc.md".to_string(), 1, 100, HighlightType::Highlight, "这是一段测试文本".to_string(), ); assert_eq!(bookmark.page_number, 1); assert_eq!(bookmark.text, "这是一段测试文本"); } }