ebook-rs 0.2.0

Pure Rust EPUB parser and reader engine with CFI location mapping, full-text search, font de-obfuscation, and WASM/HTTP reader support.
Documentation
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};

static ATOMIC_ANN_ID: AtomicU64 = AtomicU64::new(1);

/// Type of CFI-anchored user annotation.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AnnotationType {
    Highlight,
    Underline,
    Bookmark,
    Note,
}

/// A user annotation entry.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Annotation {
    pub id: String,
    pub cfi_range: String,
    pub type_: AnnotationType,
    pub color: String,
    pub note: Option<String>,
    pub selected_text: Option<String>,
    pub created_at: String,
}

/// Manager for annotations.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AnnotationManager {
    annotations: HashMap<String, Annotation>,
}

impl AnnotationManager {
    pub fn new() -> Self {
        Self {
            annotations: HashMap::new(),
        }
    }

    /// Add an annotation entry.
    pub fn add(&mut self, annotation: Annotation) {
        self.annotations.insert(annotation.id.clone(), annotation);
    }

    /// Create a highlight annotation.
    pub fn create_highlight(
        &mut self,
        cfi_range: &str,
        color: &str,
        selected_text: Option<&str>,
        note: Option<&str>,
    ) -> Annotation {
        let ann = Annotation {
            id: generate_unique_id("hl"),
            cfi_range: cfi_range.to_string(),
            type_: AnnotationType::Highlight,
            color: color.to_string(),
            note: note.map(|s| s.to_string()),
            selected_text: selected_text.map(|s| s.to_string()),
            created_at: current_timestamp_str(),
        };
        self.add(ann.clone());
        ann
    }

    /// Create a bookmark annotation.
    pub fn create_bookmark(&mut self, cfi: &str, note: Option<&str>) -> Annotation {
        let ann = Annotation {
            id: generate_unique_id("bm"),
            cfi_range: cfi.to_string(),
            type_: AnnotationType::Bookmark,
            color: "#f59e0b".to_string(),
            note: note.map(|s| s.to_string()),
            selected_text: None,
            created_at: current_timestamp_str(),
        };
        self.add(ann.clone());
        ann
    }

    /// Get annotation by ID.
    pub fn get(&self, id: &str) -> Option<&Annotation> {
        self.annotations.get(id)
    }

    /// Remove an annotation by ID.
    pub fn remove(&mut self, id: &str) -> bool {
        self.annotations.remove(id).is_some()
    }

    /// List all annotations.
    pub fn list(&self) -> Vec<Annotation> {
        self.annotations.values().cloned().collect()
    }
}

/// B6 Fix: Generate 100% collision-free unique IDs using atomic sequence counters + timestamp.
fn generate_unique_id(prefix: &str) -> String {
    let seq = ATOMIC_ANN_ID.fetch_add(1, Ordering::Relaxed);
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos();
    format!("{}-{:x}-{:x}", prefix, nanos, seq)
}

fn current_timestamp_str() -> String {
    let secs = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs();
    secs.to_string()
}