1use docparser_core::error::{DocError, Result};
8use docparser_core::plugin::{ParseCapability, RawDoc, RawPage};
9use quick_xml::events::Event;
10use quick_xml::Reader;
11
12use crate::zip_read_str;
13
14pub struct DocxRawBackend;
16
17impl ParseCapability for DocxRawBackend {
18 fn id(&self) -> &str {
19 "office-raw"
20 }
21 fn formats(&self) -> &[&str] {
22 &["docx"]
23 }
24 fn parse(&self, path: &str) -> Result<RawDoc> {
25 let file = std::fs::File::open(path)
26 .map_err(|e| DocError::corrupt(format!("cannot open {path}: {e}")))?;
27 let mut zip = zip::ZipArchive::new(file)
28 .map_err(|e| DocError::corrupt(format!("ZIP open failed: {e}")))?;
29
30 let xml = zip_read_str(&mut zip, "word/document.xml")?
31 .ok_or_else(|| DocError::corrupt("missing word/document.xml"))?;
32
33 let items = docx_raw_text(&xml)?;
34
35 let page = RawPage {
36 name: None,
37 items,
38 ..Default::default()
39 };
40 Ok(RawDoc {
41 pages: vec![page],
42 ..Default::default()
43 })
44 }
45}
46
47fn docx_raw_text(xml: &str) -> Result<Vec<docparser_core::plugin::RawTextItem>> {
49 let mut reader = Reader::from_str(xml);
50 let mut buf = Vec::new();
52 let mut items = Vec::new();
53
54 loop {
56 match reader.read_event_into(&mut buf) {
57 Ok(Event::Start(ref e)) if e.name().local_name().as_ref() == crate::tags::W_BODY => {
58 break
59 }
60 Ok(Event::Eof) => return Err(DocError::corrupt("no w:body found")),
61 Err(e) => return Err(DocError::corrupt(format!("XML parse error: {e}"))),
62 _ => {}
63 }
64 buf.clear();
65 }
66 buf.clear();
67
68 loop {
70 match reader.read_event_into(&mut buf) {
71 Ok(Event::Start(ref e)) => {
72 let name = e.name().local_name();
73 let name = name.as_ref();
74 if name == crate::tags::W_P {
75 let t = extract_para_text(&mut reader)?;
76 let trimmed = t.trim().to_string();
77 if !trimmed.is_empty() {
78 items.push(docparser_core::plugin::RawTextItem {
79 text: trimmed,
80 bbox: None,
81 });
82 }
83 } else if name == crate::tags::W_TBL {
84 let rows = extract_table_rows(&mut reader)?;
85 if !rows.is_empty() {
86 let t = rows
87 .iter()
88 .map(|r| r.join(","))
89 .collect::<Vec<_>>()
90 .join("\n");
91 items.push(docparser_core::plugin::RawTextItem {
92 text: t,
93 bbox: None,
94 });
95 }
96 } else if name == crate::tags::W_SDT {
97 extract_sdt_items(&mut reader, &mut items)?;
98 }
99 }
100 Ok(Event::End(ref e)) => {
101 if e.name().local_name().as_ref() == crate::tags::W_BODY {
102 break;
103 }
104 }
105 Ok(Event::Eof) => break,
106 Err(e) => return Err(DocError::corrupt(format!("XML parse error: {e}"))),
107 _ => {}
108 }
109 buf.clear();
110 }
111
112 Ok(items)
113}
114
115fn extract_sdt_items<R: std::io::BufRead>(
117 reader: &mut Reader<R>,
118 items: &mut Vec<docparser_core::plugin::RawTextItem>,
119) -> Result<()> {
120 let mut buf = Vec::new();
121 let mut depth = 1;
122 loop {
123 match reader.read_event_into(&mut buf) {
124 Ok(Event::Start(ref e)) => {
125 depth += 1;
126 if e.name().local_name().as_ref() == crate::tags::W_P {
127 let t = extract_para_text(reader)?;
128 let trimmed = t.trim().to_string();
129 if !trimmed.is_empty() {
130 items.push(docparser_core::plugin::RawTextItem {
131 text: trimmed,
132 bbox: None,
133 });
134 }
135 }
136 }
137 Ok(Event::End(ref _e)) => {
138 depth -= 1;
139 if depth == 0 {
140 break;
141 }
142 }
143 Ok(Event::Eof) => break,
144 Err(e) => return Err(DocError::corrupt(format!("XML sdt parse error: {e}"))),
145 _ => {}
146 }
147 buf.clear();
148 }
149 Ok(())
150}
151
152fn extract_para_text<R: std::io::BufRead>(reader: &mut Reader<R>) -> Result<String> {
155 let mut out = String::new();
156 let mut buf = Vec::new();
157 let mut depth = 1;
158
159 loop {
160 match reader.read_event_into(&mut buf) {
161 Ok(Event::Start(ref e)) => {
162 depth += 1;
163 match e.name().local_name().as_ref() {
164 crate::tags::W_TAB => out.push('\t'),
165 crate::tags::W_BR | crate::tags::W_CR => out.push('\n'),
166 _ => {}
167 }
168 }
169 Ok(Event::End(ref _e)) => {
170 depth -= 1;
171 if depth == 0 {
172 break;
173 }
174 }
175 Ok(Event::Empty(ref e)) => match e.name().local_name().as_ref() {
176 crate::tags::W_TAB => out.push('\t'),
177 crate::tags::W_BR | crate::tags::W_CR => out.push('\n'),
178 _ => {}
179 },
180 Ok(Event::Text(ref e)) => {
181 let raw = e.xml10_content();
184 if !raw.trim().is_empty() {
185 if let Ok(t) = quick_xml::escape::unescape(&raw) {
186 out.push_str(&t);
187 }
188 }
189 }
190 Ok(Event::GeneralRef(ref e)) => crate::push_general_ref(&mut out, e),
191 Ok(Event::CData(ref e)) => {
192 out.push_str(e.as_ref());
194 }
195 Err(e) => return Err(DocError::corrupt(format!("XML para parse error: {e}"))),
196 _ => {}
197 }
198 buf.clear();
199 }
200 Ok(out)
201}
202
203fn extract_table_rows<R: std::io::BufRead>(reader: &mut Reader<R>) -> Result<Vec<Vec<String>>> {
206 let mut rows: Vec<Vec<String>> = Vec::new();
207 let mut current_row: Vec<String> = Vec::new();
208 let mut current_cell = String::new();
209 let mut tbl_depth = 1;
210 let mut buf = Vec::new();
211 let mut cell_has_content = false;
212
213 loop {
214 match reader.read_event_into(&mut buf) {
215 Ok(Event::Start(ref e)) => {
216 tbl_depth += 1;
217 match e.name().local_name().as_ref() {
218 crate::tags::W_P => {
219 let t = extract_para_text(reader)?;
221 tbl_depth -= 1;
222 if !t.trim().is_empty() {
223 if cell_has_content {
224 current_cell.push('\n');
225 }
226 current_cell.push_str(t.trim());
227 cell_has_content = true;
228 }
229 continue;
230 }
231 crate::tags::W_TBL => {
232 let nested = extract_table_rows(reader)?;
234 tbl_depth -= 1;
235 if !nested.is_empty() {
236 if cell_has_content {
237 current_cell.push('\n');
238 }
239 current_cell.push_str(
240 &nested
241 .iter()
242 .map(|r| r.join(","))
243 .collect::<Vec<_>>()
244 .join("\n"),
245 );
246 cell_has_content = true;
247 }
248 continue;
249 }
250 _ => {}
251 }
252 }
253 Ok(Event::End(ref e)) => {
254 tbl_depth -= 1;
255 match e.name().local_name().as_ref() {
256 crate::tags::W_TBL => {
257 if tbl_depth == 0 {
258 if !current_row.is_empty() {
259 rows.push(std::mem::take(&mut current_row));
260 }
261 break;
262 }
263 }
264 crate::tags::W_TR => {
265 if !current_row.is_empty() {
267 rows.push(std::mem::take(&mut current_row));
268 }
269 }
270 crate::tags::W_TC => {
271 if cell_has_content {
273 current_row.push(current_cell.trim().to_string());
274 } else {
275 current_row.push(String::new());
276 }
277 current_cell.clear();
278 cell_has_content = false;
279 }
280 _ => {}
281 }
282 }
283 Ok(Event::Eof) => break,
284 Err(e) => return Err(DocError::corrupt(format!("XML table parse error: {e}"))),
285 _ => {}
286 }
287 buf.clear();
288 }
289 Ok(rows)
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn test_para_simple() {
298 let xml = r#"<w:body xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
299 <w:p><w:r><w:t>Hello</w:t></w:r></w:p>
300</w:body>"#;
301 let items = docx_raw_text(xml).unwrap();
302 assert_eq!(items.len(), 1);
303 assert_eq!(items[0].text, "Hello");
304 }
305
306 #[test]
307 fn test_para_tab_br() {
308 let xml = concat!(
309 r#"<w:body xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">"#,
310 r#" <w:p><w:r><w:t>a</w:t></w:r><w:tab/><w:r><w:t>b</w:t></w:r><w:br/><w:r><w:t>c</w:t></w:r></w:p>"#,
311 r#"</w:body>"#
312 );
313 let items = docx_raw_text(xml).unwrap();
314 assert_eq!(items.len(), 1);
315 assert_eq!(items[0].text, "a\tb\nc");
316 }
317
318 #[test]
319 fn test_simple_table() {
320 let xml = concat!(
321 r#"<w:body xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">"#,
322 r#" <w:tbl>"#,
323 r#" <w:tr><w:tc><w:p><w:r><w:t>A</w:t></w:r></w:p></w:tc><w:tc><w:p><w:r><w:t>B</w:t></w:r></w:p></w:tc></w:tr>"#,
324 r#" </w:tbl>"#,
325 r#"</w:body>"#
326 );
327 let items = docx_raw_text(xml).unwrap();
328 assert_eq!(items.len(), 1);
329 assert_eq!(items[0].text, "A,B");
330 }
331
332 #[test]
333 fn test_nested_table() {
334 let xml = concat!(
335 r#"<w:body xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">"#,
336 r#" <w:tbl>"#,
337 r#" <w:tr>"#,
338 r#" <w:tc><w:p><w:r><w:t>X</w:t></w:r></w:p></w:tc>"#,
339 r#" <w:tc>"#,
340 r#" <w:p><w:r><w:t>Y</w:t></w:r></w:p>"#,
341 r#" <w:tbl>"#,
342 r#" <w:tr><w:tc><w:p><w:r><w:t>innerA</w:t></w:r></w:p></w:tc>"#,
343 r#" <w:tc><w:p><w:r><w:t>innerB</w:t></w:r></w:p></w:tc></w:tr>"#,
344 r#" </w:tbl>"#,
345 r#" </w:tc>"#,
346 r#" </w:tr>"#,
347 r#" </w:tbl>"#,
348 r#"</w:body>"#
349 );
350 let items = docx_raw_text(xml).unwrap();
351 assert_eq!(items.len(), 1);
352 assert!(items[0].text.contains("innerA"));
353 assert!(items[0].text.contains("innerB"));
354 }
355
356 #[test]
357 fn test_multi_para() {
358 let xml = concat!(
359 r#"<w:body xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">"#,
360 r#" <w:p><w:r><w:t>Para1</w:t></w:r></w:p>"#,
361 r#" <w:p><w:r><w:t>Para2</w:t></w:r></w:p>"#,
362 r#"</w:body>"#
363 );
364 let items = docx_raw_text(xml).unwrap();
365 assert_eq!(items.len(), 2);
366 assert_eq!(items[0].text, "Para1");
367 assert_eq!(items[1].text, "Para2");
368 }
369
370 #[test]
375 fn smoke_parse_minimal_docx_zip() {
376 use std::io::Write;
377
378 const DOCUMENT_XML: &str = concat!(
379 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#,
380 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">"#,
381 r#"<w:body>"#,
382 r#"<w:p><w:r><w:t>Smoke 文本</w:t></w:r></w:p>"#,
383 r#"<w:p><w:r><w:t>a & b</w:t></w:r></w:p>"#,
384 r#"</w:body></w:document>"#
385 );
386
387 let dir = std::env::temp_dir().join("docparser_office_docx_smoke");
388 std::fs::create_dir_all(&dir).unwrap();
389 let path = dir.join("min.docx");
390 {
391 let f = std::fs::File::create(&path).unwrap();
392 let mut zw = zip::ZipWriter::new(f);
393 let opts = zip::write::SimpleFileOptions::default();
394 zw.start_file("word/document.xml", opts).unwrap();
395 zw.write_all(DOCUMENT_XML.as_bytes()).unwrap();
396 zw.finish().unwrap();
397 }
398
399 let raw = DocxRawBackend.parse(path.to_str().unwrap()).unwrap();
400 std::fs::remove_file(&path).ok();
401
402 assert_eq!(raw.pages.len(), 1);
403 assert_eq!(raw.pages[0].items.len(), 2);
404 assert_eq!(raw.pages[0].items[0].text, "Smoke 文本");
405 assert_eq!(raw.pages[0].items[1].text, "a & b");
406 }
407}