use crate::grep::error::{GrepError, GrepResult};
pub fn extract_text(data: &[u8]) -> GrepResult<String> {
use epub::doc::EpubDoc;
use std::io::Cursor;
let cursor = Cursor::new(data.to_vec());
let mut doc = EpubDoc::from_reader(cursor).map_err(|e| GrepError::DocumentExtraction {
file_path: std::path::PathBuf::from("<memory>"),
message: format!("EPUB parsing failed: {}", e),
})?;
let mut all_text = Vec::new();
if let Some(title) = doc.mdata("title") {
all_text.push(format!("Title: {:?}", title));
}
if let Some(author) = doc.mdata("creator") {
all_text.push(format!("Author: {:?}", author));
}
if !all_text.is_empty() {
all_text.push(String::new()); }
while doc.go_next() {
if let Some((content, _mime)) = doc.get_current_str() {
let plain_text = strip_html_tags(&content);
if !plain_text.trim().is_empty() {
all_text.push(plain_text);
}
}
}
if all_text.is_empty() {
return Err(GrepError::DocumentEmpty(std::path::PathBuf::from(
"<memory>",
)));
}
Ok(all_text.join("\n\n"))
}
fn strip_html_tags(html: &str) -> String {
let mut result = String::with_capacity(html.len());
let mut in_tag = false;
let mut in_script = false;
let mut in_style = false;
let mut last_was_space = true;
let chars: Vec<char> = html.chars().collect();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c == '<' {
let remaining: String = chars[i..].iter().take(10).collect();
let remaining_lower = remaining.to_lowercase();
if remaining_lower.starts_with("<script") {
in_script = true;
} else if remaining_lower.starts_with("</script") {
in_script = false;
} else if remaining_lower.starts_with("<style") {
in_style = true;
} else if remaining_lower.starts_with("</style") {
in_style = false;
}
in_tag = true;
} else if c == '>' {
in_tag = false;
} else if !in_tag && !in_script && !in_style {
if c == '&' {
let entity_end = chars[i..].iter().position(|&x| x == ';');
if let Some(end) = entity_end {
let entity: String = chars[i..=i + end].iter().collect();
let decoded = decode_html_entity(&entity);
if decoded == " " || decoded == "\n" {
if !last_was_space {
result.push(' ');
last_was_space = true;
}
} else {
result.push_str(&decoded);
last_was_space = false;
}
i += end;
} else {
result.push(c);
last_was_space = false;
}
} else if c.is_whitespace() {
if !last_was_space {
result.push(' ');
last_was_space = true;
}
} else {
result.push(c);
last_was_space = false;
}
}
i += 1;
}
result.trim().to_string()
}
fn decode_html_entity(entity: &str) -> String {
match entity {
" " | " " => " ".to_string(),
"<" | "<" => "<".to_string(),
">" | ">" => ">".to_string(),
"&" | "&" => "&".to_string(),
""" | """ => "\"".to_string(),
"'" | "'" => "'".to_string(),
"—" | "—" => "\u{2014}".to_string(),
"–" | "–" => "\u{2013}".to_string(),
"…" | "…" => "\u{2026}".to_string(),
"“" | "“" => "\u{201C}".to_string(),
"”" | "”" => "\u{201D}".to_string(),
"‘" | "‘" => "\u{2018}".to_string(),
"’" | "’" => "\u{2019}".to_string(),
"©" | "©" => "\u{00A9}".to_string(),
"®" | "®" => "\u{00AE}".to_string(),
_ => {
if entity.starts_with("&#x") && entity.ends_with(';') {
let hex = &entity[3..entity.len() - 1];
if let Ok(code) = u32::from_str_radix(hex, 16) {
if let Some(c) = char::from_u32(code) {
return c.to_string();
}
}
} else if entity.starts_with("&#") && entity.ends_with(';') {
let num = &entity[2..entity.len() - 1];
if let Ok(code) = num.parse::<u32>() {
if let Some(c) = char::from_u32(code) {
return c.to_string();
}
}
}
entity.to_string()
}
}
}
pub fn extract_text_from_file(path: &std::path::Path) -> GrepResult<String> {
let data = std::fs::read(path).map_err(GrepError::Io)?;
extract_text(&data)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strip_html_tags() {
let html = "<html><body><p>Hello <b>World</b>!</p></body></html>";
let text = strip_html_tags(html);
assert_eq!(text, "Hello World!");
}
#[test]
fn test_strip_html_with_entities() {
let html = "<p>Hello World & Friends</p>";
let text = strip_html_tags(html);
assert_eq!(text, "Hello World & Friends");
}
#[test]
fn test_decode_html_entity() {
assert_eq!(decode_html_entity("&"), "&");
assert_eq!(decode_html_entity("<"), "<");
assert_eq!(decode_html_entity("A"), "A");
assert_eq!(decode_html_entity("A"), "A");
}
#[test]
fn test_extract_invalid_epub() {
let invalid_data = b"This is not an EPUB file";
let result = extract_text(invalid_data);
assert!(result.is_err());
}
}