use cp_core::{text, CPError, Result};
use std::path::Path;
pub trait Parser: Send + Sync {
fn parse(&self, path: &Path) -> Result<String>;
fn supported_extensions(&self) -> &[&str];
}
pub struct ParserRegistry {
parsers: Vec<Box<dyn Parser>>,
}
impl Default for ParserRegistry {
fn default() -> Self {
Self::new()
}
}
impl ParserRegistry {
pub fn new() -> Self {
Self {
parsers: vec![
Box::new(MarkdownParser),
Box::new(TextParser),
Box::new(PdfParser),
Box::new(DocxParser),
],
}
}
pub fn find_parser(&self, extension: &str) -> Option<&dyn Parser> {
for parser in &self.parsers {
if parser
.supported_extensions()
.iter()
.any(|e| e.eq_ignore_ascii_case(extension))
{
return Some(parser.as_ref());
}
}
None
}
}
pub fn parse_file(path: &Path) -> Result<String> {
let registry = ParserRegistry::new();
let extension = path
.extension()
.and_then(|e| e.to_str())
.ok_or_else(|| CPError::Parse("No file extension".into()))?;
let parser = registry
.find_parser(extension)
.ok_or_else(|| CPError::Parse(format!("No parser for extension: {extension}")))?;
parser.parse(path)
}
struct MarkdownParser;
impl Parser for MarkdownParser {
fn parse(&self, path: &Path) -> Result<String> {
let content = std::fs::read_to_string(path)?;
let parser = pulldown_cmark::Parser::new(&content);
let mut text = String::new();
for event in parser {
match event {
pulldown_cmark::Event::Start(pulldown_cmark::Tag::Heading { level, .. }) => {
text.push('\n');
let level_str = match level {
pulldown_cmark::HeadingLevel::H1 => "# ",
pulldown_cmark::HeadingLevel::H2 => "## ",
pulldown_cmark::HeadingLevel::H3 => "### ",
pulldown_cmark::HeadingLevel::H4 => "#### ",
pulldown_cmark::HeadingLevel::H5 => "##### ",
pulldown_cmark::HeadingLevel::H6 => "###### ",
};
text.push_str(level_str);
}
pulldown_cmark::Event::End(pulldown_cmark::TagEnd::Heading(_))
| pulldown_cmark::Event::SoftBreak
| pulldown_cmark::Event::HardBreak => {
text.push('\n');
}
pulldown_cmark::Event::Text(t) | pulldown_cmark::Event::Code(t) => {
text.push_str(&t);
}
pulldown_cmark::Event::End(pulldown_cmark::TagEnd::Paragraph) => {
text.push_str("\n\n");
}
_ => {}
}
}
Ok(text::normalize(&text))
}
fn supported_extensions(&self) -> &[&str] {
&["md", "markdown"]
}
}
struct TextParser;
impl Parser for TextParser {
fn parse(&self, path: &Path) -> Result<String> {
let content = std::fs::read_to_string(path)?;
Ok(text::normalize(&content))
}
fn supported_extensions(&self) -> &[&str] {
&["txt", "text"]
}
}
struct PdfParser;
impl Parser for PdfParser {
fn parse(&self, path: &Path) -> Result<String> {
let bytes = std::fs::read(path)?;
let text = pdf_extract::extract_text_from_mem(&bytes)
.map_err(|e| CPError::Parse(format!("PDF extraction failed: {e}")))?;
Ok(text::normalize(&text))
}
fn supported_extensions(&self) -> &[&str] {
&["pdf"]
}
}
struct DocxParser;
impl Parser for DocxParser {
fn parse(&self, path: &Path) -> Result<String> {
use dotext::MsDoc;
use std::io::Read;
let mut doc: dotext::Docx = dotext::Docx::open(path)
.map_err(|e| CPError::Parse(format!("DOCX open failed: {e}")))?;
let mut content = String::new();
doc.read_to_string(&mut content)
.map_err(|e| CPError::Parse(format!("DOCX read failed: {e}")))?;
Ok(text::normalize(&content))
}
fn supported_extensions(&self) -> &[&str] {
&["docx"]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_registry_finds_markdown() {
let registry = ParserRegistry::new();
assert!(registry.find_parser("md").is_some());
assert!(registry.find_parser("markdown").is_some());
}
#[test]
fn test_registry_finds_pdf() {
let registry = ParserRegistry::new();
assert!(registry.find_parser("pdf").is_some());
}
#[test]
fn test_unknown_extension() {
let registry = ParserRegistry::new();
assert!(registry.find_parser("xyz").is_none());
}
}