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,576 +0,0 @@
1
- //! Native Rust reStructuredText (RST) extractor.
2
- //!
3
- //! This extractor provides comprehensive RST document parsing.
4
- //! It extracts:
5
- //! - Document title and headings
6
- //! - Field list metadata (:Author:, :Date:, :Version:, etc.)
7
- //! - Paragraphs and text content
8
- //! - Code blocks with language specifications
9
- //! - Lists (bullet, numbered, definition lists)
10
- //! - Tables (both simple and grid tables)
11
- //! - Directives (image, code-block, note, math, etc.)
12
- //! - Inline markup (emphasis, strong, code, links)
13
- //! - Images and references
14
-
15
- #[cfg(feature = "office")]
16
- use crate::Result;
17
- #[cfg(feature = "office")]
18
- use crate::core::config::ExtractionConfig;
19
- #[cfg(feature = "office")]
20
- use crate::plugins::{DocumentExtractor, Plugin};
21
- #[cfg(feature = "office")]
22
- use crate::types::{ExtractionResult, Metadata, Table};
23
- #[cfg(feature = "office")]
24
- use async_trait::async_trait;
25
- #[cfg(feature = "office")]
26
- use std::collections::HashMap;
27
-
28
- /// Native Rust reStructuredText extractor.
29
- ///
30
- /// Parses RST documents using document tree parsing and extracts:
31
- /// - Metadata from field lists
32
- /// - Document structure (headings, sections)
33
- /// - Text content and inline formatting
34
- /// - Code blocks and directives
35
- /// - Tables and lists
36
- #[cfg(feature = "office")]
37
- pub struct RstExtractor;
38
-
39
- #[cfg(feature = "office")]
40
- impl RstExtractor {
41
- /// Create a new RST extractor.
42
- pub fn new() -> Self {
43
- Self
44
- }
45
-
46
- /// Extract text content and metadata from RST document.
47
- ///
48
- /// Uses document tree parsing and fallback text extraction.
49
- fn extract_text_and_metadata(content: &str) -> (String, Metadata) {
50
- let mut metadata = Metadata::default();
51
- let mut additional = HashMap::new();
52
-
53
- let text = Self::extract_text_from_rst(content, &mut additional);
54
-
55
- metadata.additional = additional;
56
- (text, metadata)
57
- }
58
-
59
- /// Extract text and metadata from RST content.
60
- ///
61
- /// This is the main extraction engine that processes RST line-by-line
62
- /// and extracts all document content including headings, code blocks, lists, etc.
63
- fn extract_text_from_rst(content: &str, metadata: &mut HashMap<String, serde_json::Value>) -> String {
64
- let mut output = String::new();
65
- let lines: Vec<&str> = content.lines().collect();
66
- let mut i = 0;
67
-
68
- while i < lines.len() {
69
- let line = lines[i];
70
-
71
- if line.trim().starts_with(':')
72
- && line.contains(':')
73
- && let Some((key, value)) = Self::parse_field_list_line(line)
74
- {
75
- Self::add_metadata_field(&key, &value, metadata);
76
- output.push_str(&value);
77
- output.push('\n');
78
- i += 1;
79
- continue;
80
- }
81
-
82
- if i + 1 < lines.len() {
83
- let next_line = lines[i + 1];
84
- if Self::is_section_underline(next_line) && !line.trim().is_empty() {
85
- output.push_str(line.trim());
86
- output.push('\n');
87
- i += 2;
88
- continue;
89
- }
90
- }
91
-
92
- if line.trim().starts_with(".. code-block::") {
93
- let lang = line.trim_start_matches(".. code-block::").trim().to_string();
94
- if !lang.is_empty() {
95
- output.push_str("code-block: ");
96
- output.push_str(&lang);
97
- output.push('\n');
98
- }
99
- i += 1;
100
- while i < lines.len() && (lines[i].starts_with(" ") || lines[i].is_empty()) {
101
- if !lines[i].is_empty() {
102
- output.push_str(lines[i]);
103
- output.push('\n');
104
- }
105
- i += 1;
106
- }
107
- continue;
108
- }
109
-
110
- if line.trim().starts_with(".. highlight::") {
111
- let lang = line.trim_start_matches(".. highlight::").trim().to_string();
112
- if !lang.is_empty() {
113
- output.push_str("highlight: ");
114
- output.push_str(&lang);
115
- output.push('\n');
116
- }
117
- i += 1;
118
- continue;
119
- }
120
-
121
- if line.trim().ends_with("::") {
122
- if let Some(display_text) = line.strip_suffix("::")
123
- && !display_text.trim().is_empty()
124
- {
125
- output.push_str(display_text.trim());
126
- output.push('\n');
127
- }
128
- i += 1;
129
- while i < lines.len() && (lines[i].starts_with(" ") || lines[i].is_empty()) {
130
- if !lines[i].is_empty() {
131
- output.push_str(lines[i].trim_start());
132
- output.push('\n');
133
- }
134
- i += 1;
135
- }
136
- continue;
137
- }
138
-
139
- if Self::is_list_item(line) {
140
- output.push_str(line.trim());
141
- output.push('\n');
142
- i += 1;
143
- continue;
144
- }
145
-
146
- if line.trim().starts_with(".. ") || line.trim() == ".." {
147
- let trimmed = line.trim();
148
- let directive = if trimmed == ".." { "" } else { &trimmed[3..] };
149
-
150
- if directive.starts_with("image::") {
151
- let uri = directive.strip_prefix("image::").unwrap_or("").trim();
152
- output.push_str("image: ");
153
- output.push_str(uri);
154
- output.push('\n');
155
- i += 1;
156
- continue;
157
- }
158
-
159
- if directive.starts_with("note::")
160
- || directive.starts_with("warning::")
161
- || directive.starts_with("important::")
162
- || directive.starts_with("caution::")
163
- || directive.starts_with("hint::")
164
- || directive.starts_with("tip::")
165
- {
166
- i += 1;
167
- while i < lines.len() && (lines[i].starts_with(" ") || lines[i].is_empty()) {
168
- if !lines[i].is_empty() {
169
- output.push_str(lines[i].trim());
170
- output.push('\n');
171
- }
172
- i += 1;
173
- }
174
- continue;
175
- }
176
-
177
- if directive.starts_with("math::") {
178
- let math = directive.strip_prefix("math::").unwrap_or("").trim();
179
- if !math.is_empty() {
180
- output.push_str("math: ");
181
- output.push_str(math);
182
- output.push('\n');
183
- }
184
- i += 1;
185
- while i < lines.len() && (lines[i].starts_with(" ") || lines[i].is_empty()) {
186
- if !lines[i].is_empty() {
187
- output.push_str(lines[i].trim());
188
- output.push('\n');
189
- }
190
- i += 1;
191
- }
192
- continue;
193
- }
194
-
195
- i += 1;
196
- while i < lines.len() && (lines[i].starts_with(" ") || lines[i].is_empty()) {
197
- i += 1;
198
- }
199
- continue;
200
- }
201
-
202
- if !line.trim().is_empty() && !Self::is_markup_line(line) {
203
- output.push_str(line);
204
- output.push('\n');
205
- }
206
-
207
- i += 1;
208
- }
209
-
210
- output
211
- }
212
-
213
- /// Parse a field list line (e.g., ":Author: John Doe")
214
- fn parse_field_list_line(line: &str) -> Option<(String, String)> {
215
- let trimmed = line.trim();
216
- if !trimmed.starts_with(':') {
217
- return None;
218
- }
219
-
220
- let rest = &trimmed[1..];
221
- if let Some(end_pos) = rest.find(':') {
222
- let key = rest[..end_pos].to_string();
223
- let value = rest[end_pos + 1..].trim().to_string();
224
- return Some((key, value));
225
- }
226
-
227
- None
228
- }
229
-
230
- /// Add a metadata field from RST field list.
231
- fn add_metadata_field(key: &str, value: &str, metadata: &mut HashMap<String, serde_json::Value>) {
232
- let key_lower = key.to_lowercase();
233
- match key_lower.as_str() {
234
- "author" | "authors" => {
235
- metadata.insert("author".to_string(), serde_json::Value::String(value.to_string()));
236
- }
237
- "date" => {
238
- metadata.insert("date".to_string(), serde_json::Value::String(value.to_string()));
239
- }
240
- "version" | "revision" => {
241
- metadata.insert("version".to_string(), serde_json::Value::String(value.to_string()));
242
- }
243
- "title" => {
244
- metadata.insert("title".to_string(), serde_json::Value::String(value.to_string()));
245
- }
246
- _ => {
247
- metadata.insert(
248
- format!("field_{}", key_lower),
249
- serde_json::Value::String(value.to_string()),
250
- );
251
- }
252
- }
253
- }
254
-
255
- /// Check if a line is a section underline.
256
- fn is_section_underline(line: &str) -> bool {
257
- let trimmed = line.trim();
258
- if trimmed.len() < 3 {
259
- return false;
260
- }
261
- let chars: Vec<char> = trimmed.chars().collect();
262
- let first = chars[0];
263
- matches!(first, '=' | '-' | '~' | '+' | '^' | '"' | '`' | '#' | '*') && chars.iter().all(|c| *c == first)
264
- }
265
-
266
- /// Check if a line is a list item.
267
- fn is_list_item(line: &str) -> bool {
268
- let trimmed = line.trim_start();
269
- if trimmed.starts_with("* ") || trimmed.starts_with("+ ") || trimmed.starts_with("- ") {
270
- return true;
271
- }
272
- if let Some(space_pos) = trimmed.find(' ')
273
- && space_pos > 0
274
- && space_pos < 4
275
- {
276
- let prefix = &trimmed[..space_pos];
277
- if prefix.ends_with('.') || prefix.ends_with(')') {
278
- return prefix[..prefix.len() - 1].chars().all(|c| c.is_numeric());
279
- }
280
- }
281
- false
282
- }
283
-
284
- /// Check if a line is just markup (underlines, etc.)
285
- fn is_markup_line(line: &str) -> bool {
286
- let trimmed = line.trim();
287
- if trimmed.len() < 3 {
288
- return false;
289
- }
290
- let first = trimmed.chars().next().unwrap();
291
- trimmed.chars().all(|c| c == first)
292
- && matches!(first, '=' | '-' | '~' | '+' | '^' | '"' | '`' | '#' | '*' | '/')
293
- }
294
-
295
- /// Extract tables from RST content.
296
- ///
297
- /// Identifies and extracts both simple and grid tables.
298
- fn extract_tables(content: &str) -> Vec<Table> {
299
- let mut tables = Vec::new();
300
- let lines: Vec<&str> = content.lines().collect();
301
- let mut i = 0;
302
-
303
- while i < lines.len() {
304
- let line = lines[i];
305
-
306
- if line.contains("|")
307
- && (line.contains("=") || line.contains("-"))
308
- && let Some(table) = Self::parse_grid_table(&lines, &mut i)
309
- {
310
- tables.push(table);
311
- continue;
312
- }
313
-
314
- i += 1;
315
- }
316
-
317
- tables
318
- }
319
-
320
- /// Parse a grid table from lines.
321
- fn parse_grid_table(lines: &[&str], i: &mut usize) -> Option<Table> {
322
- let mut cells = Vec::new();
323
- let mut row = Vec::new();
324
-
325
- while *i < lines.len() && lines[*i].contains("|") {
326
- let line = lines[*i].trim_matches(|c| c == '|');
327
- if !line.is_empty() {
328
- let cell_content = line.split('|').map(|s| s.trim().to_string()).collect::<Vec<_>>();
329
- row.extend(cell_content);
330
-
331
- if !row.is_empty() {
332
- cells.push(row.clone());
333
- row.clear();
334
- }
335
- }
336
- *i += 1;
337
- }
338
-
339
- if cells.is_empty() {
340
- return None;
341
- }
342
-
343
- let markdown = Self::cells_to_markdown(&cells);
344
- Some(Table {
345
- cells,
346
- markdown,
347
- page_number: 1,
348
- })
349
- }
350
-
351
- /// Convert table cells to markdown format.
352
- fn cells_to_markdown(cells: &[Vec<String>]) -> String {
353
- if cells.is_empty() {
354
- return String::new();
355
- }
356
-
357
- let mut md = String::new();
358
-
359
- if !cells.is_empty() {
360
- md.push('|');
361
- for cell in &cells[0] {
362
- md.push(' ');
363
- md.push_str(cell);
364
- md.push_str(" |");
365
- }
366
- md.push('\n');
367
-
368
- md.push('|');
369
- for _ in &cells[0] {
370
- md.push_str(" --- |");
371
- }
372
- md.push('\n');
373
-
374
- for row in &cells[1..] {
375
- md.push('|');
376
- for cell in row {
377
- md.push(' ');
378
- md.push_str(cell);
379
- md.push_str(" |");
380
- }
381
- md.push('\n');
382
- }
383
- }
384
-
385
- md
386
- }
387
- }
388
-
389
- #[cfg(feature = "office")]
390
- impl Default for RstExtractor {
391
- fn default() -> Self {
392
- Self::new()
393
- }
394
- }
395
-
396
- #[cfg(feature = "office")]
397
- impl Plugin for RstExtractor {
398
- fn name(&self) -> &str {
399
- "rst-extractor"
400
- }
401
-
402
- fn version(&self) -> String {
403
- env!("CARGO_PKG_VERSION").to_string()
404
- }
405
-
406
- fn initialize(&self) -> Result<()> {
407
- Ok(())
408
- }
409
-
410
- fn shutdown(&self) -> Result<()> {
411
- Ok(())
412
- }
413
-
414
- fn description(&self) -> &str {
415
- "Native Rust extractor for reStructuredText (RST) documents"
416
- }
417
-
418
- fn author(&self) -> &str {
419
- "Kreuzberg Team"
420
- }
421
- }
422
-
423
- #[cfg(feature = "office")]
424
- #[async_trait]
425
- impl DocumentExtractor for RstExtractor {
426
- #[cfg_attr(
427
- feature = "otel",
428
- tracing::instrument(
429
- skip(self, content, _config),
430
- fields(
431
- extractor.name = self.name(),
432
- content.size_bytes = content.len(),
433
- )
434
- )
435
- )]
436
- async fn extract_bytes(
437
- &self,
438
- content: &[u8],
439
- mime_type: &str,
440
- _config: &ExtractionConfig,
441
- ) -> Result<ExtractionResult> {
442
- let text = String::from_utf8_lossy(content).into_owned();
443
-
444
- let (extracted_text, metadata) = Self::extract_text_and_metadata(&text);
445
-
446
- let tables = Self::extract_tables(&text);
447
-
448
- Ok(ExtractionResult {
449
- content: extracted_text,
450
- mime_type: mime_type.to_string(),
451
- metadata,
452
- tables,
453
- detected_languages: None,
454
- chunks: None,
455
- images: None,
456
- })
457
- }
458
-
459
- fn supported_mime_types(&self) -> &[&str] {
460
- &["text/x-rst", "text/prs.fallenstein.rst"]
461
- }
462
-
463
- fn priority(&self) -> i32 {
464
- 50
465
- }
466
- }
467
-
468
- #[cfg(all(test, feature = "office"))]
469
- mod tests {
470
- use super::*;
471
-
472
- #[test]
473
- fn test_rst_extractor_plugin_interface() {
474
- let extractor = RstExtractor::new();
475
- assert_eq!(extractor.name(), "rst-extractor");
476
- assert_eq!(extractor.version(), env!("CARGO_PKG_VERSION"));
477
- assert_eq!(extractor.priority(), 50);
478
- assert!(!extractor.supported_mime_types().is_empty());
479
- }
480
-
481
- #[test]
482
- fn test_rst_extractor_supports_text_x_rst() {
483
- let extractor = RstExtractor::new();
484
- assert!(extractor.supported_mime_types().contains(&"text/x-rst"));
485
- }
486
-
487
- #[test]
488
- fn test_rst_extractor_supports_fallenstein_rst() {
489
- let extractor = RstExtractor::new();
490
- assert!(extractor.supported_mime_types().contains(&"text/prs.fallenstein.rst"));
491
- }
492
-
493
- #[test]
494
- fn test_extract_text_from_rst_simple_document() {
495
- let content = r#"
496
- Title
497
- =====
498
-
499
- This is a paragraph.
500
-
501
- Another paragraph.
502
- "#;
503
-
504
- let mut metadata = HashMap::new();
505
- let output = RstExtractor::extract_text_from_rst(content, &mut metadata);
506
- assert!(output.contains("Title"));
507
- assert!(output.contains("This is a paragraph"));
508
- assert!(output.contains("Another paragraph"));
509
- }
510
-
511
- #[test]
512
- fn test_extract_text_from_rst_with_code_block() {
513
- let content = r#"
514
- .. code-block:: python
515
-
516
- def hello():
517
- print("world")
518
-
519
- Some text after.
520
- "#;
521
-
522
- let mut metadata = HashMap::new();
523
- let output = RstExtractor::extract_text_from_rst(content, &mut metadata);
524
- assert!(output.contains("code-block"));
525
- assert!(output.contains("def hello"));
526
- assert!(output.contains("Some text after"));
527
- }
528
-
529
- #[test]
530
- fn test_extract_text_from_rst_with_metadata() {
531
- let content = r#"
532
- :Author: John Doe
533
- :Date: 2024-01-15
534
-
535
- First paragraph.
536
-
537
- Second paragraph.
538
- "#;
539
-
540
- let mut metadata = HashMap::new();
541
- let output = RstExtractor::extract_text_from_rst(content, &mut metadata);
542
- assert!(output.contains("First paragraph"));
543
- assert!(output.contains("Second paragraph"));
544
- assert!(metadata.contains_key("author"));
545
- assert_eq!(metadata.get("author").and_then(|v| v.as_str()), Some("John Doe"));
546
- }
547
-
548
- #[test]
549
- fn test_cells_to_markdown_format() {
550
- let cells = vec![
551
- vec!["Name".to_string(), "Age".to_string()],
552
- vec!["Alice".to_string(), "30".to_string()],
553
- vec!["Bob".to_string(), "25".to_string()],
554
- ];
555
-
556
- let markdown = RstExtractor::cells_to_markdown(&cells);
557
- assert!(markdown.contains("Name"));
558
- assert!(markdown.contains("Age"));
559
- assert!(markdown.contains("Alice"));
560
- assert!(markdown.contains("Bob"));
561
- assert!(markdown.contains("---"));
562
- }
563
-
564
- #[test]
565
- fn test_rst_extractor_default() {
566
- let extractor = RstExtractor;
567
- assert_eq!(extractor.name(), "rst-extractor");
568
- }
569
-
570
- #[test]
571
- fn test_rst_extractor_initialize_shutdown() {
572
- let extractor = RstExtractor::new();
573
- assert!(extractor.initialize().is_ok());
574
- assert!(extractor.shutdown().is_ok());
575
- }
576
- }