Skip to main content

docparser_core/
engine.rs

1//! 文档引擎:sniff → 解析链(回退保留原始错误码)→ 处理链(失败仅记录)→ assemble。
2
3use crate::error::{DocError, ErrorCode, Result};
4use crate::model::Document;
5use crate::policy::BackendPolicy;
6use crate::registry::Registry;
7use crate::router;
8
9/// 文档引擎:注册表 + 策略。
10pub struct DocumentEngine {
11    pub registry: Registry,
12    pub policy: BackendPolicy,
13}
14
15impl DocumentEngine {
16    pub fn new(registry: Registry) -> Self {
17        Self {
18            registry,
19            policy: BackendPolicy::default(),
20        }
21    }
22
23    pub fn with_policy(mut self, policy: BackendPolicy) -> Self {
24        self.policy = policy;
25        self
26    }
27
28    pub fn parse(&self, path: &str) -> Result<Document> {
29        let fmt = router::sniff(path)?;
30        let plugin = self
31            .registry
32            .plugin_for(fmt)
33            .ok_or_else(|| DocError::unsupported(format!("没有可处理该格式的插件: {fmt}")))?;
34
35        // ① 解析链:按序尝试,失败保留原始已分类错误(不吞成 corrupt)
36        let caps =
37            self.registry
38                .resolve_parse_chain(fmt, &self.policy, &plugin.default_parse_chain());
39        let mut raw = None;
40        let mut last_err: Option<DocError> = None;
41        for cap in caps {
42            match cap.parse(path) {
43                Ok(r) => {
44                    raw = Some(r);
45                    break;
46                }
47                Err(e) => {
48                    // 终止码不回退:encrypted/unsupported 是确定性结论,
49                    // 不能被兜底后端(如 v1 对加密件的 corrupt)掩盖
50                    if matches!(e.code, ErrorCode::Encrypted | ErrorCode::Unsupported) {
51                        return Err(e);
52                    }
53                    last_err = Some(e);
54                }
55            }
56        }
57        let mut raw = raw.ok_or_else(|| {
58            last_err.unwrap_or_else(|| DocError::corrupt(format!("全部解析后端失败: {fmt}")))
59        })?;
60
61        // ② 处理链:就地增强(失败不致命,仅记录)
62        let proc_chain = self
63            .registry
64            .resolve_process_chain(&self.policy, &plugin.default_process_chain());
65        let mut warnings = Vec::new();
66        for proc in proc_chain {
67            if proc.should_apply(&raw) {
68                if let Err(e) = proc.process(&mut raw) {
69                    warnings.push(format!("{}: {e}", proc.id()));
70                }
71            }
72        }
73        if !warnings.is_empty() {
74            raw.meta.insert("warnings".into(), warnings.join("; "));
75        }
76
77        // ③ 组装 IR
78        let mut doc = plugin.assemble(&raw);
79        doc.meta.file_type = fmt.to_string();
80        // 处理链告警此前只留在 RawDoc.meta 里(随 raw 丢弃),落到 doc.meta.extra 才可见
81        if let Some(w) = raw.meta.get("warnings") {
82            doc.meta.extra.insert("warnings".to_string(), w.clone());
83        }
84        Ok(doc.finalize())
85    }
86}
87
88// 导出方便错误码检查(示例用)
89#[allow(dead_code)]
90fn _code(e: &DocError) -> ErrorCode {
91    e.code
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::plugins::text::register_defaults;
98
99    fn engine() -> DocumentEngine {
100        let mut reg = Registry::new();
101        register_defaults(&mut reg);
102        DocumentEngine::new(reg)
103    }
104
105    fn write_tmp(name: &str, content: &str) -> String {
106        let p = std::env::temp_dir().join(format!("docparser_core_engine_{name}"));
107        std::fs::write(&p, content).unwrap();
108        p.to_string_lossy().into_owned()
109    }
110
111    #[test]
112    fn parse_txt_end_to_end() {
113        let p = write_tmp("a.txt", "line1\n\nline2\n");
114        let doc = engine().parse(&p).unwrap();
115        std::fs::remove_file(&p).ok();
116        assert_eq!(doc.meta.file_type, "txt");
117        assert_eq!(doc.meta.text_length, 12); // "line1\n\nline2"
118        assert_eq!(doc.pages.len(), 1);
119        assert_eq!(doc.pages[0].text, "line1\n\nline2");
120        assert_eq!(doc.pages[0].blocks.len(), 2); // 空行不作为块
121    }
122
123    #[test]
124    fn sniffed_unsupported_raises_code() {
125        let mut reg = Registry::new();
126        register_defaults(&mut reg);
127        let eng = DocumentEngine::new(reg);
128        let p = write_tmp("fake.doc", "not an ole file\n");
129        let err = eng.parse(&p).unwrap_err();
130        std::fs::remove_file(&p).ok();
131        assert_eq!(err.code, ErrorCode::Unsupported);
132    }
133
134    #[test]
135    #[cfg(feature = "md")]
136    fn parse_markdown_end_to_end() {
137        let p = write_tmp("a.md", "# 标题\n\n- 甲\n- 乙\n\n```py\nprint(1)\n```\n");
138        let doc = engine().parse(&p).unwrap();
139        std::fs::remove_file(&p).ok();
140        assert_eq!(doc.meta.file_type, "md");
141        let kinds: Vec<crate::model::BlockKind> =
142            doc.pages[0].blocks.iter().map(|b| b.kind).collect();
143        assert_eq!(
144            kinds,
145            vec![
146                crate::model::BlockKind::Heading,
147                crate::model::BlockKind::List,
148                crate::model::BlockKind::Code
149            ]
150        );
151        assert_eq!(doc.pages[0].blocks[0].level, Some(1));
152        assert_eq!(doc.pages[0].blocks[1].ordered, Some(false));
153    }
154
155    #[test]
156    #[cfg(feature = "csv")]
157    fn parse_csv_end_to_end() {
158        let p = write_tmp("a.csv", "h1,h2\n\"a,1\",2\n");
159        let doc = engine().parse(&p).unwrap();
160        std::fs::remove_file(&p).ok();
161        assert_eq!(doc.meta.file_type, "csv");
162        let table = doc.pages[0].blocks[0].table.as_ref().unwrap();
163        assert_eq!(table.rows[1], vec!["a,1", "2"]);
164    }
165
166    #[test]
167    #[cfg(feature = "html")]
168    fn parse_html_end_to_end() {
169        let p = write_tmp(
170            "a.html",
171            "<h2>小节</h2><p>正文</p><table><tr><td>x</td></tr></table>",
172        );
173        let doc = engine().parse(&p).unwrap();
174        std::fs::remove_file(&p).ok();
175        assert_eq!(doc.meta.file_type, "html");
176        let kinds: Vec<crate::model::BlockKind> =
177            doc.pages[0].blocks.iter().map(|b| b.kind).collect();
178        assert_eq!(
179            kinds,
180            vec![
181                crate::model::BlockKind::Heading,
182                crate::model::BlockKind::Paragraph,
183                crate::model::BlockKind::Table
184            ]
185        );
186        assert_eq!(doc.pages[0].blocks[0].level, Some(2));
187    }
188}