From fba0a644ef1107b4e0569f27268b9cf5b4d0af6c Mon Sep 17 00:00:00 2001 From: Abimael Martell Date: Thu, 2 Apr 2026 14:39:42 -0700 Subject: [PATCH] fix: replace partial_cmp with total_cmp to prevent NaN sort panics (#20) * fix sort panics on NaN values from bogus PDF font metrics Replace all `partial_cmp(...).unwrap_or(Ordering::Equal)` and bare `partial_cmp(...).unwrap()` with `total_cmp()` across the codebase. `partial_cmp` returns `None` for NaN, and mapping that to `Equal` violates total ordering: `a == NaN` and `NaN == b` but `a != b`. Rust 1.81+ detects this and panics in sort_by. `total_cmp` handles NaN deterministically (sorts to end) and guarantees total ordering. The critical crash was in `extract_text_in_regions` (lib.rs:478) where PDFs with bogus font ascent/descent values produced NaN in text item coordinates, causing process abort via NAPI. Co-Authored-By: Claude Opus 4.6 (1M context) * fix missed partial_cmp in layout.rs and restore napi exports - Convert two remaining b.y.partial_cmp(&a.y) calls to total_cmp in group_single_column and column layout sorting - Restore missing napi exports: detectPdf, extractText, extractTextWithPositions, processPdf Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- napi/index.js | 104 ++++++++++++++++----------------- src/extractor/layout.rs | 36 +++++------- src/extractor/mod.rs | 11 ++-- src/lib.rs | 29 ++++++--- src/markdown/analysis.rs | 4 +- src/markdown/mod.rs | 8 +-- src/markdown/preprocess.rs | 2 +- src/tables/detect_heuristic.rs | 16 +++-- src/tables/detect_lines.rs | 2 +- src/tables/detect_rects.rs | 34 +++++------ src/tables/grid.rs | 8 +-- src/tables/mod.rs | 12 ++-- src/text_utils.rs | 8 +-- 13 files changed, 137 insertions(+), 137 deletions(-) diff --git a/napi/index.js b/napi/index.js index df6cd7e..41fb6d2 100644 --- a/napi/index.js +++ b/napi/index.js @@ -77,8 +77,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-android-arm64') const bindingPackageVersion = require('firecrawl-pdf-inspector-android-arm64/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -93,8 +93,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-android-arm-eabi') const bindingPackageVersion = require('firecrawl-pdf-inspector-android-arm-eabi/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -114,8 +114,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-win32-x64-gnu') const bindingPackageVersion = require('firecrawl-pdf-inspector-win32-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -130,8 +130,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-win32-x64-msvc') const bindingPackageVersion = require('firecrawl-pdf-inspector-win32-x64-msvc/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -147,8 +147,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-win32-ia32-msvc') const bindingPackageVersion = require('firecrawl-pdf-inspector-win32-ia32-msvc/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -163,8 +163,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-win32-arm64-msvc') const bindingPackageVersion = require('firecrawl-pdf-inspector-win32-arm64-msvc/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -182,8 +182,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-darwin-universal') const bindingPackageVersion = require('firecrawl-pdf-inspector-darwin-universal/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -198,8 +198,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-darwin-x64') const bindingPackageVersion = require('firecrawl-pdf-inspector-darwin-x64/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -214,8 +214,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-darwin-arm64') const bindingPackageVersion = require('firecrawl-pdf-inspector-darwin-arm64/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -234,8 +234,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-freebsd-x64') const bindingPackageVersion = require('firecrawl-pdf-inspector-freebsd-x64/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -250,8 +250,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-freebsd-arm64') const bindingPackageVersion = require('firecrawl-pdf-inspector-freebsd-arm64/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -271,8 +271,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-linux-x64-musl') const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-x64-musl/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -287,8 +287,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-linux-x64-gnu') const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -305,8 +305,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-linux-arm64-musl') const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-arm64-musl/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -321,8 +321,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-linux-arm64-gnu') const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-arm64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -339,8 +339,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-linux-arm-musleabihf') const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-arm-musleabihf/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -355,8 +355,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-linux-arm-gnueabihf') const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-arm-gnueabihf/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -373,8 +373,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-linux-loong64-musl') const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-loong64-musl/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -389,8 +389,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-linux-loong64-gnu') const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-loong64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -407,8 +407,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-linux-riscv64-musl') const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-riscv64-musl/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -423,8 +423,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-linux-riscv64-gnu') const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-riscv64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -440,8 +440,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-linux-ppc64-gnu') const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-ppc64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -456,8 +456,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-linux-s390x-gnu') const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-s390x-gnu/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -476,8 +476,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-openharmony-arm64') const bindingPackageVersion = require('firecrawl-pdf-inspector-openharmony-arm64/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -492,8 +492,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-openharmony-x64') const bindingPackageVersion = require('firecrawl-pdf-inspector-openharmony-x64/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -508,8 +508,8 @@ function requireNative() { try { const binding = require('firecrawl-pdf-inspector-openharmony-arm') const bindingPackageVersion = require('firecrawl-pdf-inspector-openharmony-arm/package.json').version - if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { diff --git a/src/extractor/layout.rs b/src/extractor/layout.rs index 60cf114..028530f 100644 --- a/src/extractor/layout.rs +++ b/src/extractor/layout.rs @@ -218,7 +218,7 @@ fn columns_have_prose(columns: &[ColumnRegion], items: &[&TextItem]) -> bool { // Sort by Y descending (top of page = higher Y in PDF coords) let mut sorted: Vec<&TextItem> = col_items; - sorted.sort_by(|a, b| b.y.partial_cmp(&a.y).unwrap_or(std::cmp::Ordering::Equal)); + sorted.sort_by(|a, b| b.y.total_cmp(&a.y)); // Group into lines by Y-proximity and measure fill + item count let mut full_lines = 0usize; @@ -616,7 +616,7 @@ fn identify_spanning_lines(items: &[TextItem], columns: &[ColumnRegion]) -> Vec< // Build (original_index, y) pairs sorted by Y descending for grouping let mut indexed: Vec<(usize, f32)> = items.iter().enumerate().map(|(i, it)| (i, it.y)).collect(); - indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); + indexed.sort_by(|a, b| b.1.total_cmp(&a.1)); // Group by Y-proximity into rough lines (as index sets) let mut groups: Vec> = Vec::new(); @@ -778,7 +778,7 @@ pub(crate) fn is_newspaper_layout( return 0.0; } let mut ys: Vec = lines.iter().map(|l| l.y).collect(); - ys.sort_by(|a, b| a.partial_cmp(b).unwrap()); + ys.sort_by(|a, b| a.total_cmp(b)); let span = ys.last().unwrap() - ys.first().unwrap(); span / (lines.len() as f32 - 1.0) }; @@ -845,7 +845,7 @@ fn split_column_stragglers(lines: Vec) -> (Vec, Vec 3× median (min 30pt) indicates a break between content clusters @@ -1078,9 +1078,8 @@ pub(crate) fn group_into_lines_with_thresholds( } } - above.sort_by(|a, b| b.y.partial_cmp(&a.y).unwrap_or(std::cmp::Ordering::Equal)); - below_spanning - .sort_by(|a, b| b.y.partial_cmp(&a.y).unwrap_or(std::cmp::Ordering::Equal)); + above.sort_by(|a, b| b.y.total_cmp(&a.y)); + below_spanning.sort_by(|a, b| b.y.total_cmp(&a.y)); all_lines.extend(above); for col in core_columns { @@ -1101,16 +1100,13 @@ pub(crate) fn group_into_lines_with_thresholds( // Sort by Y descending (top-first), then by X for same-Y lines all_page_lines.sort_by(|a, b| { - b.y.partial_cmp(&a.y) - .unwrap_or(std::cmp::Ordering::Equal) - .then( - a.items - .first() - .map(|i| i.x) - .unwrap_or(0.0) - .partial_cmp(&b.items.first().map(|i| i.x).unwrap_or(0.0)) - .unwrap_or(std::cmp::Ordering::Equal), - ) + b.y.total_cmp(&a.y).then( + a.items + .first() + .map(|i| i.x) + .unwrap_or(0.0) + .total_cmp(&b.items.first().map(|i| i.x).unwrap_or(0.0)), + ) }); // Merge lines at the same Y (within tolerance) into single lines @@ -1185,11 +1181,7 @@ fn group_single_column(items: Vec, adaptive_threshold: f32) -> Vec) -> Vec { for (_, _, group) in &mut line_groups { let rtl = is_rtl_text(group.iter().map(|i| &i.text)); if rtl { - group.sort_by(|a, b| b.x.partial_cmp(&a.x).unwrap_or(std::cmp::Ordering::Equal)); + group.sort_by(|a, b| b.x.total_cmp(&a.x)); } else { - group.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal)); + group.sort_by(|a, b| a.x.total_cmp(&b.x)); } } // Sort groups by page then Y descending (top of page first) - line_groups.sort_by(|a, b| { - a.0.cmp(&b.0) - .then_with(|| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)) - }); + line_groups.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| b.1.total_cmp(&a.1))); let mut merged = Vec::new(); @@ -452,7 +449,7 @@ pub(crate) fn merge_subscript_items(items: Vec) -> Vec { for (_, _, mut group) in line_groups { // Sort by X position - group.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal)); + group.sort_by(|a, b| a.x.total_cmp(&b.x)); // Find the dominant (most common) font size in this group let max_fs = group.iter().map(|i| i.font_size).fold(0.0_f32, f32::max); diff --git a/src/lib.rs b/src/lib.rs index 3c61add..9dc81ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -477,16 +477,29 @@ fn collect_text_in_region( return String::new(); } - // Sort top→bottom (descending Y in bottom-left coords), then left→right + // Sort top→bottom (descending Y in bottom-left coords), then left→right. + // Uses total_cmp to avoid panics on NaN values from bogus font metrics. matched.sort_by(|a, b| { - let line_threshold = a.font_size.max(b.font_size) * 0.5; - let y_diff = b.y - a.y; // descending Y = top to bottom - if y_diff.abs() < line_threshold { - a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal) + let fs_a = if a.font_size.is_finite() { + a.font_size } else { - y_diff - .partial_cmp(&0.0_f32) - .unwrap_or(std::cmp::Ordering::Equal) + 0.0 + }; + let fs_b = if b.font_size.is_finite() { + b.font_size + } else { + 0.0 + }; + let line_threshold = fs_a.max(fs_b) * 0.5; + let ay = if a.y.is_finite() { a.y } else { 0.0 }; + let by = if b.y.is_finite() { b.y } else { 0.0 }; + let y_diff = by - ay; // descending Y = top to bottom + if y_diff.abs() < line_threshold { + let ax = if a.x.is_finite() { a.x } else { 0.0 }; + let bx = if b.x.is_finite() { b.x } else { 0.0 }; + ax.total_cmp(&bx) + } else { + by.total_cmp(&ay) } }); diff --git a/src/markdown/analysis.rs b/src/markdown/analysis.rs index 65bb126..334d75e 100644 --- a/src/markdown/analysis.rs +++ b/src/markdown/analysis.rs @@ -121,7 +121,7 @@ pub(crate) fn compute_paragraph_threshold(lines: &[TextLine], base_size: f32) -> return fallback; } - gaps.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + gaps.sort_by(|a, b| a.total_cmp(b)); let median = gaps[gaps.len() / 2]; @@ -221,7 +221,7 @@ pub(crate) fn compute_heading_tiers(lines: &[TextLine], base_size: f32) -> Vec = Vec::new(); diff --git a/src/markdown/mod.rs b/src/markdown/mod.rs index 84a1471..1a7fd17 100644 --- a/src/markdown/mod.rs +++ b/src/markdown/mod.rs @@ -42,7 +42,7 @@ pub(crate) fn split_side_by_side(items: &[TextItem]) -> Vec<(f32, f32)> { // Sort items by left edge let mut xs: Vec = items.iter().map(|i| i.x).collect(); - xs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + xs.sort_by(|a, b| a.total_cmp(b)); // Find all candidate gaps: ≥30pt, in the middle 60% of the X range, // with ≥20 items on each side. @@ -118,7 +118,7 @@ pub(crate) fn split_side_by_side(items: &[TextItem]) -> Vec<(f32, f32)> { }) .copied() .collect(); - balanced_positions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + balanced_positions.sort_by(|a, b| a.total_cmp(b)); balanced_positions.dedup_by(|a, b| (*a - *b).abs() < 50.0); if balanced_positions.len() > 1 { return vec![]; @@ -209,7 +209,7 @@ fn split_from_hint_regions(items: &[TextItem], rects: &[PdfRect], page: u32) -> // Width outlier filter (same as detect_tables_from_rects) let mut widths: Vec = page_rects.iter().map(|&(_, _, w, _)| w).collect(); - widths.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + widths.sort_by(|a, b| a.total_cmp(b)); let median_width = widths[widths.len() / 2]; page_rects.retain(|&(_, _, w, _)| w <= median_width * 10.0); @@ -1037,7 +1037,7 @@ pub(crate) fn to_markdown_from_items_with_rects_and_lines( } // Sort by Y descending (top to bottom) so left and right // band lines interleave in visual reading order. - page_lines.sort_by(|a, b| b.y.partial_cmp(&a.y).unwrap_or(std::cmp::Ordering::Equal)); + page_lines.sort_by(|a, b| b.y.total_cmp(&a.y)); all_lines.extend(page_lines); } all_lines diff --git a/src/markdown/preprocess.rs b/src/markdown/preprocess.rs index 657440b..00ccec7 100644 --- a/src/markdown/preprocess.rs +++ b/src/markdown/preprocess.rs @@ -275,7 +275,7 @@ pub(crate) fn strip_repeated_lines(lines: Vec, page_count: u32) -> Vec page_sorted_ys.entry(line.page).or_default().push(line.y); } for ys in page_sorted_ys.values_mut() { - ys.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + ys.sort_by(|a, b| a.total_cmp(b)); ys.dedup(); } diff --git a/src/tables/detect_heuristic.rs b/src/tables/detect_heuristic.rs index d2579f2..1dc076e 100644 --- a/src/tables/detect_heuristic.rs +++ b/src/tables/detect_heuristic.rs @@ -48,7 +48,7 @@ pub(crate) fn merge_adjacent_items(items: &[TextItem]) -> (Vec, Vec> = Vec::new(); @@ -284,7 +284,7 @@ fn find_table_regions(items: &[(usize, &TextItem)]) -> Vec<(f32, f32)> { } let mut y_positions: Vec = items.iter().map(|(_, i)| i.y).collect(); - y_positions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + y_positions.sort_by(|a, b| a.total_cmp(b)); // Find clusters of Y positions (table regions) let mut regions = Vec::new(); @@ -347,7 +347,7 @@ fn find_table_regions_strict(items: &[(usize, &TextItem)]) -> Vec<(f32, f32, f32 let mut qualifying_rows: Vec<(f32, Vec)> = Vec::new(); // (y, cluster_starts) for (y, x_positions) in &row_groups { let mut sorted_xs = x_positions.clone(); - sorted_xs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + sorted_xs.sort_by(|a, b| a.total_cmp(b)); if sorted_xs.is_empty() { continue; @@ -379,14 +379,14 @@ fn find_table_regions_strict(items: &[(usize, &TextItem)]) -> Vec<(f32, f32, f32 // Step 3: Find contiguous runs of qualifying rows. // Use adaptive gap: median spacing × 3 (handles wrapped cells where // qualifying rows are spaced further apart), with a floor of 25pt. - qualifying_rows.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal)); + qualifying_rows.sort_by(|a, b| a.0.total_cmp(&b.0)); let max_gap = if qualifying_rows.len() >= 3 { let mut gaps: Vec = qualifying_rows .windows(2) .map(|w| (w[1].0 - w[0].0).abs()) .collect(); - gaps.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + gaps.sort_by(|a, b| a.total_cmp(b)); let median_gap = gaps[gaps.len() / 2]; (median_gap * 3.0).max(25.0) } else { @@ -566,11 +566,9 @@ fn detect_table_in_region(items: &[(usize, &TextItem)], mode: TableDetectionMode // Sort by X position (direction-aware) let rtl = is_rtl_text(col_items.iter().map(|i| &i.text)); if rtl { - col_items - .sort_by(|a, b| b.x.partial_cmp(&a.x).unwrap_or(std::cmp::Ordering::Equal)); + col_items.sort_by(|a, b| b.x.total_cmp(&a.x)); } else { - col_items - .sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal)); + col_items.sort_by(|a, b| a.x.total_cmp(&b.x)); } // Join items with subscript-aware spacing diff --git a/src/tables/detect_lines.rs b/src/tables/detect_lines.rs index 8dfea17..c60b450 100644 --- a/src/tables/detect_lines.rs +++ b/src/tables/detect_lines.rs @@ -167,7 +167,7 @@ pub fn detect_tables_from_lines(items: &[TextItem], lines: &[PdfLine], page: u32 // Row edges need to be in descending order (top of page = higher Y first) let mut row_edges_desc = row_edges; - row_edges_desc.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); + row_edges_desc.sort_by(|a, b| b.total_cmp(a)); log::debug!( "detect_lines p{}: {} row_edges, {} col_edges, table=({:.0},{:.0})-({:.0},{:.0}), spanning_h={}, spanning_v={}", diff --git a/src/tables/detect_rects.rs b/src/tables/detect_rects.rs index cfdac1e..ca70231 100644 --- a/src/tables/detect_rects.rs +++ b/src/tables/detect_rects.rs @@ -144,7 +144,7 @@ fn split_wide_cluster( // Build sorted list of X-intervals (x_left, x_right) from each rect let mut intervals: Vec<(f32, f32)> = rects.iter().map(|&(x, _, w, _)| (x, x + w)).collect(); - intervals.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal)); + intervals.sort_by(|a, b| a.0.total_cmp(&b.0)); // Merge overlapping intervals to find contiguous X-bands let mut merged: Vec<(f32, f32)> = Vec::new(); @@ -262,7 +262,7 @@ pub fn detect_tables_from_rects( // background fills stand out clearly. if page_rects.len() >= 6 { let mut widths: Vec = page_rects.iter().map(|&(_, _, w, _)| w).collect(); - widths.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + widths.sort_by(|a, b| a.total_cmp(b)); let median_width = widths[widths.len() / 2]; let width_threshold = median_width * 10.0; let before = page_rects.len(); @@ -333,7 +333,7 @@ pub fn detect_tables_from_rects( // cluster they overlap, so grid detection still has their edges. let is_page_bg = { let mut heights: Vec = page_rects.iter().map(|&(_, _, _, h)| h).collect(); - heights.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + heights.sort_by(|a, b| a.total_cmp(b)); let median_height = heights[heights.len() / 2]; let height_threshold = median_height * 20.0; let flags: Vec = page_rects @@ -621,7 +621,7 @@ fn merge_overlapping_hints(mut hints: Vec) -> Vec = Vec::new(); let mut any_merged = false; for hint in &hints { @@ -685,7 +685,7 @@ fn extract_hint_region(group_rects: &[(f32, f32, f32, f32)]) -> Option = group_rects.iter().map(|&(_, _, _, h)| h).collect(); - heights.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + heights.sort_by(|a, b| a.total_cmp(b)); let median_h = heights[heights.len() / 2]; // Keep only cell-sized rects (height ≤ 4× median) @@ -857,9 +857,9 @@ fn try_build_grid( // Sort column edges left-to-right, row edges top-to-bottom (highest Y first for PDF) let mut col_edges = x_edges; - col_edges.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + col_edges.sort_by(|a, b| a.total_cmp(b)); let mut row_edges = y_edges; - row_edges.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); + row_edges.sort_by(|a, b| b.total_cmp(a)); let num_cols = col_edges.len() - 1; let num_rows = row_edges.len() - 1; @@ -1048,7 +1048,7 @@ fn try_build_grid( /// Deduplicate nearby edge values within a tolerance, returning sorted unique edges. pub(crate) fn snap_edges(values: &[f32], tolerance: f32) -> Vec { let mut sorted: Vec = values.to_vec(); - sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + sorted.sort_by(|a, b| a.total_cmp(b)); let mut snapped: Vec = Vec::new(); for &v in &sorted { @@ -1204,7 +1204,7 @@ fn is_row_stripe_pattern(rects: &[(f32, f32, f32, f32)]) -> bool { } let mut widths: Vec = rects.iter().map(|&(_, _, w, _)| w).collect(); - widths.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + widths.sort_by(|a, b| a.total_cmp(b)); let median_width = widths[widths.len() / 2]; // Must be page-spanning (>200pt) @@ -1252,7 +1252,7 @@ fn detect_row_stripe_table( // Sort row edges top-to-bottom (highest Y first for PDF) let mut row_edges = y_edges; - row_edges.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); + row_edges.sort_by(|a, b| b.total_cmp(a)); // Compute the bounding box of the stripe region for filtering items let y_top = row_edges[0]; @@ -1476,7 +1476,7 @@ fn detect_row_stripe_table_from_cell_rects( // bounding box to scope items and derive rows from text Y-positions. let row_edges = if y_edges.len() >= 4 { let mut edges = y_edges; - edges.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); + edges.sort_by(|a, b| b.total_cmp(a)); edges } else { // Fall back: gather items in the rect region and cluster by Y @@ -1508,11 +1508,11 @@ fn detect_row_stripe_table_from_cell_rects( // Cluster Y positions using median font height as threshold let median_h = { let mut hs: Vec = region_items.iter().map(|i| i.height).collect(); - hs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + hs.sort_by(|a, b| a.total_cmp(b)); hs[hs.len() / 2] }; let mut ys: Vec = region_items.iter().map(|i| i.y).collect(); - ys.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); + ys.sort_by(|a, b| b.total_cmp(a)); let mut edges = Vec::new(); let threshold = median_h * 0.8; let mut cluster_start = ys[0]; @@ -1536,7 +1536,7 @@ fn detect_row_stripe_table_from_cell_rects( edges.push(center - median_h * 0.5); let _ = cluster_start; // suppress unused warning edges = snap_edges(&edges, 3.0); - edges.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); + edges.sort_by(|a, b| b.total_cmp(a)); if edges.len() < 4 { return None; } @@ -1546,7 +1546,7 @@ fn detect_row_stripe_table_from_cell_rects( // Compute bounding box from non-full-page rects let median_h = { let mut heights: Vec = group_rects.iter().map(|&(_, _, _, h)| h).collect(); - heights.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + heights.sort_by(|a, b| a.total_cmp(b)); heights[heights.len() / 2] }; let content_rects: Vec<_> = group_rects @@ -1724,7 +1724,7 @@ fn detect_merged_cluster_table( } let mut row_edges = y_edges; - row_edges.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); + row_edges.sort_by(|a, b| b.total_cmp(a)); // Bounding box of all rects let y_top = row_edges[0]; @@ -1890,7 +1890,7 @@ fn detect_merged_cluster_table( /// (no need for anti-paragraph safeguards). fn cluster_x_positions(items: &[(usize, &TextItem)], min_threshold: f32) -> Vec { let mut x_positions: Vec = items.iter().map(|(_, i)| i.x).collect(); - x_positions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + x_positions.sort_by(|a, b| a.total_cmp(b)); if x_positions.is_empty() { return vec![]; diff --git a/src/tables/grid.rs b/src/tables/grid.rs index 950dde4..66e994d 100644 --- a/src/tables/grid.rs +++ b/src/tables/grid.rs @@ -9,7 +9,7 @@ pub(crate) fn find_column_boundaries( mode: TableDetectionMode, ) -> Vec { let mut x_positions: Vec = items.iter().map(|(_, i)| i.x).collect(); - x_positions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + x_positions.sort_by(|a, b| a.total_cmp(b)); if x_positions.is_empty() { return vec![]; @@ -43,7 +43,7 @@ pub(crate) fn find_column_boundaries( .collect(); if consec_gaps.len() > 2 { - consec_gaps.sort_by(|a, b| a.partial_cmp(b).unwrap()); + consec_gaps.sort_by(|a, b| a.total_cmp(b)); // Find the biggest jump in the sorted gap sequence — natural break // between within-column jitter and between-column spacing. // Require at least 3 values on each side to avoid outlier-dominated @@ -151,7 +151,7 @@ pub(crate) fn find_column_boundaries( /// Find row boundaries by clustering Y positions pub(crate) fn find_row_boundaries(items: &[(usize, &TextItem)]) -> Vec { let mut y_positions: Vec = items.iter().map(|(_, i)| i.y).collect(); - y_positions.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); // Descending + y_positions.sort_by(|a, b| b.total_cmp(a)); // Descending if y_positions.is_empty() { return vec![]; @@ -162,7 +162,7 @@ pub(crate) fn find_row_boundaries(items: &[(usize, &TextItem)]) -> Vec { // inter-row gaps (≥1× font size), preventing row merging in uniform-spaced PDFs. let cluster_threshold = { let mut font_sizes: Vec = items.iter().map(|(_, i)| i.font_size).collect(); - font_sizes.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + font_sizes.sort_by(|a, b| a.total_cmp(b)); let median_font = font_sizes[font_sizes.len() / 2]; (median_font * 0.8).max(4.0) }; diff --git a/src/tables/mod.rs b/src/tables/mod.rs index ece5e81..12dc5b7 100644 --- a/src/tables/mod.rs +++ b/src/tables/mod.rs @@ -33,7 +33,7 @@ pub(crate) fn try_build_rect_guided_table( // 1. Derive column boundaries from rect X positions (snapped to 2pt tolerance) let mut x_lefts: Vec = cluster_rects.iter().map(|&(x, _, _, _)| x).collect(); - x_lefts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + x_lefts.sort_by(|a, b| a.total_cmp(b)); // Snap: deduplicate within 2pt tolerance let mut col_boundaries: Vec = Vec::new(); for x in &x_lefts { @@ -54,7 +54,7 @@ pub(crate) fn try_build_rect_guided_table( // boundaries so every day gets a column. if col_boundaries.len() >= 2 { let mut spacings: Vec = col_boundaries.windows(2).map(|w| w[1] - w[0]).collect(); - spacings.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + spacings.sort_by(|a, b| a.total_cmp(b)); let median_spacing = spacings[spacings.len() / 2]; let threshold = median_spacing * 1.5; @@ -87,7 +87,7 @@ pub(crate) fn try_build_rect_guided_table( // 3. Derive row boundaries from item Y positions (5pt tolerance) let mut y_values: Vec = expanded_items.iter().map(|(item, _)| item.y).collect(); - y_values.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); // descending + y_values.sort_by(|a, b| b.total_cmp(a)); // descending let mut row_boundaries: Vec = Vec::new(); for y in &y_values { if row_boundaries @@ -296,7 +296,7 @@ pub(crate) fn try_build_table_from_columns(items: &[TextItem], page: u32) -> Opt // Find the top-most row with items in multiple columns (likely the header) let mut ys: Vec = page_items.iter().map(|i| i.y).collect(); - ys.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); + ys.sort_by(|a, b| b.total_cmp(a)); ys.dedup_by(|a, b| (*a - *b).abs() < y_tol); for &header_y in ys.iter().take(5) { @@ -318,7 +318,7 @@ pub(crate) fn try_build_table_from_columns(items: &[TextItem], page: u32) -> Opt if col_items.len() >= 2 { // Sort by X and find the split point let mut sorted: Vec = col_items.iter().map(|i| i.x).collect(); - sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + sorted.sort_by(|a, b| a.total_cmp(b)); // Split at the midpoint between the two items let split_x = (sorted[0] + col_items.iter().find(|i| i.x == sorted[0]).unwrap().width @@ -411,7 +411,7 @@ pub(crate) fn try_build_table_from_columns(items: &[TextItem], page: u32) -> Opt } } } - row_ys.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); + row_ys.sort_by(|a, b| b.total_cmp(a)); if row_ys.len() < 3 || row_ys.len() > 40 { return None; diff --git a/src/text_utils.rs b/src/text_utils.rs index 702c4bd..8c6b069 100644 --- a/src/text_utils.rs +++ b/src/text_utils.rs @@ -68,9 +68,9 @@ where pub(crate) fn sort_line_items(items: &mut [TextItem]) { let rtl = is_rtl_text(items.iter().map(|i| &i.text)); if rtl { - items.sort_by(|a, b| b.x.partial_cmp(&a.x).unwrap_or(std::cmp::Ordering::Equal)); + items.sort_by(|a, b| b.x.total_cmp(&a.x)); } else { - items.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal)); + items.sort_by(|a, b| a.x.total_cmp(&b.x)); } } @@ -376,7 +376,7 @@ fn compute_canva_join_threshold(items: &[TextItem]) -> f32 { } let mut sorted: Vec = ratios; - sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + sorted.sort_by(|a, b| a.total_cmp(b)); if sorted[sorted.len() - 1] < 0.40 || sorted[0] < 0.40 { return DEFAULT; @@ -478,7 +478,7 @@ fn compute_single_char_join_threshold(items: &[TextItem]) -> f32 { return DEFAULT; } - ratios.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + ratios.sort_by(|a, b| a.total_cmp(b)); // If all gaps are tight (max < 0.40), use default — normal PDF let max_ratio = ratios[ratios.len() - 1];