xberg 1.1.4

High-performance document intelligence library for Rust. Extract text, metadata, and structured data from PDFs, Office documents, images, and 107 formats and 371 programming languages via tree-sitter code intelligence with async/sync APIs.
Documentation
//! Issue #961: numbered chapter headings in an untagged PDF must be classified as
//! Heading/Title elements, not ListItem, when extracting with `ElementBased` result
//! format.

#![allow(clippy::print_stdout, clippy::print_stderr, clippy::dbg_macro)] // ~keep: test/bench binaries print by design; org logging policy exempts tests
mod helpers;
use helpers::extract_bytes_document_blocking;

use std::path::Path;
use xberg::types::{ElementType, ResultFormat};
use xberg::{ExtractionConfig, OutputFormat};

/// Verifies that numbered chapter headings in an untagged ReportLab PDF are
/// classified as Heading/Title, not ListItem (#961).
#[test]
fn numbered_chapters_in_untagged_pdf_become_headings() {
    let path = Path::new("test_documents/pdf/multipage_marketing.pdf");
    if !path.exists() {
        eprintln!("skipping: test_documents/pdf/multipage_marketing.pdf not found");
        return;
    }

    let bytes = std::fs::read(path).expect("failed to read PDF");
    let config = ExtractionConfig {
        output_format: OutputFormat::Plain,
        result_format: ResultFormat::ElementBased,
        ..Default::default()
    };

    let result = extract_bytes_document_blocking(&bytes, "application/pdf", &config).expect("extraction failed");
    let elements = result.elements.unwrap_or_default();

    let chapter_list_items: Vec<_> = elements
        .iter()
        .filter(|e| {
            e.element_type == ElementType::ListItem && e.text.chars().next().is_some_and(|c| c.is_ascii_digit())
        })
        .collect();

    let numbered_headings: Vec<_> = elements
        .iter()
        .filter(|e| {
            matches!(e.element_type, ElementType::Heading | ElementType::Title)
                && e.text.chars().next().is_some_and(|c| c.is_ascii_digit())
        })
        .collect();

    assert!(
        chapter_list_items.is_empty(),
        "numbered chapter headings must not be ListItem; got: {:?}",
        chapter_list_items.iter().map(|e| &e.text).collect::<Vec<_>>()
    );
    assert!(
        !numbered_headings.is_empty(),
        "at least one numbered chapter heading must be promoted to Heading/Title"
    );
}