kreuzberg 4.0.0.pre.rc.6

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 (330) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yaml +1 -0
  5. data/.rubocop.yml +538 -0
  6. data/Gemfile +8 -0
  7. data/Gemfile.lock +157 -0
  8. data/README.md +426 -0
  9. data/Rakefile +25 -0
  10. data/Steepfile +47 -0
  11. data/examples/async_patterns.rb +341 -0
  12. data/ext/kreuzberg_rb/extconf.rb +45 -0
  13. data/ext/kreuzberg_rb/native/Cargo.lock +6535 -0
  14. data/ext/kreuzberg_rb/native/Cargo.toml +44 -0
  15. data/ext/kreuzberg_rb/native/README.md +425 -0
  16. data/ext/kreuzberg_rb/native/build.rs +15 -0
  17. data/ext/kreuzberg_rb/native/include/ieeefp.h +11 -0
  18. data/ext/kreuzberg_rb/native/include/msvc_compat/strings.h +14 -0
  19. data/ext/kreuzberg_rb/native/include/strings.h +20 -0
  20. data/ext/kreuzberg_rb/native/include/unistd.h +47 -0
  21. data/ext/kreuzberg_rb/native/src/lib.rs +2998 -0
  22. data/extconf.rb +28 -0
  23. data/kreuzberg.gemspec +148 -0
  24. data/lib/kreuzberg/api_proxy.rb +142 -0
  25. data/lib/kreuzberg/cache_api.rb +46 -0
  26. data/lib/kreuzberg/cli.rb +55 -0
  27. data/lib/kreuzberg/cli_proxy.rb +127 -0
  28. data/lib/kreuzberg/config.rb +691 -0
  29. data/lib/kreuzberg/error_context.rb +32 -0
  30. data/lib/kreuzberg/errors.rb +118 -0
  31. data/lib/kreuzberg/extraction_api.rb +85 -0
  32. data/lib/kreuzberg/mcp_proxy.rb +186 -0
  33. data/lib/kreuzberg/ocr_backend_protocol.rb +113 -0
  34. data/lib/kreuzberg/post_processor_protocol.rb +86 -0
  35. data/lib/kreuzberg/result.rb +216 -0
  36. data/lib/kreuzberg/setup_lib_path.rb +80 -0
  37. data/lib/kreuzberg/validator_protocol.rb +89 -0
  38. data/lib/kreuzberg/version.rb +5 -0
  39. data/lib/kreuzberg.rb +103 -0
  40. data/sig/kreuzberg/internal.rbs +184 -0
  41. data/sig/kreuzberg.rbs +520 -0
  42. data/spec/binding/cache_spec.rb +227 -0
  43. data/spec/binding/cli_proxy_spec.rb +85 -0
  44. data/spec/binding/cli_spec.rb +55 -0
  45. data/spec/binding/config_spec.rb +345 -0
  46. data/spec/binding/config_validation_spec.rb +283 -0
  47. data/spec/binding/error_handling_spec.rb +213 -0
  48. data/spec/binding/errors_spec.rb +66 -0
  49. data/spec/binding/plugins/ocr_backend_spec.rb +307 -0
  50. data/spec/binding/plugins/postprocessor_spec.rb +269 -0
  51. data/spec/binding/plugins/validator_spec.rb +274 -0
  52. data/spec/fixtures/config.toml +39 -0
  53. data/spec/fixtures/config.yaml +41 -0
  54. data/spec/fixtures/invalid_config.toml +4 -0
  55. data/spec/smoke/package_spec.rb +178 -0
  56. data/spec/spec_helper.rb +42 -0
  57. data/vendor/kreuzberg/Cargo.toml +204 -0
  58. data/vendor/kreuzberg/README.md +175 -0
  59. data/vendor/kreuzberg/benches/otel_overhead.rs +48 -0
  60. data/vendor/kreuzberg/build.rs +474 -0
  61. data/vendor/kreuzberg/src/api/error.rs +81 -0
  62. data/vendor/kreuzberg/src/api/handlers.rs +199 -0
  63. data/vendor/kreuzberg/src/api/mod.rs +79 -0
  64. data/vendor/kreuzberg/src/api/server.rs +353 -0
  65. data/vendor/kreuzberg/src/api/types.rs +170 -0
  66. data/vendor/kreuzberg/src/cache/mod.rs +1167 -0
  67. data/vendor/kreuzberg/src/chunking/mod.rs +677 -0
  68. data/vendor/kreuzberg/src/core/batch_mode.rs +95 -0
  69. data/vendor/kreuzberg/src/core/config.rs +1032 -0
  70. data/vendor/kreuzberg/src/core/extractor.rs +1024 -0
  71. data/vendor/kreuzberg/src/core/io.rs +329 -0
  72. data/vendor/kreuzberg/src/core/mime.rs +605 -0
  73. data/vendor/kreuzberg/src/core/mod.rs +45 -0
  74. data/vendor/kreuzberg/src/core/pipeline.rs +984 -0
  75. data/vendor/kreuzberg/src/embeddings.rs +432 -0
  76. data/vendor/kreuzberg/src/error.rs +431 -0
  77. data/vendor/kreuzberg/src/extraction/archive.rs +954 -0
  78. data/vendor/kreuzberg/src/extraction/docx.rs +40 -0
  79. data/vendor/kreuzberg/src/extraction/email.rs +854 -0
  80. data/vendor/kreuzberg/src/extraction/excel.rs +688 -0
  81. data/vendor/kreuzberg/src/extraction/html.rs +553 -0
  82. data/vendor/kreuzberg/src/extraction/image.rs +368 -0
  83. data/vendor/kreuzberg/src/extraction/libreoffice.rs +563 -0
  84. data/vendor/kreuzberg/src/extraction/markdown.rs +213 -0
  85. data/vendor/kreuzberg/src/extraction/mod.rs +81 -0
  86. data/vendor/kreuzberg/src/extraction/office_metadata/app_properties.rs +398 -0
  87. data/vendor/kreuzberg/src/extraction/office_metadata/core_properties.rs +247 -0
  88. data/vendor/kreuzberg/src/extraction/office_metadata/custom_properties.rs +240 -0
  89. data/vendor/kreuzberg/src/extraction/office_metadata/mod.rs +130 -0
  90. data/vendor/kreuzberg/src/extraction/office_metadata/odt_properties.rs +287 -0
  91. data/vendor/kreuzberg/src/extraction/pptx.rs +3000 -0
  92. data/vendor/kreuzberg/src/extraction/structured.rs +490 -0
  93. data/vendor/kreuzberg/src/extraction/table.rs +328 -0
  94. data/vendor/kreuzberg/src/extraction/text.rs +269 -0
  95. data/vendor/kreuzberg/src/extraction/xml.rs +333 -0
  96. data/vendor/kreuzberg/src/extractors/archive.rs +446 -0
  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 -0
  100. data/vendor/kreuzberg/src/extractors/email.rs +143 -0
  101. data/vendor/kreuzberg/src/extractors/epub.rs +707 -0
  102. data/vendor/kreuzberg/src/extractors/excel.rs +343 -0
  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 -0
  106. data/vendor/kreuzberg/src/extractors/image.rs +198 -0
  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 -0
  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 -0
  116. data/vendor/kreuzberg/src/extractors/pptx.rs +248 -0
  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 -0
  122. data/vendor/kreuzberg/src/extractors/text.rs +260 -0
  123. data/vendor/kreuzberg/src/extractors/typst.rs +650 -0
  124. data/vendor/kreuzberg/src/extractors/xml.rs +135 -0
  125. data/vendor/kreuzberg/src/image/dpi.rs +164 -0
  126. data/vendor/kreuzberg/src/image/mod.rs +6 -0
  127. data/vendor/kreuzberg/src/image/preprocessing.rs +417 -0
  128. data/vendor/kreuzberg/src/image/resize.rs +89 -0
  129. data/vendor/kreuzberg/src/keywords/config.rs +154 -0
  130. data/vendor/kreuzberg/src/keywords/mod.rs +237 -0
  131. data/vendor/kreuzberg/src/keywords/processor.rs +267 -0
  132. data/vendor/kreuzberg/src/keywords/rake.rs +293 -0
  133. data/vendor/kreuzberg/src/keywords/types.rs +68 -0
  134. data/vendor/kreuzberg/src/keywords/yake.rs +163 -0
  135. data/vendor/kreuzberg/src/language_detection/mod.rs +942 -0
  136. data/vendor/kreuzberg/src/lib.rs +105 -0
  137. data/vendor/kreuzberg/src/mcp/mod.rs +32 -0
  138. data/vendor/kreuzberg/src/mcp/server.rs +1968 -0
  139. data/vendor/kreuzberg/src/ocr/cache.rs +469 -0
  140. data/vendor/kreuzberg/src/ocr/error.rs +37 -0
  141. data/vendor/kreuzberg/src/ocr/hocr.rs +216 -0
  142. data/vendor/kreuzberg/src/ocr/mod.rs +58 -0
  143. data/vendor/kreuzberg/src/ocr/processor.rs +863 -0
  144. data/vendor/kreuzberg/src/ocr/table/mod.rs +4 -0
  145. data/vendor/kreuzberg/src/ocr/table/tsv_parser.rs +144 -0
  146. data/vendor/kreuzberg/src/ocr/tesseract_backend.rs +450 -0
  147. data/vendor/kreuzberg/src/ocr/types.rs +393 -0
  148. data/vendor/kreuzberg/src/ocr/utils.rs +47 -0
  149. data/vendor/kreuzberg/src/ocr/validation.rs +206 -0
  150. data/vendor/kreuzberg/src/panic_context.rs +154 -0
  151. data/vendor/kreuzberg/src/pdf/error.rs +122 -0
  152. data/vendor/kreuzberg/src/pdf/images.rs +139 -0
  153. data/vendor/kreuzberg/src/pdf/metadata.rs +346 -0
  154. data/vendor/kreuzberg/src/pdf/mod.rs +50 -0
  155. data/vendor/kreuzberg/src/pdf/rendering.rs +369 -0
  156. data/vendor/kreuzberg/src/pdf/table.rs +393 -0
  157. data/vendor/kreuzberg/src/pdf/text.rs +158 -0
  158. data/vendor/kreuzberg/src/plugins/extractor.rs +1013 -0
  159. data/vendor/kreuzberg/src/plugins/mod.rs +209 -0
  160. data/vendor/kreuzberg/src/plugins/ocr.rs +620 -0
  161. data/vendor/kreuzberg/src/plugins/processor.rs +642 -0
  162. data/vendor/kreuzberg/src/plugins/registry.rs +1337 -0
  163. data/vendor/kreuzberg/src/plugins/traits.rs +258 -0
  164. data/vendor/kreuzberg/src/plugins/validator.rs +956 -0
  165. data/vendor/kreuzberg/src/stopwords/mod.rs +1470 -0
  166. data/vendor/kreuzberg/src/text/mod.rs +19 -0
  167. data/vendor/kreuzberg/src/text/quality.rs +697 -0
  168. data/vendor/kreuzberg/src/text/string_utils.rs +217 -0
  169. data/vendor/kreuzberg/src/text/token_reduction/cjk_utils.rs +164 -0
  170. data/vendor/kreuzberg/src/text/token_reduction/config.rs +100 -0
  171. data/vendor/kreuzberg/src/text/token_reduction/core.rs +796 -0
  172. data/vendor/kreuzberg/src/text/token_reduction/filters.rs +902 -0
  173. data/vendor/kreuzberg/src/text/token_reduction/mod.rs +160 -0
  174. data/vendor/kreuzberg/src/text/token_reduction/semantic.rs +619 -0
  175. data/vendor/kreuzberg/src/text/token_reduction/simd_text.rs +147 -0
  176. data/vendor/kreuzberg/src/types.rs +903 -0
  177. data/vendor/kreuzberg/src/utils/mod.rs +17 -0
  178. data/vendor/kreuzberg/src/utils/quality.rs +959 -0
  179. data/vendor/kreuzberg/src/utils/string_utils.rs +381 -0
  180. data/vendor/kreuzberg/stopwords/af_stopwords.json +53 -0
  181. data/vendor/kreuzberg/stopwords/ar_stopwords.json +482 -0
  182. data/vendor/kreuzberg/stopwords/bg_stopwords.json +261 -0
  183. data/vendor/kreuzberg/stopwords/bn_stopwords.json +400 -0
  184. data/vendor/kreuzberg/stopwords/br_stopwords.json +1205 -0
  185. data/vendor/kreuzberg/stopwords/ca_stopwords.json +280 -0
  186. data/vendor/kreuzberg/stopwords/cs_stopwords.json +425 -0
  187. data/vendor/kreuzberg/stopwords/da_stopwords.json +172 -0
  188. data/vendor/kreuzberg/stopwords/de_stopwords.json +622 -0
  189. data/vendor/kreuzberg/stopwords/el_stopwords.json +849 -0
  190. data/vendor/kreuzberg/stopwords/en_stopwords.json +1300 -0
  191. data/vendor/kreuzberg/stopwords/eo_stopwords.json +175 -0
  192. data/vendor/kreuzberg/stopwords/es_stopwords.json +734 -0
  193. data/vendor/kreuzberg/stopwords/et_stopwords.json +37 -0
  194. data/vendor/kreuzberg/stopwords/eu_stopwords.json +100 -0
  195. data/vendor/kreuzberg/stopwords/fa_stopwords.json +801 -0
  196. data/vendor/kreuzberg/stopwords/fi_stopwords.json +849 -0
  197. data/vendor/kreuzberg/stopwords/fr_stopwords.json +693 -0
  198. data/vendor/kreuzberg/stopwords/ga_stopwords.json +111 -0
  199. data/vendor/kreuzberg/stopwords/gl_stopwords.json +162 -0
  200. data/vendor/kreuzberg/stopwords/gu_stopwords.json +226 -0
  201. data/vendor/kreuzberg/stopwords/ha_stopwords.json +41 -0
  202. data/vendor/kreuzberg/stopwords/he_stopwords.json +196 -0
  203. data/vendor/kreuzberg/stopwords/hi_stopwords.json +227 -0
  204. data/vendor/kreuzberg/stopwords/hr_stopwords.json +181 -0
  205. data/vendor/kreuzberg/stopwords/hu_stopwords.json +791 -0
  206. data/vendor/kreuzberg/stopwords/hy_stopwords.json +47 -0
  207. data/vendor/kreuzberg/stopwords/id_stopwords.json +760 -0
  208. data/vendor/kreuzberg/stopwords/it_stopwords.json +634 -0
  209. data/vendor/kreuzberg/stopwords/ja_stopwords.json +136 -0
  210. data/vendor/kreuzberg/stopwords/kn_stopwords.json +84 -0
  211. data/vendor/kreuzberg/stopwords/ko_stopwords.json +681 -0
  212. data/vendor/kreuzberg/stopwords/ku_stopwords.json +64 -0
  213. data/vendor/kreuzberg/stopwords/la_stopwords.json +51 -0
  214. data/vendor/kreuzberg/stopwords/lt_stopwords.json +476 -0
  215. data/vendor/kreuzberg/stopwords/lv_stopwords.json +163 -0
  216. data/vendor/kreuzberg/stopwords/ml_stopwords.json +1 -0
  217. data/vendor/kreuzberg/stopwords/mr_stopwords.json +101 -0
  218. data/vendor/kreuzberg/stopwords/ms_stopwords.json +477 -0
  219. data/vendor/kreuzberg/stopwords/ne_stopwords.json +490 -0
  220. data/vendor/kreuzberg/stopwords/nl_stopwords.json +415 -0
  221. data/vendor/kreuzberg/stopwords/no_stopwords.json +223 -0
  222. data/vendor/kreuzberg/stopwords/pl_stopwords.json +331 -0
  223. data/vendor/kreuzberg/stopwords/pt_stopwords.json +562 -0
  224. data/vendor/kreuzberg/stopwords/ro_stopwords.json +436 -0
  225. data/vendor/kreuzberg/stopwords/ru_stopwords.json +561 -0
  226. data/vendor/kreuzberg/stopwords/si_stopwords.json +193 -0
  227. data/vendor/kreuzberg/stopwords/sk_stopwords.json +420 -0
  228. data/vendor/kreuzberg/stopwords/sl_stopwords.json +448 -0
  229. data/vendor/kreuzberg/stopwords/so_stopwords.json +32 -0
  230. data/vendor/kreuzberg/stopwords/st_stopwords.json +33 -0
  231. data/vendor/kreuzberg/stopwords/sv_stopwords.json +420 -0
  232. data/vendor/kreuzberg/stopwords/sw_stopwords.json +76 -0
  233. data/vendor/kreuzberg/stopwords/ta_stopwords.json +129 -0
  234. data/vendor/kreuzberg/stopwords/te_stopwords.json +54 -0
  235. data/vendor/kreuzberg/stopwords/th_stopwords.json +118 -0
  236. data/vendor/kreuzberg/stopwords/tl_stopwords.json +149 -0
  237. data/vendor/kreuzberg/stopwords/tr_stopwords.json +506 -0
  238. data/vendor/kreuzberg/stopwords/uk_stopwords.json +75 -0
  239. data/vendor/kreuzberg/stopwords/ur_stopwords.json +519 -0
  240. data/vendor/kreuzberg/stopwords/vi_stopwords.json +647 -0
  241. data/vendor/kreuzberg/stopwords/yo_stopwords.json +62 -0
  242. data/vendor/kreuzberg/stopwords/zh_stopwords.json +796 -0
  243. data/vendor/kreuzberg/stopwords/zu_stopwords.json +31 -0
  244. data/vendor/kreuzberg/tests/api_extract_multipart.rs +52 -0
  245. data/vendor/kreuzberg/tests/api_tests.rs +966 -0
  246. data/vendor/kreuzberg/tests/archive_integration.rs +543 -0
  247. data/vendor/kreuzberg/tests/batch_orchestration.rs +556 -0
  248. data/vendor/kreuzberg/tests/batch_processing.rs +316 -0
  249. data/vendor/kreuzberg/tests/bibtex_parity_test.rs +421 -0
  250. data/vendor/kreuzberg/tests/concurrency_stress.rs +525 -0
  251. data/vendor/kreuzberg/tests/config_features.rs +598 -0
  252. data/vendor/kreuzberg/tests/config_loading_tests.rs +415 -0
  253. data/vendor/kreuzberg/tests/core_integration.rs +510 -0
  254. data/vendor/kreuzberg/tests/csv_integration.rs +414 -0
  255. data/vendor/kreuzberg/tests/docbook_extractor_tests.rs +498 -0
  256. data/vendor/kreuzberg/tests/docx_metadata_extraction_test.rs +122 -0
  257. data/vendor/kreuzberg/tests/docx_vs_pandoc_comparison.rs +370 -0
  258. data/vendor/kreuzberg/tests/email_integration.rs +325 -0
  259. data/vendor/kreuzberg/tests/epub_native_extractor_tests.rs +275 -0
  260. data/vendor/kreuzberg/tests/error_handling.rs +393 -0
  261. data/vendor/kreuzberg/tests/fictionbook_extractor_tests.rs +228 -0
  262. data/vendor/kreuzberg/tests/format_integration.rs +159 -0
  263. data/vendor/kreuzberg/tests/helpers/mod.rs +142 -0
  264. data/vendor/kreuzberg/tests/html_table_test.rs +551 -0
  265. data/vendor/kreuzberg/tests/image_integration.rs +253 -0
  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 -0
  270. data/vendor/kreuzberg/tests/keywords_quality.rs +509 -0
  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 -0
  274. data/vendor/kreuzberg/tests/ocr_configuration.rs +510 -0
  275. data/vendor/kreuzberg/tests/ocr_errors.rs +676 -0
  276. data/vendor/kreuzberg/tests/ocr_quality.rs +627 -0
  277. data/vendor/kreuzberg/tests/ocr_stress.rs +469 -0
  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 -0
  282. data/vendor/kreuzberg/tests/pipeline_integration.rs +1411 -0
  283. data/vendor/kreuzberg/tests/plugin_ocr_backend_test.rs +771 -0
  284. data/vendor/kreuzberg/tests/plugin_postprocessor_test.rs +560 -0
  285. data/vendor/kreuzberg/tests/plugin_system.rs +921 -0
  286. data/vendor/kreuzberg/tests/plugin_validator_test.rs +783 -0
  287. data/vendor/kreuzberg/tests/registry_integration_tests.rs +586 -0
  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 -0
  291. data/vendor/kreuzberg/tests/stopwords_integration_test.rs +888 -0
  292. data/vendor/kreuzberg/tests/test_fastembed.rs +609 -0
  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 -0
  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 +536 -0
@@ -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
+ }