kreuzberg 4.10.2 → 4.10.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f9e454c85db9b78c82dc30871278c9faa132008ce57fb0b50d09712165a8e79
4
- data.tar.gz: e1c38eccc06b401149d0a5f6863867da5b9abbc9f95895b0e2e278509f30f047
3
+ metadata.gz: dfacdc270872e87de7d3be21475808d50ec8f5ca34655eef45c740007de1b222
4
+ data.tar.gz: bb632e33fc5dc77dbe3740eac10075541974c95999bd6af65dff138c0acb5dbf
5
5
  SHA512:
6
- metadata.gz: bad7bfa9ff4200bb49704677ac64aa6c4144df3e62bf402e664c1f2dafe2c88a6ab4a88597fabbb530d2c7c5c62923a7678f7babb592bf3432269645d0a9b9a0
7
- data.tar.gz: d63b9df95db3f5d632b454611c278e6eda637598a149ca1648b92709d96a3a869c9db9f2739bcc65acb31526d33d0db628a0abc05c5e0759272c88d828e0be0b
6
+ metadata.gz: 34206c5ddf11c69313124109d5cd81ae67f6ed8b3250636359f556a94fd79cc3fd37def7754e4dd46656aa7e286c0cb1bc88c0c2f2a36d841eb941da23cfb8b4
7
+ data.tar.gz: 2a1ff1c6d8a20045c329feae6d64e0a98f23dd11e1bf6887f1ef788031a2b191c08533c21e4ff149a7474b2257d6325dad28fe04e2fef594ee69dd9bd2e078d8
@@ -3,18 +3,18 @@ rustflags = ["-A", "unpredictable-function-pointer-comparisons", "-A", "fn_ptr_e
3
3
 
4
4
  [target.x86_64-pc-windows-gnu]
5
5
  rustflags = [
6
- "-A",
7
- "unpredictable-function-pointer-comparisons",
8
- "-A",
9
- "fn_ptr_eq",
10
- "-C",
11
- "link-arg=-lbcrypt",
12
- "-C",
13
- "link-arg=-lcrypt32",
14
- "-C",
15
- "link-arg=-lws2_32",
16
- "-C",
17
- "link-arg=-luserenv",
6
+ "-A",
7
+ "unpredictable-function-pointer-comparisons",
8
+ "-A",
9
+ "fn_ptr_eq",
10
+ "-C",
11
+ "link-arg=-lbcrypt",
12
+ "-C",
13
+ "link-arg=-lcrypt32",
14
+ "-C",
15
+ "link-arg=-lws2_32",
16
+ "-C",
17
+ "link-arg=-luserenv",
18
18
  ]
19
19
 
20
20
  [target.x86_64-pc-windows-msvc]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "kreuzberg-rb"
3
- version = "4.10.2"
3
+ version = "4.10.4"
4
4
  edition = "2024"
5
5
  rust-version = "1.91"
6
6
  authors = ["Na'aman Hirschfeld <naaman@kreuzberg.dev>"]
@@ -22,7 +22,7 @@ default = ["embeddings"]
22
22
  embeddings = ["kreuzberg/embeddings"]
23
23
 
24
24
  [dependencies]
25
- async-trait = "0.1.89"
25
+ async-trait = "0.1.92"
26
26
  kreuzberg = { path = "../../../vendor/kreuzberg", default-features = false, features = [
27
27
  "pdf",
28
28
  "excel",
@@ -49,13 +49,13 @@ kreuzberg-ffi = { path = "../../../vendor/kreuzberg-ffi" }
49
49
  magnus = { git = "https://github.com/matsadler/magnus", rev = "f6db11769efb517427bf7f121f9c32e18b059b38", features = [
50
50
  "rb-sys",
51
51
  ] }
52
- rb-sys = { version = "0.9.128", default-features = false, features = [
52
+ rb-sys = { version = "0.9.130", default-features = false, features = [
53
53
  "stable-api-compiled-fallback",
54
54
  ] }
55
- serde_json = "1.0.150"
56
- toml = "1.1.2"
55
+ serde_json = "1.0.151"
56
+ toml = "1.1.5"
57
57
  serde_yaml_ng = "0.10"
58
- tokio = { version = "1.52.3", features = [
58
+ tokio = { version = "1.53.1", features = [
59
59
  "rt",
60
60
  "rt-multi-thread",
61
61
  "macros",
@@ -65,7 +65,7 @@ tokio = { version = "1.52.3", features = [
65
65
  "time",
66
66
  "io-util",
67
67
  ] }
68
- html-to-markdown-rs = { version = "3.8.3", default-features = false }
68
+ html-to-markdown-rs = { version = "3.14.3", default-features = false }
69
69
 
70
70
  [dev-dependencies]
71
71
  pretty_assertions = "1.4"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kreuzberg
4
- VERSION = "4.10.2"
4
+ VERSION = "4.10.4"
5
5
  end
data/vendor/Cargo.toml CHANGED
@@ -2,7 +2,7 @@
2
2
  members = ["kreuzberg", "kreuzberg-ffi", "kreuzberg-tesseract", "kreuzberg-paddle-ocr", "kreuzberg-pdfium-render"]
3
3
 
4
4
  [workspace.package]
5
- version = "4.10.2"
5
+ version = "4.10.4"
6
6
  edition = "2024"
7
7
  rust-version = "1.91"
8
8
  authors = ["Na'aman Hirschfeld <naaman@kreuzberg.dev>"]
@@ -12,15 +12,15 @@ homepage = "https://kreuzberg.dev"
12
12
 
13
13
  [workspace.dependencies]
14
14
  ahash = { version = "0.8.12", features = ["serde"] }
15
- anyhow = "1.0.103"
16
- async-trait = "0.1.89"
17
- base64 = "0.22.1"
15
+ anyhow = "1.0.104"
16
+ async-trait = "0.1.92"
17
+ base64 = "0.23.1"
18
18
  blake3 = "1"
19
19
  bytes = { version = "1", features = ["serde"] }
20
- cfb = "0.14"
20
+ cfb = "0.15"
21
21
  chrono = "0.4"
22
22
  clap = { version = "4.6", features = ["derive", "color", "suggestions"] }
23
- comrak = { version = "0.54", default-features = false }
23
+ comrak = { version = "0.55", default-features = false }
24
24
  console_error_panic_hook = "0.1"
25
25
  criterion = { version = "0.8", features = ["html_reports"] }
26
26
  ctor = "1.0"
@@ -28,35 +28,35 @@ dbase = "0.8"
28
28
  futures = "0.3"
29
29
  getrandom = { version = "0.4.3", features = ["wasm_js"] }
30
30
  hex = "0.4.3"
31
- html-to-markdown-rs = { version = "3.8.3", default-features = false }
31
+ html-to-markdown-rs = { version = "3.14.3", default-features = false }
32
32
  image = { version = "0.25.10", default-features = false }
33
33
  itertools = "0.15"
34
34
  js-sys = "0.3"
35
- kreuzberg = { path = "./crates/kreuzberg", version = "4.10.2", default-features = false }
36
- kreuzberg-ffi = { path = "./crates/kreuzberg-ffi", version = "4.10.2" }
35
+ kreuzberg = { path = "./crates/kreuzberg", version = "4.10.4", default-features = false }
36
+ kreuzberg-ffi = { path = "./crates/kreuzberg-ffi", version = "4.10.4" }
37
37
  lazy_static = "1.5.0"
38
- libc = "0.2.186"
39
- liter-llm = { version = "1.9", features = ["native-http", "tracing"], default-features = false }
38
+ libc = "0.2.189"
39
+ liter-llm = { version = "2.0.3", features = ["native-http"], default-features = false }
40
40
  log = "0.4"
41
- lzma-rust2 = { version = "0.16.5" }
41
+ lzma-rust2 = { version = "0.21" }
42
42
  memmap2 = "0.9.11"
43
43
  minijinja = "2"
44
44
  num_cpus = "1.17.0"
45
45
  once_cell = "1.21.4"
46
- ort = { version = "2.0.0-rc.12", features = ["std", "api-18"], default-features = false }
46
+ ort = { version = "=2.0.0-rc.12", features = ["std", "api-18"], default-features = false }
47
47
  parking_lot = "0.12.5"
48
48
  pdfium-render = { package = "kreuzberg-pdfium-render", path = "crates/kreuzberg-pdfium-render", version = "4.3" }
49
49
  rayon = "1.12.0"
50
- reqwest = { version = "0.13.4", default-features = false }
51
- serde = { version = "1.0.228", features = ["derive"] }
52
- serde_json = { version = "1.0.150" }
50
+ reqwest = { version = "0.13.5", default-features = false }
51
+ serde = { version = "1.0.229", features = ["derive"] }
52
+ serde_json = { version = "1.0.151" }
53
53
  serde_toon_format = "0.1"
54
54
  tempfile = "3.27.0"
55
- thiserror = "2.0.18"
56
- tokio = { version = "1.52.3", features = ["rt", "rt-multi-thread", "macros", "sync", "process", "fs", "time", "io-util"] }
57
- toml = "1.1.2"
55
+ thiserror = "2.0.20"
56
+ tokio = { version = "1.53.1", features = ["rt", "rt-multi-thread", "macros", "sync", "process", "fs", "time", "io-util"] }
57
+ toml = "1.1.6"
58
58
  tracing = "0.1"
59
- tree-sitter-language-pack = { version = "1.12.5", features = ["serde"], default-features = false }
59
+ tree-sitter-language-pack = { version = "1.20.0", features = ["serde"], default-features = false }
60
60
  wasm-bindgen = { version = "0.2", features = ["enable-interning"] }
61
61
  wasm-bindgen-futures = "0.4"
62
62
  web-sys = { version = "0.3", features = ["Blob", "File", "FileReader", "console", "TextDecoder", "ImageData", "Window", "Response"] }
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "kreuzberg"
3
- version = "4.10.2"
3
+ version = "4.10.4"
4
4
  edition = "2024"
5
5
  rust-version = "1.91"
6
6
  authors = ["Na'aman Hirschfeld <naaman@kreuzberg.dev>"]
@@ -191,29 +191,29 @@ cli = [
191
191
 
192
192
  [dependencies]
193
193
  ahash = { version = "0.8.12", features = ["serde"] }
194
- async-trait = "0.1.89"
194
+ async-trait = "0.1.92"
195
195
  axum = { version = "0.8", features = ["macros", "json", "multipart"], optional = true }
196
- base64 = "0.22.1"
196
+ base64 = "0.23.1"
197
197
  biblatex = { version = "0.12", optional = true }
198
- biblib = { version = "0.7", default-features = false, features = ["ris", "pubmed", "xml"], optional = true }
198
+ biblib = { version = "0.8", default-features = false, features = ["ris", "pubmed", "xml"], optional = true }
199
199
  bitvec = "1.1"
200
200
  blake3 = "1"
201
201
  bytes = { version = "1", features = ["serde"] }
202
- calamine = { version = "0.36.0", features = ["dates"], optional = true }
203
- cfb = { version = "0.14", optional = true }
202
+ calamine = { version = "0.36.1", features = ["dates"], optional = true }
203
+ cfb = { version = "0.15", optional = true }
204
204
  chardetng = { version = "1.0.0", optional = true }
205
205
  chrono = { version = "0.4", optional = true }
206
- comrak = { version = "0.54", default-features = false }
206
+ comrak = { version = "0.55", default-features = false }
207
207
  dashmap = "6.2"
208
208
  dbase = { version = "0.8", optional = true }
209
- dirs = "6"
210
- encoding_rs = { version = "0.8.35" }
211
- fast_image_resize = { version = "6.0.0", optional = true }
209
+ dirs = "7"
210
+ encoding_rs = { version = "0.8.41" }
211
+ fast_image_resize = { version = "6.1.0", optional = true }
212
212
  flate2 = { version = "1.1", optional = true }
213
213
  hayro-jbig2 = { version = "0.3", default-features = false, features = ["std"], optional = true }
214
214
  hayro-jpeg2000 = { version = "0.3", default-features = false, features = ["std", "simd"], optional = true }
215
215
  hex = "0.4.3"
216
- html-to-markdown-rs = { version = "3.8.3", default-features = false, features = ["inline-images", "metadata"], optional = true }
216
+ html-to-markdown-rs = { version = "3.14.3", default-features = false, features = ["inline-images", "metadata"], optional = true }
217
217
  image = { version = "0.25.10", default-features = false, features = [
218
218
  "png",
219
219
  "jpeg",
@@ -224,17 +224,17 @@ image = { version = "0.25.10", default-features = false, features = [
224
224
  "pnm",
225
225
  "rayon",
226
226
  ], optional = true }
227
- indexmap = "2.14.0"
228
- infer = "0.19.0"
227
+ indexmap = "2.14.2"
228
+ infer = "0.22.0"
229
229
  jotdown = "0.10"
230
230
  kamadak-exif = { version = "0.6.1", optional = true }
231
231
 
232
232
  kreuzberg-tesseract = { path = "../kreuzberg-tesseract", optional = true }
233
- libc = "0.2.186"
234
- liter-llm = { version = "1.9", features = ["native-http", "tracing"], default-features = false, optional = true }
233
+ libc = "0.2.189"
234
+ liter-llm = { version = "2.0.3", features = ["native-http"], default-features = false, optional = true }
235
235
  log = "0.4"
236
- lopdf = { version = "0.44.0", default-features = false, features = ["chrono", "rayon"], optional = true }
237
- mail-parser = { version = "0.11.5", optional = true }
236
+ lopdf = { version = "0.45.0", default-features = false, features = ["chrono", "rayon"], optional = true }
237
+ mail-parser = { version = "0.11.9", optional = true }
238
238
  memchr = "2.8.3"
239
239
  memmap2 = "0.9.11"
240
240
  mime_guess = "2.0"
@@ -245,17 +245,17 @@ once_cell = "1.21.4"
245
245
  opentelemetry = { version = "0.32", features = ["trace"], optional = true }
246
246
  opentelemetry_sdk = { version = "0.32", features = ["rt-tokio"], optional = true }
247
247
  org = { version = "0.3", optional = true }
248
- ort = { version = "2.0.0-rc.12", default-features = false, features = ["std", "ndarray", "api-18"], optional = true }
248
+ ort = { version = "=2.0.0-rc.12", default-features = false, features = ["std", "ndarray", "api-18"], optional = true }
249
249
  outlook-pst = { version = "1.2.0", optional = true }
250
250
  parking_lot = "0.12.5"
251
251
  pastey = "0.2"
252
252
  pdfium-render = { package = "kreuzberg-pdfium-render", path = "../kreuzberg-pdfium-render", features = ["thread_safe", "image_latest"], optional = true }
253
253
  pulldown-cmark = { version = "0.13" }
254
- quick-xml = { version = "0.41.0", features = ["serialize"], optional = true }
254
+ quick-xml = { version = "=0.41.0", features = ["serialize"], optional = true }
255
255
  rake = { version = "0.3.6", optional = true }
256
256
  rayon = "1.12.0"
257
- regex = "1.13.0"
258
- rmcp = { version = "2.2.0", features = [
257
+ regex = "1.13.1"
258
+ rmcp = { version = "3.4.0", features = [
259
259
  "server",
260
260
  "macros",
261
261
  "base64",
@@ -266,8 +266,8 @@ rmcp = { version = "2.2.0", features = [
266
266
  rmp-serde = "1.3"
267
267
 
268
268
  roxmltree = { version = "0.21.1", optional = true }
269
- serde = { version = "1.0.228", features = ["derive"] }
270
- serde_json = { version = "1.0.150" }
269
+ serde = { version = "1.0.229", features = ["derive"] }
270
+ serde_json = { version = "1.0.151" }
271
271
  serde_toon_format = "0.1"
272
272
  serde_yaml_ng = "0.10.0"
273
273
  sevenz-rust2 = { version = "0.20.2", optional = true }
@@ -277,13 +277,13 @@ snap = { version = "1.1", optional = true }
277
277
  tar = { version = "0.4.46", optional = true }
278
278
  tempfile = { version = "3.27.0", optional = true }
279
279
  text-splitter = { version = "0.32.0", features = ["markdown"], optional = true }
280
- thiserror = "2.0.18"
280
+ thiserror = "2.0.20"
281
281
  tiff = { version = "0.11", optional = true }
282
282
  # Keep aligned with text-splitter's optional tokenizers integration so ChunkSizer
283
283
  # is implemented for the same Tokenizer type used by Kreuzberg.
284
- tokenizers = { version = "0.23.1", optional = true, default-features = false, features = ["http", "fancy-regex"] }
285
- tokio = { version = "1.52.3", features = ["rt", "rt-multi-thread", "macros", "sync", "process", "fs", "time", "io-util"], optional = true }
286
- toml = "1.1.2"
284
+ tokenizers = { version = "0.23.2", optional = true, default-features = false, features = ["http", "fancy-regex"] }
285
+ tokio = { version = "1.53.1", features = ["rt", "rt-multi-thread", "macros", "sync", "process", "fs", "time", "io-util"], optional = true }
286
+ toml = "1.1.6"
287
287
  tower = { version = "0.5", features = ["timeout", "limit", "util"], optional = true }
288
288
  tower-http = { version = "0.7", features = [
289
289
  "cors",
@@ -310,13 +310,13 @@ hf-hub = { version = "1.0", default-features = false, features = ["blocking", "r
310
310
  # PaddleOCR via ONNX Runtime - not available on WASM (vendored from paddle-ocr-rs)
311
311
  kreuzberg-paddle-ocr = { path = "../kreuzberg-paddle-ocr", optional = true }
312
312
  pprof = { version = "0.15.0", features = ["flamegraph"], optional = true }
313
- ureq = { version = "3.3", default-features = false, features = ["rustls", "json"], optional = true }
313
+ ureq = { version = "3.4", default-features = false, features = ["rustls", "json"], optional = true }
314
314
 
315
315
  [target.'cfg(all(target_os = "windows", not(target_arch = "wasm32")))'.dependencies]
316
316
  hf-hub = { version = "1.0", default-features = false, features = ["blocking", "rustls-tls"], optional = true }
317
317
  # PaddleOCR via ONNX Runtime - not available on WASM (vendored from paddle-ocr-rs)
318
318
  kreuzberg-paddle-ocr = { path = "../kreuzberg-paddle-ocr", optional = true }
319
- ureq = { version = "3.3", default-features = false, features = ["native-tls", "json"], optional = true }
319
+ ureq = { version = "3.4", default-features = false, features = ["native-tls", "json"], optional = true }
320
320
 
321
321
  [target.'cfg(not(target_arch = "wasm32"))'.dependencies.tree-sitter-language-pack]
322
322
  workspace = true
@@ -327,7 +327,7 @@ optional = true
327
327
  # Override getrandom to enable js feature for WASM targets
328
328
  # This is needed because ring/rustls (via ureq) depend on getrandom without js feature
329
329
  getrandom = { version = "0.4.3", features = ["wasm_js"] }
330
- tree-sitter-language-pack = { version = "1.12.5", features = ["serde"], default-features = false, optional = true }
330
+ tree-sitter-language-pack = { version = "1.20.0", features = ["serde"], default-features = false, optional = true }
331
331
  wasm-bindgen-rayon = { version = "1.3", optional = true }
332
332
 
333
333
  [build-dependencies]
@@ -335,16 +335,16 @@ pkg-config = "0.3"
335
335
  tracing = "0.1"
336
336
 
337
337
  [dev-dependencies]
338
- anyhow = "1.0.103"
338
+ anyhow = "1.0.104"
339
339
  criterion = { version = "0.8", features = ["html_reports"] }
340
340
  dotenvy = "0.15"
341
341
  filetime = "0.2"
342
342
  image = { version = "0.25.10", default-features = false, features = ["png"] }
343
- jsonschema = "0.47"
343
+ jsonschema = "0.56"
344
344
  serial_test = "3.5.0"
345
345
  tar = "0.4.46"
346
346
  tempfile = "3.27.0"
347
- tokio = { version = "1.52.3", features = ["macros", "time"] }
347
+ tokio = { version = "1.53.1", features = ["macros", "time"] }
348
348
  tokio-test = "0.4"
349
349
  tracing-subscriber = { version = "0.3", features = ["env-filter"] }
350
350
  zip = { version = ">=7.0.0, <8.6.0", default-features = false, features = ["deflate-flate2"] }
@@ -60,7 +60,7 @@ High-performance document intelligence library for Rust. Extract text, metadata,
60
60
 
61
61
  This is the core Rust library that powers the Python, TypeScript, and Ruby bindings.
62
62
 
63
- > **🚀 Version 4.10.2 Release**
63
+ > **🚀 Version 4.10.4 Release**
64
64
  > This is a pre-release version. We invite you to test the library and [report any issues](https://github.com/kreuzberg-dev/kreuzberg-lts/issues) you encounter.
65
65
  >
66
66
  > **Note**: The Rust crate is not currently published to crates.io for this RC. Use git dependencies or language bindings (Python, TypeScript, Ruby) instead.
@@ -175,16 +175,36 @@ impl ExtractionConfig {
175
175
  Ok((*config_arc).clone())
176
176
  }
177
177
 
178
- /// Discover configuration file in parent directories.
178
+ /// Discover a configuration file.
179
179
  ///
180
- /// Searches for `kreuzberg.toml` in current directory and parent directories.
180
+ /// Resolution order:
181
+ /// 1. `kreuzberg.toml` in the current directory, then each parent up to the
182
+ /// filesystem root (project-local config wins).
183
+ /// 2. The user-level config directory as a fallback, resolved
184
+ /// platform-natively via [`dirs::config_dir`] (same approach as the cache
185
+ /// dir): `$XDG_CONFIG_HOME/kreuzberg/kreuzberg.toml` or
186
+ /// `~/.config/kreuzberg/kreuzberg.toml` on Linux,
187
+ /// `~/Library/Application Support/kreuzberg/kreuzberg.toml` on macOS,
188
+ /// `%APPDATA%\kreuzberg\kreuzberg.toml` on Windows.
181
189
  ///
182
190
  /// # Returns
183
191
  ///
184
192
  /// - `Some(config)` if found
185
193
  /// - `None` if no config file found
186
194
  pub fn discover() -> Result<Option<Self>> {
187
- let mut current = std::env::current_dir().map_err(KreuzbergError::Io)?;
195
+ let cwd = std::env::current_dir().map_err(KreuzbergError::Io)?;
196
+ let global = dirs::config_dir().map(|dir| dir.join("kreuzberg").join("kreuzberg.toml"));
197
+ Self::discover_from(&cwd, global.as_deref())
198
+ }
199
+
200
+ /// Walk up from `start` looking for `kreuzberg.toml`, then fall back to the
201
+ /// user-level `global` config path if provided.
202
+ ///
203
+ /// Split out from [`discover`](Self::discover) so the resolution order is
204
+ /// unit-testable without mutating process-global state (the working
205
+ /// directory or `HOME`).
206
+ fn discover_from(start: &Path, global: Option<&Path>) -> Result<Option<Self>> {
207
+ let mut current = start.to_path_buf();
188
208
 
189
209
  loop {
190
210
  let kreuzberg_toml = current.join("kreuzberg.toml");
@@ -199,6 +219,59 @@ impl ExtractionConfig {
199
219
  }
200
220
  }
201
221
 
222
+ if let Some(global) = global
223
+ && global.exists()
224
+ {
225
+ return Ok(Some(Self::from_toml_file(global)?));
226
+ }
227
+
202
228
  Ok(None)
203
229
  }
204
230
  }
231
+
232
+ #[cfg(test)]
233
+ mod discover_tests {
234
+ use super::*;
235
+ use std::fs;
236
+
237
+ const TOML: &str = "[ocr]\nenabled = true\n";
238
+
239
+ #[test]
240
+ fn project_config_takes_precedence_over_global() {
241
+ // Project config has an [ocr] section; the global fallback does not, so
242
+ // `ocr.is_some()` proves the project-local file was the one loaded.
243
+ let project = tempfile::tempdir().expect("tempdir");
244
+ fs::write(project.path().join("kreuzberg.toml"), TOML).expect("write project");
245
+ let global = tempfile::tempdir().expect("tempdir");
246
+ let global_file = global.path().join("kreuzberg.toml");
247
+ fs::write(&global_file, "output_format = \"markdown\"\n").expect("write global");
248
+
249
+ let found = ExtractionConfig::discover_from(project.path(), Some(&global_file))
250
+ .expect("discover")
251
+ .expect("some config");
252
+ assert!(
253
+ found.ocr.is_some(),
254
+ "project-local config must win over the global fallback"
255
+ );
256
+ }
257
+
258
+ #[test]
259
+ fn falls_back_to_global_when_no_project_config() {
260
+ let empty = tempfile::tempdir().expect("tempdir");
261
+ let global = tempfile::tempdir().expect("tempdir");
262
+ let global_file = global.path().join("kreuzberg.toml");
263
+ fs::write(&global_file, TOML).expect("write global");
264
+
265
+ let found = ExtractionConfig::discover_from(empty.path(), Some(&global_file)).expect("discover");
266
+ assert!(found.is_some(), "should fall back to the global config file");
267
+ }
268
+
269
+ #[test]
270
+ fn returns_none_when_no_config_anywhere() {
271
+ let empty = tempfile::tempdir().expect("tempdir");
272
+ let missing = empty.path().join("nope").join("kreuzberg.toml");
273
+
274
+ let found = ExtractionConfig::discover_from(empty.path(), Some(&missing)).expect("discover");
275
+ assert!(found.is_none(), "no project or global config should yield None");
276
+ }
277
+ }
@@ -156,6 +156,7 @@ impl From<&TreeSitterProcessConfig> for tree_sitter_language_pack::ProcessConfig
156
156
  diagnostics: p.diagnostics,
157
157
  chunk_max_size: p.chunk_max_size,
158
158
  data_extraction: false,
159
+ ..Default::default()
159
160
  }
160
161
  }
161
162
  }
@@ -8,6 +8,12 @@
8
8
  use crate::error::{KreuzbergError, Result};
9
9
  use std::io::Cursor;
10
10
 
11
+ /// Zero-based pair index of fcClx/lcbClx in FibRgFcLcb97.
12
+ ///
13
+ /// See [MS-DOC] section 2.5.6:
14
+ /// <https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-doc/0c9df81f-98d0-454e-ad84-b612cd05b1a4>
15
+ const CLX_PAIR_INDEX: usize = 33;
16
+
11
17
  /// Result of DOC text extraction.
12
18
  pub struct DocExtractionResult {
13
19
  /// Extracted text content.
@@ -116,10 +122,14 @@ fn extract_text_word97(word_doc: &[u8], table_stream: &[u8]) -> Result<String> {
116
122
  return Err(KreuzbergError::parsing("FIB too short for cbRgFcLcb"));
117
123
  }
118
124
 
119
- let _ = u16::from_le_bytes([word_doc[cbrgfclcb_offset], word_doc[cbrgfclcb_offset + 1]]) as usize;
125
+ let cb_rg_fc_lcb = u16::from_le_bytes([word_doc[cbrgfclcb_offset], word_doc[cbrgfclcb_offset + 1]]) as usize;
120
126
  let rg_fc_lcb_offset = cbrgfclcb_offset + 2;
121
127
 
122
- let fc_clx_offset = rg_fc_lcb_offset + 66 * 8;
128
+ if cb_rg_fc_lcb <= CLX_PAIR_INDEX {
129
+ return extract_text_contiguous(word_doc, ccp_text);
130
+ }
131
+
132
+ let fc_clx_offset = rg_fc_lcb_offset + CLX_PAIR_INDEX * 8;
123
133
  let lcb_clx_offset = fc_clx_offset + 4;
124
134
 
125
135
  if word_doc.len() < lcb_clx_offset + 4 {
@@ -673,4 +683,33 @@ mod tests {
673
683
  let result = extract_doc_text(b"not a doc file");
674
684
  assert!(result.is_err());
675
685
  }
686
+
687
+ #[test]
688
+ fn test_extract_word97_reads_clx_from_fib_rg_fc_lcb97_pair_33() {
689
+ let mut word_doc = vec![0_u8; 900];
690
+ word_doc[32..34].copy_from_slice(&14_u16.to_le_bytes());
691
+ word_doc[62..64].copy_from_slice(&22_u16.to_le_bytes());
692
+ word_doc[76..80].copy_from_slice(&1_u32.to_le_bytes());
693
+
694
+ let rg_fc_lcb_offset = 154;
695
+ word_doc[152..154].copy_from_slice(&34_u16.to_le_bytes());
696
+ let clx_pair_offset = rg_fc_lcb_offset + 33 * 8;
697
+ word_doc[clx_pair_offset..clx_pair_offset + 4].copy_from_slice(&8_u32.to_le_bytes());
698
+ word_doc[clx_pair_offset + 4..clx_pair_offset + 8].copy_from_slice(&21_u32.to_le_bytes());
699
+
700
+ let text_offset = 800_usize;
701
+ word_doc[text_offset] = 0xE9;
702
+
703
+ let mut table_stream = vec![0_u8; 29];
704
+ table_stream[8] = 0x02;
705
+ table_stream[9..13].copy_from_slice(&16_u32.to_le_bytes());
706
+ table_stream[13..17].copy_from_slice(&0_u32.to_le_bytes());
707
+ table_stream[17..21].copy_from_slice(&1_u32.to_le_bytes());
708
+ let compressed_fc = 0x4000_0000_u32 | (text_offset as u32 * 2);
709
+ table_stream[23..27].copy_from_slice(&compressed_fc.to_le_bytes());
710
+
711
+ let text = extract_text_word97(&word_doc, &table_stream).expect("failed to read synthetic Word 97 text");
712
+
713
+ assert_eq!(text, "é");
714
+ }
676
715
  }
@@ -9,6 +9,11 @@
9
9
  //! - Removed file-path based APIs (we only need bytes/reader)
10
10
  //! - Added markdown rendering and formatting support (fixes #376)
11
11
 
12
+ // TODO(kreuzberg-dev/kreuzberg-lts#4): 4 cyclomatic-complexity and 46 size/complexity findings
13
+ // in this file, currently excluded via the quality-debt baseline in poly.toml. Splitting
14
+ // these needs compiler-in-the-loop verification, not a mechanical pass. Delete this
15
+ // note and the file's baseline entry together once it goes green. Help wanted.
16
+
12
17
  use ahash::AHashMap;
13
18
  use std::io::{Cursor, Read, Seek};
14
19
 
@@ -7,7 +7,7 @@ use std::borrow::Cow;
7
7
  use std::sync::LazyLock;
8
8
 
9
9
  use crate::Result;
10
- use crate::core::config::ExtractionConfig;
10
+ use crate::core::config::{ExtractionConfig, OutputFormat};
11
11
  use crate::plugins::{DocumentExtractor, Plugin};
12
12
  use crate::text::utf8_validation;
13
13
  use crate::types::Table;
@@ -71,7 +71,7 @@ impl DocumentExtractor for CsvExtractor {
71
71
  &self,
72
72
  content: &[u8],
73
73
  mime_type: &str,
74
- _config: &ExtractionConfig,
74
+ config: &ExtractionConfig,
75
75
  ) -> Result<InternalDocument> {
76
76
  tracing::debug!(format = "csv", size_bytes = content.len(), "extraction starting");
77
77
  let text = decode_csv_bytes(content);
@@ -114,14 +114,21 @@ impl DocumentExtractor for CsvExtractor {
114
114
  };
115
115
 
116
116
  let mut builder = InternalDocumentBuilder::new("csv");
117
- let cloned_table = Table {
118
- cells: table.cells.clone(),
119
- markdown: table.markdown.clone(),
120
- page_number: table.page_number,
121
- bounding_box: table.bounding_box,
122
- };
123
- builder.push_table(cloned_table, None, None);
117
+ if matches!(config.output_format, OutputFormat::Plain) {
118
+ let content_text = build_content_text(&table.cells, has_header);
119
+ for section in content_text.split("\n\n") {
120
+ let section = section.trim();
121
+ if !section.is_empty() {
122
+ builder.push_paragraph(section, vec![], None, None);
123
+ }
124
+ }
125
+ } else {
126
+ builder.push_table(table.clone(), None, None);
127
+ }
124
128
  let mut doc = builder.build();
129
+ if matches!(config.output_format, OutputFormat::Plain) {
130
+ doc.tables.push(table);
131
+ }
125
132
  doc.mime_type = Cow::Owned(mime_type.to_string());
126
133
 
127
134
  doc.metadata = Metadata {
@@ -392,6 +399,51 @@ fn infer_column_types(rows: &[Vec<String>], has_header: bool) -> Vec<String> {
392
399
  .collect()
393
400
  }
394
401
 
402
+ /// Build embedding-friendly labeled text for a header/data-row CSV.
403
+ ///
404
+ /// Each data row becomes a `Row N:` block with `Header: value` pairs for its
405
+ /// non-empty cells, separated by blank lines. Rows without a header fall back
406
+ /// to space-joined cells so plain output stays readable.
407
+ fn build_content_text(rows: &[Vec<String>], has_header: bool) -> String {
408
+ if rows.is_empty() {
409
+ return String::new();
410
+ }
411
+
412
+ if !has_header || rows.len() < 2 {
413
+ return rows
414
+ .iter()
415
+ .map(|row| {
416
+ row.iter()
417
+ .map(|cell| cell.trim())
418
+ .filter(|cell| !cell.is_empty())
419
+ .collect::<Vec<_>>()
420
+ .join(" ")
421
+ })
422
+ .filter(|line| !line.is_empty())
423
+ .collect::<Vec<_>>()
424
+ .join("\n");
425
+ }
426
+
427
+ let headers = &rows[0];
428
+ let mut sections = Vec::with_capacity(rows.len() - 1);
429
+
430
+ for (i, row) in rows[1..].iter().enumerate() {
431
+ let mut lines = vec![format!("Row {}:", i + 1)];
432
+ for (header, value) in headers.iter().zip(row.iter()) {
433
+ let h = header.trim();
434
+ let v = value.trim();
435
+ if !h.is_empty() && !v.is_empty() {
436
+ lines.push(format!(" {}: {}", h, v));
437
+ }
438
+ }
439
+ if lines.len() > 1 {
440
+ sections.push(lines.join("\n"));
441
+ }
442
+ }
443
+
444
+ sections.join("\n\n")
445
+ }
446
+
395
447
  /// Build a Markdown table from parsed rows.
396
448
  fn build_markdown_table(rows: &[Vec<String>]) -> String {
397
449
  if rows.is_empty() {
@@ -15,6 +15,11 @@
15
15
  //! - Tables
16
16
  //! - Cross-references and links
17
17
 
18
+ // TODO(kreuzberg-dev/kreuzberg-lts#4): 4 cyclomatic-complexity and 27 size/complexity findings
19
+ // in this file, currently excluded via the quality-debt baseline in poly.toml. Splitting
20
+ // these needs compiler-in-the-loop verification, not a mechanical pass. Delete this
21
+ // note and the file's baseline entry together once it goes green. Help wanted.
22
+
18
23
  use crate::Result;
19
24
  use crate::core::config::ExtractionConfig;
20
25
  use crate::extraction::{cells_to_markdown, cells_to_text};