1use ahash::AHashMap;
2use serde::{Deserialize, Serialize};
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6static ATOMIC_ANN_ID: AtomicU64 = AtomicU64::new(1);
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum AnnotationType {
12 Highlight,
13 Underline,
14 Bookmark,
15 Note,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Annotation {
21 pub id: String,
22 pub cfi_range: String,
23 pub type_: AnnotationType,
24 pub color: String,
25 pub note: Option<String>,
26 pub selected_text: Option<String>,
27 pub created_at: String,
28}
29
30#[derive(Debug, Clone, Default, Serialize, Deserialize)]
32pub struct AnnotationManager {
33 annotations: AHashMap<String, Annotation>,
34}
35
36impl AnnotationManager {
37 pub fn new() -> Self {
38 Self {
39 annotations: AHashMap::new(),
40 }
41 }
42
43 pub fn add(&mut self, annotation: Annotation) {
45 self.annotations.insert(annotation.id.clone(), annotation);
46 }
47
48 pub fn create_highlight(
50 &mut self,
51 cfi_range: &str,
52 color: &str,
53 selected_text: Option<&str>,
54 note: Option<&str>,
55 ) -> Annotation {
56 let ann = Annotation {
57 id: generate_unique_id("hl"),
58 cfi_range: cfi_range.to_string(),
59 type_: AnnotationType::Highlight,
60 color: color.to_string(),
61 note: note.map(|s| s.to_string()),
62 selected_text: selected_text.map(|s| s.to_string()),
63 created_at: current_timestamp_str(),
64 };
65 self.add(ann.clone());
66 ann
67 }
68
69 pub fn create_bookmark(&mut self, cfi: &str, note: Option<&str>) -> Annotation {
71 let ann = Annotation {
72 id: generate_unique_id("bm"),
73 cfi_range: cfi.to_string(),
74 type_: AnnotationType::Bookmark,
75 color: "#f59e0b".to_string(),
76 note: note.map(|s| s.to_string()),
77 selected_text: None,
78 created_at: current_timestamp_str(),
79 };
80 self.add(ann.clone());
81 ann
82 }
83
84 pub fn get(&self, id: &str) -> Option<&Annotation> {
86 self.annotations.get(id)
87 }
88
89 pub fn remove(&mut self, id: &str) -> bool {
91 self.annotations.remove(id).is_some()
92 }
93
94 pub fn list(&self) -> Vec<Annotation> {
96 self.annotations.values().cloned().collect()
97 }
98
99 pub fn to_w3c_json(&self) -> Result<String, String> {
101 let items: Vec<serde_json::Value> = self
102 .annotations
103 .values()
104 .map(|ann| {
105 serde_json::json!({
106 "@context": "http://www.w3.org/ns/anno.jsonld",
107 "id": format!("urn:annotation:{}", ann.id),
108 "type": "Annotation",
109 "motivation": match ann.type_ {
110 AnnotationType::Highlight => "highlighting",
111 AnnotationType::Bookmark => "bookmarking",
112 AnnotationType::Underline => "underlining",
113 AnnotationType::Note => "commenting",
114 },
115 "target": {
116 "selector": {
117 "type": "FragmentSelector",
118 "conformsTo": "http://www.idpf.org/epub/linking/cfi/epub-cfi.html",
119 "value": ann.cfi_range
120 }
121 },
122 "body": {
123 "type": "TextualBody",
124 "value": ann.note.as_deref().unwrap_or(""),
125 "format": "text/plain"
126 },
127 "created": ann.created_at
128 })
129 })
130 .collect();
131
132 serde_json::to_string(&items).map_err(|e| e.to_string())
133 }
134}
135
136fn generate_unique_id(prefix: &str) -> String {
138 let seq = ATOMIC_ANN_ID.fetch_add(1, Ordering::Relaxed);
139 let nanos = SystemTime::now()
140 .duration_since(UNIX_EPOCH)
141 .unwrap_or_default()
142 .as_nanos();
143 format!("{}-{:x}-{:x}", prefix, nanos, seq)
144}
145
146fn current_timestamp_str() -> String {
147 let secs = SystemTime::now()
148 .duration_since(UNIX_EPOCH)
149 .unwrap_or_default()
150 .as_secs();
151 secs.to_string()
152}