use crate::error::{DocError, ErrorCode, Result};
use crate::model::Document;
use crate::policy::BackendPolicy;
use crate::registry::Registry;
use crate::router;
pub struct DocumentEngine {
pub registry: Registry,
pub policy: BackendPolicy,
}
impl DocumentEngine {
pub fn new(registry: Registry) -> Self {
Self {
registry,
policy: BackendPolicy::default(),
}
}
pub fn with_policy(mut self, policy: BackendPolicy) -> Self {
self.policy = policy;
self
}
pub fn parse(&self, path: &str) -> Result<Document> {
let fmt = router::sniff(path)?;
let plugin = self
.registry
.plugin_for(fmt)
.ok_or_else(|| DocError::unsupported(format!("没有可处理该格式的插件: {fmt}")))?;
let caps =
self.registry
.resolve_parse_chain(fmt, &self.policy, &plugin.default_parse_chain());
let mut raw = None;
let mut last_err: Option<DocError> = None;
for cap in caps {
match cap.parse(path) {
Ok(r) => {
raw = Some(r);
break;
}
Err(e) => {
if matches!(e.code, ErrorCode::Encrypted | ErrorCode::Unsupported) {
return Err(e);
}
last_err = Some(e);
}
}
}
let mut raw = raw.ok_or_else(|| {
last_err.unwrap_or_else(|| DocError::corrupt(format!("全部解析后端失败: {fmt}")))
})?;
let proc_chain = self
.registry
.resolve_process_chain(&self.policy, &plugin.default_process_chain());
let mut warnings = Vec::new();
for proc in proc_chain {
if proc.should_apply(&raw) {
if let Err(e) = proc.process(&mut raw) {
warnings.push(format!("{}: {e}", proc.id()));
}
}
}
if !warnings.is_empty() {
raw.meta.insert("warnings".into(), warnings.join("; "));
}
let mut doc = plugin.assemble(&raw);
doc.meta.file_type = fmt.to_string();
if let Some(w) = raw.meta.get("warnings") {
doc.meta.extra.insert("warnings".to_string(), w.clone());
}
Ok(doc.finalize())
}
}
#[allow(dead_code)]
fn _code(e: &DocError) -> ErrorCode {
e.code
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugins::text::register_defaults;
fn engine() -> DocumentEngine {
let mut reg = Registry::new();
register_defaults(&mut reg);
DocumentEngine::new(reg)
}
fn write_tmp(name: &str, content: &str) -> String {
let p = std::env::temp_dir().join(format!("docparser_core_engine_{name}"));
std::fs::write(&p, content).unwrap();
p.to_string_lossy().into_owned()
}
#[test]
fn parse_txt_end_to_end() {
let p = write_tmp("a.txt", "line1\n\nline2\n");
let doc = engine().parse(&p).unwrap();
std::fs::remove_file(&p).ok();
assert_eq!(doc.meta.file_type, "txt");
assert_eq!(doc.meta.text_length, 12); assert_eq!(doc.pages.len(), 1);
assert_eq!(doc.pages[0].text, "line1\n\nline2");
assert_eq!(doc.pages[0].blocks.len(), 2); }
#[test]
fn sniffed_unsupported_raises_code() {
let mut reg = Registry::new();
register_defaults(&mut reg);
let eng = DocumentEngine::new(reg);
let p = write_tmp("fake.doc", "not an ole file\n");
let err = eng.parse(&p).unwrap_err();
std::fs::remove_file(&p).ok();
assert_eq!(err.code, ErrorCode::Unsupported);
}
#[test]
#[cfg(feature = "md")]
fn parse_markdown_end_to_end() {
let p = write_tmp("a.md", "# 标题\n\n- 甲\n- 乙\n\n```py\nprint(1)\n```\n");
let doc = engine().parse(&p).unwrap();
std::fs::remove_file(&p).ok();
assert_eq!(doc.meta.file_type, "md");
let kinds: Vec<crate::model::BlockKind> =
doc.pages[0].blocks.iter().map(|b| b.kind).collect();
assert_eq!(
kinds,
vec![
crate::model::BlockKind::Heading,
crate::model::BlockKind::List,
crate::model::BlockKind::Code
]
);
assert_eq!(doc.pages[0].blocks[0].level, Some(1));
assert_eq!(doc.pages[0].blocks[1].ordered, Some(false));
}
#[test]
#[cfg(feature = "csv")]
fn parse_csv_end_to_end() {
let p = write_tmp("a.csv", "h1,h2\n\"a,1\",2\n");
let doc = engine().parse(&p).unwrap();
std::fs::remove_file(&p).ok();
assert_eq!(doc.meta.file_type, "csv");
let table = doc.pages[0].blocks[0].table.as_ref().unwrap();
assert_eq!(table.rows[1], vec!["a,1", "2"]);
}
#[test]
#[cfg(feature = "html")]
fn parse_html_end_to_end() {
let p = write_tmp(
"a.html",
"<h2>小节</h2><p>正文</p><table><tr><td>x</td></tr></table>",
);
let doc = engine().parse(&p).unwrap();
std::fs::remove_file(&p).ok();
assert_eq!(doc.meta.file_type, "html");
let kinds: Vec<crate::model::BlockKind> =
doc.pages[0].blocks.iter().map(|b| b.kind).collect();
assert_eq!(
kinds,
vec![
crate::model::BlockKind::Heading,
crate::model::BlockKind::Paragraph,
crate::model::BlockKind::Table
]
);
assert_eq!(doc.pages[0].blocks[0].level, Some(2));
}
}