kreuzberg 4.0.0.pre.rc.6 → 4.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -6
  3. data/.rubocop.yaml +534 -1
  4. data/Gemfile +2 -1
  5. data/Gemfile.lock +11 -11
  6. data/README.md +5 -10
  7. data/examples/async_patterns.rb +0 -1
  8. data/ext/kreuzberg_rb/extconf.rb +0 -10
  9. data/ext/kreuzberg_rb/native/Cargo.toml +15 -23
  10. data/ext/kreuzberg_rb/native/build.rs +2 -0
  11. data/ext/kreuzberg_rb/native/include/ieeefp.h +1 -1
  12. data/ext/kreuzberg_rb/native/include/msvc_compat/strings.h +1 -1
  13. data/ext/kreuzberg_rb/native/include/strings.h +2 -2
  14. data/ext/kreuzberg_rb/native/include/unistd.h +1 -1
  15. data/ext/kreuzberg_rb/native/src/lib.rs +16 -75
  16. data/kreuzberg.gemspec +14 -57
  17. data/lib/kreuzberg/cache_api.rb +0 -1
  18. data/lib/kreuzberg/cli.rb +2 -2
  19. data/lib/kreuzberg/config.rb +2 -9
  20. data/lib/kreuzberg/errors.rb +7 -75
  21. data/lib/kreuzberg/extraction_api.rb +0 -1
  22. data/lib/kreuzberg/setup_lib_path.rb +0 -1
  23. data/lib/kreuzberg/version.rb +1 -1
  24. data/lib/kreuzberg.rb +0 -21
  25. data/pkg/kreuzberg-4.0.0.rc1.gem +0 -0
  26. data/sig/kreuzberg.rbs +3 -55
  27. data/spec/binding/cli_proxy_spec.rb +4 -2
  28. data/spec/binding/cli_spec.rb +11 -12
  29. data/spec/examples.txt +104 -0
  30. data/spec/fixtures/config.yaml +1 -0
  31. data/spec/spec_helper.rb +1 -1
  32. data/vendor/kreuzberg/Cargo.toml +42 -112
  33. data/vendor/kreuzberg/README.md +2 -2
  34. data/vendor/kreuzberg/build.rs +4 -18
  35. data/vendor/kreuzberg/src/bin/profile_extract.rs +455 -0
  36. data/vendor/kreuzberg/src/cache/mod.rs +3 -27
  37. data/vendor/kreuzberg/src/core/batch_mode.rs +0 -60
  38. data/vendor/kreuzberg/src/core/extractor.rs +81 -202
  39. data/vendor/kreuzberg/src/core/io.rs +2 -4
  40. data/vendor/kreuzberg/src/core/mime.rs +12 -2
  41. data/vendor/kreuzberg/src/core/mod.rs +1 -4
  42. data/vendor/kreuzberg/src/core/pipeline.rs +33 -111
  43. data/vendor/kreuzberg/src/embeddings.rs +16 -125
  44. data/vendor/kreuzberg/src/error.rs +1 -1
  45. data/vendor/kreuzberg/src/extraction/docx.rs +1 -1
  46. data/vendor/kreuzberg/src/extraction/image.rs +13 -13
  47. data/vendor/kreuzberg/src/extraction/libreoffice.rs +1 -0
  48. data/vendor/kreuzberg/src/extraction/mod.rs +5 -9
  49. data/vendor/kreuzberg/src/extraction/office_metadata/mod.rs +0 -2
  50. data/vendor/kreuzberg/src/extraction/pandoc/batch.rs +275 -0
  51. data/vendor/kreuzberg/src/extraction/pandoc/mime_types.rs +178 -0
  52. data/vendor/kreuzberg/src/extraction/pandoc/mod.rs +491 -0
  53. data/vendor/kreuzberg/src/extraction/pandoc/server.rs +496 -0
  54. data/vendor/kreuzberg/src/extraction/pandoc/subprocess.rs +1188 -0
  55. data/vendor/kreuzberg/src/extraction/pandoc/version.rs +162 -0
  56. data/vendor/kreuzberg/src/extractors/archive.rs +0 -21
  57. data/vendor/kreuzberg/src/extractors/docx.rs +128 -16
  58. data/vendor/kreuzberg/src/extractors/email.rs +0 -14
  59. data/vendor/kreuzberg/src/extractors/excel.rs +20 -19
  60. data/vendor/kreuzberg/src/extractors/html.rs +154 -137
  61. data/vendor/kreuzberg/src/extractors/image.rs +4 -7
  62. data/vendor/kreuzberg/src/extractors/mod.rs +9 -106
  63. data/vendor/kreuzberg/src/extractors/pandoc.rs +201 -0
  64. data/vendor/kreuzberg/src/extractors/pdf.rs +15 -12
  65. data/vendor/kreuzberg/src/extractors/pptx.rs +3 -17
  66. data/vendor/kreuzberg/src/extractors/structured.rs +0 -14
  67. data/vendor/kreuzberg/src/extractors/text.rs +5 -23
  68. data/vendor/kreuzberg/src/extractors/xml.rs +0 -7
  69. data/vendor/kreuzberg/src/keywords/rake.rs +1 -0
  70. data/vendor/kreuzberg/src/lib.rs +1 -4
  71. data/vendor/kreuzberg/src/mcp/mod.rs +1 -1
  72. data/vendor/kreuzberg/src/mcp/server.rs +3 -5
  73. data/vendor/kreuzberg/src/ocr/processor.rs +2 -18
  74. data/vendor/kreuzberg/src/pdf/error.rs +1 -1
  75. data/vendor/kreuzberg/src/pdf/table.rs +44 -17
  76. data/vendor/kreuzberg/src/pdf/text.rs +3 -0
  77. data/vendor/kreuzberg/src/plugins/extractor.rs +5 -8
  78. data/vendor/kreuzberg/src/plugins/ocr.rs +11 -2
  79. data/vendor/kreuzberg/src/plugins/processor.rs +1 -2
  80. data/vendor/kreuzberg/src/plugins/registry.rs +0 -13
  81. data/vendor/kreuzberg/src/plugins/validator.rs +8 -9
  82. data/vendor/kreuzberg/src/stopwords/mod.rs +2 -2
  83. data/vendor/kreuzberg/src/types.rs +12 -42
  84. data/vendor/kreuzberg/tests/batch_orchestration.rs +5 -19
  85. data/vendor/kreuzberg/tests/batch_processing.rs +3 -15
  86. data/vendor/kreuzberg/tests/chunking_offset_demo.rs +92 -0
  87. data/vendor/kreuzberg/tests/concurrency_stress.rs +1 -17
  88. data/vendor/kreuzberg/tests/config_features.rs +0 -18
  89. data/vendor/kreuzberg/tests/config_loading_tests.rs +39 -15
  90. data/vendor/kreuzberg/tests/core_integration.rs +7 -24
  91. data/vendor/kreuzberg/tests/csv_integration.rs +81 -71
  92. data/vendor/kreuzberg/tests/docx_metadata_extraction_test.rs +25 -23
  93. data/vendor/kreuzberg/tests/pandoc_integration.rs +503 -0
  94. data/vendor/kreuzberg/tests/pipeline_integration.rs +1 -0
  95. data/vendor/kreuzberg/tests/plugin_postprocessor_test.rs +1 -0
  96. data/vendor/kreuzberg/tests/registry_integration_tests.rs +22 -1
  97. data/vendor/kreuzberg/tests/security_validation.rs +1 -12
  98. metadata +25 -90
  99. data/.rubocop.yml +0 -538
  100. data/ext/kreuzberg_rb/native/Cargo.lock +0 -6535
  101. data/lib/kreuzberg/error_context.rb +0 -32
  102. data/vendor/kreuzberg/benches/otel_overhead.rs +0 -48
  103. data/vendor/kreuzberg/src/extraction/markdown.rs +0 -213
  104. data/vendor/kreuzberg/src/extraction/office_metadata/odt_properties.rs +0 -287
  105. data/vendor/kreuzberg/src/extractors/bibtex.rs +0 -469
  106. data/vendor/kreuzberg/src/extractors/docbook.rs +0 -502
  107. data/vendor/kreuzberg/src/extractors/epub.rs +0 -707
  108. data/vendor/kreuzberg/src/extractors/fictionbook.rs +0 -491
  109. data/vendor/kreuzberg/src/extractors/fictionbook.rs.backup2 +0 -738
  110. data/vendor/kreuzberg/src/extractors/jats.rs +0 -1051
  111. data/vendor/kreuzberg/src/extractors/jupyter.rs +0 -367
  112. data/vendor/kreuzberg/src/extractors/latex.rs +0 -652
  113. data/vendor/kreuzberg/src/extractors/markdown.rs +0 -700
  114. data/vendor/kreuzberg/src/extractors/odt.rs +0 -628
  115. data/vendor/kreuzberg/src/extractors/opml.rs +0 -634
  116. data/vendor/kreuzberg/src/extractors/orgmode.rs +0 -528
  117. data/vendor/kreuzberg/src/extractors/rst.rs +0 -576
  118. data/vendor/kreuzberg/src/extractors/rtf.rs +0 -810
  119. data/vendor/kreuzberg/src/extractors/security.rs +0 -484
  120. data/vendor/kreuzberg/src/extractors/security_tests.rs +0 -367
  121. data/vendor/kreuzberg/src/extractors/typst.rs +0 -650
  122. data/vendor/kreuzberg/src/panic_context.rs +0 -154
  123. data/vendor/kreuzberg/tests/api_extract_multipart.rs +0 -52
  124. data/vendor/kreuzberg/tests/bibtex_parity_test.rs +0 -421
  125. data/vendor/kreuzberg/tests/docbook_extractor_tests.rs +0 -498
  126. data/vendor/kreuzberg/tests/docx_vs_pandoc_comparison.rs +0 -370
  127. data/vendor/kreuzberg/tests/epub_native_extractor_tests.rs +0 -275
  128. data/vendor/kreuzberg/tests/fictionbook_extractor_tests.rs +0 -228
  129. data/vendor/kreuzberg/tests/html_table_test.rs +0 -551
  130. data/vendor/kreuzberg/tests/instrumentation_test.rs +0 -139
  131. data/vendor/kreuzberg/tests/jats_extractor_tests.rs +0 -639
  132. data/vendor/kreuzberg/tests/jupyter_extractor_tests.rs +0 -704
  133. data/vendor/kreuzberg/tests/latex_extractor_tests.rs +0 -496
  134. data/vendor/kreuzberg/tests/markdown_extractor_tests.rs +0 -490
  135. data/vendor/kreuzberg/tests/odt_extractor_tests.rs +0 -695
  136. data/vendor/kreuzberg/tests/opml_extractor_tests.rs +0 -616
  137. data/vendor/kreuzberg/tests/orgmode_extractor_tests.rs +0 -822
  138. data/vendor/kreuzberg/tests/rst_extractor_tests.rs +0 -692
  139. data/vendor/kreuzberg/tests/rtf_extractor_tests.rs +0 -776
  140. data/vendor/kreuzberg/tests/typst_behavioral_tests.rs +0 -1259
  141. data/vendor/kreuzberg/tests/typst_extractor_tests.rs +0 -647
  142. data/vendor/rb-sys/.cargo-ok +0 -1
  143. data/vendor/rb-sys/.cargo_vcs_info.json +0 -6
  144. data/vendor/rb-sys/Cargo.lock +0 -393
  145. data/vendor/rb-sys/Cargo.toml +0 -70
  146. data/vendor/rb-sys/Cargo.toml.orig +0 -57
  147. data/vendor/rb-sys/LICENSE-APACHE +0 -190
  148. data/vendor/rb-sys/LICENSE-MIT +0 -21
  149. data/vendor/rb-sys/bin/release.sh +0 -21
  150. data/vendor/rb-sys/build/features.rs +0 -108
  151. data/vendor/rb-sys/build/main.rs +0 -246
  152. data/vendor/rb-sys/build/stable_api_config.rs +0 -153
  153. data/vendor/rb-sys/build/version.rs +0 -48
  154. data/vendor/rb-sys/readme.md +0 -36
  155. data/vendor/rb-sys/src/bindings.rs +0 -21
  156. data/vendor/rb-sys/src/hidden.rs +0 -11
  157. data/vendor/rb-sys/src/lib.rs +0 -34
  158. data/vendor/rb-sys/src/macros.rs +0 -371
  159. data/vendor/rb-sys/src/memory.rs +0 -53
  160. data/vendor/rb-sys/src/ruby_abi_version.rs +0 -38
  161. data/vendor/rb-sys/src/special_consts.rs +0 -31
  162. data/vendor/rb-sys/src/stable_api/compiled.c +0 -179
  163. data/vendor/rb-sys/src/stable_api/compiled.rs +0 -257
  164. data/vendor/rb-sys/src/stable_api/ruby_2_6.rs +0 -316
  165. data/vendor/rb-sys/src/stable_api/ruby_2_7.rs +0 -316
  166. data/vendor/rb-sys/src/stable_api/ruby_3_0.rs +0 -324
  167. data/vendor/rb-sys/src/stable_api/ruby_3_1.rs +0 -317
  168. data/vendor/rb-sys/src/stable_api/ruby_3_2.rs +0 -315
  169. data/vendor/rb-sys/src/stable_api/ruby_3_3.rs +0 -326
  170. data/vendor/rb-sys/src/stable_api/ruby_3_4.rs +0 -327
  171. data/vendor/rb-sys/src/stable_api.rs +0 -261
  172. data/vendor/rb-sys/src/symbol.rs +0 -31
  173. data/vendor/rb-sys/src/tracking_allocator.rs +0 -332
  174. data/vendor/rb-sys/src/utils.rs +0 -89
  175. data/vendor/rb-sys/src/value_type.rs +0 -7
@@ -1,628 +0,0 @@
1
- //! ODT (OpenDocument Text) extractor using native Rust parsing.
2
- //!
3
- //! Supports: OpenDocument Text (.odt)
4
-
5
- use crate::Result;
6
- use crate::core::config::ExtractionConfig;
7
- use crate::extraction::{cells_to_markdown, office_metadata};
8
- use crate::plugins::{DocumentExtractor, Plugin};
9
- use crate::types::{ExtractionResult, Metadata, Table};
10
- use async_trait::async_trait;
11
- use roxmltree::Document;
12
- use std::io::Cursor;
13
-
14
- /// High-performance ODT extractor using native Rust XML parsing.
15
- ///
16
- /// This extractor provides:
17
- /// - Fast text extraction via roxmltree XML parsing
18
- /// - Comprehensive metadata extraction from meta.xml
19
- /// - Table extraction with row and cell support
20
- /// - Formatting preservation (bold, italic, strikeout)
21
- /// - Support for headings, paragraphs, and special elements
22
- pub struct OdtExtractor;
23
-
24
- impl OdtExtractor {
25
- /// Create a new ODT extractor.
26
- pub fn new() -> Self {
27
- Self
28
- }
29
- }
30
-
31
- impl Default for OdtExtractor {
32
- fn default() -> Self {
33
- Self::new()
34
- }
35
- }
36
-
37
- impl Plugin for OdtExtractor {
38
- fn name(&self) -> &str {
39
- "odt-extractor"
40
- }
41
-
42
- fn version(&self) -> String {
43
- env!("CARGO_PKG_VERSION").to_string()
44
- }
45
-
46
- fn initialize(&self) -> Result<()> {
47
- Ok(())
48
- }
49
-
50
- fn shutdown(&self) -> Result<()> {
51
- Ok(())
52
- }
53
-
54
- fn description(&self) -> &str {
55
- "Native Rust ODT (OpenDocument Text) extractor with metadata and table support"
56
- }
57
-
58
- fn author(&self) -> &str {
59
- "Kreuzberg Team"
60
- }
61
- }
62
-
63
- /// Extract text from MathML formula element
64
- ///
65
- /// # Arguments
66
- /// * `math_node` - The math XML node
67
- ///
68
- /// # Returns
69
- /// * `Option<String>` - The extracted formula text
70
- fn extract_mathml_text(math_node: roxmltree::Node) -> Option<String> {
71
- for node in math_node.descendants() {
72
- if node.tag_name().name() == "annotation"
73
- && let Some(encoding) = node.attribute("encoding")
74
- && encoding.contains("StarMath")
75
- && let Some(text) = node.text()
76
- {
77
- return Some(text.to_string());
78
- }
79
- }
80
-
81
- let mut formula_parts = Vec::new();
82
- for node in math_node.descendants() {
83
- match node.tag_name().name() {
84
- "mi" | "mo" | "mn" | "ms" | "mtext" => {
85
- if let Some(text) = node.text() {
86
- formula_parts.push(text.to_string());
87
- }
88
- }
89
- _ => {}
90
- }
91
- }
92
-
93
- if !formula_parts.is_empty() {
94
- Some(formula_parts.join(" "))
95
- } else {
96
- None
97
- }
98
- }
99
-
100
- /// Extract text from embedded formula objects
101
- ///
102
- /// # Arguments
103
- /// * `archive` - ZIP archive containing the ODT document
104
- ///
105
- /// # Returns
106
- /// * `String` - Extracted formula content from embedded objects
107
- fn extract_embedded_formulas(archive: &mut zip::ZipArchive<Cursor<Vec<u8>>>) -> crate::error::Result<String> {
108
- use std::io::Read;
109
- let mut formula_parts = Vec::new();
110
-
111
- let file_names: Vec<String> = archive.file_names().map(|s| s.to_string()).collect();
112
-
113
- for file_name in file_names {
114
- if file_name.contains("Object")
115
- && file_name.ends_with("content.xml")
116
- && let Ok(mut file) = archive.by_name(&file_name)
117
- {
118
- let mut xml_content = String::new();
119
- if file.read_to_string(&mut xml_content).is_ok()
120
- && let Ok(doc) = Document::parse(&xml_content)
121
- {
122
- let root = doc.root_element();
123
-
124
- if root.tag_name().name() == "math" {
125
- if let Some(formula_text) = extract_mathml_text(root) {
126
- formula_parts.push(formula_text);
127
- }
128
- } else {
129
- for node in root.descendants() {
130
- if node.tag_name().name() == "math"
131
- && let Some(formula_text) = extract_mathml_text(node)
132
- {
133
- formula_parts.push(formula_text);
134
- }
135
- }
136
- }
137
- }
138
- }
139
- }
140
-
141
- Ok(formula_parts.join("\n"))
142
- }
143
-
144
- /// Extract text content from ODT content.xml
145
- ///
146
- /// # Arguments
147
- /// * `archive` - ZIP archive containing the ODT document
148
- ///
149
- /// # Returns
150
- /// * `String` - Extracted text content
151
- fn extract_content_text(archive: &mut zip::ZipArchive<Cursor<Vec<u8>>>) -> crate::error::Result<String> {
152
- let mut xml_content = String::new();
153
-
154
- match archive.by_name("content.xml") {
155
- Ok(mut file) => {
156
- use std::io::Read;
157
- file.read_to_string(&mut xml_content)
158
- .map_err(|e| crate::error::KreuzbergError::parsing(format!("Failed to read content.xml: {}", e)))?;
159
- }
160
- Err(_) => {
161
- return Ok(String::new());
162
- }
163
- }
164
-
165
- let doc = Document::parse(&xml_content)
166
- .map_err(|e| crate::error::KreuzbergError::parsing(format!("Failed to parse content.xml: {}", e)))?;
167
-
168
- let root = doc.root_element();
169
-
170
- let mut text_parts: Vec<String> = Vec::new();
171
-
172
- // Find the office:text or text body element - this is the main document body
173
- for body_child in root.children() {
174
- if body_child.tag_name().name() == "body" {
175
- // Process the text element inside body
176
- for text_elem in body_child.children() {
177
- if text_elem.tag_name().name() == "text" {
178
- // Now process only direct children of the text element
179
- process_document_elements(text_elem, &mut text_parts);
180
- }
181
- }
182
- }
183
- }
184
-
185
- Ok(text_parts.join("\n").trim().to_string())
186
- }
187
-
188
- /// Helper function to process document elements (paragraphs, headings, tables)
189
- /// Only processes direct children, avoiding nested content like table cells
190
- fn process_document_elements(parent: roxmltree::Node, text_parts: &mut Vec<String>) {
191
- for node in parent.children() {
192
- match node.tag_name().name() {
193
- "h" => {
194
- if let Some(text) = extract_node_text(node)
195
- && !text.trim().is_empty()
196
- {
197
- text_parts.push(format!("# {}", text.trim()));
198
- text_parts.push(String::new());
199
- }
200
- }
201
- "p" => {
202
- if let Some(text) = extract_node_text(node)
203
- && !text.trim().is_empty()
204
- {
205
- text_parts.push(text.trim().to_string());
206
- text_parts.push(String::new());
207
- }
208
- }
209
- "table" => {
210
- if let Some(table_text) = extract_table_text(node) {
211
- text_parts.push(table_text);
212
- text_parts.push(String::new());
213
- }
214
- }
215
- _ => {}
216
- }
217
- }
218
- }
219
-
220
- /// Extract text from a single XML node, handling spans and formatting
221
- ///
222
- /// # Arguments
223
- /// * `node` - The XML node to extract text from
224
- ///
225
- /// # Returns
226
- /// * `Option<String>` - The extracted text with formatting preserved
227
- fn extract_node_text(node: roxmltree::Node) -> Option<String> {
228
- let mut text_parts = Vec::new();
229
-
230
- for child in node.children() {
231
- match child.tag_name().name() {
232
- "span" => {
233
- if let Some(text) = child.text() {
234
- text_parts.push(text.to_string());
235
- }
236
- }
237
- "tab" => {
238
- text_parts.push("\t".to_string());
239
- }
240
- "line-break" => {
241
- text_parts.push("\n".to_string());
242
- }
243
- _ => {
244
- if let Some(text) = child.text() {
245
- text_parts.push(text.to_string());
246
- }
247
- }
248
- }
249
- }
250
-
251
- if text_parts.is_empty() {
252
- node.text().map(|s| s.to_string())
253
- } else {
254
- Some(text_parts.join(""))
255
- }
256
- }
257
-
258
- /// Extract table content as text with markdown formatting
259
- ///
260
- /// # Arguments
261
- /// * `table_node` - The table XML node
262
- ///
263
- /// # Returns
264
- /// * `Option<String>` - Markdown formatted table
265
- fn extract_table_text(table_node: roxmltree::Node) -> Option<String> {
266
- let mut rows = Vec::new();
267
- let mut max_cols = 0;
268
-
269
- for row_node in table_node.children() {
270
- if row_node.tag_name().name() == "table-row" {
271
- let mut row_cells = Vec::new();
272
-
273
- for cell_node in row_node.children() {
274
- if cell_node.tag_name().name() == "table-cell" {
275
- let cell_text = extract_node_text(cell_node).unwrap_or_default();
276
- row_cells.push(cell_text.trim().to_string());
277
- }
278
- }
279
-
280
- if !row_cells.is_empty() {
281
- max_cols = max_cols.max(row_cells.len());
282
- rows.push(row_cells);
283
- }
284
- }
285
- }
286
-
287
- if rows.is_empty() {
288
- return None;
289
- }
290
-
291
- for row in &mut rows {
292
- while row.len() < max_cols {
293
- row.push(String::new());
294
- }
295
- }
296
-
297
- let mut markdown = String::new();
298
-
299
- if !rows.is_empty() {
300
- markdown.push('|');
301
- for cell in &rows[0] {
302
- markdown.push(' ');
303
- markdown.push_str(cell);
304
- markdown.push_str(" |");
305
- }
306
- markdown.push('\n');
307
-
308
- markdown.push('|');
309
- for _ in 0..rows[0].len() {
310
- markdown.push_str(" --- |");
311
- }
312
- markdown.push('\n');
313
-
314
- for row in rows.iter().skip(1) {
315
- markdown.push('|');
316
- for cell in row {
317
- markdown.push(' ');
318
- markdown.push_str(cell);
319
- markdown.push_str(" |");
320
- }
321
- markdown.push('\n');
322
- }
323
- }
324
-
325
- Some(markdown)
326
- }
327
-
328
- /// Extract tables from ODT content.xml
329
- ///
330
- /// # Arguments
331
- /// * `archive` - ZIP archive containing the ODT document
332
- ///
333
- /// # Returns
334
- /// * `Result<Vec<Table>>` - Extracted tables
335
- fn extract_tables(archive: &mut zip::ZipArchive<Cursor<Vec<u8>>>) -> crate::error::Result<Vec<Table>> {
336
- let mut xml_content = String::new();
337
-
338
- match archive.by_name("content.xml") {
339
- Ok(mut file) => {
340
- use std::io::Read;
341
- file.read_to_string(&mut xml_content)
342
- .map_err(|e| crate::error::KreuzbergError::parsing(format!("Failed to read content.xml: {}", e)))?;
343
- }
344
- Err(_) => {
345
- return Ok(Vec::new());
346
- }
347
- }
348
-
349
- let doc = Document::parse(&xml_content)
350
- .map_err(|e| crate::error::KreuzbergError::parsing(format!("Failed to parse content.xml: {}", e)))?;
351
-
352
- let root = doc.root_element();
353
- let mut tables = Vec::new();
354
- let mut table_index = 0;
355
-
356
- for node in root.descendants() {
357
- if node.tag_name().name() == "table"
358
- && let Some(table) = parse_odt_table(node, table_index)
359
- {
360
- tables.push(table);
361
- table_index += 1;
362
- }
363
- }
364
-
365
- Ok(tables)
366
- }
367
-
368
- /// Parse a single ODT table element into a Table struct
369
- ///
370
- /// # Arguments
371
- /// * `table_node` - The table XML node
372
- /// * `table_index` - Index of the table in the document
373
- ///
374
- /// # Returns
375
- /// * `Option<Table>` - Parsed table
376
- fn parse_odt_table(table_node: roxmltree::Node, table_index: usize) -> Option<Table> {
377
- let mut cells: Vec<Vec<String>> = Vec::new();
378
-
379
- for row_node in table_node.children() {
380
- if row_node.tag_name().name() == "table-row" {
381
- let mut row_cells = Vec::new();
382
-
383
- for cell_node in row_node.children() {
384
- if cell_node.tag_name().name() == "table-cell" {
385
- let cell_text = extract_node_text(cell_node).unwrap_or_default();
386
- row_cells.push(cell_text.trim().to_string());
387
- }
388
- }
389
-
390
- if !row_cells.is_empty() {
391
- cells.push(row_cells);
392
- }
393
- }
394
- }
395
-
396
- if cells.is_empty() {
397
- return None;
398
- }
399
-
400
- let markdown = cells_to_markdown(&cells);
401
-
402
- Some(Table {
403
- cells,
404
- markdown,
405
- page_number: table_index + 1,
406
- })
407
- }
408
-
409
- #[async_trait]
410
- impl DocumentExtractor for OdtExtractor {
411
- #[cfg_attr(
412
- feature = "otel",
413
- tracing::instrument(
414
- skip(self, content, _config),
415
- fields(
416
- extractor.name = self.name(),
417
- content.size_bytes = content.len(),
418
- )
419
- )
420
- )]
421
- async fn extract_bytes(
422
- &self,
423
- content: &[u8],
424
- mime_type: &str,
425
- _config: &ExtractionConfig,
426
- ) -> Result<ExtractionResult> {
427
- let content_owned = content.to_vec();
428
-
429
- let (text, tables) = if crate::core::batch_mode::is_batch_mode() {
430
- let content_for_task = content_owned.clone();
431
- let span = tracing::Span::current();
432
- tokio::task::spawn_blocking(move || -> crate::error::Result<(String, Vec<Table>)> {
433
- let _guard = span.entered();
434
-
435
- let cursor = Cursor::new(content_for_task);
436
- let mut archive = zip::ZipArchive::new(cursor)
437
- .map_err(|e| crate::error::KreuzbergError::parsing(format!("Failed to open ZIP archive: {}", e)))?;
438
-
439
- let text = extract_content_text(&mut archive)?;
440
- let tables = extract_tables(&mut archive)?;
441
- let embedded_formulas = extract_embedded_formulas(&mut archive)?;
442
-
443
- let combined_text = if !embedded_formulas.is_empty() {
444
- if !text.is_empty() {
445
- format!("{}\n{}", text, embedded_formulas)
446
- } else {
447
- embedded_formulas
448
- }
449
- } else {
450
- text
451
- };
452
-
453
- Ok((combined_text, tables))
454
- })
455
- .await
456
- .map_err(|e| crate::error::KreuzbergError::parsing(format!("ODT extraction task failed: {}", e)))??
457
- } else {
458
- let cursor = Cursor::new(content_owned.clone());
459
- let mut archive = zip::ZipArchive::new(cursor)
460
- .map_err(|e| crate::error::KreuzbergError::parsing(format!("Failed to open ZIP archive: {}", e)))?;
461
-
462
- let text = extract_content_text(&mut archive)?;
463
- let tables = extract_tables(&mut archive)?;
464
- let embedded_formulas = extract_embedded_formulas(&mut archive)?;
465
-
466
- let combined_text = if !embedded_formulas.is_empty() {
467
- if !text.is_empty() {
468
- format!("{}\n{}", text, embedded_formulas)
469
- } else {
470
- embedded_formulas
471
- }
472
- } else {
473
- text
474
- };
475
-
476
- (combined_text, tables)
477
- };
478
-
479
- let mut metadata_map = std::collections::HashMap::new();
480
-
481
- let cursor = Cursor::new(content_owned.clone());
482
- let mut archive = zip::ZipArchive::new(cursor).map_err(|e| {
483
- crate::error::KreuzbergError::parsing(format!("Failed to open ZIP archive for metadata: {}", e))
484
- })?;
485
-
486
- if let Ok(odt_props) = office_metadata::extract_odt_properties(&mut archive) {
487
- if let Some(title) = odt_props.title {
488
- metadata_map.insert("title".to_string(), serde_json::Value::String(title));
489
- }
490
- if let Some(creator) = odt_props.creator {
491
- metadata_map.insert(
492
- "authors".to_string(),
493
- serde_json::Value::Array(vec![serde_json::Value::String(creator.clone())]),
494
- );
495
- metadata_map.insert("created_by".to_string(), serde_json::Value::String(creator));
496
- }
497
- if let Some(initial_creator) = odt_props.initial_creator {
498
- metadata_map.insert(
499
- "initial_creator".to_string(),
500
- serde_json::Value::String(initial_creator),
501
- );
502
- }
503
- if let Some(subject) = odt_props.subject {
504
- metadata_map.insert("subject".to_string(), serde_json::Value::String(subject));
505
- }
506
- if let Some(keywords) = odt_props.keywords {
507
- metadata_map.insert("keywords".to_string(), serde_json::Value::String(keywords));
508
- }
509
- if let Some(description) = odt_props.description {
510
- metadata_map.insert("description".to_string(), serde_json::Value::String(description));
511
- }
512
- if let Some(creation_date) = odt_props.creation_date {
513
- metadata_map.insert("created_at".to_string(), serde_json::Value::String(creation_date));
514
- }
515
- if let Some(date) = odt_props.date {
516
- metadata_map.insert("modified_at".to_string(), serde_json::Value::String(date));
517
- }
518
- if let Some(language) = odt_props.language {
519
- metadata_map.insert("language".to_string(), serde_json::Value::String(language));
520
- }
521
- if let Some(generator) = odt_props.generator {
522
- metadata_map.insert("generator".to_string(), serde_json::Value::String(generator));
523
- }
524
- if let Some(editing_duration) = odt_props.editing_duration {
525
- metadata_map.insert(
526
- "editing_duration".to_string(),
527
- serde_json::Value::String(editing_duration),
528
- );
529
- }
530
- if let Some(editing_cycles) = odt_props.editing_cycles {
531
- metadata_map.insert("editing_cycles".to_string(), serde_json::Value::String(editing_cycles));
532
- }
533
- if let Some(page_count) = odt_props.page_count {
534
- metadata_map.insert("page_count".to_string(), serde_json::Value::Number(page_count.into()));
535
- }
536
- if let Some(word_count) = odt_props.word_count {
537
- metadata_map.insert("word_count".to_string(), serde_json::Value::Number(word_count.into()));
538
- }
539
- if let Some(character_count) = odt_props.character_count {
540
- metadata_map.insert(
541
- "character_count".to_string(),
542
- serde_json::Value::Number(character_count.into()),
543
- );
544
- }
545
- if let Some(paragraph_count) = odt_props.paragraph_count {
546
- metadata_map.insert(
547
- "paragraph_count".to_string(),
548
- serde_json::Value::Number(paragraph_count.into()),
549
- );
550
- }
551
- if let Some(table_count) = odt_props.table_count {
552
- metadata_map.insert("table_count".to_string(), serde_json::Value::Number(table_count.into()));
553
- }
554
- if let Some(image_count) = odt_props.image_count {
555
- metadata_map.insert("image_count".to_string(), serde_json::Value::Number(image_count.into()));
556
- }
557
- }
558
-
559
- Ok(ExtractionResult {
560
- content: text,
561
- mime_type: mime_type.to_string(),
562
- metadata: Metadata {
563
- additional: metadata_map,
564
- ..Default::default()
565
- },
566
- tables,
567
- detected_languages: None,
568
- chunks: None,
569
- images: None,
570
- })
571
- }
572
-
573
- fn supported_mime_types(&self) -> &[&str] {
574
- &["application/vnd.oasis.opendocument.text"]
575
- }
576
-
577
- fn priority(&self) -> i32 {
578
- 60
579
- }
580
- }
581
-
582
- #[cfg(test)]
583
- mod tests {
584
- use super::*;
585
-
586
- #[tokio::test]
587
- async fn test_odt_extractor_plugin_interface() {
588
- let extractor = OdtExtractor::new();
589
- assert_eq!(extractor.name(), "odt-extractor");
590
- assert_eq!(extractor.version(), env!("CARGO_PKG_VERSION"));
591
- assert_eq!(extractor.priority(), 60);
592
- assert_eq!(extractor.supported_mime_types().len(), 1);
593
- }
594
-
595
- #[tokio::test]
596
- async fn test_odt_extractor_supports_odt() {
597
- let extractor = OdtExtractor::new();
598
- assert!(
599
- extractor
600
- .supported_mime_types()
601
- .contains(&"application/vnd.oasis.opendocument.text")
602
- );
603
- }
604
-
605
- #[tokio::test]
606
- async fn test_odt_extractor_default() {
607
- let extractor = OdtExtractor;
608
- assert_eq!(extractor.name(), "odt-extractor");
609
- }
610
-
611
- #[tokio::test]
612
- async fn test_odt_extractor_initialize_shutdown() {
613
- let extractor = OdtExtractor::new();
614
- assert!(extractor.initialize().is_ok());
615
- assert!(extractor.shutdown().is_ok());
616
- }
617
-
618
- #[test]
619
- fn test_extract_node_text_simple() {
620
- let xml = r#"<p xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0">Hello world</p>"#;
621
- let doc = roxmltree::Document::parse(xml).unwrap();
622
- let node = doc.root_element();
623
-
624
- let result = extract_node_text(node);
625
- assert!(result.is_some());
626
- assert!(!result.unwrap().is_empty());
627
- }
628
- }