1use crate::error::{DocError, ErrorCode, Result};
4use crate::model::Document;
5use crate::policy::BackendPolicy;
6use crate::registry::Registry;
7use crate::router;
8
9pub 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 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 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 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 let mut doc = plugin.assemble(&raw);
79 doc.meta.file_type = fmt.to_string();
80 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#[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); 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); }
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}