extractPagesMarkdown: return classification metadata (0.7.0) (#32)
Publish npm package / Build aarch64-apple-darwin (push) Has been cancelled
Publish npm package / Build x86_64-unknown-linux-gnu (push) Has been cancelled
Publish npm package / Publish to npm (push) Has been cancelled

Combine per-page markdown extraction with layout classification into a
single parse. extractPagesMarkdown now returns PagesExtractionResult with
pages_with_tables, pages_with_columns, pages_needing_ocr, and is_complex
alongside the per-page markdown — eliminating redundant PDF parses for
callers that need both.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Abimael Martell
2026-04-14 13:08:07 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent abb0b925fb
commit 20f24d1f8d
4 changed files with 153 additions and 48 deletions
+74 -27
View File
@@ -1445,55 +1445,56 @@ fn test_extract_tables_in_regions_nonexistent_page() {
#[test]
fn test_extract_pages_markdown_basic() {
let buf = std::fs::read("tests/fixtures/nexo-price-en.pdf").unwrap();
let results = extract_pages_markdown_mem(&buf, &[0, 1]).unwrap();
let result = extract_pages_markdown_mem(&buf, &[0, 1]).unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0].page, 0);
assert_eq!(results[1].page, 1);
assert_eq!(result.pages.len(), 2);
assert_eq!(result.pages[0].page, 0);
assert_eq!(result.pages[1].page, 1);
// Text-based PDF should produce non-empty markdown
assert!(!results[0].markdown.is_empty());
assert!(!results[0].needs_ocr);
assert!(!result.pages[0].markdown.is_empty());
assert!(!result.pages[0].needs_ocr);
}
#[test]
fn test_extract_pages_markdown_page_ordering() {
let buf = std::fs::read("tests/fixtures/nexo-price-en.pdf").unwrap();
// Request pages in non-sequential order
let results = extract_pages_markdown_mem(&buf, &[1, 0]).unwrap();
let result = extract_pages_markdown_mem(&buf, &[1, 0]).unwrap();
assert_eq!(results.len(), 2);
assert_eq!(result.pages.len(), 2);
// Results should match input order, not document order
assert_eq!(results[0].page, 1);
assert_eq!(results[1].page, 0);
assert_eq!(result.pages[0].page, 1);
assert_eq!(result.pages[1].page, 0);
}
#[test]
fn test_extract_pages_markdown_out_of_range() {
let buf = std::fs::read("tests/fixtures/nexo-price-en.pdf").unwrap();
let results = extract_pages_markdown_mem(&buf, &[9999]).unwrap();
let result = extract_pages_markdown_mem(&buf, &[9999]).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].page, 9999);
assert!(results[0].markdown.is_empty());
assert!(results[0].needs_ocr);
assert_eq!(result.pages.len(), 1);
assert_eq!(result.pages[0].page, 9999);
assert!(result.pages[0].markdown.is_empty());
assert!(result.pages[0].needs_ocr);
assert!(result.pages_needing_ocr.contains(&10000)); // 1-indexed
}
#[test]
fn test_extract_pages_markdown_empty_pages_list() {
let buf = std::fs::read("tests/fixtures/nexo-price-en.pdf").unwrap();
let results = extract_pages_markdown_mem(&buf, &[]).unwrap();
assert!(results.is_empty());
let result = extract_pages_markdown_mem(&buf, &[]).unwrap();
assert!(result.pages.is_empty());
}
#[test]
fn test_extract_pages_markdown_single_page() {
let buf = std::fs::read("tests/fixtures/nexo-price-en.pdf").unwrap();
let results = extract_pages_markdown_mem(&buf, &[0]).unwrap();
let result = extract_pages_markdown_mem(&buf, &[0]).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].page, 0);
assert!(!results[0].markdown.is_empty());
assert!(!results[0].needs_ocr);
assert_eq!(result.pages.len(), 1);
assert_eq!(result.pages[0].page, 0);
assert!(!result.pages[0].markdown.is_empty());
assert!(!result.pages[0].needs_ocr);
}
#[test]
@@ -1506,10 +1507,55 @@ fn test_extract_pages_markdown_invalid_buffer() {
fn test_extract_pages_markdown_gid_pages_need_ocr() {
// shinagawa_identity_h.pdf has GID-encoded fonts
let buf = std::fs::read("tests/fixtures/shinagawa_identity_h.pdf").unwrap();
let results = extract_pages_markdown_mem(&buf, &[0]).unwrap();
let result = extract_pages_markdown_mem(&buf, &[0]).unwrap();
assert_eq!(results.len(), 1);
assert!(results[0].needs_ocr);
assert_eq!(result.pages.len(), 1);
assert!(result.pages[0].needs_ocr);
assert!(result.pages_needing_ocr.contains(&1)); // 1-indexed
}
#[test]
fn test_extract_pages_markdown_classification_with_tables() {
// nexo-price-en.pdf is known to have tables
let buf = std::fs::read("tests/fixtures/nexo-price-en.pdf").unwrap();
let page_count = process_pdf_mem(&buf).unwrap().page_count;
let page_indices: Vec<u32> = (0..page_count).collect();
let result = extract_pages_markdown_mem(&buf, &page_indices).unwrap();
assert!(
!result.pages_with_tables.is_empty(),
"nexo-price-en.pdf should have pages with tables"
);
assert!(result.is_complex);
}
#[test]
fn test_extract_pages_markdown_simple_pdf_no_complexity() {
// bare_name_struct.pdf is a simple document with a heading and code block
let buf = std::fs::read("tests/fixtures/bare_name_struct.pdf").unwrap();
let result = extract_pages_markdown_mem(&buf, &[0]).unwrap();
assert!(result.pages_with_tables.is_empty());
assert!(result.pages_with_columns.is_empty());
assert!(!result.is_complex);
}
#[test]
fn test_extract_pages_markdown_classification_matches_process_pdf() {
let buf = std::fs::read("tests/fixtures/nexo-price-en.pdf").unwrap();
let full = process_pdf_mem(&buf).unwrap();
let page_count = full.page_count;
let page_indices: Vec<u32> = (0..page_count).collect();
let result = extract_pages_markdown_mem(&buf, &page_indices).unwrap();
assert_eq!(
result.pages_with_tables, full.layout.pages_with_tables,
"pages_with_tables should match process_pdf"
);
assert_eq!(
result.pages_with_columns, full.layout.pages_with_columns,
"pages_with_columns should match process_pdf"
);
}
#[test]
@@ -1523,12 +1569,13 @@ fn test_extract_pages_markdown_consistency_with_process_pdf() {
// Get per-page output for all pages
let page_count = full.page_count;
let page_indices: Vec<u32> = (0..page_count).collect();
let per_page = extract_pages_markdown_mem(&buf, &page_indices).unwrap();
let result = extract_pages_markdown_mem(&buf, &page_indices).unwrap();
// Concatenated per-page markdown should contain substantial overlap with
// the full output (exact match not expected due to header/footer stripping
// and cross-page paragraph merging differences)
let concat: String = per_page
let concat: String = result
.pages
.iter()
.map(|p| p.markdown.as_str())
.collect::<Vec<_>>()