changxi 0.3.0

TUI EPUB Reader
pub mod msh;

use crate::core::models::SearchResult;
use crate::core::search::msh::MaxShiftHorspool;
use crate::progress::Bookmark;

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SearchType {
    Local,
    Global,
    Bookmark,
    Chapter,
}

pub fn search_content(
    query: &str,
    chapter_index: usize,
    chapter_title: &str,
    content: &str,
    case_sensitive: bool,
) -> Vec<SearchResult> {
    if query.is_empty() {
        return Vec::new();
    }

    let (final_query, final_content) = if case_sensitive {
        (query.to_string(), content.to_string())
    } else {
        (query.to_lowercase(), content.to_lowercase())
    };

    let msh = MaxShiftHorspool::new(&final_query);
    let positions = msh.find_all(&final_content);

    positions
        .into_iter()
        .filter(|&pos| content.is_char_boundary(pos) && content.is_char_boundary(pos + query.len()))
        .map(|pos| {
            let mut start = pos.saturating_sub(30);
            while start > 0 && !content.is_char_boundary(start) {
                start -= 1;
            }

            let mut end = (pos + query.len() + 30).min(content.len());
            while end < content.len() && !content.is_char_boundary(end) {
                end += 1;
            }

            let context_before = content[start..pos].replace('\n', " ");
            let match_text = content[pos..pos + query.len()].replace('\n', " ");
            let context_after = content[pos + query.len()..end].replace('\n', " ");

            SearchResult::new(
                chapter_index,
                chapter_title.to_string(),
                context_before,
                match_text,
                context_after,
                pos,
            )
        })
        .collect()
}

pub fn search_bookmarks(
    query: &str,
    bookmarks: &[Bookmark],
    case_sensitive: bool,
) -> Vec<SearchResult> {
    if query.is_empty() {
        return Vec::new();
    }
    let query_str = if case_sensitive {
        query.to_string()
    } else {
        query.to_lowercase()
    };

    bookmarks
        .iter()
        .filter(|b| {
            if case_sensitive {
                b.name.contains(&query_str) || b.text_preview.contains(&query_str)
            } else {
                b.name.to_lowercase().contains(&query_str)
                    || b.text_preview.to_lowercase().contains(&query_str)
            }
        })
        .map(|b| {
            SearchResult::new(
                b.chapter_index,
                b.chapter_title.clone(),
                "".to_string(),
                b.name.clone(),
                "".to_string(),
                b.scroll_position,
            )
        })
        .collect()
}

pub fn search_chapters(
    query: &str,
    chapter_titles: &[String],
    case_sensitive: bool,
) -> Vec<SearchResult> {
    if query.is_empty() {
        return Vec::new();
    }
    let query_str = if case_sensitive {
        query.to_string()
    } else {
        query.to_lowercase()
    };

    chapter_titles
        .iter()
        .enumerate()
        .filter(|(_, title)| {
            if case_sensitive {
                title.contains(&query_str)
            } else {
                title.to_lowercase().contains(&query_str)
            }
        })
        .map(|(i, title)| {
            SearchResult::new(
                i,
                title.clone(),
                "".to_string(),
                title.clone(),
                "".to_string(),
                0,
            )
        })
        .collect()
}