kreuzberg 4.0.0.rc1 → 4.0.0.rc2

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 (342) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +14 -8
  3. data/.rspec +3 -3
  4. data/.rubocop.yaml +1 -534
  5. data/.rubocop.yml +538 -0
  6. data/Gemfile +8 -9
  7. data/Gemfile.lock +9 -109
  8. data/README.md +426 -421
  9. data/Rakefile +25 -25
  10. data/Steepfile +47 -47
  11. data/examples/async_patterns.rb +341 -340
  12. data/ext/kreuzberg_rb/extconf.rb +45 -35
  13. data/ext/kreuzberg_rb/native/Cargo.lock +6535 -0
  14. data/ext/kreuzberg_rb/native/Cargo.toml +44 -36
  15. data/ext/kreuzberg_rb/native/README.md +425 -425
  16. data/ext/kreuzberg_rb/native/build.rs +15 -17
  17. data/ext/kreuzberg_rb/native/include/ieeefp.h +11 -11
  18. data/ext/kreuzberg_rb/native/include/msvc_compat/strings.h +14 -14
  19. data/ext/kreuzberg_rb/native/include/strings.h +20 -20
  20. data/ext/kreuzberg_rb/native/include/unistd.h +47 -47
  21. data/ext/kreuzberg_rb/native/src/lib.rs +2998 -2939
  22. data/extconf.rb +28 -28
  23. data/kreuzberg.gemspec +148 -105
  24. data/lib/kreuzberg/api_proxy.rb +142 -142
  25. data/lib/kreuzberg/cache_api.rb +46 -45
  26. data/lib/kreuzberg/cli.rb +55 -55
  27. data/lib/kreuzberg/cli_proxy.rb +127 -127
  28. data/lib/kreuzberg/config.rb +691 -684
  29. data/lib/kreuzberg/error_context.rb +32 -0
  30. data/lib/kreuzberg/errors.rb +118 -50
  31. data/lib/kreuzberg/extraction_api.rb +85 -84
  32. data/lib/kreuzberg/mcp_proxy.rb +186 -186
  33. data/lib/kreuzberg/ocr_backend_protocol.rb +113 -113
  34. data/lib/kreuzberg/post_processor_protocol.rb +86 -86
  35. data/lib/kreuzberg/result.rb +216 -216
  36. data/lib/kreuzberg/setup_lib_path.rb +80 -79
  37. data/lib/kreuzberg/validator_protocol.rb +89 -89
  38. data/lib/kreuzberg/version.rb +5 -5
  39. data/lib/kreuzberg.rb +103 -82
  40. data/sig/kreuzberg/internal.rbs +184 -184
  41. data/sig/kreuzberg.rbs +520 -468
  42. data/spec/binding/cache_spec.rb +227 -227
  43. data/spec/binding/cli_proxy_spec.rb +85 -87
  44. data/spec/binding/cli_spec.rb +55 -54
  45. data/spec/binding/config_spec.rb +345 -345
  46. data/spec/binding/config_validation_spec.rb +283 -283
  47. data/spec/binding/error_handling_spec.rb +213 -213
  48. data/spec/binding/errors_spec.rb +66 -66
  49. data/spec/binding/plugins/ocr_backend_spec.rb +307 -307
  50. data/spec/binding/plugins/postprocessor_spec.rb +269 -269
  51. data/spec/binding/plugins/validator_spec.rb +274 -274
  52. data/spec/fixtures/config.toml +39 -39
  53. data/spec/fixtures/config.yaml +41 -42
  54. data/spec/fixtures/invalid_config.toml +4 -4
  55. data/spec/smoke/package_spec.rb +178 -178
  56. data/spec/spec_helper.rb +42 -42
  57. data/vendor/kreuzberg/Cargo.toml +204 -134
  58. data/vendor/kreuzberg/README.md +175 -175
  59. data/vendor/kreuzberg/benches/otel_overhead.rs +48 -0
  60. data/vendor/kreuzberg/build.rs +474 -460
  61. data/vendor/kreuzberg/src/api/error.rs +81 -81
  62. data/vendor/kreuzberg/src/api/handlers.rs +199 -199
  63. data/vendor/kreuzberg/src/api/mod.rs +79 -79
  64. data/vendor/kreuzberg/src/api/server.rs +353 -353
  65. data/vendor/kreuzberg/src/api/types.rs +170 -170
  66. data/vendor/kreuzberg/src/cache/mod.rs +1167 -1143
  67. data/vendor/kreuzberg/src/chunking/mod.rs +677 -677
  68. data/vendor/kreuzberg/src/core/batch_mode.rs +95 -35
  69. data/vendor/kreuzberg/src/core/config.rs +1032 -1032
  70. data/vendor/kreuzberg/src/core/extractor.rs +1024 -903
  71. data/vendor/kreuzberg/src/core/io.rs +329 -327
  72. data/vendor/kreuzberg/src/core/mime.rs +605 -615
  73. data/vendor/kreuzberg/src/core/mod.rs +45 -42
  74. data/vendor/kreuzberg/src/core/pipeline.rs +984 -906
  75. data/vendor/kreuzberg/src/embeddings.rs +432 -323
  76. data/vendor/kreuzberg/src/error.rs +431 -431
  77. data/vendor/kreuzberg/src/extraction/archive.rs +954 -954
  78. data/vendor/kreuzberg/src/extraction/docx.rs +40 -40
  79. data/vendor/kreuzberg/src/extraction/email.rs +854 -854
  80. data/vendor/kreuzberg/src/extraction/excel.rs +688 -688
  81. data/vendor/kreuzberg/src/extraction/html.rs +553 -553
  82. data/vendor/kreuzberg/src/extraction/image.rs +368 -368
  83. data/vendor/kreuzberg/src/extraction/libreoffice.rs +563 -564
  84. data/vendor/kreuzberg/src/extraction/markdown.rs +213 -0
  85. data/vendor/kreuzberg/src/extraction/mod.rs +81 -77
  86. data/vendor/kreuzberg/src/extraction/office_metadata/app_properties.rs +398 -398
  87. data/vendor/kreuzberg/src/extraction/office_metadata/core_properties.rs +247 -247
  88. data/vendor/kreuzberg/src/extraction/office_metadata/custom_properties.rs +240 -240
  89. data/vendor/kreuzberg/src/extraction/office_metadata/mod.rs +130 -128
  90. data/vendor/kreuzberg/src/extraction/office_metadata/odt_properties.rs +287 -0
  91. data/vendor/kreuzberg/src/extraction/pptx.rs +3000 -3000
  92. data/vendor/kreuzberg/src/extraction/structured.rs +490 -490
  93. data/vendor/kreuzberg/src/extraction/table.rs +328 -328
  94. data/vendor/kreuzberg/src/extraction/text.rs +269 -269
  95. data/vendor/kreuzberg/src/extraction/xml.rs +333 -333
  96. data/vendor/kreuzberg/src/extractors/archive.rs +446 -425
  97. data/vendor/kreuzberg/src/extractors/bibtex.rs +469 -0
  98. data/vendor/kreuzberg/src/extractors/docbook.rs +502 -0
  99. data/vendor/kreuzberg/src/extractors/docx.rs +367 -479
  100. data/vendor/kreuzberg/src/extractors/email.rs +143 -129
  101. data/vendor/kreuzberg/src/extractors/epub.rs +707 -0
  102. data/vendor/kreuzberg/src/extractors/excel.rs +343 -344
  103. data/vendor/kreuzberg/src/extractors/fictionbook.rs +491 -0
  104. data/vendor/kreuzberg/src/extractors/fictionbook.rs.backup2 +738 -0
  105. data/vendor/kreuzberg/src/extractors/html.rs +393 -410
  106. data/vendor/kreuzberg/src/extractors/image.rs +198 -195
  107. data/vendor/kreuzberg/src/extractors/jats.rs +1051 -0
  108. data/vendor/kreuzberg/src/extractors/jupyter.rs +367 -0
  109. data/vendor/kreuzberg/src/extractors/latex.rs +652 -0
  110. data/vendor/kreuzberg/src/extractors/markdown.rs +700 -0
  111. data/vendor/kreuzberg/src/extractors/mod.rs +365 -268
  112. data/vendor/kreuzberg/src/extractors/odt.rs +628 -0
  113. data/vendor/kreuzberg/src/extractors/opml.rs +634 -0
  114. data/vendor/kreuzberg/src/extractors/orgmode.rs +528 -0
  115. data/vendor/kreuzberg/src/extractors/pdf.rs +493 -496
  116. data/vendor/kreuzberg/src/extractors/pptx.rs +248 -234
  117. data/vendor/kreuzberg/src/extractors/rst.rs +576 -0
  118. data/vendor/kreuzberg/src/extractors/rtf.rs +810 -0
  119. data/vendor/kreuzberg/src/extractors/security.rs +484 -0
  120. data/vendor/kreuzberg/src/extractors/security_tests.rs +367 -0
  121. data/vendor/kreuzberg/src/extractors/structured.rs +140 -126
  122. data/vendor/kreuzberg/src/extractors/text.rs +260 -242
  123. data/vendor/kreuzberg/src/extractors/typst.rs +650 -0
  124. data/vendor/kreuzberg/src/extractors/xml.rs +135 -128
  125. data/vendor/kreuzberg/src/image/dpi.rs +164 -164
  126. data/vendor/kreuzberg/src/image/mod.rs +6 -6
  127. data/vendor/kreuzberg/src/image/preprocessing.rs +417 -417
  128. data/vendor/kreuzberg/src/image/resize.rs +89 -89
  129. data/vendor/kreuzberg/src/keywords/config.rs +154 -154
  130. data/vendor/kreuzberg/src/keywords/mod.rs +237 -237
  131. data/vendor/kreuzberg/src/keywords/processor.rs +267 -267
  132. data/vendor/kreuzberg/src/keywords/rake.rs +293 -294
  133. data/vendor/kreuzberg/src/keywords/types.rs +68 -68
  134. data/vendor/kreuzberg/src/keywords/yake.rs +163 -163
  135. data/vendor/kreuzberg/src/language_detection/mod.rs +942 -942
  136. data/vendor/kreuzberg/src/lib.rs +105 -102
  137. data/vendor/kreuzberg/src/mcp/mod.rs +32 -32
  138. data/vendor/kreuzberg/src/mcp/server.rs +1968 -1966
  139. data/vendor/kreuzberg/src/ocr/cache.rs +469 -469
  140. data/vendor/kreuzberg/src/ocr/error.rs +37 -37
  141. data/vendor/kreuzberg/src/ocr/hocr.rs +216 -216
  142. data/vendor/kreuzberg/src/ocr/mod.rs +58 -58
  143. data/vendor/kreuzberg/src/ocr/processor.rs +863 -847
  144. data/vendor/kreuzberg/src/ocr/table/mod.rs +4 -4
  145. data/vendor/kreuzberg/src/ocr/table/tsv_parser.rs +144 -144
  146. data/vendor/kreuzberg/src/ocr/tesseract_backend.rs +450 -450
  147. data/vendor/kreuzberg/src/ocr/types.rs +393 -393
  148. data/vendor/kreuzberg/src/ocr/utils.rs +47 -47
  149. data/vendor/kreuzberg/src/ocr/validation.rs +206 -206
  150. data/vendor/kreuzberg/src/panic_context.rs +154 -0
  151. data/vendor/kreuzberg/src/pdf/error.rs +122 -122
  152. data/vendor/kreuzberg/src/pdf/images.rs +139 -139
  153. data/vendor/kreuzberg/src/pdf/metadata.rs +346 -346
  154. data/vendor/kreuzberg/src/pdf/mod.rs +50 -50
  155. data/vendor/kreuzberg/src/pdf/rendering.rs +369 -369
  156. data/vendor/kreuzberg/src/pdf/table.rs +393 -420
  157. data/vendor/kreuzberg/src/pdf/text.rs +158 -161
  158. data/vendor/kreuzberg/src/plugins/extractor.rs +1013 -1010
  159. data/vendor/kreuzberg/src/plugins/mod.rs +209 -209
  160. data/vendor/kreuzberg/src/plugins/ocr.rs +620 -629
  161. data/vendor/kreuzberg/src/plugins/processor.rs +642 -641
  162. data/vendor/kreuzberg/src/plugins/registry.rs +1337 -1324
  163. data/vendor/kreuzberg/src/plugins/traits.rs +258 -258
  164. data/vendor/kreuzberg/src/plugins/validator.rs +956 -955
  165. data/vendor/kreuzberg/src/stopwords/mod.rs +1470 -1470
  166. data/vendor/kreuzberg/src/text/mod.rs +19 -19
  167. data/vendor/kreuzberg/src/text/quality.rs +697 -697
  168. data/vendor/kreuzberg/src/text/string_utils.rs +217 -217
  169. data/vendor/kreuzberg/src/text/token_reduction/cjk_utils.rs +164 -164
  170. data/vendor/kreuzberg/src/text/token_reduction/config.rs +100 -100
  171. data/vendor/kreuzberg/src/text/token_reduction/core.rs +796 -796
  172. data/vendor/kreuzberg/src/text/token_reduction/filters.rs +902 -902
  173. data/vendor/kreuzberg/src/text/token_reduction/mod.rs +160 -160
  174. data/vendor/kreuzberg/src/text/token_reduction/semantic.rs +619 -619
  175. data/vendor/kreuzberg/src/text/token_reduction/simd_text.rs +147 -147
  176. data/vendor/kreuzberg/src/types.rs +903 -873
  177. data/vendor/kreuzberg/src/utils/mod.rs +17 -17
  178. data/vendor/kreuzberg/src/utils/quality.rs +959 -959
  179. data/vendor/kreuzberg/src/utils/string_utils.rs +381 -381
  180. data/vendor/kreuzberg/stopwords/af_stopwords.json +53 -53
  181. data/vendor/kreuzberg/stopwords/ar_stopwords.json +482 -482
  182. data/vendor/kreuzberg/stopwords/bg_stopwords.json +261 -261
  183. data/vendor/kreuzberg/stopwords/bn_stopwords.json +400 -400
  184. data/vendor/kreuzberg/stopwords/br_stopwords.json +1205 -1205
  185. data/vendor/kreuzberg/stopwords/ca_stopwords.json +280 -280
  186. data/vendor/kreuzberg/stopwords/cs_stopwords.json +425 -425
  187. data/vendor/kreuzberg/stopwords/da_stopwords.json +172 -172
  188. data/vendor/kreuzberg/stopwords/de_stopwords.json +622 -622
  189. data/vendor/kreuzberg/stopwords/el_stopwords.json +849 -849
  190. data/vendor/kreuzberg/stopwords/en_stopwords.json +1300 -1300
  191. data/vendor/kreuzberg/stopwords/eo_stopwords.json +175 -175
  192. data/vendor/kreuzberg/stopwords/es_stopwords.json +734 -734
  193. data/vendor/kreuzberg/stopwords/et_stopwords.json +37 -37
  194. data/vendor/kreuzberg/stopwords/eu_stopwords.json +100 -100
  195. data/vendor/kreuzberg/stopwords/fa_stopwords.json +801 -801
  196. data/vendor/kreuzberg/stopwords/fi_stopwords.json +849 -849
  197. data/vendor/kreuzberg/stopwords/fr_stopwords.json +693 -693
  198. data/vendor/kreuzberg/stopwords/ga_stopwords.json +111 -111
  199. data/vendor/kreuzberg/stopwords/gl_stopwords.json +162 -162
  200. data/vendor/kreuzberg/stopwords/gu_stopwords.json +226 -226
  201. data/vendor/kreuzberg/stopwords/ha_stopwords.json +41 -41
  202. data/vendor/kreuzberg/stopwords/he_stopwords.json +196 -196
  203. data/vendor/kreuzberg/stopwords/hi_stopwords.json +227 -227
  204. data/vendor/kreuzberg/stopwords/hr_stopwords.json +181 -181
  205. data/vendor/kreuzberg/stopwords/hu_stopwords.json +791 -791
  206. data/vendor/kreuzberg/stopwords/hy_stopwords.json +47 -47
  207. data/vendor/kreuzberg/stopwords/id_stopwords.json +760 -760
  208. data/vendor/kreuzberg/stopwords/it_stopwords.json +634 -634
  209. data/vendor/kreuzberg/stopwords/ja_stopwords.json +136 -136
  210. data/vendor/kreuzberg/stopwords/kn_stopwords.json +84 -84
  211. data/vendor/kreuzberg/stopwords/ko_stopwords.json +681 -681
  212. data/vendor/kreuzberg/stopwords/ku_stopwords.json +64 -64
  213. data/vendor/kreuzberg/stopwords/la_stopwords.json +51 -51
  214. data/vendor/kreuzberg/stopwords/lt_stopwords.json +476 -476
  215. data/vendor/kreuzberg/stopwords/lv_stopwords.json +163 -163
  216. data/vendor/kreuzberg/stopwords/ml_stopwords.json +1 -1
  217. data/vendor/kreuzberg/stopwords/mr_stopwords.json +101 -101
  218. data/vendor/kreuzberg/stopwords/ms_stopwords.json +477 -477
  219. data/vendor/kreuzberg/stopwords/ne_stopwords.json +490 -490
  220. data/vendor/kreuzberg/stopwords/nl_stopwords.json +415 -415
  221. data/vendor/kreuzberg/stopwords/no_stopwords.json +223 -223
  222. data/vendor/kreuzberg/stopwords/pl_stopwords.json +331 -331
  223. data/vendor/kreuzberg/stopwords/pt_stopwords.json +562 -562
  224. data/vendor/kreuzberg/stopwords/ro_stopwords.json +436 -436
  225. data/vendor/kreuzberg/stopwords/ru_stopwords.json +561 -561
  226. data/vendor/kreuzberg/stopwords/si_stopwords.json +193 -193
  227. data/vendor/kreuzberg/stopwords/sk_stopwords.json +420 -420
  228. data/vendor/kreuzberg/stopwords/sl_stopwords.json +448 -448
  229. data/vendor/kreuzberg/stopwords/so_stopwords.json +32 -32
  230. data/vendor/kreuzberg/stopwords/st_stopwords.json +33 -33
  231. data/vendor/kreuzberg/stopwords/sv_stopwords.json +420 -420
  232. data/vendor/kreuzberg/stopwords/sw_stopwords.json +76 -76
  233. data/vendor/kreuzberg/stopwords/ta_stopwords.json +129 -129
  234. data/vendor/kreuzberg/stopwords/te_stopwords.json +54 -54
  235. data/vendor/kreuzberg/stopwords/th_stopwords.json +118 -118
  236. data/vendor/kreuzberg/stopwords/tl_stopwords.json +149 -149
  237. data/vendor/kreuzberg/stopwords/tr_stopwords.json +506 -506
  238. data/vendor/kreuzberg/stopwords/uk_stopwords.json +75 -75
  239. data/vendor/kreuzberg/stopwords/ur_stopwords.json +519 -519
  240. data/vendor/kreuzberg/stopwords/vi_stopwords.json +647 -647
  241. data/vendor/kreuzberg/stopwords/yo_stopwords.json +62 -62
  242. data/vendor/kreuzberg/stopwords/zh_stopwords.json +796 -796
  243. data/vendor/kreuzberg/stopwords/zu_stopwords.json +31 -31
  244. data/vendor/kreuzberg/tests/api_extract_multipart.rs +52 -0
  245. data/vendor/kreuzberg/tests/api_tests.rs +966 -966
  246. data/vendor/kreuzberg/tests/archive_integration.rs +543 -543
  247. data/vendor/kreuzberg/tests/batch_orchestration.rs +556 -542
  248. data/vendor/kreuzberg/tests/batch_processing.rs +316 -304
  249. data/vendor/kreuzberg/tests/bibtex_parity_test.rs +421 -0
  250. data/vendor/kreuzberg/tests/concurrency_stress.rs +525 -509
  251. data/vendor/kreuzberg/tests/config_features.rs +598 -580
  252. data/vendor/kreuzberg/tests/config_loading_tests.rs +415 -439
  253. data/vendor/kreuzberg/tests/core_integration.rs +510 -493
  254. data/vendor/kreuzberg/tests/csv_integration.rs +414 -424
  255. data/vendor/kreuzberg/tests/docbook_extractor_tests.rs +498 -0
  256. data/vendor/kreuzberg/tests/docx_metadata_extraction_test.rs +122 -124
  257. data/vendor/kreuzberg/tests/docx_vs_pandoc_comparison.rs +370 -0
  258. data/vendor/kreuzberg/tests/email_integration.rs +325 -325
  259. data/vendor/kreuzberg/tests/epub_native_extractor_tests.rs +275 -0
  260. data/vendor/kreuzberg/tests/error_handling.rs +393 -393
  261. data/vendor/kreuzberg/tests/fictionbook_extractor_tests.rs +228 -0
  262. data/vendor/kreuzberg/tests/format_integration.rs +159 -159
  263. data/vendor/kreuzberg/tests/helpers/mod.rs +142 -142
  264. data/vendor/kreuzberg/tests/html_table_test.rs +551 -0
  265. data/vendor/kreuzberg/tests/image_integration.rs +253 -253
  266. data/vendor/kreuzberg/tests/instrumentation_test.rs +139 -0
  267. data/vendor/kreuzberg/tests/jats_extractor_tests.rs +639 -0
  268. data/vendor/kreuzberg/tests/jupyter_extractor_tests.rs +704 -0
  269. data/vendor/kreuzberg/tests/keywords_integration.rs +479 -479
  270. data/vendor/kreuzberg/tests/keywords_quality.rs +509 -509
  271. data/vendor/kreuzberg/tests/latex_extractor_tests.rs +496 -0
  272. data/vendor/kreuzberg/tests/markdown_extractor_tests.rs +490 -0
  273. data/vendor/kreuzberg/tests/mime_detection.rs +428 -428
  274. data/vendor/kreuzberg/tests/ocr_configuration.rs +510 -510
  275. data/vendor/kreuzberg/tests/ocr_errors.rs +676 -676
  276. data/vendor/kreuzberg/tests/ocr_quality.rs +627 -627
  277. data/vendor/kreuzberg/tests/ocr_stress.rs +469 -469
  278. data/vendor/kreuzberg/tests/odt_extractor_tests.rs +695 -0
  279. data/vendor/kreuzberg/tests/opml_extractor_tests.rs +616 -0
  280. data/vendor/kreuzberg/tests/orgmode_extractor_tests.rs +822 -0
  281. data/vendor/kreuzberg/tests/pdf_integration.rs +43 -43
  282. data/vendor/kreuzberg/tests/pipeline_integration.rs +1411 -1412
  283. data/vendor/kreuzberg/tests/plugin_ocr_backend_test.rs +771 -771
  284. data/vendor/kreuzberg/tests/plugin_postprocessor_test.rs +560 -561
  285. data/vendor/kreuzberg/tests/plugin_system.rs +921 -921
  286. data/vendor/kreuzberg/tests/plugin_validator_test.rs +783 -783
  287. data/vendor/kreuzberg/tests/registry_integration_tests.rs +586 -607
  288. data/vendor/kreuzberg/tests/rst_extractor_tests.rs +692 -0
  289. data/vendor/kreuzberg/tests/rtf_extractor_tests.rs +776 -0
  290. data/vendor/kreuzberg/tests/security_validation.rs +415 -404
  291. data/vendor/kreuzberg/tests/stopwords_integration_test.rs +888 -888
  292. data/vendor/kreuzberg/tests/test_fastembed.rs +609 -609
  293. data/vendor/kreuzberg/tests/typst_behavioral_tests.rs +1259 -0
  294. data/vendor/kreuzberg/tests/typst_extractor_tests.rs +647 -0
  295. data/vendor/kreuzberg/tests/xlsx_metadata_extraction_test.rs +87 -87
  296. data/vendor/rb-sys/.cargo-ok +1 -0
  297. data/vendor/rb-sys/.cargo_vcs_info.json +6 -0
  298. data/vendor/rb-sys/Cargo.lock +393 -0
  299. data/vendor/rb-sys/Cargo.toml +70 -0
  300. data/vendor/rb-sys/Cargo.toml.orig +57 -0
  301. data/vendor/rb-sys/LICENSE-APACHE +190 -0
  302. data/vendor/rb-sys/LICENSE-MIT +21 -0
  303. data/vendor/rb-sys/bin/release.sh +21 -0
  304. data/vendor/rb-sys/build/features.rs +108 -0
  305. data/vendor/rb-sys/build/main.rs +246 -0
  306. data/vendor/rb-sys/build/stable_api_config.rs +153 -0
  307. data/vendor/rb-sys/build/version.rs +48 -0
  308. data/vendor/rb-sys/readme.md +36 -0
  309. data/vendor/rb-sys/src/bindings.rs +21 -0
  310. data/vendor/rb-sys/src/hidden.rs +11 -0
  311. data/vendor/rb-sys/src/lib.rs +34 -0
  312. data/vendor/rb-sys/src/macros.rs +371 -0
  313. data/vendor/rb-sys/src/memory.rs +53 -0
  314. data/vendor/rb-sys/src/ruby_abi_version.rs +38 -0
  315. data/vendor/rb-sys/src/special_consts.rs +31 -0
  316. data/vendor/rb-sys/src/stable_api/compiled.c +179 -0
  317. data/vendor/rb-sys/src/stable_api/compiled.rs +257 -0
  318. data/vendor/rb-sys/src/stable_api/ruby_2_6.rs +316 -0
  319. data/vendor/rb-sys/src/stable_api/ruby_2_7.rs +316 -0
  320. data/vendor/rb-sys/src/stable_api/ruby_3_0.rs +324 -0
  321. data/vendor/rb-sys/src/stable_api/ruby_3_1.rs +317 -0
  322. data/vendor/rb-sys/src/stable_api/ruby_3_2.rs +315 -0
  323. data/vendor/rb-sys/src/stable_api/ruby_3_3.rs +326 -0
  324. data/vendor/rb-sys/src/stable_api/ruby_3_4.rs +327 -0
  325. data/vendor/rb-sys/src/stable_api.rs +261 -0
  326. data/vendor/rb-sys/src/symbol.rs +31 -0
  327. data/vendor/rb-sys/src/tracking_allocator.rs +332 -0
  328. data/vendor/rb-sys/src/utils.rs +89 -0
  329. data/vendor/rb-sys/src/value_type.rs +7 -0
  330. metadata +90 -95
  331. data/pkg/kreuzberg-4.0.0.rc1.gem +0 -0
  332. data/spec/examples.txt +0 -104
  333. data/vendor/kreuzberg/src/bin/profile_extract.rs +0 -455
  334. data/vendor/kreuzberg/src/extraction/pandoc/batch.rs +0 -275
  335. data/vendor/kreuzberg/src/extraction/pandoc/mime_types.rs +0 -178
  336. data/vendor/kreuzberg/src/extraction/pandoc/mod.rs +0 -491
  337. data/vendor/kreuzberg/src/extraction/pandoc/server.rs +0 -496
  338. data/vendor/kreuzberg/src/extraction/pandoc/subprocess.rs +0 -1188
  339. data/vendor/kreuzberg/src/extraction/pandoc/version.rs +0 -162
  340. data/vendor/kreuzberg/src/extractors/pandoc.rs +0 -201
  341. data/vendor/kreuzberg/tests/chunking_offset_demo.rs +0 -92
  342. data/vendor/kreuzberg/tests/pandoc_integration.rs +0 -503
@@ -0,0 +1,695 @@
1
+ //! Comprehensive TDD test suite for ODT (OpenDocument Text) extraction
2
+ //!
3
+ //! This test suite validates ODT extraction capabilities using Pandoc's output as the baseline.
4
+ //! It covers:
5
+ //! - Metadata extraction (title, creator, date, keywords from meta.xml)
6
+ //! - Content extraction (text, formatting, structure)
7
+ //! - Table extraction with captions
8
+ //! - Formatting preservation (bold, italic, strikeout)
9
+ //! - Image handling with captions
10
+ //! - Math formula extraction
11
+ //! - Note handling (footnotes, endnotes)
12
+ //! - Citation/reference extraction
13
+ //! - Unicode and special character handling
14
+ //!
15
+ //! Note: These tests require the `office` feature to be enabled and Pandoc to be installed.
16
+
17
+ #![cfg(feature = "office")]
18
+
19
+ use kreuzberg::core::config::ExtractionConfig;
20
+ use kreuzberg::core::extractor::extract_file;
21
+ use std::path::{Path, PathBuf};
22
+
23
+ mod helpers;
24
+
25
+ /// Helper function to get the workspace root and construct test file paths
26
+ fn get_test_file_path(filename: &str) -> PathBuf {
27
+ let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
28
+ .parent()
29
+ .unwrap()
30
+ .parent()
31
+ .unwrap();
32
+ workspace_root.join(format!("test_documents/odt/{}", filename))
33
+ }
34
+
35
+ /// Helper to verify a test file exists before running test
36
+ fn ensure_test_file_exists(path: &Path) -> bool {
37
+ if !path.exists() {
38
+ println!("Skipping test: Test file not found at {:?}", path);
39
+ false
40
+ } else {
41
+ true
42
+ }
43
+ }
44
+
45
+ /// Tests extraction of document metadata from ODT meta.xml
46
+ /// Validates: title, subject, creator, dates, generator
47
+ #[tokio::test]
48
+ async fn test_odt_metadata_extraction() {
49
+ let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
50
+ .parent()
51
+ .unwrap()
52
+ .parent()
53
+ .unwrap();
54
+ let test_file = workspace_root.join("test_documents/metadata_test.odt");
55
+
56
+ if !ensure_test_file_exists(&test_file) {
57
+ println!("Skipping metadata test: metadata_test.odt not found");
58
+ return;
59
+ }
60
+
61
+ let config = ExtractionConfig::default();
62
+ let result = extract_file(&test_file, None, &config)
63
+ .await
64
+ .expect("Should extract ODT metadata successfully");
65
+
66
+ assert!(!result.content.is_empty(), "Content should not be empty");
67
+ assert!(
68
+ result.content.contains("Test Document"),
69
+ "Should contain document title in content"
70
+ );
71
+
72
+ // Verify metadata extraction
73
+ let metadata = &result.metadata.additional;
74
+ println!("Extracted metadata: {:?}", metadata);
75
+
76
+ // Check title
77
+ if let Some(title) = metadata.get("title") {
78
+ assert_eq!(title.as_str(), Some("Test Metadata Document"), "Title should match");
79
+ }
80
+
81
+ // Check subject
82
+ if let Some(subject) = metadata.get("subject") {
83
+ assert_eq!(
84
+ subject.as_str(),
85
+ Some("Testing ODT Metadata Extraction"),
86
+ "Subject should match"
87
+ );
88
+ }
89
+
90
+ // Check creator/author
91
+ if let Some(created_by) = metadata.get("created_by") {
92
+ assert_eq!(created_by.as_str(), Some("John Doe"), "Creator should match");
93
+ }
94
+
95
+ // Check authors array
96
+ if let Some(authors) = metadata.get("authors") {
97
+ let authors_array = authors.as_array().expect("Authors should be an array");
98
+ assert_eq!(authors_array.len(), 1, "Should have one author");
99
+ assert_eq!(authors_array[0].as_str(), Some("John Doe"), "Author name should match");
100
+ }
101
+
102
+ // Check creation date (should exist)
103
+ assert!(metadata.get("created_at").is_some(), "Creation date should be present");
104
+
105
+ // Check modification date (should exist)
106
+ assert!(
107
+ metadata.get("modified_at").is_some(),
108
+ "Modification date should be present"
109
+ );
110
+
111
+ // Check generator
112
+ if let Some(generator) = metadata.get("generator") {
113
+ let gen_str = generator.as_str().expect("Generator should be a string");
114
+ assert!(gen_str.contains("Pandoc"), "Generator should be Pandoc");
115
+ }
116
+
117
+ println!("✅ ODT metadata extraction test passed!");
118
+ println!(" Metadata fields extracted: {}", metadata.len());
119
+ }
120
+
121
+ /// Tests extraction of tables with captions from ODT
122
+ /// Baseline from Pandoc: simpleTableWithCaption.odt
123
+ /// Expected Pandoc output:
124
+ /// ```
125
+ /// --------- --------------
126
+ /// Content More content
127
+ /// --------- --------------
128
+ /// : Table 1: Some caption for a table
129
+ /// ```
130
+ #[tokio::test]
131
+ async fn test_odt_table_with_caption_extraction() {
132
+ let test_file = get_test_file_path("simpleTableWithCaption.odt");
133
+ if !ensure_test_file_exists(&test_file) {
134
+ return;
135
+ }
136
+
137
+ let config = ExtractionConfig::default();
138
+ let result = extract_file(&test_file, None, &config).await;
139
+
140
+ if let Ok(result) = result {
141
+ if !result.content.is_empty() {
142
+ let content_lower = result.content.to_lowercase();
143
+ assert!(
144
+ content_lower.contains("content") || content_lower.contains("table") || !result.tables.is_empty(),
145
+ "Should either extract table content or structured tables"
146
+ );
147
+ }
148
+ println!("✅ ODT table with caption extraction test passed!");
149
+ println!(" Extracted {} tables", result.tables.len());
150
+ } else {
151
+ println!("⚠️ ODT table extraction not fully supported yet (Pandoc integration needed)");
152
+ }
153
+ }
154
+
155
+ /// Tests extraction of basic tables without captions
156
+ /// Baseline from Pandoc: simpleTable.odt
157
+ /// Expected: Table with "Content" and "More content" cells
158
+ #[tokio::test]
159
+ async fn test_odt_simple_table_extraction() {
160
+ let test_file = get_test_file_path("simpleTable.odt");
161
+ if !ensure_test_file_exists(&test_file) {
162
+ return;
163
+ }
164
+
165
+ let config = ExtractionConfig::default();
166
+ let result = extract_file(&test_file, None, &config).await;
167
+
168
+ if let Ok(result) = result {
169
+ if !result.content.is_empty() {
170
+ let content_lower = result.content.to_lowercase();
171
+ assert!(
172
+ content_lower.contains("content") || !result.tables.is_empty(),
173
+ "Table should either contain 'content' text or be in structured tables"
174
+ );
175
+ }
176
+ println!("✅ ODT simple table extraction test passed!");
177
+ } else {
178
+ println!("⚠️ ODT table extraction not fully supported yet");
179
+ }
180
+ }
181
+
182
+ /// Tests extraction of document heading hierarchy
183
+ /// Baseline from Pandoc: headers.odt
184
+ /// Expected:
185
+ /// - H1: "A header (Lv 1)"
186
+ /// - H2: "Another header (Lv 2)"
187
+ /// - H1: "Back to Level 1"
188
+ #[tokio::test]
189
+ async fn test_odt_heading_structure_extraction() {
190
+ let test_file = get_test_file_path("headers.odt");
191
+ if !ensure_test_file_exists(&test_file) {
192
+ return;
193
+ }
194
+
195
+ let config = ExtractionConfig::default();
196
+ let result = extract_file(&test_file, None, &config)
197
+ .await
198
+ .expect("Should extract heading structure successfully");
199
+
200
+ assert!(!result.content.is_empty(), "Content should not be empty");
201
+
202
+ assert!(
203
+ result.content.contains("header") || result.content.contains("Header"),
204
+ "Should contain heading text"
205
+ );
206
+
207
+ assert!(
208
+ result.content.contains("#") || result.content.contains("header"),
209
+ "Should indicate heading structure"
210
+ );
211
+
212
+ println!("✅ ODT heading structure extraction test passed!");
213
+ }
214
+
215
+ /// Tests extraction of bold text formatting
216
+ /// Baseline from Pandoc: bold.odt
217
+ /// Expected Pandoc output: "Here comes **bold** text"
218
+ #[tokio::test]
219
+ async fn test_odt_bold_formatting_extraction() {
220
+ let test_file = get_test_file_path("bold.odt");
221
+ if !ensure_test_file_exists(&test_file) {
222
+ return;
223
+ }
224
+
225
+ let config = ExtractionConfig::default();
226
+ let result = extract_file(&test_file, None, &config)
227
+ .await
228
+ .expect("Should extract bold formatting successfully");
229
+
230
+ assert!(!result.content.is_empty(), "Content should not be empty");
231
+
232
+ let content = result.content.to_lowercase();
233
+ assert!(content.contains("bold"), "Should contain 'bold' text");
234
+
235
+ assert!(
236
+ result.content.contains("**bold**") || result.content.contains("bold"),
237
+ "Should preserve bold text"
238
+ );
239
+
240
+ println!("✅ ODT bold formatting extraction test passed!");
241
+ }
242
+
243
+ /// Tests extraction of italic text formatting
244
+ /// Baseline from Pandoc: italic.odt
245
+ /// Expected Pandoc output: "Here comes *italic* text"
246
+ #[tokio::test]
247
+ async fn test_odt_italic_formatting_extraction() {
248
+ let test_file = get_test_file_path("italic.odt");
249
+ if !ensure_test_file_exists(&test_file) {
250
+ return;
251
+ }
252
+
253
+ let config = ExtractionConfig::default();
254
+ let result = extract_file(&test_file, None, &config)
255
+ .await
256
+ .expect("Should extract italic formatting successfully");
257
+
258
+ assert!(!result.content.is_empty(), "Content should not be empty");
259
+
260
+ let content = result.content.to_lowercase();
261
+ assert!(content.contains("italic"), "Should contain 'italic' text");
262
+
263
+ assert!(
264
+ result.content.contains("*italic*") || result.content.contains("italic"),
265
+ "Should preserve italic text"
266
+ );
267
+
268
+ println!("✅ ODT italic formatting extraction test passed!");
269
+ }
270
+
271
+ /// Tests extraction of strikeout/strikethrough text formatting
272
+ /// Baseline from Pandoc: strikeout.odt
273
+ /// Expected Pandoc output: "Here comes text that was ~~striken out~~."
274
+ #[tokio::test]
275
+ async fn test_odt_strikeout_formatting_extraction() {
276
+ let test_file = get_test_file_path("strikeout.odt");
277
+ if !ensure_test_file_exists(&test_file) {
278
+ return;
279
+ }
280
+
281
+ let config = ExtractionConfig::default();
282
+ let result = extract_file(&test_file, None, &config)
283
+ .await
284
+ .expect("Should extract strikeout formatting successfully");
285
+
286
+ assert!(!result.content.is_empty(), "Content should not be empty");
287
+
288
+ let content = result.content.to_lowercase();
289
+ assert!(
290
+ content.contains("strike") || content.contains("striken"),
291
+ "Should contain strikeout text"
292
+ );
293
+
294
+ println!("✅ ODT strikeout formatting extraction test passed!");
295
+ }
296
+
297
+ /// Tests extraction of images with captions
298
+ /// Baseline from Pandoc: imageWithCaption.odt
299
+ /// Expected: Image reference with caption
300
+ /// Expected Pandoc output:
301
+ /// ```
302
+ /// ![Image caption](Pictures/10000000000000FA000000FAD6A15225.jpg)
303
+ /// {alt="Abbildung 1: Image caption" width="5.292cm" height="5.292cm"}
304
+ /// ```
305
+ #[tokio::test]
306
+ async fn test_odt_image_with_caption_extraction() {
307
+ let test_file = get_test_file_path("imageWithCaption.odt");
308
+ if !ensure_test_file_exists(&test_file) {
309
+ return;
310
+ }
311
+
312
+ let config = ExtractionConfig::default();
313
+ let result = extract_file(&test_file, None, &config).await;
314
+
315
+ if let Ok(result) = result {
316
+ if !result.content.is_empty() {
317
+ let content_lower = result.content.to_lowercase();
318
+ assert!(
319
+ content_lower.contains("image")
320
+ || content_lower.contains("caption")
321
+ || content_lower.contains("!")
322
+ || result.images.is_some(),
323
+ "Should reference image or caption or have extracted images"
324
+ );
325
+ }
326
+ println!("✅ ODT image with caption extraction test passed!");
327
+ } else {
328
+ println!("⚠️ ODT image extraction not fully supported yet");
329
+ }
330
+ }
331
+
332
+ /// Tests extraction of mathematical formulas
333
+ /// Baseline from Pandoc: formula.odt
334
+ /// Expected Pandoc output: "$$E = {m \\cdot c^{2}}$$"
335
+ #[tokio::test]
336
+ async fn test_odt_formula_extraction() {
337
+ let test_file = get_test_file_path("formula.odt");
338
+ if !ensure_test_file_exists(&test_file) {
339
+ return;
340
+ }
341
+
342
+ let config = ExtractionConfig::default();
343
+ let result = extract_file(&test_file, None, &config)
344
+ .await
345
+ .expect("Should extract formula successfully");
346
+
347
+ assert!(!result.content.is_empty(), "Content should not be empty");
348
+
349
+ let content = &result.content;
350
+ assert!(
351
+ content.contains("E") && (content.contains("m") || content.contains("$")),
352
+ "Should extract formula content"
353
+ );
354
+
355
+ println!("✅ ODT formula extraction test passed!");
356
+ }
357
+
358
+ /// Tests extraction of footnotes
359
+ /// Baseline from Pandoc: footnote.odt
360
+ /// Expected Pandoc output:
361
+ /// ```
362
+ /// Some text[^1] with a footnote.
363
+ ///
364
+ /// [^1]: Footnote text
365
+ /// ```
366
+ #[tokio::test]
367
+ async fn test_odt_footnote_extraction() {
368
+ let test_file = get_test_file_path("footnote.odt");
369
+ if !ensure_test_file_exists(&test_file) {
370
+ return;
371
+ }
372
+
373
+ let config = ExtractionConfig::default();
374
+ let result = extract_file(&test_file, None, &config)
375
+ .await
376
+ .expect("Should extract footnote successfully");
377
+
378
+ assert!(!result.content.is_empty(), "Content should not be empty");
379
+
380
+ let content_lower = result.content.to_lowercase();
381
+ assert!(
382
+ content_lower.contains("footnote") || content_lower.contains("[^"),
383
+ "Should extract footnote"
384
+ );
385
+
386
+ println!("✅ ODT footnote extraction test passed!");
387
+ }
388
+
389
+ /// Tests extraction of endnotes
390
+ /// Baseline from Pandoc: endnote.odt
391
+ /// Expected: Endnote content with reference (similar to footnotes)
392
+ #[tokio::test]
393
+ async fn test_odt_endnote_extraction() {
394
+ let test_file = get_test_file_path("endnote.odt");
395
+ if !ensure_test_file_exists(&test_file) {
396
+ return;
397
+ }
398
+
399
+ let config = ExtractionConfig::default();
400
+ let result = extract_file(&test_file, None, &config)
401
+ .await
402
+ .expect("Should extract endnote successfully");
403
+
404
+ assert!(!result.content.is_empty(), "Content should not be empty");
405
+
406
+ let content_lower = result.content.to_lowercase();
407
+ assert!(
408
+ content_lower.contains("endnote") || content_lower.contains("[^"),
409
+ "Should extract endnote"
410
+ );
411
+
412
+ println!("✅ ODT endnote extraction test passed!");
413
+ }
414
+
415
+ /// Tests extraction of citations and references
416
+ /// Baseline from Pandoc: citation.odt
417
+ /// Expected Pandoc output: "Some text[@Ex] with a citation."
418
+ #[tokio::test]
419
+ async fn test_odt_citation_extraction() {
420
+ let test_file = get_test_file_path("citation.odt");
421
+ if !ensure_test_file_exists(&test_file) {
422
+ return;
423
+ }
424
+
425
+ let config = ExtractionConfig::default();
426
+ let result = extract_file(&test_file, None, &config)
427
+ .await
428
+ .expect("Should extract citation successfully");
429
+
430
+ assert!(!result.content.is_empty(), "Content should not be empty");
431
+
432
+ let content_lower = result.content.to_lowercase();
433
+ assert!(
434
+ content_lower.contains("citation") || content_lower.contains("text") || content_lower.contains("@"),
435
+ "Should extract citation"
436
+ );
437
+
438
+ println!("✅ ODT citation extraction test passed!");
439
+ }
440
+
441
+ /// Tests extraction of unicode characters and special symbols
442
+ /// Baseline from Pandoc: unicode.odt
443
+ /// Expected: Proper preservation of unicode characters
444
+ /// Expected Pandoc output: ""'çӨ©¼вбФШöɵ"
445
+ #[tokio::test]
446
+ async fn test_odt_unicode_extraction() {
447
+ let test_file = get_test_file_path("unicode.odt");
448
+ if !ensure_test_file_exists(&test_file) {
449
+ return;
450
+ }
451
+
452
+ let config = ExtractionConfig::default();
453
+ let result = extract_file(&test_file, None, &config)
454
+ .await
455
+ .expect("Should extract unicode successfully");
456
+
457
+ assert!(!result.content.is_empty(), "Content should not be empty");
458
+
459
+ assert!(!result.content.is_empty(), "Should extract unicode content (not empty)");
460
+
461
+ println!("✅ ODT unicode extraction test passed!");
462
+ println!(" Extracted unicode content: {:?}", result.content);
463
+ }
464
+
465
+ /// Tests extraction of inline code formatting
466
+ /// Baseline from Pandoc: inlinedCode.odt
467
+ /// Expected Pandoc output: "Here comes `inlined code` text and `an another` one."
468
+ #[tokio::test]
469
+ async fn test_odt_inlined_code_extraction() {
470
+ let test_file = get_test_file_path("inlinedCode.odt");
471
+ if !ensure_test_file_exists(&test_file) {
472
+ return;
473
+ }
474
+
475
+ let config = ExtractionConfig::default();
476
+ let result = extract_file(&test_file, None, &config)
477
+ .await
478
+ .expect("Should extract inline code successfully");
479
+
480
+ assert!(!result.content.is_empty(), "Content should not be empty");
481
+
482
+ let content_lower = result.content.to_lowercase();
483
+ assert!(
484
+ content_lower.contains("code") || content_lower.contains("`"),
485
+ "Should extract inline code"
486
+ );
487
+
488
+ println!("✅ ODT inline code extraction test passed!");
489
+ }
490
+
491
+ /// Tests extraction of paragraph structure and content
492
+ /// Baseline from Pandoc: paragraph.odt
493
+ /// Expected: Multiple paragraphs separated by blank lines
494
+ #[tokio::test]
495
+ async fn test_odt_paragraph_structure_extraction() {
496
+ let test_file = get_test_file_path("paragraph.odt");
497
+ if !ensure_test_file_exists(&test_file) {
498
+ return;
499
+ }
500
+
501
+ let config = ExtractionConfig::default();
502
+ let result = extract_file(&test_file, None, &config)
503
+ .await
504
+ .expect("Should extract paragraph structure successfully");
505
+
506
+ assert!(!result.content.is_empty(), "Content should not be empty");
507
+
508
+ let content_lower = result.content.to_lowercase();
509
+ assert!(content_lower.contains("paragraph"), "Should contain paragraph text");
510
+
511
+ let paragraph_count = result.content.split('\n').filter(|l| !l.is_empty()).count();
512
+ assert!(paragraph_count >= 2, "Should extract multiple paragraphs");
513
+
514
+ println!("✅ ODT paragraph structure extraction test passed!");
515
+ println!(" Extracted {} paragraph segments", paragraph_count);
516
+ }
517
+
518
+ /// Integration test: Verify ODT extraction works with standard API
519
+ #[tokio::test]
520
+ async fn test_odt_extraction_api_integration() {
521
+ let test_file = get_test_file_path("bold.odt");
522
+ if !ensure_test_file_exists(&test_file) {
523
+ return;
524
+ }
525
+
526
+ let config = ExtractionConfig::default();
527
+ let result = extract_file(&test_file, None, &config)
528
+ .await
529
+ .expect("Should extract via standard API");
530
+
531
+ assert!(!result.content.is_empty(), "Should have content");
532
+ assert_eq!(result.mime_type, "application/vnd.oasis.opendocument.text");
533
+
534
+ println!("✅ ODT extraction API integration test passed!");
535
+ }
536
+
537
+ /// Test error handling for non-existent files
538
+ #[tokio::test]
539
+ async fn test_odt_extraction_missing_file_handling() {
540
+ let test_file = get_test_file_path("nonexistent.odt");
541
+ let config = ExtractionConfig::default();
542
+
543
+ let result = extract_file(&test_file, None, &config).await;
544
+
545
+ assert!(result.is_err(), "Should return error for non-existent file");
546
+
547
+ println!("✅ ODT extraction error handling test passed!");
548
+ }
549
+
550
+ /// Test extraction from multiple representative files
551
+ #[tokio::test]
552
+ async fn test_odt_extraction_variety() {
553
+ let test_files = vec![
554
+ "bold.odt",
555
+ "italic.odt",
556
+ "headers.odt",
557
+ "simpleTable.odt",
558
+ "footnote.odt",
559
+ ];
560
+
561
+ let config = ExtractionConfig::default();
562
+ let mut successful_extractions = 0;
563
+
564
+ for filename in &test_files {
565
+ let test_file = get_test_file_path(filename);
566
+ if !test_file.exists() {
567
+ continue;
568
+ }
569
+
570
+ if let Ok(result) = extract_file(&test_file, None, &config).await
571
+ && !result.content.is_empty()
572
+ {
573
+ successful_extractions += 1;
574
+ }
575
+ }
576
+
577
+ assert!(
578
+ successful_extractions >= 3,
579
+ "Should successfully extract from at least 3 test files"
580
+ );
581
+
582
+ println!("✅ ODT extraction variety test passed!");
583
+ println!(
584
+ " Successfully extracted {} out of {} files",
585
+ successful_extractions,
586
+ test_files.len()
587
+ );
588
+ }
589
+
590
+ /// Test that ODT table extraction doesn't include duplicate cell content
591
+ /// This is a regression test for the bug where table cells were extracted twice:
592
+ /// once as markdown tables and once as raw cell text
593
+ #[tokio::test]
594
+ async fn test_odt_table_no_duplicate_content() {
595
+ let test_file = get_test_file_path("simpleTable.odt");
596
+ if !ensure_test_file_exists(&test_file) {
597
+ return;
598
+ }
599
+
600
+ let config = ExtractionConfig::default();
601
+ let result = extract_file(&test_file, None, &config)
602
+ .await
603
+ .expect("Should extract table successfully");
604
+
605
+ assert!(!result.content.is_empty(), "Content should not be empty");
606
+
607
+ // Count how many times we see "Content" in the output
608
+ // In a properly fixed version, it should appear only once in the markdown table
609
+ // or possibly twice if headers appear with the same name, but not multiple times
610
+ // for the same cell
611
+ let content_count = result.content.matches("Content").count();
612
+
613
+ // "Content" appears twice in the header "More content" in a simple table
614
+ // It should not appear more than 3 times (once in header, once in data cell, once in a different word like "More content")
615
+ println!(" 'Content' appears {} times in output", content_count);
616
+ println!(" Content preview:\n{}", result.content);
617
+
618
+ // This verifies that we're not getting duplicate cell content extracted
619
+ assert!(
620
+ content_count <= 3,
621
+ "Content should not appear excessively, indicating no duplicate table cell extraction"
622
+ );
623
+
624
+ println!("✅ ODT table no duplicate content test passed!");
625
+ }
626
+
627
+ /// Test comprehensive table extraction with headers, multiple rows, and tables
628
+ /// Uses the extraction_test document created with pandoc to ensure complete content
629
+ #[tokio::test]
630
+ async fn test_odt_comprehensive_table_extraction() {
631
+ // This test uses the pandoc-generated test document
632
+ let test_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
633
+ .parent()
634
+ .unwrap()
635
+ .parent()
636
+ .unwrap()
637
+ .join("test_documents/extraction_test.odt");
638
+
639
+ if !test_file.exists() {
640
+ println!("⚠️ Test document not found at {:?}, skipping", test_file);
641
+ return;
642
+ }
643
+
644
+ let config = ExtractionConfig::default();
645
+ let result = extract_file(&test_file, None, &config)
646
+ .await
647
+ .expect("Should extract comprehensive table document successfully");
648
+
649
+ assert!(!result.content.is_empty(), "Content should not be empty");
650
+
651
+ // Verify all sections are present
652
+ assert!(result.content.contains("Comprehensive"), "Should contain heading");
653
+ assert!(
654
+ result.content.contains("First Section") || result.content.contains("First"),
655
+ "Should contain first section"
656
+ );
657
+ assert!(
658
+ result.content.contains("Second Section") || result.content.contains("Second"),
659
+ "Should contain second section"
660
+ );
661
+ assert!(
662
+ result.content.contains("Third Section") || result.content.contains("Third"),
663
+ "Should contain third section"
664
+ );
665
+
666
+ // Verify tables are present and formatted correctly (as markdown)
667
+ assert!(
668
+ result.content.contains("|"),
669
+ "Should contain pipe characters for markdown tables"
670
+ );
671
+ assert!(result.content.contains("---"), "Should contain table separator");
672
+
673
+ // Verify table content is extracted
674
+ assert!(
675
+ result.content.contains("Header 1") || result.content.contains("Cell 1A"),
676
+ "Should contain table data"
677
+ );
678
+ assert!(
679
+ result.content.contains("Product") || result.content.contains("Apple"),
680
+ "Should contain second table data"
681
+ );
682
+
683
+ // Verify no excessive duplication of cells (a simple heuristic check)
684
+ // Count "Cell 1A" - should appear once or twice at most
685
+ let cell_count = result.content.matches("Cell 1A").count();
686
+ assert!(
687
+ cell_count <= 2,
688
+ "Cell content should not be heavily duplicated (found {} instances)",
689
+ cell_count
690
+ );
691
+
692
+ println!("✅ ODT comprehensive table extraction test passed!");
693
+ println!(" Extracted content length: {} chars", result.content.len());
694
+ println!(" Tables found in output: {}", result.tables.len());
695
+ }