use crate::config::{ConfigManager, ParsingConfig};
use crate::types::*;
use anyhow::Result;
use regex::Regex;
use super::paragraph_clustering::ParagraphClusteringRule;
use super::section_detection::SectionAndHierarchyDetectionRule;
use super::section_detection_v2::SectionDetectionV2Rule;
use super::spatial_clustering::SpatialClusteringRule;
use super::validation::ValidationRule;
#[derive(Debug, Clone)]
pub struct DebugConfig {
pub enabled: bool,
pub filter_patterns: Vec<String>,
}
impl DebugConfig {
pub fn new(enabled: bool, filter_patterns: Vec<String>) -> Self {
Self {
enabled,
filter_patterns,
}
}
pub fn disabled() -> Self {
Self {
enabled: false,
filter_patterns: Vec::new(),
}
}
}
pub fn debug_pipeline_elements(
rule_name: &str,
elements: &[ParsedPdfElement],
debug_config: &DebugConfig,
) {
if !debug_config.enabled || debug_config.filter_patterns.is_empty() {
return;
}
let matching_elements: Vec<_> = elements
.iter()
.enumerate()
.filter(|(_, element)| {
debug_config.filter_patterns.iter().any(|pattern| {
if let Ok(regex) = Regex::new(pattern) {
regex.is_match(&element.text)
} else {
element.text.contains(pattern)
}
})
})
.collect();
if !matching_elements.is_empty() {
println!(
"🔍 [{}] {} matching elements:",
rule_name,
matching_elements.len()
);
for (index, element) in matching_elements {
let text_preview = if element.text.len() > 50 {
format!("{}...", &element.text[..47])
} else {
element.text.clone()
};
println!(
" Element {}: \"{}\" ({:?}, depth: {}, text_order: {})",
index,
text_preview,
element.element_type,
element.hierarchy_level,
element.position
);
}
println!();
}
}
pub struct RuleEngine {
config_manager: ConfigManager,
debug_config: DebugConfig,
minimal_parse_override: Option<bool>,
pub rule_timings: std::cell::RefCell<Vec<(String, std::time::Duration)>>,
}
impl RuleEngine {
pub fn new() -> Result<Self> {
let config_manager = ConfigManager::new()?;
Ok(Self {
config_manager,
debug_config: DebugConfig::disabled(),
minimal_parse_override: None,
rule_timings: std::cell::RefCell::new(Vec::new()),
})
}
pub fn set_debug_config(&mut self, debug_config: DebugConfig) {
self.debug_config = debug_config;
}
pub fn set_minimal_parse_override(&mut self, minimal_parse: bool) {
self.minimal_parse_override = Some(minimal_parse);
}
pub fn load_custom_config(&mut self, config_path: &str) -> Result<()> {
println!("📁 Loading custom config from: {config_path}");
self.config_manager.load_config_from_file(config_path)?;
println!("✅ Custom config loaded successfully");
Ok(())
}
pub fn get_config_for_cache(
&self,
doc_type: &crate::types::DocumentType,
) -> &crate::config::ParsingConfig {
self.config_manager.get_config(doc_type)
}
pub fn apply_rules(
&self,
text_elements: &[PdfTextElement],
classification: &ClassificationResult,
document_analysis: &DocumentAnalysis,
font_size_analysis: &FontSizeAnalysis,
style_data: &StyleData,
) -> Result<Vec<ParsedPdfElement>> {
println!(
"⚙️ Applying enhanced parsing rules with SEQUENTIAL PIPELINE for: {:?}",
classification.document_type
);
println!("📊 Available text elements: {}", text_elements.len());
let config = self
.config_manager
.get_config(&classification.document_type);
println!(
"📝 Using config thresholds: large={:.1}%, medium={:.1}%, small={:.1}%",
config.section_and_hierarchy.large_header_threshold * 100.0,
config.section_and_hierarchy.medium_header_threshold * 100.0,
config.section_and_hierarchy.small_header_threshold * 100.0
);
println!("🔧 Applying BaseConversion...");
let mut elements = self.convert_text_elements_to_parsed(text_elements);
debug_pipeline_elements("BaseConversion", &elements, &self.debug_config);
println!(" ✅ {} elements after BaseConversion", elements.len());
let minimal_parse = self.minimal_parse_override.unwrap_or(config.minimal_parse);
if minimal_parse {
println!("⚡ Minimal parse mode enabled - bypassing all rule processing");
return Ok(elements);
}
println!("🔗 Executing config-driven rule pipeline...");
self.rule_timings.borrow_mut().clear();
for rule_config in &config.pipeline.rules {
if !rule_config.enabled {
println!(" ⏭️ Skipping disabled rule: {}", rule_config.name);
continue;
}
println!("🔧 Applying rule: {}", rule_config.name);
elements = self.apply_rule_by_name(
&rule_config.name,
elements,
text_elements,
config,
document_analysis,
font_size_analysis,
style_data,
)?;
println!(
" ✅ {} elements after {}",
elements.len(),
rule_config.name
);
}
Ok(elements)
}
pub fn apply_rules_with_config(
&self,
text_elements: &[PdfTextElement],
classification: &ClassificationResult,
document_analysis: &DocumentAnalysis,
font_size_analysis: &FontSizeAnalysis,
style_data: &StyleData,
config: &ParsingConfig,
) -> Result<Vec<ParsedPdfElement>> {
println!(
"⚙️ Applying rules with config flow for: {:?}",
classification.document_type
);
println!("📊 Available text elements: {}", text_elements.len());
let mut elements = self.convert_text_elements_to_parsed(text_elements);
for rule_config in &config.pipeline.rules {
if !rule_config.enabled {
println!(" ⏭️ Skipping disabled rule: {}", rule_config.name);
continue;
}
println!(" 🔄 Applying rule: {}", rule_config.name);
elements = self.apply_rule_by_name(
&rule_config.name,
elements,
text_elements,
config,
document_analysis,
font_size_analysis,
style_data,
)?;
println!(
" ✅ {} elements after {}",
elements.len(),
rule_config.name
);
}
Ok(elements)
}
fn apply_rule_by_name(
&self,
rule_name: &str,
elements: Vec<ParsedPdfElement>,
text_elements: &[PdfTextElement],
config: &ParsingConfig,
document_analysis: &DocumentAnalysis,
font_size_analysis: &FontSizeAnalysis,
style_data: &StyleData,
) -> Result<Vec<ParsedPdfElement>> {
let rule_start = std::time::Instant::now();
let result = match rule_name {
"ParagraphClustering" => {
println!("🧩 APPLYING PARAGRAPH CLUSTERING...");
let rule = ParagraphClusteringRule::new(
self,
text_elements,
config,
document_analysis,
font_size_analysis,
style_data,
);
let result = rule.apply(elements)?;
debug_pipeline_elements("ParagraphClustering", &result, &self.debug_config);
Ok(result)
}
"SpatialClustering" => {
println!("🧩 APPLYING SPATIAL CLUSTERING...");
let spatial_rule = SpatialClusteringRule::new(config);
let result = spatial_rule.apply(elements)?;
debug_pipeline_elements("SpatialClustering", &result, &self.debug_config);
Ok(result)
}
"Validation" => {
println!("🔍 APPLYING VALIDATION...");
let validation_rule = ValidationRule::new(config);
let result = validation_rule.apply(elements)?;
debug_pipeline_elements("Validation", &result, &self.debug_config);
Ok(result)
}
"SectionDetection" => {
println!("📝 DETECTING SECTIONS AND ASSIGNING HIERARCHY...");
let section_rule = SectionAndHierarchyDetectionRule::new(
self,
text_elements,
config,
document_analysis,
font_size_analysis,
style_data,
);
let result = section_rule.apply(elements)?;
debug_pipeline_elements("SectionDetection", &result, &self.debug_config);
Ok(result)
}
"SectionDetectionV2" => {
println!("📝 DETECTING SECTIONS (V2 — candidate-then-refine)...");
let rule = SectionDetectionV2Rule::new(
self,
text_elements,
config,
document_analysis,
font_size_analysis,
style_data,
);
let result = rule.apply(elements)?;
debug_pipeline_elements("SectionDetectionV2", &result, &self.debug_config);
Ok(result)
}
"PatternBasedSectionDetection" => {
println!("🔍 PATTERN-BASED SECTION DETECTION (DISABLED - WILL BE REWRITTEN)");
println!(
" ⏭️ Passing through {} elements unchanged",
elements.len()
);
Ok(elements)
}
"ListDetection" => {
println!("📝 LIST DETECTION (DISABLED - WILL BE REWRITTEN)");
println!(
" ⏭️ Passing through {} elements unchanged",
elements.len()
);
Ok(elements)
}
"SizeEnforcer" => {
println!("🔪 SIZE ENFORCEMENT (DISABLED - WILL BE REWRITTEN)");
println!(
" ⏭️ Passing through {} elements unchanged",
elements.len()
);
Ok(elements)
}
_ => {
println!("⚠️ Unknown rule: {rule_name}. Skipping...");
Ok(elements)
}
};
let rule_duration = rule_start.elapsed();
self.rule_timings
.borrow_mut()
.push((rule_name.to_string(), rule_duration));
result
}
pub fn analyze_font_sizes(
&self,
text_elements: &[PdfTextElement],
style_data: &StyleData,
) -> FontSizeAnalysis {
let mut class_usage_counts = std::collections::HashMap::new();
for element in text_elements {
if element.rotation() != 0 {
continue;
}
*class_usage_counts
.entry(element.style_info.class_name.clone())
.or_insert(0) += 1;
}
let mut size_frequency_map = std::collections::HashMap::new();
let mut font_sizes = Vec::new();
let mut size_to_count_vec: Vec<(f32, usize)> = Vec::new();
for (class_name, usage_count) in &class_usage_counts {
if let Some(font_class) = style_data.font_classes.get(class_name) {
let size_key = format!("{:.1}", font_class.font_size); *size_frequency_map.entry(size_key).or_insert(0) += usage_count;
if let Some(existing) = size_to_count_vec
.iter_mut()
.find(|(size, _)| (size - font_class.font_size).abs() < 0.01)
{
existing.1 += usage_count;
} else {
size_to_count_vec.push((font_class.font_size, *usage_count));
}
for _ in 0..*usage_count {
font_sizes.push(font_class.font_size);
}
}
}
if font_sizes.is_empty() {
return FontSizeAnalysis::default();
}
font_sizes.sort_by(|a, b| a.partial_cmp(b).unwrap());
let min_size = font_sizes[0];
let max_size = font_sizes[font_sizes.len() - 1];
let median_size = font_sizes[font_sizes.len() / 2];
let total_elements = font_sizes.len();
let (most_common_size, max_frequency) = size_to_count_vec
.iter()
.max_by_key(|(_, count)| *count)
.map(|(size, count)| (*size, *count))
.unwrap_or((median_size, 1));
let most_common_class = class_usage_counts
.iter()
.max_by_key(|(_, &count)| count)
.map(|(class, _)| class.clone())
.unwrap_or_else(|| "unknown".to_string());
let frequency_threshold = (total_elements as f32 * 0.1).max(1.0) as usize; let rare_large_sizes: Vec<f32> = size_to_count_vec
.iter()
.filter(|(size, count)| *size > median_size && *count <= frequency_threshold)
.map(|(size, _)| *size)
.collect();
let mut potential_header_sizes: Vec<f32> = size_to_count_vec
.iter()
.filter(|(size, count)| {
*size > most_common_size && *count < max_frequency / 2
})
.map(|(size, _)| *size)
.collect();
potential_header_sizes.sort_by(|a, b| b.partial_cmp(a).unwrap());
let mut hierarchy_levels: Vec<(f32, usize)> = size_to_count_vec.clone();
hierarchy_levels.sort_by(|(size_a, count_a), (size_b, count_b)| {
match size_b.partial_cmp(size_a).unwrap() {
std::cmp::Ordering::Equal => count_a.cmp(count_b), other => other,
}
});
let hierarchy_levels: Vec<f32> =
hierarchy_levels.into_iter().map(|(size, _)| size).collect();
let size_usage_ratio = max_frequency as f32 / total_elements as f32;
let body_text_size = most_common_size;
println!("🎯 Semantic Font Analysis Results:");
println!(
" 📊 {} unique classes, {} total elements",
class_usage_counts.len(),
total_elements
);
println!(
" 📏 Size range: {:.1}pt - {:.1}pt (median: {:.1}pt)",
min_size, max_size, median_size
);
println!(
" 📝 Body text: {:.1}pt ({} elements, {:.1}% usage)",
body_text_size,
max_frequency,
size_usage_ratio * 100.0
);
println!(" 🎯 Potential headers: {:?}", potential_header_sizes);
println!(" 📚 Hierarchy levels: {:?}", hierarchy_levels);
if !rare_large_sizes.is_empty() {
println!(" ⭐ Rare large sizes: {:?}", rare_large_sizes);
}
FontSizeAnalysis {
median_size,
min_size,
max_size,
most_common_size,
most_common_class,
rare_large_sizes,
size_frequency_map,
class_usage_counts,
potential_header_sizes,
body_text_size,
hierarchy_levels,
size_usage_ratio,
}
}
pub fn convert_text_elements_to_parsed(
&self,
text_elements: &[PdfTextElement],
) -> Vec<ParsedPdfElement> {
let mut elements = Vec::new();
for (position, text_element) in text_elements.iter().enumerate() {
if text_element.text.trim().is_empty() {
continue;
}
let paragraph_element = ParsedPdfElement {
element_type: ParsedElementType::Paragraph,
text: text_element.text.trim().to_string(),
hierarchy_level: 1, position,
style_info: text_element.style_info.clone(),
placement: Some(text_element.placement.clone()),
reading_order: text_element.reading_order,
bookmark_match: text_element.bookmark_match.clone(),
token_count: text_element.token_count,
};
elements.push(paragraph_element);
}
elements
}
}
#[derive(Debug, Clone)]
pub struct FontSizeAnalysis {
pub median_size: f32,
pub min_size: f32,
pub max_size: f32,
pub most_common_size: f32, pub most_common_class: String, pub rare_large_sizes: Vec<f32>,
pub size_frequency_map: std::collections::HashMap<String, usize>, pub class_usage_counts: std::collections::HashMap<String, usize>,
pub potential_header_sizes: Vec<f32>, pub body_text_size: f32,
pub hierarchy_levels: Vec<f32>, pub size_usage_ratio: f32, }
impl Default for FontSizeAnalysis {
fn default() -> Self {
Self {
median_size: 12.0,
min_size: 12.0,
max_size: 12.0,
most_common_size: 12.0,
most_common_class: "default".to_string(),
rare_large_sizes: Vec::new(),
size_frequency_map: std::collections::HashMap::new(),
class_usage_counts: std::collections::HashMap::new(),
potential_header_sizes: Vec::new(),
body_text_size: 12.0,
hierarchy_levels: Vec::new(),
size_usage_ratio: 1.0,
}
}
}
pub trait ParseRule {
fn apply(&self, elements: Vec<ParsedPdfElement>) -> Result<Vec<ParsedPdfElement>>;
fn name(&self) -> &str;
}