// This file is auto-generated by alef. DO NOT EDIT.
// alef:hash:4e15143f4af1ae8bafbdb1506ef057da924484c66a19483966333558ad437e75
// Re-generate with: alef generate
#![allow(dead_code, unused_imports, unused_variables)]
#![allow(unsafe_code)]
#![allow(
    clippy::too_many_arguments,
    clippy::let_unit_value,
    clippy::needless_borrow,
    clippy::map_identity,
    clippy::just_underscores_and_digits,
    clippy::unnecessary_cast,
    clippy::unused_unit,
    clippy::unwrap_or_default,
    clippy::derivable_impls,
    clippy::needless_borrows_for_generic_args,
    clippy::unnecessary_fallible_conversions,
    clippy::arc_with_non_send_sync,
    clippy::collapsible_if,
    clippy::clone_on_copy,
    clippy::should_implement_trait
)]
#![allow(
    clippy::cast_possible_wrap,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    clippy::default_trait_access,
    clippy::useless_conversion,
    clippy::unsafe_derive_deserialize,
    clippy::must_use_candidate,
    clippy::return_self_not_must_use,
    clippy::use_self,
    clippy::missing_const_for_fn,
    clippy::missing_errors_doc,
    clippy::needless_pass_by_value,
    clippy::doc_markdown,
    clippy::derive_partial_eq_without_eq,
    clippy::uninlined_format_args,
    clippy::redundant_clone,
    clippy::implicit_clone,
    clippy::redundant_closure_for_method_calls,
    clippy::wildcard_imports,
    clippy::option_if_let_else,
    clippy::too_many_lines
)]

use napi::JsValue;
use napi::bindgen_prelude::{JsObjectValue, Object, ToNapiValue, Unknown};
use napi::*;
use napi_derive::napi;
use std::collections::HashMap;
use std::sync::Arc;

static WORKER_POOL: std::sync::LazyLock<tokio::runtime::Runtime> = std::sync::LazyLock::new(|| {
    tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .expect("Failed to create Tokio runtime")
});

/// Wrapper for byte arrays that implements custom FromNapiValue to accept Buffer.from(...).
///
/// NAPI v3's default FromNapiValue for Vec<u8> expects Array[number], not Buffer.
/// This wrapper provides custom deserialization that accepts Buffer, Uint8Array, or Array,
/// converting them to Vec<u8>. Implements Clone and serde traits for use in struct fields.
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct JsBytes(pub Vec<u8>);

impl From<Vec<u8>> for JsBytes {
    fn from(v: Vec<u8>) -> Self {
        JsBytes(v)
    }
}

impl From<JsBytes> for Vec<u8> {
    fn from(js_bytes: JsBytes) -> Self {
        js_bytes.0
    }
}

impl AsRef<[u8]> for JsBytes {
    fn as_ref(&self) -> &[u8] {
        &self.0
    }
}

impl std::ops::Deref for JsBytes {
    type Target = Vec<u8>;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl std::ops::DerefMut for JsBytes {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl napi::bindgen_prelude::FromNapiValue for JsBytes {
    unsafe fn from_napi_value(env: napi::sys::napi_env, napi_val: napi::sys::napi_value) -> napi::Result<Self> {
        use napi::bindgen_prelude::FromNapiValue;

        // Try Buffer first (most common for binary data in JS)
        if let Ok(buffer) = unsafe { napi::bindgen_prelude::Buffer::from_napi_value(env, napi_val) } {
            return Ok(JsBytes(buffer.as_ref().to_vec()));
        }

        // Try Uint8Array
        if let Ok(ua) = unsafe { napi::bindgen_prelude::Uint8Array::from_napi_value(env, napi_val) } {
            return Ok(JsBytes(ua.to_vec()));
        }

        // Fall back to Array[number]
        if let Ok(vec) = unsafe { Vec::<u8>::from_napi_value(env, napi_val) } {
            return Ok(JsBytes(vec));
        }

        Err(napi::Error::new(
            napi::Status::InvalidArg,
            "Expected Buffer, Uint8Array, or Array<number> for bytes field",
        ))
    }
}

impl napi::bindgen_prelude::ToNapiValue for JsBytes {
    unsafe fn to_napi_value(env: napi::sys::napi_env, val: Self) -> napi::Result<napi::sys::napi_value> {
        // Delegate to Vec<u8>'s implementation (which returns an Uint8Array/Buffer).
        unsafe { <Vec<u8> as napi::bindgen_prelude::ToNapiValue>::to_napi_value(env, val.0) }
    }
}

/// Wrapper for trait visitor types (napi::Object<'static>) that implements Clone.
///
/// Object is not Clone. This wrapper uses Arc<Object<'static>> internally for cheap cloning.
/// The .inner field is public for compatibility with generated code that needs to access
/// the underlying Object for trait dispatch.
pub struct JsVisitorRef {
    pub inner: std::sync::Arc<napi::bindgen_prelude::Object<'static>>,
}

impl Clone for JsVisitorRef {
    fn clone(&self) -> Self {
        JsVisitorRef {
            inner: std::sync::Arc::clone(&self.inner),
        }
    }
}

#[allow(clippy::arc_with_non_send_sync)]
impl From<napi::bindgen_prelude::Object<'static>> for JsVisitorRef {
    fn from(visitor: napi::bindgen_prelude::Object<'static>) -> Self {
        JsVisitorRef {
            inner: std::sync::Arc::new(visitor),
        }
    }
}

impl From<JsVisitorRef> for napi::bindgen_prelude::Object<'static> {
    fn from(visitor_ref: JsVisitorRef) -> Self {
        // Object<'static> is Copy (it just holds an env+handle pair), so deref directly.
        *visitor_ref.inner
    }
}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "CacheStats")]
pub struct JsCacheStats {
    #[napi(js_name = "totalFiles")]
    #[serde(rename = "totalFiles")]
    pub total_files: i64,
    #[napi(js_name = "totalSizeMb")]
    #[serde(rename = "totalSizeMb")]
    pub total_size_mb: f64,
    #[napi(js_name = "availableSpaceMb")]
    #[serde(rename = "availableSpaceMb")]
    pub available_space_mb: f64,
    #[napi(js_name = "oldestFileAgeDays")]
    #[serde(rename = "oldestFileAgeDays")]
    pub oldest_file_age_days: f64,
    #[napi(js_name = "newestFileAgeDays")]
    #[serde(rename = "newestFileAgeDays")]
    pub newest_file_age_days: f64,
}

/// Hardware acceleration configuration for ONNX Runtime models.
///
/// Controls which execution provider (CPU, CoreML, CUDA, TensorRT) is used
/// for inference in layout detection and embedding generation.
///
/// # Example
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "AccelerationConfig")]
pub struct JsAccelerationConfig {
    /// Execution provider to use for ONNX inference.
    pub provider: Option<JsExecutionProviderType>,
    /// GPU device ID (for CUDA/TensorRT). Ignored for CPU/CoreML/Auto.
    #[napi(js_name = "deviceId")]
    #[serde(rename = "deviceId")]
    pub device_id: Option<u32>,
}

/// Cross-extractor content filtering configuration.
///
/// Controls whether "furniture" content (headers, footers, page numbers,
/// watermarks, repeating text) is included in or stripped from extraction
/// results. Applies across all extractors (PDF, DOCX, RTF, ODT, HTML, etc.)
/// with format-specific implementation.
///
/// When `None` on `ExtractionConfig`, each extractor uses its current
/// default behavior unchanged.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ContentFilterConfig")]
pub struct JsContentFilterConfig {
    /// Include running headers in extraction output.
    ///
    /// - PDF: Disables top-margin furniture stripping and prevents the layout
    ///   model from treating `PageHeader`-classified regions as furniture.
    /// - DOCX: Includes document headers in text output.
    /// - RTF/ODT: Headers already included; this is a no-op when true.
    /// - HTML/EPUB: Keeps `<header>` element content.
    ///
    /// Default: `false` (headers are stripped or excluded).
    #[napi(js_name = "includeHeaders")]
    #[serde(rename = "includeHeaders")]
    pub include_headers: Option<bool>,
    /// Include running footers in extraction output.
    ///
    /// - PDF: Disables bottom-margin furniture stripping and prevents the layout
    ///   model from treating `PageFooter`-classified regions as furniture.
    /// - DOCX: Includes document footers in text output.
    /// - RTF/ODT: Footers already included; this is a no-op when true.
    /// - HTML/EPUB: Keeps `<footer>` element content.
    ///
    /// Default: `false` (footers are stripped or excluded).
    #[napi(js_name = "includeFooters")]
    #[serde(rename = "includeFooters")]
    pub include_footers: Option<bool>,
    /// Enable the heuristic cross-page repeating text detector.
    ///
    /// When `true` (default), text that repeats verbatim across a supermajority
    /// of pages is classified as furniture and stripped.  Disable this if brand
    /// names or repeated headings are being incorrectly removed by the heuristic.
    ///
    /// Note: when a layout-detection model is active, the model may independently
    /// classify page-header / page-footer regions as furniture on a per-page basis.
    /// To preserve those regions, set `include_headers = true`, `include_footers = true`,
    /// or both, in addition to disabling this flag.
    ///
    /// Primarily affects PDF extraction.
    ///
    /// Default: `true`.
    #[napi(js_name = "stripRepeatingText")]
    #[serde(rename = "stripRepeatingText")]
    pub strip_repeating_text: Option<bool>,
    /// Include watermark text in extraction output.
    ///
    /// - PDF: Keeps watermark artifacts and arXiv identifiers.
    /// - Other formats: No effect currently.
    ///
    /// Default: `false` (watermarks are stripped).
    #[napi(js_name = "includeWatermarks")]
    #[serde(rename = "includeWatermarks")]
    pub include_watermarks: Option<bool>,
}

#[napi(js_name = "contentFilterConfigDefault")]
pub fn content_filter_config_default() -> JsContentFilterConfig {
    kreuzberg::ContentFilterConfig::default().into()
}

/// Configuration for email extraction.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "EmailConfig")]
pub struct JsEmailConfig {
    /// Windows codepage number to use when an MSG file contains no codepage property.
    /// Defaults to `None`, which falls back to windows-1252.
    ///
    /// If an unrecognized or invalid codepage number is supplied (including 0),
    /// the behavior silently falls back to windows-1252 — the same as when the
    /// MSG file itself contains an unrecognized codepage. No error or warning is
    /// emitted. Users should verify output when supplying unusual values.
    ///
    /// Common values:
    /// - 1250: Central European (Polish, Czech, Hungarian, etc.)
    /// - 1251: Cyrillic (Russian, Ukrainian, Bulgarian, etc.)
    /// - 1252: Western European (default)
    /// - 1253: Greek
    /// - 1254: Turkish
    /// - 1255: Hebrew
    /// - 1256: Arabic
    /// - 932:  Japanese (Shift-JIS)
    /// - 936:  Simplified Chinese (GBK)
    #[napi(js_name = "msgFallbackCodepage")]
    #[serde(rename = "msgFallbackCodepage")]
    pub msg_fallback_codepage: Option<u32>,
}

/// Main extraction configuration.
///
/// This struct contains all configuration options for the extraction process.
/// It can be loaded from TOML, YAML, or JSON files, or created programmatically.
///
/// # Example
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ExtractionConfig")]
pub struct JsExtractionConfig {
    /// Enable caching of extraction results
    #[napi(js_name = "useCache")]
    #[serde(rename = "useCache")]
    pub use_cache: Option<bool>,
    /// Enable quality post-processing
    #[napi(js_name = "enableQualityProcessing")]
    #[serde(rename = "enableQualityProcessing")]
    pub enable_quality_processing: Option<bool>,
    /// OCR configuration (None = OCR disabled)
    pub ocr: Option<JsOcrConfig>,
    /// Force OCR even for searchable PDFs
    #[napi(js_name = "forceOcr")]
    #[serde(rename = "forceOcr")]
    pub force_ocr: Option<bool>,
    /// Force OCR on specific pages only (1-indexed page numbers, must be >= 1).
    ///
    /// When set, only the listed pages are OCR'd regardless of text layer quality.
    /// Unlisted pages use native text extraction. Ignored when `force_ocr` is `true`.
    /// Only applies to PDF documents. Duplicates are automatically deduplicated.
    /// An `ocr` config is recommended for backend/language selection; defaults are used if absent.
    #[napi(js_name = "forceOcrPages")]
    #[serde(rename = "forceOcrPages")]
    pub force_ocr_pages: Option<Vec<u32>>,
    /// Disable OCR entirely, even for images.
    ///
    /// When `true`, OCR is skipped for all document types. Images return metadata
    /// only (dimensions, format, EXIF) without text extraction. PDFs use only
    /// native text extraction without OCR fallback.
    ///
    /// Cannot be `true` simultaneously with `force_ocr`.
    ///
    /// *Added in v4.7.0.*
    #[napi(js_name = "disableOcr")]
    #[serde(rename = "disableOcr")]
    pub disable_ocr: Option<bool>,
    /// Text chunking configuration (None = chunking disabled)
    pub chunking: Option<JsChunkingConfig>,
    /// Content filtering configuration (None = use extractor defaults).
    ///
    /// Controls whether document "furniture" (headers, footers, watermarks,
    /// repeating text) is included in or stripped from extraction results.
    /// See [`ContentFilterConfig`] for per-field documentation.
    #[napi(js_name = "contentFilter")]
    #[serde(rename = "contentFilter")]
    pub content_filter: Option<JsContentFilterConfig>,
    /// Image extraction configuration (None = no image extraction)
    pub images: Option<JsImageExtractionConfig>,
    /// PDF-specific options (None = use defaults)
    #[napi(js_name = "pdfOptions")]
    #[serde(rename = "pdfOptions")]
    #[serde(skip)]
    pub pdf_options: Option<JsPdfConfig>,
    /// Token reduction configuration (None = no token reduction)
    #[napi(js_name = "tokenReduction")]
    #[serde(rename = "tokenReduction")]
    pub token_reduction: Option<JsTokenReductionOptions>,
    /// Language detection configuration (None = no language detection)
    #[napi(js_name = "languageDetection")]
    #[serde(rename = "languageDetection")]
    pub language_detection: Option<JsLanguageDetectionConfig>,
    /// Page extraction configuration (None = no page tracking)
    pub pages: Option<JsPageConfig>,
    /// Keyword extraction configuration (None = no keyword extraction)
    #[serde(skip)]
    pub keywords: Option<JsKeywordConfig>,
    /// Post-processor configuration (None = use defaults)
    pub postprocessor: Option<JsPostProcessorConfig>,
    /// HTML to Markdown conversion options (None = use defaults)
    ///
    /// Configure how HTML documents are converted to Markdown, including heading styles,
    /// list formatting, code block styles, and preprocessing options.
    #[napi(js_name = "htmlOptions")]
    #[serde(rename = "htmlOptions")]
    #[serde(skip)]
    pub html_options: Option<String>,
    /// Styled HTML output configuration.
    ///
    /// When set alongside `output_format = OutputFormat::Html`, the extraction
    /// pipeline uses [`StyledHtmlRenderer`](crate::rendering::StyledHtmlRenderer)
    /// which emits stable `kb-*` CSS class hooks on every structural element
    /// and optionally embeds theme CSS or user-supplied CSS in a `<style>` block.
    ///
    /// When `None`, the existing plain comrak-based HTML renderer is used.
    #[napi(js_name = "htmlOutput")]
    #[serde(rename = "htmlOutput")]
    #[serde(skip)]
    pub html_output: Option<JsHtmlOutputConfig>,
    /// Default per-file timeout in seconds for batch extraction.
    ///
    /// When set, each file in a batch will be canceled after this duration
    /// unless overridden by [`FileExtractionConfig::timeout_secs`].
    ///
    /// Defaults to `Some(60)` to prevent pathological files (e.g. deeply
    /// nested archives, documents with millions of cells) from running
    /// indefinitely and exhausting caller resources. Set to `None` to
    /// disable the timeout for trusted input or long-running workloads.
    #[napi(js_name = "extractionTimeoutSecs")]
    #[serde(rename = "extractionTimeoutSecs")]
    pub extraction_timeout_secs: Option<i64>,
    /// Maximum concurrent extractions in batch operations (None = (num_cpus × 1.5).ceil()).
    ///
    /// Limits parallelism to prevent resource exhaustion when processing
    /// large batches. Defaults to (num_cpus × 1.5).ceil() when not set.
    #[napi(js_name = "maxConcurrentExtractions")]
    #[serde(rename = "maxConcurrentExtractions")]
    pub max_concurrent_extractions: Option<i64>,
    /// Result structure format
    ///
    /// Controls whether results are returned in unified format (default) with all
    /// content in the `content` field, or element-based format with semantic
    /// elements (for Unstructured-compatible output).
    #[napi(js_name = "resultFormat")]
    #[serde(rename = "resultFormat")]
    pub result_format: Option<JsResultFormat>,
    /// Security limits for archive extraction.
    ///
    /// Controls maximum archive size, compression ratio, file count, and other
    /// security thresholds to prevent decompression bomb attacks. Also caps
    /// nesting depth, iteration count, entity / token length, total
    /// content size, and table cell count for every extraction path that
    /// ingests user-controlled bytes.
    /// When `None`, default limits are used.
    #[napi(js_name = "securityLimits")]
    #[serde(rename = "securityLimits")]
    pub security_limits: Option<JsSecurityLimits>,
    /// Maximum uncompressed size in bytes for a single embedded file before
    /// recursive extraction is attempted (default: 50 MiB).
    ///
    /// Applies to embedded objects inside OOXML containers (DOCX, PPTX) and
    /// to email attachments processed via recursive extraction. Files that
    /// exceed this limit are skipped with a `ProcessingWarning` rather than
    /// passed to the extraction pipeline, preventing a single oversized
    /// embedded object from consuming unbounded memory or time.
    ///
    /// Set to `None` to disable the per-embedded-file cap (falls back to
    /// `security_limits.max_archive_size` as the only guard).
    #[napi(js_name = "maxEmbeddedFileBytes")]
    #[serde(rename = "maxEmbeddedFileBytes")]
    pub max_embedded_file_bytes: Option<i64>,
    /// Content text format (default: Plain).
    ///
    /// Controls the format of the extracted content:
    /// - `Plain`: Raw extracted text (default)
    /// - `Markdown`: Markdown formatted output
    /// - `Djot`: Djot markup format (requires djot feature)
    /// - `Html`: HTML formatted output
    ///
    /// When set to a structured format, extraction results will include
    /// formatted output. The `formatted_content` field may be populated
    /// when format conversion is applied.
    #[napi(js_name = "outputFormat")]
    #[serde(rename = "outputFormat")]
    pub output_format: Option<JsOutputFormat>,
    /// Layout detection configuration (None = layout detection disabled).
    ///
    /// When set, PDF pages and images are analyzed for document structure
    /// (headings, code, formulas, tables, figures, etc.) using RT-DETR models
    /// via ONNX Runtime. For PDFs, layout hints override paragraph classification
    /// in the markdown pipeline. For images, per-region OCR is performed with
    /// markdown formatting based on detected layout classes.
    /// Requires the `layout-detection` feature to run inference; the field is
    /// present whenever the `layout-types` feature is active (which includes
    /// `layout-detection` as well as the no-ORT target groups).
    #[serde(skip)]
    pub layout: Option<JsLayoutDetectionConfig>,
    /// Run layout detection on the non-OCR PDF markdown path.
    ///
    /// When `true` and `layout` is `Some(_)`, layout regions inform heading,
    /// table, list, and figure detection in the structure pipeline that would
    /// otherwise rely on font-clustering heuristics alone. Significantly
    /// improves SF1 (structural F1) at the cost of inference latency
    /// (~150-300ms/page CPU, ~20-50ms/page GPU). Default: `false`.
    /// Requires the `layout-detection` feature.
    #[napi(js_name = "useLayoutForMarkdown")]
    #[serde(rename = "useLayoutForMarkdown")]
    pub use_layout_for_markdown: Option<bool>,
    /// Enable structured document tree output.
    ///
    /// When true, populates the `document` field on `ExtractionResult` with a
    /// hierarchical `DocumentStructure` containing heading-driven section nesting,
    /// table grids, content layer classification, and inline annotations.
    ///
    /// Independent of `result_format` — can be combined with Unified or ElementBased.
    #[napi(js_name = "includeDocumentStructure")]
    #[serde(rename = "includeDocumentStructure")]
    pub include_document_structure: Option<bool>,
    /// Hardware acceleration configuration for ONNX Runtime models.
    ///
    /// Controls execution provider selection for layout detection and embedding
    /// models. When `None`, uses platform defaults (CoreML on macOS, CUDA on
    /// Linux, CPU on Windows).
    pub acceleration: Option<JsAccelerationConfig>,
    /// Cache namespace for tenant isolation.
    ///
    /// When set, cache entries are stored under `{cache_dir}/{namespace}/`.
    /// Must be alphanumeric, hyphens, or underscores only (max 64 chars).
    /// Different namespaces have isolated cache spaces on the same filesystem.
    #[napi(js_name = "cacheNamespace")]
    #[serde(rename = "cacheNamespace")]
    pub cache_namespace: Option<String>,
    /// Per-request cache TTL in seconds.
    ///
    /// Overrides the global `max_age_days` for this specific extraction.
    /// When `0`, caching is completely skipped (no read or write).
    /// When `None`, the global TTL applies.
    #[napi(js_name = "cacheTtlSecs")]
    #[serde(rename = "cacheTtlSecs")]
    pub cache_ttl_secs: Option<i64>,
    /// Email extraction configuration (None = use defaults).
    ///
    /// Currently supports configuring the fallback codepage for MSG files
    /// that do not specify one. See `EmailConfig` for details.
    pub email: Option<JsEmailConfig>,
    /// Concurrency limits for constrained environments (None = use defaults).
    ///
    /// Controls Rayon thread pool size, ONNX Runtime intra-op threads, and
    /// (when `max_concurrent_extractions` is unset) the batch concurrency
    /// semaphore. See `ConcurrencyConfig` for details.
    pub concurrency: Option<String>,
    /// Maximum recursion depth for archive extraction (default: 3).
    /// Set to 0 to disable recursive extraction (legacy behavior).
    #[napi(js_name = "maxArchiveDepth")]
    #[serde(rename = "maxArchiveDepth")]
    pub max_archive_depth: Option<i64>,
    /// Tree-sitter language pack configuration (None = tree-sitter disabled).
    ///
    /// When set, enables code file extraction using tree-sitter parsers.
    /// Controls grammar download behavior and code analysis options.
    #[napi(js_name = "treeSitter")]
    #[serde(rename = "treeSitter")]
    #[serde(skip)]
    pub tree_sitter: Option<JsTreeSitterConfig>,
    /// Structured extraction via LLM (None = disabled).
    ///
    /// When set, the extracted document content is sent to an LLM with the
    /// provided JSON schema. The structured response is stored in
    /// `ExtractionResult::structured_output`.
    #[napi(js_name = "structuredExtraction")]
    #[serde(rename = "structuredExtraction")]
    pub structured_extraction: Option<JsStructuredExtractionConfig>,
    /// Cancellation token for this extraction (None = no external cancellation).
    ///
    /// Pass a [`CancellationToken`] clone here and call [`CancellationToken::cancel`]
    /// from another thread / task to abort the extraction in progress. The extractor
    /// checks the token at safe checkpoints (before lock acquisition, between pages,
    /// between batch items) and returns [`KreuzbergError::Cancelled`] when set.
    ///
    /// The field is excluded from serialization because `CancellationToken` is a
    /// runtime handle, not a configuration value.
    #[napi(js_name = "cancelToken")]
    #[serde(rename = "cancelToken")]
    pub cancel_token: Option<String>,
}

#[napi(js_name = "extractionConfigDefault")]
pub fn extraction_config_default() -> JsExtractionConfig {
    kreuzberg::ExtractionConfig::default().into()
}

/// Per-file extraction configuration overrides for batch processing.
///
/// All fields are `Option<T>` — `None` means "use the batch-level default."
/// This type is used with `batch_extract_files` and
/// `batch_extract_bytes` to allow heterogeneous
/// extraction settings within a single batch.
///
/// # Excluded Fields
///
/// The following `ExtractionConfig` fields are batch-level only and
/// cannot be overridden per file:
/// - `max_concurrent_extractions` — controls batch parallelism
/// - `use_cache` — global caching policy
/// - `acceleration` — shared ONNX execution provider
/// - `security_limits` — global archive security policy
///
/// # Example
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "FileExtractionConfig")]
pub struct JsFileExtractionConfig {
    /// Override quality post-processing for this file.
    #[napi(js_name = "enableQualityProcessing")]
    #[serde(rename = "enableQualityProcessing")]
    pub enable_quality_processing: Option<bool>,
    /// Override OCR configuration for this file (None in the Option = use batch default).
    pub ocr: Option<JsOcrConfig>,
    /// Override force OCR for this file.
    #[napi(js_name = "forceOcr")]
    #[serde(rename = "forceOcr")]
    pub force_ocr: Option<bool>,
    /// Override force OCR pages for this file (1-indexed page numbers).
    #[napi(js_name = "forceOcrPages")]
    #[serde(rename = "forceOcrPages")]
    pub force_ocr_pages: Option<Vec<u32>>,
    /// Override disable OCR for this file.
    #[napi(js_name = "disableOcr")]
    #[serde(rename = "disableOcr")]
    pub disable_ocr: Option<bool>,
    /// Override chunking configuration for this file.
    pub chunking: Option<JsChunkingConfig>,
    /// Override content filtering configuration for this file.
    #[napi(js_name = "contentFilter")]
    #[serde(rename = "contentFilter")]
    pub content_filter: Option<JsContentFilterConfig>,
    /// Override image extraction configuration for this file.
    pub images: Option<JsImageExtractionConfig>,
    /// Override PDF options for this file.
    #[napi(js_name = "pdfOptions")]
    #[serde(rename = "pdfOptions")]
    #[serde(skip)]
    pub pdf_options: Option<JsPdfConfig>,
    /// Override token reduction for this file.
    #[napi(js_name = "tokenReduction")]
    #[serde(rename = "tokenReduction")]
    pub token_reduction: Option<JsTokenReductionOptions>,
    /// Override language detection for this file.
    #[napi(js_name = "languageDetection")]
    #[serde(rename = "languageDetection")]
    pub language_detection: Option<JsLanguageDetectionConfig>,
    /// Override page extraction for this file.
    pub pages: Option<JsPageConfig>,
    /// Override keyword extraction for this file.
    #[serde(skip)]
    pub keywords: Option<JsKeywordConfig>,
    /// Override post-processor for this file.
    pub postprocessor: Option<JsPostProcessorConfig>,
    /// Override HTML conversion options for this file.
    #[napi(js_name = "htmlOptions")]
    #[serde(rename = "htmlOptions")]
    #[serde(skip)]
    pub html_options: Option<String>,
    /// Override result format for this file.
    #[napi(js_name = "resultFormat")]
    #[serde(rename = "resultFormat")]
    pub result_format: Option<JsResultFormat>,
    /// Override output content format for this file.
    #[napi(js_name = "outputFormat")]
    #[serde(rename = "outputFormat")]
    pub output_format: Option<JsOutputFormat>,
    /// Override document structure output for this file.
    #[napi(js_name = "includeDocumentStructure")]
    #[serde(rename = "includeDocumentStructure")]
    pub include_document_structure: Option<bool>,
    /// Override layout detection for this file.
    #[serde(skip)]
    pub layout: Option<JsLayoutDetectionConfig>,
    /// Override per-file extraction timeout in seconds.
    ///
    /// When set, the extraction for this file will be canceled after the
    /// specified duration. A timed-out file produces an error result without
    /// affecting other files in the batch.
    #[napi(js_name = "timeoutSecs")]
    #[serde(rename = "timeoutSecs")]
    pub timeout_secs: Option<i64>,
    /// Override tree-sitter configuration for this file.
    #[napi(js_name = "treeSitter")]
    #[serde(rename = "treeSitter")]
    #[serde(skip)]
    pub tree_sitter: Option<JsTreeSitterConfig>,
    /// Override structured extraction configuration for this file.
    ///
    /// When set, enables LLM-based structured extraction with a JSON schema
    /// for this specific file. The extracted content is sent to a VLM/LLM
    /// and the response is parsed according to the provided schema.
    #[napi(js_name = "structuredExtraction")]
    #[serde(rename = "structuredExtraction")]
    pub structured_extraction: Option<JsStructuredExtractionConfig>,
}

/// Batch item for byte array extraction.
///
/// Used with `batch_extract_bytes` and `batch_extract_bytes_sync`
/// to represent a single item in a batch extraction job.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "BatchBytesItem")]
pub struct JsBatchBytesItem {
    /// The content bytes to extract from
    #[napi(ts_type = "Uint8Array | Buffer | Array<number>")]
    pub content: JsBytes,
    /// MIME type of the content (e.g., "application/pdf", "text/html")
    #[napi(js_name = "mimeType")]
    #[serde(rename = "mimeType")]
    pub mime_type: String,
    /// Per-item configuration overrides (None uses batch-level defaults)
    pub config: Option<JsFileExtractionConfig>,
}

/// Batch item for file extraction.
///
/// Used with `batch_extract_files` and `batch_extract_files_sync`
/// to represent a single file in a batch extraction job.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "BatchFileItem")]
pub struct JsBatchFileItem {
    /// Path to the file to extract from
    pub path: String,
    /// Per-file configuration overrides (None uses batch-level defaults)
    pub config: Option<JsFileExtractionConfig>,
}

/// Image extraction configuration.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ImageExtractionConfig")]
pub struct JsImageExtractionConfig {
    /// Extract images from documents
    #[napi(js_name = "extractImages")]
    #[serde(rename = "extractImages")]
    pub extract_images: Option<bool>,
    /// Target DPI for image normalization
    #[napi(js_name = "targetDpi")]
    #[serde(rename = "targetDpi")]
    pub target_dpi: Option<i32>,
    /// Maximum dimension for images (width or height)
    #[napi(js_name = "maxImageDimension")]
    #[serde(rename = "maxImageDimension")]
    pub max_image_dimension: Option<i32>,
    /// Whether to inject image reference placeholders into markdown output.
    /// When `true` (default), image references like `![Image 1](embedded:p1_i0)`
    /// are appended to the markdown. Set to `false` to extract images as data
    /// without polluting the markdown output.
    #[napi(js_name = "injectPlaceholders")]
    #[serde(rename = "injectPlaceholders")]
    pub inject_placeholders: Option<bool>,
    /// Automatically adjust DPI based on image content
    #[napi(js_name = "autoAdjustDpi")]
    #[serde(rename = "autoAdjustDpi")]
    pub auto_adjust_dpi: Option<bool>,
    /// Minimum DPI threshold
    #[napi(js_name = "minDpi")]
    #[serde(rename = "minDpi")]
    pub min_dpi: Option<i32>,
    /// Maximum DPI threshold
    #[napi(js_name = "maxDpi")]
    #[serde(rename = "maxDpi")]
    pub max_dpi: Option<i32>,
    /// Maximum number of image objects to extract per PDF page.
    ///
    /// Some PDFs (e.g. technical diagrams stored as thousands of raster fragments)
    /// can trigger extremely long or indefinite extraction times when every image
    /// object on a dense page is decoded individually via the PDF extractor. Setting this
    /// limit causes kreuzberg to stop collecting individual images once the count
    /// per page reaches the cap and emit a warning instead.
    ///
    /// `None` (default) means no limit — all images are extracted.
    #[napi(js_name = "maxImagesPerPage")]
    #[serde(rename = "maxImagesPerPage")]
    pub max_images_per_page: Option<u32>,
    /// When `true` (default), extracted images are classified by kind and grouped
    /// into clusters where they appear to belong to one figure.
    pub classify: Option<bool>,
    /// When `true`, full-page renders produced during OCR preprocessing are captured
    /// and returned as `ImageKind::PageRaster` entries in `ExtractionResult.images`.
    ///
    /// **PDF + OCR only.** No rasters are captured for non-PDF inputs or when the
    /// document-level OCR bypass is active (whole-document backend). When OCR is
    /// enabled and this flag is set but the active backend skips per-page rendering,
    /// a `ProcessingWarning` is emitted in `ExtractionResult.processing_warnings`.
    ///
    /// Defaults to `false`. Enable when downstream consumers need page thumbnails
    /// (e.g. citation previews, visual grounding).
    #[napi(js_name = "includePageRasters")]
    #[serde(rename = "includePageRasters")]
    pub include_page_rasters: Option<bool>,
    /// Run OCR on extracted images and include the recognized text in the document content.
    ///
    /// When `true` (default) and `ExtractionConfig.ocr` is configured, extracted images
    /// are processed with the configured OCR backend. Set to `false` to extract images
    /// without OCR processing, even when OCR is enabled.
    #[napi(js_name = "runOcrOnImages")]
    #[serde(rename = "runOcrOnImages")]
    pub run_ocr_on_images: Option<bool>,
    /// When `true`, image OCR results are rendered as plain text without the
    /// `![...](...)` markdown placeholder. Only takes effect when `run_ocr_on_images`
    /// is also `true`.
    #[napi(js_name = "ocrTextOnly")]
    #[serde(rename = "ocrTextOnly")]
    pub ocr_text_only: Option<bool>,
    /// When `true` and `ocr_text_only` is `false`, append the OCR text after
    /// the image placeholder in the rendered output.
    #[napi(js_name = "appendOcrText")]
    #[serde(rename = "appendOcrText")]
    pub append_ocr_text: Option<bool>,
}

#[napi(js_name = "imageExtractionConfigDefault")]
pub fn image_extraction_config_default() -> JsImageExtractionConfig {
    kreuzberg::ImageExtractionConfig::default().into()
}

/// Token reduction configuration.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "TokenReductionOptions")]
pub struct JsTokenReductionOptions {
    /// Reduction mode: "off", "light", "moderate", "aggressive", "maximum"
    pub mode: Option<String>,
    /// Preserve important words (capitalized, technical terms)
    #[napi(js_name = "preserveImportantWords")]
    #[serde(rename = "preserveImportantWords")]
    pub preserve_important_words: Option<bool>,
}

#[napi(js_name = "tokenReductionOptionsDefault")]
pub fn token_reduction_options_default() -> JsTokenReductionOptions {
    kreuzberg::TokenReductionOptions::default().into()
}

/// Language detection configuration.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "LanguageDetectionConfig")]
pub struct JsLanguageDetectionConfig {
    /// Enable language detection
    pub enabled: Option<bool>,
    /// Minimum confidence threshold (0.0-1.0)
    #[napi(js_name = "minConfidence")]
    #[serde(rename = "minConfidence")]
    pub min_confidence: Option<f64>,
    /// Detect multiple languages in the document
    #[napi(js_name = "detectMultiple")]
    #[serde(rename = "detectMultiple")]
    pub detect_multiple: Option<bool>,
}

#[napi(js_name = "languageDetectionConfigDefault")]
pub fn language_detection_config_default() -> JsLanguageDetectionConfig {
    kreuzberg::LanguageDetectionConfig::default().into()
}

/// Configuration for styled HTML output.
///
/// When set on `ExtractionConfig.html_output` alongside
/// `output_format = OutputFormat.Html`, the pipeline builds a
/// `StyledHtmlRenderer`(crate.rendering.StyledHtmlRenderer) instead of
/// the plain comrak-based renderer.
///
/// # Example
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "HtmlOutputConfig")]
pub struct JsHtmlOutputConfig {
    /// Inline CSS string injected into the output after the theme stylesheet.
    /// Concatenated after `css_file` content when both are set.
    pub css: Option<String>,
    /// Path to a CSS file loaded once at renderer construction time.
    /// Concatenated before `css` when both are set.
    #[napi(js_name = "cssFile")]
    #[serde(rename = "cssFile")]
    pub css_file: Option<String>,
    /// Built-in colour/typography theme. Default: [`HtmlTheme::Unstyled`].
    pub theme: Option<JsHtmlTheme>,
    /// CSS class prefix applied to every emitted class name.
    ///
    /// Default: `"kb-"`. Change this if your host application already uses
    /// classes that start with `kb-`.
    #[napi(js_name = "classPrefix")]
    #[serde(rename = "classPrefix")]
    pub class_prefix: Option<String>,
    /// When `true` (default), write the resolved CSS into a `<style>` block
    /// immediately after the opening `<div class="{prefix}doc">`.
    ///
    /// Set to `false` to emit only the structural markup and wire up your
    /// own stylesheet targeting the `kb-*` class names.
    #[napi(js_name = "embedCss")]
    #[serde(rename = "embedCss")]
    pub embed_css: Option<bool>,
}

#[napi(js_name = "htmlOutputConfigDefault")]
pub fn html_output_config_default() -> JsHtmlOutputConfig {
    kreuzberg::HtmlOutputConfig::default().into()
}

/// Layout detection configuration.
///
/// Controls layout detection behavior in the extraction pipeline.
/// When set on `ExtractionConfig`(super.ExtractionConfig), layout detection
/// is enabled for PDF extraction.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "LayoutDetectionConfig")]
pub struct JsLayoutDetectionConfig {
    /// Confidence threshold override (None = use model default).
    #[napi(js_name = "confidenceThreshold")]
    #[serde(rename = "confidenceThreshold")]
    pub confidence_threshold: Option<f64>,
    /// Whether to apply postprocessing heuristics (default: true).
    #[napi(js_name = "applyHeuristics")]
    #[serde(rename = "applyHeuristics")]
    pub apply_heuristics: Option<bool>,
    /// Table structure recognition model.
    ///
    /// Controls which model is used for table cell detection within layout-detected
    /// table regions. Defaults to [`TableModel::Tatr`].
    #[napi(js_name = "tableModel")]
    #[serde(rename = "tableModel")]
    pub table_model: Option<JsTableModel>,
    /// Hardware acceleration for ONNX models (layout detection + table structure).
    ///
    /// When set, controls which execution provider (CPU, CUDA, CoreML, TensorRT)
    /// is used for inference. Defaults to `None` (auto-select per platform).
    pub acceleration: Option<JsAccelerationConfig>,
}

#[napi(js_name = "layoutDetectionConfigDefault")]
pub fn layout_detection_config_default() -> JsLayoutDetectionConfig {
    kreuzberg::LayoutDetectionConfig::default().into()
}

/// Configuration for an LLM provider/model via liter-llm.
///
/// Each feature (VLM OCR, VLM embeddings, structured extraction) carries
/// its own `LlmConfig`, allowing different providers per feature.
///
/// # Example
///
/// ```toml
/// [structured_extraction.llm]
/// model = "openai/gpt-4o"
/// api_key = "sk-..."  # or use KREUZBERG_LLM_API_KEY env var
/// ```
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "LlmConfig")]
pub struct JsLlmConfig {
    /// Provider/model string using liter-llm routing format.
    ///
    /// Examples: `"openai/gpt-4o"`, `"anthropic/claude-sonnet-4-20250514"`,
    /// `"groq/llama-3.1-70b-versatile"`.
    pub model: Option<String>,
    /// API key for the provider. When `None`, liter-llm falls back to
    /// the provider's standard environment variable (e.g., `OPENAI_API_KEY`).
    #[napi(js_name = "apiKey")]
    #[serde(rename = "apiKey")]
    pub api_key: Option<String>,
    /// Custom base URL override for the provider endpoint.
    #[napi(js_name = "baseUrl")]
    #[serde(rename = "baseUrl")]
    pub base_url: Option<String>,
    /// Request timeout in seconds (default: 60).
    #[napi(js_name = "timeoutSecs")]
    #[serde(rename = "timeoutSecs")]
    pub timeout_secs: Option<i64>,
    /// Maximum retry attempts (default: 3).
    #[napi(js_name = "maxRetries")]
    #[serde(rename = "maxRetries")]
    pub max_retries: Option<u32>,
    /// Sampling temperature for generation tasks.
    pub temperature: Option<f64>,
    /// Maximum tokens to generate.
    #[napi(js_name = "maxTokens")]
    #[serde(rename = "maxTokens")]
    pub max_tokens: Option<i64>,
}

/// Configuration for LLM-based structured data extraction.
///
/// Sends extracted document content to a VLM with a JSON schema,
/// returning structured data that conforms to the schema.
///
/// # Example
///
/// ```toml
/// [structured_extraction]
/// schema_name = "invoice_data"
/// strict = true
///
/// [structured_extraction.schema]
/// type = "object"
/// properties.vendor = { type = "string" }
/// properties.total = { type = "number" }
/// required = ["vendor", "total"]
///
/// [structured_extraction.llm]
/// model = "openai/gpt-4o"
/// ```
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "StructuredExtractionConfig")]
pub struct JsStructuredExtractionConfig {
    /// JSON Schema defining the desired output structure.
    pub schema: serde_json::Value,
    /// Schema name passed to the LLM's structured output mode.
    #[napi(js_name = "schemaName")]
    #[serde(rename = "schemaName")]
    pub schema_name: String,
    /// Optional schema description for the LLM.
    #[napi(js_name = "schemaDescription")]
    #[serde(rename = "schemaDescription")]
    pub schema_description: Option<String>,
    /// Enable strict mode — output must exactly match the schema.
    pub strict: bool,
    /// Custom Jinja2 extraction prompt template. When `None`, a default template is used.
    ///
    /// Available template variables:
    /// - `{{ content }}` — The extracted document text.
    /// - `{{ schema }}` — The JSON schema as a formatted string.
    /// - `{{ schema_name }}` — The schema name.
    /// - `{{ schema_description }}` — The schema description (may be empty).
    pub prompt: Option<String>,
    /// LLM configuration for the extraction.
    pub llm: JsLlmConfig,
}

/// Quality thresholds for OCR fallback decisions and pipeline quality gating.
///
/// All fields default to the values that match the previous hardcoded behavior,
/// so `OcrQualityThresholds.default()` preserves existing semantics exactly.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "OcrQualityThresholds")]
pub struct JsOcrQualityThresholds {
    /// Minimum total non-whitespace characters to consider text substantive.
    #[napi(js_name = "minTotalNonWhitespace")]
    #[serde(rename = "minTotalNonWhitespace")]
    pub min_total_non_whitespace: Option<i64>,
    /// Minimum non-whitespace characters per page on average.
    #[napi(js_name = "minNonWhitespacePerPage")]
    #[serde(rename = "minNonWhitespacePerPage")]
    pub min_non_whitespace_per_page: Option<f64>,
    /// Minimum character count for a word to be "meaningful".
    #[napi(js_name = "minMeaningfulWordLen")]
    #[serde(rename = "minMeaningfulWordLen")]
    pub min_meaningful_word_len: Option<i64>,
    /// Minimum count of meaningful words before text is accepted.
    #[napi(js_name = "minMeaningfulWords")]
    #[serde(rename = "minMeaningfulWords")]
    pub min_meaningful_words: Option<i64>,
    /// Minimum alphanumeric ratio (non-whitespace chars that are alphanumeric).
    #[napi(js_name = "minAlnumRatio")]
    #[serde(rename = "minAlnumRatio")]
    pub min_alnum_ratio: Option<f64>,
    /// Minimum Unicode replacement characters (U+FFFD) to trigger OCR fallback.
    #[napi(js_name = "minGarbageChars")]
    #[serde(rename = "minGarbageChars")]
    pub min_garbage_chars: Option<i64>,
    /// Maximum fraction of short (1-2 char) words before text is considered fragmented.
    #[napi(js_name = "maxFragmentedWordRatio")]
    #[serde(rename = "maxFragmentedWordRatio")]
    pub max_fragmented_word_ratio: Option<f64>,
    /// Critical fragmentation threshold — triggers OCR regardless of meaningful words.
    /// Normal English text has ~20-30% short words. 80%+ is definitive garbage.
    #[napi(js_name = "criticalFragmentedWordRatio")]
    #[serde(rename = "criticalFragmentedWordRatio")]
    pub critical_fragmented_word_ratio: Option<f64>,
    /// Minimum average word length. Below this with enough words indicates garbled extraction.
    #[napi(js_name = "minAvgWordLength")]
    #[serde(rename = "minAvgWordLength")]
    pub min_avg_word_length: Option<f64>,
    /// Minimum word count before average word length check applies.
    #[napi(js_name = "minWordsForAvgLengthCheck")]
    #[serde(rename = "minWordsForAvgLengthCheck")]
    pub min_words_for_avg_length_check: Option<i64>,
    /// Minimum consecutive word repetition ratio to detect column scrambling.
    #[napi(js_name = "minConsecutiveRepeatRatio")]
    #[serde(rename = "minConsecutiveRepeatRatio")]
    pub min_consecutive_repeat_ratio: Option<f64>,
    /// Minimum word count before consecutive repetition check is applied.
    #[napi(js_name = "minWordsForRepeatCheck")]
    #[serde(rename = "minWordsForRepeatCheck")]
    pub min_words_for_repeat_check: Option<i64>,
    /// Minimum character count for "substantive markdown" OCR skip gate.
    #[napi(js_name = "substantiveMinChars")]
    #[serde(rename = "substantiveMinChars")]
    pub substantive_min_chars: Option<i64>,
    /// Minimum character count for "non-text content" OCR skip gate.
    #[napi(js_name = "nonTextMinChars")]
    #[serde(rename = "nonTextMinChars")]
    pub non_text_min_chars: Option<i64>,
    /// Alphanumeric+whitespace ratio threshold for skip decisions.
    #[napi(js_name = "alnumWsRatioThreshold")]
    #[serde(rename = "alnumWsRatioThreshold")]
    pub alnum_ws_ratio_threshold: Option<f64>,
    /// Minimum quality score (0.0-1.0) for a pipeline stage result to be accepted.
    /// If the result from a backend scores below this, try the next backend.
    #[napi(js_name = "pipelineMinQuality")]
    #[serde(rename = "pipelineMinQuality")]
    pub pipeline_min_quality: Option<f64>,
}

#[napi(js_name = "ocrQualityThresholdsDefault")]
pub fn ocr_quality_thresholds_default() -> JsOcrQualityThresholds {
    kreuzberg::OcrQualityThresholds::default().into()
}

/// A single backend stage in the OCR pipeline.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "OcrPipelineStage")]
pub struct JsOcrPipelineStage {
    /// Backend name: "tesseract", "paddleocr", "easyocr", or a custom registered name.
    pub backend: String,
    /// Priority weight (higher = tried first). Stages are sorted by priority descending.
    pub priority: u32,
    /// Language override for this stage (None = use parent OcrConfig.language).
    pub language: Option<String>,
    /// Tesseract-specific config override for this stage.
    #[napi(js_name = "tesseractConfig")]
    #[serde(rename = "tesseractConfig")]
    pub tesseract_config: Option<JsTesseractConfig>,
    /// PaddleOCR-specific config for this stage.
    #[napi(js_name = "paddleOcrConfig")]
    #[serde(rename = "paddleOcrConfig")]
    pub paddle_ocr_config: Option<serde_json::Value>,
    /// VLM config override for this pipeline stage.
    #[napi(js_name = "vlmConfig")]
    #[serde(rename = "vlmConfig")]
    pub vlm_config: Option<JsLlmConfig>,
    /// Arbitrary per-call options passed through to the backend unchanged.
    ///
    /// Backends that support runtime tuning (mode switching, preprocessing
    /// flags, inference parameters, etc.) read this value and deserialize
    /// the keys they care about. Keys unknown to the backend are silently
    /// ignored, so options from different backends can coexist in the same
    /// config without conflict.
    ///
    /// Example (custom backend):
    /// ```json
    /// { "mode": "fast", "enable_layout": true }
    /// ```
    #[napi(js_name = "backendOptions")]
    #[serde(rename = "backendOptions")]
    pub backend_options: Option<serde_json::Value>,
}

/// Multi-backend OCR pipeline with quality-based fallback.
///
/// Backends are tried in priority order (highest first). After each backend
/// produces output, quality is evaluated. If it meets `quality_thresholds.pipeline_min_quality`,
/// the result is accepted. Otherwise the next backend is tried.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "OcrPipelineConfig")]
pub struct JsOcrPipelineConfig {
    /// Ordered list of backends to try. Sorted by priority (descending) at runtime.
    pub stages: Vec<JsOcrPipelineStage>,
    /// Quality thresholds for deciding whether to accept a result or try the next backend.
    #[napi(js_name = "qualityThresholds")]
    #[serde(rename = "qualityThresholds")]
    pub quality_thresholds: JsOcrQualityThresholds,
}

/// OCR configuration.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "OcrConfig")]
#[serde_with::serde_as]
pub struct JsOcrConfig {
    /// Whether OCR is enabled.
    ///
    /// Setting `enabled: false` is a shorthand for `disable_ocr: true` on the parent
    /// [`ExtractionConfig`](crate::core::config::ExtractionConfig). Images return
    /// metadata only; PDFs use native text extraction without OCR fallback.
    ///
    /// Defaults to `true`. When `false`, all other OCR settings are ignored.
    pub enabled: Option<bool>,
    /// OCR backend: tesseract, easyocr, paddleocr
    pub backend: Option<String>,
    /// Language code (e.g., "eng", "deu")
    pub language: Option<String>,
    /// Tesseract-specific configuration (optional)
    #[napi(js_name = "tesseractConfig")]
    #[serde(rename = "tesseractConfig")]
    pub tesseract_config: Option<JsTesseractConfig>,
    /// Output format for OCR results (optional, for format conversion)
    #[napi(js_name = "outputFormat")]
    #[serde(rename = "outputFormat")]
    pub output_format: Option<JsOutputFormat>,
    /// PaddleOCR-specific configuration (optional, JSON passthrough)
    #[napi(js_name = "paddleOcrConfig")]
    #[serde(rename = "paddleOcrConfig")]
    pub paddle_ocr_config: Option<serde_json::Value>,
    /// Arbitrary per-call options passed through to the backend unchanged.
    ///
    /// Custom OCR backends and built-in backends that support runtime tuning
    /// can read this value and deserialize the keys they care about. Keys
    /// unknown to the backend are silently ignored.
    ///
    /// This is the recommended extension point for per-call parameters that
    /// are not covered by the typed fields above (e.g. mode switching,
    /// preprocessing flags, inference batch size).
    ///
    /// **Scope:** when `pipeline` is `None`, this value is propagated to the
    /// primary stage of the auto-constructed pipeline. When `pipeline` is
    /// explicitly set, this field has **no effect** — the caller must set
    /// `OcrPipelineStage.backend_options` directly on the relevant stage(s)
    /// instead.
    ///
    /// Example:
    /// ```json
    /// { "mode": "fast", "enable_layout": true, "timeout_ms": 5000 }
    /// ```
    #[napi(js_name = "backendOptions")]
    #[serde(rename = "backendOptions")]
    pub backend_options: Option<serde_json::Value>,
    /// OCR element extraction configuration
    #[napi(js_name = "elementConfig")]
    #[serde(rename = "elementConfig")]
    pub element_config: Option<JsOcrElementConfig>,
    /// Quality thresholds for the native-text-to-OCR fallback decision.
    /// When None, uses compiled defaults (matching previous hardcoded behavior).
    #[napi(js_name = "qualityThresholds")]
    #[serde(rename = "qualityThresholds")]
    pub quality_thresholds: Option<JsOcrQualityThresholds>,
    /// Multi-backend OCR pipeline configuration. When set, enables weighted
    /// fallback across multiple OCR backends based on output quality.
    /// When None, uses the single `backend` field (same as today).
    pub pipeline: Option<JsOcrPipelineConfig>,
    /// Enable automatic page rotation based on orientation detection.
    ///
    /// When enabled, uses Tesseract's `DetectOrientationScript()` to detect
    /// page orientation (0/90/180/270 degrees) before OCR. If the page is
    /// rotated with high confidence, the image is corrected before recognition.
    /// This is critical for handling rotated scanned documents.
    #[napi(js_name = "autoRotate")]
    #[serde(rename = "autoRotate")]
    pub auto_rotate: Option<bool>,
    /// VLM (Vision Language Model) OCR configuration.
    ///
    /// Required when `backend` is `"vlm"`. Uses liter-llm to send page
    /// images to a vision model for text extraction.
    #[napi(js_name = "vlmConfig")]
    #[serde(rename = "vlmConfig")]
    pub vlm_config: Option<JsLlmConfig>,
    /// Custom Jinja2 prompt template for VLM OCR.
    ///
    /// When `None`, uses the default template. Available variables:
    /// - `{{ language }}` — The document language code (e.g., "eng", "deu").
    #[napi(js_name = "vlmPrompt")]
    #[serde(rename = "vlmPrompt")]
    pub vlm_prompt: Option<String>,
    /// Hardware acceleration for ONNX Runtime models (e.g. PaddleOCR, layout detection).
    ///
    /// Not user-configurable via config files — injected at runtime from
    /// `ExtractionConfig::acceleration` before each `process_image` call.
    pub acceleration: Option<JsAccelerationConfig>,
    /// Caller-supplied Tesseract `traineddata` bytes per language code.
    ///
    /// Primary use case is the WASM build, which has no filesystem and cannot
    /// download tessdata at runtime. Native builds typically rely on
    /// `TessdataManager` and ignore this field. When present, the WASM
    /// Tesseract backend prefers these bytes over its compile-time-bundled
    /// English data.
    ///
    /// Skipped by serde to keep config files small — supply via the typed API
    /// at runtime.
    #[napi(
        js_name = "tessdataBytes",
        ts_type = "Record<string, Uint8Array | Buffer | Array<number>>"
    )]
    #[serde(rename = "tessdataBytes")]
    #[serde_as(as = "HashMap<_, serde_with::Bytes>")]
    pub tessdata_bytes: Option<HashMap<String, JsBytes>>,
}

#[napi(js_name = "ocrConfigDefault")]
pub fn ocr_config_default() -> JsOcrConfig {
    kreuzberg::OcrConfig::default().into()
}

/// Page extraction and tracking configuration.
///
/// Controls how pages are extracted, tracked, and represented in the extraction results.
/// When `None`, page tracking is disabled.
///
/// Page range tracking in chunk metadata (first_page/last_page) is automatically enabled
/// when page boundaries are available and chunking is configured.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "PageConfig")]
pub struct JsPageConfig {
    /// Extract pages as separate array (ExtractionResult.pages)
    #[napi(js_name = "extractPages")]
    #[serde(rename = "extractPages")]
    pub extract_pages: Option<bool>,
    /// Insert page markers in main content string
    #[napi(js_name = "insertPageMarkers")]
    #[serde(rename = "insertPageMarkers")]
    pub insert_page_markers: Option<bool>,
    /// Page marker format (use {page_num} placeholder)
    /// Default: "\n\n<!-- PAGE {page_num} -->\n\n"
    #[napi(js_name = "markerFormat")]
    #[serde(rename = "markerFormat")]
    pub marker_format: Option<String>,
}

#[napi(js_name = "pageConfigDefault")]
pub fn page_config_default() -> JsPageConfig {
    kreuzberg::PageConfig::default().into()
}

/// PDF-specific configuration.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "PdfConfig")]
pub struct JsPdfConfig {
    /// Extract images from PDF
    #[napi(js_name = "extractImages")]
    #[serde(rename = "extractImages")]
    pub extract_images: Option<bool>,
    /// Extract tables from PDF.
    ///
    /// When `true` (default), runs pdf_oxide's native grid detector and, if it
    /// finds nothing, falls back to the heuristic text-layer reconstruction in
    /// `pdf::oxide::table::extract_tables_heuristic`. Set to `false` to skip
    /// both passes — `tables` will then be empty in the result.
    #[napi(js_name = "extractTables")]
    #[serde(rename = "extractTables")]
    pub extract_tables: Option<bool>,
    /// List of passwords to try when opening encrypted PDFs
    pub passwords: Option<Vec<String>>,
    /// Extract PDF metadata
    #[napi(js_name = "extractMetadata")]
    #[serde(rename = "extractMetadata")]
    pub extract_metadata: Option<bool>,
    /// Hierarchy extraction configuration (None = hierarchy extraction disabled)
    pub hierarchy: Option<JsHierarchyConfig>,
    /// Extract PDF annotations (text notes, highlights, links, stamps).
    /// Default: false
    #[napi(js_name = "extractAnnotations")]
    #[serde(rename = "extractAnnotations")]
    pub extract_annotations: Option<bool>,
    /// Top margin fraction (0.0–1.0) of page height to exclude headers/running heads.
    /// Default: 0.06 (6%)
    #[napi(js_name = "topMarginFraction")]
    #[serde(rename = "topMarginFraction")]
    pub top_margin_fraction: Option<f64>,
    /// Bottom margin fraction (0.0–1.0) of page height to exclude footers/page numbers.
    /// Default: 0.05 (5%)
    #[napi(js_name = "bottomMarginFraction")]
    #[serde(rename = "bottomMarginFraction")]
    pub bottom_margin_fraction: Option<f64>,
    /// Allow single-column pseudo tables in extraction results.
    ///
    /// By default, tables with fewer than 2 columns (layout-guided) or 3 columns
    /// (heuristic) are rejected. When `true`, the minimum column count is relaxed
    /// to 1, allowing single-column structured data (glossaries, itemized lists)
    /// to be emitted as tables. Other quality filters (density, sparsity, prose
    /// detection) still apply.
    #[napi(js_name = "allowSingleColumnTables")]
    #[serde(rename = "allowSingleColumnTables")]
    pub allow_single_column_tables: Option<bool>,
    /// Perform OCR on inline images extracted from PDF pages and attach the
    /// recognized text to each `ExtractedImage.ocr_result`. Requires Tesseract
    /// to be available; if `ExtractionConfig.ocr` is `None` the extractor
    /// falls back to `TesseractConfig::default()`. Per-image failures degrade
    /// gracefully (the image is returned without OCR text rather than failing
    /// the whole extraction). Default: `false`.
    #[napi(js_name = "ocrInlineImages")]
    #[serde(rename = "ocrInlineImages")]
    pub ocr_inline_images: Option<bool>,
}

#[napi(js_name = "pdfConfigDefault")]
pub fn pdf_config_default() -> JsPdfConfig {
    kreuzberg::PdfConfig::default().into()
}

/// Hierarchy extraction configuration for PDF text structure analysis.
///
/// Enables extraction of document hierarchy levels (H1-H6) based on font size
/// clustering and semantic analysis. When enabled, hierarchical blocks are
/// included in page content.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "HierarchyConfig")]
pub struct JsHierarchyConfig {
    /// Enable hierarchy extraction
    pub enabled: Option<bool>,
    /// Number of font size clusters to use for hierarchy levels (1-7)
    ///
    /// Default: 6, which provides H1-H6 heading levels with body text.
    /// Larger values create more fine-grained hierarchy levels.
    #[napi(js_name = "kClusters")]
    #[serde(rename = "kClusters")]
    pub k_clusters: Option<i64>,
    /// Include bounding box information in hierarchy blocks
    #[napi(js_name = "includeBbox")]
    #[serde(rename = "includeBbox")]
    pub include_bbox: Option<bool>,
    /// OCR coverage threshold for smart OCR triggering (0.0-1.0)
    ///
    /// Determines when OCR should be triggered based on text block coverage.
    /// OCR is triggered when text blocks cover less than this fraction of the page.
    /// Default: 0.5 (trigger OCR if less than 50% of page has text)
    #[napi(js_name = "ocrCoverageThreshold")]
    #[serde(rename = "ocrCoverageThreshold")]
    pub ocr_coverage_threshold: Option<f64>,
}

#[napi(js_name = "hierarchyConfigDefault")]
pub fn hierarchy_config_default() -> JsHierarchyConfig {
    kreuzberg::HierarchyConfig::default().into()
}

/// Post-processor configuration.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "PostProcessorConfig")]
pub struct JsPostProcessorConfig {
    /// Enable post-processors
    pub enabled: Option<bool>,
    /// Whitelist of processor names to run (None = all enabled)
    #[napi(js_name = "enabledProcessors")]
    #[serde(rename = "enabledProcessors")]
    pub enabled_processors: Option<Vec<String>>,
    /// Blacklist of processor names to skip (None = none disabled)
    #[napi(js_name = "disabledProcessors")]
    #[serde(rename = "disabledProcessors")]
    pub disabled_processors: Option<Vec<String>>,
    /// Pre-computed AHashSet for O(1) enabled processor lookup
    #[napi(js_name = "enabledSet")]
    #[serde(rename = "enabledSet")]
    pub enabled_set: Option<Vec<String>>,
    /// Pre-computed AHashSet for O(1) disabled processor lookup
    #[napi(js_name = "disabledSet")]
    #[serde(rename = "disabledSet")]
    pub disabled_set: Option<Vec<String>>,
}

#[napi(js_name = "postProcessorConfigDefault")]
pub fn post_processor_config_default() -> JsPostProcessorConfig {
    kreuzberg::PostProcessorConfig::default().into()
}

/// Chunking configuration.
///
/// Configures text chunking for document content, including chunk size,
/// overlap, trimming behavior, and optional embeddings.
///
/// Use `..Default.default()` when constructing to allow for future field additions:
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ChunkingConfig")]
pub struct JsChunkingConfig {
    /// Maximum size per chunk (in units determined by `sizing`).
    ///
    /// When `sizing` is `Characters` (default), this is the max character count.
    /// When using token-based sizing, this is the max token count.
    ///
    /// Default: 1000
    #[napi(js_name = "max_chars")]
    #[serde(rename = "max_chars")]
    pub max_characters: Option<i64>,
    /// Overlap between chunks (in units determined by `sizing`).
    ///
    /// Default: 200
    #[napi(js_name = "max_overlap")]
    #[serde(rename = "max_overlap")]
    pub overlap: Option<i64>,
    /// Whether to trim whitespace from chunk boundaries.
    ///
    /// Default: true
    pub trim: Option<bool>,
    /// Type of chunker to use (Text or Markdown).
    ///
    /// Default: Text
    #[napi(js_name = "chunkerType")]
    #[serde(rename = "chunkerType")]
    pub chunker_type: Option<JsChunkerType>,
    /// Optional embedding configuration for chunk embeddings.
    pub embedding: Option<JsEmbeddingConfig>,
    /// Use a preset configuration (overrides individual settings if provided).
    pub preset: Option<String>,
    /// How to measure chunk size.
    ///
    /// Default: `Characters` (Unicode character count).
    /// Enable `chunking-tiktoken` or `chunking-tokenizers` features for token-based sizing.
    pub sizing: Option<JsChunkSizing>,
    /// When `true` and `chunker_type` is `Markdown`, prepend the heading hierarchy
    /// path (e.g. `"# Title > ## Section\n\n"`) to each chunk's content string.
    ///
    /// This is useful for RAG pipelines where each chunk needs self-contained
    /// context about its position in the document structure.
    ///
    /// Default: `false`
    #[napi(js_name = "prependHeadingContext")]
    #[serde(rename = "prependHeadingContext")]
    pub prepend_heading_context: Option<bool>,
    /// Optional cosine similarity threshold for semantic topic boundary detection.
    ///
    /// Only used when `chunker_type` is `Semantic` and an `EmbeddingConfig` is
    /// provided. You almost never need to set this. When omitted, defaults to
    /// `0.75` which works well for most documents. Lower values detect more
    /// topic boundaries (more, smaller chunks); higher values detect fewer.
    /// Range: `0.0..=1.0`.
    #[napi(js_name = "topicThreshold")]
    #[serde(rename = "topicThreshold")]
    pub topic_threshold: Option<f64>,
}

#[napi(js_name = "chunkingConfigDefault")]
pub fn chunking_config_default() -> JsChunkingConfig {
    kreuzberg::ChunkingConfig::default().into()
}

/// Embedding configuration for text chunks.
///
/// Configures embedding generation using ONNX models via the vendored embedding engine.
/// Requires the `embeddings` feature to be enabled.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "EmbeddingConfig")]
pub struct JsEmbeddingConfig {
    /// The embedding model to use (defaults to "balanced" preset if not specified)
    pub model: Option<JsEmbeddingModelType>,
    /// Whether to normalize embedding vectors (recommended for cosine similarity)
    pub normalize: Option<bool>,
    /// Batch size for embedding generation
    #[napi(js_name = "batchSize")]
    #[serde(rename = "batchSize")]
    pub batch_size: Option<i64>,
    /// Show model download progress
    #[napi(js_name = "showDownloadProgress")]
    #[serde(rename = "showDownloadProgress")]
    pub show_download_progress: Option<bool>,
    /// Custom cache directory for model files
    ///
    /// Defaults to `~/.cache/kreuzberg/embeddings/` if not specified.
    /// Allows full customization of model download location.
    #[napi(js_name = "cacheDir")]
    #[serde(rename = "cacheDir")]
    pub cache_dir: Option<String>,
    /// Hardware acceleration for the embedding ONNX model.
    ///
    /// When set, controls which execution provider (CPU, CUDA, CoreML, TensorRT)
    /// is used for inference. Defaults to `None` (auto-select per platform).
    pub acceleration: Option<JsAccelerationConfig>,
    /// Maximum wall-clock duration (in seconds) for a single `embed()` call when
    /// using [`EmbeddingModelType::Plugin`].
    ///
    /// Applies only to the in-process plugin path — protects against hung
    /// host-language backends (e.g. a Python callback deadlocked on the GIL,
    /// a model stuck on CUDA OOM retries, etc.). On timeout, the dispatcher
    /// returns `Plugin` instead of blocking forever.
    ///
    /// `None` disables the timeout. The default (60 seconds) is conservative
    /// for common in-process inference; increase for large batches on slow
    /// hardware.
    #[napi(js_name = "maxEmbedDurationSecs")]
    #[serde(rename = "maxEmbedDurationSecs")]
    pub max_embed_duration_secs: Option<i64>,
}

#[napi(js_name = "embeddingConfigDefault")]
pub fn embedding_config_default() -> JsEmbeddingConfig {
    kreuzberg::EmbeddingConfig::default().into()
}

/// Configuration for tree-sitter language pack integration.
///
/// Controls grammar download behavior and code analysis options.
///
/// # Example (TOML)
///
/// ```toml
/// [tree_sitter]
/// languages = ["python", "rust"]
/// groups = ["web"]
///
/// [tree_sitter.process]
/// structure = true
/// comments = true
/// docstrings = true
/// ```
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "TreeSitterConfig")]
pub struct JsTreeSitterConfig {
    /// Enable code intelligence processing (default: true).
    ///
    /// When `false`, tree-sitter analysis is completely skipped even if
    /// the config section is present.
    pub enabled: Option<bool>,
    /// Custom cache directory for downloaded grammars.
    ///
    /// When `None`, uses the default: `~/.cache/tree-sitter-language-pack/v{version}/libs/`.
    #[napi(js_name = "cacheDir")]
    #[serde(rename = "cacheDir")]
    pub cache_dir: Option<String>,
    /// Languages to pre-download on init (e.g., `["python", "rust"]`).
    pub languages: Option<Vec<String>>,
    /// Language groups to pre-download (e.g., `["web", "systems", "scripting"]`).
    pub groups: Option<Vec<String>>,
    /// Processing options for code analysis.
    pub process: Option<JsTreeSitterProcessConfig>,
}

#[napi(js_name = "treeSitterConfigDefault")]
pub fn tree_sitter_config_default() -> JsTreeSitterConfig {
    kreuzberg::TreeSitterConfig::default().into()
}

/// Processing options for tree-sitter code analysis.
///
/// Controls which analysis features are enabled when extracting code files.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "TreeSitterProcessConfig")]
pub struct JsTreeSitterProcessConfig {
    /// Extract structural items (functions, classes, structs, etc.). Default: true.
    pub structure: Option<bool>,
    /// Extract import statements. Default: true.
    pub imports: Option<bool>,
    /// Extract export statements. Default: true.
    pub exports: Option<bool>,
    /// Extract comments. Default: false.
    pub comments: Option<bool>,
    /// Extract docstrings. Default: false.
    pub docstrings: Option<bool>,
    /// Extract symbol definitions. Default: false.
    pub symbols: Option<bool>,
    /// Include parse diagnostics. Default: false.
    pub diagnostics: Option<bool>,
    /// Maximum chunk size in bytes. `None` disables chunking.
    #[napi(js_name = "chunkMaxSize")]
    #[serde(rename = "chunkMaxSize")]
    pub chunk_max_size: Option<i64>,
    /// Content rendering mode for code extraction.
    #[napi(js_name = "contentMode")]
    #[serde(rename = "contentMode")]
    pub content_mode: Option<JsCodeContentMode>,
}

#[napi(js_name = "treeSitterProcessConfigDefault")]
pub fn tree_sitter_process_config_default() -> JsTreeSitterProcessConfig {
    kreuzberg::TreeSitterProcessConfig::default().into()
}

/// A supported document format entry.
///
/// Represents a file extension and its corresponding MIME type that Kreuzberg can process.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "SupportedFormat")]
pub struct JsSupportedFormat {
    /// File extension (without leading dot), e.g., "pdf", "docx"
    pub extension: String,
    /// MIME type string, e.g., "application/pdf"
    #[napi(js_name = "mimeType")]
    #[serde(rename = "mimeType")]
    pub mime_type: String,
}

/// API server configuration.
///
/// This struct holds all configuration options for the Kreuzberg API server,
/// including host/port settings, CORS configuration, and upload limits.
///
/// # Defaults
///
/// - `host`: "127.0.0.1" (localhost only)
/// - `port`: 8000
/// - `cors_origins`: empty vector (allows all origins)
/// - `max_request_body_bytes`: 104_857_600 (100 MB)
/// - `max_multipart_field_bytes`: 104_857_600 (100 MB)
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ServerConfig")]
pub struct JsServerConfig {
    /// Server host address (e.g., "127.0.0.1", "0.0.0.0")
    pub host: Option<String>,
    /// Server port number
    pub port: Option<u16>,
    /// CORS allowed origins. Empty vector means allow all origins.
    ///
    /// If this is an empty vector, the server will accept requests from any origin.
    /// If populated with specific origins (e.g., `"https://example.com"`), only
    /// those origins will be allowed.
    #[napi(js_name = "corsOrigins")]
    #[serde(rename = "corsOrigins")]
    pub cors_origins: Option<Vec<String>>,
    /// Maximum size of request body in bytes (default: 100 MB)
    #[napi(js_name = "maxRequestBodyBytes")]
    #[serde(rename = "maxRequestBodyBytes")]
    pub max_request_body_bytes: Option<i64>,
    /// Maximum size of multipart fields in bytes (default: 100 MB)
    #[napi(js_name = "maxMultipartFieldBytes")]
    #[serde(rename = "maxMultipartFieldBytes")]
    pub max_multipart_field_bytes: Option<i64>,
}

#[napi(js_name = "serverConfigDefault")]
pub fn server_config_default() -> JsServerConfig {
    kreuzberg::ServerConfig::default().into()
}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "StructuredDataResult")]
pub struct JsStructuredDataResult {
    pub content: String,
    pub format: String,
    pub metadata: HashMap<String, String>,
    #[napi(js_name = "textFields")]
    #[serde(rename = "textFields")]
    pub text_fields: Vec<String>,
}

/// Application properties from docProps/app.xml for DOCX
///
/// Contains Word-specific document statistics and metadata.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "DocxAppProperties")]
pub struct JsDocxAppProperties {
    /// Application name (e.g., "Microsoft Office Word")
    pub application: Option<String>,
    /// Application version
    #[napi(js_name = "appVersion")]
    #[serde(rename = "appVersion")]
    pub app_version: Option<String>,
    /// Template filename
    pub template: Option<String>,
    /// Total editing time in minutes
    #[napi(js_name = "totalTime")]
    #[serde(rename = "totalTime")]
    pub total_time: Option<i32>,
    /// Number of pages
    pub pages: Option<i32>,
    /// Number of words
    pub words: Option<i32>,
    /// Number of characters (excluding spaces)
    pub characters: Option<i32>,
    /// Number of characters (including spaces)
    #[napi(js_name = "charactersWithSpaces")]
    #[serde(rename = "charactersWithSpaces")]
    pub characters_with_spaces: Option<i32>,
    /// Number of lines
    pub lines: Option<i32>,
    /// Number of paragraphs
    pub paragraphs: Option<i32>,
    /// Company name
    pub company: Option<String>,
    /// Document security level
    #[napi(js_name = "docSecurity")]
    #[serde(rename = "docSecurity")]
    pub doc_security: Option<i32>,
    /// Scale crop flag
    #[napi(js_name = "scaleCrop")]
    #[serde(rename = "scaleCrop")]
    pub scale_crop: Option<bool>,
    /// Links up to date flag
    #[napi(js_name = "linksUpToDate")]
    #[serde(rename = "linksUpToDate")]
    pub links_up_to_date: Option<bool>,
    /// Shared document flag
    #[napi(js_name = "sharedDoc")]
    #[serde(rename = "sharedDoc")]
    pub shared_doc: Option<bool>,
    /// Hyperlinks changed flag
    #[napi(js_name = "hyperlinksChanged")]
    #[serde(rename = "hyperlinksChanged")]
    pub hyperlinks_changed: Option<bool>,
}

/// Application properties from docProps/app.xml for XLSX
///
/// Contains Excel-specific document metadata.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "XlsxAppProperties")]
pub struct JsXlsxAppProperties {
    /// Application name (e.g., "Microsoft Excel")
    pub application: Option<String>,
    /// Application version
    #[napi(js_name = "appVersion")]
    #[serde(rename = "appVersion")]
    pub app_version: Option<String>,
    /// Document security level
    #[napi(js_name = "docSecurity")]
    #[serde(rename = "docSecurity")]
    pub doc_security: Option<i32>,
    /// Scale crop flag
    #[napi(js_name = "scaleCrop")]
    #[serde(rename = "scaleCrop")]
    pub scale_crop: Option<bool>,
    /// Links up to date flag
    #[napi(js_name = "linksUpToDate")]
    #[serde(rename = "linksUpToDate")]
    pub links_up_to_date: Option<bool>,
    /// Shared document flag
    #[napi(js_name = "sharedDoc")]
    #[serde(rename = "sharedDoc")]
    pub shared_doc: Option<bool>,
    /// Hyperlinks changed flag
    #[napi(js_name = "hyperlinksChanged")]
    #[serde(rename = "hyperlinksChanged")]
    pub hyperlinks_changed: Option<bool>,
    /// Company name
    pub company: Option<String>,
    /// Worksheet names
    #[napi(js_name = "worksheetNames")]
    #[serde(rename = "worksheetNames")]
    pub worksheet_names: Option<Vec<String>>,
}

/// Application properties from docProps/app.xml for PPTX
///
/// Contains PowerPoint-specific document metadata.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "PptxAppProperties")]
pub struct JsPptxAppProperties {
    /// Application name (e.g., "Microsoft Office PowerPoint")
    pub application: Option<String>,
    /// Application version
    #[napi(js_name = "appVersion")]
    #[serde(rename = "appVersion")]
    pub app_version: Option<String>,
    /// Total editing time in minutes
    #[napi(js_name = "totalTime")]
    #[serde(rename = "totalTime")]
    pub total_time: Option<i32>,
    /// Company name
    pub company: Option<String>,
    /// Document security level
    #[napi(js_name = "docSecurity")]
    #[serde(rename = "docSecurity")]
    pub doc_security: Option<i32>,
    /// Scale crop flag
    #[napi(js_name = "scaleCrop")]
    #[serde(rename = "scaleCrop")]
    pub scale_crop: Option<bool>,
    /// Links up to date flag
    #[napi(js_name = "linksUpToDate")]
    #[serde(rename = "linksUpToDate")]
    pub links_up_to_date: Option<bool>,
    /// Shared document flag
    #[napi(js_name = "sharedDoc")]
    #[serde(rename = "sharedDoc")]
    pub shared_doc: Option<bool>,
    /// Hyperlinks changed flag
    #[napi(js_name = "hyperlinksChanged")]
    #[serde(rename = "hyperlinksChanged")]
    pub hyperlinks_changed: Option<bool>,
    /// Number of slides
    pub slides: Option<i32>,
    /// Number of notes
    pub notes: Option<i32>,
    /// Number of hidden slides
    #[napi(js_name = "hiddenSlides")]
    #[serde(rename = "hiddenSlides")]
    pub hidden_slides: Option<i32>,
    /// Number of multimedia clips
    #[napi(js_name = "multimediaClips")]
    #[serde(rename = "multimediaClips")]
    pub multimedia_clips: Option<i32>,
    /// Presentation format (e.g., "Widescreen", "Standard")
    #[napi(js_name = "presentationFormat")]
    #[serde(rename = "presentationFormat")]
    pub presentation_format: Option<String>,
    /// Slide titles
    #[napi(js_name = "slideTitles")]
    #[serde(rename = "slideTitles")]
    pub slide_titles: Option<Vec<String>>,
}

/// Dublin Core metadata from docProps/core.xml
///
/// Contains standard metadata fields defined by the Dublin Core standard
/// and Office-specific extensions.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "CoreProperties")]
pub struct JsCoreProperties {
    /// Document title
    pub title: Option<String>,
    /// Document subject/topic
    pub subject: Option<String>,
    /// Document creator/author
    pub creator: Option<String>,
    /// Keywords or tags
    pub keywords: Option<String>,
    /// Document description/abstract
    pub description: Option<String>,
    /// User who last modified the document
    #[napi(js_name = "lastModifiedBy")]
    #[serde(rename = "lastModifiedBy")]
    pub last_modified_by: Option<String>,
    /// Revision number
    pub revision: Option<String>,
    /// Creation timestamp (ISO 8601)
    pub created: Option<String>,
    /// Last modification timestamp (ISO 8601)
    pub modified: Option<String>,
    /// Document category
    pub category: Option<String>,
    /// Content status (Draft, Final, etc.)
    #[napi(js_name = "contentStatus")]
    #[serde(rename = "contentStatus")]
    pub content_status: Option<String>,
    /// Document language
    pub language: Option<String>,
    /// Unique identifier
    pub identifier: Option<String>,
    /// Document version
    pub version: Option<String>,
    /// Last print timestamp (ISO 8601)
    #[napi(js_name = "lastPrinted")]
    #[serde(rename = "lastPrinted")]
    pub last_printed: Option<String>,
}

/// Configuration for security limits across extractors.
///
/// All limits are intentionally conservative to prevent DoS attacks
/// while still supporting legitimate documents.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "SecurityLimits")]
pub struct JsSecurityLimits {
    /// Maximum uncompressed size for archives (500 MB)
    #[napi(js_name = "maxArchiveSize")]
    #[serde(rename = "maxArchiveSize")]
    pub max_archive_size: Option<i64>,
    /// Maximum compression ratio before flagging as potential bomb (100:1)
    #[napi(js_name = "maxCompressionRatio")]
    #[serde(rename = "maxCompressionRatio")]
    pub max_compression_ratio: Option<i64>,
    /// Maximum number of files in archive (10,000)
    #[napi(js_name = "maxFilesInArchive")]
    #[serde(rename = "maxFilesInArchive")]
    pub max_files_in_archive: Option<i64>,
    /// Maximum nesting depth for structures (100)
    #[napi(js_name = "maxNestingDepth")]
    #[serde(rename = "maxNestingDepth")]
    pub max_nesting_depth: Option<i64>,
    /// Maximum length of any single XML entity / attribute / token (1 MiB).
    /// This is a per-token cap, NOT a total cap — billion-laughs class
    /// attacks where a single entity expands to hundreds of MB are caught
    /// here, while normal long text content (a paragraph, a CDATA block) is
    /// caught by `max_content_size` instead.
    #[napi(js_name = "maxEntityLength")]
    #[serde(rename = "maxEntityLength")]
    pub max_entity_length: Option<i64>,
    /// Maximum string growth per document (100 MB)
    #[napi(js_name = "maxContentSize")]
    #[serde(rename = "maxContentSize")]
    pub max_content_size: Option<i64>,
    /// Maximum iterations per operation
    #[napi(js_name = "maxIterations")]
    #[serde(rename = "maxIterations")]
    pub max_iterations: Option<i64>,
    /// Maximum XML depth (100 levels)
    #[napi(js_name = "maxXmlDepth")]
    #[serde(rename = "maxXmlDepth")]
    pub max_xml_depth: Option<i64>,
    /// Maximum cells per table (100,000)
    #[napi(js_name = "maxTableCells")]
    #[serde(rename = "maxTableCells")]
    pub max_table_cells: Option<i64>,
}

#[napi(js_name = "securityLimitsDefault")]
pub fn security_limits_default() -> JsSecurityLimits {
    kreuzberg::SecurityLimits::default().into()
}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "TokenReductionConfig")]
pub struct JsTokenReductionConfig {
    pub level: Option<JsReductionLevel>,
    #[napi(js_name = "languageHint")]
    #[serde(rename = "languageHint")]
    pub language_hint: Option<String>,
    #[napi(js_name = "preserveMarkdown")]
    #[serde(rename = "preserveMarkdown")]
    pub preserve_markdown: Option<bool>,
    #[napi(js_name = "preserveCode")]
    #[serde(rename = "preserveCode")]
    pub preserve_code: Option<bool>,
    #[napi(js_name = "semanticThreshold")]
    #[serde(rename = "semanticThreshold")]
    pub semantic_threshold: Option<f64>,
    #[napi(js_name = "enableParallel")]
    #[serde(rename = "enableParallel")]
    pub enable_parallel: Option<bool>,
    #[napi(js_name = "useSimd")]
    #[serde(rename = "useSimd")]
    pub use_simd: Option<bool>,
    #[napi(js_name = "customStopwords")]
    #[serde(rename = "customStopwords")]
    pub custom_stopwords: Option<HashMap<String, Vec<String>>>,
    #[napi(js_name = "preservePatterns")]
    #[serde(rename = "preservePatterns")]
    pub preserve_patterns: Option<Vec<String>>,
    #[napi(js_name = "targetReduction")]
    #[serde(rename = "targetReduction")]
    pub target_reduction: Option<f64>,
    #[napi(js_name = "enableSemanticClustering")]
    #[serde(rename = "enableSemanticClustering")]
    pub enable_semantic_clustering: Option<bool>,
}

#[napi(js_name = "tokenReductionConfigDefault")]
pub fn token_reduction_config_default() -> JsTokenReductionConfig {
    kreuzberg::TokenReductionConfig::default().into()
}

/// A PDF annotation extracted from a document page.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "PdfAnnotation")]
pub struct JsPdfAnnotation {
    /// The type of annotation.
    #[napi(js_name = "annotationType")]
    #[serde(rename = "annotationType")]
    pub annotation_type: JsPdfAnnotationType,
    /// Text content of the annotation (e.g., comment text, link URL).
    pub content: Option<String>,
    /// Page number where the annotation appears (1-indexed).
    #[napi(js_name = "pageNumber")]
    #[serde(rename = "pageNumber")]
    pub page_number: u32,
    /// Bounding box of the annotation on the page.
    #[napi(js_name = "boundingBox")]
    #[serde(rename = "boundingBox")]
    pub bounding_box: Option<JsBoundingBox>,
}

/// Comprehensive Djot document structure with semantic preservation.
///
/// This type captures the full richness of Djot markup, including:
/// - Block-level structures (headings, lists, blockquotes, code blocks, etc.)
/// - Inline formatting (emphasis, strong, highlight, subscript, superscript, etc.)
/// - Attributes (classes, IDs, key-value pairs)
/// - Links, images, footnotes
/// - Math expressions (inline and display)
/// - Tables with full structure
///
/// Available when the `djot` feature is enabled.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "DjotContent")]
pub struct JsDjotContent {
    /// Plain text representation for backwards compatibility
    #[napi(js_name = "plainText")]
    #[serde(rename = "plainText")]
    pub plain_text: String,
    /// Structured block-level content
    pub blocks: Vec<JsFormattedBlock>,
    /// Metadata from YAML frontmatter
    pub metadata: JsMetadata,
    /// Extracted tables as structured data
    pub tables: Vec<JsTable>,
    /// Extracted images with metadata
    pub images: Vec<JsDjotImage>,
    /// Extracted links with URLs
    pub links: Vec<JsDjotLink>,
    /// Footnote definitions
    pub footnotes: Vec<JsFootnote>,
    /// Attributes mapped by element identifier (if present)
    pub attributes: Vec<String>,
}

/// Block-level element in a Djot document.
///
/// Represents structural elements like headings, paragraphs, lists, code blocks, etc.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "FormattedBlock")]
pub struct JsFormattedBlock {
    /// Type of block element
    #[napi(js_name = "blockType")]
    #[serde(rename = "blockType")]
    pub block_type: JsBlockType,
    /// Heading level (1-6) for headings, or nesting level for lists
    pub level: Option<i64>,
    /// Inline content within the block
    #[napi(js_name = "inlineContent")]
    #[serde(rename = "inlineContent")]
    pub inline_content: Vec<JsInlineElement>,
    /// Element attributes (classes, IDs, key-value pairs)
    pub attributes: Option<String>,
    /// Language identifier for code blocks
    pub language: Option<String>,
    /// Raw code content for code blocks
    pub code: Option<String>,
    /// Nested blocks for containers (blockquotes, list items, divs)
    pub children: Vec<JsFormattedBlock>,
}

/// Inline element within a block.
///
/// Represents text with formatting, links, images, etc.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "InlineElement")]
pub struct JsInlineElement {
    /// Type of inline element
    #[napi(js_name = "elementType")]
    #[serde(rename = "elementType")]
    pub element_type: JsInlineType,
    /// Text content
    pub content: String,
    /// Element attributes
    pub attributes: Option<String>,
    /// Additional metadata (e.g., href for links, src/alt for images)
    pub metadata: Option<HashMap<String, String>>,
}

/// Image element in Djot.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "DjotImage")]
pub struct JsDjotImage {
    /// Image source URL or path
    pub src: String,
    /// Alternative text
    pub alt: String,
    /// Optional title
    pub title: Option<String>,
    /// Element attributes
    pub attributes: Option<String>,
}

/// Link element in Djot.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "DjotLink")]
pub struct JsDjotLink {
    /// Link URL
    pub url: String,
    /// Link text content
    pub text: String,
    /// Optional title
    pub title: Option<String>,
    /// Element attributes
    pub attributes: Option<String>,
}

/// Footnote in Djot.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "Footnote")]
pub struct JsFootnote {
    /// Footnote label
    pub label: String,
    /// Footnote content blocks
    pub content: Vec<JsFormattedBlock>,
}

/// Top-level structured document representation.
///
/// A flat array of nodes with index-based parent/child references forming a tree.
/// Root-level nodes have `parent: None`. Use `body_roots()` and `furniture_roots()`
/// to iterate over top-level content by layer.
///
/// # Validation
///
/// Call `validate()` after construction to verify all node indices are in bounds
/// and parent-child relationships are bidirectionally consistent.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "DocumentStructure")]
pub struct JsDocumentStructure {
    /// All nodes in document/reading order.
    pub nodes: Option<Vec<JsDocumentNode>>,
    /// Origin format identifier (e.g. "docx", "pptx", "html", "pdf").
    ///
    /// Allows renderers to apply format-aware heuristics when converting
    /// the document tree to output formats.
    #[napi(js_name = "sourceFormat")]
    #[serde(rename = "sourceFormat")]
    pub source_format: Option<String>,
    /// Resolved relationships between nodes (footnote refs, citations, anchor links, etc.).
    ///
    /// Populated during derivation from the internal document representation.
    /// Empty when no relationships are detected.
    pub relationships: Option<Vec<JsDocumentRelationship>>,
    /// Sorted, deduplicated list of node type names present in this document.
    ///
    /// Each value is the snake_case `node_type` tag of the corresponding
    /// [`NodeContent`] variant (e.g. `"paragraph"`, `"heading"`, `"table"`, …).
    ///
    /// Computed from [`nodes`] via [`DocumentStructure::finalize_node_types`].
    /// Empty until that method is called (internal construction paths call it
    /// at the end of derivation).
    #[napi(js_name = "nodeTypes")]
    #[serde(rename = "nodeTypes")]
    pub node_types: Option<Vec<String>>,
}

#[napi(js_name = "documentStructureDefault")]
pub fn document_structure_default() -> JsDocumentStructure {
    kreuzberg::DocumentStructure::default().into()
}

/// A resolved relationship between two nodes in the document tree.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "DocumentRelationship")]
pub struct JsDocumentRelationship {
    /// Source node index (the referencing node).
    pub source: u32,
    /// Target node index (the referenced node).
    pub target: u32,
    /// Semantic kind of the relationship.
    pub kind: JsRelationshipKind,
}

/// A single node in the document tree.
///
/// Each node has deterministic `id`, typed `content`, optional `parent`/`children`
/// for tree structure, and metadata like page number, bounding box, and content layer.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "DocumentNode")]
pub struct JsDocumentNode {
    /// Deterministic identifier (hash of content + position).
    pub id: String,
    /// Node content — tagged enum, type-specific data only.
    pub content: JsNodeContent,
    /// Parent node index (`None` = root-level node).
    pub parent: Option<u32>,
    /// Child node indices in reading order.
    pub children: Vec<u32>,
    /// Content layer classification.
    #[napi(js_name = "contentLayer")]
    #[serde(rename = "contentLayer")]
    pub content_layer: JsContentLayer,
    /// Page number where this node starts (1-indexed).
    pub page: Option<u32>,
    /// Page number where this node ends (for multi-page tables/sections).
    #[napi(js_name = "pageEnd")]
    #[serde(rename = "pageEnd")]
    pub page_end: Option<u32>,
    /// Bounding box in document coordinates.
    pub bbox: Option<JsBoundingBox>,
    /// Inline annotations (formatting, links) on this node's text content.
    ///
    /// Only meaningful for text-carrying nodes; empty for containers.
    pub annotations: Vec<JsTextAnnotation>,
    /// Format-specific key-value attributes.
    ///
    /// Extensible bag for miscellaneous data without a dedicated typed field: CSS classes,
    /// LaTeX environment names, Excel cell formulas, slide layout names, etc.
    pub attributes: Option<HashMap<String, String>>,
}

/// Structured table grid with cell-level metadata.
///
/// Stores row/column dimensions and a flat list of cells with position info.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "TableGrid")]
pub struct JsTableGrid {
    /// Number of rows in the table.
    pub rows: Option<u32>,
    /// Number of columns in the table.
    pub cols: Option<u32>,
    /// All cells in row-major order.
    pub cells: Option<Vec<JsGridCell>>,
}

/// Individual grid cell with position and span metadata.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "GridCell")]
pub struct JsGridCell {
    /// Cell text content.
    pub content: String,
    /// Zero-indexed row position.
    pub row: u32,
    /// Zero-indexed column position.
    pub col: u32,
    /// Number of rows this cell spans.
    #[napi(js_name = "rowSpan")]
    #[serde(rename = "rowSpan")]
    pub row_span: u32,
    /// Number of columns this cell spans.
    #[napi(js_name = "colSpan")]
    #[serde(rename = "colSpan")]
    pub col_span: u32,
    /// Whether this is a header cell.
    #[napi(js_name = "isHeader")]
    #[serde(rename = "isHeader")]
    pub is_header: bool,
    /// Bounding box for this cell (if available).
    pub bbox: Option<JsBoundingBox>,
}

/// Inline text annotation — byte-range based formatting and links.
///
/// Annotations reference byte offsets into the node's text content,
/// enabling precise identification of formatted regions.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "TextAnnotation")]
pub struct JsTextAnnotation {
    /// Start byte offset in the node's text content (inclusive).
    pub start: u32,
    /// End byte offset in the node's text content (exclusive).
    pub end: u32,
    /// Annotation type.
    pub kind: JsAnnotationKind,
}

/// General extraction result used by the core extraction API.
///
/// This is the main result type returned by all extraction functions.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ExtractionResult")]
pub struct JsExtractionResult {
    pub content: Option<String>,
    #[napi(js_name = "mimeType")]
    #[serde(rename = "mimeType")]
    pub mime_type: Option<String>,
    pub metadata: Option<JsMetadata>,
    /// Extraction strategy used to produce the returned text.
    ///
    /// Populated when the extractor can reliably distinguish native text extraction,
    /// OCR-only extraction, or mixed native/OCR output.
    #[napi(js_name = "extractionMethod")]
    #[serde(rename = "extractionMethod")]
    pub extraction_method: Option<JsExtractionMethod>,
    pub tables: Option<Vec<JsTable>>,
    #[napi(js_name = "detectedLanguages")]
    #[serde(rename = "detectedLanguages")]
    pub detected_languages: Option<Vec<String>>,
    /// Text chunks when chunking is enabled.
    ///
    /// When chunking configuration is provided, the content is split into
    /// overlapping chunks for efficient processing. Each chunk contains the text,
    /// optional embeddings (if enabled), and metadata about its position.
    pub chunks: Option<Vec<JsChunk>>,
    /// Extracted images from the document.
    ///
    /// When image extraction is enabled via `ImageExtractionConfig`, this field
    /// contains all images found in the document with their raw data and metadata.
    /// Each image may optionally contain a nested `ocr_result` if OCR was performed.
    pub images: Option<Vec<JsExtractedImage>>,
    /// Per-page content when page extraction is enabled.
    ///
    /// When page extraction is configured, the document is split into per-page content
    /// with tables and images mapped to their respective pages.
    pub pages: Option<Vec<JsPageContent>>,
    /// Semantic elements when element-based result format is enabled.
    ///
    /// When result_format is set to ElementBased, this field contains semantic
    /// elements with type classification, unique identifiers, and metadata for
    /// Unstructured-compatible element-based processing.
    pub elements: Option<Vec<JsElement>>,
    /// Rich Djot content structure (when extracting Djot documents).
    ///
    /// When extracting Djot documents with structured extraction enabled,
    /// this field contains the full semantic structure including:
    /// - Block-level elements with nesting
    /// - Inline formatting with attributes
    /// - Links, images, footnotes
    /// - Math expressions
    /// - Complete attribute information
    ///
    /// The `content` field still contains plain text for backward compatibility.
    ///
    /// Always `None` for non-Djot documents.
    #[napi(js_name = "djotContent")]
    #[serde(rename = "djotContent")]
    pub djot_content: Option<JsDjotContent>,
    /// OCR elements with full spatial and confidence metadata.
    ///
    /// When OCR is performed with element extraction enabled, this field contains
    /// the structured representation of detected text including:
    /// - Bounding geometry (rectangles or quadrilaterals)
    /// - Confidence scores (detection and recognition)
    /// - Rotation information
    /// - Hierarchical relationships (Tesseract only)
    ///
    /// This field preserves all metadata that would otherwise be lost when
    /// converting to plain text or markdown output formats.
    ///
    /// Only populated when `OcrElementConfig.include_elements` is true.
    #[napi(js_name = "ocrElements")]
    #[serde(rename = "ocrElements")]
    pub ocr_elements: Option<Vec<JsOcrElement>>,
    /// Structured document tree (when document structure extraction is enabled).
    ///
    /// When `include_document_structure` is true in `ExtractionConfig`, this field
    /// contains the full hierarchical representation of the document including:
    /// - Heading-driven section nesting
    /// - Table grids with cell-level metadata
    /// - Content layer classification (body, header, footer, footnote)
    /// - Inline text annotations (formatting, links)
    /// - Bounding boxes and page numbers
    ///
    /// Independent of `result_format` — can be combined with Unified or ElementBased.
    pub document: Option<JsDocumentStructure>,
    /// Extracted keywords when keyword extraction is enabled.
    ///
    /// When keyword extraction (RAKE or YAKE) is configured, this field contains
    /// the extracted keywords with scores, algorithm info, and position data.
    /// Previously stored in `metadata.additional["keywords"]`.
    #[napi(js_name = "extractedKeywords")]
    #[serde(rename = "extractedKeywords")]
    #[serde(skip)]
    pub extracted_keywords: Option<Vec<JsKeyword>>,
    /// Document quality score from quality analysis.
    ///
    /// A value between 0.0 and 1.0 indicating the overall text quality.
    /// Previously stored in `metadata.additional["quality_score"]`.
    #[napi(js_name = "qualityScore")]
    #[serde(rename = "qualityScore")]
    pub quality_score: Option<f64>,
    /// Non-fatal warnings collected during processing pipeline stages.
    ///
    /// Captures errors from optional pipeline features (embedding, chunking,
    /// language detection, output formatting) that don't prevent extraction
    /// but may indicate degraded results.
    /// Previously stored as individual keys in `metadata.additional`.
    #[napi(js_name = "processingWarnings")]
    #[serde(rename = "processingWarnings")]
    pub processing_warnings: Option<Vec<JsProcessingWarning>>,
    /// PDF annotations extracted from the document.
    ///
    /// When annotation extraction is enabled via `PdfConfig::extract_annotations`,
    /// this field contains text notes, highlights, links, stamps, and other
    /// annotations found in PDF documents.
    pub annotations: Option<Vec<JsPdfAnnotation>>,
    /// Nested extraction results from archive contents.
    ///
    /// When extracting archives, each processable file inside produces its own
    /// full extraction result. Set to `None` for non-archive formats.
    /// Use `max_archive_depth` in config to control recursion depth.
    pub children: Option<Vec<JsArchiveEntry>>,
    /// URIs/links discovered during document extraction.
    ///
    /// Contains hyperlinks, image references, citations, email addresses, and
    /// other URI-like references found in the document. Always extracted when
    /// present in the source document.
    pub uris: Option<Vec<JsExtractedUri>>,
    /// Tracked changes embedded in the source document.
    ///
    /// Populated by per-format extractors that understand change-tracking
    /// metadata (DOCX `w:ins`/`w:del`/`w:rPrChange`, ODT `text:change-*`,
    /// …). Every extractor defaults to `None` until its format-specific
    /// implementation is added. Extractors that do populate this field follow
    /// the "accepted-changes" convention: inserted text is present in
    /// `content`, deleted text is absent — the revision list is the separate
    /// audit trail.
    pub revisions: Option<Vec<JsDocumentRevision>>,
    /// Structured extraction output from LLM-based JSON schema extraction.
    ///
    /// When `structured_extraction` is configured in `ExtractionConfig`, the
    /// extracted document content is sent to a VLM with the provided JSON schema.
    /// The response is parsed and stored here as a JSON value matching the schema.
    #[napi(js_name = "structuredOutput")]
    #[serde(rename = "structuredOutput")]
    pub structured_output: Option<serde_json::Value>,
    /// Code intelligence results from tree-sitter analysis.
    ///
    /// Populated when extracting source code files with the `tree-sitter` feature.
    /// Contains metrics, structural analysis, imports/exports, comments,
    /// docstrings, symbols, diagnostics, and optionally chunked code segments.
    ///
    /// Stored as an opaque JSON value so that all language bindings (Go, Java,
    /// C#, …) can deserialize it as a raw JSON object rather than a typed struct.
    /// The underlying type is `tree_sitter_language_pack::ProcessResult`.
    #[napi(js_name = "codeIntelligence")]
    #[serde(rename = "codeIntelligence")]
    #[serde(skip)]
    pub code_intelligence: Option<serde_json::Value>,
    /// LLM token usage and cost data for all LLM calls made during this extraction.
    ///
    /// Contains one entry per LLM call. Multiple entries are produced when
    /// VLM OCR, structured extraction, or LLM embeddings run during
    /// the same extraction.
    ///
    /// `None` when no LLM was used.
    #[napi(js_name = "llmUsage")]
    #[serde(rename = "llmUsage")]
    pub llm_usage: Option<Vec<JsLlmUsage>>,
    /// Pre-rendered content in the requested output format.
    ///
    /// Populated during `derive_extraction_result` before tree derivation consumes
    /// element data. `apply_output_format` swaps this into `content` at the end
    /// of the pipeline, after post-processors have operated on plain text.
    #[napi(js_name = "formattedContent")]
    #[serde(rename = "formattedContent")]
    pub formatted_content: Option<String>,
    /// Structured hOCR document for the OCR+layout pipeline.
    ///
    /// When tesseract produces hOCR output, the parsed `InternalDocument` carries
    /// paragraph structure with bounding boxes and confidence scores. The layout
    /// classification step enriches these elements before final rendering.
    #[napi(js_name = "ocrInternalDocument")]
    #[serde(rename = "ocrInternalDocument")]
    pub ocr_internal_document: Option<String>,
}

/// Convert from an OCR result.
#[napi(js_name = "extractionResultFromOcr")]
pub fn extraction_result_from_ocr(ocr: JsOcrExtractionResult) -> JsExtractionResult {
    kreuzberg::ExtractionResult::from_ocr(ocr.into()).into()
}

/// A single file extracted from an archive.
///
/// When archives (ZIP, TAR, 7Z, GZIP) are extracted with recursive extraction
/// enabled, each processable file produces its own full `ExtractionResult`.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ArchiveEntry")]
pub struct JsArchiveEntry {
    /// Archive-relative file path (e.g. "folder/document.pdf").
    pub path: String,
    /// Detected MIME type of the file.
    #[napi(js_name = "mimeType")]
    #[serde(rename = "mimeType")]
    pub mime_type: String,
    /// Full extraction result for this file.
    pub result: JsExtractionResult,
}

/// A non-fatal warning from a processing pipeline stage.
///
/// Captures errors from optional features that don't prevent extraction
/// but may indicate degraded results.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ProcessingWarning")]
pub struct JsProcessingWarning {
    /// The pipeline stage or feature that produced this warning
    /// (e.g., "embedding", "chunking", "language_detection", "output_format").
    pub source: String,
    /// Human-readable description of what went wrong.
    pub message: String,
}

/// Token usage and cost data for a single LLM call made during extraction.
///
/// Populated when VLM OCR, structured extraction, or LLM-based embeddings
/// are used. Multiple entries may be present when multiple LLM calls occur
/// within one extraction (e.g. VLM OCR + structured extraction).
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "LlmUsage")]
pub struct JsLlmUsage {
    /// The LLM model identifier (e.g. "openai/gpt-4o", "anthropic/claude-sonnet-4-20250514").
    pub model: Option<String>,
    /// The pipeline stage that triggered this LLM call
    /// (e.g. "vlm_ocr", "structured_extraction", "embeddings").
    pub source: Option<String>,
    /// Number of input/prompt tokens consumed.
    #[napi(js_name = "inputTokens")]
    #[serde(rename = "inputTokens")]
    pub input_tokens: Option<i64>,
    /// Number of output/completion tokens generated.
    #[napi(js_name = "outputTokens")]
    #[serde(rename = "outputTokens")]
    pub output_tokens: Option<i64>,
    /// Total tokens (input + output).
    #[napi(js_name = "totalTokens")]
    #[serde(rename = "totalTokens")]
    pub total_tokens: Option<i64>,
    /// Estimated cost in USD based on the provider's published pricing.
    #[napi(js_name = "estimatedCost")]
    #[serde(rename = "estimatedCost")]
    pub estimated_cost: Option<f64>,
    /// Why the model stopped generating (e.g. "stop", "length", "content_filter").
    #[napi(js_name = "finishReason")]
    #[serde(rename = "finishReason")]
    pub finish_reason: Option<String>,
}

/// A text chunk with optional embedding and metadata.
///
/// Chunks are created when chunking is enabled in `ExtractionConfig`. Each chunk
/// contains the text content, optional embedding vector (if embedding generation
/// is configured), and metadata about its position in the document.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "Chunk")]
pub struct JsChunk {
    /// The text content of this chunk.
    pub content: String,
    /// Semantic structural classification of this chunk.
    ///
    /// Assigned by the heuristic classifier based on content patterns and
    /// heading context. Defaults to `ChunkType::Unknown` when no rule matches.
    #[napi(js_name = "chunkType")]
    #[serde(rename = "chunkType")]
    pub chunk_type: JsChunkType,
    /// Optional embedding vector for this chunk.
    ///
    /// Only populated when `EmbeddingConfig` is provided in chunking configuration.
    /// The dimensionality depends on the chosen embedding model.
    pub embedding: Option<Vec<f64>>,
    /// Metadata about this chunk's position and properties.
    pub metadata: JsChunkMetadata,
}

/// Heading context for a chunk within a Markdown document.
///
/// Contains the heading hierarchy from document root to this chunk's section.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "HeadingContext")]
pub struct JsHeadingContext {
    /// The heading hierarchy from document root to this chunk's section.
    /// Index 0 is the outermost (h1), last element is the most specific.
    pub headings: Vec<JsHeadingLevel>,
}

/// A single heading in the hierarchy.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "HeadingLevel")]
pub struct JsHeadingLevel {
    /// Heading depth (1 = h1, 2 = h2, etc.)
    pub level: u8,
    /// The text content of the heading.
    pub text: String,
}

/// Metadata about a chunk's position in the original document.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ChunkMetadata")]
pub struct JsChunkMetadata {
    /// Byte offset where this chunk starts in the original text (UTF-8 valid boundary).
    #[napi(js_name = "byteStart")]
    #[serde(rename = "byteStart")]
    pub byte_start: i64,
    /// Byte offset where this chunk ends in the original text (UTF-8 valid boundary).
    #[napi(js_name = "byteEnd")]
    #[serde(rename = "byteEnd")]
    pub byte_end: i64,
    /// Number of tokens in this chunk (if available).
    ///
    /// This is calculated by the embedding model's tokenizer if embeddings are enabled.
    #[napi(js_name = "tokenCount")]
    #[serde(rename = "tokenCount")]
    pub token_count: Option<i64>,
    /// Zero-based index of this chunk in the document.
    #[napi(js_name = "chunkIndex")]
    #[serde(rename = "chunkIndex")]
    pub chunk_index: i64,
    /// Total number of chunks in the document.
    #[napi(js_name = "totalChunks")]
    #[serde(rename = "totalChunks")]
    pub total_chunks: i64,
    /// First page number this chunk spans (1-indexed).
    ///
    /// Only populated when page tracking is enabled in extraction configuration.
    #[napi(js_name = "firstPage")]
    #[serde(rename = "firstPage")]
    pub first_page: Option<u32>,
    /// Last page number this chunk spans (1-indexed, equal to first_page for single-page chunks).
    ///
    /// Only populated when page tracking is enabled in extraction configuration.
    #[napi(js_name = "lastPage")]
    #[serde(rename = "lastPage")]
    pub last_page: Option<u32>,
    /// Heading context when using Markdown chunker.
    ///
    /// Contains the heading hierarchy this chunk falls under.
    /// Only populated when `ChunkerType::Markdown` is used.
    #[napi(js_name = "headingContext")]
    #[serde(rename = "headingContext")]
    pub heading_context: Option<JsHeadingContext>,
    /// Indices into `ExtractionResult.images` for images on pages covered by this chunk.
    ///
    /// Contains zero-based indices into the top-level `images` collection for every
    /// image whose `page_number` falls within `[first_page, last_page]`.
    /// Empty when image extraction is disabled or the chunk spans no pages with images.
    #[napi(js_name = "imageIndices")]
    #[serde(rename = "imageIndices")]
    pub image_indices: Vec<u32>,
}

/// Extracted image from a document.
///
/// Contains raw image data, metadata, and optional nested OCR results.
/// Raw bytes allow cross-language compatibility - users can convert to
/// PIL.Image (Python), Sharp (Node.js), or other formats as needed.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ExtractedImage")]
pub struct JsExtractedImage {
    /// Raw image data (PNG, JPEG, WebP, etc. bytes).
    /// Uses `bytes::Bytes` for cheap cloning of large buffers.
    #[napi(ts_type = "Uint8Array | Buffer | Array<number>")]
    pub data: JsBytes,
    /// Image format (e.g., "jpeg", "png", "webp")
    /// Uses Cow<'static, str> to avoid allocation for static literals.
    pub format: String,
    /// Zero-indexed position of this image in the document/page
    #[napi(js_name = "imageIndex")]
    #[serde(rename = "imageIndex")]
    pub image_index: u32,
    /// Page/slide number where image was found (1-indexed)
    #[napi(js_name = "pageNumber")]
    #[serde(rename = "pageNumber")]
    pub page_number: Option<u32>,
    /// Image width in pixels
    pub width: Option<u32>,
    /// Image height in pixels
    pub height: Option<u32>,
    /// Colorspace information (e.g., "RGB", "CMYK", "Gray")
    pub colorspace: Option<String>,
    /// Bits per color component (e.g., 8, 16)
    #[napi(js_name = "bitsPerComponent")]
    #[serde(rename = "bitsPerComponent")]
    pub bits_per_component: Option<u32>,
    /// Whether this image is a mask image
    #[napi(js_name = "isMask")]
    #[serde(rename = "isMask")]
    pub is_mask: bool,
    /// Optional description of the image
    pub description: Option<String>,
    /// Nested OCR extraction result (if image was OCRed)
    ///
    /// When OCR is performed on this image, the result is embedded here
    /// rather than in a separate collection, making the relationship explicit.
    #[napi(js_name = "ocrResult")]
    #[serde(rename = "ocrResult")]
    pub ocr_result: Option<JsExtractionResult>,
    /// Bounding box of the image on the page (PDF coordinates: x0=left, y0=bottom, x1=right, y1=top).
    /// Only populated for PDF-extracted images when position data is available from the PDF extractor.
    #[napi(js_name = "boundingBox")]
    #[serde(rename = "boundingBox")]
    pub bounding_box: Option<JsBoundingBox>,
    /// Original source path of the image within the document archive (e.g., "media/image1.png" in DOCX).
    /// Used for rendering image references when the binary data is not extracted.
    #[napi(js_name = "sourcePath")]
    #[serde(rename = "sourcePath")]
    pub source_path: Option<String>,
    /// Heuristic classification of what this image likely depicts.
    /// `None` if classification was disabled or inconclusive.
    #[napi(js_name = "imageKind")]
    #[serde(rename = "imageKind")]
    pub image_kind: Option<JsImageKind>,
    /// Confidence score for `image_kind`, in the range 0.0 to 1.0.
    #[napi(js_name = "kindConfidence")]
    #[serde(rename = "kindConfidence")]
    pub kind_confidence: Option<f64>,
    /// Identifier shared across images that form a single logical figure
    /// (e.g. all raster tiles of one technical drawing). `None` for singletons.
    #[napi(js_name = "clusterId")]
    #[serde(rename = "clusterId")]
    pub cluster_id: Option<u32>,
}

/// Bounding box coordinates for element positioning.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "BoundingBox")]
pub struct JsBoundingBox {
    /// Left x-coordinate
    pub x0: Option<f64>,
    /// Bottom y-coordinate
    pub y0: Option<f64>,
    /// Right x-coordinate
    pub x1: Option<f64>,
    /// Top y-coordinate
    pub y1: Option<f64>,
}

/// Metadata for a semantic element.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ElementMetadata")]
pub struct JsElementMetadata {
    /// Page number (1-indexed)
    #[napi(js_name = "pageNumber")]
    #[serde(rename = "pageNumber")]
    pub page_number: Option<u32>,
    /// Source filename or document name
    pub filename: Option<String>,
    /// Bounding box coordinates if available
    pub coordinates: Option<JsBoundingBox>,
    /// Position index in the element sequence
    #[napi(js_name = "elementIndex")]
    #[serde(rename = "elementIndex")]
    pub element_index: Option<i64>,
    /// Additional custom metadata
    pub additional: HashMap<String, String>,
}

/// Semantic element extracted from document.
///
/// Represents a logical unit of content with semantic classification,
/// unique identifier, and metadata for tracking origin and position.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "Element")]
pub struct JsElement {
    /// Unique element identifier
    #[napi(js_name = "elementId")]
    #[serde(rename = "elementId")]
    pub element_id: String,
    /// Semantic type of this element
    #[napi(js_name = "elementType")]
    #[serde(rename = "elementType")]
    pub element_type: JsElementType,
    /// Text content of the element
    pub text: String,
    /// Metadata about the element
    pub metadata: JsElementMetadata,
}

/// Excel workbook representation.
///
/// Contains all sheets from an Excel file (.xlsx, .xls, etc.) with
/// extracted content and metadata.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ExcelWorkbook")]
pub struct JsExcelWorkbook {
    /// All sheets in the workbook
    pub sheets: Vec<JsExcelSheet>,
    /// Workbook-level metadata (author, creation date, etc.)
    pub metadata: HashMap<String, String>,
    /// Collaborative-edit revision headers from `xl/revisions/revisionHeaders.xml`.
    ///
    /// Populated for legacy shared-workbook `.xlsx` files that contain the
    /// `xl/revisions/` directory. Each `<header>` element maps to one
    /// `DocumentRevision { kind: FormatChange }` carrying the header's `guid`
    /// (→ `revision_id`), `userName` (→ `author`), and `dateTime` (→ `timestamp`).
    /// `anchor` and `delta` are `None`/empty for v1 (per-cell log parsing is a
    /// follow-up). `None` when `xl/revisions/revisionHeaders.xml` is absent.
    pub revisions: Option<Vec<JsDocumentRevision>>,
}

/// Single Excel worksheet.
///
/// Represents one sheet from an Excel workbook with its content
/// converted to Markdown format and dimensional statistics.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ExcelSheet")]
pub struct JsExcelSheet {
    /// Sheet name as it appears in Excel
    pub name: String,
    /// Sheet content converted to Markdown tables
    pub markdown: String,
    /// Number of rows
    #[napi(js_name = "rowCount")]
    #[serde(rename = "rowCount")]
    pub row_count: i64,
    /// Number of columns
    #[napi(js_name = "colCount")]
    #[serde(rename = "colCount")]
    pub col_count: i64,
    /// Total number of non-empty cells
    #[napi(js_name = "cellCount")]
    #[serde(rename = "cellCount")]
    pub cell_count: i64,
    /// Pre-extracted table cells (2D vector of cell values)
    /// Populated during markdown generation to avoid re-parsing markdown.
    /// None for empty sheets.
    #[napi(js_name = "tableCells")]
    #[serde(rename = "tableCells")]
    pub table_cells: Option<Vec<Vec<String>>>,
}

/// XML extraction result.
///
/// Contains extracted text content from XML files along with
/// structural statistics about the XML document.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "XmlExtractionResult")]
pub struct JsXmlExtractionResult {
    /// Extracted text content (XML structure filtered out)
    pub content: String,
    /// Total number of XML elements processed
    #[napi(js_name = "elementCount")]
    #[serde(rename = "elementCount")]
    pub element_count: i64,
    /// List of unique element names found (sorted)
    #[napi(js_name = "uniqueElements")]
    #[serde(rename = "uniqueElements")]
    pub unique_elements: Vec<String>,
}

/// Plain text and Markdown extraction result.
///
/// Contains the extracted text along with statistics and,
/// for Markdown files, structural elements like headers and links.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "TextExtractionResult")]
pub struct JsTextExtractionResult {
    /// Extracted text content
    pub content: String,
    /// Number of lines
    #[napi(js_name = "lineCount")]
    #[serde(rename = "lineCount")]
    pub line_count: i64,
    /// Number of words
    #[napi(js_name = "wordCount")]
    #[serde(rename = "wordCount")]
    pub word_count: i64,
    /// Number of characters
    #[napi(js_name = "characterCount")]
    #[serde(rename = "characterCount")]
    pub character_count: i64,
    /// Markdown headers (text only, Markdown files only)
    pub headers: Option<Vec<String>>,
    /// Markdown links as (text, URL) tuples (Markdown files only)
    pub links: Option<Vec<Vec<String>>>,
    /// Code blocks as (language, code) tuples (Markdown files only)
    #[napi(js_name = "codeBlocks")]
    #[serde(rename = "codeBlocks")]
    pub code_blocks: Option<Vec<Vec<String>>>,
}

/// PowerPoint (PPTX) extraction result.
///
/// Contains extracted slide content, metadata, and embedded images/tables.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "PptxExtractionResult")]
pub struct JsPptxExtractionResult {
    /// Extracted text content from all slides
    pub content: String,
    /// Presentation metadata
    pub metadata: JsPptxMetadata,
    /// Total number of slides
    #[napi(js_name = "slideCount")]
    #[serde(rename = "slideCount")]
    pub slide_count: i64,
    /// Total number of embedded images
    #[napi(js_name = "imageCount")]
    #[serde(rename = "imageCount")]
    pub image_count: i64,
    /// Total number of tables
    #[napi(js_name = "tableCount")]
    #[serde(rename = "tableCount")]
    pub table_count: i64,
    /// Extracted images from the presentation
    pub images: Vec<JsExtractedImage>,
    /// Slide structure with boundaries (when page tracking is enabled)
    #[napi(js_name = "pageStructure")]
    #[serde(rename = "pageStructure")]
    pub page_structure: Option<JsPageStructure>,
    /// Per-slide content (when page tracking is enabled)
    #[napi(js_name = "pageContents")]
    #[serde(rename = "pageContents")]
    pub page_contents: Option<Vec<JsPageContent>>,
    /// Structured document representation
    pub document: Option<JsDocumentStructure>,
    /// Hyperlinks discovered in slides as (url, optional_label) pairs.
    pub hyperlinks: Vec<String>,
    /// Office metadata extracted from docProps/core.xml and docProps/app.xml.
    ///
    /// Contains keys like "title", "author", "created_by", "subject", "keywords",
    /// "modified_by", "created_at", "modified_at", etc.
    #[napi(js_name = "officeMetadata")]
    #[serde(rename = "officeMetadata")]
    pub office_metadata: HashMap<String, String>,
    /// Slide comments as revisions.
    ///
    /// Each `<p:cm>` element in `ppt/comments/comment{N}.xml` becomes a
    /// `DocumentRevision { kind: Comment }` with author (resolved from
    /// `ppt/commentAuthors.xml`), ISO-8601 timestamp, and
    /// `RevisionAnchor::Slide { index }`. `None` when no comment XML parts exist.
    pub revisions: Option<Vec<JsDocumentRevision>>,
}

/// Email extraction result.
///
/// Complete representation of an extracted email message (.eml or .msg)
/// including headers, body content, and attachments.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "EmailExtractionResult")]
pub struct JsEmailExtractionResult {
    /// Email subject line
    pub subject: Option<String>,
    /// Sender email address
    #[napi(js_name = "fromEmail")]
    #[serde(rename = "fromEmail")]
    pub from_email: Option<String>,
    /// Primary recipient email addresses
    #[napi(js_name = "toEmails")]
    #[serde(rename = "toEmails")]
    pub to_emails: Vec<String>,
    /// CC recipient email addresses
    #[napi(js_name = "ccEmails")]
    #[serde(rename = "ccEmails")]
    pub cc_emails: Vec<String>,
    /// BCC recipient email addresses
    #[napi(js_name = "bccEmails")]
    #[serde(rename = "bccEmails")]
    pub bcc_emails: Vec<String>,
    /// Email date/timestamp
    pub date: Option<String>,
    /// Message-ID header value
    #[napi(js_name = "messageId")]
    #[serde(rename = "messageId")]
    pub message_id: Option<String>,
    /// Plain text version of the email body
    #[napi(js_name = "plainText")]
    #[serde(rename = "plainText")]
    pub plain_text: Option<String>,
    /// HTML version of the email body
    #[napi(js_name = "htmlContent")]
    #[serde(rename = "htmlContent")]
    pub html_content: Option<String>,
    /// Cleaned/processed text content. Aliased as `cleaned_text` for back-compat.
    pub content: String,
    /// List of email attachments
    pub attachments: Vec<JsEmailAttachment>,
    /// Additional email headers and metadata
    pub metadata: HashMap<String, String>,
}

/// Email attachment representation.
///
/// Contains metadata and optionally the content of an email attachment.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "EmailAttachment")]
pub struct JsEmailAttachment {
    /// Attachment name (from Content-Disposition header)
    pub name: Option<String>,
    /// Filename of the attachment
    pub filename: Option<String>,
    /// MIME type of the attachment
    #[napi(js_name = "mimeType")]
    #[serde(rename = "mimeType")]
    pub mime_type: Option<String>,
    /// Size in bytes
    pub size: Option<i64>,
    /// Whether this attachment is an image
    #[napi(js_name = "isImage")]
    #[serde(rename = "isImage")]
    pub is_image: bool,
    /// Attachment data (if extracted).
    /// Uses `bytes::Bytes` for cheap cloning of large buffers.
    #[napi(ts_type = "Uint8Array | Buffer | Array<number>")]
    pub data: Option<JsBytes>,
}

/// OCR extraction result.
///
/// Result of performing OCR on an image or scanned document,
/// including recognized text and detected tables.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "OcrExtractionResult")]
pub struct JsOcrExtractionResult {
    /// Recognized text content
    pub content: String,
    /// Original MIME type of the processed image
    #[napi(js_name = "mimeType")]
    #[serde(rename = "mimeType")]
    pub mime_type: String,
    /// OCR processing metadata (confidence scores, language, etc.)
    pub metadata: HashMap<String, serde_json::Value>,
    /// Tables detected and extracted via OCR
    pub tables: Vec<JsOcrTable>,
    /// Structured OCR elements with bounding boxes and confidence scores.
    /// Available when TSV output is requested or table detection is enabled.
    #[napi(js_name = "ocrElements")]
    #[serde(rename = "ocrElements")]
    pub ocr_elements: Option<Vec<JsOcrElement>>,
    /// Structured document produced from hOCR parsing.
    /// Carries paragraph structure, bounding boxes, and confidence scores
    /// that the flattened `content` string discards.
    #[napi(js_name = "internalDocument")]
    #[serde(rename = "internalDocument")]
    pub internal_document: Option<String>,
}

/// Table detected via OCR.
///
/// Represents a table structure recognized during OCR processing.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "OcrTable")]
pub struct JsOcrTable {
    /// Table cells as a 2D vector (rows × columns)
    pub cells: Vec<Vec<String>>,
    /// Markdown representation of the table
    pub markdown: String,
    /// Page number where the table was found (1-indexed)
    #[napi(js_name = "pageNumber")]
    #[serde(rename = "pageNumber")]
    pub page_number: u32,
    /// Bounding box of the table in pixel coordinates (from OCR word positions).
    #[napi(js_name = "boundingBox")]
    #[serde(rename = "boundingBox")]
    pub bounding_box: Option<JsOcrTableBoundingBox>,
}

/// Bounding box for an OCR-detected table in pixel coordinates.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "OcrTableBoundingBox")]
pub struct JsOcrTableBoundingBox {
    /// Left x-coordinate (pixels)
    pub left: u32,
    /// Top y-coordinate (pixels)
    pub top: u32,
    /// Right x-coordinate (pixels)
    pub right: u32,
    /// Bottom y-coordinate (pixels)
    pub bottom: u32,
}

/// Image preprocessing configuration for OCR.
///
/// These settings control how images are preprocessed before OCR to improve
/// text recognition quality. Different preprocessing strategies work better
/// for different document types.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ImagePreprocessingConfig")]
pub struct JsImagePreprocessingConfig {
    /// Target DPI for the image (300 is standard, 600 for small text).
    #[napi(js_name = "targetDpi")]
    #[serde(rename = "targetDpi")]
    pub target_dpi: Option<i32>,
    /// Auto-detect and correct image rotation.
    #[napi(js_name = "autoRotate")]
    #[serde(rename = "autoRotate")]
    pub auto_rotate: Option<bool>,
    /// Correct skew (tilted images).
    pub deskew: Option<bool>,
    /// Remove noise from the image.
    pub denoise: Option<bool>,
    /// Enhance contrast for better text visibility.
    #[napi(js_name = "contrastEnhance")]
    #[serde(rename = "contrastEnhance")]
    pub contrast_enhance: Option<bool>,
    /// Binarization method: "otsu", "sauvola", "adaptive".
    #[napi(js_name = "binarizationMethod")]
    #[serde(rename = "binarizationMethod")]
    pub binarization_method: Option<String>,
    /// Invert colors (white text on black → black on white).
    #[napi(js_name = "invertColors")]
    #[serde(rename = "invertColors")]
    pub invert_colors: Option<bool>,
}

#[napi(js_name = "imagePreprocessingConfigDefault")]
pub fn image_preprocessing_config_default() -> JsImagePreprocessingConfig {
    kreuzberg::ImagePreprocessingConfig::default().into()
}

/// Tesseract OCR configuration.
///
/// Provides fine-grained control over Tesseract OCR engine parameters.
/// Most users can use the defaults, but these settings allow optimization
/// for specific document types (invoices, handwriting, etc.).
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "TesseractConfig")]
pub struct JsTesseractConfig {
    /// Language code (e.g., "eng", "deu", "fra")
    pub language: Option<String>,
    /// Page Segmentation Mode (0-13).
    ///
    /// Common values:
    /// - 3: Fully automatic page segmentation (native default)
    /// - 6: Assume a single uniform block of text (WASM default — avoids layout-analysis hang)
    /// - 11: Sparse text with no particular order
    pub psm: Option<i32>,
    /// Output format ("text" or "markdown")
    #[napi(js_name = "outputFormat")]
    #[serde(rename = "outputFormat")]
    pub output_format: Option<String>,
    /// OCR Engine Mode (0-3).
    ///
    /// - 0: Legacy engine only
    /// - 1: Neural nets (LSTM) only (usually best)
    /// - 2: Legacy + LSTM
    /// - 3: Default (based on what's available)
    pub oem: Option<i32>,
    /// Minimum confidence threshold (0.0-100.0).
    ///
    /// Words with confidence below this threshold may be rejected or flagged.
    #[napi(js_name = "minConfidence")]
    #[serde(rename = "minConfidence")]
    pub min_confidence: Option<f64>,
    /// Image preprocessing configuration.
    ///
    /// Controls how images are preprocessed before OCR. Can significantly
    /// improve quality for scanned documents or low-quality images.
    pub preprocessing: Option<JsImagePreprocessingConfig>,
    /// Enable automatic table detection and reconstruction
    #[napi(js_name = "enableTableDetection")]
    #[serde(rename = "enableTableDetection")]
    pub enable_table_detection: Option<bool>,
    /// Minimum confidence threshold for table detection (0.0-1.0)
    #[napi(js_name = "tableMinConfidence")]
    #[serde(rename = "tableMinConfidence")]
    pub table_min_confidence: Option<f64>,
    /// Column threshold for table detection (pixels)
    #[napi(js_name = "tableColumnThreshold")]
    #[serde(rename = "tableColumnThreshold")]
    pub table_column_threshold: Option<i32>,
    /// Row threshold ratio for table detection (0.0-1.0)
    #[napi(js_name = "tableRowThresholdRatio")]
    #[serde(rename = "tableRowThresholdRatio")]
    pub table_row_threshold_ratio: Option<f64>,
    /// Enable OCR result caching
    #[napi(js_name = "useCache")]
    #[serde(rename = "useCache")]
    pub use_cache: Option<bool>,
    /// Use pre-adapted templates for character classification
    #[napi(js_name = "classifyUsePreAdaptedTemplates")]
    #[serde(rename = "classifyUsePreAdaptedTemplates")]
    pub classify_use_pre_adapted_templates: Option<bool>,
    /// Enable N-gram language model
    #[napi(js_name = "languageModelNgramOn")]
    #[serde(rename = "languageModelNgramOn")]
    pub language_model_ngram_on: Option<bool>,
    /// Don't reject good words during block-level processing
    #[napi(js_name = "tesseditDontBlkrejGoodWds")]
    #[serde(rename = "tesseditDontBlkrejGoodWds")]
    pub tessedit_dont_blkrej_good_wds: Option<bool>,
    /// Don't reject good words during row-level processing
    #[napi(js_name = "tesseditDontRowrejGoodWds")]
    #[serde(rename = "tesseditDontRowrejGoodWds")]
    pub tessedit_dont_rowrej_good_wds: Option<bool>,
    /// Enable dictionary correction
    #[napi(js_name = "tesseditEnableDictCorrection")]
    #[serde(rename = "tesseditEnableDictCorrection")]
    pub tessedit_enable_dict_correction: Option<bool>,
    /// Whitelist of allowed characters (empty = all allowed)
    #[napi(js_name = "tesseditCharWhitelist")]
    #[serde(rename = "tesseditCharWhitelist")]
    pub tessedit_char_whitelist: Option<String>,
    /// Blacklist of forbidden characters (empty = none forbidden)
    #[napi(js_name = "tesseditCharBlacklist")]
    #[serde(rename = "tesseditCharBlacklist")]
    pub tessedit_char_blacklist: Option<String>,
    /// Use primary language params model
    #[napi(js_name = "tesseditUsePrimaryParamsModel")]
    #[serde(rename = "tesseditUsePrimaryParamsModel")]
    pub tessedit_use_primary_params_model: Option<bool>,
    /// Variable-width space detection
    #[napi(js_name = "textordSpaceSizeIsVariable")]
    #[serde(rename = "textordSpaceSizeIsVariable")]
    pub textord_space_size_is_variable: Option<bool>,
    /// Use adaptive thresholding method
    #[napi(js_name = "thresholdingMethod")]
    #[serde(rename = "thresholdingMethod")]
    pub thresholding_method: Option<bool>,
}

#[napi(js_name = "tesseractConfigDefault")]
pub fn tesseract_config_default() -> JsTesseractConfig {
    kreuzberg::TesseractConfig::default().into()
}

/// Image preprocessing metadata.
///
/// Tracks the transformations applied to an image during OCR preprocessing,
/// including DPI normalization, resizing, and resampling.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ImagePreprocessingMetadata")]
pub struct JsImagePreprocessingMetadata {
    /// Original image dimensions (width, height) in pixels
    #[napi(js_name = "originalDimensions")]
    #[serde(rename = "originalDimensions")]
    pub original_dimensions: Vec<i64>,
    /// Original image DPI (horizontal, vertical)
    #[napi(js_name = "originalDpi")]
    #[serde(rename = "originalDpi")]
    pub original_dpi: Vec<f64>,
    /// Target DPI from configuration
    #[napi(js_name = "targetDpi")]
    #[serde(rename = "targetDpi")]
    pub target_dpi: i32,
    /// Scaling factor applied to the image
    #[napi(js_name = "scaleFactor")]
    #[serde(rename = "scaleFactor")]
    pub scale_factor: f64,
    /// Whether DPI was auto-adjusted based on content
    #[napi(js_name = "autoAdjusted")]
    #[serde(rename = "autoAdjusted")]
    pub auto_adjusted: bool,
    /// Final DPI after processing
    #[napi(js_name = "finalDpi")]
    #[serde(rename = "finalDpi")]
    pub final_dpi: i32,
    /// New dimensions after resizing (if resized)
    #[napi(js_name = "newDimensions")]
    #[serde(rename = "newDimensions")]
    pub new_dimensions: Option<Vec<i64>>,
    /// Resampling algorithm used ("LANCZOS3", "CATMULLROM", etc.)
    #[napi(js_name = "resampleMethod")]
    #[serde(rename = "resampleMethod")]
    pub resample_method: String,
    /// Whether dimensions were clamped to max_image_dimension
    #[napi(js_name = "dimensionClamped")]
    #[serde(rename = "dimensionClamped")]
    pub dimension_clamped: bool,
    /// Calculated optimal DPI (if auto_adjust_dpi enabled)
    #[napi(js_name = "calculatedDpi")]
    #[serde(rename = "calculatedDpi")]
    pub calculated_dpi: Option<i32>,
    /// Whether resize was skipped (dimensions already optimal)
    #[napi(js_name = "skippedResize")]
    #[serde(rename = "skippedResize")]
    pub skipped_resize: bool,
    /// Error message if resize failed
    #[napi(js_name = "resizeError")]
    #[serde(rename = "resizeError")]
    pub resize_error: Option<String>,
}

/// Extraction result metadata.
///
/// Contains common fields applicable to all formats, format-specific metadata
/// via a discriminated union, and additional custom fields from postprocessors.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "Metadata")]
pub struct JsMetadata {
    /// Document title
    pub title: Option<String>,
    /// Document subject or description
    pub subject: Option<String>,
    /// Primary author(s) - always Vec for consistency
    pub authors: Option<Vec<String>>,
    /// Keywords/tags - always Vec for consistency
    pub keywords: Option<Vec<String>>,
    /// Primary language (ISO 639 code)
    pub language: Option<String>,
    /// Creation timestamp (ISO 8601 format)
    #[napi(js_name = "createdAt")]
    #[serde(rename = "createdAt")]
    pub created_at: Option<String>,
    /// Last modification timestamp (ISO 8601 format)
    #[napi(js_name = "modifiedAt")]
    #[serde(rename = "modifiedAt")]
    pub modified_at: Option<String>,
    /// User who created the document
    #[napi(js_name = "createdBy")]
    #[serde(rename = "createdBy")]
    pub created_by: Option<String>,
    /// User who last modified the document
    #[napi(js_name = "modifiedBy")]
    #[serde(rename = "modifiedBy")]
    pub modified_by: Option<String>,
    /// Page/slide/sheet structure with boundaries
    pub pages: Option<JsPageStructure>,
    /// Format-specific metadata (discriminated union)
    ///
    /// Contains detailed metadata specific to the document format.
    /// Serialized as a nested `"format"` object with a `format_type` discriminator field.
    pub format: Option<JsFormatMetadata>,
    /// Image preprocessing metadata (when OCR preprocessing was applied)
    #[napi(js_name = "imagePreprocessing")]
    #[serde(rename = "imagePreprocessing")]
    pub image_preprocessing: Option<JsImagePreprocessingMetadata>,
    /// JSON schema (for structured data extraction)
    #[napi(js_name = "jsonSchema")]
    #[serde(rename = "jsonSchema")]
    pub json_schema: Option<serde_json::Value>,
    /// Error metadata (for batch operations)
    pub error: Option<JsErrorMetadata>,
    /// Extraction duration in milliseconds (for benchmarking).
    ///
    /// This field is populated by batch extraction to provide per-file timing
    /// information. It's `None` for single-file extraction (which uses external timing).
    #[napi(js_name = "extractionDurationMs")]
    #[serde(rename = "extractionDurationMs")]
    pub extraction_duration_ms: Option<i64>,
    /// Document category (from frontmatter or classification).
    pub category: Option<String>,
    /// Document tags (from frontmatter).
    pub tags: Option<Vec<String>>,
    /// Document version string (from frontmatter).
    #[napi(js_name = "documentVersion")]
    #[serde(rename = "documentVersion")]
    pub document_version: Option<String>,
    /// Abstract or summary text (from frontmatter).
    #[napi(js_name = "abstractText")]
    #[serde(rename = "abstractText")]
    pub abstract_text: Option<String>,
    /// Output format identifier (e.g., "markdown", "html", "text").
    ///
    /// Set by the output format pipeline stage when format conversion is applied.
    /// Previously stored in `metadata.additional["output_format"]`.
    #[napi(js_name = "outputFormat")]
    #[serde(rename = "outputFormat")]
    pub output_format: Option<String>,
    /// Whether OCR was used during extraction.
    ///
    /// Set to `true` whenever the extraction pipeline ran an OCR backend
    /// (Tesseract, PaddleOCR, VLM, etc.) and used that output as the primary
    /// or fallback text. `false` means native text extraction was used exclusively.
    #[napi(js_name = "ocrUsed")]
    #[serde(rename = "ocrUsed")]
    pub ocr_used: Option<bool>,
    /// Additional custom fields from postprocessors.
    ///
    /// Serialized as a nested `"additional"` object (not flattened at root level).
    /// Uses `Cow<'static, str>` keys so static string keys avoid allocation.
    pub additional: Option<HashMap<String, serde_json::Value>>,
}

/// Excel/spreadsheet format metadata.
///
/// Identifies the document as a spreadsheet source via the `FormatMetadata.Excel`
/// discriminant. Sheet count and sheet names are stored inside this struct.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ExcelMetadata")]
pub struct JsExcelMetadata {
    /// Number of sheets in the workbook.
    #[napi(js_name = "sheetCount")]
    #[serde(rename = "sheetCount")]
    pub sheet_count: Option<u32>,
    /// Names of all sheets in the workbook.
    #[napi(js_name = "sheetNames")]
    #[serde(rename = "sheetNames")]
    pub sheet_names: Option<Vec<String>>,
}

/// Email metadata extracted from .eml and .msg files.
///
/// Includes sender/recipient information, message ID, and attachment list.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "EmailMetadata")]
pub struct JsEmailMetadata {
    /// Sender's email address
    #[napi(js_name = "fromEmail")]
    #[serde(rename = "fromEmail")]
    pub from_email: Option<String>,
    /// Sender's display name
    #[napi(js_name = "fromName")]
    #[serde(rename = "fromName")]
    pub from_name: Option<String>,
    /// Primary recipients
    #[napi(js_name = "toEmails")]
    #[serde(rename = "toEmails")]
    pub to_emails: Option<Vec<String>>,
    /// CC recipients
    #[napi(js_name = "ccEmails")]
    #[serde(rename = "ccEmails")]
    pub cc_emails: Option<Vec<String>>,
    /// BCC recipients
    #[napi(js_name = "bccEmails")]
    #[serde(rename = "bccEmails")]
    pub bcc_emails: Option<Vec<String>>,
    /// Message-ID header value
    #[napi(js_name = "messageId")]
    #[serde(rename = "messageId")]
    pub message_id: Option<String>,
    /// List of attachment filenames
    pub attachments: Option<Vec<String>>,
}

/// Archive (ZIP/TAR/7Z) metadata.
///
/// Extracted from compressed archive files containing file lists and size information.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ArchiveMetadata")]
pub struct JsArchiveMetadata {
    /// Archive format ("ZIP", "TAR", "7Z", etc.)
    pub format: Option<String>,
    /// Total number of files in the archive
    #[napi(js_name = "fileCount")]
    #[serde(rename = "fileCount")]
    pub file_count: Option<u32>,
    /// List of file paths within the archive
    #[napi(js_name = "fileList")]
    #[serde(rename = "fileList")]
    pub file_list: Option<Vec<String>>,
    /// Total uncompressed size in bytes
    #[napi(js_name = "totalSize")]
    #[serde(rename = "totalSize")]
    pub total_size: Option<i64>,
    /// Compressed size in bytes (if available)
    #[napi(js_name = "compressedSize")]
    #[serde(rename = "compressedSize")]
    pub compressed_size: Option<i64>,
}

/// Image metadata extracted from image files.
///
/// Includes dimensions, format, and EXIF data.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ImageMetadata")]
pub struct JsImageMetadata {
    /// Image width in pixels
    pub width: Option<u32>,
    /// Image height in pixels
    pub height: Option<u32>,
    /// Image format (e.g., "PNG", "JPEG", "TIFF")
    pub format: Option<String>,
    /// EXIF metadata tags
    pub exif: Option<HashMap<String, String>>,
}

/// XML metadata extracted during XML parsing.
///
/// Provides statistics about XML document structure.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "XmlMetadata")]
pub struct JsXmlMetadata {
    /// Total number of XML elements processed
    #[napi(js_name = "elementCount")]
    #[serde(rename = "elementCount")]
    pub element_count: Option<u32>,
    /// List of unique element tag names (sorted)
    #[napi(js_name = "uniqueElements")]
    #[serde(rename = "uniqueElements")]
    pub unique_elements: Option<Vec<String>>,
}

/// Text/Markdown metadata.
///
/// Extracted from plain text and Markdown files. Includes word counts and,
/// for Markdown, structural elements like headers and links.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "TextMetadata")]
pub struct JsTextMetadata {
    /// Number of lines in the document
    #[napi(js_name = "lineCount")]
    #[serde(rename = "lineCount")]
    pub line_count: Option<u32>,
    /// Number of words
    #[napi(js_name = "wordCount")]
    #[serde(rename = "wordCount")]
    pub word_count: Option<u32>,
    /// Number of characters
    #[napi(js_name = "characterCount")]
    #[serde(rename = "characterCount")]
    pub character_count: Option<u32>,
    /// Markdown headers (headings text only, for Markdown files)
    pub headers: Option<Vec<String>>,
    /// Markdown links as (text, url) tuples (for Markdown files)
    pub links: Option<Vec<Vec<String>>>,
    /// Code blocks as (language, code) tuples (for Markdown files)
    #[napi(js_name = "codeBlocks")]
    #[serde(rename = "codeBlocks")]
    pub code_blocks: Option<Vec<Vec<String>>>,
}

/// Header/heading element metadata.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "HeaderMetadata")]
pub struct JsHeaderMetadata {
    /// Header level: 1 (h1) through 6 (h6)
    pub level: u8,
    /// Normalized text content of the header
    pub text: String,
    /// HTML id attribute if present
    pub id: Option<String>,
    /// Document tree depth at the header element
    pub depth: u32,
    /// Byte offset in original HTML document
    #[napi(js_name = "htmlOffset")]
    #[serde(rename = "htmlOffset")]
    pub html_offset: u32,
}

/// Link element metadata.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "LinkMetadata")]
pub struct JsLinkMetadata {
    /// The href URL value
    pub href: String,
    /// Link text content (normalized)
    pub text: String,
    /// Optional title attribute
    pub title: Option<String>,
    /// Link type classification
    #[napi(js_name = "linkType")]
    #[serde(rename = "linkType")]
    pub link_type: JsLinkType,
    /// Rel attribute values
    pub rel: Vec<String>,
    /// Additional attributes as key-value pairs
    pub attributes: Vec<Vec<String>>,
}

/// Image element metadata.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ImageMetadataType")]
pub struct JsImageMetadataType {
    /// Image source (URL, data URI, or SVG content)
    pub src: String,
    /// Alternative text from alt attribute
    pub alt: Option<String>,
    /// Title attribute
    pub title: Option<String>,
    /// Image dimensions as (width, height) if available
    pub dimensions: Option<Vec<u32>>,
    /// Image type classification
    #[napi(js_name = "imageType")]
    #[serde(rename = "imageType")]
    pub image_type: JsImageType,
    /// Additional attributes as key-value pairs
    pub attributes: Vec<Vec<String>>,
}

/// Structured data (Schema.org, microdata, RDFa) block.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "StructuredData")]
pub struct JsStructuredData {
    /// Type of structured data
    #[napi(js_name = "dataType")]
    #[serde(rename = "dataType")]
    pub data_type: JsStructuredDataType,
    /// Raw JSON string representation
    #[napi(js_name = "rawJson")]
    #[serde(rename = "rawJson")]
    pub raw_json: String,
    /// Schema type if detectable (e.g., "Article", "Event", "Product")
    #[napi(js_name = "schemaType")]
    #[serde(rename = "schemaType")]
    pub schema_type: Option<String>,
}

/// HTML metadata extracted from HTML documents.
///
/// Includes document-level metadata, Open Graph data, Twitter Card metadata,
/// and extracted structural elements (headers, links, images, structured data).
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "HtmlMetadata")]
pub struct JsHtmlMetadata {
    /// Document title from `<title>` tag
    pub title: Option<String>,
    /// Document description from `<meta name="description">` tag
    pub description: Option<String>,
    /// Document keywords from `<meta name="keywords">` tag, split on commas
    pub keywords: Option<Vec<String>>,
    /// Document author from `<meta name="author">` tag
    pub author: Option<String>,
    /// Canonical URL from `<link rel="canonical">` tag
    #[napi(js_name = "canonicalUrl")]
    #[serde(rename = "canonicalUrl")]
    pub canonical_url: Option<String>,
    /// Base URL from `<base href="">` tag for resolving relative URLs
    #[napi(js_name = "baseHref")]
    #[serde(rename = "baseHref")]
    pub base_href: Option<String>,
    /// Document language from `lang` attribute
    pub language: Option<String>,
    /// Document text direction from `dir` attribute
    #[napi(js_name = "textDirection")]
    #[serde(rename = "textDirection")]
    pub text_direction: Option<JsTextDirection>,
    /// Open Graph metadata (og:* properties) for social media
    /// Keys like "title", "description", "image", "url", etc.
    #[napi(js_name = "openGraph")]
    #[serde(rename = "openGraph")]
    pub open_graph: Option<HashMap<String, String>>,
    /// Twitter Card metadata (twitter:* properties)
    /// Keys like "card", "site", "creator", "title", "description", "image", etc.
    #[napi(js_name = "twitterCard")]
    #[serde(rename = "twitterCard")]
    pub twitter_card: Option<HashMap<String, String>>,
    /// Additional meta tags not covered by specific fields
    /// Keys are meta name/property attributes, values are content
    #[napi(js_name = "metaTags")]
    #[serde(rename = "metaTags")]
    pub meta_tags: Option<HashMap<String, String>>,
    /// Extracted header elements with hierarchy
    pub headers: Option<Vec<JsHeaderMetadata>>,
    /// Extracted hyperlinks with type classification
    pub links: Option<Vec<JsLinkMetadata>>,
    /// Extracted images with source and dimensions
    pub images: Option<Vec<JsImageMetadataType>>,
    /// Extracted structured data blocks
    #[napi(js_name = "structuredData")]
    #[serde(rename = "structuredData")]
    pub structured_data: Option<Vec<JsStructuredData>>,
}

/// OCR processing metadata.
///
/// Captures information about OCR processing configuration and results.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "OcrMetadata")]
pub struct JsOcrMetadata {
    /// OCR language code(s) used
    pub language: Option<String>,
    /// Tesseract Page Segmentation Mode (PSM)
    pub psm: Option<i32>,
    /// Output format (e.g., "text", "hocr")
    #[napi(js_name = "outputFormat")]
    #[serde(rename = "outputFormat")]
    pub output_format: Option<String>,
    /// Number of tables detected
    #[napi(js_name = "tableCount")]
    #[serde(rename = "tableCount")]
    pub table_count: Option<u32>,
    #[napi(js_name = "tableRows")]
    #[serde(rename = "tableRows")]
    pub table_rows: Option<u32>,
    #[napi(js_name = "tableCols")]
    #[serde(rename = "tableCols")]
    pub table_cols: Option<u32>,
}

/// Error metadata (for batch operations).
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ErrorMetadata")]
pub struct JsErrorMetadata {
    #[napi(js_name = "errorType")]
    #[serde(rename = "errorType")]
    pub error_type: String,
    pub message: String,
}

/// PowerPoint presentation metadata.
///
/// Extracted from PPTX files containing slide counts and presentation details.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "PptxMetadata")]
pub struct JsPptxMetadata {
    /// Total number of slides in the presentation
    #[napi(js_name = "slideCount")]
    #[serde(rename = "slideCount")]
    pub slide_count: Option<u32>,
    /// Names of slides (if available)
    #[napi(js_name = "slideNames")]
    #[serde(rename = "slideNames")]
    pub slide_names: Option<Vec<String>>,
    /// Number of embedded images
    #[napi(js_name = "imageCount")]
    #[serde(rename = "imageCount")]
    pub image_count: Option<u32>,
    /// Number of tables
    #[napi(js_name = "tableCount")]
    #[serde(rename = "tableCount")]
    pub table_count: Option<u32>,
}

/// Word document metadata.
///
/// Extracted from DOCX files using shared Office Open XML metadata extraction.
/// Integrates with `office_metadata` module for core/app/custom properties.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "DocxMetadata")]
pub struct JsDocxMetadata {
    /// Core properties from docProps/core.xml (Dublin Core metadata)
    ///
    /// Contains title, creator, subject, keywords, dates, etc.
    /// Shared format across DOCX/PPTX/XLSX documents.
    #[napi(js_name = "coreProperties")]
    #[serde(rename = "coreProperties")]
    pub core_properties: Option<JsCoreProperties>,
    /// Application properties from docProps/app.xml (Word-specific statistics)
    ///
    /// Contains word count, page count, paragraph count, editing time, etc.
    /// DOCX-specific variant of Office application properties.
    #[napi(js_name = "appProperties")]
    #[serde(rename = "appProperties")]
    pub app_properties: Option<JsDocxAppProperties>,
    /// Custom properties from docProps/custom.xml (user-defined properties)
    ///
    /// Contains key-value pairs defined by users or applications.
    /// Values can be strings, numbers, booleans, or dates.
    #[napi(js_name = "customProperties")]
    #[serde(rename = "customProperties")]
    pub custom_properties: Option<HashMap<String, serde_json::Value>>,
}

/// CSV/TSV file metadata.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "CsvMetadata")]
pub struct JsCsvMetadata {
    #[napi(js_name = "rowCount")]
    #[serde(rename = "rowCount")]
    pub row_count: Option<u32>,
    #[napi(js_name = "columnCount")]
    #[serde(rename = "columnCount")]
    pub column_count: Option<u32>,
    pub delimiter: Option<String>,
    #[napi(js_name = "hasHeader")]
    #[serde(rename = "hasHeader")]
    pub has_header: Option<bool>,
    #[napi(js_name = "columnTypes")]
    #[serde(rename = "columnTypes")]
    pub column_types: Option<Vec<String>>,
}

/// BibTeX bibliography metadata.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "BibtexMetadata")]
pub struct JsBibtexMetadata {
    /// Number of entries in the bibliography.
    #[napi(js_name = "entryCount")]
    #[serde(rename = "entryCount")]
    pub entry_count: Option<i64>,
    #[napi(js_name = "citationKeys")]
    #[serde(rename = "citationKeys")]
    pub citation_keys: Option<Vec<String>>,
    pub authors: Option<Vec<String>>,
    #[napi(js_name = "yearRange")]
    #[serde(rename = "yearRange")]
    pub year_range: Option<JsYearRange>,
    #[napi(js_name = "entryTypes")]
    #[serde(rename = "entryTypes")]
    pub entry_types: Option<HashMap<String, i64>>,
}

/// Citation file metadata (RIS, PubMed, EndNote).
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "CitationMetadata")]
pub struct JsCitationMetadata {
    #[napi(js_name = "citationCount")]
    #[serde(rename = "citationCount")]
    pub citation_count: Option<i64>,
    pub format: Option<String>,
    pub authors: Option<Vec<String>>,
    #[napi(js_name = "yearRange")]
    #[serde(rename = "yearRange")]
    pub year_range: Option<JsYearRange>,
    pub dois: Option<Vec<String>>,
    pub keywords: Option<Vec<String>>,
}

/// Year range for bibliographic metadata.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "YearRange")]
pub struct JsYearRange {
    pub min: Option<u32>,
    pub max: Option<u32>,
    pub years: Vec<u32>,
}

/// FictionBook (FB2) metadata.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "FictionBookMetadata")]
pub struct JsFictionBookMetadata {
    pub genres: Option<Vec<String>>,
    pub sequences: Option<Vec<String>>,
    pub annotation: Option<String>,
}

/// dBASE (DBF) file metadata.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "DbfMetadata")]
pub struct JsDbfMetadata {
    #[napi(js_name = "recordCount")]
    #[serde(rename = "recordCount")]
    pub record_count: Option<i64>,
    #[napi(js_name = "fieldCount")]
    #[serde(rename = "fieldCount")]
    pub field_count: Option<i64>,
    pub fields: Option<Vec<JsDbfFieldInfo>>,
}

/// dBASE field information.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "DbfFieldInfo")]
pub struct JsDbfFieldInfo {
    pub name: String,
    #[napi(js_name = "fieldType")]
    #[serde(rename = "fieldType")]
    pub field_type: String,
}

/// JATS (Journal Article Tag Suite) metadata.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "JatsMetadata")]
pub struct JsJatsMetadata {
    pub copyright: Option<String>,
    pub license: Option<String>,
    #[napi(js_name = "historyDates")]
    #[serde(rename = "historyDates")]
    pub history_dates: Option<HashMap<String, String>>,
    #[napi(js_name = "contributorRoles")]
    #[serde(rename = "contributorRoles")]
    pub contributor_roles: Option<Vec<JsContributorRole>>,
}

/// JATS contributor with role.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ContributorRole")]
pub struct JsContributorRole {
    pub name: String,
    pub role: Option<String>,
}

/// EPUB metadata (Dublin Core extensions).
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "EpubMetadata")]
pub struct JsEpubMetadata {
    pub coverage: Option<String>,
    #[napi(js_name = "dcFormat")]
    #[serde(rename = "dcFormat")]
    pub dc_format: Option<String>,
    pub relation: Option<String>,
    pub source: Option<String>,
    #[napi(js_name = "dcType")]
    #[serde(rename = "dcType")]
    pub dc_type: Option<String>,
    #[napi(js_name = "coverImage")]
    #[serde(rename = "coverImage")]
    pub cover_image: Option<String>,
}

/// Outlook PST archive metadata.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "PstMetadata")]
pub struct JsPstMetadata {
    #[napi(js_name = "messageCount")]
    #[serde(rename = "messageCount")]
    pub message_count: Option<i64>,
}

/// Confidence scores for an OCR element.
///
/// Separates detection confidence (how confident that text exists at this location)
/// from recognition confidence (how confident about the actual text content).
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "OcrConfidence")]
pub struct JsOcrConfidence {
    /// Detection confidence: how confident the OCR engine is that text exists here.
    ///
    /// PaddleOCR provides this as `box_score`, Tesseract doesn't have a direct equivalent.
    /// Range: 0.0 to 1.0 (or None if not available).
    pub detection: Option<f64>,
    /// Recognition confidence: how confident about the text content.
    ///
    /// Range: 0.0 to 1.0.
    pub recognition: Option<f64>,
}

/// Rotation information for an OCR element.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "OcrRotation")]
pub struct JsOcrRotation {
    /// Rotation angle in degrees (0, 90, 180, 270 for PaddleOCR).
    #[napi(js_name = "angleDegrees")]
    #[serde(rename = "angleDegrees")]
    pub angle_degrees: f64,
    /// Confidence score for the rotation detection.
    pub confidence: Option<f64>,
}

/// A unified OCR element representing detected text with full metadata.
///
/// This is the primary type for structured OCR output, preserving all information
/// from both Tesseract and PaddleOCR backends.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "OcrElement")]
pub struct JsOcrElement {
    /// The recognized text content.
    pub text: Option<String>,
    /// Bounding geometry (rectangle or quadrilateral).
    pub geometry: Option<JsOcrBoundingGeometry>,
    /// Confidence scores for detection and recognition.
    pub confidence: Option<JsOcrConfidence>,
    /// Hierarchical level (word, line, block, page).
    pub level: Option<JsOcrElementLevel>,
    /// Rotation information (if detected).
    pub rotation: Option<JsOcrRotation>,
    /// Page number (1-indexed).
    #[napi(js_name = "pageNumber")]
    #[serde(rename = "pageNumber")]
    pub page_number: Option<u32>,
    /// Parent element ID for hierarchical relationships.
    ///
    /// Only used for Tesseract output which has word -> line -> block hierarchy.
    #[napi(js_name = "parentId")]
    #[serde(rename = "parentId")]
    pub parent_id: Option<String>,
    /// Backend-specific metadata that doesn't fit the unified schema.
    #[napi(js_name = "backendMetadata")]
    #[serde(rename = "backendMetadata")]
    pub backend_metadata: Option<HashMap<String, serde_json::Value>>,
}

/// Configuration for OCR element extraction.
///
/// Controls how OCR elements are extracted and filtered.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "OcrElementConfig")]
pub struct JsOcrElementConfig {
    /// Whether to include OCR elements in the extraction result.
    ///
    /// When true, the `ocr_elements` field in `ExtractionResult` will be populated.
    #[napi(js_name = "includeElements")]
    #[serde(rename = "includeElements")]
    pub include_elements: Option<bool>,
    /// Minimum hierarchical level to include.
    ///
    /// Elements below this level (e.g., words when min_level is Line) will be excluded.
    #[napi(js_name = "minLevel")]
    #[serde(rename = "minLevel")]
    pub min_level: Option<JsOcrElementLevel>,
    /// Minimum recognition confidence threshold (0.0-1.0).
    ///
    /// Elements with confidence below this threshold will be filtered out.
    #[napi(js_name = "minConfidence")]
    #[serde(rename = "minConfidence")]
    pub min_confidence: Option<f64>,
    /// Whether to build hierarchical relationships between elements.
    ///
    /// When true, `parent_id` fields will be populated based on spatial containment.
    /// Only meaningful for Tesseract output.
    #[napi(js_name = "buildHierarchy")]
    #[serde(rename = "buildHierarchy")]
    pub build_hierarchy: Option<bool>,
}

/// Unified page structure for documents.
///
/// Supports different page types (PDF pages, PPTX slides, Excel sheets)
/// with character offset boundaries for chunk-to-page mapping.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "PageStructure")]
pub struct JsPageStructure {
    /// Total number of pages/slides/sheets
    #[napi(js_name = "totalCount")]
    #[serde(rename = "totalCount")]
    pub total_count: u32,
    /// Type of paginated unit
    #[napi(js_name = "unitType")]
    #[serde(rename = "unitType")]
    pub unit_type: JsPageUnitType,
    /// Character offset boundaries for each page
    ///
    /// Maps character ranges in the extracted content to page numbers.
    /// Used for chunk page range calculation.
    pub boundaries: Option<Vec<JsPageBoundary>>,
    /// Detailed per-page metadata (optional, only when needed)
    pub pages: Option<Vec<JsPageInfo>>,
}

/// Byte offset boundary for a page.
///
/// Tracks where a specific page's content starts and ends in the main content string,
/// enabling mapping from byte positions to page numbers. Offsets are guaranteed to be
/// at valid UTF-8 character boundaries when using standard String methods (push_str, push, etc.).
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "PageBoundary")]
pub struct JsPageBoundary {
    /// Byte offset where this page starts in the content string (UTF-8 valid boundary, inclusive)
    #[napi(js_name = "byteStart")]
    #[serde(rename = "byteStart")]
    pub byte_start: i64,
    /// Byte offset where this page ends in the content string (UTF-8 valid boundary, exclusive)
    #[napi(js_name = "byteEnd")]
    #[serde(rename = "byteEnd")]
    pub byte_end: i64,
    /// Page number (1-indexed)
    #[napi(js_name = "pageNumber")]
    #[serde(rename = "pageNumber")]
    pub page_number: u32,
}

/// Metadata for individual page/slide/sheet.
///
/// Captures per-page information including dimensions, content counts,
/// and visibility state (for presentations).
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "PageInfo")]
pub struct JsPageInfo {
    /// Page number (1-indexed)
    pub number: u32,
    /// Page title (usually for presentations)
    pub title: Option<String>,
    /// Dimensions in points (PDF) or pixels (images): (width, height)
    pub dimensions: Option<Vec<f64>>,
    /// Number of images on this page
    #[napi(js_name = "imageCount")]
    #[serde(rename = "imageCount")]
    pub image_count: Option<u32>,
    /// Number of tables on this page
    #[napi(js_name = "tableCount")]
    #[serde(rename = "tableCount")]
    pub table_count: Option<u32>,
    /// Whether this page is hidden (e.g., in presentations)
    pub hidden: Option<bool>,
    /// Whether this page is blank (no meaningful text, no images, no tables)
    ///
    /// A page is considered blank if it has fewer than 3 non-whitespace characters
    /// and contains no tables or images. This is useful for filtering out empty pages
    /// in scanned documents or PDFs with blank separator pages.
    #[napi(js_name = "isBlank")]
    #[serde(rename = "isBlank")]
    pub is_blank: Option<bool>,
    /// Whether this page contains non-trivial vector graphics (paths, shapes, curves)
    ///
    /// Indicates the presence of vector-drawn content such as charts, diagrams,
    /// or geometric shapes (e.g., from Adobe InDesign, LaTeX TikZ). These are
    /// invisible to `ExtractionResult.images` since they are not embedded as raster
    /// XObjects. Set to `true` when path count exceeds a heuristic threshold,
    /// signaling that downstream consumers may want to rasterize the page to
    /// capture this content.
    ///
    /// Only populated for PDFs; `None` for other document types.
    #[napi(js_name = "hasVectorGraphics")]
    #[serde(rename = "hasVectorGraphics")]
    pub has_vector_graphics: bool,
}

/// Content for a single page/slide.
///
/// When page extraction is enabled, documents are split into per-page content
/// with associated tables and images mapped to each page.
///
/// # Performance
///
/// Uses Arc-wrapped tables and images for memory efficiency:
/// - `Vec<Arc<Table>>` enables zero-copy sharing of table data
/// - `Vec<Arc<ExtractedImage>>` enables zero-copy sharing of image data
/// - Maintains exact JSON compatibility via custom Serialize/Deserialize
///
/// This reduces memory overhead for documents with shared tables/images
/// by avoiding redundant copies during serialization.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "PageContent")]
pub struct JsPageContent {
    /// Page number (1-indexed)
    #[napi(js_name = "pageNumber")]
    #[serde(rename = "pageNumber")]
    pub page_number: u32,
    /// Text content for this page
    pub content: String,
    /// Tables found on this page (uses Arc for memory efficiency)
    ///
    /// Serializes as Vec<Table> for JSON compatibility while maintaining
    /// Arc semantics in-memory for zero-copy sharing.
    pub tables: Vec<JsTable>,
    /// Indices into `ExtractionResult.images` for images found on this page.
    ///
    /// Each value is a zero-based index into the top-level `images` collection.
    /// Only populated when `extract_images = true` in the extraction config.
    #[napi(js_name = "imageIndices")]
    #[serde(rename = "imageIndices")]
    pub image_indices: Vec<u32>,
    /// Hierarchy information for the page (when hierarchy extraction is enabled)
    ///
    /// Contains text hierarchy levels (H1-H6) extracted from the page content.
    pub hierarchy: Option<JsPageHierarchy>,
    /// Whether this page is blank (no meaningful text content)
    ///
    /// Determined during extraction based on text content analysis.
    /// A page is blank if it has fewer than 3 non-whitespace characters
    /// and contains no tables or images.
    #[napi(js_name = "isBlank")]
    #[serde(rename = "isBlank")]
    pub is_blank: Option<bool>,
    /// Layout detection regions for this page (when layout detection is enabled).
    ///
    /// Contains detected layout regions with class, confidence, bounding box,
    /// and area fraction. Only populated when layout detection is configured.
    #[napi(js_name = "layoutRegions")]
    #[serde(rename = "layoutRegions")]
    pub layout_regions: Option<Vec<JsLayoutRegion>>,
    /// Speaker notes for this slide (PPTX only).
    ///
    /// Contains the text from the slide's notes pane (`ppt/notesSlides/notesSlide{N}.xml`).
    /// Only populated when the source is a PPTX file and notes are present.
    #[napi(js_name = "speakerNotes")]
    #[serde(rename = "speakerNotes")]
    pub speaker_notes: Option<String>,
    /// Section name this slide belongs to (PPTX only).
    ///
    /// PowerPoint sections group slides into logical chapters (`<p:sectionLst>` in
    /// `ppt/presentation.xml`). Only populated when the source is a PPTX file and
    /// the slide belongs to a named section.
    #[napi(js_name = "sectionName")]
    #[serde(rename = "sectionName")]
    pub section_name: Option<String>,
    /// Sheet name for this page (XLSX/ODS only).
    ///
    /// Each spreadsheet sheet maps to one `PageContent` entry. This field carries the
    /// sheet's display name as it appears in the workbook. `None` for all non-spreadsheet
    /// formats and for sheets with an empty name.
    #[napi(js_name = "sheetName")]
    #[serde(rename = "sheetName")]
    pub sheet_name: Option<String>,
}

/// A detected layout region on a page.
///
/// When layout detection is enabled, each page may have layout regions
/// identifying different content types (text, pictures, tables, etc.)
/// with confidence scores and spatial positions.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "LayoutRegion")]
pub struct JsLayoutRegion {
    /// Layout class name (e.g. "picture", "table", "text", "section_header").
    #[napi(js_name = "className")]
    #[serde(rename = "className")]
    pub class_name: Option<String>,
    /// Confidence score from the layout detection model (0.0 to 1.0).
    pub confidence: Option<f64>,
    /// Bounding box in document coordinate space.
    #[napi(js_name = "boundingBox")]
    #[serde(rename = "boundingBox")]
    pub bounding_box: Option<JsBoundingBox>,
    /// Fraction of the page area covered by this region (0.0 to 1.0).
    #[napi(js_name = "areaFraction")]
    #[serde(rename = "areaFraction")]
    pub area_fraction: Option<f64>,
}

/// Page hierarchy structure containing heading levels and block information.
///
/// Used when PDF text hierarchy extraction is enabled. Contains hierarchical
/// blocks with heading levels (H1-H6) for semantic document structure.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "PageHierarchy")]
pub struct JsPageHierarchy {
    /// Number of hierarchy blocks on this page
    #[napi(js_name = "blockCount")]
    #[serde(rename = "blockCount")]
    pub block_count: u32,
    /// Hierarchical blocks with heading levels
    pub blocks: Vec<JsHierarchicalBlock>,
}

/// A text block with hierarchy level assignment.
///
/// Represents a block of text with semantic heading information extracted from
/// font size clustering and hierarchical analysis.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "HierarchicalBlock")]
pub struct JsHierarchicalBlock {
    /// The text content of this block
    pub text: String,
    /// The font size of the text in this block
    #[napi(js_name = "fontSize")]
    #[serde(rename = "fontSize")]
    pub font_size: f64,
    /// The hierarchy level of this block (H1-H6 or Body)
    ///
    /// Levels correspond to HTML heading tags:
    /// - "h1": Top-level heading
    /// - "h2": Secondary heading
    /// - "h3": Tertiary heading
    /// - "h4": Quaternary heading
    /// - "h5": Quinary heading
    /// - "h6": Senary heading
    /// - "body": Body text (no heading level)
    pub level: String,
    /// Bounding box information for the block
    ///
    /// Contains coordinates as (left, top, right, bottom) in PDF units.
    pub bbox: Option<Vec<f64>>,
}

/// A single changed cell within a table.
///
/// Defined here (rather than only in `crate.diff`) so `RevisionDelta` can
/// reference it unconditionally, without requiring the `diff` Cargo feature.
/// `crate.diff` re-exports this type verbatim.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "CellChange")]
pub struct JsCellChange {
    /// Zero-based row index.
    pub row: i64,
    /// Zero-based column index.
    pub col: i64,
    /// Value before the change.
    pub from: String,
    /// Value after the change.
    pub to: String,
}

/// A single tracked change embedded in a document.
///
/// Populated by per-format extractors that understand change-tracking metadata
/// (DOCX `w:ins`/`w:del`/`w:rPrChange`, ODT `text:change-*`, …). Every
/// extractor defaults to `ExtractionResult.revisions = None` until a
/// format-specific implementation is added.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "DocumentRevision")]
pub struct JsDocumentRevision {
    /// Format-specific revision identifier.
    ///
    /// For DOCX this is the `w:id` attribute value on the change element
    /// (e.g. `"42"`). When the attribute is absent a synthetic fallback is
    /// generated (`"docx-ins-0"`, `"docx-del-3"`, …).
    #[napi(js_name = "revisionId")]
    #[serde(rename = "revisionId")]
    pub revision_id: String,
    /// Display name of the author who made this change, when available.
    pub author: Option<String>,
    /// ISO-8601 timestamp of the change, when available.
    ///
    /// Stored as a plain string so this type remains FFI-friendly and
    /// unconditionally available without the `chrono` optional dep.
    /// DOCX populates this from the `w:date` attribute (e.g.
    /// `"2024-03-15T10:30:00Z"`).
    pub timestamp: Option<String>,
    /// Semantic kind of this revision.
    pub kind: JsRevisionKind,
    /// Best-effort document location for this revision.
    ///
    /// Resolution is format-dependent and may be `None` when the location
    /// cannot be determined (e.g. changes inside table cells before
    /// table-cell anchor support is added).
    pub anchor: Option<JsRevisionAnchor>,
    /// The content changes that make up this revision.
    pub delta: JsRevisionDelta,
}

/// The content changes that make up a single revision.
///
/// For insertions and deletions the `content` field carries the added/removed
/// lines as `DiffLine.Added` / `DiffLine.Removed` entries. For format
/// changes, `content` is empty — the property diff is left as a TODO for a
/// later enrichment pass.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "RevisionDelta")]
pub struct JsRevisionDelta {
    /// Line-level content changes for this revision.
    pub content: Option<Vec<JsDiffLine>>,
    /// Cell-level table changes for this revision.
    #[napi(js_name = "tableChanges")]
    #[serde(rename = "tableChanges")]
    pub table_changes: Option<Vec<JsCellChange>>,
}

/// Extracted table structure.
///
/// Represents a table detected and extracted from a document (PDF, image, etc.).
/// Tables are converted to both structured cell data and Markdown format.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "Table")]
pub struct JsTable {
    /// Table cells as a 2D vector (rows × columns)
    pub cells: Option<Vec<Vec<String>>>,
    /// Markdown representation of the table
    pub markdown: Option<String>,
    /// Page number where the table was found (1-indexed)
    #[napi(js_name = "pageNumber")]
    #[serde(rename = "pageNumber")]
    pub page_number: Option<u32>,
    /// Bounding box of the table on the page (PDF coordinates: x0=left, y0=bottom, x1=right, y1=top).
    /// Only populated for PDF-extracted tables when position data is available.
    #[napi(js_name = "boundingBox")]
    #[serde(rename = "boundingBox")]
    pub bounding_box: Option<JsBoundingBox>,
}

/// Individual table cell with content and optional styling.
///
/// Future extension point for rich table support with cell-level metadata.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "TableCell")]
pub struct JsTableCell {
    /// Cell content as text
    pub content: Option<String>,
    /// Row span (number of rows this cell spans)
    #[napi(js_name = "rowSpan")]
    #[serde(rename = "rowSpan")]
    pub row_span: Option<u32>,
    /// Column span (number of columns this cell spans)
    #[napi(js_name = "colSpan")]
    #[serde(rename = "colSpan")]
    pub col_span: Option<u32>,
    /// Whether this is a header cell
    #[napi(js_name = "isHeader")]
    #[serde(rename = "isHeader")]
    pub is_header: Option<bool>,
}

/// A URI extracted from a document.
///
/// Represents any link, reference, or resource pointer found during extraction.
/// The `kind` field classifies the URI semantically, while `label` carries
/// optional human-readable display text.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ExtractedUri")]
pub struct JsExtractedUri {
    /// The URL or path string.
    pub url: String,
    /// Optional display text / label for the link.
    pub label: Option<String>,
    /// Optional page number where the URI was found (1-indexed).
    pub page: Option<u32>,
    /// Semantic classification of the URI.
    pub kind: JsUriKind,
}

/// MIME type detection response.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "DetectResponse")]
pub struct JsDetectResponse {
    /// Detected MIME type
    #[napi(js_name = "mimeType")]
    #[serde(rename = "mimeType")]
    pub mime_type: String,
    /// Original filename (if provided)
    pub filename: Option<String>,
}

/// Options controlling how two `ExtractionResult` values are compared.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "DiffOptions")]
pub struct JsDiffOptions {
    /// Include metadata changes in the diff. Default: `true`.
    #[napi(js_name = "includeMetadata")]
    #[serde(rename = "includeMetadata")]
    pub include_metadata: Option<bool>,
    /// Include embedded-children changes in the diff. Default: `true`.
    #[napi(js_name = "includeEmbedded")]
    #[serde(rename = "includeEmbedded")]
    pub include_embedded: Option<bool>,
    /// Truncate content to this many characters before diffing.
    ///
    /// Useful for very large documents where only the first N characters matter.
    /// `None` means no truncation.
    #[napi(js_name = "maxContentChars")]
    #[serde(rename = "maxContentChars")]
    pub max_content_chars: Option<i64>,
}

#[napi(js_name = "diffOptionsDefault")]
pub fn diff_options_default() -> JsDiffOptions {
    kreuzberg::DiffOptions::default().into()
}

/// The complete diff between two `ExtractionResult` values.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ExtractionDiff")]
pub struct JsExtractionDiff {
    /// Unified-diff hunks for the `content` field.
    ///
    /// Empty when the content is identical.
    #[napi(js_name = "contentDiff")]
    #[serde(rename = "contentDiff")]
    pub content_diff: Vec<JsDiffHunk>,
    /// Tables present in `b` but not in `a` (by index position, excess right-side tables).
    #[napi(js_name = "tablesAdded")]
    #[serde(rename = "tablesAdded")]
    pub tables_added: Vec<JsTable>,
    /// Tables present in `a` but not in `b` (by index position, excess left-side tables).
    #[napi(js_name = "tablesRemoved")]
    #[serde(rename = "tablesRemoved")]
    pub tables_removed: Vec<JsTable>,
    /// Cell-level changes for table pairs that share the same index and dimensions.
    #[napi(js_name = "tablesChanged")]
    #[serde(rename = "tablesChanged")]
    pub tables_changed: Vec<JsTableDiff>,
    /// Metadata difference, encoded as a JSON object with three top-level keys:
    /// `added` (keys present in `b` but not `a`), `removed` (keys present in `a`
    /// but not `b`), and `changed` (keys whose values differ — each entry is
    /// `{ "from": <value-in-a>, "to": <value-in-b> }`).
    ///
    /// This is NOT RFC 6902 JSON Patch — we deliberately chose a flatter shape
    /// to avoid pulling in a json-patch crate. If you need RFC 6902 semantics
    /// (with JSON Pointer paths) feed `a.metadata` and `b.metadata` to your
    /// preferred json-patch impl directly.
    #[napi(js_name = "metadataChanged")]
    #[serde(rename = "metadataChanged")]
    pub metadata_changed: serde_json::Value,
    /// Changes to embedded archive children.
    #[napi(js_name = "embeddedChanges")]
    #[serde(rename = "embeddedChanges")]
    pub embedded_changes: JsEmbeddedChanges,
}

/// A single contiguous hunk in a unified diff.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "DiffHunk")]
pub struct JsDiffHunk {
    /// Starting line number in the old content (0-indexed).
    #[napi(js_name = "fromLine")]
    #[serde(rename = "fromLine")]
    pub from_line: i64,
    /// Number of lines from the old content in this hunk.
    #[napi(js_name = "fromCount")]
    #[serde(rename = "fromCount")]
    pub from_count: i64,
    /// Starting line number in the new content (0-indexed).
    #[napi(js_name = "toLine")]
    #[serde(rename = "toLine")]
    pub to_line: i64,
    /// Number of lines from the new content in this hunk.
    #[napi(js_name = "toCount")]
    #[serde(rename = "toCount")]
    pub to_count: i64,
    /// Lines that make up this hunk.
    pub lines: Vec<JsDiffLine>,
}

/// Cell-level changes for a pair of tables that share the same index.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "TableDiff")]
pub struct JsTableDiff {
    /// Zero-based index of the table in both `a.tables` and `b.tables`.
    #[napi(js_name = "fromIndex")]
    #[serde(rename = "fromIndex")]
    pub from_index: i64,
    /// Zero-based index in `b.tables` (equal to `from_index` for same-dimension tables).
    #[napi(js_name = "toIndex")]
    #[serde(rename = "toIndex")]
    pub to_index: i64,
    /// Cell-level changes within the table.
    #[napi(js_name = "cellChanges")]
    #[serde(rename = "cellChanges")]
    pub cell_changes: Vec<JsCellChange>,
}

/// Changes to embedded archive children between two results.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "EmbeddedChanges")]
pub struct JsEmbeddedChanges {
    /// Children present in `b` but not in `a` (matched by `path`).
    pub added: Vec<JsArchiveEntry>,
    /// Children present in `a` but not in `b` (matched by `path`).
    pub removed: Vec<JsArchiveEntry>,
    /// Children present in both but with differing content (matched by `path`).
    ///
    /// Each entry holds the diff of the nested `ExtractionResult`.
    pub changed: Vec<JsEmbeddedDiff>,
}

/// Diff for a single embedded archive entry that appears in both results.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "EmbeddedDiff")]
pub struct JsEmbeddedDiff {
    /// Archive-relative path identifying this entry.
    pub path: String,
    /// The recursive diff of the entry's extraction result.
    pub diff: JsExtractionDiff,
}

/// Preset configurations for common RAG use cases.
///
/// Each preset combines chunk size, overlap, and embedding model
/// to provide an optimized configuration for specific scenarios.
///
/// All string fields are owned `String` for FFI compatibility — instances
/// are safe to clone and pass across language boundaries.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "EmbeddingPreset")]
pub struct JsEmbeddingPreset {
    pub name: String,
    #[napi(js_name = "chunkSize")]
    #[serde(rename = "chunkSize")]
    pub chunk_size: i64,
    pub overlap: i64,
    /// HuggingFace repository name for the model.
    #[napi(js_name = "modelRepo")]
    #[serde(rename = "modelRepo")]
    pub model_repo: String,
    /// Pooling strategy: "cls" or "mean".
    pub pooling: String,
    /// Path to the ONNX model file within the repo.
    #[napi(js_name = "modelFile")]
    #[serde(rename = "modelFile")]
    pub model_file: String,
    pub dimensions: i64,
    pub description: String,
}

/// YAKE-specific parameters.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "YakeParams")]
pub struct JsYakeParams {
    /// Window size for co-occurrence analysis (default: 2).
    ///
    /// Controls the context window for computing co-occurrence statistics.
    #[napi(js_name = "windowSize")]
    #[serde(rename = "windowSize")]
    pub window_size: Option<i64>,
}

#[napi(js_name = "yakeParamsDefault")]
pub fn yake_params_default() -> JsYakeParams {
    kreuzberg::YakeParams::default().into()
}

/// RAKE-specific parameters.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "RakeParams")]
pub struct JsRakeParams {
    /// Minimum word length to consider (default: 1).
    #[napi(js_name = "minWordLength")]
    #[serde(rename = "minWordLength")]
    pub min_word_length: Option<i64>,
    /// Maximum words in a keyword phrase (default: 3).
    #[napi(js_name = "maxWordsPerPhrase")]
    #[serde(rename = "maxWordsPerPhrase")]
    pub max_words_per_phrase: Option<i64>,
}

#[napi(js_name = "rakeParamsDefault")]
pub fn rake_params_default() -> JsRakeParams {
    kreuzberg::RakeParams::default().into()
}

/// Keyword extraction configuration.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "KeywordConfig")]
pub struct JsKeywordConfig {
    /// Algorithm to use for extraction.
    pub algorithm: Option<JsKeywordAlgorithm>,
    /// Maximum number of keywords to extract (default: 10).
    #[napi(js_name = "maxKeywords")]
    #[serde(rename = "maxKeywords")]
    pub max_keywords: Option<i64>,
    /// Minimum score threshold (0.0-1.0, default: 0.0).
    ///
    /// Keywords with scores below this threshold are filtered out.
    /// Note: Score ranges differ between algorithms.
    #[napi(js_name = "minScore")]
    #[serde(rename = "minScore")]
    pub min_score: Option<f64>,
    /// N-gram range for keyword extraction (min, max).
    ///
    /// (1, 1) = unigrams only
    /// (1, 2) = unigrams and bigrams
    /// (1, 3) = unigrams, bigrams, and trigrams (default)
    #[napi(js_name = "ngramRange")]
    #[serde(rename = "ngramRange")]
    pub ngram_range: Option<Vec<i64>>,
    /// Language code for stopword filtering (e.g., "en", "de", "fr").
    ///
    /// If None, no stopword filtering is applied.
    pub language: Option<String>,
    /// YAKE-specific tuning parameters.
    #[napi(js_name = "yakeParams")]
    #[serde(rename = "yakeParams")]
    #[serde(skip)]
    pub yake_params: Option<JsYakeParams>,
    /// RAKE-specific tuning parameters.
    #[napi(js_name = "rakeParams")]
    #[serde(rename = "rakeParams")]
    #[serde(skip)]
    pub rake_params: Option<JsRakeParams>,
}

#[napi(js_name = "keywordConfigDefault")]
pub fn keyword_config_default() -> JsKeywordConfig {
    kreuzberg::KeywordConfig::default().into()
}

/// Extracted keyword with metadata.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "Keyword")]
pub struct JsKeyword {
    /// The keyword text.
    pub text: String,
    /// Relevance score (higher is better, algorithm-specific range).
    pub score: f64,
    /// Algorithm that extracted this keyword.
    pub algorithm: JsKeywordAlgorithm,
    /// Optional positions where keyword appears in text (character offsets).
    pub positions: Option<Vec<i64>>,
}

/// Configuration for PaddleOCR backend.
///
/// Configures PaddleOCR text detection and recognition with multi-language support.
/// Uses a builder pattern for convenient configuration.
///
/// # Examples
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "PaddleOcrConfig")]
pub struct JsPaddleOcrConfig {
    /// Language code (e.g., "en", "ch", "jpn", "kor", "deu", "fra")
    pub language: Option<String>,
    /// Optional custom cache directory for model files
    #[napi(js_name = "cacheDir")]
    #[serde(rename = "cacheDir")]
    pub cache_dir: Option<String>,
    /// Enable angle classification for rotated text (default: false).
    /// Can misfire on short text regions, rotating crops incorrectly before recognition.
    #[napi(js_name = "useAngleCls")]
    #[serde(rename = "useAngleCls")]
    pub use_angle_cls: Option<bool>,
    /// Enable table structure detection (default: false)
    #[napi(js_name = "enableTableDetection")]
    #[serde(rename = "enableTableDetection")]
    pub enable_table_detection: Option<bool>,
    /// Database threshold for text detection (default: 0.3)
    /// Range: 0.0-1.0, higher values require more confident detections
    #[napi(js_name = "detDbThresh")]
    #[serde(rename = "detDbThresh")]
    pub det_db_thresh: Option<f64>,
    /// Box threshold for text bounding box refinement (default: 0.5)
    /// Range: 0.0-1.0
    #[napi(js_name = "detDbBoxThresh")]
    #[serde(rename = "detDbBoxThresh")]
    pub det_db_box_thresh: Option<f64>,
    /// Unclip ratio for expanding text bounding boxes (default: 1.6)
    /// Controls the expansion of detected text regions
    #[napi(js_name = "detDbUnclipRatio")]
    #[serde(rename = "detDbUnclipRatio")]
    pub det_db_unclip_ratio: Option<f64>,
    /// Maximum side length for detection image (default: 960)
    /// Larger images may be resized to this limit for faster inference
    #[napi(js_name = "detLimitSideLen")]
    #[serde(rename = "detLimitSideLen")]
    pub det_limit_side_len: Option<u32>,
    /// Batch size for recognition inference (default: 6)
    /// Number of text regions to process simultaneously
    #[napi(js_name = "recBatchNum")]
    #[serde(rename = "recBatchNum")]
    pub rec_batch_num: Option<u32>,
    /// Padding in pixels added around the image before detection (default: 10).
    /// Large values can include surrounding content like table gridlines.
    pub padding: Option<u32>,
    /// Minimum recognition confidence score for text lines (default: 0.5).
    /// Text regions with recognition confidence below this threshold are discarded.
    /// Matches PaddleOCR Python's `drop_score` parameter.
    /// Range: 0.0-1.0
    #[napi(js_name = "dropScore")]
    #[serde(rename = "dropScore")]
    pub drop_score: Option<f64>,
    /// Model tier controlling detection/recognition model size and accuracy trade-off.
    /// - `"mobile"` (default): Lightweight models (~4.5MB detection, ~16.5MB recognition), fast download and inference
    /// - `"server"`: Large, high-accuracy models (~88MB detection, ~84MB recognition), best for GPU or complex documents
    #[napi(js_name = "modelTier")]
    #[serde(rename = "modelTier")]
    pub model_tier: Option<String>,
}

/// Sets a custom cache directory for model files.
///
/// # Arguments
///
/// * `path` - Path to cache directory
///
/// # Examples
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "paddleOcrConfigWithCacheDir")]
pub fn paddle_ocr_config_with_cache_dir(cfg: JsPaddleOcrConfig, path: String) -> napi::Result<JsPaddleOcrConfig> {
    let _json =
        serde_json::to_string(&cfg).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _core: kreuzberg::PaddleOcrConfig =
        serde_json::from_str(&_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _result = _core.with_cache_dir(std::path::PathBuf::from(path));
    let _out_json =
        serde_json::to_string(&_result).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    serde_json::from_str(&_out_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// Enables or disables table structure detection.
///
/// # Arguments
///
/// * `enable` - Whether to enable table detection
///
/// # Examples
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "paddleOcrConfigWithTableDetection")]
pub fn paddle_ocr_config_with_table_detection(cfg: JsPaddleOcrConfig, enable: bool) -> napi::Result<JsPaddleOcrConfig> {
    let _json =
        serde_json::to_string(&cfg).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _core: kreuzberg::PaddleOcrConfig =
        serde_json::from_str(&_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _result = _core.with_table_detection(enable);
    let _out_json =
        serde_json::to_string(&_result).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    serde_json::from_str(&_out_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// Enables or disables angle classification for rotated text.
///
/// # Arguments
///
/// * `enable` - Whether to enable angle classification
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "paddleOcrConfigWithAngleCls")]
pub fn paddle_ocr_config_with_angle_cls(cfg: JsPaddleOcrConfig, enable: bool) -> napi::Result<JsPaddleOcrConfig> {
    let _json =
        serde_json::to_string(&cfg).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _core: kreuzberg::PaddleOcrConfig =
        serde_json::from_str(&_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _result = _core.with_angle_cls(enable);
    let _out_json =
        serde_json::to_string(&_result).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    serde_json::from_str(&_out_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// Sets the database threshold for text detection.
///
/// # Arguments
///
/// * `threshold` - Detection threshold (0.0-1.0)
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "paddleOcrConfigWithDetDbThresh")]
pub fn paddle_ocr_config_with_det_db_thresh(cfg: JsPaddleOcrConfig, threshold: f64) -> napi::Result<JsPaddleOcrConfig> {
    let _json =
        serde_json::to_string(&cfg).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _core: kreuzberg::PaddleOcrConfig =
        serde_json::from_str(&_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _result = _core.with_det_db_thresh(threshold as f32);
    let _out_json =
        serde_json::to_string(&_result).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    serde_json::from_str(&_out_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// Sets the box threshold for text bounding box refinement.
///
/// # Arguments
///
/// * `threshold` - Box threshold (0.0-1.0)
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "paddleOcrConfigWithDetDbBoxThresh")]
pub fn paddle_ocr_config_with_det_db_box_thresh(
    cfg: JsPaddleOcrConfig,
    threshold: f64,
) -> napi::Result<JsPaddleOcrConfig> {
    let _json =
        serde_json::to_string(&cfg).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _core: kreuzberg::PaddleOcrConfig =
        serde_json::from_str(&_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _result = _core.with_det_db_box_thresh(threshold as f32);
    let _out_json =
        serde_json::to_string(&_result).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    serde_json::from_str(&_out_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// Sets the unclip ratio for expanding text bounding boxes.
///
/// # Arguments
///
/// * `ratio` - Unclip ratio (typically 1.5-2.0)
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "paddleOcrConfigWithDetDbUnclipRatio")]
pub fn paddle_ocr_config_with_det_db_unclip_ratio(
    cfg: JsPaddleOcrConfig,
    ratio: f64,
) -> napi::Result<JsPaddleOcrConfig> {
    let _json =
        serde_json::to_string(&cfg).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _core: kreuzberg::PaddleOcrConfig =
        serde_json::from_str(&_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _result = _core.with_det_db_unclip_ratio(ratio as f32);
    let _out_json =
        serde_json::to_string(&_result).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    serde_json::from_str(&_out_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// Sets the maximum side length for detection images.
///
/// # Arguments
///
/// * `length` - Maximum side length in pixels
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "paddleOcrConfigWithDetLimitSideLen")]
pub fn paddle_ocr_config_with_det_limit_side_len(
    cfg: JsPaddleOcrConfig,
    length: u32,
) -> napi::Result<JsPaddleOcrConfig> {
    let _json =
        serde_json::to_string(&cfg).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _core: kreuzberg::PaddleOcrConfig =
        serde_json::from_str(&_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _result = _core.with_det_limit_side_len(length);
    let _out_json =
        serde_json::to_string(&_result).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    serde_json::from_str(&_out_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// Sets the batch size for recognition inference.
///
/// # Arguments
///
/// * `batch_size` - Number of text regions to process simultaneously
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "paddleOcrConfigWithRecBatchNum")]
pub fn paddle_ocr_config_with_rec_batch_num(
    cfg: JsPaddleOcrConfig,
    batch_size: u32,
) -> napi::Result<JsPaddleOcrConfig> {
    let _json =
        serde_json::to_string(&cfg).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _core: kreuzberg::PaddleOcrConfig =
        serde_json::from_str(&_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _result = _core.with_rec_batch_num(batch_size);
    let _out_json =
        serde_json::to_string(&_result).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    serde_json::from_str(&_out_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// Sets the minimum recognition confidence threshold.
///
/// # Arguments
///
/// * `score` - Minimum confidence (0.0-1.0), text below this is dropped
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "paddleOcrConfigWithDropScore")]
pub fn paddle_ocr_config_with_drop_score(cfg: JsPaddleOcrConfig, score: f64) -> napi::Result<JsPaddleOcrConfig> {
    let _json =
        serde_json::to_string(&cfg).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _core: kreuzberg::PaddleOcrConfig =
        serde_json::from_str(&_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _result = _core.with_drop_score(score as f32);
    let _out_json =
        serde_json::to_string(&_result).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    serde_json::from_str(&_out_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// Sets padding in pixels added around images before detection.
///
/// # Arguments
///
/// * `padding` - Padding in pixels (0-100)
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "paddleOcrConfigWithPadding")]
pub fn paddle_ocr_config_with_padding(cfg: JsPaddleOcrConfig, padding: u32) -> napi::Result<JsPaddleOcrConfig> {
    let _json =
        serde_json::to_string(&cfg).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _core: kreuzberg::PaddleOcrConfig =
        serde_json::from_str(&_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _result = _core.with_padding(padding);
    let _out_json =
        serde_json::to_string(&_result).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    serde_json::from_str(&_out_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// Sets the model tier controlling detection/recognition model size.
///
/// # Arguments
///
/// * `tier` - `"mobile"` (default, lightweight, faster) or `"server"` (high accuracy, GPU/complex documents)
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "paddleOcrConfigWithModelTier")]
pub fn paddle_ocr_config_with_model_tier(cfg: JsPaddleOcrConfig, tier: String) -> napi::Result<JsPaddleOcrConfig> {
    let _json =
        serde_json::to_string(&cfg).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _core: kreuzberg::PaddleOcrConfig =
        serde_json::from_str(&_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    let _result = _core.with_model_tier(tier);
    let _out_json =
        serde_json::to_string(&_result).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    serde_json::from_str(&_out_json).map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// Creates a default configuration with English language support.
#[napi(js_name = "paddleOcrConfigDefault")]
pub fn paddle_ocr_config_default() -> JsPaddleOcrConfig {
    kreuzberg::PaddleOcrConfig::default().into()
}

/// Combined paths to all models needed for OCR (backward compatibility).
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ModelPaths")]
pub struct JsModelPaths {
    /// Path to the detection model directory.
    #[napi(js_name = "detModel")]
    #[serde(rename = "detModel")]
    pub det_model: String,
    /// Path to the classification model directory.
    #[napi(js_name = "clsModel")]
    #[serde(rename = "clsModel")]
    pub cls_model: String,
    /// Path to the recognition model directory.
    #[napi(js_name = "recModel")]
    #[serde(rename = "recModel")]
    pub rec_model: String,
    /// Path to the character dictionary file.
    #[napi(js_name = "dictFile")]
    #[serde(rename = "dictFile")]
    pub dict_file: String,
}

/// Document orientation detection result.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "OrientationResult")]
pub struct JsOrientationResult {
    /// Detected orientation in degrees (0, 90, 180, or 270).
    pub degrees: u32,
    /// Confidence score (0.0-1.0).
    pub confidence: f64,
}

/// Bounding box in original image coordinates (x1, y1) top-left, (x2, y2) bottom-right.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "BBox")]
pub struct JsBBox {
    pub x1: f64,
    pub y1: f64,
    pub x2: f64,
    pub y2: f64,
}

/// A single layout detection result.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "LayoutDetection")]
pub struct JsLayoutDetection {
    #[napi(js_name = "className")]
    #[serde(rename = "className")]
    pub class_name: JsLayoutClass,
    pub confidence: f64,
    pub bbox: JsBBox,
}

/// Pre-computed table markdown for a table detection region.
///
/// Produced by the TATR-based table structure recognizer and surfaced as part of
/// layout-aware OCR results.  The struct lives here (under `layout-types`, pure-Rust)
/// so that consumers who do not enable `layout-detection` (ORT) can still reference
/// the type in their own code.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "RecognizedTable")]
pub struct JsRecognizedTable {
    /// Detection bbox that this table corresponds to (for matching).
    #[napi(js_name = "detectionBbox")]
    #[serde(rename = "detectionBbox")]
    pub detection_bbox: JsBBox,
    /// Table cells as a 2D vector (rows × columns).
    pub cells: Vec<Vec<String>>,
    /// Rendered markdown table.
    pub markdown: String,
}

/// Page-level detection result containing all detections and page metadata.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "DetectionResult")]
pub struct JsDetectionResult {
    #[napi(js_name = "pageWidth")]
    #[serde(rename = "pageWidth")]
    pub page_width: u32,
    #[napi(js_name = "pageHeight")]
    #[serde(rename = "pageHeight")]
    pub page_height: u32,
    pub detections: Vec<JsLayoutDetection>,
}

/// Embedded file descriptor extracted from the PDF name tree.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "EmbeddedFile")]
pub struct JsEmbeddedFile {
    /// The filename as stored in the PDF name tree.
    pub name: String,
    /// Raw file bytes from the embedded stream (already decompressed by lopdf).
    #[napi(ts_type = "Uint8Array | Buffer | Array<number>")]
    pub data: JsBytes,
    /// Compressed byte count of the original stream (before decompression).
    ///
    /// Used by callers to compute the decompression ratio and detect zip-bomb-style
    /// attacks that embed a tiny compressed stream expanding to gigabytes of data.
    #[napi(js_name = "compressedSize")]
    #[serde(rename = "compressedSize")]
    pub compressed_size: i64,
    /// MIME type if specified in the filespec, otherwise `None`.
    #[napi(js_name = "mimeType")]
    #[serde(rename = "mimeType")]
    pub mime_type: Option<String>,
}

/// PDF-specific metadata.
///
/// Contains metadata fields specific to PDF documents that are not in the common
/// `Metadata` structure. Common fields like title, authors, keywords, and dates
/// are at the `Metadata` level.
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "PdfMetadata")]
pub struct JsPdfMetadata {
    /// PDF version (e.g., "1.7", "2.0")
    #[napi(js_name = "pdfVersion")]
    #[serde(rename = "pdfVersion")]
    pub pdf_version: Option<String>,
    /// PDF producer (application that created the PDF)
    pub producer: Option<String>,
    /// Whether the PDF is encrypted/password-protected
    #[napi(js_name = "isEncrypted")]
    #[serde(rename = "isEncrypted")]
    pub is_encrypted: Option<bool>,
    /// First page width in points (1/72 inch)
    pub width: Option<i64>,
    /// First page height in points (1/72 inch)
    pub height: Option<i64>,
    /// Total number of pages in the PDF document
    #[napi(js_name = "pageCount")]
    #[serde(rename = "pageCount")]
    pub page_count: Option<u32>,
}

/// ONNX Runtime execution provider type.
///
/// Determines which hardware backend is used for model inference.
/// `Auto` (default) selects the best available provider per platform.
#[napi(string_enum = "lowercase", js_name = "ExecutionProviderType")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsExecutionProviderType {
    /// Auto-select: CoreML on macOS, CUDA on Linux, CPU elsewhere.
    Auto,
    /// CPU execution provider (always available).
    Cpu,
    /// Apple CoreML (macOS/iOS Neural Engine + GPU).
    CoreMl,
    /// NVIDIA CUDA GPU acceleration.
    Cuda,
    /// NVIDIA TensorRT (optimized CUDA inference).
    TensorRt,
}

#[allow(clippy::derivable_impls)]
impl Default for JsExecutionProviderType {
    fn default() -> Self {
        Self::Auto
    }
}

/// Output format for extraction results.
///
/// Controls the format of the `content` field in `ExtractionResult`.
/// When set to `Markdown`, `Djot`, or `Html`, the output uses that format.
/// `Plain` returns the raw extracted text.
/// `Structured` returns JSON with full OCR element data including bounding
/// boxes and confidence scores.
#[napi(string_enum = "lowercase", js_name = "OutputFormat")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsOutputFormat {
    /// Plain text content only (default)
    Plain,
    /// Markdown format
    Markdown,
    /// Djot markup format
    Djot,
    /// HTML format
    Html,
    /// JSON tree format with heading-driven sections.
    Json,
    /// Structured JSON format with full OCR element metadata.
    Structured,
    /// Custom renderer registered via the RendererRegistry.
    /// The string is the renderer name (e.g., "docx", "latex").
    Custom,
}

#[allow(clippy::derivable_impls)]
impl Default for JsOutputFormat {
    fn default() -> Self {
        Self::Plain
    }
}

/// Built-in HTML theme selection.
#[napi(string_enum = "lowercase", js_name = "HtmlTheme")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsHtmlTheme {
    /// Sensible defaults: system font stack, neutral colours, readable line
    /// measure. CSS custom properties (`--kb-*`) are all defined so user CSS
    /// can override individual values.
    Default,
    /// GitHub Markdown-inspired palette and spacing.
    GitHub,
    /// Dark background, light text.
    Dark,
    /// Minimal light theme with generous whitespace.
    Light,
    /// No built-in stylesheet emitted. CSS custom properties are still defined
    /// on `:root` so user stylesheets can reference `var(--kb-*)` tokens.
    Unstyled,
}

#[allow(clippy::derivable_impls)]
impl Default for JsHtmlTheme {
    fn default() -> Self {
        Self::Default
    }
}

/// Which table structure recognition model to use.
///
/// Controls the model used for table cell detection within layout-detected
/// table regions. Wire format is snake_case in all serializers (JSON, TOML,
/// YAML).
#[napi(string_enum = "snake_case", js_name = "TableModel")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsTableModel {
    /// TATR (Table Transformer) -- default, 30MB, DETR-based row/column detection.
    Tatr,
    /// SLANeXT wired variant -- 365MB, optimized for bordered tables.
    SlanetWired,
    /// SLANeXT wireless variant -- 365MB, optimized for borderless tables.
    SlanetWireless,
    /// SLANet-plus -- 7.78MB, lightweight general-purpose.
    SlanetPlus,
    /// Classifier-routed SLANeXT: auto-select wired/wireless per table.
    /// Uses PP-LCNet classifier (6.78MB) + both SLANeXT variants (730MB total).
    SlanetAuto,
    /// Disable table structure model inference entirely; use heuristic path only.
    Disabled,
}

#[allow(clippy::derivable_impls)]
impl Default for JsTableModel {
    fn default() -> Self {
        Self::Tatr
    }
}

/// Type of text chunker to use.
///
/// # Variants
///
/// * `Text` - Generic text splitter, splits on whitespace and punctuation
/// * `Markdown` - Markdown-aware splitter, preserves formatting and structure
/// * `Yaml` - YAML-aware splitter, creates one chunk per top-level key
/// * `Semantic` - Topic-aware chunker. With an `EmbeddingConfig`, splits at
///   embedding-based topic shifts tuned by `topic_threshold` (default 0.75,
///   lower = more splits). Without an embedding, falls back to a
///   structural-boundary heuristic (ALL-CAPS headers, numbered sections,
///   blank-line paragraphs) and merges groups into chunks capped at
///   `max_characters` (default 1000). `topic_threshold` has no effect in the
///   fallback path. For best results, pair with an embedding model.
#[napi(string_enum = "lowercase", js_name = "ChunkerType")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsChunkerType {
    Text,
    Markdown,
    Yaml,
    Semantic,
}

#[allow(clippy::derivable_impls)]
impl Default for JsChunkerType {
    fn default() -> Self {
        Self::Text
    }
}

/// How chunk size is measured.
///
/// Defaults to `Characters` (Unicode character count). When using token-based sizing,
/// chunks are sized by token count according to the specified tokenizer.
///
/// Token-based sizing uses HuggingFace tokenizers loaded at runtime. Any tokenizer
/// available on HuggingFace Hub can be used, including OpenAI-compatible tokenizers
/// (e.g., `Xenova/gpt-4o`, `Xenova/cl100k_base`).
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "ChunkSizing")]
pub struct JsChunkSizing {
    #[napi(js_name = "type")]
    #[serde(rename = "type")]
    pub type_tag: String,
    pub model: Option<String>,
    #[napi(js_name = "cacheDir")]
    #[serde(rename = "cacheDir")]
    pub cache_dir: Option<String>,
}

#[allow(clippy::derivable_impls)]
impl Default for JsChunkSizing {
    fn default() -> Self {
        Self {
            type_tag: String::new(),
            cache_dir: None,
            model: None,
        }
    }
}

/// Embedding model types supported by Kreuzberg.
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "EmbeddingModelType")]
pub struct JsEmbeddingModelType {
    #[napi(js_name = "type")]
    #[serde(rename = "type")]
    pub type_tag: String,
    pub name: Option<String>,
    #[napi(js_name = "modelId")]
    #[serde(rename = "modelId")]
    pub model_id: Option<String>,
    pub dimensions: Option<i64>,
    pub llm: Option<JsLlmConfig>,
}

#[allow(clippy::derivable_impls)]
impl Default for JsEmbeddingModelType {
    fn default() -> Self {
        Self {
            type_tag: String::new(),
            dimensions: None,
            llm: None,
            model_id: None,
            name: None,
        }
    }
}

/// Content rendering mode for code extraction.
///
/// Controls how extracted code content is represented in the `content` field
/// of `ExtractionResult`.
#[napi(string_enum = "snake_case", js_name = "CodeContentMode")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsCodeContentMode {
    /// Use TSLP semantic chunks as content (default).
    Chunks,
    /// Use raw source code as content.
    Raw,
    /// Emit function/class headings + docstrings (no code bodies).
    Structure,
}

#[allow(clippy::derivable_impls)]
impl Default for JsCodeContentMode {
    fn default() -> Self {
        Self::Chunks
    }
}

/// Type of list detection.
#[napi(string_enum, js_name = "ListType")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsListType {
    /// Bullet points (-, *, •, etc.)
    Bullet,
    /// Numbered lists (1., 2., etc.)
    Numbered,
    /// Lettered lists (a., b., A., B., etc.)
    Lettered,
    /// Indented items
    Indented,
}

#[allow(clippy::derivable_impls)]
impl Default for JsListType {
    fn default() -> Self {
        Self::Bullet
    }
}

/// OCR backend types.
#[napi(string_enum, js_name = "OcrBackendType")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsOcrBackendType {
    /// Tesseract OCR (native Rust binding)
    Tesseract,
    /// EasyOCR (Python-based, via FFI)
    EasyOCR,
    /// PaddleOCR (Python-based, via FFI)
    PaddleOCR,
    /// Custom/third-party OCR backend
    Custom,
}

#[allow(clippy::derivable_impls)]
impl Default for JsOcrBackendType {
    fn default() -> Self {
        Self::Tesseract
    }
}

/// Processing stages for post-processors.
///
/// Post-processors are executed in stage order (Early → Middle → Late).
/// Use stages to control the order of post-processing operations.
#[napi(string_enum, js_name = "ProcessingStage")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsProcessingStage {
    /// Early stage - foundational processing.
    ///
    /// Use for:
    /// - Language detection
    /// - Character encoding normalization
    /// - Entity extraction (NER)
    /// - Text quality scoring
    Early,
    /// Middle stage - content transformation.
    ///
    /// Use for:
    /// - Keyword extraction
    /// - Token reduction
    /// - Text summarization
    /// - Semantic analysis
    Middle,
    /// Late stage - final enrichment.
    ///
    /// Use for:
    /// - Custom user hooks
    /// - Analytics/logging
    /// - Final validation
    /// - Output formatting
    Late,
}

#[allow(clippy::derivable_impls)]
impl Default for JsProcessingStage {
    fn default() -> Self {
        Self::Early
    }
}

#[napi(string_enum, js_name = "ReductionLevel")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsReductionLevel {
    Off,
    Light,
    Moderate,
    Aggressive,
    Maximum,
}

#[allow(clippy::derivable_impls)]
impl Default for JsReductionLevel {
    fn default() -> Self {
        Self::Off
    }
}

/// Type of PDF annotation.
#[napi(string_enum = "snake_case", js_name = "PdfAnnotationType")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsPdfAnnotationType {
    /// Sticky note / text annotation
    Text,
    /// Highlighted text region
    Highlight,
    /// Hyperlink annotation
    Link,
    /// Rubber stamp annotation
    Stamp,
    /// Underline text markup
    Underline,
    /// Strikeout text markup
    StrikeOut,
    /// Any other annotation type
    Other,
}

#[allow(clippy::derivable_impls)]
impl Default for JsPdfAnnotationType {
    fn default() -> Self {
        Self::Text
    }
}

/// Types of block-level elements in Djot.
#[napi(string_enum = "snake_case", js_name = "BlockType")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsBlockType {
    Paragraph,
    Heading,
    Blockquote,
    CodeBlock,
    ListItem,
    OrderedList,
    BulletList,
    TaskList,
    DefinitionList,
    DefinitionTerm,
    DefinitionDescription,
    Div,
    Section,
    ThematicBreak,
    RawBlock,
    MathDisplay,
}

#[allow(clippy::derivable_impls)]
impl Default for JsBlockType {
    fn default() -> Self {
        Self::Paragraph
    }
}

/// Types of inline elements in Djot.
#[napi(string_enum = "snake_case", js_name = "InlineType")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsInlineType {
    Text,
    Strong,
    Emphasis,
    Highlight,
    Subscript,
    Superscript,
    Insert,
    Delete,
    Code,
    Link,
    Image,
    Span,
    Math,
    RawInline,
    FootnoteRef,
    Symbol,
}

#[allow(clippy::derivable_impls)]
impl Default for JsInlineType {
    fn default() -> Self {
        Self::Text
    }
}

/// Semantic kind of a relationship between document elements.
#[napi(string_enum = "snake_case", js_name = "RelationshipKind")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsRelationshipKind {
    /// Footnote marker -> footnote definition.
    FootnoteReference,
    /// Citation marker -> bibliography entry.
    CitationReference,
    /// Internal anchor link (`#id`) -> target heading/element.
    InternalLink,
    /// Caption paragraph -> figure/table it describes.
    Caption,
    /// Label -> labeled element (HTML `<label for>`, LaTeX `\label{}`).
    Label,
    /// TOC entry -> target section.
    TocEntry,
    /// Cross-reference (LaTeX `\ref{}`, DOCX cross-reference field).
    CrossReference,
}

#[allow(clippy::derivable_impls)]
impl Default for JsRelationshipKind {
    fn default() -> Self {
        Self::FootnoteReference
    }
}

/// Content layer classification for document nodes.
///
/// Replaces separate body/furniture arrays with per-node granularity.
#[napi(string_enum = "snake_case", js_name = "ContentLayer")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsContentLayer {
    /// Main document body content.
    Body,
    /// Page/section header (running header).
    Header,
    /// Page/section footer (running footer).
    Footer,
    /// Footnote content.
    Footnote,
}

#[allow(clippy::derivable_impls)]
impl Default for JsContentLayer {
    fn default() -> Self {
        Self::Body
    }
}

/// Tagged enum for node content. Each variant carries only type-specific data.
///
/// Uses `#[serde(tag = "node_type")]` to avoid "type" keyword collision in
/// Go/Java/TypeScript bindings.
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "NodeContent")]
pub struct JsNodeContent {
    #[napi(js_name = "node_type")]
    #[serde(rename = "node_type")]
    pub node_type_tag: String,
    pub text: Option<String>,
    pub level: Option<u8>,
    pub ordered: Option<bool>,
    pub grid: Option<JsTableGrid>,
    pub description: Option<String>,
    #[napi(js_name = "imageIndex")]
    #[serde(rename = "imageIndex")]
    pub image_index: Option<u32>,
    pub src: Option<String>,
    pub language: Option<String>,
    pub label: Option<String>,
    #[napi(js_name = "headingLevel")]
    #[serde(rename = "headingLevel")]
    pub heading_level: Option<u8>,
    #[napi(js_name = "headingText")]
    #[serde(rename = "headingText")]
    pub heading_text: Option<String>,
    pub number: Option<u32>,
    pub title: Option<String>,
    pub term: Option<String>,
    pub definition: Option<String>,
    pub key: Option<String>,
    pub kind: Option<String>,
    pub format: Option<String>,
    pub content: Option<String>,
    pub entries: Option<Vec<Vec<String>>>,
}

#[allow(clippy::derivable_impls)]
impl Default for JsNodeContent {
    fn default() -> Self {
        Self {
            node_type_tag: String::new(),
            content: None,
            definition: None,
            description: None,
            entries: None,
            format: None,
            grid: None,
            heading_level: None,
            heading_text: None,
            image_index: None,
            key: None,
            kind: None,
            label: None,
            language: None,
            level: None,
            number: None,
            ordered: None,
            src: None,
            term: None,
            text: None,
            title: None,
        }
    }
}

/// Types of inline text annotations.
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "AnnotationKind")]
pub struct JsAnnotationKind {
    #[napi(js_name = "annotation_type")]
    #[serde(rename = "annotation_type")]
    pub annotation_type_tag: String,
    pub url: Option<String>,
    pub title: Option<String>,
    pub value: Option<String>,
    pub name: Option<String>,
}

#[allow(clippy::derivable_impls)]
impl Default for JsAnnotationKind {
    fn default() -> Self {
        Self {
            annotation_type_tag: String::new(),
            name: None,
            title: None,
            url: None,
            value: None,
        }
    }
}

/// How the extracted text was produced.
#[napi(string_enum = "snake_case", js_name = "ExtractionMethod")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsExtractionMethod {
    Native,
    Ocr,
    Mixed,
}

#[allow(clippy::derivable_impls)]
impl Default for JsExtractionMethod {
    fn default() -> Self {
        Self::Native
    }
}

/// Semantic structural classification of a text chunk.
///
/// Assigned by the heuristic classifier in `chunking.classifier`.
/// Defaults to `Unknown` when no rule matches.
/// Designed to be extended in future versions without breaking changes.
#[napi(string_enum = "snake_case", js_name = "ChunkType")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsChunkType {
    /// Section heading or document title.
    Heading,
    /// Party list: names, addresses, and signatories.
    PartyList,
    /// Definition clause ("X means…", "X shall mean…").
    Definitions,
    /// Operative clause containing legal/contractual action verbs.
    OperativeClause,
    /// Signature block with signatures, names, and dates.
    SignatureBlock,
    /// Schedule, annex, appendix, or exhibit section.
    Schedule,
    /// Table-like content with aligned columns or repeated patterns.
    TableLike,
    /// Mathematical formula or equation.
    Formula,
    /// Code block or preformatted content.
    CodeBlock,
    /// Embedded or referenced image content.
    Image,
    /// Organizational chart or hierarchy diagram.
    OrgChart,
    /// Diagram, figure, or visual illustration.
    Diagram,
    /// Unclassified or mixed content.
    Unknown,
}

#[allow(clippy::derivable_impls)]
impl Default for JsChunkType {
    fn default() -> Self {
        Self::Heading
    }
}

/// Heuristic classification of what an image likely depicts.
#[napi(string_enum = "snake_case", js_name = "ImageKind")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsImageKind {
    /// Photographic image (natural scene, photograph)
    Photograph,
    /// Technical or schematic diagram
    Diagram,
    /// Chart, graph, or plot
    Chart,
    /// Freehand or technical drawing
    Drawing,
    /// Text-heavy image (scanned text, document)
    TextBlock,
    /// Decorative element or border
    Decoration,
    /// Logo or brand mark
    Logo,
    /// Small icon
    Icon,
    /// Fragment of a larger tiled image (tile of a technical drawing)
    TileFragment,
    /// Mask or transparency map
    Mask,
    /// Full-page render produced during OCR preprocessing; used as a citation thumbnail.
    PageRaster,
    /// Could not classify with reasonable confidence
    Unknown,
}

#[allow(clippy::derivable_impls)]
impl Default for JsImageKind {
    fn default() -> Self {
        Self::Photograph
    }
}

/// Result-shape selection for extraction results.
///
/// Distinct from `OutputFormat` (which controls rendering — Plain, Markdown,
/// HTML, etc.). `ResultFormat` controls the *shape* of the result: a unified content
/// blob vs. an element-based decomposition.
#[napi(string_enum = "snake_case", js_name = "ResultFormat")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsResultFormat {
    /// Unified format with all content in `content` field
    Unified,
    /// Element-based format with semantic element extraction
    ElementBased,
}

#[allow(clippy::derivable_impls)]
impl Default for JsResultFormat {
    fn default() -> Self {
        Self::Unified
    }
}

/// Semantic element type classification.
///
/// Categorizes text content into semantic units for downstream processing.
/// Supports the element types commonly found in Unstructured documents.
#[napi(string_enum = "snake_case", js_name = "ElementType")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsElementType {
    /// Document title
    Title,
    /// Main narrative text body
    NarrativeText,
    /// Section heading
    Heading,
    /// List item (bullet, numbered, etc.)
    ListItem,
    /// Table element
    Table,
    /// Image element
    Image,
    /// Page break marker
    PageBreak,
    /// Code block
    CodeBlock,
    /// Block quote
    BlockQuote,
    /// Footer text
    Footer,
    /// Header text
    Header,
}

#[allow(clippy::derivable_impls)]
impl Default for JsElementType {
    fn default() -> Self {
        Self::Title
    }
}

/// Format-specific metadata (discriminated union).
///
/// Only one format type can exist per extraction result. This provides
/// type-safe, clean metadata without nested optionals.
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "FormatMetadata")]
pub struct JsFormatMetadata {
    #[napi(js_name = "format_type")]
    #[serde(rename = "format_type")]
    pub format_type_tag: String,
    pub code: Option<String>,
    pub pdf: Option<JsPdfMetadata>,
    pub docx: Option<JsDocxMetadata>,
    pub excel: Option<JsExcelMetadata>,
    pub email: Option<JsEmailMetadata>,
    pub pptx: Option<JsPptxMetadata>,
    pub archive: Option<JsArchiveMetadata>,
    pub image: Option<JsImageMetadata>,
    pub xml: Option<JsXmlMetadata>,
    pub text: Option<JsTextMetadata>,
    pub html: Option<JsHtmlMetadata>,
    pub ocr: Option<JsOcrMetadata>,
    pub csv: Option<JsCsvMetadata>,
    pub bibtex: Option<JsBibtexMetadata>,
    pub citation: Option<JsCitationMetadata>,
    #[napi(js_name = "fictionBook")]
    #[serde(rename = "fictionBook")]
    pub fiction_book: Option<JsFictionBookMetadata>,
    pub dbf: Option<JsDbfMetadata>,
    pub jats: Option<JsJatsMetadata>,
    pub epub: Option<JsEpubMetadata>,
    pub pst: Option<JsPstMetadata>,
}

#[allow(clippy::derivable_impls)]
impl Default for JsFormatMetadata {
    fn default() -> Self {
        Self {
            format_type_tag: String::new(),
            code: None,
            pdf: None,
            docx: None,
            excel: None,
            email: None,
            pptx: None,
            archive: None,
            image: None,
            xml: None,
            text: None,
            html: None,
            ocr: None,
            csv: None,
            bibtex: None,
            citation: None,
            fiction_book: None,
            dbf: None,
            jats: None,
            epub: None,
            pst: None,
        }
    }
}

/// Text direction enumeration for HTML documents.
#[napi(string_enum = "lowercase", js_name = "TextDirection")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsTextDirection {
    /// Left-to-right text direction
    #[napi(value = "ltr")]
    LeftToRight,
    /// Right-to-left text direction
    #[napi(value = "rtl")]
    RightToLeft,
    /// Automatic text direction detection
    #[napi(value = "auto")]
    Auto,
}

#[allow(clippy::derivable_impls)]
impl Default for JsTextDirection {
    fn default() -> Self {
        Self::LeftToRight
    }
}

/// Link type classification.
#[napi(string_enum = "lowercase", js_name = "LinkType")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsLinkType {
    /// Anchor link (#section)
    Anchor,
    /// Internal link (same domain)
    Internal,
    /// External link (different domain)
    External,
    /// Email link (mailto:)
    Email,
    /// Phone link (tel:)
    Phone,
    /// Other link type
    Other,
}

#[allow(clippy::derivable_impls)]
impl Default for JsLinkType {
    fn default() -> Self {
        Self::Anchor
    }
}

/// Image type classification.
#[napi(string_enum = "lowercase", js_name = "ImageType")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsImageType {
    /// Data URI image
    #[napi(value = "data-uri")]
    DataUri,
    /// Inline SVG
    #[napi(value = "inline-svg")]
    InlineSvg,
    /// External image URL
    External,
    /// Relative path image
    Relative,
}

#[allow(clippy::derivable_impls)]
impl Default for JsImageType {
    fn default() -> Self {
        Self::DataUri
    }
}

/// Structured data type classification.
#[napi(string_enum = "lowercase", js_name = "StructuredDataType")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsStructuredDataType {
    /// JSON-LD structured data
    #[napi(value = "json-ld")]
    JsonLd,
    /// Microdata
    Microdata,
    /// RDFa
    #[napi(value = "rdfa")]
    RDFa,
}

#[allow(clippy::derivable_impls)]
impl Default for JsStructuredDataType {
    fn default() -> Self {
        Self::JsonLd
    }
}

/// Bounding geometry for an OCR element.
///
/// Supports both axis-aligned rectangles (from Tesseract) and 4-point quadrilaterals
/// (from PaddleOCR and rotated text detection).
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "OcrBoundingGeometry")]
pub struct JsOcrBoundingGeometry {
    #[napi(js_name = "type")]
    #[serde(rename = "type")]
    pub type_tag: String,
    pub left: Option<u32>,
    pub top: Option<u32>,
    pub width: Option<u32>,
    pub height: Option<u32>,
    pub points: Option<String>,
}

#[allow(clippy::derivable_impls)]
impl Default for JsOcrBoundingGeometry {
    fn default() -> Self {
        Self {
            type_tag: String::new(),
            height: None,
            left: None,
            points: None,
            top: None,
            width: None,
        }
    }
}

/// Hierarchical level of an OCR element.
///
/// Maps to Tesseract's page segmentation hierarchy and provides
/// equivalent semantics for PaddleOCR.
#[napi(string_enum = "snake_case", js_name = "OcrElementLevel")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsOcrElementLevel {
    /// Individual word
    Word,
    /// Line of text (default for PaddleOCR)
    Line,
    /// Paragraph or text block
    Block,
    /// Page-level element
    Page,
}

#[allow(clippy::derivable_impls)]
impl Default for JsOcrElementLevel {
    fn default() -> Self {
        Self::Word
    }
}

/// Type of paginated unit in a document.
///
/// Distinguishes between different types of "pages" (PDF pages, presentation slides, spreadsheet sheets).
#[napi(string_enum = "snake_case", js_name = "PageUnitType")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsPageUnitType {
    /// Standard document pages (PDF, DOCX, images)
    Page,
    /// Presentation slides (PPTX, ODP)
    Slide,
    /// Spreadsheet sheets (XLSX, ODS)
    Sheet,
}

#[allow(clippy::derivable_impls)]
impl Default for JsPageUnitType {
    fn default() -> Self {
        Self::Page
    }
}

/// A single line in a unified-diff hunk.
///
/// Defined here (rather than only in `crate.diff`) so `RevisionDelta` can
/// reference it unconditionally, without requiring the `diff` Cargo feature.
/// `crate.diff` re-exports this type verbatim.
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "DiffLine")]
pub struct JsDiffLine {
    #[napi(js_name = "kind")]
    #[serde(rename = "kind")]
    pub kind_tag: String,
    pub context: Option<String>,
    pub added: Option<String>,
    pub removed: Option<String>,
}

#[allow(clippy::derivable_impls)]
impl Default for JsDiffLine {
    fn default() -> Self {
        Self {
            kind_tag: String::new(),
            added: None,
            context: None,
            removed: None,
        }
    }
}

/// Semantic classification of a tracked change.
#[napi(string_enum = "snake_case", js_name = "RevisionKind")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsRevisionKind {
    /// Text or content was inserted.
    Insertion,
    /// Text or content was deleted.
    Deletion,
    /// Run-level formatting (font, size, colour, …) was changed.
    FormatChange,
    /// A reviewer comment or annotation.
    Comment,
}

#[allow(clippy::derivable_impls)]
impl Default for JsRevisionKind {
    fn default() -> Self {
        Self::Insertion
    }
}

/// Best-effort document location for a revision.
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[napi(object, js_name = "RevisionAnchor")]
pub struct JsRevisionAnchor {
    #[napi(js_name = "type")]
    #[serde(rename = "type")]
    pub type_tag: String,
    pub index: Option<i64>,
    pub row: Option<i64>,
    pub col: Option<i64>,
    #[napi(js_name = "tableIndex")]
    #[serde(rename = "tableIndex")]
    pub table_index: Option<i64>,
    pub name: Option<String>,
}

#[allow(clippy::derivable_impls)]
impl Default for JsRevisionAnchor {
    fn default() -> Self {
        Self {
            type_tag: String::new(),
            col: None,
            index: None,
            name: None,
            row: None,
            table_index: None,
        }
    }
}

/// Semantic classification of an extracted URI.
#[napi(string_enum = "snake_case", js_name = "UriKind")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsUriKind {
    /// A clickable hyperlink (web URL, file link).
    Hyperlink,
    /// An image or media resource reference.
    Image,
    /// An internal anchor or cross-reference target.
    Anchor,
    /// A citation or bibliographic reference (DOI, academic ref).
    Citation,
    /// A general reference (e.g. `\ref{}` in LaTeX, `:ref:` in RST).
    Reference,
    /// An email address (`mailto:` link or bare email).
    Email,
}

#[allow(clippy::derivable_impls)]
impl Default for JsUriKind {
    fn default() -> Self {
        Self::Hyperlink
    }
}

/// Keyword algorithm selection.
#[napi(string_enum = "lowercase", js_name = "KeywordAlgorithm")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsKeywordAlgorithm {
    /// YAKE (Yet Another Keyword Extractor) - statistical approach
    Yake,
    /// RAKE (Rapid Automatic Keyword Extraction) - co-occurrence based
    Rake,
}

#[allow(clippy::derivable_impls)]
impl Default for JsKeywordAlgorithm {
    fn default() -> Self {
        Self::Yake
    }
}

/// Page Segmentation Mode for Tesseract OCR
#[napi(string_enum, js_name = "PSMMode")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsPSMMode {
    OsdOnly,
    AutoOsd,
    AutoOnly,
    Auto,
    SingleColumn,
    SingleBlockVertical,
    SingleBlock,
    SingleLine,
    SingleWord,
    CircleWord,
    SingleChar,
}

#[allow(clippy::derivable_impls)]
impl Default for JsPSMMode {
    fn default() -> Self {
        Self::OsdOnly
    }
}

/// Supported languages in PaddleOCR.
///
/// Maps user-friendly language codes to paddle-ocr-rs language identifiers.
#[napi(string_enum, js_name = "PaddleLanguage")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsPaddleLanguage {
    /// English
    English,
    /// Simplified Chinese
    Chinese,
    /// Japanese
    Japanese,
    /// Korean
    Korean,
    /// German
    German,
    /// French
    French,
    /// Latin script (covers most European languages)
    Latin,
    /// Cyrillic (Russian and related)
    Cyrillic,
    /// Traditional Chinese
    TraditionalChinese,
    /// Thai
    Thai,
    /// Greek
    Greek,
    /// East Slavic (Russian, Ukrainian, Belarusian)
    EastSlavic,
    /// Arabic (Arabic, Persian, Urdu)
    Arabic,
    /// Devanagari (Hindi, Marathi, Sanskrit, Nepali)
    Devanagari,
    /// Tamil
    Tamil,
    /// Telugu
    Telugu,
}

#[allow(clippy::derivable_impls)]
impl Default for JsPaddleLanguage {
    fn default() -> Self {
        Self::English
    }
}

/// The 17 canonical document layout classes.
///
/// All model backends (RT-DETR, YOLO, etc.) map their native class IDs
/// to this shared set. Models with fewer classes (DocLayNet: 11, PubLayNet: 5)
/// map to the closest equivalent.
///
/// Wire format is snake_case in all serializers (JSON, TOML, YAML).
#[napi(string_enum = "snake_case", js_name = "LayoutClass")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum JsLayoutClass {
    Caption,
    Footnote,
    Formula,
    ListItem,
    PageFooter,
    PageHeader,
    Picture,
    SectionHeader,
    Table,
    Text,
    Title,
    DocumentIndex,
    Code,
    CheckboxSelected,
    CheckboxUnselected,
    Form,
    KeyValueRegion,
}

#[allow(clippy::derivable_impls)]
impl Default for JsLayoutClass {
    fn default() -> Self {
        Self::Caption
    }
}

/// Extract content from a byte array.
///
/// This is the main entry point for in-memory extraction. It performs the following steps:
/// 1. Validate MIME type
/// 2. Handle legacy format conversion if needed
/// 3. Select appropriate extractor from registry
/// 4. Extract content
/// 5. Run post-processing pipeline
///
/// # Arguments
///
/// * `content` - The byte array to extract
/// * `mime_type` - MIME type of the content
/// * `config` - Extraction configuration
///
/// # Returns
///
/// An `ExtractionResult` containing the extracted content and metadata.
///
/// # Errors
///
/// Returns `KreuzbergError.Validation` if MIME type is invalid.
/// Returns `KreuzbergError.UnsupportedFormat` if MIME type is not supported.
///
/// # Example
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "extractBytes")]
pub async fn extract_bytes(
    content: napi::bindgen_prelude::Buffer,
    mime_type: String,
    config: Option<JsExtractionConfig>,
) -> Result<JsExtractionResult> {
    let config = config.unwrap_or_default();
    let config_core: kreuzberg::ExtractionConfig = config.into();

    let content: Vec<u8> = content.to_vec();
    let result = kreuzberg::extract_bytes(&content, &mime_type, &config_core)
        .await
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    Ok(result.into())
}

/// Extract content from a file.
///
/// This is the main entry point for file-based extraction. It performs the following steps:
/// 1. Check cache for existing result (if caching enabled)
/// 2. Detect or validate MIME type
/// 3. Select appropriate extractor from registry
/// 4. Extract content
/// 5. Run post-processing pipeline
/// 6. Store result in cache (if caching enabled)
///
/// # Arguments
///
/// * `path` - Path to the file to extract
/// * `mime_type` - Optional MIME type override. If undefined, will be auto-detected
/// * `config` - Extraction configuration
///
/// # Returns
///
/// An `ExtractionResult` containing the extracted content and metadata.
///
/// # Errors
///
/// Returns `KreuzbergError.Io` if the file doesn't exist (NotFound) or for other file I/O errors.
/// Returns `KreuzbergError.UnsupportedFormat` if MIME type is not supported.
///
/// # Example
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "extractFile")]
pub async fn extract_file(
    path: String,
    mime_type: Option<String>,
    config: Option<JsExtractionConfig>,
) -> Result<JsExtractionResult> {
    let config_core: kreuzberg::ExtractionConfig = config.unwrap_or_default().into();

    let result = kreuzberg::extract_file(std::path::PathBuf::from(path), mime_type.as_deref(), &config_core)
        .await
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    Ok(result.into())
}

/// Synchronous wrapper for `extract_file`.
///
/// This is a convenience function that blocks the current thread until extraction completes.
/// For async code, use `extract_file` directly.
///
/// Uses the global Tokio runtime for 100x+ performance improvement over creating
/// a new runtime per call. Always uses the global runtime to avoid nested runtime issues.
///
/// This function is only available with the `tokio-runtime` feature. For WASM targets,
/// use a truly synchronous extraction approach instead.
///
/// # Example
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "extractFileSync")]
pub fn extract_file_sync(
    path: String,
    mime_type: Option<String>,
    config: Option<JsExtractionConfig>,
) -> Result<JsExtractionResult> {
    let config_core: kreuzberg::ExtractionConfig = config.unwrap_or_default().into();

    kreuzberg::extract_file_sync(std::path::PathBuf::from(path), mime_type.as_deref(), &config_core)
        .map(|val| val.into())
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// Synchronous wrapper for `extract_bytes`.
///
/// Uses the global Tokio runtime for 100x+ performance improvement over creating
/// a new runtime per call.
///
/// With the `tokio-runtime` feature, this blocks the current thread using the global
/// Tokio runtime. Without it (WASM), this calls a truly synchronous implementation.
///
/// # Example
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "extractBytesSync")]
pub fn extract_bytes_sync(
    content: napi::bindgen_prelude::Buffer,
    mime_type: String,
    config: Option<JsExtractionConfig>,
) -> Result<JsExtractionResult> {
    let config = config.unwrap_or_default();
    let config_core: kreuzberg::ExtractionConfig = config.into();

    let content: Vec<u8> = content.to_vec();
    kreuzberg::extract_bytes_sync(&content, &mime_type, &config_core)
        .map(|val| val.into())
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// Synchronous wrapper for `batch_extract_files`.
///
/// Uses the global Tokio runtime for optimal performance.
/// Only available with `tokio-runtime` (WASM has no filesystem).
///
/// # Example
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "batchExtractFilesSync")]
pub fn batch_extract_files_sync(
    items: Vec<JsBatchFileItem>,
    config: Option<JsExtractionConfig>,
) -> Result<Vec<JsExtractionResult>> {
    let config = config.unwrap_or_default();
    let items_core: Vec<_> = items.into_iter().map(Into::into).collect();

    let config_core: kreuzberg::ExtractionConfig = config.into();

    kreuzberg::batch_extract_files_sync(items_core, &config_core)
        .map(|val| val.into_iter().map(Into::into).collect())
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// Synchronous wrapper for `batch_extract_bytes`.
///
/// Uses the global Tokio runtime for optimal performance.
/// With the `tokio-runtime` feature, this blocks the current thread using the global
/// Tokio runtime. Without it (WASM), this calls a truly synchronous implementation
/// that iterates through items and calls `extract_bytes_sync()`.
///
/// # Example
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "batchExtractBytesSync")]
pub fn batch_extract_bytes_sync(
    items: Vec<JsBatchBytesItem>,
    config: Option<JsExtractionConfig>,
) -> Result<Vec<JsExtractionResult>> {
    let config = config.unwrap_or_default();
    let items_core: Vec<_> = items.into_iter().map(Into::into).collect();

    let config_core: kreuzberg::ExtractionConfig = config.into();

    kreuzberg::batch_extract_bytes_sync(items_core, &config_core)
        .map(|val| val.into_iter().map(Into::into).collect())
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// Extract content from multiple files concurrently.
///
/// This function processes multiple files in parallel, automatically managing
/// concurrency to prevent resource exhaustion. The concurrency limit can be
/// configured via `ExtractionConfig.max_concurrent_extractions` or defaults
/// to `(num_cpus * 1.5).ceil()`.
///
/// Each file can optionally specify a `FileExtractionConfig` that overrides specific
/// fields from the batch-level `config`. Pass `None` for a file to use the batch defaults.
/// Batch-level settings like `max_concurrent_extractions` and `use_cache` are always
/// taken from the batch-level `config`.
///
/// # Arguments
///
/// * `items` - Vector of `BatchFileItem` structs, each containing a path and optional
///   per-file configuration overrides.
/// * `config` - Batch-level extraction configuration (provides defaults and batch settings)
///
/// # Returns
///
/// A vector of `ExtractionResult` in the same order as the input items.
///
/// # Errors
///
/// Individual file errors are captured in the result metadata. System errors
/// (IO, RuntimeError equivalents) will bubble up and fail the entire batch.
///
/// # Examples
///
/// Simple usage with no per-file overrides:
///
///
/// Per-file configuration overrides:
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "batchExtractFiles")]
pub async fn batch_extract_files(
    items: Vec<JsBatchFileItem>,
    config: Option<JsExtractionConfig>,
) -> Result<Vec<JsExtractionResult>> {
    let config = config.unwrap_or_default();
    let items_core: Vec<_> = items.into_iter().map(Into::into).collect();

    let config_core: kreuzberg::ExtractionConfig = config.into();

    let result = kreuzberg::batch_extract_files(items_core, &config_core)
        .await
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    Ok(result.into_iter().map(Into::into).collect())
}

/// Extract content from multiple byte arrays concurrently.
///
/// This function processes multiple byte arrays in parallel, automatically managing
/// concurrency to prevent resource exhaustion. The concurrency limit can be
/// configured via `ExtractionConfig.max_concurrent_extractions` or defaults
/// to `(num_cpus * 1.5).ceil()`.
///
/// Each item can optionally specify a `FileExtractionConfig` that overrides specific
/// fields from the batch-level `config`. Pass `None` as the config to use
/// the batch-level defaults for that item.
///
/// # Arguments
///
/// * `items` - Vector of `BatchBytesItem` structs, each containing content bytes,
///   MIME type, and optional per-item configuration overrides.
/// * `config` - Batch-level extraction configuration
///
/// # Returns
///
/// A vector of `ExtractionResult` in the same order as the input items.
///
/// # Examples
///
/// Simple usage with no per-item overrides:
///
///
/// Per-item configuration overrides:
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "batchExtractBytes")]
pub async fn batch_extract_bytes(
    items: Vec<JsBatchBytesItem>,
    config: Option<JsExtractionConfig>,
) -> Result<Vec<JsExtractionResult>> {
    let config = config.unwrap_or_default();
    let items_core: Vec<_> = items.into_iter().map(Into::into).collect();

    let config_core: kreuzberg::ExtractionConfig = config.into();

    let result = kreuzberg::batch_extract_bytes(items_core, &config_core)
        .await
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    Ok(result.into_iter().map(Into::into).collect())
}

/// Detect MIME type from raw file bytes.
///
/// Uses magic byte signatures to detect file type from content.
/// Falls back to `infer` crate for comprehensive detection.
///
/// For ZIP-based files, inspects contents to distinguish Office Open XML
/// formats (DOCX, XLSX, PPTX) from plain ZIP archives.
///
/// # Arguments
///
/// * `content` - Raw file bytes
///
/// # Returns
///
/// The detected MIME type string.
///
/// # Errors
///
/// Returns `KreuzbergError.UnsupportedFormat` if MIME type cannot be determined.
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "detectMimeTypeFromBytes")]
pub fn detect_mime_type_from_bytes(content: napi::bindgen_prelude::Buffer) -> Result<String> {
    let content: Vec<u8> = content.to_vec();
    kreuzberg::detect_mime_type_from_bytes(&content)
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// Get file extensions for a given MIME type.
///
/// Returns all known file extensions that map to the specified MIME type.
///
/// # Arguments
///
/// * `mime_type` - The MIME type to look up
///
/// # Returns
///
/// A vector of file extensions (without leading dot) for the MIME type.
///
/// # Example
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "getExtensionsForMime")]
pub fn get_extensions_for_mime(mime_type: String) -> Result<Vec<String>> {
    kreuzberg::get_extensions_for_mime(&mime_type)
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// List the names of all registered embedding backends.
///
/// Used by `kreuzberg-cli`, the api/mcp endpoints, and generated language
/// bindings.
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "listEmbeddingBackends")]
pub fn list_embedding_backends() -> Result<Vec<String>> {
    kreuzberg::list_embedding_backends().map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// List names of all registered document extractors.
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "listDocumentExtractors")]
pub fn list_document_extractors() -> Result<Vec<String>> {
    kreuzberg::list_document_extractors().map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// List all registered OCR backends.
///
/// Returns the names of all OCR backends currently registered in the global registry.
///
/// # Returns
///
/// A vector of OCR backend names.
///
/// # Example
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "listOcrBackends")]
pub fn list_ocr_backends() -> Result<Vec<String>> {
    kreuzberg::list_ocr_backends().map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// List all registered post-processor names.
///
/// Returns a vector of all post-processor names currently registered in the
/// global registry.
///
/// # Returns
///
/// - `Ok(Vec<String>)` - Vector of post-processor names
/// - `Err(...)` if the registry lock is poisoned
///
/// # Example
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "listPostProcessors")]
pub fn list_post_processors() -> Result<Vec<String>> {
    kreuzberg::list_post_processors().map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// List names of all registered renderers.
///
/// # Errors
///
/// Returns an error if the registry lock is poisoned.
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "listRenderers")]
pub fn list_renderers() -> Result<Vec<String>> {
    kreuzberg::list_renderers().map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// List names of all registered validators.
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "listValidators")]
pub fn list_validators() -> Result<Vec<String>> {
    kreuzberg::list_validators().map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// Compare two extraction results and return a structured diff.
///
/// The comparison is purely structural — no I/O, no side effects. All fields
/// of `ExtractionDiff` are populated according to the provided `DiffOptions`.
///
/// # Arguments
///
/// * `a` — the "before" extraction result
/// * `b` — the "after" extraction result
/// * `opts` — controls which sections are compared and optional truncation
///
/// # Example
#[napi]
pub fn compare(
    a: Option<JsExtractionResult>,
    b: Option<JsExtractionResult>,
    opts: Option<JsDiffOptions>,
) -> JsExtractionDiff {
    let a = a.unwrap_or_default();
    let b = b.unwrap_or_default();
    let opts = opts.unwrap_or_default();
    let a_core: kreuzberg::ExtractionResult = a.into();

    let b_core: kreuzberg::ExtractionResult = b.into();

    let opts_core: kreuzberg::DiffOptions = opts.into();

    kreuzberg::compare(&a_core, &b_core, &opts_core).into()
}

/// Generate embeddings asynchronously for a list of text strings.
///
/// This is the async counterpart to `embed_texts`. It offloads the blocking
/// ONNX inference work to a dedicated blocking thread pool via Tokio's
/// `spawn_blocking`, keeping the async executor free.
///
/// Returns one embedding vector per input text in the same order.
///
/// # Arguments
///
/// * `texts` - Vec of strings to embed (owned, sent to blocking thread)
/// * `config` - Embedding configuration specifying model, batch size, and normalization
///
/// # Errors
///
/// - `KreuzbergError.MissingDependency` if ONNX Runtime is not installed
/// - `KreuzbergError.Embedding` if the preset name is unknown, model download fails,
///   or the blocking inference task panics
///
/// # Example
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "embedTextsAsync")]
pub async fn embed_texts_async(texts: Vec<String>, config: Option<JsEmbeddingConfig>) -> Result<Vec<Vec<f64>>> {
    let config = config.unwrap_or_default();
    let config_core: kreuzberg::EmbeddingConfig = config.into();

    let result = kreuzberg::embed_texts_async(texts, &config_core)
        .await
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;
    Ok(result
        .into_iter()
        .map(|row| row.into_iter().map(|x| x as f64).collect::<Vec<_>>())
        .collect::<Vec<_>>())
}

/// Render a single PDF page to PNG bytes.
///
/// Returns raw PNG-encoded bytes for the specified page at the given DPI.
/// Uses pdf_oxide with tiny-skia for pure-Rust rendering.
///
/// # Arguments
///
/// * `pdf_bytes` - Raw PDF file bytes
/// * `page_index` - Zero-based page index
/// * `dpi` - Resolution in dots per inch (default: 150)
/// * `password` - Optional password for encrypted PDFs
///
/// # Errors
///
/// Returns `KreuzbergError.Parsing` if the PDF cannot be opened, authenticated,
/// or rendered, or if `page_index` is out of range.
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "renderPdfPageToPng")]
pub fn render_pdf_page_to_png(
    pdf_bytes: napi::bindgen_prelude::Buffer,
    page_index: i64,
    dpi: Option<i32>,
    password: Option<String>,
) -> Result<napi::bindgen_prelude::Buffer> {
    let pdf_bytes: Vec<u8> = pdf_bytes.to_vec();
    kreuzberg::render_pdf_page_to_png(&pdf_bytes, page_index as usize, dpi, password.as_deref())
        .map(|val| val.into())
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// Detect the MIME type of a file at the given path.
///
/// Uses the file extension and optionally the file content to determine the MIME type.
/// Set `check_exists` to `true` to verify the file exists before detection.
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "detectMimeType")]
pub fn detect_mime_type(path: String, check_exists: bool) -> Result<String> {
    kreuzberg::detect_mime_type(path, check_exists)
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// Embed a list of texts using the configured embedding model.
///
/// Returns a 2D vector where each inner vector is the embedding for the corresponding text.
#[allow(clippy::missing_errors_doc)]
#[napi(js_name = "embedTexts")]
pub fn embed_texts(texts: Vec<String>, config: Option<JsEmbeddingConfig>) -> Result<Vec<Vec<f64>>> {
    let config = config.unwrap_or_default();
    let config_core: kreuzberg::EmbeddingConfig = config.into();

    kreuzberg::embed_texts(texts, &config_core)
        .map(|val| {
            val.into_iter()
                .map(|row| row.into_iter().map(|x| x as f64).collect::<Vec<_>>())
                .collect::<Vec<_>>()
        })
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))
}

/// Get an embedding preset by name.
///
/// Returns `None` if no preset with the given name exists. Returns an owned
/// clone so the value is safe to pass across FFI boundaries.
#[napi(js_name = "getEmbeddingPreset")]
pub fn get_embedding_preset(name: String) -> Option<JsEmbeddingPreset> {
    kreuzberg::get_embedding_preset(&name).map(Into::into)
}

/// List the names of all available embedding presets.
///
/// Returns owned `String`s so the values are safe to pass across FFI boundaries.
#[napi(js_name = "listEmbeddingPresets")]
pub fn list_embedding_presets() -> Vec<String> {
    kreuzberg::list_embedding_presets()
}

/// Wrapper that bridges a foreign Js object to the `OcrBackend` trait.
pub struct JsOcrBackendBridge {
    inner: napi::bindgen_prelude::Object<'static>,
    cached_name: String,
}

impl std::fmt::Debug for JsOcrBackendBridge {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "JsOcrBackendBridge")
    }
}

impl JsOcrBackendBridge {
    /// Create a new bridge wrapping a NAPI Object.
    ///
    /// Validates that the object provides all required methods.
    pub fn new(js_obj: napi::bindgen_prelude::Object<'_>) -> napi::Result<Self> {
        if !js_obj.has_named_property("processImage").unwrap_or(false) {
            return Err(napi::Error::new(
                napi::Status::GenericFailure,
                format!("Object missing required method: {}", "processImage"),
            ));
        }
        if !js_obj.has_named_property("supportsLanguage").unwrap_or(false) {
            return Err(napi::Error::new(
                napi::Status::GenericFailure,
                format!("Object missing required method: {}", "supportsLanguage"),
            ));
        }
        if !js_obj.has_named_property("backendType").unwrap_or(false) {
            return Err(napi::Error::new(
                napi::Status::GenericFailure,
                format!("Object missing required method: {}", "backendType"),
            ));
        }
        if !js_obj.has_named_property("name").unwrap_or(false) {
            return Err(napi::Error::new(
                napi::Status::GenericFailure,
                "Object missing required method: name".to_string(),
            ));
        } // SAFETY: The JS object is owned by the Node.js runtime and lives for
        // the duration of the enclosing #[napi] call. The bridge is only used
        // synchronously during that same call, so 'static is safe here.
        let js_obj: napi::bindgen_prelude::Object<'static> = unsafe { std::mem::transmute(js_obj) };

        // Cache the plugin name. `name` may be either a string property or a
        // zero-arg function returning a string (the trait method form). Try the
        // function first, then fall back to a string property.
        let cached_name = js_obj
            .get_named_property::<napi::bindgen_prelude::Function<(), String>>("name")
            .and_then(|f| f.call(()))
            .or_else(|_| js_obj.get_named_property::<String>("name"))
            .map_err(|e| {
                napi::Error::new(
                    napi::Status::GenericFailure,
                    format!("Object missing required method: name ({e})"),
                )
            })?;
        Ok(Self {
            inner: js_obj,
            cached_name,
        })
    }

    /// Extract napi::Env from the stored Object.
    fn env(&self) -> napi::Env {
        // SAFETY: Object<'static> is 3 pointer-sized words; first word is napi_env.
        let raw: [*mut std::ffi::c_void; 3] = unsafe { std::mem::transmute_copy(&self.inner) };
        napi::Env::from_raw(raw[0] as napi::sys::napi_env)
    }
}

// SAFETY: The bridge is created from a NAPI Object that is pinned to the
// Node.js event loop thread. All access occurs on that thread. Send+Sync
// are required by the Plugin trait but the bridge is never actually moved
// across threads.
unsafe impl Send for JsOcrBackendBridge {}
unsafe impl Sync for JsOcrBackendBridge {}

impl kreuzberg::plugins::Plugin for JsOcrBackendBridge {
    fn name(&self) -> &str {
        &self.cached_name
    }

    fn version(&self) -> String {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("version") {
                Ok(f) => f,
                Err(_) => return Default::default(),
            };

        let result = func.call(());

        match result {
            Err(_) => Default::default(),
            Ok(val) => {
                // Convert JS value to Rust type via string coercion
                let s = val
                    .coerce_to_string()
                    .and_then(|s| s.into_utf8())
                    .and_then(|s| s.into_owned())
                    .unwrap_or_default();
                match s.as_str() {
                    "" | "null" => Default::default(),
                    _ => serde_json::from_str::<_>(&s).unwrap_or_default(),
                }
            }
        }
    }

    fn initialize(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("initialize") {
                Ok(f) => f,
                // Method has a default impl on the Rust trait — treat missing JS property as a no-op
                // (the foreign object opted out of overriding it).
                Err(_) => return Ok(()),
            };

        let result = func.call(());

        match result {
            Err(e) => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'initialize' failed: {}",
                self.cached_name, e
            ))),
            Ok(_) => Ok(()),
        }
    }

    fn shutdown(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("shutdown") {
                Ok(f) => f,
                // Method has a default impl on the Rust trait — treat missing JS property as a no-op
                // (the foreign object opted out of overriding it).
                Err(_) => return Ok(()),
            };

        let result = func.call(());

        match result {
            Err(e) => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'shutdown' failed: {}",
                self.cached_name, e
            ))),
            Ok(_) => Ok(()),
        }
    }
}

#[async_trait::async_trait]
impl kreuzberg::OcrBackend for JsOcrBackendBridge {
    async fn process_image(
        &self,
        image_bytes: &[u8],
        config: &kreuzberg::OcrConfig,
    ) -> std::result::Result<kreuzberg::ExtractionResult, kreuzberg::KreuzbergError> {
        let cached_name = self.cached_name.clone();

        let func: napi::bindgen_prelude::Function<
            napi::bindgen_prelude::FnArgs<(napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown)>,
            napi::bindgen_prelude::Unknown,
        > = match self.inner.get_named_property("processImage") {
            Ok(f) => f,
            Err(e) => {
                return Err(kreuzberg::KreuzbergError::Other(format!(
                    "Method '{}' not found on bridge object: {}",
                    cached_name, e
                )));
            }
        };

        let result = func.call(napi::bindgen_prelude::FnArgs::from((
            match self.env().create_string(&format!("{:?}", image_bytes)) {
                Ok(s) => s.to_unknown(),
                Err(_) => unsafe {
                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(
                        self.env().raw(),
                        napi::bindgen_prelude::Null,
                    )
                    .unwrap_or(std::ptr::null_mut());
                    napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r)
                },
            },
            match self.env().create_string(&format!("{:?}", config)) {
                Ok(s) => s.to_unknown(),
                Err(_) => unsafe {
                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(
                        self.env().raw(),
                        napi::bindgen_prelude::Null,
                    )
                    .unwrap_or(std::ptr::null_mut());
                    napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r)
                },
            },
        )));

        match result {
            Err(e) => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'process_image' failed: {}",
                cached_name, e
            ))),
            Ok(val) => {
                // Convert JS value to Rust type via string coercion
                let s = val
                    .coerce_to_string()
                    .and_then(|s| s.into_utf8())
                    .and_then(|s| s.into_owned())
                    .map_err(|e| {
                        kreuzberg::KreuzbergError::Other(format!(
                            "Failed to extract return value from method 'process_image': {}",
                            e
                        ))
                    })?;
                // null/empty is a protocol error for Result-returning methods: the JS
                // implementation must always return a serialisable value, never null.
                // Avoid requiring Default on the return type (excluded types like
                // InternalDocument do not implement Default).
                match s.as_str() {
                    "" | "null" => Err(kreuzberg::KreuzbergError::Other(
                        "Failed to parse return value for method 'process_image'".to_string(),
                    )),
                    _ => serde_json::from_str::<_>(&s).map_err(|_| {
                        kreuzberg::KreuzbergError::Other(
                            "Failed to parse return value for method 'process_image'".to_string(),
                        )
                    }),
                }
            }
        }
    }

    fn supports_language(&self, lang: &str) -> bool {
        let func: napi::bindgen_prelude::Function<
            napi::bindgen_prelude::FnArgs<(napi::bindgen_prelude::Unknown,)>,
            napi::bindgen_prelude::Unknown,
        > = match self.inner.get_named_property("supportsLanguage") {
            Ok(f) => f,
            Err(_) => return Default::default(),
        };

        let result = func.call(napi::bindgen_prelude::FnArgs::from((
            match self.env().create_string(lang) {
                Ok(s) => s.to_unknown(),
                Err(_) => unsafe {
                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(
                        self.env().raw(),
                        napi::bindgen_prelude::Null,
                    )
                    .unwrap_or(std::ptr::null_mut());
                    napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r)
                },
            },
        )));

        match result {
            Err(_) => Default::default(),
            Ok(val) => {
                // Convert JS value to Rust type via string coercion
                let s = val
                    .coerce_to_string()
                    .and_then(|s| s.into_utf8())
                    .and_then(|s| s.into_owned())
                    .unwrap_or_default();
                match s.as_str() {
                    "" | "null" => Default::default(),
                    _ => serde_json::from_str::<_>(&s).unwrap_or_default(),
                }
            }
        }
    }

    fn backend_type(&self) -> kreuzberg::OcrBackendType {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("backendType") {
                Ok(f) => f,
                Err(_) => return Default::default(),
            };

        let result = func.call(());

        match result {
            Err(_) => Default::default(),
            Ok(val) => {
                // Convert JS value to Rust type via string coercion
                let s = val
                    .coerce_to_string()
                    .and_then(|s| s.into_utf8())
                    .and_then(|s| s.into_owned())
                    .unwrap_or_default();
                match s.as_str() {
                    "" | "null" => Default::default(),
                    _ => serde_json::from_str::<_>(&s).unwrap_or_default(),
                }
            }
        }
    }
}

#[napi]
pub fn register_ocr_backend(obj: napi::bindgen_prelude::Object) -> napi::Result<()> {
    let bridge = JsOcrBackendBridge::new(obj)?;
    let arc: Arc<dyn kreuzberg::OcrBackend> = Arc::new(bridge);

    let registry = kreuzberg::plugins::registry::get_ocr_backend_registry();
    let mut registry = registry.write();
    registry.register(arc).map_err(|e| {
        napi::Error::new(
            napi::Status::GenericFailure,
            format!("Failed to register backend: {}", e),
        )
    })
}

#[napi(js_name = "unregisterOcrBackend")]
pub fn _alef_unregister_ocr_backend(name: String) -> napi::Result<()> {
    kreuzberg::plugins::ocr_backend::unregister_ocr_backend(&name)
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!("{}", e)))
}

#[napi(js_name = "clearOcrBackends")]
pub fn _alef_clear_ocr_backends() -> napi::Result<()> {
    kreuzberg::plugins::ocr_backend::clear_ocr_backends()
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!("{}", e)))
}

/// Wrapper that bridges a foreign Js object to the `PostProcessor` trait.
pub struct JsPostProcessorBridge {
    inner: napi::bindgen_prelude::Object<'static>,
    cached_name: String,
}

impl std::fmt::Debug for JsPostProcessorBridge {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "JsPostProcessorBridge")
    }
}

impl JsPostProcessorBridge {
    /// Create a new bridge wrapping a NAPI Object.
    ///
    /// Validates that the object provides all required methods.
    pub fn new(js_obj: napi::bindgen_prelude::Object<'_>) -> napi::Result<Self> {
        if !js_obj.has_named_property("process").unwrap_or(false) {
            return Err(napi::Error::new(
                napi::Status::GenericFailure,
                format!("Object missing required method: {}", "process"),
            ));
        }
        if !js_obj.has_named_property("processingStage").unwrap_or(false) {
            return Err(napi::Error::new(
                napi::Status::GenericFailure,
                format!("Object missing required method: {}", "processingStage"),
            ));
        }
        if !js_obj.has_named_property("name").unwrap_or(false) {
            return Err(napi::Error::new(
                napi::Status::GenericFailure,
                "Object missing required method: name".to_string(),
            ));
        } // SAFETY: The JS object is owned by the Node.js runtime and lives for
        // the duration of the enclosing #[napi] call. The bridge is only used
        // synchronously during that same call, so 'static is safe here.
        let js_obj: napi::bindgen_prelude::Object<'static> = unsafe { std::mem::transmute(js_obj) };

        // Cache the plugin name. `name` may be either a string property or a
        // zero-arg function returning a string (the trait method form). Try the
        // function first, then fall back to a string property.
        let cached_name = js_obj
            .get_named_property::<napi::bindgen_prelude::Function<(), String>>("name")
            .and_then(|f| f.call(()))
            .or_else(|_| js_obj.get_named_property::<String>("name"))
            .map_err(|e| {
                napi::Error::new(
                    napi::Status::GenericFailure,
                    format!("Object missing required method: name ({e})"),
                )
            })?;
        Ok(Self {
            inner: js_obj,
            cached_name,
        })
    }

    /// Extract napi::Env from the stored Object.
    fn env(&self) -> napi::Env {
        // SAFETY: Object<'static> is 3 pointer-sized words; first word is napi_env.
        let raw: [*mut std::ffi::c_void; 3] = unsafe { std::mem::transmute_copy(&self.inner) };
        napi::Env::from_raw(raw[0] as napi::sys::napi_env)
    }
}

// SAFETY: The bridge is created from a NAPI Object that is pinned to the
// Node.js event loop thread. All access occurs on that thread. Send+Sync
// are required by the Plugin trait but the bridge is never actually moved
// across threads.
unsafe impl Send for JsPostProcessorBridge {}
unsafe impl Sync for JsPostProcessorBridge {}

impl kreuzberg::plugins::Plugin for JsPostProcessorBridge {
    fn name(&self) -> &str {
        &self.cached_name
    }

    fn version(&self) -> String {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("version") {
                Ok(f) => f,
                Err(_) => return Default::default(),
            };

        let result = func.call(());

        match result {
            Err(_) => Default::default(),
            Ok(val) => {
                // Convert JS value to Rust type via string coercion
                let s = val
                    .coerce_to_string()
                    .and_then(|s| s.into_utf8())
                    .and_then(|s| s.into_owned())
                    .unwrap_or_default();
                match s.as_str() {
                    "" | "null" => Default::default(),
                    _ => serde_json::from_str::<_>(&s).unwrap_or_default(),
                }
            }
        }
    }

    fn initialize(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("initialize") {
                Ok(f) => f,
                // Method has a default impl on the Rust trait — treat missing JS property as a no-op
                // (the foreign object opted out of overriding it).
                Err(_) => return Ok(()),
            };

        let result = func.call(());

        match result {
            Err(e) => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'initialize' failed: {}",
                self.cached_name, e
            ))),
            Ok(_) => Ok(()),
        }
    }

    fn shutdown(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("shutdown") {
                Ok(f) => f,
                // Method has a default impl on the Rust trait — treat missing JS property as a no-op
                // (the foreign object opted out of overriding it).
                Err(_) => return Ok(()),
            };

        let result = func.call(());

        match result {
            Err(e) => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'shutdown' failed: {}",
                self.cached_name, e
            ))),
            Ok(_) => Ok(()),
        }
    }
}

#[async_trait::async_trait]
impl kreuzberg::PostProcessor for JsPostProcessorBridge {
    async fn process(
        &self,
        result: &mut kreuzberg::ExtractionResult,
        config: &kreuzberg::ExtractionConfig,
    ) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let cached_name = self.cached_name.clone();

        let func: napi::bindgen_prelude::Function<
            napi::bindgen_prelude::FnArgs<(napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown)>,
            napi::bindgen_prelude::Unknown,
        > = match self.inner.get_named_property("process") {
            Ok(f) => f,
            Err(e) => {
                return Err(kreuzberg::KreuzbergError::Other(format!(
                    "Method '{}' not found on bridge object: {}",
                    cached_name, e
                )));
            }
        };

        let result = func.call(napi::bindgen_prelude::FnArgs::from((
            match self.env().create_string(&format!("{:?}", result)) {
                Ok(s) => s.to_unknown(),
                Err(_) => unsafe {
                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(
                        self.env().raw(),
                        napi::bindgen_prelude::Null,
                    )
                    .unwrap_or(std::ptr::null_mut());
                    napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r)
                },
            },
            match self.env().create_string(&format!("{:?}", config)) {
                Ok(s) => s.to_unknown(),
                Err(_) => unsafe {
                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(
                        self.env().raw(),
                        napi::bindgen_prelude::Null,
                    )
                    .unwrap_or(std::ptr::null_mut());
                    napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r)
                },
            },
        )));

        match result {
            Err(e) => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'process' failed: {}",
                cached_name, e
            ))),
            Ok(_) => Ok(()),
        }
    }

    fn processing_stage(&self) -> kreuzberg::ProcessingStage {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("processingStage") {
                Ok(f) => f,
                Err(_) => return Default::default(),
            };

        let result = func.call(());

        match result {
            Err(_) => Default::default(),
            Ok(val) => {
                // Convert JS value to Rust type via string coercion
                let s = val
                    .coerce_to_string()
                    .and_then(|s| s.into_utf8())
                    .and_then(|s| s.into_owned())
                    .unwrap_or_default();
                match s.as_str() {
                    "" | "null" => Default::default(),
                    _ => serde_json::from_str::<_>(&s).unwrap_or_default(),
                }
            }
        }
    }
}

#[napi]
pub fn register_post_processor(obj: napi::bindgen_prelude::Object) -> napi::Result<()> {
    let bridge = JsPostProcessorBridge::new(obj)?;
    let arc: Arc<dyn kreuzberg::PostProcessor> = Arc::new(bridge);

    let registry = kreuzberg::plugins::registry::get_post_processor_registry();
    let mut registry = registry.write();
    registry.register(arc).map_err(|e| {
        napi::Error::new(
            napi::Status::GenericFailure,
            format!("Failed to register backend: {}", e),
        )
    })
}

#[napi(js_name = "unregisterPostProcessor")]
pub fn _alef_unregister_post_processor(name: String) -> napi::Result<()> {
    kreuzberg::plugins::post_processor::unregister_post_processor(&name)
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!("{}", e)))
}

#[napi(js_name = "clearPostProcessors")]
pub fn _alef_clear_post_processors() -> napi::Result<()> {
    kreuzberg::plugins::post_processor::clear_post_processors()
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!("{}", e)))
}

/// Wrapper that bridges a foreign Js object to the `Validator` trait.
pub struct JsValidatorBridge {
    inner: napi::bindgen_prelude::Object<'static>,
    cached_name: String,
}

impl std::fmt::Debug for JsValidatorBridge {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "JsValidatorBridge")
    }
}

impl JsValidatorBridge {
    /// Create a new bridge wrapping a NAPI Object.
    ///
    /// Validates that the object provides all required methods.
    pub fn new(js_obj: napi::bindgen_prelude::Object<'_>) -> napi::Result<Self> {
        if !js_obj.has_named_property("validate").unwrap_or(false) {
            return Err(napi::Error::new(
                napi::Status::GenericFailure,
                format!("Object missing required method: {}", "validate"),
            ));
        }
        if !js_obj.has_named_property("name").unwrap_or(false) {
            return Err(napi::Error::new(
                napi::Status::GenericFailure,
                "Object missing required method: name".to_string(),
            ));
        } // SAFETY: The JS object is owned by the Node.js runtime and lives for
        // the duration of the enclosing #[napi] call. The bridge is only used
        // synchronously during that same call, so 'static is safe here.
        let js_obj: napi::bindgen_prelude::Object<'static> = unsafe { std::mem::transmute(js_obj) };

        // Cache the plugin name. `name` may be either a string property or a
        // zero-arg function returning a string (the trait method form). Try the
        // function first, then fall back to a string property.
        let cached_name = js_obj
            .get_named_property::<napi::bindgen_prelude::Function<(), String>>("name")
            .and_then(|f| f.call(()))
            .or_else(|_| js_obj.get_named_property::<String>("name"))
            .map_err(|e| {
                napi::Error::new(
                    napi::Status::GenericFailure,
                    format!("Object missing required method: name ({e})"),
                )
            })?;
        Ok(Self {
            inner: js_obj,
            cached_name,
        })
    }

    /// Extract napi::Env from the stored Object.
    fn env(&self) -> napi::Env {
        // SAFETY: Object<'static> is 3 pointer-sized words; first word is napi_env.
        let raw: [*mut std::ffi::c_void; 3] = unsafe { std::mem::transmute_copy(&self.inner) };
        napi::Env::from_raw(raw[0] as napi::sys::napi_env)
    }
}

// SAFETY: The bridge is created from a NAPI Object that is pinned to the
// Node.js event loop thread. All access occurs on that thread. Send+Sync
// are required by the Plugin trait but the bridge is never actually moved
// across threads.
unsafe impl Send for JsValidatorBridge {}
unsafe impl Sync for JsValidatorBridge {}

impl kreuzberg::plugins::Plugin for JsValidatorBridge {
    fn name(&self) -> &str {
        &self.cached_name
    }

    fn version(&self) -> String {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("version") {
                Ok(f) => f,
                Err(_) => return Default::default(),
            };

        let result = func.call(());

        match result {
            Err(_) => Default::default(),
            Ok(val) => {
                // Convert JS value to Rust type via string coercion
                let s = val
                    .coerce_to_string()
                    .and_then(|s| s.into_utf8())
                    .and_then(|s| s.into_owned())
                    .unwrap_or_default();
                match s.as_str() {
                    "" | "null" => Default::default(),
                    _ => serde_json::from_str::<_>(&s).unwrap_or_default(),
                }
            }
        }
    }

    fn initialize(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("initialize") {
                Ok(f) => f,
                // Method has a default impl on the Rust trait — treat missing JS property as a no-op
                // (the foreign object opted out of overriding it).
                Err(_) => return Ok(()),
            };

        let result = func.call(());

        match result {
            Err(e) => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'initialize' failed: {}",
                self.cached_name, e
            ))),
            Ok(_) => Ok(()),
        }
    }

    fn shutdown(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("shutdown") {
                Ok(f) => f,
                // Method has a default impl on the Rust trait — treat missing JS property as a no-op
                // (the foreign object opted out of overriding it).
                Err(_) => return Ok(()),
            };

        let result = func.call(());

        match result {
            Err(e) => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'shutdown' failed: {}",
                self.cached_name, e
            ))),
            Ok(_) => Ok(()),
        }
    }
}

#[async_trait::async_trait]
impl kreuzberg::Validator for JsValidatorBridge {
    async fn validate(
        &self,
        result: &kreuzberg::ExtractionResult,
        config: &kreuzberg::ExtractionConfig,
    ) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let cached_name = self.cached_name.clone();

        let func: napi::bindgen_prelude::Function<
            napi::bindgen_prelude::FnArgs<(napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown)>,
            napi::bindgen_prelude::Unknown,
        > = match self.inner.get_named_property("validate") {
            Ok(f) => f,
            Err(e) => {
                return Err(kreuzberg::KreuzbergError::Other(format!(
                    "Method '{}' not found on bridge object: {}",
                    cached_name, e
                )));
            }
        };

        let result = func.call(napi::bindgen_prelude::FnArgs::from((
            match self.env().create_string(&format!("{:?}", result)) {
                Ok(s) => s.to_unknown(),
                Err(_) => unsafe {
                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(
                        self.env().raw(),
                        napi::bindgen_prelude::Null,
                    )
                    .unwrap_or(std::ptr::null_mut());
                    napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r)
                },
            },
            match self.env().create_string(&format!("{:?}", config)) {
                Ok(s) => s.to_unknown(),
                Err(_) => unsafe {
                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(
                        self.env().raw(),
                        napi::bindgen_prelude::Null,
                    )
                    .unwrap_or(std::ptr::null_mut());
                    napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r)
                },
            },
        )));

        match result {
            Err(e) => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'validate' failed: {}",
                cached_name, e
            ))),
            Ok(_) => Ok(()),
        }
    }
}

#[napi]
pub fn register_validator(obj: napi::bindgen_prelude::Object) -> napi::Result<()> {
    let bridge = JsValidatorBridge::new(obj)?;
    let arc: Arc<dyn kreuzberg::Validator> = Arc::new(bridge);

    let registry = kreuzberg::plugins::registry::get_validator_registry();
    let mut registry = registry.write();
    registry.register(arc).map_err(|e| {
        napi::Error::new(
            napi::Status::GenericFailure,
            format!("Failed to register backend: {}", e),
        )
    })
}

#[napi(js_name = "unregisterValidator")]
pub fn _alef_unregister_validator(name: String) -> napi::Result<()> {
    kreuzberg::plugins::validator::unregister_validator(&name)
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!("{}", e)))
}

#[napi(js_name = "clearValidators")]
pub fn _alef_clear_validators() -> napi::Result<()> {
    kreuzberg::plugins::validator::clear_validators()
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!("{}", e)))
}

/// Wrapper that bridges a foreign Js object to the `EmbeddingBackend` trait.
pub struct JsEmbeddingBackendBridge {
    inner: napi::bindgen_prelude::Object<'static>,
    cached_name: String,
}

impl std::fmt::Debug for JsEmbeddingBackendBridge {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "JsEmbeddingBackendBridge")
    }
}

impl JsEmbeddingBackendBridge {
    /// Create a new bridge wrapping a NAPI Object.
    ///
    /// Validates that the object provides all required methods.
    pub fn new(js_obj: napi::bindgen_prelude::Object<'_>) -> napi::Result<Self> {
        if !js_obj.has_named_property("dimensions").unwrap_or(false) {
            return Err(napi::Error::new(
                napi::Status::GenericFailure,
                format!("Object missing required method: {}", "dimensions"),
            ));
        }
        if !js_obj.has_named_property("embed").unwrap_or(false) {
            return Err(napi::Error::new(
                napi::Status::GenericFailure,
                format!("Object missing required method: {}", "embed"),
            ));
        }
        if !js_obj.has_named_property("name").unwrap_or(false) {
            return Err(napi::Error::new(
                napi::Status::GenericFailure,
                "Object missing required method: name".to_string(),
            ));
        } // SAFETY: The JS object is owned by the Node.js runtime and lives for
        // the duration of the enclosing #[napi] call. The bridge is only used
        // synchronously during that same call, so 'static is safe here.
        let js_obj: napi::bindgen_prelude::Object<'static> = unsafe { std::mem::transmute(js_obj) };

        // Cache the plugin name. `name` may be either a string property or a
        // zero-arg function returning a string (the trait method form). Try the
        // function first, then fall back to a string property.
        let cached_name = js_obj
            .get_named_property::<napi::bindgen_prelude::Function<(), String>>("name")
            .and_then(|f| f.call(()))
            .or_else(|_| js_obj.get_named_property::<String>("name"))
            .map_err(|e| {
                napi::Error::new(
                    napi::Status::GenericFailure,
                    format!("Object missing required method: name ({e})"),
                )
            })?;
        Ok(Self {
            inner: js_obj,
            cached_name,
        })
    }

    /// Extract napi::Env from the stored Object.
    fn env(&self) -> napi::Env {
        // SAFETY: Object<'static> is 3 pointer-sized words; first word is napi_env.
        let raw: [*mut std::ffi::c_void; 3] = unsafe { std::mem::transmute_copy(&self.inner) };
        napi::Env::from_raw(raw[0] as napi::sys::napi_env)
    }
}

// SAFETY: The bridge is created from a NAPI Object that is pinned to the
// Node.js event loop thread. All access occurs on that thread. Send+Sync
// are required by the Plugin trait but the bridge is never actually moved
// across threads.
unsafe impl Send for JsEmbeddingBackendBridge {}
unsafe impl Sync for JsEmbeddingBackendBridge {}

impl kreuzberg::plugins::Plugin for JsEmbeddingBackendBridge {
    fn name(&self) -> &str {
        &self.cached_name
    }

    fn version(&self) -> String {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("version") {
                Ok(f) => f,
                Err(_) => return Default::default(),
            };

        let result = func.call(());

        match result {
            Err(_) => Default::default(),
            Ok(val) => {
                // Convert JS value to Rust type via string coercion
                let s = val
                    .coerce_to_string()
                    .and_then(|s| s.into_utf8())
                    .and_then(|s| s.into_owned())
                    .unwrap_or_default();
                match s.as_str() {
                    "" | "null" => Default::default(),
                    _ => serde_json::from_str::<_>(&s).unwrap_or_default(),
                }
            }
        }
    }

    fn initialize(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("initialize") {
                Ok(f) => f,
                // Method has a default impl on the Rust trait — treat missing JS property as a no-op
                // (the foreign object opted out of overriding it).
                Err(_) => return Ok(()),
            };

        let result = func.call(());

        match result {
            Err(e) => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'initialize' failed: {}",
                self.cached_name, e
            ))),
            Ok(_) => Ok(()),
        }
    }

    fn shutdown(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("shutdown") {
                Ok(f) => f,
                // Method has a default impl on the Rust trait — treat missing JS property as a no-op
                // (the foreign object opted out of overriding it).
                Err(_) => return Ok(()),
            };

        let result = func.call(());

        match result {
            Err(e) => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'shutdown' failed: {}",
                self.cached_name, e
            ))),
            Ok(_) => Ok(()),
        }
    }
}

#[async_trait::async_trait]
impl kreuzberg::EmbeddingBackend for JsEmbeddingBackendBridge {
    fn dimensions(&self) -> usize {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("dimensions") {
                Ok(f) => f,
                Err(_) => return Default::default(),
            };

        let result = func.call(());

        match result {
            Err(_) => Default::default(),
            Ok(val) => {
                // Convert JS value to Rust type via string coercion
                let s = val
                    .coerce_to_string()
                    .and_then(|s| s.into_utf8())
                    .and_then(|s| s.into_owned())
                    .unwrap_or_default();
                match s.as_str() {
                    "" | "null" => Default::default(),
                    _ => serde_json::from_str::<_>(&s).unwrap_or_default(),
                }
            }
        }
    }

    async fn embed(&self, texts: Vec<String>) -> std::result::Result<Vec<Vec<f32>>, kreuzberg::KreuzbergError> {
        let cached_name = self.cached_name.clone();

        let func: napi::bindgen_prelude::Function<
            napi::bindgen_prelude::FnArgs<(napi::bindgen_prelude::Unknown,)>,
            napi::bindgen_prelude::Unknown,
        > = match self.inner.get_named_property("embed") {
            Ok(f) => f,
            Err(e) => {
                return Err(kreuzberg::KreuzbergError::Other(format!(
                    "Method '{}' not found on bridge object: {}",
                    cached_name, e
                )));
            }
        };

        let result = func.call(napi::bindgen_prelude::FnArgs::from((
            match self.env().create_string(&format!("{:?}", texts)) {
                Ok(s) => s.to_unknown(),
                Err(_) => unsafe {
                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(
                        self.env().raw(),
                        napi::bindgen_prelude::Null,
                    )
                    .unwrap_or(std::ptr::null_mut());
                    napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r)
                },
            },
        )));

        match result {
            Err(e) => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'embed' failed: {}",
                cached_name, e
            ))),
            Ok(val) => {
                // Convert JS value to Rust type via string coercion
                let s = val
                    .coerce_to_string()
                    .and_then(|s| s.into_utf8())
                    .and_then(|s| s.into_owned())
                    .map_err(|e| {
                        kreuzberg::KreuzbergError::Other(format!(
                            "Failed to extract return value from method 'embed': {}",
                            e
                        ))
                    })?;
                // null/empty is a protocol error for Result-returning methods: the JS
                // implementation must always return a serialisable value, never null.
                // Avoid requiring Default on the return type (excluded types like
                // InternalDocument do not implement Default).
                match s.as_str() {
                    "" | "null" => Err(kreuzberg::KreuzbergError::Other(
                        "Failed to parse return value for method 'embed'".to_string(),
                    )),
                    _ => serde_json::from_str::<_>(&s).map_err(|_| {
                        kreuzberg::KreuzbergError::Other("Failed to parse return value for method 'embed'".to_string())
                    }),
                }
            }
        }
    }
}

#[napi]
pub fn register_embedding_backend(obj: napi::bindgen_prelude::Object) -> napi::Result<()> {
    let bridge = JsEmbeddingBackendBridge::new(obj)?;
    let arc: Arc<dyn kreuzberg::EmbeddingBackend> = Arc::new(bridge);

    let registry = kreuzberg::plugins::registry::get_embedding_backend_registry();
    let mut registry = registry.write();
    registry.register(arc).map_err(|e| {
        napi::Error::new(
            napi::Status::GenericFailure,
            format!("Failed to register backend: {}", e),
        )
    })
}

#[napi(js_name = "unregisterEmbeddingBackend")]
pub fn _alef_unregister_embedding_backend(name: String) -> napi::Result<()> {
    kreuzberg::plugins::embedding_backend::unregister_embedding_backend(&name)
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!("{}", e)))
}

#[napi(js_name = "clearEmbeddingBackends")]
pub fn _alef_clear_embedding_backends() -> napi::Result<()> {
    kreuzberg::plugins::embedding_backend::clear_embedding_backends()
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!("{}", e)))
}

/// Wrapper that bridges a foreign Js object to the `DocumentExtractor` trait.
pub struct JsDocumentExtractorBridge {
    inner: napi::bindgen_prelude::Object<'static>,
    cached_name: String,
}

impl std::fmt::Debug for JsDocumentExtractorBridge {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "JsDocumentExtractorBridge")
    }
}

impl JsDocumentExtractorBridge {
    /// Create a new bridge wrapping a NAPI Object.
    ///
    /// Validates that the object provides all required methods.
    pub fn new(js_obj: napi::bindgen_prelude::Object<'_>) -> napi::Result<Self> {
        if !js_obj.has_named_property("extractBytes").unwrap_or(false) {
            return Err(napi::Error::new(
                napi::Status::GenericFailure,
                format!("Object missing required method: {}", "extractBytes"),
            ));
        }
        if !js_obj.has_named_property("supportedMimeTypes").unwrap_or(false) {
            return Err(napi::Error::new(
                napi::Status::GenericFailure,
                format!("Object missing required method: {}", "supportedMimeTypes"),
            ));
        }
        if !js_obj.has_named_property("name").unwrap_or(false) {
            return Err(napi::Error::new(
                napi::Status::GenericFailure,
                "Object missing required method: name".to_string(),
            ));
        } // SAFETY: The JS object is owned by the Node.js runtime and lives for
        // the duration of the enclosing #[napi] call. The bridge is only used
        // synchronously during that same call, so 'static is safe here.
        let js_obj: napi::bindgen_prelude::Object<'static> = unsafe { std::mem::transmute(js_obj) };

        // Cache the plugin name. `name` may be either a string property or a
        // zero-arg function returning a string (the trait method form). Try the
        // function first, then fall back to a string property.
        let cached_name = js_obj
            .get_named_property::<napi::bindgen_prelude::Function<(), String>>("name")
            .and_then(|f| f.call(()))
            .or_else(|_| js_obj.get_named_property::<String>("name"))
            .map_err(|e| {
                napi::Error::new(
                    napi::Status::GenericFailure,
                    format!("Object missing required method: name ({e})"),
                )
            })?;
        Ok(Self {
            inner: js_obj,
            cached_name,
        })
    }

    /// Extract napi::Env from the stored Object.
    fn env(&self) -> napi::Env {
        // SAFETY: Object<'static> is 3 pointer-sized words; first word is napi_env.
        let raw: [*mut std::ffi::c_void; 3] = unsafe { std::mem::transmute_copy(&self.inner) };
        napi::Env::from_raw(raw[0] as napi::sys::napi_env)
    }
}

// SAFETY: The bridge is created from a NAPI Object that is pinned to the
// Node.js event loop thread. All access occurs on that thread. Send+Sync
// are required by the Plugin trait but the bridge is never actually moved
// across threads.
unsafe impl Send for JsDocumentExtractorBridge {}
unsafe impl Sync for JsDocumentExtractorBridge {}

impl kreuzberg::plugins::Plugin for JsDocumentExtractorBridge {
    fn name(&self) -> &str {
        &self.cached_name
    }

    fn version(&self) -> String {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("version") {
                Ok(f) => f,
                Err(_) => return Default::default(),
            };

        let result = func.call(());

        match result {
            Err(_) => Default::default(),
            Ok(val) => {
                // Convert JS value to Rust type via string coercion
                let s = val
                    .coerce_to_string()
                    .and_then(|s| s.into_utf8())
                    .and_then(|s| s.into_owned())
                    .unwrap_or_default();
                match s.as_str() {
                    "" | "null" => Default::default(),
                    _ => serde_json::from_str::<_>(&s).unwrap_or_default(),
                }
            }
        }
    }

    fn initialize(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("initialize") {
                Ok(f) => f,
                // Method has a default impl on the Rust trait — treat missing JS property as a no-op
                // (the foreign object opted out of overriding it).
                Err(_) => return Ok(()),
            };

        let result = func.call(());

        match result {
            Err(e) => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'initialize' failed: {}",
                self.cached_name, e
            ))),
            Ok(_) => Ok(()),
        }
    }

    fn shutdown(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("shutdown") {
                Ok(f) => f,
                // Method has a default impl on the Rust trait — treat missing JS property as a no-op
                // (the foreign object opted out of overriding it).
                Err(_) => return Ok(()),
            };

        let result = func.call(());

        match result {
            Err(e) => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'shutdown' failed: {}",
                self.cached_name, e
            ))),
            Ok(_) => Ok(()),
        }
    }
}

#[async_trait::async_trait]
impl kreuzberg::DocumentExtractor for JsDocumentExtractorBridge {
    async fn extract_bytes(
        &self,
        content: &[u8],
        mime_type: &str,
        config: &kreuzberg::ExtractionConfig,
    ) -> std::result::Result<kreuzberg::InternalDocument, kreuzberg::KreuzbergError> {
        let cached_name = self.cached_name.clone();

        let func: napi::bindgen_prelude::Function<
            napi::bindgen_prelude::FnArgs<(
                napi::bindgen_prelude::Unknown,
                napi::bindgen_prelude::Unknown,
                napi::bindgen_prelude::Unknown,
            )>,
            napi::bindgen_prelude::Unknown,
        > = match self.inner.get_named_property("extractBytes") {
            Ok(f) => f,
            Err(e) => {
                return Err(kreuzberg::KreuzbergError::Other(format!(
                    "Method '{}' not found on bridge object: {}",
                    cached_name, e
                )));
            }
        };

        let result = func.call(napi::bindgen_prelude::FnArgs::from((
            match self.env().create_string(&format!("{:?}", content)) {
                Ok(s) => s.to_unknown(),
                Err(_) => unsafe {
                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(
                        self.env().raw(),
                        napi::bindgen_prelude::Null,
                    )
                    .unwrap_or(std::ptr::null_mut());
                    napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r)
                },
            },
            match self.env().create_string(mime_type) {
                Ok(s) => s.to_unknown(),
                Err(_) => unsafe {
                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(
                        self.env().raw(),
                        napi::bindgen_prelude::Null,
                    )
                    .unwrap_or(std::ptr::null_mut());
                    napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r)
                },
            },
            match self.env().create_string(&format!("{:?}", config)) {
                Ok(s) => s.to_unknown(),
                Err(_) => unsafe {
                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(
                        self.env().raw(),
                        napi::bindgen_prelude::Null,
                    )
                    .unwrap_or(std::ptr::null_mut());
                    napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r)
                },
            },
        )));

        match result {
            Err(e) => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'extract_bytes' failed: {}",
                cached_name, e
            ))),
            Ok(val) => {
                // Convert JS value to Rust type via string coercion
                let s = val
                    .coerce_to_string()
                    .and_then(|s| s.into_utf8())
                    .and_then(|s| s.into_owned())
                    .map_err(|e| {
                        kreuzberg::KreuzbergError::Other(format!(
                            "Failed to extract return value from method 'extract_bytes': {}",
                            e
                        ))
                    })?;
                // null/empty is a protocol error for Result-returning methods: the JS
                // implementation must always return a serialisable value, never null.
                // Avoid requiring Default on the return type (excluded types like
                // InternalDocument do not implement Default).
                match s.as_str() {
                    "" | "null" => Err(kreuzberg::KreuzbergError::Other(
                        "Failed to parse return value for method 'extract_bytes'".to_string(),
                    )),
                    _ => serde_json::from_str::<_>(&s).map_err(|_| {
                        kreuzberg::KreuzbergError::Other(
                            "Failed to parse return value for method 'extract_bytes'".to_string(),
                        )
                    }),
                }
            }
        }
    }

    fn supported_mime_types(&self) -> &[&str] {
        let __types: Vec<String> = {
            let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
                match self.inner.get_named_property("supportedMimeTypes") {
                    Ok(f) => f,
                    Err(_) => return Default::default(),
                };

            let result = func.call(());

            match result {
                Err(_) => Default::default(),
                Ok(val) => {
                    // Convert JS value to Rust type via string coercion
                    let s = val
                        .coerce_to_string()
                        .and_then(|s| s.into_utf8())
                        .and_then(|s| s.into_owned())
                        .unwrap_or_default();
                    match s.as_str() {
                        "" | "null" => Default::default(),
                        _ => serde_json::from_str::<_>(&s).unwrap_or_default(),
                    }
                }
            }
        };
        let __strs: Vec<&'static str> = __types
            .into_iter()
            .map(|s| -> &'static str { Box::leak(s.into_boxed_str()) })
            .collect();
        Box::leak(__strs.into_boxed_slice())
    }
}

#[napi]
pub fn register_document_extractor(obj: napi::bindgen_prelude::Object) -> napi::Result<()> {
    let bridge = JsDocumentExtractorBridge::new(obj)?;
    let arc: Arc<dyn kreuzberg::DocumentExtractor> = Arc::new(bridge);

    let registry = kreuzberg::plugins::registry::get_document_extractor_registry();
    let mut registry = registry.write();
    registry.register(arc).map_err(|e| {
        napi::Error::new(
            napi::Status::GenericFailure,
            format!("Failed to register backend: {}", e),
        )
    })
}

#[napi(js_name = "unregisterDocumentExtractor")]
pub fn _alef_unregister_document_extractor(name: String) -> napi::Result<()> {
    kreuzberg::plugins::document_extractor::unregister_document_extractor(&name)
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!("{}", e)))
}

#[napi(js_name = "clearDocumentExtractors")]
pub fn _alef_clear_document_extractors() -> napi::Result<()> {
    kreuzberg::plugins::document_extractor::clear_document_extractors()
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!("{}", e)))
}

/// Wrapper that bridges a foreign Js object to the `Renderer` trait.
pub struct JsRendererBridge {
    inner: napi::bindgen_prelude::Object<'static>,
    cached_name: String,
}

impl std::fmt::Debug for JsRendererBridge {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "JsRendererBridge")
    }
}

impl JsRendererBridge {
    /// Create a new bridge wrapping a NAPI Object.
    ///
    /// Validates that the object provides all required methods.
    pub fn new(js_obj: napi::bindgen_prelude::Object<'_>) -> napi::Result<Self> {
        if !js_obj.has_named_property("render").unwrap_or(false) {
            return Err(napi::Error::new(
                napi::Status::GenericFailure,
                format!("Object missing required method: {}", "render"),
            ));
        }
        if !js_obj.has_named_property("name").unwrap_or(false) {
            return Err(napi::Error::new(
                napi::Status::GenericFailure,
                "Object missing required method: name".to_string(),
            ));
        } // SAFETY: The JS object is owned by the Node.js runtime and lives for
        // the duration of the enclosing #[napi] call. The bridge is only used
        // synchronously during that same call, so 'static is safe here.
        let js_obj: napi::bindgen_prelude::Object<'static> = unsafe { std::mem::transmute(js_obj) };

        // Cache the plugin name. `name` may be either a string property or a
        // zero-arg function returning a string (the trait method form). Try the
        // function first, then fall back to a string property.
        let cached_name = js_obj
            .get_named_property::<napi::bindgen_prelude::Function<(), String>>("name")
            .and_then(|f| f.call(()))
            .or_else(|_| js_obj.get_named_property::<String>("name"))
            .map_err(|e| {
                napi::Error::new(
                    napi::Status::GenericFailure,
                    format!("Object missing required method: name ({e})"),
                )
            })?;
        Ok(Self {
            inner: js_obj,
            cached_name,
        })
    }

    /// Extract napi::Env from the stored Object.
    fn env(&self) -> napi::Env {
        // SAFETY: Object<'static> is 3 pointer-sized words; first word is napi_env.
        let raw: [*mut std::ffi::c_void; 3] = unsafe { std::mem::transmute_copy(&self.inner) };
        napi::Env::from_raw(raw[0] as napi::sys::napi_env)
    }
}

// SAFETY: The bridge is created from a NAPI Object that is pinned to the
// Node.js event loop thread. All access occurs on that thread. Send+Sync
// are required by the Plugin trait but the bridge is never actually moved
// across threads.
unsafe impl Send for JsRendererBridge {}
unsafe impl Sync for JsRendererBridge {}

impl kreuzberg::plugins::Plugin for JsRendererBridge {
    fn name(&self) -> &str {
        &self.cached_name
    }

    fn version(&self) -> String {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("version") {
                Ok(f) => f,
                Err(_) => return Default::default(),
            };

        let result = func.call(());

        match result {
            Err(_) => Default::default(),
            Ok(val) => {
                // Convert JS value to Rust type via string coercion
                let s = val
                    .coerce_to_string()
                    .and_then(|s| s.into_utf8())
                    .and_then(|s| s.into_owned())
                    .unwrap_or_default();
                match s.as_str() {
                    "" | "null" => Default::default(),
                    _ => serde_json::from_str::<_>(&s).unwrap_or_default(),
                }
            }
        }
    }

    fn initialize(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("initialize") {
                Ok(f) => f,
                // Method has a default impl on the Rust trait — treat missing JS property as a no-op
                // (the foreign object opted out of overriding it).
                Err(_) => return Ok(()),
            };

        let result = func.call(());

        match result {
            Err(e) => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'initialize' failed: {}",
                self.cached_name, e
            ))),
            Ok(_) => Ok(()),
        }
    }

    fn shutdown(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let func: napi::bindgen_prelude::Function<(), napi::bindgen_prelude::Unknown> =
            match self.inner.get_named_property("shutdown") {
                Ok(f) => f,
                // Method has a default impl on the Rust trait — treat missing JS property as a no-op
                // (the foreign object opted out of overriding it).
                Err(_) => return Ok(()),
            };

        let result = func.call(());

        match result {
            Err(e) => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'shutdown' failed: {}",
                self.cached_name, e
            ))),
            Ok(_) => Ok(()),
        }
    }
}

impl kreuzberg::Renderer for JsRendererBridge {
    fn render(&self, doc: &kreuzberg::InternalDocument) -> std::result::Result<String, kreuzberg::KreuzbergError> {
        let func: napi::bindgen_prelude::Function<
            napi::bindgen_prelude::FnArgs<(napi::bindgen_prelude::Unknown,)>,
            napi::bindgen_prelude::Unknown,
        > = match self.inner.get_named_property("render") {
            Ok(f) => f,
            Err(e) => {
                return Err(kreuzberg::KreuzbergError::Other(format!(
                    "Method '{}' not found on bridge object: {}",
                    self.cached_name, e
                )));
            }
        };

        let result = func.call(napi::bindgen_prelude::FnArgs::from((
            match self.env().create_string(&format!("{:?}", doc)) {
                Ok(s) => s.to_unknown(),
                Err(_) => unsafe {
                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(
                        self.env().raw(),
                        napi::bindgen_prelude::Null,
                    )
                    .unwrap_or(std::ptr::null_mut());
                    napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r)
                },
            },
        )));

        match result {
            Err(e) => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'render' failed: {}",
                self.cached_name, e
            ))),
            Ok(val) => {
                // Convert JS value to Rust type via string coercion
                let s = val
                    .coerce_to_string()
                    .and_then(|s| s.into_utf8())
                    .and_then(|s| s.into_owned())
                    .map_err(|e| {
                        kreuzberg::KreuzbergError::Other(format!(
                            "Failed to extract return value from method 'render': {}",
                            e
                        ))
                    })?;
                match s.as_str() {
                    // null/empty is a protocol error for Result-returning methods: the JS
                    // implementation must always return a serialisable value, never null.
                    // Avoid requiring Default on the return type (excluded types like
                    // InternalDocument do not implement Default).
                    "" | "null" => Err(kreuzberg::KreuzbergError::Other(format!(
                        "Plugin '{}' failed to parse return value for method 'render'",
                        self.cached_name
                    ))),
                    _ => serde_json::from_str::<_>(&s).map_err(|_| {
                        kreuzberg::KreuzbergError::Other(format!(
                            "Plugin '{}' failed to parse return value for method 'render'",
                            self.cached_name
                        ))
                    }),
                }
            }
        }
    }
}

#[napi]
pub fn register_renderer(obj: napi::bindgen_prelude::Object) -> napi::Result<()> {
    let bridge = JsRendererBridge::new(obj)?;
    let arc: Arc<dyn kreuzberg::Renderer> = Arc::new(bridge);

    let registry = kreuzberg::plugins::registry::get_renderer_registry();
    let mut registry = registry.write();
    registry.register(arc).map_err(|e| {
        napi::Error::new(
            napi::Status::GenericFailure,
            format!("Failed to register backend: {}", e),
        )
    })
}

#[napi(js_name = "unregisterRenderer")]
pub fn _alef_unregister_renderer(name: String) -> napi::Result<()> {
    kreuzberg::plugins::renderer::unregister_renderer(&name)
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!("{}", e)))
}

#[napi(js_name = "clearRenderers")]
pub fn _alef_clear_renderers() -> napi::Result<()> {
    kreuzberg::plugins::renderer::clear_renderers()
        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!("{}", e)))
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::CacheStats> for JsCacheStats {
    fn from(val: kreuzberg::CacheStats) -> Self {
        Self {
            total_files: val.total_files as i64,
            total_size_mb: val.total_size_mb,
            available_space_mb: val.available_space_mb,
            oldest_file_age_days: val.oldest_file_age_days,
            newest_file_age_days: val.newest_file_age_days,
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsAccelerationConfig> for kreuzberg::AccelerationConfig {
    fn from(val: JsAccelerationConfig) -> Self {
        let mut __result = kreuzberg::AccelerationConfig::default();
        if let Some(__v) = val.provider {
            __result.provider = __v.into();
        }
        if let Some(__v) = val.device_id {
            __result.device_id = __v;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::AccelerationConfig> for JsAccelerationConfig {
    fn from(val: kreuzberg::AccelerationConfig) -> Self {
        Self {
            provider: Some(val.provider.into()),
            device_id: Some(val.device_id),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsContentFilterConfig> for kreuzberg::ContentFilterConfig {
    fn from(val: JsContentFilterConfig) -> Self {
        let mut __result = kreuzberg::ContentFilterConfig::default();
        if let Some(__v) = val.include_headers {
            __result.include_headers = __v;
        }
        if let Some(__v) = val.include_footers {
            __result.include_footers = __v;
        }
        if let Some(__v) = val.strip_repeating_text {
            __result.strip_repeating_text = __v;
        }
        if let Some(__v) = val.include_watermarks {
            __result.include_watermarks = __v;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ContentFilterConfig> for JsContentFilterConfig {
    fn from(val: kreuzberg::ContentFilterConfig) -> Self {
        Self {
            include_headers: Some(val.include_headers),
            include_footers: Some(val.include_footers),
            strip_repeating_text: Some(val.strip_repeating_text),
            include_watermarks: Some(val.include_watermarks),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsEmailConfig> for kreuzberg::EmailConfig {
    fn from(val: JsEmailConfig) -> Self {
        let mut __result = kreuzberg::EmailConfig::default();
        __result.msg_fallback_codepage = val.msg_fallback_codepage;
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmailConfig> for JsEmailConfig {
    fn from(val: kreuzberg::EmailConfig) -> Self {
        Self {
            msg_fallback_codepage: val.msg_fallback_codepage,
        }
    }
}

#[allow(clippy::needless_update)]
#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsExtractionConfig> for kreuzberg::ExtractionConfig {
    fn from(val: JsExtractionConfig) -> Self {
        let mut __result = kreuzberg::ExtractionConfig::default();
        if let Some(__v) = val.use_cache {
            __result.use_cache = __v;
        }
        if let Some(__v) = val.enable_quality_processing {
            __result.enable_quality_processing = __v;
        }
        __result.ocr = val.ocr.map(Into::into);
        if let Some(__v) = val.force_ocr {
            __result.force_ocr = __v;
        }
        __result.force_ocr_pages = val.force_ocr_pages.map(|v| v.into_iter().collect());
        if let Some(__v) = val.disable_ocr {
            __result.disable_ocr = __v;
        }
        __result.chunking = val.chunking.map(Into::into);
        __result.content_filter = val.content_filter.map(Into::into);
        __result.images = val.images.map(Into::into);
        __result.pdf_options = val.pdf_options.map(Into::into);
        __result.token_reduction = val.token_reduction.map(Into::into);
        __result.language_detection = val.language_detection.map(Into::into);
        __result.pages = val.pages.map(Into::into);
        __result.keywords = val.keywords.map(Into::into);
        __result.postprocessor = val.postprocessor.map(Into::into);
        __result.html_output = val.html_output.map(Into::into);
        __result.extraction_timeout_secs = val.extraction_timeout_secs.map(|v| v as u64);
        __result.max_concurrent_extractions = val.max_concurrent_extractions.map(|v| v as usize);
        if let Some(__v) = val.result_format {
            __result.result_format = __v.into();
        }
        __result.security_limits = val.security_limits.map(Into::into);
        __result.max_embedded_file_bytes = val.max_embedded_file_bytes.map(|v| v as u64);
        if let Some(__v) = val.output_format {
            __result.output_format = __v.into();
        }
        __result.layout = val.layout.map(Into::into);
        if let Some(__v) = val.use_layout_for_markdown {
            __result.use_layout_for_markdown = __v;
        }
        if let Some(__v) = val.include_document_structure {
            __result.include_document_structure = __v;
        }
        __result.acceleration = val.acceleration.map(Into::into);
        __result.cache_namespace = val.cache_namespace;
        __result.cache_ttl_secs = val.cache_ttl_secs.map(|v| v as u64);
        __result.email = val.email.map(Into::into);
        if let Some(__v) = val.max_archive_depth {
            __result.max_archive_depth = __v as usize;
        }
        __result.tree_sitter = val.tree_sitter.map(Into::into);
        __result.structured_extraction = val.structured_extraction.map(Into::into);
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExtractionConfig> for JsExtractionConfig {
    fn from(val: kreuzberg::ExtractionConfig) -> Self {
        Self {
            use_cache: Some(val.use_cache),
            enable_quality_processing: Some(val.enable_quality_processing),
            ocr: val.ocr.map(Into::into),
            force_ocr: Some(val.force_ocr),
            force_ocr_pages: val.force_ocr_pages.map(|v| v.into_iter().collect()),
            disable_ocr: Some(val.disable_ocr),
            chunking: val.chunking.map(Into::into),
            content_filter: val.content_filter.map(Into::into),
            images: val.images.map(Into::into),
            pdf_options: val.pdf_options.map(Into::into),
            token_reduction: val.token_reduction.map(Into::into),
            language_detection: val.language_detection.map(Into::into),
            pages: val.pages.map(Into::into),
            keywords: val.keywords.map(Into::into),
            postprocessor: val.postprocessor.map(Into::into),
            html_options: val.html_options.as_ref().map(|v| format!("{v:?}")),
            html_output: val.html_output.map(Into::into),
            extraction_timeout_secs: val.extraction_timeout_secs.map(|v| v as i64),
            max_concurrent_extractions: val.max_concurrent_extractions.map(|v| v as i64),
            result_format: Some(val.result_format.into()),
            security_limits: val.security_limits.map(Into::into),
            max_embedded_file_bytes: val.max_embedded_file_bytes.map(|v| v as i64),
            output_format: Some(val.output_format.into()),
            layout: val.layout.map(Into::into),
            use_layout_for_markdown: Some(val.use_layout_for_markdown),
            include_document_structure: Some(val.include_document_structure),
            acceleration: val.acceleration.map(Into::into),
            cache_namespace: val.cache_namespace,
            cache_ttl_secs: val.cache_ttl_secs.map(|v| v as i64),
            email: val.email.map(Into::into),
            concurrency: val.concurrency.as_ref().map(|v| format!("{v:?}")),
            max_archive_depth: Some(val.max_archive_depth as i64),
            tree_sitter: val.tree_sitter.map(Into::into),
            structured_extraction: val.structured_extraction.map(Into::into),
            cancel_token: val.cancel_token.as_ref().map(|v| format!("{v:?}")),
        }
    }
}

#[allow(clippy::needless_update)]
#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsFileExtractionConfig> for kreuzberg::FileExtractionConfig {
    fn from(val: JsFileExtractionConfig) -> Self {
        let mut __result = kreuzberg::FileExtractionConfig::default();
        __result.enable_quality_processing = val.enable_quality_processing;
        __result.ocr = val.ocr.map(Into::into);
        __result.force_ocr = val.force_ocr;
        __result.force_ocr_pages = val.force_ocr_pages.map(|v| v.into_iter().collect());
        __result.disable_ocr = val.disable_ocr;
        __result.chunking = val.chunking.map(Into::into);
        __result.content_filter = val.content_filter.map(Into::into);
        __result.images = val.images.map(Into::into);
        __result.pdf_options = val.pdf_options.map(Into::into);
        __result.token_reduction = val.token_reduction.map(Into::into);
        __result.language_detection = val.language_detection.map(Into::into);
        __result.pages = val.pages.map(Into::into);
        __result.keywords = val.keywords.map(Into::into);
        __result.postprocessor = val.postprocessor.map(Into::into);
        __result.result_format = val.result_format.map(Into::into);
        __result.output_format = val.output_format.map(Into::into);
        __result.include_document_structure = val.include_document_structure;
        __result.layout = val.layout.map(Into::into);
        __result.timeout_secs = val.timeout_secs.map(|v| v as u64);
        __result.tree_sitter = val.tree_sitter.map(Into::into);
        __result.structured_extraction = val.structured_extraction.map(Into::into);
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::FileExtractionConfig> for JsFileExtractionConfig {
    fn from(val: kreuzberg::FileExtractionConfig) -> Self {
        Self {
            enable_quality_processing: val.enable_quality_processing,
            ocr: val.ocr.map(Into::into),
            force_ocr: val.force_ocr,
            force_ocr_pages: val.force_ocr_pages.map(|v| v.into_iter().collect()),
            disable_ocr: val.disable_ocr,
            chunking: val.chunking.map(Into::into),
            content_filter: val.content_filter.map(Into::into),
            images: val.images.map(Into::into),
            pdf_options: val.pdf_options.map(Into::into),
            token_reduction: val.token_reduction.map(Into::into),
            language_detection: val.language_detection.map(Into::into),
            pages: val.pages.map(Into::into),
            keywords: val.keywords.map(Into::into),
            postprocessor: val.postprocessor.map(Into::into),
            html_options: val.html_options.as_ref().map(|v| format!("{v:?}")),
            result_format: val.result_format.map(Into::into),
            output_format: val.output_format.map(Into::into),
            include_document_structure: val.include_document_structure,
            layout: val.layout.map(Into::into),
            timeout_secs: val.timeout_secs.map(|v| v as i64),
            tree_sitter: val.tree_sitter.map(Into::into),
            structured_extraction: val.structured_extraction.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsBatchBytesItem> for kreuzberg::BatchBytesItem {
    fn from(val: JsBatchBytesItem) -> Self {
        Self {
            content: val.content.to_vec().into(),
            mime_type: val.mime_type,
            config: val.config.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::BatchBytesItem> for JsBatchBytesItem {
    fn from(val: kreuzberg::BatchBytesItem) -> Self {
        Self {
            content: val.content.to_vec().into(),
            mime_type: val.mime_type,
            config: val.config.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsBatchFileItem> for kreuzberg::BatchFileItem {
    fn from(val: JsBatchFileItem) -> Self {
        Self {
            path: val.path.into(),
            config: val.config.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::BatchFileItem> for JsBatchFileItem {
    fn from(val: kreuzberg::BatchFileItem) -> Self {
        Self {
            path: val.path.to_string_lossy().to_string(),
            config: val.config.map(Into::into),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsImageExtractionConfig> for kreuzberg::ImageExtractionConfig {
    fn from(val: JsImageExtractionConfig) -> Self {
        let mut __result = kreuzberg::ImageExtractionConfig::default();
        if let Some(__v) = val.extract_images {
            __result.extract_images = __v;
        }
        if let Some(__v) = val.target_dpi {
            __result.target_dpi = __v;
        }
        if let Some(__v) = val.max_image_dimension {
            __result.max_image_dimension = __v;
        }
        if let Some(__v) = val.inject_placeholders {
            __result.inject_placeholders = __v;
        }
        if let Some(__v) = val.auto_adjust_dpi {
            __result.auto_adjust_dpi = __v;
        }
        if let Some(__v) = val.min_dpi {
            __result.min_dpi = __v;
        }
        if let Some(__v) = val.max_dpi {
            __result.max_dpi = __v;
        }
        __result.max_images_per_page = val.max_images_per_page;
        if let Some(__v) = val.classify {
            __result.classify = __v;
        }
        if let Some(__v) = val.include_page_rasters {
            __result.include_page_rasters = __v;
        }
        if let Some(__v) = val.run_ocr_on_images {
            __result.run_ocr_on_images = __v;
        }
        if let Some(__v) = val.ocr_text_only {
            __result.ocr_text_only = __v;
        }
        if let Some(__v) = val.append_ocr_text {
            __result.append_ocr_text = __v;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ImageExtractionConfig> for JsImageExtractionConfig {
    fn from(val: kreuzberg::ImageExtractionConfig) -> Self {
        Self {
            extract_images: Some(val.extract_images),
            target_dpi: Some(val.target_dpi),
            max_image_dimension: Some(val.max_image_dimension),
            inject_placeholders: Some(val.inject_placeholders),
            auto_adjust_dpi: Some(val.auto_adjust_dpi),
            min_dpi: Some(val.min_dpi),
            max_dpi: Some(val.max_dpi),
            max_images_per_page: val.max_images_per_page,
            classify: Some(val.classify),
            include_page_rasters: Some(val.include_page_rasters),
            run_ocr_on_images: Some(val.run_ocr_on_images),
            ocr_text_only: Some(val.ocr_text_only),
            append_ocr_text: Some(val.append_ocr_text),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsTokenReductionOptions> for kreuzberg::TokenReductionOptions {
    fn from(val: JsTokenReductionOptions) -> Self {
        let mut __result = kreuzberg::TokenReductionOptions::default();
        if let Some(__v) = val.mode {
            __result.mode = __v;
        }
        if let Some(__v) = val.preserve_important_words {
            __result.preserve_important_words = __v;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TokenReductionOptions> for JsTokenReductionOptions {
    fn from(val: kreuzberg::TokenReductionOptions) -> Self {
        Self {
            mode: Some(val.mode),
            preserve_important_words: Some(val.preserve_important_words),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsLanguageDetectionConfig> for kreuzberg::LanguageDetectionConfig {
    fn from(val: JsLanguageDetectionConfig) -> Self {
        let mut __result = kreuzberg::LanguageDetectionConfig::default();
        if let Some(__v) = val.enabled {
            __result.enabled = __v;
        }
        if let Some(__v) = val.min_confidence {
            __result.min_confidence = __v;
        }
        if let Some(__v) = val.detect_multiple {
            __result.detect_multiple = __v;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::LanguageDetectionConfig> for JsLanguageDetectionConfig {
    fn from(val: kreuzberg::LanguageDetectionConfig) -> Self {
        Self {
            enabled: Some(val.enabled),
            min_confidence: Some(val.min_confidence),
            detect_multiple: Some(val.detect_multiple),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsHtmlOutputConfig> for kreuzberg::HtmlOutputConfig {
    fn from(val: JsHtmlOutputConfig) -> Self {
        let mut __result = kreuzberg::HtmlOutputConfig::default();
        __result.css = val.css;
        __result.css_file = val.css_file.map(Into::into);
        if let Some(__v) = val.theme {
            __result.theme = __v.into();
        }
        if let Some(__v) = val.class_prefix {
            __result.class_prefix = __v;
        }
        if let Some(__v) = val.embed_css {
            __result.embed_css = __v;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HtmlOutputConfig> for JsHtmlOutputConfig {
    fn from(val: kreuzberg::HtmlOutputConfig) -> Self {
        Self {
            css: val.css,
            css_file: val.css_file.map(|p| p.to_string_lossy().to_string()),
            theme: Some(val.theme.into()),
            class_prefix: Some(val.class_prefix),
            embed_css: Some(val.embed_css),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsLayoutDetectionConfig> for kreuzberg::LayoutDetectionConfig {
    fn from(val: JsLayoutDetectionConfig) -> Self {
        let mut __result = kreuzberg::LayoutDetectionConfig::default();
        __result.confidence_threshold = val.confidence_threshold.map(|v| v as f32);
        if let Some(__v) = val.apply_heuristics {
            __result.apply_heuristics = __v;
        }
        if let Some(__v) = val.table_model {
            __result.table_model = __v.into();
        }
        __result.acceleration = val.acceleration.map(Into::into);
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::LayoutDetectionConfig> for JsLayoutDetectionConfig {
    fn from(val: kreuzberg::LayoutDetectionConfig) -> Self {
        Self {
            confidence_threshold: val.confidence_threshold.map(|v| v as f64),
            apply_heuristics: Some(val.apply_heuristics),
            table_model: Some(val.table_model.into()),
            acceleration: val.acceleration.map(Into::into),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsLlmConfig> for kreuzberg::LlmConfig {
    fn from(val: JsLlmConfig) -> Self {
        let mut __result = kreuzberg::LlmConfig::default();
        if let Some(__v) = val.model {
            __result.model = __v;
        }
        __result.api_key = val.api_key;
        __result.base_url = val.base_url;
        __result.timeout_secs = val.timeout_secs.map(|v| v as u64);
        __result.max_retries = val.max_retries;
        __result.temperature = val.temperature;
        __result.max_tokens = val.max_tokens.map(|v| v as u64);
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::LlmConfig> for JsLlmConfig {
    fn from(val: kreuzberg::LlmConfig) -> Self {
        Self {
            model: Some(val.model),
            api_key: val.api_key,
            base_url: val.base_url,
            timeout_secs: val.timeout_secs.map(|v| v as i64),
            max_retries: val.max_retries,
            temperature: val.temperature,
            max_tokens: val.max_tokens.map(|v| v as i64),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsStructuredExtractionConfig> for kreuzberg::StructuredExtractionConfig {
    fn from(val: JsStructuredExtractionConfig) -> Self {
        Self {
            schema: val.schema,
            schema_name: val.schema_name,
            schema_description: val.schema_description,
            strict: val.strict,
            prompt: val.prompt,
            llm: val.llm.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::StructuredExtractionConfig> for JsStructuredExtractionConfig {
    fn from(val: kreuzberg::StructuredExtractionConfig) -> Self {
        Self {
            schema: val.schema,
            schema_name: val.schema_name,
            schema_description: val.schema_description,
            strict: val.strict,
            prompt: val.prompt,
            llm: val.llm.into(),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsOcrQualityThresholds> for kreuzberg::OcrQualityThresholds {
    fn from(val: JsOcrQualityThresholds) -> Self {
        let mut __result = kreuzberg::OcrQualityThresholds::default();
        if let Some(__v) = val.min_total_non_whitespace {
            __result.min_total_non_whitespace = __v as usize;
        }
        if let Some(__v) = val.min_non_whitespace_per_page {
            __result.min_non_whitespace_per_page = __v;
        }
        if let Some(__v) = val.min_meaningful_word_len {
            __result.min_meaningful_word_len = __v as usize;
        }
        if let Some(__v) = val.min_meaningful_words {
            __result.min_meaningful_words = __v as usize;
        }
        if let Some(__v) = val.min_alnum_ratio {
            __result.min_alnum_ratio = __v;
        }
        if let Some(__v) = val.min_garbage_chars {
            __result.min_garbage_chars = __v as usize;
        }
        if let Some(__v) = val.max_fragmented_word_ratio {
            __result.max_fragmented_word_ratio = __v;
        }
        if let Some(__v) = val.critical_fragmented_word_ratio {
            __result.critical_fragmented_word_ratio = __v;
        }
        if let Some(__v) = val.min_avg_word_length {
            __result.min_avg_word_length = __v;
        }
        if let Some(__v) = val.min_words_for_avg_length_check {
            __result.min_words_for_avg_length_check = __v as usize;
        }
        if let Some(__v) = val.min_consecutive_repeat_ratio {
            __result.min_consecutive_repeat_ratio = __v;
        }
        if let Some(__v) = val.min_words_for_repeat_check {
            __result.min_words_for_repeat_check = __v as usize;
        }
        if let Some(__v) = val.substantive_min_chars {
            __result.substantive_min_chars = __v as usize;
        }
        if let Some(__v) = val.non_text_min_chars {
            __result.non_text_min_chars = __v as usize;
        }
        if let Some(__v) = val.alnum_ws_ratio_threshold {
            __result.alnum_ws_ratio_threshold = __v;
        }
        if let Some(__v) = val.pipeline_min_quality {
            __result.pipeline_min_quality = __v;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrQualityThresholds> for JsOcrQualityThresholds {
    fn from(val: kreuzberg::OcrQualityThresholds) -> Self {
        Self {
            min_total_non_whitespace: Some(val.min_total_non_whitespace as i64),
            min_non_whitespace_per_page: Some(val.min_non_whitespace_per_page),
            min_meaningful_word_len: Some(val.min_meaningful_word_len as i64),
            min_meaningful_words: Some(val.min_meaningful_words as i64),
            min_alnum_ratio: Some(val.min_alnum_ratio),
            min_garbage_chars: Some(val.min_garbage_chars as i64),
            max_fragmented_word_ratio: Some(val.max_fragmented_word_ratio),
            critical_fragmented_word_ratio: Some(val.critical_fragmented_word_ratio),
            min_avg_word_length: Some(val.min_avg_word_length),
            min_words_for_avg_length_check: Some(val.min_words_for_avg_length_check as i64),
            min_consecutive_repeat_ratio: Some(val.min_consecutive_repeat_ratio),
            min_words_for_repeat_check: Some(val.min_words_for_repeat_check as i64),
            substantive_min_chars: Some(val.substantive_min_chars as i64),
            non_text_min_chars: Some(val.non_text_min_chars as i64),
            alnum_ws_ratio_threshold: Some(val.alnum_ws_ratio_threshold),
            pipeline_min_quality: Some(val.pipeline_min_quality),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsOcrPipelineStage> for kreuzberg::OcrPipelineStage {
    fn from(val: JsOcrPipelineStage) -> Self {
        Self {
            backend: val.backend,
            priority: val.priority,
            language: val.language,
            tesseract_config: val.tesseract_config.map(Into::into),
            paddle_ocr_config: val.paddle_ocr_config,
            vlm_config: val.vlm_config.map(Into::into),
            backend_options: val.backend_options,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrPipelineStage> for JsOcrPipelineStage {
    fn from(val: kreuzberg::OcrPipelineStage) -> Self {
        Self {
            backend: val.backend,
            priority: val.priority,
            language: val.language,
            tesseract_config: val.tesseract_config.map(Into::into),
            paddle_ocr_config: val.paddle_ocr_config,
            vlm_config: val.vlm_config.map(Into::into),
            backend_options: val.backend_options,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsOcrPipelineConfig> for kreuzberg::OcrPipelineConfig {
    fn from(val: JsOcrPipelineConfig) -> Self {
        Self {
            stages: val.stages.into_iter().map(Into::into).collect(),
            quality_thresholds: val.quality_thresholds.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrPipelineConfig> for JsOcrPipelineConfig {
    fn from(val: kreuzberg::OcrPipelineConfig) -> Self {
        Self {
            stages: val.stages.into_iter().map(Into::into).collect(),
            quality_thresholds: val.quality_thresholds.into(),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsOcrConfig> for kreuzberg::OcrConfig {
    fn from(val: JsOcrConfig) -> Self {
        let mut __result = kreuzberg::OcrConfig::default();
        if let Some(__v) = val.enabled {
            __result.enabled = __v;
        }
        if let Some(__v) = val.backend {
            __result.backend = __v;
        }
        if let Some(__v) = val.language {
            __result.language = __v;
        }
        __result.tesseract_config = val.tesseract_config.map(Into::into);
        __result.output_format = val.output_format.map(Into::into);
        __result.paddle_ocr_config = val.paddle_ocr_config;
        __result.backend_options = val.backend_options;
        __result.element_config = val.element_config.map(Into::into);
        __result.quality_thresholds = val.quality_thresholds.map(Into::into);
        __result.pipeline = val.pipeline.map(Into::into);
        if let Some(__v) = val.auto_rotate {
            __result.auto_rotate = __v;
        }
        __result.vlm_config = val.vlm_config.map(Into::into);
        __result.vlm_prompt = val.vlm_prompt;
        __result.acceleration = val.acceleration.map(Into::into);
        __result.tessdata_bytes = val
            .tessdata_bytes
            .map(|m| m.into_iter().map(|(k, v)| (k, v.to_vec().into())).collect());
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrConfig> for JsOcrConfig {
    fn from(val: kreuzberg::OcrConfig) -> Self {
        Self {
            enabled: Some(val.enabled),
            backend: Some(val.backend),
            language: Some(val.language),
            tesseract_config: val.tesseract_config.map(Into::into),
            output_format: val.output_format.map(Into::into),
            paddle_ocr_config: val.paddle_ocr_config,
            backend_options: val.backend_options,
            element_config: val.element_config.map(Into::into),
            quality_thresholds: val.quality_thresholds.map(Into::into),
            pipeline: val.pipeline.map(Into::into),
            auto_rotate: Some(val.auto_rotate),
            vlm_config: val.vlm_config.map(Into::into),
            vlm_prompt: val.vlm_prompt,
            acceleration: val.acceleration.map(Into::into),
            tessdata_bytes: val
                .tessdata_bytes
                .map(|m| m.into_iter().map(|(k, v)| (k, v.to_vec().into())).collect()),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsPageConfig> for kreuzberg::PageConfig {
    fn from(val: JsPageConfig) -> Self {
        let mut __result = kreuzberg::PageConfig::default();
        if let Some(__v) = val.extract_pages {
            __result.extract_pages = __v;
        }
        if let Some(__v) = val.insert_page_markers {
            __result.insert_page_markers = __v;
        }
        if let Some(__v) = val.marker_format {
            __result.marker_format = __v;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PageConfig> for JsPageConfig {
    fn from(val: kreuzberg::PageConfig) -> Self {
        Self {
            extract_pages: Some(val.extract_pages),
            insert_page_markers: Some(val.insert_page_markers),
            marker_format: Some(val.marker_format),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsPdfConfig> for kreuzberg::PdfConfig {
    fn from(val: JsPdfConfig) -> Self {
        let mut __result = kreuzberg::PdfConfig::default();
        if let Some(__v) = val.extract_images {
            __result.extract_images = __v;
        }
        if let Some(__v) = val.extract_tables {
            __result.extract_tables = __v;
        }
        __result.passwords = val.passwords.map(|v| v.into_iter().collect());
        if let Some(__v) = val.extract_metadata {
            __result.extract_metadata = __v;
        }
        __result.hierarchy = val.hierarchy.map(Into::into);
        if let Some(__v) = val.extract_annotations {
            __result.extract_annotations = __v;
        }
        __result.top_margin_fraction = val.top_margin_fraction.map(|v| v as f32);
        __result.bottom_margin_fraction = val.bottom_margin_fraction.map(|v| v as f32);
        if let Some(__v) = val.allow_single_column_tables {
            __result.allow_single_column_tables = __v;
        }
        if let Some(__v) = val.ocr_inline_images {
            __result.ocr_inline_images = __v;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PdfConfig> for JsPdfConfig {
    fn from(val: kreuzberg::PdfConfig) -> Self {
        Self {
            extract_images: Some(val.extract_images),
            extract_tables: Some(val.extract_tables),
            passwords: val.passwords.map(|v| v.into_iter().collect()),
            extract_metadata: Some(val.extract_metadata),
            hierarchy: val.hierarchy.map(Into::into),
            extract_annotations: Some(val.extract_annotations),
            top_margin_fraction: val.top_margin_fraction.map(|v| v as f64),
            bottom_margin_fraction: val.bottom_margin_fraction.map(|v| v as f64),
            allow_single_column_tables: Some(val.allow_single_column_tables),
            ocr_inline_images: Some(val.ocr_inline_images),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsHierarchyConfig> for kreuzberg::HierarchyConfig {
    fn from(val: JsHierarchyConfig) -> Self {
        let mut __result = kreuzberg::HierarchyConfig::default();
        if let Some(__v) = val.enabled {
            __result.enabled = __v;
        }
        if let Some(__v) = val.k_clusters {
            __result.k_clusters = __v as usize;
        }
        if let Some(__v) = val.include_bbox {
            __result.include_bbox = __v;
        }
        __result.ocr_coverage_threshold = val.ocr_coverage_threshold.map(|v| v as f32);
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HierarchyConfig> for JsHierarchyConfig {
    fn from(val: kreuzberg::HierarchyConfig) -> Self {
        Self {
            enabled: Some(val.enabled),
            k_clusters: Some(val.k_clusters as i64),
            include_bbox: Some(val.include_bbox),
            ocr_coverage_threshold: val.ocr_coverage_threshold.map(|v| v as f64),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsPostProcessorConfig> for kreuzberg::PostProcessorConfig {
    fn from(val: JsPostProcessorConfig) -> Self {
        let mut __result = kreuzberg::PostProcessorConfig::default();
        if let Some(__v) = val.enabled {
            __result.enabled = __v;
        }
        __result.enabled_processors = val.enabled_processors.map(|v| v.into_iter().collect());
        __result.disabled_processors = val.disabled_processors.map(|v| v.into_iter().collect());
        __result.enabled_set = val.enabled_set.map(|v| v.into_iter().collect());
        __result.disabled_set = val.disabled_set.map(|v| v.into_iter().collect());
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PostProcessorConfig> for JsPostProcessorConfig {
    fn from(val: kreuzberg::PostProcessorConfig) -> Self {
        Self {
            enabled: Some(val.enabled),
            enabled_processors: val.enabled_processors.map(|v| v.into_iter().collect()),
            disabled_processors: val.disabled_processors.map(|v| v.into_iter().collect()),
            enabled_set: val.enabled_set.map(|v| v.into_iter().collect()),
            disabled_set: val.disabled_set.map(|v| v.into_iter().collect()),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsChunkingConfig> for kreuzberg::ChunkingConfig {
    fn from(val: JsChunkingConfig) -> Self {
        let mut __result = kreuzberg::ChunkingConfig::default();
        if let Some(__v) = val.max_characters {
            __result.max_characters = __v as usize;
        }
        if let Some(__v) = val.overlap {
            __result.overlap = __v as usize;
        }
        if let Some(__v) = val.trim {
            __result.trim = __v;
        }
        if let Some(__v) = val.chunker_type {
            __result.chunker_type = __v.into();
        }
        __result.embedding = val.embedding.map(Into::into);
        __result.preset = val.preset;
        if let Some(__v) = val.sizing {
            __result.sizing = __v.into();
        }
        if let Some(__v) = val.prepend_heading_context {
            __result.prepend_heading_context = __v;
        }
        __result.topic_threshold = val.topic_threshold.map(|v| v as f32);
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ChunkingConfig> for JsChunkingConfig {
    fn from(val: kreuzberg::ChunkingConfig) -> Self {
        Self {
            max_characters: Some(val.max_characters as i64),
            overlap: Some(val.overlap as i64),
            trim: Some(val.trim),
            chunker_type: Some(val.chunker_type.into()),
            embedding: val.embedding.map(Into::into),
            preset: val.preset,
            sizing: Some(val.sizing.into()),
            prepend_heading_context: Some(val.prepend_heading_context),
            topic_threshold: val.topic_threshold.map(|v| v as f64),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsEmbeddingConfig> for kreuzberg::EmbeddingConfig {
    fn from(val: JsEmbeddingConfig) -> Self {
        let mut __result = kreuzberg::EmbeddingConfig::default();
        if let Some(__v) = val.model {
            __result.model = __v.into();
        }
        if let Some(__v) = val.normalize {
            __result.normalize = __v;
        }
        if let Some(__v) = val.batch_size {
            __result.batch_size = __v as usize;
        }
        if let Some(__v) = val.show_download_progress {
            __result.show_download_progress = __v;
        }
        __result.cache_dir = val.cache_dir.map(Into::into);
        __result.acceleration = val.acceleration.map(Into::into);
        __result.max_embed_duration_secs = val.max_embed_duration_secs.map(|v| v as u64);
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmbeddingConfig> for JsEmbeddingConfig {
    fn from(val: kreuzberg::EmbeddingConfig) -> Self {
        Self {
            model: Some(val.model.into()),
            normalize: Some(val.normalize),
            batch_size: Some(val.batch_size as i64),
            show_download_progress: Some(val.show_download_progress),
            cache_dir: val.cache_dir.map(|p| p.to_string_lossy().to_string()),
            acceleration: val.acceleration.map(Into::into),
            max_embed_duration_secs: val.max_embed_duration_secs.map(|v| v as i64),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsTreeSitterConfig> for kreuzberg::TreeSitterConfig {
    fn from(val: JsTreeSitterConfig) -> Self {
        let mut __result = kreuzberg::TreeSitterConfig::default();
        if let Some(__v) = val.enabled {
            __result.enabled = __v;
        }
        __result.cache_dir = val.cache_dir.map(Into::into);
        __result.languages = val.languages.map(|v| v.into_iter().collect());
        __result.groups = val.groups.map(|v| v.into_iter().collect());
        if let Some(__v) = val.process {
            __result.process = __v.into();
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TreeSitterConfig> for JsTreeSitterConfig {
    fn from(val: kreuzberg::TreeSitterConfig) -> Self {
        Self {
            enabled: Some(val.enabled),
            cache_dir: val.cache_dir.map(|p| p.to_string_lossy().to_string()),
            languages: val.languages.map(|v| v.into_iter().collect()),
            groups: val.groups.map(|v| v.into_iter().collect()),
            process: Some(val.process.into()),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsTreeSitterProcessConfig> for kreuzberg::TreeSitterProcessConfig {
    fn from(val: JsTreeSitterProcessConfig) -> Self {
        let mut __result = kreuzberg::TreeSitterProcessConfig::default();
        if let Some(__v) = val.structure {
            __result.structure = __v;
        }
        if let Some(__v) = val.imports {
            __result.imports = __v;
        }
        if let Some(__v) = val.exports {
            __result.exports = __v;
        }
        if let Some(__v) = val.comments {
            __result.comments = __v;
        }
        if let Some(__v) = val.docstrings {
            __result.docstrings = __v;
        }
        if let Some(__v) = val.symbols {
            __result.symbols = __v;
        }
        if let Some(__v) = val.diagnostics {
            __result.diagnostics = __v;
        }
        __result.chunk_max_size = val.chunk_max_size.map(|v| v as usize);
        if let Some(__v) = val.content_mode {
            __result.content_mode = __v.into();
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TreeSitterProcessConfig> for JsTreeSitterProcessConfig {
    fn from(val: kreuzberg::TreeSitterProcessConfig) -> Self {
        Self {
            structure: Some(val.structure),
            imports: Some(val.imports),
            exports: Some(val.exports),
            comments: Some(val.comments),
            docstrings: Some(val.docstrings),
            symbols: Some(val.symbols),
            diagnostics: Some(val.diagnostics),
            chunk_max_size: val.chunk_max_size.map(|v| v as i64),
            content_mode: Some(val.content_mode.into()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::SupportedFormat> for JsSupportedFormat {
    fn from(val: kreuzberg::SupportedFormat) -> Self {
        Self {
            extension: val.extension,
            mime_type: val.mime_type,
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsServerConfig> for kreuzberg::ServerConfig {
    fn from(val: JsServerConfig) -> Self {
        let mut __result = kreuzberg::ServerConfig::default();
        if let Some(__v) = val.host {
            __result.host = __v;
        }
        if let Some(__v) = val.port {
            __result.port = __v;
        }
        if let Some(__v) = val.cors_origins {
            __result.cors_origins = __v.into_iter().collect();
        }
        if let Some(__v) = val.max_request_body_bytes {
            __result.max_request_body_bytes = __v as usize;
        }
        if let Some(__v) = val.max_multipart_field_bytes {
            __result.max_multipart_field_bytes = __v as usize;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ServerConfig> for JsServerConfig {
    fn from(val: kreuzberg::ServerConfig) -> Self {
        Self {
            host: Some(val.host),
            port: Some(val.port),
            cors_origins: Some(val.cors_origins.into_iter().collect()),
            max_request_body_bytes: Some(val.max_request_body_bytes as i64),
            max_multipart_field_bytes: Some(val.max_multipart_field_bytes as i64),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::extraction::structured::StructuredDataResult> for JsStructuredDataResult {
    fn from(val: kreuzberg::extraction::structured::StructuredDataResult) -> Self {
        Self {
            content: val.content,
            format: val.format.to_string(),
            metadata: val.metadata.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
            text_fields: val.text_fields.into_iter().collect(),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsDocxAppProperties> for kreuzberg::DocxAppProperties {
    fn from(val: JsDocxAppProperties) -> Self {
        let mut __result = kreuzberg::DocxAppProperties::default();
        __result.application = val.application;
        __result.app_version = val.app_version;
        __result.template = val.template;
        __result.total_time = val.total_time;
        __result.pages = val.pages;
        __result.words = val.words;
        __result.characters = val.characters;
        __result.characters_with_spaces = val.characters_with_spaces;
        __result.lines = val.lines;
        __result.paragraphs = val.paragraphs;
        __result.company = val.company;
        __result.doc_security = val.doc_security;
        __result.scale_crop = val.scale_crop;
        __result.links_up_to_date = val.links_up_to_date;
        __result.shared_doc = val.shared_doc;
        __result.hyperlinks_changed = val.hyperlinks_changed;
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DocxAppProperties> for JsDocxAppProperties {
    fn from(val: kreuzberg::DocxAppProperties) -> Self {
        Self {
            application: val.application,
            app_version: val.app_version,
            template: val.template,
            total_time: val.total_time,
            pages: val.pages,
            words: val.words,
            characters: val.characters,
            characters_with_spaces: val.characters_with_spaces,
            lines: val.lines,
            paragraphs: val.paragraphs,
            company: val.company,
            doc_security: val.doc_security,
            scale_crop: val.scale_crop,
            links_up_to_date: val.links_up_to_date,
            shared_doc: val.shared_doc,
            hyperlinks_changed: val.hyperlinks_changed,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::extraction::office_metadata::app_properties::XlsxAppProperties> for JsXlsxAppProperties {
    fn from(val: kreuzberg::extraction::office_metadata::app_properties::XlsxAppProperties) -> Self {
        Self {
            application: val.application,
            app_version: val.app_version,
            doc_security: val.doc_security,
            scale_crop: val.scale_crop,
            links_up_to_date: val.links_up_to_date,
            shared_doc: val.shared_doc,
            hyperlinks_changed: val.hyperlinks_changed,
            company: val.company,
            worksheet_names: Some(val.worksheet_names.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::extraction::office_metadata::app_properties::PptxAppProperties> for JsPptxAppProperties {
    fn from(val: kreuzberg::extraction::office_metadata::app_properties::PptxAppProperties) -> Self {
        Self {
            application: val.application,
            app_version: val.app_version,
            total_time: val.total_time,
            company: val.company,
            doc_security: val.doc_security,
            scale_crop: val.scale_crop,
            links_up_to_date: val.links_up_to_date,
            shared_doc: val.shared_doc,
            hyperlinks_changed: val.hyperlinks_changed,
            slides: val.slides,
            notes: val.notes,
            hidden_slides: val.hidden_slides,
            multimedia_clips: val.multimedia_clips,
            presentation_format: val.presentation_format,
            slide_titles: Some(val.slide_titles.into_iter().collect()),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsCoreProperties> for kreuzberg::CoreProperties {
    fn from(val: JsCoreProperties) -> Self {
        let mut __result = kreuzberg::CoreProperties::default();
        __result.title = val.title;
        __result.subject = val.subject;
        __result.creator = val.creator;
        __result.keywords = val.keywords;
        __result.description = val.description;
        __result.last_modified_by = val.last_modified_by;
        __result.revision = val.revision;
        __result.created = val.created;
        __result.modified = val.modified;
        __result.category = val.category;
        __result.content_status = val.content_status;
        __result.language = val.language;
        __result.identifier = val.identifier;
        __result.version = val.version;
        __result.last_printed = val.last_printed;
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::CoreProperties> for JsCoreProperties {
    fn from(val: kreuzberg::CoreProperties) -> Self {
        Self {
            title: val.title,
            subject: val.subject,
            creator: val.creator,
            keywords: val.keywords,
            description: val.description,
            last_modified_by: val.last_modified_by,
            revision: val.revision,
            created: val.created,
            modified: val.modified,
            category: val.category,
            content_status: val.content_status,
            language: val.language,
            identifier: val.identifier,
            version: val.version,
            last_printed: val.last_printed,
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsSecurityLimits> for kreuzberg::SecurityLimits {
    fn from(val: JsSecurityLimits) -> Self {
        let mut __result = kreuzberg::SecurityLimits::default();
        if let Some(__v) = val.max_archive_size {
            __result.max_archive_size = __v as usize;
        }
        if let Some(__v) = val.max_compression_ratio {
            __result.max_compression_ratio = __v as usize;
        }
        if let Some(__v) = val.max_files_in_archive {
            __result.max_files_in_archive = __v as usize;
        }
        if let Some(__v) = val.max_nesting_depth {
            __result.max_nesting_depth = __v as usize;
        }
        if let Some(__v) = val.max_entity_length {
            __result.max_entity_length = __v as usize;
        }
        if let Some(__v) = val.max_content_size {
            __result.max_content_size = __v as usize;
        }
        if let Some(__v) = val.max_iterations {
            __result.max_iterations = __v as usize;
        }
        if let Some(__v) = val.max_xml_depth {
            __result.max_xml_depth = __v as usize;
        }
        if let Some(__v) = val.max_table_cells {
            __result.max_table_cells = __v as usize;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::SecurityLimits> for JsSecurityLimits {
    fn from(val: kreuzberg::SecurityLimits) -> Self {
        Self {
            max_archive_size: Some(val.max_archive_size as i64),
            max_compression_ratio: Some(val.max_compression_ratio as i64),
            max_files_in_archive: Some(val.max_files_in_archive as i64),
            max_nesting_depth: Some(val.max_nesting_depth as i64),
            max_entity_length: Some(val.max_entity_length as i64),
            max_content_size: Some(val.max_content_size as i64),
            max_iterations: Some(val.max_iterations as i64),
            max_xml_depth: Some(val.max_xml_depth as i64),
            max_table_cells: Some(val.max_table_cells as i64),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsTokenReductionConfig> for kreuzberg::TokenReductionConfig {
    fn from(val: JsTokenReductionConfig) -> Self {
        let mut __result = kreuzberg::TokenReductionConfig::default();
        if let Some(__v) = val.level {
            __result.level = __v.into();
        }
        __result.language_hint = val.language_hint;
        if let Some(__v) = val.preserve_markdown {
            __result.preserve_markdown = __v;
        }
        if let Some(__v) = val.preserve_code {
            __result.preserve_code = __v;
        }
        if let Some(__v) = val.semantic_threshold {
            __result.semantic_threshold = __v as f32;
        }
        if let Some(__v) = val.enable_parallel {
            __result.enable_parallel = __v;
        }
        if let Some(__v) = val.use_simd {
            __result.use_simd = __v;
        }
        __result.custom_stopwords = val.custom_stopwords.map(|m| m.into_iter().collect());
        if let Some(__v) = val.preserve_patterns {
            __result.preserve_patterns = __v.into_iter().collect();
        }
        __result.target_reduction = val.target_reduction.map(|v| v as f32);
        if let Some(__v) = val.enable_semantic_clustering {
            __result.enable_semantic_clustering = __v;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TokenReductionConfig> for JsTokenReductionConfig {
    fn from(val: kreuzberg::TokenReductionConfig) -> Self {
        Self {
            level: Some(val.level.into()),
            language_hint: val.language_hint,
            preserve_markdown: Some(val.preserve_markdown),
            preserve_code: Some(val.preserve_code),
            semantic_threshold: Some(val.semantic_threshold as f64),
            enable_parallel: Some(val.enable_parallel),
            use_simd: Some(val.use_simd),
            custom_stopwords: val.custom_stopwords.map(|m| m.into_iter().collect()),
            preserve_patterns: Some(val.preserve_patterns.into_iter().collect()),
            target_reduction: val.target_reduction.map(|v| v as f64),
            enable_semantic_clustering: Some(val.enable_semantic_clustering),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsPdfAnnotation> for kreuzberg::PdfAnnotation {
    fn from(val: JsPdfAnnotation) -> Self {
        Self {
            annotation_type: val.annotation_type.into(),
            content: val.content,
            page_number: val.page_number,
            bounding_box: val.bounding_box.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PdfAnnotation> for JsPdfAnnotation {
    fn from(val: kreuzberg::PdfAnnotation) -> Self {
        Self {
            annotation_type: val.annotation_type.into(),
            content: val.content,
            page_number: val.page_number,
            bounding_box: val.bounding_box.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsDjotContent> for kreuzberg::DjotContent {
    fn from(val: JsDjotContent) -> Self {
        Self {
            plain_text: val.plain_text,
            blocks: val.blocks.into_iter().map(Into::into).collect(),
            metadata: val.metadata.into(),
            tables: val.tables.into_iter().map(Into::into).collect(),
            images: val.images.into_iter().map(Into::into).collect(),
            links: val.links.into_iter().map(Into::into).collect(),
            footnotes: val.footnotes.into_iter().map(Into::into).collect(),
            attributes: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DjotContent> for JsDjotContent {
    fn from(val: kreuzberg::DjotContent) -> Self {
        Self {
            plain_text: val.plain_text,
            blocks: val.blocks.into_iter().map(Into::into).collect(),
            metadata: val.metadata.into(),
            tables: val.tables.into_iter().map(Into::into).collect(),
            images: val.images.into_iter().map(Into::into).collect(),
            links: val.links.into_iter().map(Into::into).collect(),
            footnotes: val.footnotes.into_iter().map(Into::into).collect(),
            attributes: val.attributes.iter().map(|i| format!("{:?}", i)).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsFormattedBlock> for kreuzberg::FormattedBlock {
    fn from(val: JsFormattedBlock) -> Self {
        Self {
            block_type: val.block_type.into(),
            level: val.level.map(|v| v as usize),
            inline_content: val.inline_content.into_iter().map(Into::into).collect(),
            attributes: Default::default(),
            language: val.language,
            code: val.code,
            children: val.children.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::FormattedBlock> for JsFormattedBlock {
    fn from(val: kreuzberg::FormattedBlock) -> Self {
        Self {
            block_type: val.block_type.into(),
            level: val.level.map(|v| v as i64),
            inline_content: val.inline_content.into_iter().map(Into::into).collect(),
            attributes: val.attributes.as_ref().map(|v| format!("{v:?}")),
            language: val.language,
            code: val.code,
            children: val.children.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsInlineElement> for kreuzberg::InlineElement {
    fn from(val: JsInlineElement) -> Self {
        Self {
            element_type: val.element_type.into(),
            content: val.content,
            attributes: Default::default(),
            metadata: val
                .metadata
                .map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::InlineElement> for JsInlineElement {
    fn from(val: kreuzberg::InlineElement) -> Self {
        Self {
            element_type: val.element_type.into(),
            content: val.content,
            attributes: val.attributes.as_ref().map(|v| format!("{v:?}")),
            metadata: val
                .metadata
                .map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsDjotImage> for kreuzberg::DjotImage {
    fn from(val: JsDjotImage) -> Self {
        Self {
            src: val.src,
            alt: val.alt,
            title: val.title,
            attributes: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DjotImage> for JsDjotImage {
    fn from(val: kreuzberg::DjotImage) -> Self {
        Self {
            src: val.src,
            alt: val.alt,
            title: val.title,
            attributes: val.attributes.as_ref().map(|v| format!("{v:?}")),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsDjotLink> for kreuzberg::DjotLink {
    fn from(val: JsDjotLink) -> Self {
        Self {
            url: val.url,
            text: val.text,
            title: val.title,
            attributes: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DjotLink> for JsDjotLink {
    fn from(val: kreuzberg::DjotLink) -> Self {
        Self {
            url: val.url,
            text: val.text,
            title: val.title,
            attributes: val.attributes.as_ref().map(|v| format!("{v:?}")),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsFootnote> for kreuzberg::Footnote {
    fn from(val: JsFootnote) -> Self {
        Self {
            label: val.label,
            content: val.content.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::Footnote> for JsFootnote {
    fn from(val: kreuzberg::Footnote) -> Self {
        Self {
            label: val.label,
            content: val.content.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsDocumentStructure> for kreuzberg::DocumentStructure {
    fn from(val: JsDocumentStructure) -> Self {
        let mut __result = kreuzberg::DocumentStructure::default();
        if let Some(__v) = val.nodes {
            __result.nodes = __v.into_iter().map(Into::into).collect();
        }
        __result.source_format = val.source_format;
        if let Some(__v) = val.relationships {
            __result.relationships = __v.into_iter().map(Into::into).collect();
        }
        if let Some(__v) = val.node_types {
            __result.node_types = __v.into_iter().collect();
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DocumentStructure> for JsDocumentStructure {
    fn from(val: kreuzberg::DocumentStructure) -> Self {
        Self {
            nodes: Some(val.nodes.into_iter().map(Into::into).collect()),
            source_format: val.source_format,
            relationships: Some(val.relationships.into_iter().map(Into::into).collect()),
            node_types: Some(val.node_types.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsDocumentRelationship> for kreuzberg::DocumentRelationship {
    fn from(val: JsDocumentRelationship) -> Self {
        Self {
            source: kreuzberg::NodeIndex(val.source),
            target: kreuzberg::NodeIndex(val.target),
            kind: val.kind.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DocumentRelationship> for JsDocumentRelationship {
    fn from(val: kreuzberg::DocumentRelationship) -> Self {
        Self {
            source: val.source.0,
            target: val.target.0,
            kind: val.kind.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsDocumentNode> for kreuzberg::DocumentNode {
    fn from(val: JsDocumentNode) -> Self {
        Self {
            id: Default::default(),
            content: val.content.into(),
            parent: (val.parent).map(kreuzberg::NodeIndex),
            children: (val.children.into_iter().collect::<Vec<_>>())
                .into_iter()
                .map(kreuzberg::NodeIndex)
                .collect(),
            content_layer: val.content_layer.into(),
            page: val.page,
            page_end: val.page_end,
            bbox: val.bbox.map(Into::into),
            annotations: val.annotations.into_iter().map(Into::into).collect(),
            attributes: val
                .attributes
                .map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DocumentNode> for JsDocumentNode {
    fn from(val: kreuzberg::DocumentNode) -> Self {
        Self {
            id: format!("{:?}", val.id),
            content: val.content.into(),
            parent: val.parent.map(|v| v.0),
            children: val
                .children
                .iter()
                .map(|v| v.0)
                .collect::<Vec<_>>()
                .into_iter()
                .collect(),
            content_layer: val.content_layer.into(),
            page: val.page,
            page_end: val.page_end,
            bbox: val.bbox.map(Into::into),
            annotations: val.annotations.into_iter().map(Into::into).collect(),
            attributes: val
                .attributes
                .map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect()),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsTableGrid> for kreuzberg::TableGrid {
    fn from(val: JsTableGrid) -> Self {
        let mut __result = kreuzberg::TableGrid::default();
        if let Some(__v) = val.rows {
            __result.rows = __v;
        }
        if let Some(__v) = val.cols {
            __result.cols = __v;
        }
        if let Some(__v) = val.cells {
            __result.cells = __v.into_iter().map(Into::into).collect();
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TableGrid> for JsTableGrid {
    fn from(val: kreuzberg::TableGrid) -> Self {
        Self {
            rows: Some(val.rows),
            cols: Some(val.cols),
            cells: Some(val.cells.into_iter().map(Into::into).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsGridCell> for kreuzberg::GridCell {
    fn from(val: JsGridCell) -> Self {
        Self {
            content: val.content,
            row: val.row,
            col: val.col,
            row_span: val.row_span,
            col_span: val.col_span,
            is_header: val.is_header,
            bbox: val.bbox.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::GridCell> for JsGridCell {
    fn from(val: kreuzberg::GridCell) -> Self {
        Self {
            content: val.content,
            row: val.row,
            col: val.col,
            row_span: val.row_span,
            col_span: val.col_span,
            is_header: val.is_header,
            bbox: val.bbox.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsTextAnnotation> for kreuzberg::TextAnnotation {
    fn from(val: JsTextAnnotation) -> Self {
        Self {
            start: val.start,
            end: val.end,
            kind: val.kind.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TextAnnotation> for JsTextAnnotation {
    fn from(val: kreuzberg::TextAnnotation) -> Self {
        Self {
            start: val.start,
            end: val.end,
            kind: val.kind.into(),
        }
    }
}

#[allow(clippy::needless_update)]
#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsExtractionResult> for kreuzberg::ExtractionResult {
    fn from(val: JsExtractionResult) -> Self {
        let mut __result = kreuzberg::ExtractionResult::default();
        if let Some(__v) = val.content {
            __result.content = __v;
        }
        if let Some(__v) = val.mime_type {
            __result.mime_type = __v.into();
        }
        if let Some(__v) = val.metadata {
            __result.metadata = __v.into();
        }
        __result.extraction_method = val.extraction_method.map(Into::into);
        if let Some(__v) = val.tables {
            __result.tables = __v.into_iter().map(Into::into).collect();
        }
        __result.detected_languages = val.detected_languages.map(|v| v.into_iter().collect());
        __result.chunks = val.chunks.map(|v| v.into_iter().map(Into::into).collect());
        __result.images = val.images.map(|v| v.into_iter().map(Into::into).collect());
        __result.pages = val.pages.map(|v| v.into_iter().map(Into::into).collect());
        __result.elements = val.elements.map(|v| v.into_iter().map(Into::into).collect());
        __result.djot_content = val.djot_content.map(Into::into);
        __result.ocr_elements = val.ocr_elements.map(|v| v.into_iter().map(Into::into).collect());
        __result.document = val.document.map(Into::into);
        __result.extracted_keywords = val.extracted_keywords.map(|v| v.into_iter().map(Into::into).collect());
        __result.quality_score = val.quality_score;
        if let Some(__v) = val.processing_warnings {
            __result.processing_warnings = __v.into_iter().map(Into::into).collect();
        }
        __result.annotations = val.annotations.map(|v| v.into_iter().map(Into::into).collect());
        __result.children = val.children.map(|v| v.into_iter().map(Into::into).collect());
        __result.uris = val.uris.map(|v| v.into_iter().map(Into::into).collect());
        __result.revisions = val.revisions.map(|v| v.into_iter().map(Into::into).collect());
        __result.structured_output = val.structured_output;
        __result.code_intelligence = val.code_intelligence;
        __result.llm_usage = val.llm_usage.map(|v| v.into_iter().map(Into::into).collect());
        __result.formatted_content = val.formatted_content;
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExtractionResult> for JsExtractionResult {
    fn from(val: kreuzberg::ExtractionResult) -> Self {
        Self {
            content: Some(val.content),
            mime_type: Some(val.mime_type.to_string()),
            metadata: Some(val.metadata.into()),
            extraction_method: val.extraction_method.map(Into::into),
            tables: Some(val.tables.into_iter().map(Into::into).collect()),
            detected_languages: val.detected_languages.map(|v| v.into_iter().collect()),
            chunks: val.chunks.map(|v| v.into_iter().map(Into::into).collect()),
            images: val.images.map(|v| v.into_iter().map(Into::into).collect()),
            pages: val.pages.map(|v| v.into_iter().map(Into::into).collect()),
            elements: val.elements.map(|v| v.into_iter().map(Into::into).collect()),
            djot_content: val.djot_content.map(Into::into),
            ocr_elements: val.ocr_elements.map(|v| v.into_iter().map(Into::into).collect()),
            document: val.document.map(Into::into),
            extracted_keywords: val.extracted_keywords.map(|v| v.into_iter().map(Into::into).collect()),
            quality_score: val.quality_score,
            processing_warnings: Some(val.processing_warnings.into_iter().map(Into::into).collect()),
            annotations: val.annotations.map(|v| v.into_iter().map(Into::into).collect()),
            children: val.children.map(|v| v.into_iter().map(Into::into).collect()),
            uris: val.uris.map(|v| v.into_iter().map(Into::into).collect()),
            revisions: val.revisions.map(|v| v.into_iter().map(Into::into).collect()),
            structured_output: val.structured_output,
            code_intelligence: val.code_intelligence,
            llm_usage: val.llm_usage.map(|v| v.into_iter().map(Into::into).collect()),
            formatted_content: val.formatted_content,
            ocr_internal_document: val.ocr_internal_document.as_ref().map(|v| format!("{v:?}")),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsArchiveEntry> for kreuzberg::ArchiveEntry {
    fn from(val: JsArchiveEntry) -> Self {
        Self {
            path: val.path,
            mime_type: val.mime_type,
            result: Box::new(val.result.into()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ArchiveEntry> for JsArchiveEntry {
    fn from(val: kreuzberg::ArchiveEntry) -> Self {
        Self {
            path: val.path,
            mime_type: val.mime_type,
            result: (*val.result).into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsProcessingWarning> for kreuzberg::ProcessingWarning {
    fn from(val: JsProcessingWarning) -> Self {
        Self {
            source: val.source.into(),
            message: val.message.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ProcessingWarning> for JsProcessingWarning {
    fn from(val: kreuzberg::ProcessingWarning) -> Self {
        Self {
            source: val.source.to_string(),
            message: val.message.to_string(),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsLlmUsage> for kreuzberg::LlmUsage {
    fn from(val: JsLlmUsage) -> Self {
        let mut __result = kreuzberg::LlmUsage::default();
        if let Some(__v) = val.model {
            __result.model = __v;
        }
        if let Some(__v) = val.source {
            __result.source = __v;
        }
        __result.input_tokens = val.input_tokens.map(|v| v as u64);
        __result.output_tokens = val.output_tokens.map(|v| v as u64);
        __result.total_tokens = val.total_tokens.map(|v| v as u64);
        __result.estimated_cost = val.estimated_cost;
        __result.finish_reason = val.finish_reason;
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::LlmUsage> for JsLlmUsage {
    fn from(val: kreuzberg::LlmUsage) -> Self {
        Self {
            model: Some(val.model),
            source: Some(val.source),
            input_tokens: val.input_tokens.map(|v| v as i64),
            output_tokens: val.output_tokens.map(|v| v as i64),
            total_tokens: val.total_tokens.map(|v| v as i64),
            estimated_cost: val.estimated_cost,
            finish_reason: val.finish_reason,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsChunk> for kreuzberg::Chunk {
    fn from(val: JsChunk) -> Self {
        Self {
            content: val.content,
            chunk_type: val.chunk_type.into(),
            embedding: val.embedding.map(|v| v.into_iter().map(|x| x as f32).collect()),
            metadata: val.metadata.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::Chunk> for JsChunk {
    fn from(val: kreuzberg::Chunk) -> Self {
        Self {
            content: val.content,
            chunk_type: val.chunk_type.into(),
            embedding: val.embedding.as_ref().map(|v| v.iter().map(|&x| x as f64).collect()),
            metadata: val.metadata.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsHeadingContext> for kreuzberg::HeadingContext {
    fn from(val: JsHeadingContext) -> Self {
        Self {
            headings: val.headings.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HeadingContext> for JsHeadingContext {
    fn from(val: kreuzberg::HeadingContext) -> Self {
        Self {
            headings: val.headings.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsHeadingLevel> for kreuzberg::HeadingLevel {
    fn from(val: JsHeadingLevel) -> Self {
        Self {
            level: val.level,
            text: val.text,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HeadingLevel> for JsHeadingLevel {
    fn from(val: kreuzberg::HeadingLevel) -> Self {
        Self {
            level: val.level,
            text: val.text,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsChunkMetadata> for kreuzberg::ChunkMetadata {
    fn from(val: JsChunkMetadata) -> Self {
        Self {
            byte_start: val.byte_start as usize,
            byte_end: val.byte_end as usize,
            token_count: val.token_count.map(|v| v as usize),
            chunk_index: val.chunk_index as usize,
            total_chunks: val.total_chunks as usize,
            first_page: val.first_page,
            last_page: val.last_page,
            heading_context: val.heading_context.map(Into::into),
            image_indices: val.image_indices.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ChunkMetadata> for JsChunkMetadata {
    fn from(val: kreuzberg::ChunkMetadata) -> Self {
        Self {
            byte_start: val.byte_start as i64,
            byte_end: val.byte_end as i64,
            token_count: val.token_count.map(|v| v as i64),
            chunk_index: val.chunk_index as i64,
            total_chunks: val.total_chunks as i64,
            first_page: val.first_page,
            last_page: val.last_page,
            heading_context: val.heading_context.map(Into::into),
            image_indices: val.image_indices.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsExtractedImage> for kreuzberg::ExtractedImage {
    fn from(val: JsExtractedImage) -> Self {
        Self {
            data: val.data.to_vec().into(),
            format: val.format.into(),
            image_index: val.image_index,
            page_number: val.page_number,
            width: val.width,
            height: val.height,
            colorspace: val.colorspace,
            bits_per_component: val.bits_per_component,
            is_mask: val.is_mask,
            description: val.description,
            ocr_result: val.ocr_result.map(Into::into).map(Box::new),
            bounding_box: val.bounding_box.map(Into::into),
            source_path: val.source_path,
            image_kind: val.image_kind.map(Into::into),
            kind_confidence: val.kind_confidence.map(|v| v as f32),
            cluster_id: val.cluster_id,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExtractedImage> for JsExtractedImage {
    fn from(val: kreuzberg::ExtractedImage) -> Self {
        Self {
            data: val.data.to_vec().into(),
            format: val.format.to_string(),
            image_index: val.image_index,
            page_number: val.page_number,
            width: val.width,
            height: val.height,
            colorspace: val.colorspace,
            bits_per_component: val.bits_per_component,
            is_mask: val.is_mask,
            description: val.description,
            ocr_result: val.ocr_result.map(|v| (*v).into()),
            bounding_box: val.bounding_box.map(Into::into),
            source_path: val.source_path,
            image_kind: val.image_kind.map(Into::into),
            kind_confidence: val.kind_confidence.map(|v| v as f64),
            cluster_id: val.cluster_id,
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsBoundingBox> for kreuzberg::BoundingBox {
    fn from(val: JsBoundingBox) -> Self {
        let mut __result = kreuzberg::BoundingBox::default();
        if let Some(__v) = val.x0 {
            __result.x0 = __v;
        }
        if let Some(__v) = val.y0 {
            __result.y0 = __v;
        }
        if let Some(__v) = val.x1 {
            __result.x1 = __v;
        }
        if let Some(__v) = val.y1 {
            __result.y1 = __v;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::BoundingBox> for JsBoundingBox {
    fn from(val: kreuzberg::BoundingBox) -> Self {
        Self {
            x0: Some(val.x0),
            y0: Some(val.y0),
            x1: Some(val.x1),
            y1: Some(val.y1),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsElementMetadata> for kreuzberg::ElementMetadata {
    fn from(val: JsElementMetadata) -> Self {
        Self {
            page_number: val.page_number,
            filename: val.filename,
            coordinates: val.coordinates.map(Into::into),
            element_index: val.element_index.map(|v| v as usize),
            additional: val.additional.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ElementMetadata> for JsElementMetadata {
    fn from(val: kreuzberg::ElementMetadata) -> Self {
        Self {
            page_number: val.page_number,
            filename: val.filename,
            coordinates: val.coordinates.map(Into::into),
            element_index: val.element_index.map(|v| v as i64),
            additional: val.additional.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsElement> for kreuzberg::Element {
    fn from(val: JsElement) -> Self {
        Self {
            element_id: Default::default(),
            element_type: val.element_type.into(),
            text: val.text,
            metadata: val.metadata.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::Element> for JsElement {
    fn from(val: kreuzberg::Element) -> Self {
        Self {
            element_id: format!("{:?}", val.element_id),
            element_type: val.element_type.into(),
            text: val.text,
            metadata: val.metadata.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExcelWorkbook> for JsExcelWorkbook {
    fn from(val: kreuzberg::ExcelWorkbook) -> Self {
        Self {
            sheets: val.sheets.into_iter().map(Into::into).collect(),
            metadata: val.metadata.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
            revisions: val.revisions.map(|v| v.into_iter().map(Into::into).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExcelSheet> for JsExcelSheet {
    fn from(val: kreuzberg::ExcelSheet) -> Self {
        Self {
            name: val.name,
            markdown: val.markdown,
            row_count: val.row_count as i64,
            col_count: val.col_count as i64,
            cell_count: val.cell_count as i64,
            table_cells: val.table_cells.map(|v| v.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::XmlExtractionResult> for JsXmlExtractionResult {
    fn from(val: kreuzberg::XmlExtractionResult) -> Self {
        Self {
            content: val.content,
            element_count: val.element_count as i64,
            unique_elements: val.unique_elements.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TextExtractionResult> for JsTextExtractionResult {
    fn from(val: kreuzberg::TextExtractionResult) -> Self {
        Self {
            content: val.content,
            line_count: val.line_count as i64,
            word_count: val.word_count as i64,
            character_count: val.character_count as i64,
            headers: val.headers.map(|v| v.into_iter().collect()),
            links: val.links.as_ref().map(|v| {
                v.iter()
                    .map(|(a, b)| vec![a.to_string(), b.to_string()])
                    .collect::<Vec<Vec<String>>>()
            }),
            code_blocks: val.code_blocks.as_ref().map(|v| {
                v.iter()
                    .map(|(a, b)| vec![a.to_string(), b.to_string()])
                    .collect::<Vec<Vec<String>>>()
            }),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PptxExtractionResult> for JsPptxExtractionResult {
    fn from(val: kreuzberg::PptxExtractionResult) -> Self {
        Self {
            content: val.content,
            metadata: val.metadata.into(),
            slide_count: val.slide_count as i64,
            image_count: val.image_count as i64,
            table_count: val.table_count as i64,
            images: val.images.into_iter().map(Into::into).collect(),
            page_structure: val.page_structure.map(Into::into),
            page_contents: val.page_contents.map(|v| v.into_iter().map(Into::into).collect()),
            document: val.document.map(Into::into),
            hyperlinks: val.hyperlinks.iter().map(|i| format!("{:?}", i)).collect(),
            office_metadata: val
                .office_metadata
                .into_iter()
                .map(|(k, v)| (k.into(), v.into()))
                .collect(),
            revisions: val.revisions.map(|v| v.into_iter().map(Into::into).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmailExtractionResult> for JsEmailExtractionResult {
    fn from(val: kreuzberg::EmailExtractionResult) -> Self {
        Self {
            subject: val.subject,
            from_email: val.from_email,
            to_emails: val.to_emails.into_iter().collect(),
            cc_emails: val.cc_emails.into_iter().collect(),
            bcc_emails: val.bcc_emails.into_iter().collect(),
            date: val.date,
            message_id: val.message_id,
            plain_text: val.plain_text,
            html_content: val.html_content,
            content: val.content,
            attachments: val.attachments.into_iter().map(Into::into).collect(),
            metadata: val.metadata.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmailAttachment> for JsEmailAttachment {
    fn from(val: kreuzberg::EmailAttachment) -> Self {
        Self {
            name: val.name,
            filename: val.filename,
            mime_type: val.mime_type,
            size: val.size.map(|v| v as i64),
            is_image: val.is_image,
            data: val.data.map(|v| v.to_vec().into()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsOcrExtractionResult> for kreuzberg::OcrExtractionResult {
    fn from(val: JsOcrExtractionResult) -> Self {
        Self {
            content: val.content,
            mime_type: val.mime_type,
            metadata: val.metadata.into_iter().map(|(k, v)| (k.into(), v)).collect(),
            tables: val.tables.into_iter().map(Into::into).collect(),
            ocr_elements: val.ocr_elements.map(|v| v.into_iter().map(Into::into).collect()),
            internal_document: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrExtractionResult> for JsOcrExtractionResult {
    fn from(val: kreuzberg::OcrExtractionResult) -> Self {
        Self {
            content: val.content,
            mime_type: val.mime_type,
            metadata: val.metadata.into_iter().map(|(k, v)| (k.into(), v)).collect(),
            tables: val.tables.into_iter().map(Into::into).collect(),
            ocr_elements: val.ocr_elements.map(|v| v.into_iter().map(Into::into).collect()),
            internal_document: val.internal_document.as_ref().map(|v| format!("{v:?}")),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsOcrTable> for kreuzberg::OcrTable {
    fn from(val: JsOcrTable) -> Self {
        Self {
            cells: val.cells.into_iter().collect(),
            markdown: val.markdown,
            page_number: val.page_number,
            bounding_box: val.bounding_box.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrTable> for JsOcrTable {
    fn from(val: kreuzberg::OcrTable) -> Self {
        Self {
            cells: val.cells.into_iter().collect(),
            markdown: val.markdown,
            page_number: val.page_number,
            bounding_box: val.bounding_box.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsOcrTableBoundingBox> for kreuzberg::OcrTableBoundingBox {
    fn from(val: JsOcrTableBoundingBox) -> Self {
        Self {
            left: val.left,
            top: val.top,
            right: val.right,
            bottom: val.bottom,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrTableBoundingBox> for JsOcrTableBoundingBox {
    fn from(val: kreuzberg::OcrTableBoundingBox) -> Self {
        Self {
            left: val.left,
            top: val.top,
            right: val.right,
            bottom: val.bottom,
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsImagePreprocessingConfig> for kreuzberg::ImagePreprocessingConfig {
    fn from(val: JsImagePreprocessingConfig) -> Self {
        let mut __result = kreuzberg::ImagePreprocessingConfig::default();
        if let Some(__v) = val.target_dpi {
            __result.target_dpi = __v;
        }
        if let Some(__v) = val.auto_rotate {
            __result.auto_rotate = __v;
        }
        if let Some(__v) = val.deskew {
            __result.deskew = __v;
        }
        if let Some(__v) = val.denoise {
            __result.denoise = __v;
        }
        if let Some(__v) = val.contrast_enhance {
            __result.contrast_enhance = __v;
        }
        if let Some(__v) = val.binarization_method {
            __result.binarization_method = __v;
        }
        if let Some(__v) = val.invert_colors {
            __result.invert_colors = __v;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ImagePreprocessingConfig> for JsImagePreprocessingConfig {
    fn from(val: kreuzberg::ImagePreprocessingConfig) -> Self {
        Self {
            target_dpi: Some(val.target_dpi),
            auto_rotate: Some(val.auto_rotate),
            deskew: Some(val.deskew),
            denoise: Some(val.denoise),
            contrast_enhance: Some(val.contrast_enhance),
            binarization_method: Some(val.binarization_method),
            invert_colors: Some(val.invert_colors),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsTesseractConfig> for kreuzberg::TesseractConfig {
    fn from(val: JsTesseractConfig) -> Self {
        let mut __result = kreuzberg::TesseractConfig::default();
        if let Some(__v) = val.language {
            __result.language = __v;
        }
        if let Some(__v) = val.psm {
            __result.psm = __v;
        }
        if let Some(__v) = val.output_format {
            __result.output_format = __v;
        }
        if let Some(__v) = val.oem {
            __result.oem = __v;
        }
        if let Some(__v) = val.min_confidence {
            __result.min_confidence = __v;
        }
        __result.preprocessing = val.preprocessing.map(Into::into);
        if let Some(__v) = val.enable_table_detection {
            __result.enable_table_detection = __v;
        }
        if let Some(__v) = val.table_min_confidence {
            __result.table_min_confidence = __v;
        }
        if let Some(__v) = val.table_column_threshold {
            __result.table_column_threshold = __v;
        }
        if let Some(__v) = val.table_row_threshold_ratio {
            __result.table_row_threshold_ratio = __v;
        }
        if let Some(__v) = val.use_cache {
            __result.use_cache = __v;
        }
        if let Some(__v) = val.classify_use_pre_adapted_templates {
            __result.classify_use_pre_adapted_templates = __v;
        }
        if let Some(__v) = val.language_model_ngram_on {
            __result.language_model_ngram_on = __v;
        }
        if let Some(__v) = val.tessedit_dont_blkrej_good_wds {
            __result.tessedit_dont_blkrej_good_wds = __v;
        }
        if let Some(__v) = val.tessedit_dont_rowrej_good_wds {
            __result.tessedit_dont_rowrej_good_wds = __v;
        }
        if let Some(__v) = val.tessedit_enable_dict_correction {
            __result.tessedit_enable_dict_correction = __v;
        }
        if let Some(__v) = val.tessedit_char_whitelist {
            __result.tessedit_char_whitelist = __v;
        }
        if let Some(__v) = val.tessedit_char_blacklist {
            __result.tessedit_char_blacklist = __v;
        }
        if let Some(__v) = val.tessedit_use_primary_params_model {
            __result.tessedit_use_primary_params_model = __v;
        }
        if let Some(__v) = val.textord_space_size_is_variable {
            __result.textord_space_size_is_variable = __v;
        }
        if let Some(__v) = val.thresholding_method {
            __result.thresholding_method = __v;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TesseractConfig> for JsTesseractConfig {
    fn from(val: kreuzberg::TesseractConfig) -> Self {
        Self {
            language: Some(val.language),
            psm: Some(val.psm),
            output_format: Some(val.output_format),
            oem: Some(val.oem),
            min_confidence: Some(val.min_confidence),
            preprocessing: val.preprocessing.map(Into::into),
            enable_table_detection: Some(val.enable_table_detection),
            table_min_confidence: Some(val.table_min_confidence),
            table_column_threshold: Some(val.table_column_threshold),
            table_row_threshold_ratio: Some(val.table_row_threshold_ratio),
            use_cache: Some(val.use_cache),
            classify_use_pre_adapted_templates: Some(val.classify_use_pre_adapted_templates),
            language_model_ngram_on: Some(val.language_model_ngram_on),
            tessedit_dont_blkrej_good_wds: Some(val.tessedit_dont_blkrej_good_wds),
            tessedit_dont_rowrej_good_wds: Some(val.tessedit_dont_rowrej_good_wds),
            tessedit_enable_dict_correction: Some(val.tessedit_enable_dict_correction),
            tessedit_char_whitelist: Some(val.tessedit_char_whitelist),
            tessedit_char_blacklist: Some(val.tessedit_char_blacklist),
            tessedit_use_primary_params_model: Some(val.tessedit_use_primary_params_model),
            textord_space_size_is_variable: Some(val.textord_space_size_is_variable),
            thresholding_method: Some(val.thresholding_method),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsImagePreprocessingMetadata> for kreuzberg::ImagePreprocessingMetadata {
    fn from(val: JsImagePreprocessingMetadata) -> Self {
        Self {
            original_dimensions: Default::default(),
            original_dpi: Default::default(),
            target_dpi: val.target_dpi,
            scale_factor: val.scale_factor,
            auto_adjusted: val.auto_adjusted,
            final_dpi: val.final_dpi,
            new_dimensions: Default::default(),
            resample_method: val.resample_method,
            dimension_clamped: val.dimension_clamped,
            calculated_dpi: val.calculated_dpi,
            skipped_resize: val.skipped_resize,
            resize_error: val.resize_error,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ImagePreprocessingMetadata> for JsImagePreprocessingMetadata {
    fn from(val: kreuzberg::ImagePreprocessingMetadata) -> Self {
        Self {
            original_dimensions: vec![val.original_dimensions.0 as _, val.original_dimensions.1 as _],
            original_dpi: vec![val.original_dpi.0 as _, val.original_dpi.1 as _],
            target_dpi: val.target_dpi,
            scale_factor: val.scale_factor,
            auto_adjusted: val.auto_adjusted,
            final_dpi: val.final_dpi,
            new_dimensions: val.new_dimensions.map(|t| {
                let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect();
                arr
            }),
            resample_method: val.resample_method,
            dimension_clamped: val.dimension_clamped,
            calculated_dpi: val.calculated_dpi,
            skipped_resize: val.skipped_resize,
            resize_error: val.resize_error,
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsMetadata> for kreuzberg::Metadata {
    fn from(val: JsMetadata) -> Self {
        let mut __result = kreuzberg::Metadata::default();
        __result.title = val.title;
        __result.subject = val.subject;
        __result.authors = val.authors.map(|v| v.into_iter().collect());
        __result.keywords = val.keywords.map(|v| v.into_iter().collect());
        __result.language = val.language;
        __result.created_at = val.created_at;
        __result.modified_at = val.modified_at;
        __result.created_by = val.created_by;
        __result.modified_by = val.modified_by;
        __result.pages = val.pages.map(Into::into);
        __result.format = val.format.map(Into::into);
        __result.image_preprocessing = val.image_preprocessing.map(Into::into);
        __result.json_schema = val.json_schema;
        __result.error = val.error.map(Into::into);
        __result.extraction_duration_ms = val.extraction_duration_ms.map(|v| v as u64);
        __result.category = val.category;
        __result.tags = val.tags.map(|v| v.into_iter().collect());
        __result.document_version = val.document_version;
        __result.abstract_text = val.abstract_text;
        __result.output_format = val.output_format;
        if let Some(__v) = val.ocr_used {
            __result.ocr_used = __v;
        }
        if let Some(__v) = val.additional {
            __result.additional = __v.into_iter().map(|(k, v)| (k.into(), v)).collect();
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::Metadata> for JsMetadata {
    fn from(val: kreuzberg::Metadata) -> Self {
        Self {
            title: val.title,
            subject: val.subject,
            authors: val.authors.map(|v| v.into_iter().collect()),
            keywords: val.keywords.map(|v| v.into_iter().collect()),
            language: val.language,
            created_at: val.created_at,
            modified_at: val.modified_at,
            created_by: val.created_by,
            modified_by: val.modified_by,
            pages: val.pages.map(Into::into),
            format: val.format.map(Into::into),
            image_preprocessing: val.image_preprocessing.map(Into::into),
            json_schema: val.json_schema,
            error: val.error.map(Into::into),
            extraction_duration_ms: val.extraction_duration_ms.map(|v| v as i64),
            category: val.category,
            tags: val.tags.map(|v| v.into_iter().collect()),
            document_version: val.document_version,
            abstract_text: val.abstract_text,
            output_format: val.output_format,
            ocr_used: Some(val.ocr_used),
            additional: Some(val.additional.into_iter().map(|(k, v)| (k.into(), v)).collect()),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsExcelMetadata> for kreuzberg::ExcelMetadata {
    fn from(val: JsExcelMetadata) -> Self {
        let mut __result = kreuzberg::ExcelMetadata::default();
        __result.sheet_count = val.sheet_count;
        __result.sheet_names = val.sheet_names.map(|v| v.into_iter().collect());
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExcelMetadata> for JsExcelMetadata {
    fn from(val: kreuzberg::ExcelMetadata) -> Self {
        Self {
            sheet_count: val.sheet_count,
            sheet_names: val.sheet_names.map(|v| v.into_iter().collect()),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsEmailMetadata> for kreuzberg::EmailMetadata {
    fn from(val: JsEmailMetadata) -> Self {
        let mut __result = kreuzberg::EmailMetadata::default();
        __result.from_email = val.from_email;
        __result.from_name = val.from_name;
        if let Some(__v) = val.to_emails {
            __result.to_emails = __v.into_iter().collect();
        }
        if let Some(__v) = val.cc_emails {
            __result.cc_emails = __v.into_iter().collect();
        }
        if let Some(__v) = val.bcc_emails {
            __result.bcc_emails = __v.into_iter().collect();
        }
        __result.message_id = val.message_id;
        if let Some(__v) = val.attachments {
            __result.attachments = __v.into_iter().collect();
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmailMetadata> for JsEmailMetadata {
    fn from(val: kreuzberg::EmailMetadata) -> Self {
        Self {
            from_email: val.from_email,
            from_name: val.from_name,
            to_emails: Some(val.to_emails.into_iter().collect()),
            cc_emails: Some(val.cc_emails.into_iter().collect()),
            bcc_emails: Some(val.bcc_emails.into_iter().collect()),
            message_id: val.message_id,
            attachments: Some(val.attachments.into_iter().collect()),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsArchiveMetadata> for kreuzberg::ArchiveMetadata {
    fn from(val: JsArchiveMetadata) -> Self {
        let mut __result = kreuzberg::ArchiveMetadata::default();
        if let Some(__v) = val.format {
            __result.format = __v.into();
        }
        if let Some(__v) = val.file_count {
            __result.file_count = __v;
        }
        if let Some(__v) = val.file_list {
            __result.file_list = __v.into_iter().collect();
        }
        if let Some(__v) = val.total_size {
            __result.total_size = __v as u64;
        }
        __result.compressed_size = val.compressed_size.map(|v| v as u64);
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ArchiveMetadata> for JsArchiveMetadata {
    fn from(val: kreuzberg::ArchiveMetadata) -> Self {
        Self {
            format: Some(val.format.to_string()),
            file_count: Some(val.file_count),
            file_list: Some(val.file_list.into_iter().collect()),
            total_size: Some(val.total_size as i64),
            compressed_size: val.compressed_size.map(|v| v as i64),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsImageMetadata> for kreuzberg::ImageMetadata {
    fn from(val: JsImageMetadata) -> Self {
        let mut __result = kreuzberg::ImageMetadata::default();
        if let Some(__v) = val.width {
            __result.width = __v;
        }
        if let Some(__v) = val.height {
            __result.height = __v;
        }
        if let Some(__v) = val.format {
            __result.format = __v;
        }
        if let Some(__v) = val.exif {
            __result.exif = __v.into_iter().map(|(k, v)| (k.into(), v.into())).collect();
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ImageMetadata> for JsImageMetadata {
    fn from(val: kreuzberg::ImageMetadata) -> Self {
        Self {
            width: Some(val.width),
            height: Some(val.height),
            format: Some(val.format),
            exif: Some(val.exif.into_iter().map(|(k, v)| (k.into(), v.into())).collect()),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsXmlMetadata> for kreuzberg::XmlMetadata {
    fn from(val: JsXmlMetadata) -> Self {
        let mut __result = kreuzberg::XmlMetadata::default();
        if let Some(__v) = val.element_count {
            __result.element_count = __v;
        }
        if let Some(__v) = val.unique_elements {
            __result.unique_elements = __v.into_iter().collect();
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::XmlMetadata> for JsXmlMetadata {
    fn from(val: kreuzberg::XmlMetadata) -> Self {
        Self {
            element_count: Some(val.element_count),
            unique_elements: Some(val.unique_elements.into_iter().collect()),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsTextMetadata> for kreuzberg::TextMetadata {
    fn from(val: JsTextMetadata) -> Self {
        let mut __result = kreuzberg::TextMetadata::default();
        if let Some(__v) = val.line_count {
            __result.line_count = __v;
        }
        if let Some(__v) = val.word_count {
            __result.word_count = __v;
        }
        if let Some(__v) = val.character_count {
            __result.character_count = __v;
        }
        __result.headers = val.headers.map(|v| v.into_iter().collect());
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TextMetadata> for JsTextMetadata {
    fn from(val: kreuzberg::TextMetadata) -> Self {
        Self {
            line_count: Some(val.line_count),
            word_count: Some(val.word_count),
            character_count: Some(val.character_count),
            headers: val.headers.map(|v| v.into_iter().collect()),
            links: val.links.as_ref().map(|v| {
                v.iter()
                    .map(|(a, b)| vec![a.to_string(), b.to_string()])
                    .collect::<Vec<Vec<String>>>()
            }),
            code_blocks: val.code_blocks.as_ref().map(|v| {
                v.iter()
                    .map(|(a, b)| vec![a.to_string(), b.to_string()])
                    .collect::<Vec<Vec<String>>>()
            }),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsHeaderMetadata> for kreuzberg::HeaderMetadata {
    fn from(val: JsHeaderMetadata) -> Self {
        Self {
            level: val.level,
            text: val.text,
            id: val.id,
            depth: val.depth,
            html_offset: val.html_offset,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HeaderMetadata> for JsHeaderMetadata {
    fn from(val: kreuzberg::HeaderMetadata) -> Self {
        Self {
            level: val.level,
            text: val.text,
            id: val.id,
            depth: val.depth,
            html_offset: val.html_offset,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsLinkMetadata> for kreuzberg::LinkMetadata {
    fn from(val: JsLinkMetadata) -> Self {
        Self {
            href: val.href,
            text: val.text,
            title: val.title,
            link_type: val.link_type.into(),
            rel: val.rel.into_iter().collect(),
            attributes: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::LinkMetadata> for JsLinkMetadata {
    fn from(val: kreuzberg::LinkMetadata) -> Self {
        Self {
            href: val.href,
            text: val.text,
            title: val.title,
            link_type: val.link_type.into(),
            rel: val.rel.into_iter().collect(),
            attributes: val
                .attributes
                .iter()
                .map(|(a, b)| vec![a.to_string(), b.to_string()])
                .collect::<Vec<Vec<String>>>(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsImageMetadataType> for kreuzberg::ImageMetadataType {
    fn from(val: JsImageMetadataType) -> Self {
        Self {
            src: val.src,
            alt: val.alt,
            title: val.title,
            dimensions: Default::default(),
            image_type: val.image_type.into(),
            attributes: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ImageMetadataType> for JsImageMetadataType {
    fn from(val: kreuzberg::ImageMetadataType) -> Self {
        Self {
            src: val.src,
            alt: val.alt,
            title: val.title,
            dimensions: val.dimensions.map(|t| {
                let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect();
                arr
            }),
            image_type: val.image_type.into(),
            attributes: val
                .attributes
                .iter()
                .map(|(a, b)| vec![a.to_string(), b.to_string()])
                .collect::<Vec<Vec<String>>>(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsStructuredData> for kreuzberg::StructuredData {
    fn from(val: JsStructuredData) -> Self {
        Self {
            data_type: val.data_type.into(),
            raw_json: val.raw_json,
            schema_type: val.schema_type,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::StructuredData> for JsStructuredData {
    fn from(val: kreuzberg::StructuredData) -> Self {
        Self {
            data_type: val.data_type.into(),
            raw_json: val.raw_json,
            schema_type: val.schema_type,
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsHtmlMetadata> for kreuzberg::HtmlMetadata {
    fn from(val: JsHtmlMetadata) -> Self {
        let mut __result = kreuzberg::HtmlMetadata::default();
        __result.title = val.title;
        __result.description = val.description;
        if let Some(__v) = val.keywords {
            __result.keywords = __v.into_iter().collect();
        }
        __result.author = val.author;
        __result.canonical_url = val.canonical_url;
        __result.base_href = val.base_href;
        __result.language = val.language;
        __result.text_direction = val.text_direction.map(Into::into);
        if let Some(__v) = val.open_graph {
            __result.open_graph = __v.into_iter().map(|(k, v)| (k.into(), v.into())).collect();
        }
        if let Some(__v) = val.twitter_card {
            __result.twitter_card = __v.into_iter().map(|(k, v)| (k.into(), v.into())).collect();
        }
        if let Some(__v) = val.meta_tags {
            __result.meta_tags = __v.into_iter().map(|(k, v)| (k.into(), v.into())).collect();
        }
        if let Some(__v) = val.headers {
            __result.headers = __v.into_iter().map(Into::into).collect();
        }
        if let Some(__v) = val.links {
            __result.links = __v.into_iter().map(Into::into).collect();
        }
        if let Some(__v) = val.images {
            __result.images = __v.into_iter().map(Into::into).collect();
        }
        if let Some(__v) = val.structured_data {
            __result.structured_data = __v.into_iter().map(Into::into).collect();
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HtmlMetadata> for JsHtmlMetadata {
    fn from(val: kreuzberg::HtmlMetadata) -> Self {
        Self {
            title: val.title,
            description: val.description,
            keywords: Some(val.keywords.into_iter().collect()),
            author: val.author,
            canonical_url: val.canonical_url,
            base_href: val.base_href,
            language: val.language,
            text_direction: val.text_direction.map(Into::into),
            open_graph: Some(val.open_graph.into_iter().map(|(k, v)| (k.into(), v.into())).collect()),
            twitter_card: Some(
                val.twitter_card
                    .into_iter()
                    .map(|(k, v)| (k.into(), v.into()))
                    .collect(),
            ),
            meta_tags: Some(val.meta_tags.into_iter().map(|(k, v)| (k.into(), v.into())).collect()),
            headers: Some(val.headers.into_iter().map(Into::into).collect()),
            links: Some(val.links.into_iter().map(Into::into).collect()),
            images: Some(val.images.into_iter().map(Into::into).collect()),
            structured_data: Some(val.structured_data.into_iter().map(Into::into).collect()),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsOcrMetadata> for kreuzberg::OcrMetadata {
    fn from(val: JsOcrMetadata) -> Self {
        let mut __result = kreuzberg::OcrMetadata::default();
        if let Some(__v) = val.language {
            __result.language = __v;
        }
        if let Some(__v) = val.psm {
            __result.psm = __v;
        }
        if let Some(__v) = val.output_format {
            __result.output_format = __v;
        }
        if let Some(__v) = val.table_count {
            __result.table_count = __v;
        }
        __result.table_rows = val.table_rows;
        __result.table_cols = val.table_cols;
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrMetadata> for JsOcrMetadata {
    fn from(val: kreuzberg::OcrMetadata) -> Self {
        Self {
            language: Some(val.language),
            psm: Some(val.psm),
            output_format: Some(val.output_format),
            table_count: Some(val.table_count),
            table_rows: val.table_rows,
            table_cols: val.table_cols,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsErrorMetadata> for kreuzberg::ErrorMetadata {
    fn from(val: JsErrorMetadata) -> Self {
        Self {
            error_type: val.error_type,
            message: val.message,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ErrorMetadata> for JsErrorMetadata {
    fn from(val: kreuzberg::ErrorMetadata) -> Self {
        Self {
            error_type: val.error_type,
            message: val.message,
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsPptxMetadata> for kreuzberg::PptxMetadata {
    fn from(val: JsPptxMetadata) -> Self {
        let mut __result = kreuzberg::PptxMetadata::default();
        if let Some(__v) = val.slide_count {
            __result.slide_count = __v;
        }
        if let Some(__v) = val.slide_names {
            __result.slide_names = __v.into_iter().collect();
        }
        __result.image_count = val.image_count;
        __result.table_count = val.table_count;
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PptxMetadata> for JsPptxMetadata {
    fn from(val: kreuzberg::PptxMetadata) -> Self {
        Self {
            slide_count: Some(val.slide_count),
            slide_names: Some(val.slide_names.into_iter().collect()),
            image_count: val.image_count,
            table_count: val.table_count,
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsDocxMetadata> for kreuzberg::DocxMetadata {
    fn from(val: JsDocxMetadata) -> Self {
        let mut __result = kreuzberg::DocxMetadata::default();
        __result.core_properties = val.core_properties.map(Into::into);
        __result.app_properties = val.app_properties.map(Into::into);
        __result.custom_properties = val
            .custom_properties
            .map(|m| m.into_iter().map(|(k, v)| (k.into(), v)).collect());
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DocxMetadata> for JsDocxMetadata {
    fn from(val: kreuzberg::DocxMetadata) -> Self {
        Self {
            core_properties: val.core_properties.map(Into::into),
            app_properties: val.app_properties.map(Into::into),
            custom_properties: val
                .custom_properties
                .map(|m| m.into_iter().map(|(k, v)| (k.into(), v)).collect()),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsCsvMetadata> for kreuzberg::CsvMetadata {
    fn from(val: JsCsvMetadata) -> Self {
        let mut __result = kreuzberg::CsvMetadata::default();
        if let Some(__v) = val.row_count {
            __result.row_count = __v;
        }
        if let Some(__v) = val.column_count {
            __result.column_count = __v;
        }
        __result.delimiter = val.delimiter;
        if let Some(__v) = val.has_header {
            __result.has_header = __v;
        }
        __result.column_types = val.column_types.map(|v| v.into_iter().collect());
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::CsvMetadata> for JsCsvMetadata {
    fn from(val: kreuzberg::CsvMetadata) -> Self {
        Self {
            row_count: Some(val.row_count),
            column_count: Some(val.column_count),
            delimiter: val.delimiter,
            has_header: Some(val.has_header),
            column_types: val.column_types.map(|v| v.into_iter().collect()),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsBibtexMetadata> for kreuzberg::BibtexMetadata {
    fn from(val: JsBibtexMetadata) -> Self {
        let mut __result = kreuzberg::BibtexMetadata::default();
        if let Some(__v) = val.entry_count {
            __result.entry_count = __v as usize;
        }
        if let Some(__v) = val.citation_keys {
            __result.citation_keys = __v.into_iter().collect();
        }
        if let Some(__v) = val.authors {
            __result.authors = __v.into_iter().collect();
        }
        __result.year_range = val.year_range.map(Into::into);
        __result.entry_types = val
            .entry_types
            .map(|m| m.into_iter().map(|(k, v)| (k, v as usize)).collect());
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::BibtexMetadata> for JsBibtexMetadata {
    fn from(val: kreuzberg::BibtexMetadata) -> Self {
        Self {
            entry_count: Some(val.entry_count as i64),
            citation_keys: Some(val.citation_keys.into_iter().collect()),
            authors: Some(val.authors.into_iter().collect()),
            year_range: val.year_range.map(Into::into),
            entry_types: val
                .entry_types
                .as_ref()
                .map(|m| m.iter().map(|(k, v)| (k.clone(), *v as i64)).collect()),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsCitationMetadata> for kreuzberg::CitationMetadata {
    fn from(val: JsCitationMetadata) -> Self {
        let mut __result = kreuzberg::CitationMetadata::default();
        if let Some(__v) = val.citation_count {
            __result.citation_count = __v as usize;
        }
        __result.format = val.format;
        if let Some(__v) = val.authors {
            __result.authors = __v.into_iter().collect();
        }
        __result.year_range = val.year_range.map(Into::into);
        if let Some(__v) = val.dois {
            __result.dois = __v.into_iter().collect();
        }
        if let Some(__v) = val.keywords {
            __result.keywords = __v.into_iter().collect();
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::CitationMetadata> for JsCitationMetadata {
    fn from(val: kreuzberg::CitationMetadata) -> Self {
        Self {
            citation_count: Some(val.citation_count as i64),
            format: val.format,
            authors: Some(val.authors.into_iter().collect()),
            year_range: val.year_range.map(Into::into),
            dois: Some(val.dois.into_iter().collect()),
            keywords: Some(val.keywords.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsYearRange> for kreuzberg::YearRange {
    fn from(val: JsYearRange) -> Self {
        Self {
            min: val.min,
            max: val.max,
            years: val.years.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::YearRange> for JsYearRange {
    fn from(val: kreuzberg::YearRange) -> Self {
        Self {
            min: val.min,
            max: val.max,
            years: val.years.into_iter().collect(),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsFictionBookMetadata> for kreuzberg::FictionBookMetadata {
    fn from(val: JsFictionBookMetadata) -> Self {
        let mut __result = kreuzberg::FictionBookMetadata::default();
        if let Some(__v) = val.genres {
            __result.genres = __v.into_iter().collect();
        }
        if let Some(__v) = val.sequences {
            __result.sequences = __v.into_iter().collect();
        }
        __result.annotation = val.annotation;
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::FictionBookMetadata> for JsFictionBookMetadata {
    fn from(val: kreuzberg::FictionBookMetadata) -> Self {
        Self {
            genres: Some(val.genres.into_iter().collect()),
            sequences: Some(val.sequences.into_iter().collect()),
            annotation: val.annotation,
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsDbfMetadata> for kreuzberg::DbfMetadata {
    fn from(val: JsDbfMetadata) -> Self {
        let mut __result = kreuzberg::DbfMetadata::default();
        if let Some(__v) = val.record_count {
            __result.record_count = __v as usize;
        }
        if let Some(__v) = val.field_count {
            __result.field_count = __v as usize;
        }
        if let Some(__v) = val.fields {
            __result.fields = __v.into_iter().map(Into::into).collect();
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DbfMetadata> for JsDbfMetadata {
    fn from(val: kreuzberg::DbfMetadata) -> Self {
        Self {
            record_count: Some(val.record_count as i64),
            field_count: Some(val.field_count as i64),
            fields: Some(val.fields.into_iter().map(Into::into).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsDbfFieldInfo> for kreuzberg::DbfFieldInfo {
    fn from(val: JsDbfFieldInfo) -> Self {
        Self {
            name: val.name,
            field_type: val.field_type,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DbfFieldInfo> for JsDbfFieldInfo {
    fn from(val: kreuzberg::DbfFieldInfo) -> Self {
        Self {
            name: val.name,
            field_type: val.field_type,
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsJatsMetadata> for kreuzberg::JatsMetadata {
    fn from(val: JsJatsMetadata) -> Self {
        let mut __result = kreuzberg::JatsMetadata::default();
        __result.copyright = val.copyright;
        __result.license = val.license;
        if let Some(__v) = val.history_dates {
            __result.history_dates = __v.into_iter().map(|(k, v)| (k.into(), v.into())).collect();
        }
        if let Some(__v) = val.contributor_roles {
            __result.contributor_roles = __v.into_iter().map(Into::into).collect();
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::JatsMetadata> for JsJatsMetadata {
    fn from(val: kreuzberg::JatsMetadata) -> Self {
        Self {
            copyright: val.copyright,
            license: val.license,
            history_dates: Some(
                val.history_dates
                    .into_iter()
                    .map(|(k, v)| (k.into(), v.into()))
                    .collect(),
            ),
            contributor_roles: Some(val.contributor_roles.into_iter().map(Into::into).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsContributorRole> for kreuzberg::ContributorRole {
    fn from(val: JsContributorRole) -> Self {
        Self {
            name: val.name,
            role: val.role,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ContributorRole> for JsContributorRole {
    fn from(val: kreuzberg::ContributorRole) -> Self {
        Self {
            name: val.name,
            role: val.role,
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsEpubMetadata> for kreuzberg::EpubMetadata {
    fn from(val: JsEpubMetadata) -> Self {
        let mut __result = kreuzberg::EpubMetadata::default();
        __result.coverage = val.coverage;
        __result.dc_format = val.dc_format;
        __result.relation = val.relation;
        __result.source = val.source;
        __result.dc_type = val.dc_type;
        __result.cover_image = val.cover_image;
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EpubMetadata> for JsEpubMetadata {
    fn from(val: kreuzberg::EpubMetadata) -> Self {
        Self {
            coverage: val.coverage,
            dc_format: val.dc_format,
            relation: val.relation,
            source: val.source,
            dc_type: val.dc_type,
            cover_image: val.cover_image,
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsPstMetadata> for kreuzberg::PstMetadata {
    fn from(val: JsPstMetadata) -> Self {
        let mut __result = kreuzberg::PstMetadata::default();
        if let Some(__v) = val.message_count {
            __result.message_count = __v as usize;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PstMetadata> for JsPstMetadata {
    fn from(val: kreuzberg::PstMetadata) -> Self {
        Self {
            message_count: Some(val.message_count as i64),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsOcrConfidence> for kreuzberg::OcrConfidence {
    fn from(val: JsOcrConfidence) -> Self {
        let mut __result = kreuzberg::OcrConfidence::default();
        __result.detection = val.detection;
        if let Some(__v) = val.recognition {
            __result.recognition = __v;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrConfidence> for JsOcrConfidence {
    fn from(val: kreuzberg::OcrConfidence) -> Self {
        Self {
            detection: val.detection,
            recognition: Some(val.recognition),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsOcrRotation> for kreuzberg::OcrRotation {
    fn from(val: JsOcrRotation) -> Self {
        Self {
            angle_degrees: val.angle_degrees,
            confidence: val.confidence,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrRotation> for JsOcrRotation {
    fn from(val: kreuzberg::OcrRotation) -> Self {
        Self {
            angle_degrees: val.angle_degrees,
            confidence: val.confidence,
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsOcrElement> for kreuzberg::OcrElement {
    fn from(val: JsOcrElement) -> Self {
        let mut __result = kreuzberg::OcrElement::default();
        if let Some(__v) = val.text {
            __result.text = __v;
        }
        if let Some(__v) = val.geometry {
            __result.geometry = __v.into();
        }
        if let Some(__v) = val.confidence {
            __result.confidence = __v.into();
        }
        if let Some(__v) = val.level {
            __result.level = __v.into();
        }
        __result.rotation = val.rotation.map(Into::into);
        if let Some(__v) = val.page_number {
            __result.page_number = __v;
        }
        __result.parent_id = val.parent_id;
        if let Some(__v) = val.backend_metadata {
            __result.backend_metadata = __v.into_iter().map(|(k, v)| (k.into(), v)).collect();
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrElement> for JsOcrElement {
    fn from(val: kreuzberg::OcrElement) -> Self {
        Self {
            text: Some(val.text),
            geometry: Some(val.geometry.into()),
            confidence: Some(val.confidence.into()),
            level: Some(val.level.into()),
            rotation: val.rotation.map(Into::into),
            page_number: Some(val.page_number),
            parent_id: val.parent_id,
            backend_metadata: Some(val.backend_metadata.into_iter().map(|(k, v)| (k.into(), v)).collect()),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsOcrElementConfig> for kreuzberg::OcrElementConfig {
    fn from(val: JsOcrElementConfig) -> Self {
        let mut __result = kreuzberg::OcrElementConfig::default();
        if let Some(__v) = val.include_elements {
            __result.include_elements = __v;
        }
        if let Some(__v) = val.min_level {
            __result.min_level = __v.into();
        }
        if let Some(__v) = val.min_confidence {
            __result.min_confidence = __v;
        }
        if let Some(__v) = val.build_hierarchy {
            __result.build_hierarchy = __v;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrElementConfig> for JsOcrElementConfig {
    fn from(val: kreuzberg::OcrElementConfig) -> Self {
        Self {
            include_elements: Some(val.include_elements),
            min_level: Some(val.min_level.into()),
            min_confidence: Some(val.min_confidence),
            build_hierarchy: Some(val.build_hierarchy),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsPageStructure> for kreuzberg::PageStructure {
    fn from(val: JsPageStructure) -> Self {
        Self {
            total_count: val.total_count,
            unit_type: val.unit_type.into(),
            boundaries: val.boundaries.map(|v| v.into_iter().map(Into::into).collect()),
            pages: val.pages.map(|v| v.into_iter().map(Into::into).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PageStructure> for JsPageStructure {
    fn from(val: kreuzberg::PageStructure) -> Self {
        Self {
            total_count: val.total_count,
            unit_type: val.unit_type.into(),
            boundaries: val.boundaries.map(|v| v.into_iter().map(Into::into).collect()),
            pages: val.pages.map(|v| v.into_iter().map(Into::into).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsPageBoundary> for kreuzberg::PageBoundary {
    fn from(val: JsPageBoundary) -> Self {
        Self {
            byte_start: val.byte_start as usize,
            byte_end: val.byte_end as usize,
            page_number: val.page_number,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PageBoundary> for JsPageBoundary {
    fn from(val: kreuzberg::PageBoundary) -> Self {
        Self {
            byte_start: val.byte_start as i64,
            byte_end: val.byte_end as i64,
            page_number: val.page_number,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsPageInfo> for kreuzberg::PageInfo {
    fn from(val: JsPageInfo) -> Self {
        Self {
            number: val.number,
            title: val.title,
            dimensions: Default::default(),
            image_count: val.image_count,
            table_count: val.table_count,
            hidden: val.hidden,
            is_blank: val.is_blank,
            has_vector_graphics: val.has_vector_graphics,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PageInfo> for JsPageInfo {
    fn from(val: kreuzberg::PageInfo) -> Self {
        Self {
            number: val.number,
            title: val.title,
            dimensions: val.dimensions.map(|t| {
                let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect();
                arr
            }),
            image_count: val.image_count,
            table_count: val.table_count,
            hidden: val.hidden,
            is_blank: val.is_blank,
            has_vector_graphics: val.has_vector_graphics,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsPageContent> for kreuzberg::PageContent {
    fn from(val: JsPageContent) -> Self {
        Self {
            page_number: val.page_number,
            content: val.content,
            tables: val.tables.into_iter().map(|v| std::sync::Arc::new(v.into())).collect(),
            image_indices: val.image_indices.into_iter().collect(),
            hierarchy: val.hierarchy.map(Into::into),
            is_blank: val.is_blank,
            layout_regions: val.layout_regions.map(|v| v.into_iter().map(Into::into).collect()),
            speaker_notes: val.speaker_notes,
            section_name: val.section_name,
            sheet_name: val.sheet_name,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PageContent> for JsPageContent {
    fn from(val: kreuzberg::PageContent) -> Self {
        Self {
            page_number: val.page_number,
            content: val.content,
            tables: val.tables.into_iter().map(|v| (*v).clone().into()).collect(),
            image_indices: val.image_indices.into_iter().collect(),
            hierarchy: val.hierarchy.map(Into::into),
            is_blank: val.is_blank,
            layout_regions: val.layout_regions.map(|v| v.into_iter().map(Into::into).collect()),
            speaker_notes: val.speaker_notes,
            section_name: val.section_name,
            sheet_name: val.sheet_name,
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsLayoutRegion> for kreuzberg::LayoutRegion {
    fn from(val: JsLayoutRegion) -> Self {
        let mut __result = kreuzberg::LayoutRegion::default();
        if let Some(__v) = val.class_name {
            __result.class_name = __v;
        }
        if let Some(__v) = val.confidence {
            __result.confidence = __v;
        }
        if let Some(__v) = val.bounding_box {
            __result.bounding_box = __v.into();
        }
        if let Some(__v) = val.area_fraction {
            __result.area_fraction = __v;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::LayoutRegion> for JsLayoutRegion {
    fn from(val: kreuzberg::LayoutRegion) -> Self {
        Self {
            class_name: Some(val.class_name),
            confidence: Some(val.confidence),
            bounding_box: Some(val.bounding_box.into()),
            area_fraction: Some(val.area_fraction),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsPageHierarchy> for kreuzberg::PageHierarchy {
    fn from(val: JsPageHierarchy) -> Self {
        Self {
            block_count: val.block_count,
            blocks: val.blocks.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PageHierarchy> for JsPageHierarchy {
    fn from(val: kreuzberg::PageHierarchy) -> Self {
        Self {
            block_count: val.block_count,
            blocks: val.blocks.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsHierarchicalBlock> for kreuzberg::HierarchicalBlock {
    fn from(val: JsHierarchicalBlock) -> Self {
        Self {
            text: val.text,
            font_size: val.font_size as f32,
            level: val.level,
            bbox: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HierarchicalBlock> for JsHierarchicalBlock {
    fn from(val: kreuzberg::HierarchicalBlock) -> Self {
        Self {
            text: val.text,
            font_size: val.font_size as f64,
            level: val.level,
            bbox: val.bbox.map(|t| {
                let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect();
                arr
            }),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsCellChange> for kreuzberg::CellChange {
    fn from(val: JsCellChange) -> Self {
        Self {
            row: val.row as usize,
            col: val.col as usize,
            from: val.from,
            to: val.to,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::CellChange> for JsCellChange {
    fn from(val: kreuzberg::CellChange) -> Self {
        Self {
            row: val.row as i64,
            col: val.col as i64,
            from: val.from,
            to: val.to,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsDocumentRevision> for kreuzberg::DocumentRevision {
    fn from(val: JsDocumentRevision) -> Self {
        Self {
            revision_id: val.revision_id,
            author: val.author,
            timestamp: val.timestamp,
            kind: val.kind.into(),
            anchor: val.anchor.map(Into::into),
            delta: val.delta.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DocumentRevision> for JsDocumentRevision {
    fn from(val: kreuzberg::DocumentRevision) -> Self {
        Self {
            revision_id: val.revision_id,
            author: val.author,
            timestamp: val.timestamp,
            kind: val.kind.into(),
            anchor: val.anchor.map(Into::into),
            delta: val.delta.into(),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsRevisionDelta> for kreuzberg::RevisionDelta {
    fn from(val: JsRevisionDelta) -> Self {
        let mut __result = kreuzberg::RevisionDelta::default();
        if let Some(__v) = val.content {
            __result.content = __v.into_iter().map(Into::into).collect();
        }
        if let Some(__v) = val.table_changes {
            __result.table_changes = __v.into_iter().map(Into::into).collect();
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::RevisionDelta> for JsRevisionDelta {
    fn from(val: kreuzberg::RevisionDelta) -> Self {
        Self {
            content: Some(val.content.into_iter().map(Into::into).collect()),
            table_changes: Some(val.table_changes.into_iter().map(Into::into).collect()),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsTable> for kreuzberg::Table {
    fn from(val: JsTable) -> Self {
        let mut __result = kreuzberg::Table::default();
        if let Some(__v) = val.cells {
            __result.cells = __v.into_iter().collect();
        }
        if let Some(__v) = val.markdown {
            __result.markdown = __v;
        }
        if let Some(__v) = val.page_number {
            __result.page_number = __v;
        }
        __result.bounding_box = val.bounding_box.map(Into::into);
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::Table> for JsTable {
    fn from(val: kreuzberg::Table) -> Self {
        Self {
            cells: Some(val.cells.into_iter().collect()),
            markdown: Some(val.markdown),
            page_number: Some(val.page_number),
            bounding_box: val.bounding_box.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TableCell> for JsTableCell {
    fn from(val: kreuzberg::TableCell) -> Self {
        Self {
            content: Some(val.content),
            row_span: Some(val.row_span),
            col_span: Some(val.col_span),
            is_header: Some(val.is_header),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsExtractedUri> for kreuzberg::ExtractedUri {
    fn from(val: JsExtractedUri) -> Self {
        Self {
            url: val.url,
            label: val.label,
            page: val.page,
            kind: val.kind.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExtractedUri> for JsExtractedUri {
    fn from(val: kreuzberg::ExtractedUri) -> Self {
        Self {
            url: val.url,
            label: val.label,
            page: val.page,
            kind: val.kind.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::api::DetectResponse> for JsDetectResponse {
    fn from(val: kreuzberg::api::DetectResponse) -> Self {
        Self {
            mime_type: val.mime_type,
            filename: val.filename,
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsDiffOptions> for kreuzberg::DiffOptions {
    fn from(val: JsDiffOptions) -> Self {
        let mut __result = kreuzberg::DiffOptions::default();
        if let Some(__v) = val.include_metadata {
            __result.include_metadata = __v;
        }
        if let Some(__v) = val.include_embedded {
            __result.include_embedded = __v;
        }
        __result.max_content_chars = val.max_content_chars.map(|v| v as usize);
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DiffOptions> for JsDiffOptions {
    fn from(val: kreuzberg::DiffOptions) -> Self {
        Self {
            include_metadata: Some(val.include_metadata),
            include_embedded: Some(val.include_embedded),
            max_content_chars: val.max_content_chars.map(|v| v as i64),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsExtractionDiff> for kreuzberg::ExtractionDiff {
    fn from(val: JsExtractionDiff) -> Self {
        Self {
            content_diff: val.content_diff.into_iter().map(Into::into).collect(),
            tables_added: val.tables_added.into_iter().map(Into::into).collect(),
            tables_removed: val.tables_removed.into_iter().map(Into::into).collect(),
            tables_changed: val.tables_changed.into_iter().map(Into::into).collect(),
            metadata_changed: val.metadata_changed,
            embedded_changes: val.embedded_changes.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExtractionDiff> for JsExtractionDiff {
    fn from(val: kreuzberg::ExtractionDiff) -> Self {
        Self {
            content_diff: val.content_diff.into_iter().map(Into::into).collect(),
            tables_added: val.tables_added.into_iter().map(Into::into).collect(),
            tables_removed: val.tables_removed.into_iter().map(Into::into).collect(),
            tables_changed: val.tables_changed.into_iter().map(Into::into).collect(),
            metadata_changed: val.metadata_changed,
            embedded_changes: val.embedded_changes.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsDiffHunk> for kreuzberg::DiffHunk {
    fn from(val: JsDiffHunk) -> Self {
        Self {
            from_line: val.from_line as usize,
            from_count: val.from_count as usize,
            to_line: val.to_line as usize,
            to_count: val.to_count as usize,
            lines: val.lines.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DiffHunk> for JsDiffHunk {
    fn from(val: kreuzberg::DiffHunk) -> Self {
        Self {
            from_line: val.from_line as i64,
            from_count: val.from_count as i64,
            to_line: val.to_line as i64,
            to_count: val.to_count as i64,
            lines: val.lines.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsTableDiff> for kreuzberg::TableDiff {
    fn from(val: JsTableDiff) -> Self {
        Self {
            from_index: val.from_index as usize,
            to_index: val.to_index as usize,
            cell_changes: val.cell_changes.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TableDiff> for JsTableDiff {
    fn from(val: kreuzberg::TableDiff) -> Self {
        Self {
            from_index: val.from_index as i64,
            to_index: val.to_index as i64,
            cell_changes: val.cell_changes.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsEmbeddedChanges> for kreuzberg::EmbeddedChanges {
    fn from(val: JsEmbeddedChanges) -> Self {
        Self {
            added: val.added.into_iter().map(Into::into).collect(),
            removed: val.removed.into_iter().map(Into::into).collect(),
            changed: val.changed.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmbeddedChanges> for JsEmbeddedChanges {
    fn from(val: kreuzberg::EmbeddedChanges) -> Self {
        Self {
            added: val.added.into_iter().map(Into::into).collect(),
            removed: val.removed.into_iter().map(Into::into).collect(),
            changed: val.changed.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsEmbeddedDiff> for kreuzberg::EmbeddedDiff {
    fn from(val: JsEmbeddedDiff) -> Self {
        Self {
            path: val.path,
            diff: Box::new(val.diff.into()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmbeddedDiff> for JsEmbeddedDiff {
    fn from(val: kreuzberg::EmbeddedDiff) -> Self {
        Self {
            path: val.path,
            diff: (*val.diff).into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsEmbeddingPreset> for kreuzberg::EmbeddingPreset {
    fn from(val: JsEmbeddingPreset) -> Self {
        Self {
            name: val.name,
            chunk_size: val.chunk_size as usize,
            overlap: val.overlap as usize,
            model_repo: val.model_repo,
            pooling: val.pooling,
            model_file: val.model_file,
            dimensions: val.dimensions as usize,
            description: val.description,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmbeddingPreset> for JsEmbeddingPreset {
    fn from(val: kreuzberg::EmbeddingPreset) -> Self {
        Self {
            name: val.name,
            chunk_size: val.chunk_size as i64,
            overlap: val.overlap as i64,
            model_repo: val.model_repo,
            pooling: val.pooling,
            model_file: val.model_file,
            dimensions: val.dimensions as i64,
            description: val.description,
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsYakeParams> for kreuzberg::YakeParams {
    fn from(val: JsYakeParams) -> Self {
        let mut __result = kreuzberg::YakeParams::default();
        if let Some(__v) = val.window_size {
            __result.window_size = __v as usize;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::YakeParams> for JsYakeParams {
    fn from(val: kreuzberg::YakeParams) -> Self {
        Self {
            window_size: Some(val.window_size as i64),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsRakeParams> for kreuzberg::RakeParams {
    fn from(val: JsRakeParams) -> Self {
        let mut __result = kreuzberg::RakeParams::default();
        if let Some(__v) = val.min_word_length {
            __result.min_word_length = __v as usize;
        }
        if let Some(__v) = val.max_words_per_phrase {
            __result.max_words_per_phrase = __v as usize;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::RakeParams> for JsRakeParams {
    fn from(val: kreuzberg::RakeParams) -> Self {
        Self {
            min_word_length: Some(val.min_word_length as i64),
            max_words_per_phrase: Some(val.max_words_per_phrase as i64),
        }
    }
}

#[allow(clippy::needless_update)]
#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsKeywordConfig> for kreuzberg::KeywordConfig {
    fn from(val: JsKeywordConfig) -> Self {
        let mut __result = kreuzberg::KeywordConfig::default();
        if let Some(__v) = val.algorithm {
            __result.algorithm = __v.into();
        }
        if let Some(__v) = val.max_keywords {
            __result.max_keywords = __v as usize;
        }
        if let Some(__v) = val.min_score {
            __result.min_score = __v as f32;
        }
        __result.language = val.language;
        __result.yake_params = val.yake_params.map(Into::into);
        __result.rake_params = val.rake_params.map(Into::into);
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::KeywordConfig> for JsKeywordConfig {
    fn from(val: kreuzberg::KeywordConfig) -> Self {
        Self {
            algorithm: Some(val.algorithm.into()),
            max_keywords: Some(val.max_keywords as i64),
            min_score: Some(val.min_score as f64),
            ngram_range: Some(vec![val.ngram_range.0 as _, val.ngram_range.1 as _]),
            language: val.language,
            yake_params: val.yake_params.map(Into::into),
            rake_params: val.rake_params.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsKeyword> for kreuzberg::Keyword {
    fn from(val: JsKeyword) -> Self {
        Self {
            text: val.text,
            score: val.score as f32,
            algorithm: val.algorithm.into(),
            positions: val.positions.map(|v| v.into_iter().map(|x| x as usize).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::Keyword> for JsKeyword {
    fn from(val: kreuzberg::Keyword) -> Self {
        Self {
            text: val.text,
            score: val.score as f64,
            algorithm: val.algorithm.into(),
            positions: val.positions.as_ref().map(|v| v.iter().map(|&x| x as i64).collect()),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsPaddleOcrConfig> for kreuzberg::PaddleOcrConfig {
    fn from(val: JsPaddleOcrConfig) -> Self {
        let mut __result = kreuzberg::PaddleOcrConfig::default();
        if let Some(__v) = val.language {
            __result.language = __v;
        }
        __result.cache_dir = val.cache_dir.map(Into::into);
        if let Some(__v) = val.use_angle_cls {
            __result.use_angle_cls = __v;
        }
        if let Some(__v) = val.enable_table_detection {
            __result.enable_table_detection = __v;
        }
        if let Some(__v) = val.det_db_thresh {
            __result.det_db_thresh = __v as f32;
        }
        if let Some(__v) = val.det_db_box_thresh {
            __result.det_db_box_thresh = __v as f32;
        }
        if let Some(__v) = val.det_db_unclip_ratio {
            __result.det_db_unclip_ratio = __v as f32;
        }
        if let Some(__v) = val.det_limit_side_len {
            __result.det_limit_side_len = __v;
        }
        if let Some(__v) = val.rec_batch_num {
            __result.rec_batch_num = __v;
        }
        if let Some(__v) = val.padding {
            __result.padding = __v;
        }
        if let Some(__v) = val.drop_score {
            __result.drop_score = __v as f32;
        }
        if let Some(__v) = val.model_tier {
            __result.model_tier = __v;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PaddleOcrConfig> for JsPaddleOcrConfig {
    fn from(val: kreuzberg::PaddleOcrConfig) -> Self {
        Self {
            language: Some(val.language),
            cache_dir: val.cache_dir.map(|p| p.to_string_lossy().to_string()),
            use_angle_cls: Some(val.use_angle_cls),
            enable_table_detection: Some(val.enable_table_detection),
            det_db_thresh: Some(val.det_db_thresh as f64),
            det_db_box_thresh: Some(val.det_db_box_thresh as f64),
            det_db_unclip_ratio: Some(val.det_db_unclip_ratio as f64),
            det_limit_side_len: Some(val.det_limit_side_len),
            rec_batch_num: Some(val.rec_batch_num),
            padding: Some(val.padding),
            drop_score: Some(val.drop_score as f64),
            model_tier: Some(val.model_tier),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ModelPaths> for JsModelPaths {
    fn from(val: kreuzberg::ModelPaths) -> Self {
        Self {
            det_model: val.det_model.to_string_lossy().to_string(),
            cls_model: val.cls_model.to_string_lossy().to_string(),
            rec_model: val.rec_model.to_string_lossy().to_string(),
            dict_file: val.dict_file.to_string_lossy().to_string(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OrientationResult> for JsOrientationResult {
    fn from(val: kreuzberg::OrientationResult) -> Self {
        Self {
            degrees: val.degrees,
            confidence: val.confidence as f64,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::BBox> for JsBBox {
    fn from(val: kreuzberg::BBox) -> Self {
        Self {
            x1: val.x1 as f64,
            y1: val.y1 as f64,
            x2: val.x2 as f64,
            y2: val.y2 as f64,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::LayoutDetection> for JsLayoutDetection {
    fn from(val: kreuzberg::LayoutDetection) -> Self {
        Self {
            class_name: val.class_name.into(),
            confidence: val.confidence as f64,
            bbox: val.bbox.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::RecognizedTable> for JsRecognizedTable {
    fn from(val: kreuzberg::RecognizedTable) -> Self {
        Self {
            detection_bbox: val.detection_bbox.into(),
            cells: val.cells.into_iter().collect(),
            markdown: val.markdown,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DetectionResult> for JsDetectionResult {
    fn from(val: kreuzberg::DetectionResult) -> Self {
        Self {
            page_width: val.page_width,
            page_height: val.page_height,
            detections: val.detections.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::pdf::embedded_files::EmbeddedFile> for JsEmbeddedFile {
    fn from(val: kreuzberg::pdf::embedded_files::EmbeddedFile) -> Self {
        Self {
            name: val.name,
            data: val.data.to_vec().into(),
            compressed_size: val.compressed_size as i64,
            mime_type: val.mime_type,
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsPdfMetadata> for kreuzberg::pdf::metadata::PdfMetadata {
    fn from(val: JsPdfMetadata) -> Self {
        let mut __result = kreuzberg::pdf::metadata::PdfMetadata::default();
        __result.pdf_version = val.pdf_version;
        __result.producer = val.producer;
        __result.is_encrypted = val.is_encrypted;
        __result.width = val.width;
        __result.height = val.height;
        __result.page_count = val.page_count;
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::pdf::metadata::PdfMetadata> for JsPdfMetadata {
    fn from(val: kreuzberg::pdf::metadata::PdfMetadata) -> Self {
        Self {
            pdf_version: val.pdf_version,
            producer: val.producer,
            is_encrypted: val.is_encrypted,
            width: val.width,
            height: val.height,
            page_count: val.page_count,
        }
    }
}

impl From<JsExecutionProviderType> for kreuzberg::ExecutionProviderType {
    fn from(val: JsExecutionProviderType) -> Self {
        match val {
            JsExecutionProviderType::Auto => Self::Auto,
            JsExecutionProviderType::Cpu => Self::Cpu,
            JsExecutionProviderType::CoreMl => Self::CoreMl,
            JsExecutionProviderType::Cuda => Self::Cuda,
            JsExecutionProviderType::TensorRt => Self::TensorRt,
        }
    }
}

impl From<kreuzberg::ExecutionProviderType> for JsExecutionProviderType {
    fn from(val: kreuzberg::ExecutionProviderType) -> Self {
        match val {
            kreuzberg::ExecutionProviderType::Auto => Self::Auto,
            kreuzberg::ExecutionProviderType::Cpu => Self::Cpu,
            kreuzberg::ExecutionProviderType::CoreMl => Self::CoreMl,
            kreuzberg::ExecutionProviderType::Cuda => Self::Cuda,
            kreuzberg::ExecutionProviderType::TensorRt => Self::TensorRt,
        }
    }
}

impl From<JsOutputFormat> for kreuzberg::OutputFormat {
    fn from(val: JsOutputFormat) -> Self {
        match val {
            JsOutputFormat::Plain => Self::Plain,
            JsOutputFormat::Markdown => Self::Markdown,
            JsOutputFormat::Djot => Self::Djot,
            JsOutputFormat::Html => Self::Html,
            JsOutputFormat::Json => Self::Json,
            JsOutputFormat::Structured => Self::Structured,
            JsOutputFormat::Custom => Self::Custom(Default::default()),
        }
    }
}

impl From<kreuzberg::OutputFormat> for JsOutputFormat {
    fn from(val: kreuzberg::OutputFormat) -> Self {
        match val {
            kreuzberg::OutputFormat::Plain => Self::Plain,
            kreuzberg::OutputFormat::Markdown => Self::Markdown,
            kreuzberg::OutputFormat::Djot => Self::Djot,
            kreuzberg::OutputFormat::Html => Self::Html,
            kreuzberg::OutputFormat::Json => Self::Json,
            kreuzberg::OutputFormat::Structured => Self::Structured,
            kreuzberg::OutputFormat::Custom(..) => Self::Custom,
        }
    }
}

impl From<JsHtmlTheme> for kreuzberg::HtmlTheme {
    fn from(val: JsHtmlTheme) -> Self {
        match val {
            JsHtmlTheme::Default => Self::Default,
            JsHtmlTheme::GitHub => Self::GitHub,
            JsHtmlTheme::Dark => Self::Dark,
            JsHtmlTheme::Light => Self::Light,
            JsHtmlTheme::Unstyled => Self::Unstyled,
        }
    }
}

impl From<kreuzberg::HtmlTheme> for JsHtmlTheme {
    fn from(val: kreuzberg::HtmlTheme) -> Self {
        match val {
            kreuzberg::HtmlTheme::Default => Self::Default,
            kreuzberg::HtmlTheme::GitHub => Self::GitHub,
            kreuzberg::HtmlTheme::Dark => Self::Dark,
            kreuzberg::HtmlTheme::Light => Self::Light,
            kreuzberg::HtmlTheme::Unstyled => Self::Unstyled,
        }
    }
}

impl From<JsTableModel> for kreuzberg::TableModel {
    fn from(val: JsTableModel) -> Self {
        match val {
            JsTableModel::Tatr => Self::Tatr,
            JsTableModel::SlanetWired => Self::SlanetWired,
            JsTableModel::SlanetWireless => Self::SlanetWireless,
            JsTableModel::SlanetPlus => Self::SlanetPlus,
            JsTableModel::SlanetAuto => Self::SlanetAuto,
            JsTableModel::Disabled => Self::Disabled,
        }
    }
}

impl From<kreuzberg::TableModel> for JsTableModel {
    fn from(val: kreuzberg::TableModel) -> Self {
        match val {
            kreuzberg::TableModel::Tatr => Self::Tatr,
            kreuzberg::TableModel::SlanetWired => Self::SlanetWired,
            kreuzberg::TableModel::SlanetWireless => Self::SlanetWireless,
            kreuzberg::TableModel::SlanetPlus => Self::SlanetPlus,
            kreuzberg::TableModel::SlanetAuto => Self::SlanetAuto,
            kreuzberg::TableModel::Disabled => Self::Disabled,
        }
    }
}

impl From<JsChunkerType> for kreuzberg::ChunkerType {
    fn from(val: JsChunkerType) -> Self {
        match val {
            JsChunkerType::Text => Self::Text,
            JsChunkerType::Markdown => Self::Markdown,
            JsChunkerType::Yaml => Self::Yaml,
            JsChunkerType::Semantic => Self::Semantic,
        }
    }
}

impl From<kreuzberg::ChunkerType> for JsChunkerType {
    fn from(val: kreuzberg::ChunkerType) -> Self {
        match val {
            kreuzberg::ChunkerType::Text => Self::Text,
            kreuzberg::ChunkerType::Markdown => Self::Markdown,
            kreuzberg::ChunkerType::Yaml => Self::Yaml,
            kreuzberg::ChunkerType::Semantic => Self::Semantic,
        }
    }
}

impl From<JsChunkSizing> for kreuzberg::ChunkSizing {
    fn from(val: JsChunkSizing) -> Self {
        match val.type_tag.as_str() {
            "characters" => Self::Characters,
            "tokenizer" => Self::Tokenizer {
                model: val.model.unwrap_or_default(),
                cache_dir: val.cache_dir.map(std::path::PathBuf::from),
            },
            _ => Self::Characters,
        }
    }
}

impl From<kreuzberg::ChunkSizing> for JsChunkSizing {
    fn from(val: kreuzberg::ChunkSizing) -> Self {
        match val {
            kreuzberg::ChunkSizing::Characters => Self {
                type_tag: "characters".to_string(),
                cache_dir: None,
                model: None,
            },
            kreuzberg::ChunkSizing::Tokenizer { model, cache_dir } => Self {
                type_tag: "tokenizer".to_string(),
                cache_dir: cache_dir.map(|p| p.to_string_lossy().to_string()),
                model: Some(model),
            },
        }
    }
}

impl From<JsEmbeddingModelType> for kreuzberg::EmbeddingModelType {
    fn from(val: JsEmbeddingModelType) -> Self {
        match val.type_tag.as_str() {
            "preset" => Self::Preset {
                name: val.name.unwrap_or_default(),
            },
            "custom" => Self::Custom {
                model_id: val.model_id.unwrap_or_default(),
                dimensions: val.dimensions.map(|v| v as usize).unwrap_or_default(),
            },
            "llm" => Self::Llm {
                llm: val.llm.map(|v| v.into()).unwrap_or_default(),
            },
            "plugin" => Self::Plugin {
                name: val.name.unwrap_or_default(),
            },
            _ => Self::Preset {
                name: Default::default(),
            },
        }
    }
}

impl From<kreuzberg::EmbeddingModelType> for JsEmbeddingModelType {
    fn from(val: kreuzberg::EmbeddingModelType) -> Self {
        match val {
            kreuzberg::EmbeddingModelType::Preset { name } => Self {
                type_tag: "preset".to_string(),
                dimensions: None,
                llm: None,
                model_id: None,
                name: Some(name),
            },
            kreuzberg::EmbeddingModelType::Custom { model_id, dimensions } => Self {
                type_tag: "custom".to_string(),
                dimensions: Some(dimensions as i64),
                llm: None,
                model_id: Some(model_id),
                name: None,
            },
            kreuzberg::EmbeddingModelType::Llm { llm } => Self {
                type_tag: "llm".to_string(),
                dimensions: None,
                llm: Some((llm).into()),
                model_id: None,
                name: None,
            },
            kreuzberg::EmbeddingModelType::Plugin { name } => Self {
                type_tag: "plugin".to_string(),
                dimensions: None,
                llm: None,
                model_id: None,
                name: Some(name),
            },
        }
    }
}

impl From<JsCodeContentMode> for kreuzberg::CodeContentMode {
    fn from(val: JsCodeContentMode) -> Self {
        match val {
            JsCodeContentMode::Chunks => Self::Chunks,
            JsCodeContentMode::Raw => Self::Raw,
            JsCodeContentMode::Structure => Self::Structure,
        }
    }
}

impl From<kreuzberg::CodeContentMode> for JsCodeContentMode {
    fn from(val: kreuzberg::CodeContentMode) -> Self {
        match val {
            kreuzberg::CodeContentMode::Chunks => Self::Chunks,
            kreuzberg::CodeContentMode::Raw => Self::Raw,
            kreuzberg::CodeContentMode::Structure => Self::Structure,
        }
    }
}

impl From<kreuzberg::extraction::transform::ListType> for JsListType {
    fn from(val: kreuzberg::extraction::transform::ListType) -> Self {
        match val {
            kreuzberg::extraction::transform::ListType::Bullet => Self::Bullet,
            kreuzberg::extraction::transform::ListType::Numbered => Self::Numbered,
            kreuzberg::extraction::transform::ListType::Lettered => Self::Lettered,
            kreuzberg::extraction::transform::ListType::Indented => Self::Indented,
        }
    }
}

impl From<kreuzberg::OcrBackendType> for JsOcrBackendType {
    fn from(val: kreuzberg::OcrBackendType) -> Self {
        match val {
            kreuzberg::OcrBackendType::Tesseract => Self::Tesseract,
            kreuzberg::OcrBackendType::EasyOCR => Self::EasyOCR,
            kreuzberg::OcrBackendType::PaddleOCR => Self::PaddleOCR,
            kreuzberg::OcrBackendType::Custom => Self::Custom,
        }
    }
}

impl From<kreuzberg::ProcessingStage> for JsProcessingStage {
    fn from(val: kreuzberg::ProcessingStage) -> Self {
        match val {
            kreuzberg::ProcessingStage::Early => Self::Early,
            kreuzberg::ProcessingStage::Middle => Self::Middle,
            kreuzberg::ProcessingStage::Late => Self::Late,
        }
    }
}

impl From<JsReductionLevel> for kreuzberg::ReductionLevel {
    fn from(val: JsReductionLevel) -> Self {
        match val {
            JsReductionLevel::Off => Self::Off,
            JsReductionLevel::Light => Self::Light,
            JsReductionLevel::Moderate => Self::Moderate,
            JsReductionLevel::Aggressive => Self::Aggressive,
            JsReductionLevel::Maximum => Self::Maximum,
        }
    }
}

impl From<kreuzberg::ReductionLevel> for JsReductionLevel {
    fn from(val: kreuzberg::ReductionLevel) -> Self {
        match val {
            kreuzberg::ReductionLevel::Off => Self::Off,
            kreuzberg::ReductionLevel::Light => Self::Light,
            kreuzberg::ReductionLevel::Moderate => Self::Moderate,
            kreuzberg::ReductionLevel::Aggressive => Self::Aggressive,
            kreuzberg::ReductionLevel::Maximum => Self::Maximum,
        }
    }
}

impl From<JsPdfAnnotationType> for kreuzberg::PdfAnnotationType {
    fn from(val: JsPdfAnnotationType) -> Self {
        match val {
            JsPdfAnnotationType::Text => Self::Text,
            JsPdfAnnotationType::Highlight => Self::Highlight,
            JsPdfAnnotationType::Link => Self::Link,
            JsPdfAnnotationType::Stamp => Self::Stamp,
            JsPdfAnnotationType::Underline => Self::Underline,
            JsPdfAnnotationType::StrikeOut => Self::StrikeOut,
            JsPdfAnnotationType::Other => Self::Other,
        }
    }
}

impl From<kreuzberg::PdfAnnotationType> for JsPdfAnnotationType {
    fn from(val: kreuzberg::PdfAnnotationType) -> Self {
        match val {
            kreuzberg::PdfAnnotationType::Text => Self::Text,
            kreuzberg::PdfAnnotationType::Highlight => Self::Highlight,
            kreuzberg::PdfAnnotationType::Link => Self::Link,
            kreuzberg::PdfAnnotationType::Stamp => Self::Stamp,
            kreuzberg::PdfAnnotationType::Underline => Self::Underline,
            kreuzberg::PdfAnnotationType::StrikeOut => Self::StrikeOut,
            kreuzberg::PdfAnnotationType::Other => Self::Other,
        }
    }
}

impl From<JsBlockType> for kreuzberg::BlockType {
    fn from(val: JsBlockType) -> Self {
        match val {
            JsBlockType::Paragraph => Self::Paragraph,
            JsBlockType::Heading => Self::Heading,
            JsBlockType::Blockquote => Self::Blockquote,
            JsBlockType::CodeBlock => Self::CodeBlock,
            JsBlockType::ListItem => Self::ListItem,
            JsBlockType::OrderedList => Self::OrderedList,
            JsBlockType::BulletList => Self::BulletList,
            JsBlockType::TaskList => Self::TaskList,
            JsBlockType::DefinitionList => Self::DefinitionList,
            JsBlockType::DefinitionTerm => Self::DefinitionTerm,
            JsBlockType::DefinitionDescription => Self::DefinitionDescription,
            JsBlockType::Div => Self::Div,
            JsBlockType::Section => Self::Section,
            JsBlockType::ThematicBreak => Self::ThematicBreak,
            JsBlockType::RawBlock => Self::RawBlock,
            JsBlockType::MathDisplay => Self::MathDisplay,
        }
    }
}

impl From<kreuzberg::BlockType> for JsBlockType {
    fn from(val: kreuzberg::BlockType) -> Self {
        match val {
            kreuzberg::BlockType::Paragraph => Self::Paragraph,
            kreuzberg::BlockType::Heading => Self::Heading,
            kreuzberg::BlockType::Blockquote => Self::Blockquote,
            kreuzberg::BlockType::CodeBlock => Self::CodeBlock,
            kreuzberg::BlockType::ListItem => Self::ListItem,
            kreuzberg::BlockType::OrderedList => Self::OrderedList,
            kreuzberg::BlockType::BulletList => Self::BulletList,
            kreuzberg::BlockType::TaskList => Self::TaskList,
            kreuzberg::BlockType::DefinitionList => Self::DefinitionList,
            kreuzberg::BlockType::DefinitionTerm => Self::DefinitionTerm,
            kreuzberg::BlockType::DefinitionDescription => Self::DefinitionDescription,
            kreuzberg::BlockType::Div => Self::Div,
            kreuzberg::BlockType::Section => Self::Section,
            kreuzberg::BlockType::ThematicBreak => Self::ThematicBreak,
            kreuzberg::BlockType::RawBlock => Self::RawBlock,
            kreuzberg::BlockType::MathDisplay => Self::MathDisplay,
        }
    }
}

impl From<JsInlineType> for kreuzberg::InlineType {
    fn from(val: JsInlineType) -> Self {
        match val {
            JsInlineType::Text => Self::Text,
            JsInlineType::Strong => Self::Strong,
            JsInlineType::Emphasis => Self::Emphasis,
            JsInlineType::Highlight => Self::Highlight,
            JsInlineType::Subscript => Self::Subscript,
            JsInlineType::Superscript => Self::Superscript,
            JsInlineType::Insert => Self::Insert,
            JsInlineType::Delete => Self::Delete,
            JsInlineType::Code => Self::Code,
            JsInlineType::Link => Self::Link,
            JsInlineType::Image => Self::Image,
            JsInlineType::Span => Self::Span,
            JsInlineType::Math => Self::Math,
            JsInlineType::RawInline => Self::RawInline,
            JsInlineType::FootnoteRef => Self::FootnoteRef,
            JsInlineType::Symbol => Self::Symbol,
        }
    }
}

impl From<kreuzberg::InlineType> for JsInlineType {
    fn from(val: kreuzberg::InlineType) -> Self {
        match val {
            kreuzberg::InlineType::Text => Self::Text,
            kreuzberg::InlineType::Strong => Self::Strong,
            kreuzberg::InlineType::Emphasis => Self::Emphasis,
            kreuzberg::InlineType::Highlight => Self::Highlight,
            kreuzberg::InlineType::Subscript => Self::Subscript,
            kreuzberg::InlineType::Superscript => Self::Superscript,
            kreuzberg::InlineType::Insert => Self::Insert,
            kreuzberg::InlineType::Delete => Self::Delete,
            kreuzberg::InlineType::Code => Self::Code,
            kreuzberg::InlineType::Link => Self::Link,
            kreuzberg::InlineType::Image => Self::Image,
            kreuzberg::InlineType::Span => Self::Span,
            kreuzberg::InlineType::Math => Self::Math,
            kreuzberg::InlineType::RawInline => Self::RawInline,
            kreuzberg::InlineType::FootnoteRef => Self::FootnoteRef,
            kreuzberg::InlineType::Symbol => Self::Symbol,
        }
    }
}

impl From<JsRelationshipKind> for kreuzberg::RelationshipKind {
    fn from(val: JsRelationshipKind) -> Self {
        match val {
            JsRelationshipKind::FootnoteReference => Self::FootnoteReference,
            JsRelationshipKind::CitationReference => Self::CitationReference,
            JsRelationshipKind::InternalLink => Self::InternalLink,
            JsRelationshipKind::Caption => Self::Caption,
            JsRelationshipKind::Label => Self::Label,
            JsRelationshipKind::TocEntry => Self::TocEntry,
            JsRelationshipKind::CrossReference => Self::CrossReference,
        }
    }
}

impl From<kreuzberg::RelationshipKind> for JsRelationshipKind {
    fn from(val: kreuzberg::RelationshipKind) -> Self {
        match val {
            kreuzberg::RelationshipKind::FootnoteReference => Self::FootnoteReference,
            kreuzberg::RelationshipKind::CitationReference => Self::CitationReference,
            kreuzberg::RelationshipKind::InternalLink => Self::InternalLink,
            kreuzberg::RelationshipKind::Caption => Self::Caption,
            kreuzberg::RelationshipKind::Label => Self::Label,
            kreuzberg::RelationshipKind::TocEntry => Self::TocEntry,
            kreuzberg::RelationshipKind::CrossReference => Self::CrossReference,
        }
    }
}

impl From<JsContentLayer> for kreuzberg::ContentLayer {
    fn from(val: JsContentLayer) -> Self {
        match val {
            JsContentLayer::Body => Self::Body,
            JsContentLayer::Header => Self::Header,
            JsContentLayer::Footer => Self::Footer,
            JsContentLayer::Footnote => Self::Footnote,
        }
    }
}

impl From<kreuzberg::ContentLayer> for JsContentLayer {
    fn from(val: kreuzberg::ContentLayer) -> Self {
        match val {
            kreuzberg::ContentLayer::Body => Self::Body,
            kreuzberg::ContentLayer::Header => Self::Header,
            kreuzberg::ContentLayer::Footer => Self::Footer,
            kreuzberg::ContentLayer::Footnote => Self::Footnote,
        }
    }
}

impl From<JsNodeContent> for kreuzberg::NodeContent {
    fn from(val: JsNodeContent) -> Self {
        match val.node_type_tag.as_str() {
            "title" => Self::Title {
                text: val.text.unwrap_or_default(),
            },
            "heading" => Self::Heading {
                level: val.level.unwrap_or_default(),
                text: val.text.unwrap_or_default(),
            },
            "paragraph" => Self::Paragraph {
                text: val.text.unwrap_or_default(),
            },
            "list" => Self::List {
                ordered: val.ordered.unwrap_or_default(),
            },
            "listitem" => Self::ListItem {
                text: val.text.unwrap_or_default(),
            },
            "table" => Self::Table {
                grid: val.grid.map(|v| v.into()).unwrap_or_default(),
            },
            "image" => Self::Image {
                description: val.description,
                image_index: val.image_index,
                src: val.src,
            },
            "code" => Self::Code {
                text: val.text.unwrap_or_default(),
                language: val.language,
            },
            "quote" => Self::Quote,
            "formula" => Self::Formula {
                text: val.text.unwrap_or_default(),
            },
            "footnote" => Self::Footnote {
                text: val.text.unwrap_or_default(),
            },
            "group" => Self::Group {
                label: val.label,
                heading_level: val.heading_level,
                heading_text: val.heading_text,
            },
            "pagebreak" => Self::PageBreak,
            "slide" => Self::Slide {
                number: val.number.unwrap_or_default(),
                title: val.title,
            },
            "definitionlist" => Self::DefinitionList,
            "definitionitem" => Self::DefinitionItem {
                term: val.term.unwrap_or_default(),
                definition: val.definition.unwrap_or_default(),
            },
            "citation" => Self::Citation {
                key: val.key.unwrap_or_default(),
                text: val.text.unwrap_or_default(),
            },
            "admonition" => Self::Admonition {
                kind: val.kind.unwrap_or_default(),
                title: val.title,
            },
            "rawblock" => Self::RawBlock {
                format: val.format.unwrap_or_default(),
                content: val.content.unwrap_or_default(),
            },
            "metadatablock" => Self::MetadataBlock {
                entries: Default::default(),
            },
            _ => Self::Title {
                text: Default::default(),
            },
        }
    }
}

impl From<kreuzberg::NodeContent> for JsNodeContent {
    fn from(val: kreuzberg::NodeContent) -> Self {
        match val {
            kreuzberg::NodeContent::Title { text } => Self {
                node_type_tag: "title".to_string(),
                content: None,
                definition: None,
                description: None,
                entries: None,
                format: None,
                grid: None,
                heading_level: None,
                heading_text: None,
                image_index: None,
                key: None,
                kind: None,
                label: None,
                language: None,
                level: None,
                number: None,
                ordered: None,
                src: None,
                term: None,
                text: Some(text),
                title: None,
            },
            kreuzberg::NodeContent::Heading { level, text } => Self {
                node_type_tag: "heading".to_string(),
                content: None,
                definition: None,
                description: None,
                entries: None,
                format: None,
                grid: None,
                heading_level: None,
                heading_text: None,
                image_index: None,
                key: None,
                kind: None,
                label: None,
                language: None,
                level: Some(level),
                number: None,
                ordered: None,
                src: None,
                term: None,
                text: Some(text),
                title: None,
            },
            kreuzberg::NodeContent::Paragraph { text } => Self {
                node_type_tag: "paragraph".to_string(),
                content: None,
                definition: None,
                description: None,
                entries: None,
                format: None,
                grid: None,
                heading_level: None,
                heading_text: None,
                image_index: None,
                key: None,
                kind: None,
                label: None,
                language: None,
                level: None,
                number: None,
                ordered: None,
                src: None,
                term: None,
                text: Some(text),
                title: None,
            },
            kreuzberg::NodeContent::List { ordered } => Self {
                node_type_tag: "list".to_string(),
                content: None,
                definition: None,
                description: None,
                entries: None,
                format: None,
                grid: None,
                heading_level: None,
                heading_text: None,
                image_index: None,
                key: None,
                kind: None,
                label: None,
                language: None,
                level: None,
                number: None,
                ordered: Some(ordered),
                src: None,
                term: None,
                text: None,
                title: None,
            },
            kreuzberg::NodeContent::ListItem { text } => Self {
                node_type_tag: "listitem".to_string(),
                content: None,
                definition: None,
                description: None,
                entries: None,
                format: None,
                grid: None,
                heading_level: None,
                heading_text: None,
                image_index: None,
                key: None,
                kind: None,
                label: None,
                language: None,
                level: None,
                number: None,
                ordered: None,
                src: None,
                term: None,
                text: Some(text),
                title: None,
            },
            kreuzberg::NodeContent::Table { grid } => Self {
                node_type_tag: "table".to_string(),
                content: None,
                definition: None,
                description: None,
                entries: None,
                format: None,
                grid: Some((grid).into()),
                heading_level: None,
                heading_text: None,
                image_index: None,
                key: None,
                kind: None,
                label: None,
                language: None,
                level: None,
                number: None,
                ordered: None,
                src: None,
                term: None,
                text: None,
                title: None,
            },
            kreuzberg::NodeContent::Image {
                description,
                image_index,
                src,
            } => Self {
                node_type_tag: "image".to_string(),
                content: None,
                definition: None,
                description,
                entries: None,
                format: None,
                grid: None,
                heading_level: None,
                heading_text: None,
                image_index,
                key: None,
                kind: None,
                label: None,
                language: None,
                level: None,
                number: None,
                ordered: None,
                src,
                term: None,
                text: None,
                title: None,
            },
            kreuzberg::NodeContent::Code { text, language } => Self {
                node_type_tag: "code".to_string(),
                content: None,
                definition: None,
                description: None,
                entries: None,
                format: None,
                grid: None,
                heading_level: None,
                heading_text: None,
                image_index: None,
                key: None,
                kind: None,
                label: None,
                language,
                level: None,
                number: None,
                ordered: None,
                src: None,
                term: None,
                text: Some(text),
                title: None,
            },
            kreuzberg::NodeContent::Quote => Self {
                node_type_tag: "quote".to_string(),
                content: None,
                definition: None,
                description: None,
                entries: None,
                format: None,
                grid: None,
                heading_level: None,
                heading_text: None,
                image_index: None,
                key: None,
                kind: None,
                label: None,
                language: None,
                level: None,
                number: None,
                ordered: None,
                src: None,
                term: None,
                text: None,
                title: None,
            },
            kreuzberg::NodeContent::Formula { text } => Self {
                node_type_tag: "formula".to_string(),
                content: None,
                definition: None,
                description: None,
                entries: None,
                format: None,
                grid: None,
                heading_level: None,
                heading_text: None,
                image_index: None,
                key: None,
                kind: None,
                label: None,
                language: None,
                level: None,
                number: None,
                ordered: None,
                src: None,
                term: None,
                text: Some(text),
                title: None,
            },
            kreuzberg::NodeContent::Footnote { text } => Self {
                node_type_tag: "footnote".to_string(),
                content: None,
                definition: None,
                description: None,
                entries: None,
                format: None,
                grid: None,
                heading_level: None,
                heading_text: None,
                image_index: None,
                key: None,
                kind: None,
                label: None,
                language: None,
                level: None,
                number: None,
                ordered: None,
                src: None,
                term: None,
                text: Some(text),
                title: None,
            },
            kreuzberg::NodeContent::Group {
                label,
                heading_level,
                heading_text,
            } => Self {
                node_type_tag: "group".to_string(),
                content: None,
                definition: None,
                description: None,
                entries: None,
                format: None,
                grid: None,
                heading_level,
                heading_text,
                image_index: None,
                key: None,
                kind: None,
                label,
                language: None,
                level: None,
                number: None,
                ordered: None,
                src: None,
                term: None,
                text: None,
                title: None,
            },
            kreuzberg::NodeContent::PageBreak => Self {
                node_type_tag: "pagebreak".to_string(),
                content: None,
                definition: None,
                description: None,
                entries: None,
                format: None,
                grid: None,
                heading_level: None,
                heading_text: None,
                image_index: None,
                key: None,
                kind: None,
                label: None,
                language: None,
                level: None,
                number: None,
                ordered: None,
                src: None,
                term: None,
                text: None,
                title: None,
            },
            kreuzberg::NodeContent::Slide { number, title } => Self {
                node_type_tag: "slide".to_string(),
                content: None,
                definition: None,
                description: None,
                entries: None,
                format: None,
                grid: None,
                heading_level: None,
                heading_text: None,
                image_index: None,
                key: None,
                kind: None,
                label: None,
                language: None,
                level: None,
                number: Some(number),
                ordered: None,
                src: None,
                term: None,
                text: None,
                title,
            },
            kreuzberg::NodeContent::DefinitionList => Self {
                node_type_tag: "definitionlist".to_string(),
                content: None,
                definition: None,
                description: None,
                entries: None,
                format: None,
                grid: None,
                heading_level: None,
                heading_text: None,
                image_index: None,
                key: None,
                kind: None,
                label: None,
                language: None,
                level: None,
                number: None,
                ordered: None,
                src: None,
                term: None,
                text: None,
                title: None,
            },
            kreuzberg::NodeContent::DefinitionItem { term, definition } => Self {
                node_type_tag: "definitionitem".to_string(),
                content: None,
                definition: Some(definition),
                description: None,
                entries: None,
                format: None,
                grid: None,
                heading_level: None,
                heading_text: None,
                image_index: None,
                key: None,
                kind: None,
                label: None,
                language: None,
                level: None,
                number: None,
                ordered: None,
                src: None,
                term: Some(term),
                text: None,
                title: None,
            },
            kreuzberg::NodeContent::Citation { key, text } => Self {
                node_type_tag: "citation".to_string(),
                content: None,
                definition: None,
                description: None,
                entries: None,
                format: None,
                grid: None,
                heading_level: None,
                heading_text: None,
                image_index: None,
                key: Some(key),
                kind: None,
                label: None,
                language: None,
                level: None,
                number: None,
                ordered: None,
                src: None,
                term: None,
                text: Some(text),
                title: None,
            },
            kreuzberg::NodeContent::Admonition { kind, title } => Self {
                node_type_tag: "admonition".to_string(),
                content: None,
                definition: None,
                description: None,
                entries: None,
                format: None,
                grid: None,
                heading_level: None,
                heading_text: None,
                image_index: None,
                key: None,
                kind: Some(kind),
                label: None,
                language: None,
                level: None,
                number: None,
                ordered: None,
                src: None,
                term: None,
                text: None,
                title,
            },
            kreuzberg::NodeContent::RawBlock { format, content } => Self {
                node_type_tag: "rawblock".to_string(),
                content: Some(content),
                definition: None,
                description: None,
                entries: None,
                format: Some(format),
                grid: None,
                heading_level: None,
                heading_text: None,
                image_index: None,
                key: None,
                kind: None,
                label: None,
                language: None,
                level: None,
                number: None,
                ordered: None,
                src: None,
                term: None,
                text: None,
                title: None,
            },
            kreuzberg::NodeContent::MetadataBlock { entries: _entries } => Self {
                node_type_tag: "metadatablock".to_string(),
                content: None,
                definition: None,
                description: None,
                entries: None,
                format: None,
                grid: None,
                heading_level: None,
                heading_text: None,
                image_index: None,
                key: None,
                kind: None,
                label: None,
                language: None,
                level: None,
                number: None,
                ordered: None,
                src: None,
                term: None,
                text: None,
                title: None,
            },
        }
    }
}

impl From<JsAnnotationKind> for kreuzberg::AnnotationKind {
    fn from(val: JsAnnotationKind) -> Self {
        match val.annotation_type_tag.as_str() {
            "bold" => Self::Bold,
            "italic" => Self::Italic,
            "underline" => Self::Underline,
            "strikethrough" => Self::Strikethrough,
            "code" => Self::Code,
            "subscript" => Self::Subscript,
            "superscript" => Self::Superscript,
            "link" => Self::Link {
                url: val.url.unwrap_or_default(),
                title: val.title,
            },
            "highlight" => Self::Highlight,
            "color" => Self::Color {
                value: val.value.unwrap_or_default(),
            },
            "fontsize" => Self::FontSize {
                value: val.value.unwrap_or_default(),
            },
            "custom" => Self::Custom {
                name: val.name.unwrap_or_default(),
                value: val.value,
            },
            _ => Self::Bold,
        }
    }
}

impl From<kreuzberg::AnnotationKind> for JsAnnotationKind {
    fn from(val: kreuzberg::AnnotationKind) -> Self {
        match val {
            kreuzberg::AnnotationKind::Bold => Self {
                annotation_type_tag: "bold".to_string(),
                name: None,
                title: None,
                url: None,
                value: None,
            },
            kreuzberg::AnnotationKind::Italic => Self {
                annotation_type_tag: "italic".to_string(),
                name: None,
                title: None,
                url: None,
                value: None,
            },
            kreuzberg::AnnotationKind::Underline => Self {
                annotation_type_tag: "underline".to_string(),
                name: None,
                title: None,
                url: None,
                value: None,
            },
            kreuzberg::AnnotationKind::Strikethrough => Self {
                annotation_type_tag: "strikethrough".to_string(),
                name: None,
                title: None,
                url: None,
                value: None,
            },
            kreuzberg::AnnotationKind::Code => Self {
                annotation_type_tag: "code".to_string(),
                name: None,
                title: None,
                url: None,
                value: None,
            },
            kreuzberg::AnnotationKind::Subscript => Self {
                annotation_type_tag: "subscript".to_string(),
                name: None,
                title: None,
                url: None,
                value: None,
            },
            kreuzberg::AnnotationKind::Superscript => Self {
                annotation_type_tag: "superscript".to_string(),
                name: None,
                title: None,
                url: None,
                value: None,
            },
            kreuzberg::AnnotationKind::Link { url, title } => Self {
                annotation_type_tag: "link".to_string(),
                name: None,
                title,
                url: Some(url),
                value: None,
            },
            kreuzberg::AnnotationKind::Highlight => Self {
                annotation_type_tag: "highlight".to_string(),
                name: None,
                title: None,
                url: None,
                value: None,
            },
            kreuzberg::AnnotationKind::Color { value } => Self {
                annotation_type_tag: "color".to_string(),
                name: None,
                title: None,
                url: None,
                value: Some(value),
            },
            kreuzberg::AnnotationKind::FontSize { value } => Self {
                annotation_type_tag: "fontsize".to_string(),
                name: None,
                title: None,
                url: None,
                value: Some(value),
            },
            kreuzberg::AnnotationKind::Custom { name, value } => Self {
                annotation_type_tag: "custom".to_string(),
                name: Some(name),
                title: None,
                url: None,
                value,
            },
        }
    }
}

impl From<JsExtractionMethod> for kreuzberg::ExtractionMethod {
    fn from(val: JsExtractionMethod) -> Self {
        match val {
            JsExtractionMethod::Native => Self::Native,
            JsExtractionMethod::Ocr => Self::Ocr,
            JsExtractionMethod::Mixed => Self::Mixed,
        }
    }
}

impl From<kreuzberg::ExtractionMethod> for JsExtractionMethod {
    fn from(val: kreuzberg::ExtractionMethod) -> Self {
        match val {
            kreuzberg::ExtractionMethod::Native => Self::Native,
            kreuzberg::ExtractionMethod::Ocr => Self::Ocr,
            kreuzberg::ExtractionMethod::Mixed => Self::Mixed,
        }
    }
}

impl From<JsChunkType> for kreuzberg::ChunkType {
    fn from(val: JsChunkType) -> Self {
        match val {
            JsChunkType::Heading => Self::Heading,
            JsChunkType::PartyList => Self::PartyList,
            JsChunkType::Definitions => Self::Definitions,
            JsChunkType::OperativeClause => Self::OperativeClause,
            JsChunkType::SignatureBlock => Self::SignatureBlock,
            JsChunkType::Schedule => Self::Schedule,
            JsChunkType::TableLike => Self::TableLike,
            JsChunkType::Formula => Self::Formula,
            JsChunkType::CodeBlock => Self::CodeBlock,
            JsChunkType::Image => Self::Image,
            JsChunkType::OrgChart => Self::OrgChart,
            JsChunkType::Diagram => Self::Diagram,
            JsChunkType::Unknown => Self::Unknown,
        }
    }
}

impl From<kreuzberg::ChunkType> for JsChunkType {
    fn from(val: kreuzberg::ChunkType) -> Self {
        match val {
            kreuzberg::ChunkType::Heading => Self::Heading,
            kreuzberg::ChunkType::PartyList => Self::PartyList,
            kreuzberg::ChunkType::Definitions => Self::Definitions,
            kreuzberg::ChunkType::OperativeClause => Self::OperativeClause,
            kreuzberg::ChunkType::SignatureBlock => Self::SignatureBlock,
            kreuzberg::ChunkType::Schedule => Self::Schedule,
            kreuzberg::ChunkType::TableLike => Self::TableLike,
            kreuzberg::ChunkType::Formula => Self::Formula,
            kreuzberg::ChunkType::CodeBlock => Self::CodeBlock,
            kreuzberg::ChunkType::Image => Self::Image,
            kreuzberg::ChunkType::OrgChart => Self::OrgChart,
            kreuzberg::ChunkType::Diagram => Self::Diagram,
            kreuzberg::ChunkType::Unknown => Self::Unknown,
        }
    }
}

impl From<JsImageKind> for kreuzberg::ImageKind {
    fn from(val: JsImageKind) -> Self {
        match val {
            JsImageKind::Photograph => Self::Photograph,
            JsImageKind::Diagram => Self::Diagram,
            JsImageKind::Chart => Self::Chart,
            JsImageKind::Drawing => Self::Drawing,
            JsImageKind::TextBlock => Self::TextBlock,
            JsImageKind::Decoration => Self::Decoration,
            JsImageKind::Logo => Self::Logo,
            JsImageKind::Icon => Self::Icon,
            JsImageKind::TileFragment => Self::TileFragment,
            JsImageKind::Mask => Self::Mask,
            JsImageKind::PageRaster => Self::PageRaster,
            JsImageKind::Unknown => Self::Unknown,
        }
    }
}

impl From<kreuzberg::ImageKind> for JsImageKind {
    fn from(val: kreuzberg::ImageKind) -> Self {
        match val {
            kreuzberg::ImageKind::Photograph => Self::Photograph,
            kreuzberg::ImageKind::Diagram => Self::Diagram,
            kreuzberg::ImageKind::Chart => Self::Chart,
            kreuzberg::ImageKind::Drawing => Self::Drawing,
            kreuzberg::ImageKind::TextBlock => Self::TextBlock,
            kreuzberg::ImageKind::Decoration => Self::Decoration,
            kreuzberg::ImageKind::Logo => Self::Logo,
            kreuzberg::ImageKind::Icon => Self::Icon,
            kreuzberg::ImageKind::TileFragment => Self::TileFragment,
            kreuzberg::ImageKind::Mask => Self::Mask,
            kreuzberg::ImageKind::PageRaster => Self::PageRaster,
            kreuzberg::ImageKind::Unknown => Self::Unknown,
        }
    }
}

impl From<JsResultFormat> for kreuzberg::ResultFormat {
    fn from(val: JsResultFormat) -> Self {
        match val {
            JsResultFormat::Unified => Self::Unified,
            JsResultFormat::ElementBased => Self::ElementBased,
        }
    }
}

impl From<kreuzberg::ResultFormat> for JsResultFormat {
    fn from(val: kreuzberg::ResultFormat) -> Self {
        match val {
            kreuzberg::ResultFormat::Unified => Self::Unified,
            kreuzberg::ResultFormat::ElementBased => Self::ElementBased,
        }
    }
}

impl From<JsElementType> for kreuzberg::ElementType {
    fn from(val: JsElementType) -> Self {
        match val {
            JsElementType::Title => Self::Title,
            JsElementType::NarrativeText => Self::NarrativeText,
            JsElementType::Heading => Self::Heading,
            JsElementType::ListItem => Self::ListItem,
            JsElementType::Table => Self::Table,
            JsElementType::Image => Self::Image,
            JsElementType::PageBreak => Self::PageBreak,
            JsElementType::CodeBlock => Self::CodeBlock,
            JsElementType::BlockQuote => Self::BlockQuote,
            JsElementType::Footer => Self::Footer,
            JsElementType::Header => Self::Header,
        }
    }
}

impl From<kreuzberg::ElementType> for JsElementType {
    fn from(val: kreuzberg::ElementType) -> Self {
        match val {
            kreuzberg::ElementType::Title => Self::Title,
            kreuzberg::ElementType::NarrativeText => Self::NarrativeText,
            kreuzberg::ElementType::Heading => Self::Heading,
            kreuzberg::ElementType::ListItem => Self::ListItem,
            kreuzberg::ElementType::Table => Self::Table,
            kreuzberg::ElementType::Image => Self::Image,
            kreuzberg::ElementType::PageBreak => Self::PageBreak,
            kreuzberg::ElementType::CodeBlock => Self::CodeBlock,
            kreuzberg::ElementType::BlockQuote => Self::BlockQuote,
            kreuzberg::ElementType::Footer => Self::Footer,
            kreuzberg::ElementType::Header => Self::Header,
        }
    }
}

impl From<JsFormatMetadata> for kreuzberg::FormatMetadata {
    fn from(val: JsFormatMetadata) -> Self {
        match val.format_type_tag.as_str() {
            "pdf" => Self::Pdf(val.pdf.map(|v| v.into()).unwrap_or_default()),
            "docx" => Self::Docx(Box::new(val.docx.map(|v| v.into()).unwrap_or_default())),
            "excel" => Self::Excel(val.excel.map(|v| v.into()).unwrap_or_default()),
            "email" => Self::Email(val.email.map(|v| v.into()).unwrap_or_default()),
            "pptx" => Self::Pptx(val.pptx.map(|v| v.into()).unwrap_or_default()),
            "archive" => Self::Archive(val.archive.map(|v| v.into()).unwrap_or_default()),
            "image" => Self::Image(val.image.map(|v| v.into()).unwrap_or_default()),
            "xml" => Self::Xml(val.xml.map(|v| v.into()).unwrap_or_default()),
            "text" => Self::Text(val.text.map(|v| v.into()).unwrap_or_default()),
            "html" => Self::Html(Box::new(val.html.map(|v| v.into()).unwrap_or_default())),
            "ocr" => Self::Ocr(val.ocr.map(|v| v.into()).unwrap_or_default()),
            "csv" => Self::Csv(val.csv.map(|v| v.into()).unwrap_or_default()),
            "bibtex" => Self::Bibtex(val.bibtex.map(|v| v.into()).unwrap_or_default()),
            "citation" => Self::Citation(val.citation.map(|v| v.into()).unwrap_or_default()),
            "fictionbook" => Self::FictionBook(val.fiction_book.map(|v| v.into()).unwrap_or_default()),
            "dbf" => Self::Dbf(val.dbf.map(|v| v.into()).unwrap_or_default()),
            "jats" => Self::Jats(val.jats.map(|v| v.into()).unwrap_or_default()),
            "epub" => Self::Epub(val.epub.map(|v| v.into()).unwrap_or_default()),
            "pst" => Self::Pst(val.pst.map(|v| v.into()).unwrap_or_default()),
            "code" => Self::Code(Default::default()),
            _ => Self::Pdf(Default::default()),
        }
    }
}

impl From<kreuzberg::FormatMetadata> for JsFormatMetadata {
    fn from(val: kreuzberg::FormatMetadata) -> Self {
        match val {
            kreuzberg::FormatMetadata::Pdf(pdf) => Self {
                format_type_tag: "pdf".to_string(),
                code: None,
                pdf: Some(pdf.into()),
                docx: None,
                excel: None,
                email: None,
                pptx: None,
                archive: None,
                image: None,
                xml: None,
                text: None,
                html: None,
                ocr: None,
                csv: None,
                bibtex: None,
                citation: None,
                fiction_book: None,
                dbf: None,
                jats: None,
                epub: None,
                pst: None,
            },
            kreuzberg::FormatMetadata::Docx(docx) => Self {
                format_type_tag: "docx".to_string(),
                code: None,
                pdf: None,
                docx: Some((*docx).into()),
                excel: None,
                email: None,
                pptx: None,
                archive: None,
                image: None,
                xml: None,
                text: None,
                html: None,
                ocr: None,
                csv: None,
                bibtex: None,
                citation: None,
                fiction_book: None,
                dbf: None,
                jats: None,
                epub: None,
                pst: None,
            },
            kreuzberg::FormatMetadata::Excel(excel) => Self {
                format_type_tag: "excel".to_string(),
                code: None,
                pdf: None,
                docx: None,
                excel: Some(excel.into()),
                email: None,
                pptx: None,
                archive: None,
                image: None,
                xml: None,
                text: None,
                html: None,
                ocr: None,
                csv: None,
                bibtex: None,
                citation: None,
                fiction_book: None,
                dbf: None,
                jats: None,
                epub: None,
                pst: None,
            },
            kreuzberg::FormatMetadata::Email(email) => Self {
                format_type_tag: "email".to_string(),
                code: None,
                pdf: None,
                docx: None,
                excel: None,
                email: Some(email.into()),
                pptx: None,
                archive: None,
                image: None,
                xml: None,
                text: None,
                html: None,
                ocr: None,
                csv: None,
                bibtex: None,
                citation: None,
                fiction_book: None,
                dbf: None,
                jats: None,
                epub: None,
                pst: None,
            },
            kreuzberg::FormatMetadata::Pptx(pptx) => Self {
                format_type_tag: "pptx".to_string(),
                code: None,
                pdf: None,
                docx: None,
                excel: None,
                email: None,
                pptx: Some(pptx.into()),
                archive: None,
                image: None,
                xml: None,
                text: None,
                html: None,
                ocr: None,
                csv: None,
                bibtex: None,
                citation: None,
                fiction_book: None,
                dbf: None,
                jats: None,
                epub: None,
                pst: None,
            },
            kreuzberg::FormatMetadata::Archive(archive) => Self {
                format_type_tag: "archive".to_string(),
                code: None,
                pdf: None,
                docx: None,
                excel: None,
                email: None,
                pptx: None,
                archive: Some(archive.into()),
                image: None,
                xml: None,
                text: None,
                html: None,
                ocr: None,
                csv: None,
                bibtex: None,
                citation: None,
                fiction_book: None,
                dbf: None,
                jats: None,
                epub: None,
                pst: None,
            },
            kreuzberg::FormatMetadata::Image(image) => Self {
                format_type_tag: "image".to_string(),
                code: None,
                pdf: None,
                docx: None,
                excel: None,
                email: None,
                pptx: None,
                archive: None,
                image: Some(image.into()),
                xml: None,
                text: None,
                html: None,
                ocr: None,
                csv: None,
                bibtex: None,
                citation: None,
                fiction_book: None,
                dbf: None,
                jats: None,
                epub: None,
                pst: None,
            },
            kreuzberg::FormatMetadata::Xml(xml) => Self {
                format_type_tag: "xml".to_string(),
                code: None,
                pdf: None,
                docx: None,
                excel: None,
                email: None,
                pptx: None,
                archive: None,
                image: None,
                xml: Some(xml.into()),
                text: None,
                html: None,
                ocr: None,
                csv: None,
                bibtex: None,
                citation: None,
                fiction_book: None,
                dbf: None,
                jats: None,
                epub: None,
                pst: None,
            },
            kreuzberg::FormatMetadata::Text(text) => Self {
                format_type_tag: "text".to_string(),
                code: None,
                pdf: None,
                docx: None,
                excel: None,
                email: None,
                pptx: None,
                archive: None,
                image: None,
                xml: None,
                text: Some(text.into()),
                html: None,
                ocr: None,
                csv: None,
                bibtex: None,
                citation: None,
                fiction_book: None,
                dbf: None,
                jats: None,
                epub: None,
                pst: None,
            },
            kreuzberg::FormatMetadata::Html(html) => Self {
                format_type_tag: "html".to_string(),
                code: None,
                pdf: None,
                docx: None,
                excel: None,
                email: None,
                pptx: None,
                archive: None,
                image: None,
                xml: None,
                text: None,
                html: Some((*html).into()),
                ocr: None,
                csv: None,
                bibtex: None,
                citation: None,
                fiction_book: None,
                dbf: None,
                jats: None,
                epub: None,
                pst: None,
            },
            kreuzberg::FormatMetadata::Ocr(ocr) => Self {
                format_type_tag: "ocr".to_string(),
                code: None,
                pdf: None,
                docx: None,
                excel: None,
                email: None,
                pptx: None,
                archive: None,
                image: None,
                xml: None,
                text: None,
                html: None,
                ocr: Some(ocr.into()),
                csv: None,
                bibtex: None,
                citation: None,
                fiction_book: None,
                dbf: None,
                jats: None,
                epub: None,
                pst: None,
            },
            kreuzberg::FormatMetadata::Csv(csv) => Self {
                format_type_tag: "csv".to_string(),
                code: None,
                pdf: None,
                docx: None,
                excel: None,
                email: None,
                pptx: None,
                archive: None,
                image: None,
                xml: None,
                text: None,
                html: None,
                ocr: None,
                csv: Some(csv.into()),
                bibtex: None,
                citation: None,
                fiction_book: None,
                dbf: None,
                jats: None,
                epub: None,
                pst: None,
            },
            kreuzberg::FormatMetadata::Bibtex(bibtex) => Self {
                format_type_tag: "bibtex".to_string(),
                code: None,
                pdf: None,
                docx: None,
                excel: None,
                email: None,
                pptx: None,
                archive: None,
                image: None,
                xml: None,
                text: None,
                html: None,
                ocr: None,
                csv: None,
                bibtex: Some(bibtex.into()),
                citation: None,
                fiction_book: None,
                dbf: None,
                jats: None,
                epub: None,
                pst: None,
            },
            kreuzberg::FormatMetadata::Citation(citation) => Self {
                format_type_tag: "citation".to_string(),
                code: None,
                pdf: None,
                docx: None,
                excel: None,
                email: None,
                pptx: None,
                archive: None,
                image: None,
                xml: None,
                text: None,
                html: None,
                ocr: None,
                csv: None,
                bibtex: None,
                citation: Some(citation.into()),
                fiction_book: None,
                dbf: None,
                jats: None,
                epub: None,
                pst: None,
            },
            kreuzberg::FormatMetadata::FictionBook(fiction_book) => Self {
                format_type_tag: "fictionbook".to_string(),
                code: None,
                pdf: None,
                docx: None,
                excel: None,
                email: None,
                pptx: None,
                archive: None,
                image: None,
                xml: None,
                text: None,
                html: None,
                ocr: None,
                csv: None,
                bibtex: None,
                citation: None,
                fiction_book: Some(fiction_book.into()),
                dbf: None,
                jats: None,
                epub: None,
                pst: None,
            },
            kreuzberg::FormatMetadata::Dbf(dbf) => Self {
                format_type_tag: "dbf".to_string(),
                code: None,
                pdf: None,
                docx: None,
                excel: None,
                email: None,
                pptx: None,
                archive: None,
                image: None,
                xml: None,
                text: None,
                html: None,
                ocr: None,
                csv: None,
                bibtex: None,
                citation: None,
                fiction_book: None,
                dbf: Some(dbf.into()),
                jats: None,
                epub: None,
                pst: None,
            },
            kreuzberg::FormatMetadata::Jats(jats) => Self {
                format_type_tag: "jats".to_string(),
                code: None,
                pdf: None,
                docx: None,
                excel: None,
                email: None,
                pptx: None,
                archive: None,
                image: None,
                xml: None,
                text: None,
                html: None,
                ocr: None,
                csv: None,
                bibtex: None,
                citation: None,
                fiction_book: None,
                dbf: None,
                jats: Some(jats.into()),
                epub: None,
                pst: None,
            },
            kreuzberg::FormatMetadata::Epub(epub) => Self {
                format_type_tag: "epub".to_string(),
                code: None,
                pdf: None,
                docx: None,
                excel: None,
                email: None,
                pptx: None,
                archive: None,
                image: None,
                xml: None,
                text: None,
                html: None,
                ocr: None,
                csv: None,
                bibtex: None,
                citation: None,
                fiction_book: None,
                dbf: None,
                jats: None,
                epub: Some(epub.into()),
                pst: None,
            },
            kreuzberg::FormatMetadata::Pst(pst) => Self {
                format_type_tag: "pst".to_string(),
                code: None,
                pdf: None,
                docx: None,
                excel: None,
                email: None,
                pptx: None,
                archive: None,
                image: None,
                xml: None,
                text: None,
                html: None,
                ocr: None,
                csv: None,
                bibtex: None,
                citation: None,
                fiction_book: None,
                dbf: None,
                jats: None,
                epub: None,
                pst: Some(pst.into()),
            },
            kreuzberg::FormatMetadata::Code(_code) => Self {
                format_type_tag: "code".to_string(),
                code: None,
                pdf: None,
                docx: None,
                excel: None,
                email: None,
                pptx: None,
                archive: None,
                image: None,
                xml: None,
                text: None,
                html: None,
                ocr: None,
                csv: None,
                bibtex: None,
                citation: None,
                fiction_book: None,
                dbf: None,
                jats: None,
                epub: None,
                pst: None,
            },
        }
    }
}

impl From<JsTextDirection> for kreuzberg::TextDirection {
    fn from(val: JsTextDirection) -> Self {
        match val {
            JsTextDirection::LeftToRight => Self::LeftToRight,
            JsTextDirection::RightToLeft => Self::RightToLeft,
            JsTextDirection::Auto => Self::Auto,
        }
    }
}

impl From<kreuzberg::TextDirection> for JsTextDirection {
    fn from(val: kreuzberg::TextDirection) -> Self {
        match val {
            kreuzberg::TextDirection::LeftToRight => Self::LeftToRight,
            kreuzberg::TextDirection::RightToLeft => Self::RightToLeft,
            kreuzberg::TextDirection::Auto => Self::Auto,
        }
    }
}

impl From<JsLinkType> for kreuzberg::LinkType {
    fn from(val: JsLinkType) -> Self {
        match val {
            JsLinkType::Anchor => Self::Anchor,
            JsLinkType::Internal => Self::Internal,
            JsLinkType::External => Self::External,
            JsLinkType::Email => Self::Email,
            JsLinkType::Phone => Self::Phone,
            JsLinkType::Other => Self::Other,
        }
    }
}

impl From<kreuzberg::LinkType> for JsLinkType {
    fn from(val: kreuzberg::LinkType) -> Self {
        match val {
            kreuzberg::LinkType::Anchor => Self::Anchor,
            kreuzberg::LinkType::Internal => Self::Internal,
            kreuzberg::LinkType::External => Self::External,
            kreuzberg::LinkType::Email => Self::Email,
            kreuzberg::LinkType::Phone => Self::Phone,
            kreuzberg::LinkType::Other => Self::Other,
        }
    }
}

impl From<JsImageType> for kreuzberg::ImageType {
    fn from(val: JsImageType) -> Self {
        match val {
            JsImageType::DataUri => Self::DataUri,
            JsImageType::InlineSvg => Self::InlineSvg,
            JsImageType::External => Self::External,
            JsImageType::Relative => Self::Relative,
        }
    }
}

impl From<kreuzberg::ImageType> for JsImageType {
    fn from(val: kreuzberg::ImageType) -> Self {
        match val {
            kreuzberg::ImageType::DataUri => Self::DataUri,
            kreuzberg::ImageType::InlineSvg => Self::InlineSvg,
            kreuzberg::ImageType::External => Self::External,
            kreuzberg::ImageType::Relative => Self::Relative,
        }
    }
}

impl From<JsStructuredDataType> for kreuzberg::StructuredDataType {
    fn from(val: JsStructuredDataType) -> Self {
        match val {
            JsStructuredDataType::JsonLd => Self::JsonLd,
            JsStructuredDataType::Microdata => Self::Microdata,
            JsStructuredDataType::RDFa => Self::RDFa,
        }
    }
}

impl From<kreuzberg::StructuredDataType> for JsStructuredDataType {
    fn from(val: kreuzberg::StructuredDataType) -> Self {
        match val {
            kreuzberg::StructuredDataType::JsonLd => Self::JsonLd,
            kreuzberg::StructuredDataType::Microdata => Self::Microdata,
            kreuzberg::StructuredDataType::RDFa => Self::RDFa,
        }
    }
}

impl From<JsOcrBoundingGeometry> for kreuzberg::OcrBoundingGeometry {
    fn from(val: JsOcrBoundingGeometry) -> Self {
        match val.type_tag.as_str() {
            "rectangle" => Self::Rectangle {
                left: val.left.unwrap_or_default(),
                top: val.top.unwrap_or_default(),
                width: val.width.unwrap_or_default(),
                height: val.height.unwrap_or_default(),
            },
            "quadrilateral" => Self::Quadrilateral {
                points: Default::default(),
            },
            _ => Self::Rectangle {
                left: Default::default(),
                top: Default::default(),
                width: Default::default(),
                height: Default::default(),
            },
        }
    }
}

impl From<kreuzberg::OcrBoundingGeometry> for JsOcrBoundingGeometry {
    fn from(val: kreuzberg::OcrBoundingGeometry) -> Self {
        match val {
            kreuzberg::OcrBoundingGeometry::Rectangle {
                left,
                top,
                width,
                height,
            } => Self {
                type_tag: "rectangle".to_string(),
                height: Some(height),
                left: Some(left),
                points: None,
                top: Some(top),
                width: Some(width),
            },
            kreuzberg::OcrBoundingGeometry::Quadrilateral { points: _points } => Self {
                type_tag: "quadrilateral".to_string(),
                height: None,
                left: None,
                points: None,
                top: None,
                width: None,
            },
        }
    }
}

impl From<JsOcrElementLevel> for kreuzberg::OcrElementLevel {
    fn from(val: JsOcrElementLevel) -> Self {
        match val {
            JsOcrElementLevel::Word => Self::Word,
            JsOcrElementLevel::Line => Self::Line,
            JsOcrElementLevel::Block => Self::Block,
            JsOcrElementLevel::Page => Self::Page,
        }
    }
}

impl From<kreuzberg::OcrElementLevel> for JsOcrElementLevel {
    fn from(val: kreuzberg::OcrElementLevel) -> Self {
        match val {
            kreuzberg::OcrElementLevel::Word => Self::Word,
            kreuzberg::OcrElementLevel::Line => Self::Line,
            kreuzberg::OcrElementLevel::Block => Self::Block,
            kreuzberg::OcrElementLevel::Page => Self::Page,
        }
    }
}

impl From<JsPageUnitType> for kreuzberg::PageUnitType {
    fn from(val: JsPageUnitType) -> Self {
        match val {
            JsPageUnitType::Page => Self::Page,
            JsPageUnitType::Slide => Self::Slide,
            JsPageUnitType::Sheet => Self::Sheet,
        }
    }
}

impl From<kreuzberg::PageUnitType> for JsPageUnitType {
    fn from(val: kreuzberg::PageUnitType) -> Self {
        match val {
            kreuzberg::PageUnitType::Page => Self::Page,
            kreuzberg::PageUnitType::Slide => Self::Slide,
            kreuzberg::PageUnitType::Sheet => Self::Sheet,
        }
    }
}

impl From<JsDiffLine> for kreuzberg::DiffLine {
    fn from(val: JsDiffLine) -> Self {
        match val.kind_tag.as_str() {
            "context" => Self::Context(val.context.unwrap_or_default()),
            "added" => Self::Added(val.added.unwrap_or_default()),
            "removed" => Self::Removed(val.removed.unwrap_or_default()),
            _ => Self::Context(Default::default()),
        }
    }
}

impl From<kreuzberg::DiffLine> for JsDiffLine {
    fn from(val: kreuzberg::DiffLine) -> Self {
        match val {
            kreuzberg::DiffLine::Context(context) => Self {
                kind_tag: "context".to_string(),
                added: None,
                context: Some(context),
                removed: None,
            },
            kreuzberg::DiffLine::Added(added) => Self {
                kind_tag: "added".to_string(),
                added: Some(added),
                context: None,
                removed: None,
            },
            kreuzberg::DiffLine::Removed(removed) => Self {
                kind_tag: "removed".to_string(),
                added: None,
                context: None,
                removed: Some(removed),
            },
        }
    }
}

impl From<JsRevisionKind> for kreuzberg::RevisionKind {
    fn from(val: JsRevisionKind) -> Self {
        match val {
            JsRevisionKind::Insertion => Self::Insertion,
            JsRevisionKind::Deletion => Self::Deletion,
            JsRevisionKind::FormatChange => Self::FormatChange,
            JsRevisionKind::Comment => Self::Comment,
        }
    }
}

impl From<kreuzberg::RevisionKind> for JsRevisionKind {
    fn from(val: kreuzberg::RevisionKind) -> Self {
        match val {
            kreuzberg::RevisionKind::Insertion => Self::Insertion,
            kreuzberg::RevisionKind::Deletion => Self::Deletion,
            kreuzberg::RevisionKind::FormatChange => Self::FormatChange,
            kreuzberg::RevisionKind::Comment => Self::Comment,
        }
    }
}

impl From<JsRevisionAnchor> for kreuzberg::RevisionAnchor {
    fn from(val: JsRevisionAnchor) -> Self {
        match val.type_tag.as_str() {
            "paragraph" => Self::Paragraph {
                index: val.index.map(|v| v as usize).unwrap_or_default(),
            },
            "tablecell" => Self::TableCell {
                row: val.row.map(|v| v as usize).unwrap_or_default(),
                col: val.col.map(|v| v as usize).unwrap_or_default(),
                table_index: val.table_index.map(|v| v as usize).unwrap_or_default(),
            },
            "page" => Self::Page {
                index: val.index.map(|v| v as usize).unwrap_or_default(),
            },
            "slide" => Self::Slide {
                index: val.index.map(|v| v as usize).unwrap_or_default(),
            },
            "sheet" => Self::Sheet {
                index: val.index.map(|v| v as usize).unwrap_or_default(),
                name: val.name,
            },
            _ => Self::Paragraph {
                index: Default::default(),
            },
        }
    }
}

impl From<kreuzberg::RevisionAnchor> for JsRevisionAnchor {
    fn from(val: kreuzberg::RevisionAnchor) -> Self {
        match val {
            kreuzberg::RevisionAnchor::Paragraph { index } => Self {
                type_tag: "paragraph".to_string(),
                col: None,
                index: Some(index as i64),
                name: None,
                row: None,
                table_index: None,
            },
            kreuzberg::RevisionAnchor::TableCell { row, col, table_index } => Self {
                type_tag: "tablecell".to_string(),
                col: Some(col as i64),
                index: None,
                name: None,
                row: Some(row as i64),
                table_index: Some(table_index as i64),
            },
            kreuzberg::RevisionAnchor::Page { index } => Self {
                type_tag: "page".to_string(),
                col: None,
                index: Some(index as i64),
                name: None,
                row: None,
                table_index: None,
            },
            kreuzberg::RevisionAnchor::Slide { index } => Self {
                type_tag: "slide".to_string(),
                col: None,
                index: Some(index as i64),
                name: None,
                row: None,
                table_index: None,
            },
            kreuzberg::RevisionAnchor::Sheet { index, name } => Self {
                type_tag: "sheet".to_string(),
                col: None,
                index: Some(index as i64),
                name,
                row: None,
                table_index: None,
            },
        }
    }
}

impl From<JsUriKind> for kreuzberg::UriKind {
    fn from(val: JsUriKind) -> Self {
        match val {
            JsUriKind::Hyperlink => Self::Hyperlink,
            JsUriKind::Image => Self::Image,
            JsUriKind::Anchor => Self::Anchor,
            JsUriKind::Citation => Self::Citation,
            JsUriKind::Reference => Self::Reference,
            JsUriKind::Email => Self::Email,
        }
    }
}

impl From<kreuzberg::UriKind> for JsUriKind {
    fn from(val: kreuzberg::UriKind) -> Self {
        match val {
            kreuzberg::UriKind::Hyperlink => Self::Hyperlink,
            kreuzberg::UriKind::Image => Self::Image,
            kreuzberg::UriKind::Anchor => Self::Anchor,
            kreuzberg::UriKind::Citation => Self::Citation,
            kreuzberg::UriKind::Reference => Self::Reference,
            kreuzberg::UriKind::Email => Self::Email,
        }
    }
}

impl From<JsKeywordAlgorithm> for kreuzberg::KeywordAlgorithm {
    fn from(val: JsKeywordAlgorithm) -> Self {
        match val {
            JsKeywordAlgorithm::Yake => Self::Yake,
            JsKeywordAlgorithm::Rake => Self::Rake,
        }
    }
}

impl From<kreuzberg::KeywordAlgorithm> for JsKeywordAlgorithm {
    fn from(val: kreuzberg::KeywordAlgorithm) -> Self {
        match val {
            kreuzberg::KeywordAlgorithm::Yake => Self::Yake,
            kreuzberg::KeywordAlgorithm::Rake => Self::Rake,
        }
    }
}

impl From<kreuzberg::PSMMode> for JsPSMMode {
    fn from(val: kreuzberg::PSMMode) -> Self {
        match val {
            kreuzberg::PSMMode::OsdOnly => Self::OsdOnly,
            kreuzberg::PSMMode::AutoOsd => Self::AutoOsd,
            kreuzberg::PSMMode::AutoOnly => Self::AutoOnly,
            kreuzberg::PSMMode::Auto => Self::Auto,
            kreuzberg::PSMMode::SingleColumn => Self::SingleColumn,
            kreuzberg::PSMMode::SingleBlockVertical => Self::SingleBlockVertical,
            kreuzberg::PSMMode::SingleBlock => Self::SingleBlock,
            kreuzberg::PSMMode::SingleLine => Self::SingleLine,
            kreuzberg::PSMMode::SingleWord => Self::SingleWord,
            kreuzberg::PSMMode::CircleWord => Self::CircleWord,
            kreuzberg::PSMMode::SingleChar => Self::SingleChar,
        }
    }
}

impl From<kreuzberg::PaddleLanguage> for JsPaddleLanguage {
    fn from(val: kreuzberg::PaddleLanguage) -> Self {
        match val {
            kreuzberg::PaddleLanguage::English => Self::English,
            kreuzberg::PaddleLanguage::Chinese => Self::Chinese,
            kreuzberg::PaddleLanguage::Japanese => Self::Japanese,
            kreuzberg::PaddleLanguage::Korean => Self::Korean,
            kreuzberg::PaddleLanguage::German => Self::German,
            kreuzberg::PaddleLanguage::French => Self::French,
            kreuzberg::PaddleLanguage::Latin => Self::Latin,
            kreuzberg::PaddleLanguage::Cyrillic => Self::Cyrillic,
            kreuzberg::PaddleLanguage::TraditionalChinese => Self::TraditionalChinese,
            kreuzberg::PaddleLanguage::Thai => Self::Thai,
            kreuzberg::PaddleLanguage::Greek => Self::Greek,
            kreuzberg::PaddleLanguage::EastSlavic => Self::EastSlavic,
            kreuzberg::PaddleLanguage::Arabic => Self::Arabic,
            kreuzberg::PaddleLanguage::Devanagari => Self::Devanagari,
            kreuzberg::PaddleLanguage::Tamil => Self::Tamil,
            kreuzberg::PaddleLanguage::Telugu => Self::Telugu,
        }
    }
}

impl From<kreuzberg::LayoutClass> for JsLayoutClass {
    fn from(val: kreuzberg::LayoutClass) -> Self {
        match val {
            kreuzberg::LayoutClass::Caption => Self::Caption,
            kreuzberg::LayoutClass::Footnote => Self::Footnote,
            kreuzberg::LayoutClass::Formula => Self::Formula,
            kreuzberg::LayoutClass::ListItem => Self::ListItem,
            kreuzberg::LayoutClass::PageFooter => Self::PageFooter,
            kreuzberg::LayoutClass::PageHeader => Self::PageHeader,
            kreuzberg::LayoutClass::Picture => Self::Picture,
            kreuzberg::LayoutClass::SectionHeader => Self::SectionHeader,
            kreuzberg::LayoutClass::Table => Self::Table,
            kreuzberg::LayoutClass::Text => Self::Text,
            kreuzberg::LayoutClass::Title => Self::Title,
            kreuzberg::LayoutClass::DocumentIndex => Self::DocumentIndex,
            kreuzberg::LayoutClass::Code => Self::Code,
            kreuzberg::LayoutClass::CheckboxSelected => Self::CheckboxSelected,
            kreuzberg::LayoutClass::CheckboxUnselected => Self::CheckboxUnselected,
            kreuzberg::LayoutClass::Form => Self::Form,
            kreuzberg::LayoutClass::KeyValueRegion => Self::KeyValueRegion,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsCacheStats> for kreuzberg::CacheStats {
    fn from(val: JsCacheStats) -> Self {
        Self {
            total_files: val.total_files as usize,
            total_size_mb: val.total_size_mb,
            available_space_mb: val.available_space_mb,
            oldest_file_age_days: val.oldest_file_age_days,
            newest_file_age_days: val.newest_file_age_days,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsSupportedFormat> for kreuzberg::SupportedFormat {
    fn from(val: JsSupportedFormat) -> Self {
        Self {
            extension: val.extension,
            mime_type: val.mime_type,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsStructuredDataResult> for kreuzberg::extraction::structured::StructuredDataResult {
    fn from(val: JsStructuredDataResult) -> Self {
        Self {
            content: val.content,
            format: val.format.into(),
            metadata: val.metadata.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
            text_fields: val.text_fields.into_iter().collect(),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsXlsxAppProperties> for kreuzberg::extraction::office_metadata::app_properties::XlsxAppProperties {
    fn from(val: JsXlsxAppProperties) -> Self {
        let mut __result = kreuzberg::extraction::office_metadata::app_properties::XlsxAppProperties::default();
        __result.application = val.application;
        __result.app_version = val.app_version;
        __result.doc_security = val.doc_security;
        __result.scale_crop = val.scale_crop;
        __result.links_up_to_date = val.links_up_to_date;
        __result.shared_doc = val.shared_doc;
        __result.hyperlinks_changed = val.hyperlinks_changed;
        __result.company = val.company;
        if let Some(__v) = val.worksheet_names {
            __result.worksheet_names = __v.into_iter().collect();
        }
        __result
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsPptxAppProperties> for kreuzberg::extraction::office_metadata::app_properties::PptxAppProperties {
    fn from(val: JsPptxAppProperties) -> Self {
        let mut __result = kreuzberg::extraction::office_metadata::app_properties::PptxAppProperties::default();
        __result.application = val.application;
        __result.app_version = val.app_version;
        __result.total_time = val.total_time;
        __result.company = val.company;
        __result.doc_security = val.doc_security;
        __result.scale_crop = val.scale_crop;
        __result.links_up_to_date = val.links_up_to_date;
        __result.shared_doc = val.shared_doc;
        __result.hyperlinks_changed = val.hyperlinks_changed;
        __result.slides = val.slides;
        __result.notes = val.notes;
        __result.hidden_slides = val.hidden_slides;
        __result.multimedia_clips = val.multimedia_clips;
        __result.presentation_format = val.presentation_format;
        if let Some(__v) = val.slide_titles {
            __result.slide_titles = __v.into_iter().collect();
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsExcelWorkbook> for kreuzberg::ExcelWorkbook {
    fn from(val: JsExcelWorkbook) -> Self {
        Self {
            sheets: val.sheets.into_iter().map(Into::into).collect(),
            metadata: val.metadata.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
            revisions: val.revisions.map(|v| v.into_iter().map(Into::into).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsExcelSheet> for kreuzberg::ExcelSheet {
    fn from(val: JsExcelSheet) -> Self {
        Self {
            name: val.name,
            markdown: val.markdown,
            row_count: val.row_count as usize,
            col_count: val.col_count as usize,
            cell_count: val.cell_count as usize,
            table_cells: val.table_cells.map(|v| v.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsXmlExtractionResult> for kreuzberg::XmlExtractionResult {
    fn from(val: JsXmlExtractionResult) -> Self {
        Self {
            content: val.content,
            element_count: val.element_count as usize,
            unique_elements: val.unique_elements.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsTextExtractionResult> for kreuzberg::TextExtractionResult {
    fn from(val: JsTextExtractionResult) -> Self {
        Self {
            content: val.content,
            line_count: val.line_count as usize,
            word_count: val.word_count as usize,
            character_count: val.character_count as usize,
            headers: val.headers.map(|v| v.into_iter().collect()),
            links: Default::default(),
            code_blocks: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsPptxExtractionResult> for kreuzberg::PptxExtractionResult {
    fn from(val: JsPptxExtractionResult) -> Self {
        Self {
            content: val.content,
            metadata: val.metadata.into(),
            slide_count: val.slide_count as usize,
            image_count: val.image_count as usize,
            table_count: val.table_count as usize,
            images: val.images.into_iter().map(Into::into).collect(),
            page_structure: val.page_structure.map(Into::into),
            page_contents: val.page_contents.map(|v| v.into_iter().map(Into::into).collect()),
            document: val.document.map(Into::into),
            hyperlinks: Default::default(),
            office_metadata: val
                .office_metadata
                .into_iter()
                .map(|(k, v)| (k.into(), v.into()))
                .collect(),
            revisions: val.revisions.map(|v| v.into_iter().map(Into::into).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsEmailExtractionResult> for kreuzberg::EmailExtractionResult {
    fn from(val: JsEmailExtractionResult) -> Self {
        Self {
            subject: val.subject,
            from_email: val.from_email,
            to_emails: val.to_emails.into_iter().collect(),
            cc_emails: val.cc_emails.into_iter().collect(),
            bcc_emails: val.bcc_emails.into_iter().collect(),
            date: val.date,
            message_id: val.message_id,
            plain_text: val.plain_text,
            html_content: val.html_content,
            content: val.content,
            attachments: val.attachments.into_iter().map(Into::into).collect(),
            metadata: val.metadata.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsEmailAttachment> for kreuzberg::EmailAttachment {
    fn from(val: JsEmailAttachment) -> Self {
        Self {
            name: val.name,
            filename: val.filename,
            mime_type: val.mime_type,
            size: val.size.map(|v| v as usize),
            is_image: val.is_image,
            data: val.data.map(|v| v.to_vec().into()),
        }
    }
}

#[allow(clippy::field_reassign_with_default, clippy::let_and_return)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsTableCell> for kreuzberg::TableCell {
    fn from(val: JsTableCell) -> Self {
        let mut __result = kreuzberg::TableCell::default();
        if let Some(__v) = val.content {
            __result.content = __v;
        }
        if let Some(__v) = val.row_span {
            __result.row_span = __v;
        }
        if let Some(__v) = val.col_span {
            __result.col_span = __v;
        }
        if let Some(__v) = val.is_header {
            __result.is_header = __v;
        }
        __result
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsDetectResponse> for kreuzberg::api::DetectResponse {
    fn from(val: JsDetectResponse) -> Self {
        Self {
            mime_type: val.mime_type,
            filename: val.filename,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsModelPaths> for kreuzberg::ModelPaths {
    fn from(val: JsModelPaths) -> Self {
        Self {
            det_model: val.det_model.into(),
            cls_model: val.cls_model.into(),
            rec_model: val.rec_model.into(),
            dict_file: val.dict_file.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsOrientationResult> for kreuzberg::OrientationResult {
    fn from(val: JsOrientationResult) -> Self {
        Self {
            degrees: val.degrees,
            confidence: val.confidence as f32,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsBBox> for kreuzberg::BBox {
    fn from(val: JsBBox) -> Self {
        Self {
            x1: val.x1 as f32,
            y1: val.y1 as f32,
            x2: val.x2 as f32,
            y2: val.y2 as f32,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsLayoutDetection> for kreuzberg::LayoutDetection {
    fn from(val: JsLayoutDetection) -> Self {
        Self {
            class_name: val.class_name.into(),
            confidence: val.confidence as f32,
            bbox: val.bbox.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsRecognizedTable> for kreuzberg::RecognizedTable {
    fn from(val: JsRecognizedTable) -> Self {
        Self {
            detection_bbox: val.detection_bbox.into(),
            cells: val.cells.into_iter().collect(),
            markdown: val.markdown,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsDetectionResult> for kreuzberg::DetectionResult {
    fn from(val: JsDetectionResult) -> Self {
        Self {
            page_width: val.page_width,
            page_height: val.page_height,
            detections: val.detections.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JsEmbeddedFile> for kreuzberg::pdf::embedded_files::EmbeddedFile {
    fn from(val: JsEmbeddedFile) -> Self {
        Self {
            name: val.name,
            data: val.data.to_vec().into(),
            compressed_size: val.compressed_size as usize,
            mime_type: val.mime_type,
        }
    }
}

impl From<JsLayoutClass> for kreuzberg::LayoutClass {
    fn from(val: JsLayoutClass) -> Self {
        match val {
            JsLayoutClass::Caption => Self::Caption,
            JsLayoutClass::Footnote => Self::Footnote,
            JsLayoutClass::Formula => Self::Formula,
            JsLayoutClass::ListItem => Self::ListItem,
            JsLayoutClass::PageFooter => Self::PageFooter,
            JsLayoutClass::PageHeader => Self::PageHeader,
            JsLayoutClass::Picture => Self::Picture,
            JsLayoutClass::SectionHeader => Self::SectionHeader,
            JsLayoutClass::Table => Self::Table,
            JsLayoutClass::Text => Self::Text,
            JsLayoutClass::Title => Self::Title,
            JsLayoutClass::DocumentIndex => Self::DocumentIndex,
            JsLayoutClass::Code => Self::Code,
            JsLayoutClass::CheckboxSelected => Self::CheckboxSelected,
            JsLayoutClass::CheckboxUnselected => Self::CheckboxUnselected,
            JsLayoutClass::Form => Self::Form,
            JsLayoutClass::KeyValueRegion => Self::KeyValueRegion,
        }
    }
}

// Error variant name constants
pub const KREUZBERG_ERROR_ERROR_IO: &str = "Io";
pub const KREUZBERG_ERROR_ERROR_PARSING: &str = "Parsing";
pub const KREUZBERG_ERROR_ERROR_OCR: &str = "Ocr";
pub const KREUZBERG_ERROR_ERROR_VALIDATION: &str = "Validation";
pub const KREUZBERG_ERROR_ERROR_CACHE: &str = "Cache";
pub const KREUZBERG_ERROR_ERROR_IMAGE_PROCESSING: &str = "ImageProcessing";
pub const KREUZBERG_ERROR_ERROR_SERIALIZATION: &str = "Serialization";
pub const KREUZBERG_ERROR_ERROR_MISSING_DEPENDENCY: &str = "MissingDependency";
pub const KREUZBERG_ERROR_ERROR_PLUGIN: &str = "Plugin";
pub const KREUZBERG_ERROR_ERROR_LOCK_POISONED: &str = "LockPoisoned";
pub const KREUZBERG_ERROR_ERROR_UNSUPPORTED_FORMAT: &str = "UnsupportedFormat";
pub const KREUZBERG_ERROR_ERROR_EMBEDDING: &str = "Embedding";
pub const KREUZBERG_ERROR_ERROR_TIMEOUT: &str = "Timeout";
pub const KREUZBERG_ERROR_ERROR_CANCELLED: &str = "Cancelled";
pub const KREUZBERG_ERROR_ERROR_SECURITY: &str = "Security";
pub const KREUZBERG_ERROR_ERROR_OTHER: &str = "Other";

/// Convert a `kreuzberg::error::KreuzbergError` error to a NAPI error.
#[allow(dead_code)]
fn kreuzberg_error_to_napi_err(e: kreuzberg::error::KreuzbergError) -> napi::Error {
    let msg = e.to_string();
    #[allow(unreachable_patterns)]
    match &e {
        kreuzberg::error::KreuzbergError::Io(..) => {
            napi::Error::new(napi::Status::GenericFailure, format!("[Io] {}", msg))
        }
        kreuzberg::error::KreuzbergError::Parsing { .. } => {
            napi::Error::new(napi::Status::GenericFailure, format!("[Parsing] {}", msg))
        }
        kreuzberg::error::KreuzbergError::Ocr { .. } => {
            napi::Error::new(napi::Status::GenericFailure, format!("[Ocr] {}", msg))
        }
        kreuzberg::error::KreuzbergError::Validation { .. } => {
            napi::Error::new(napi::Status::GenericFailure, format!("[Validation] {}", msg))
        }
        kreuzberg::error::KreuzbergError::Cache { .. } => {
            napi::Error::new(napi::Status::GenericFailure, format!("[Cache] {}", msg))
        }
        kreuzberg::error::KreuzbergError::ImageProcessing { .. } => {
            napi::Error::new(napi::Status::GenericFailure, format!("[ImageProcessing] {}", msg))
        }
        kreuzberg::error::KreuzbergError::Serialization { .. } => {
            napi::Error::new(napi::Status::GenericFailure, format!("[Serialization] {}", msg))
        }
        kreuzberg::error::KreuzbergError::MissingDependency(..) => {
            napi::Error::new(napi::Status::GenericFailure, format!("[MissingDependency] {}", msg))
        }
        kreuzberg::error::KreuzbergError::Plugin { .. } => {
            napi::Error::new(napi::Status::GenericFailure, format!("[Plugin] {}", msg))
        }
        kreuzberg::error::KreuzbergError::LockPoisoned(..) => {
            napi::Error::new(napi::Status::GenericFailure, format!("[LockPoisoned] {}", msg))
        }
        kreuzberg::error::KreuzbergError::UnsupportedFormat(..) => {
            napi::Error::new(napi::Status::GenericFailure, format!("[UnsupportedFormat] {}", msg))
        }
        kreuzberg::error::KreuzbergError::Embedding { .. } => {
            napi::Error::new(napi::Status::GenericFailure, format!("[Embedding] {}", msg))
        }
        kreuzberg::error::KreuzbergError::Timeout { .. } => {
            napi::Error::new(napi::Status::GenericFailure, format!("[Timeout] {}", msg))
        }
        kreuzberg::error::KreuzbergError::Cancelled => {
            napi::Error::new(napi::Status::GenericFailure, format!("[Cancelled] {}", msg))
        }
        kreuzberg::error::KreuzbergError::Security { .. } => {
            napi::Error::new(napi::Status::GenericFailure, format!("[Security] {}", msg))
        }
        kreuzberg::error::KreuzbergError::Other(..) => {
            napi::Error::new(napi::Status::GenericFailure, format!("[Other] {}", msg))
        }
        _ => napi::Error::new(napi::Status::GenericFailure, msg),
    }
}
