use crate::types::DocumentType;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParsingConfig {
pub document_type: DocumentType,
#[serde(default)]
pub section_and_hierarchy: SectionAndHierarchyConfig,
pub spatial_clustering: SpatialClusteringConfig,
pub section_patterns: Vec<String>,
#[serde(default)]
pub include_raw_tika: bool,
#[serde(default)]
pub pipeline: PipelineConfig,
#[serde(default)]
pub list_detection: ListDetectionConfig,
#[serde(default)]
pub size_enforcer: SizeEnforcerConfig,
#[serde(default)]
pub minimal_parse: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineConfig {
pub rules: Vec<RuleConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuleConfig {
pub name: String,
#[serde(default = "default_true")]
pub enabled: bool,
}
impl Default for PipelineConfig {
fn default() -> Self {
Self {
rules: vec![
RuleConfig {
name: "SpatialClustering+StyleAnalysis".to_string(),
enabled: true,
},
RuleConfig {
name: "Validation".to_string(),
enabled: true,
},
],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SectionAndHierarchyConfig {
pub large_header_threshold: f32,
pub medium_header_threshold: f32,
pub small_header_threshold: f32,
pub min_header_size: f32,
pub use_bold_indicator: bool,
pub bold_size_strict: bool,
pub max_depth: u32,
pub font_size_tolerance: f32,
pub enforce_max_depth: bool,
pub starting_section_level: u32,
pub pattern_detection: PatternDetectionConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatternDetectionConfig {
pub enabled: bool,
pub patterns: Vec<String>,
pub respect_font_constraints: bool,
}
impl Default for PatternDetectionConfig {
fn default() -> Self {
Self {
enabled: true,
patterns: vec![
r"^[A-Z][A-Z\s]{2,}$".to_string(), r"^\d+\.\s+[A-Z][a-z]{3,}".to_string(), r"^(Chapter|Section|Part|Article)\s+\d+".to_string(), r"^[A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})*:$".to_string(), ],
respect_font_constraints: true,
}
}
}
impl Default for SectionAndHierarchyConfig {
fn default() -> Self {
Self {
large_header_threshold: 0.7,
medium_header_threshold: 0.3,
small_header_threshold: 0.1,
min_header_size: 8.5,
use_bold_indicator: true,
bold_size_strict: true, max_depth: 5,
font_size_tolerance: 0.1,
enforce_max_depth: true,
starting_section_level: 1,
pattern_detection: PatternDetectionConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpatialClusteringConfig {
pub enabled: bool,
#[serde(default = "default_true")]
pub enable_paragraph_merging: bool,
#[serde(default)]
pub enable_spatial_adjacency: bool,
pub min_line_height: f32,
pub vertical_gap_threshold_multiplier: f32,
pub horizontal_alignment_tolerance: f32,
pub line_grouping_tolerance: f32,
pub sections: ElementClusteringConfig,
pub paragraphs: ElementClusteringConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElementClusteringConfig {
pub min_segment_size: usize,
pub max_segment_size: usize,
}
fn default_y_tolerance() -> f32 {
15.0
}
fn default_false() -> bool {
false
}
fn default_bullet_patterns() -> Vec<String> {
vec![
"•".to_string(),
"·".to_string(),
"●".to_string(),
"■".to_string(),
"▪".to_string(),
"▫".to_string(),
"◦".to_string(),
"‣".to_string(),
"⁃".to_string(),
"-".to_string(),
"*".to_string(),
"→".to_string(),
"➤".to_string(),
"✓".to_string(),
"•".to_string(),
"·".to_string(),
]
}
fn default_numbered_patterns() -> Vec<String> {
vec![
r"^\d+\.".to_string(), r"^\d+\)".to_string(), r"^\(\d+\)".to_string(), r"^[a-z]\.".to_string(), r"^[a-z]\)".to_string(), r"^[A-Z]\.".to_string(), r"^[A-Z]\)".to_string(), r"^[ivx]+\.".to_string(), r"^[IVX]+\.".to_string(), ]
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListDetectionConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_sequence_lookahead_elements")]
pub sequence_lookahead_elements: usize,
#[serde(default = "default_sequence_boundary_extension")]
pub sequence_boundary_extension: usize,
#[serde(default = "default_y_tolerance")]
pub y_tolerance: f32,
#[serde(default = "default_bullet_patterns")]
pub bullet_patterns: Vec<String>,
#[serde(default = "default_numbered_patterns")]
pub numbered_patterns: Vec<String>,
#[serde(default = "default_true")]
pub create_list_containers: bool,
#[serde(default = "default_false")]
pub preserve_list_items: bool,
#[serde(default = "default_max_lookahead_elements")]
pub max_lookahead_elements: usize,
#[serde(default = "default_last_item_boundary_gap")]
pub last_item_boundary_gap: f32,
#[serde(default)]
pub validation: ListValidationConfig,
}
fn default_sequence_lookahead_elements() -> usize {
10 }
fn default_sequence_boundary_extension() -> usize {
3 }
fn default_max_lookahead_elements() -> usize {
25 }
fn default_last_item_boundary_gap() -> f32 {
80.0 }
fn default_validation_enabled() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SequentialNumberingConfig {
#[serde(default = "default_true")]
pub allow_letter_sequences: bool,
#[serde(default = "default_zero")]
pub max_gap_tolerance: u32,
}
impl Default for SequentialNumberingConfig {
fn default() -> Self {
Self {
allow_letter_sequences: true,
max_gap_tolerance: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MathematicalContextConfig {
#[serde(default = "default_mathematical_symbols")]
pub symbols: Vec<String>,
#[serde(default = "default_mathematical_terms")]
pub terms: Vec<String>,
}
impl Default for MathematicalContextConfig {
fn default() -> Self {
Self {
symbols: default_mathematical_symbols(),
terms: default_mathematical_terms(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HyphenContextConfig {
#[serde(default = "default_hyphen_strategy")]
pub strategy: String,
#[serde(default = "default_true")]
pub require_space_after: bool,
}
impl Default for HyphenContextConfig {
fn default() -> Self {
Self {
strategy: default_hyphen_strategy(),
require_space_after: true,
}
}
}
fn default_zero() -> u32 {
0
}
fn default_mathematical_symbols() -> Vec<String> {
vec![
"→".to_string(),
"←".to_string(),
"⇒".to_string(),
"⇐".to_string(),
"∀".to_string(),
"∃".to_string(),
]
}
fn default_mathematical_terms() -> Vec<String> {
vec![
"equation".to_string(),
"formula".to_string(),
"coordinates".to_string(),
"system".to_string(),
"transform".to_string(),
]
}
fn default_hyphen_strategy() -> String {
"strict".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListValidationConfig {
#[serde(default = "default_validation_enabled")]
pub enabled: bool,
#[serde(default = "default_true")]
pub minimum_size_check: bool,
#[serde(default = "default_true")]
pub first_item_validation: bool,
#[serde(default = "default_true")]
pub parenthetical_context_check: bool,
#[serde(default = "default_true")]
pub sequential_numbering_check: bool,
#[serde(default = "default_true")]
pub mathematical_context_check: bool,
#[serde(default = "default_true")]
pub hyphen_context_check: bool,
#[serde(default)]
pub sequential_numbering: SequentialNumberingConfig,
#[serde(default)]
pub mathematical_context: MathematicalContextConfig,
#[serde(default)]
pub hyphen_context: HyphenContextConfig,
#[serde(default = "default_false")]
pub sequence_pattern_check: bool,
#[serde(default = "default_false")]
pub content_quality_check: bool,
#[serde(default = "default_false")]
pub spatial_coherence_check: bool,
}
impl Default for ListValidationConfig {
fn default() -> Self {
Self {
enabled: true,
minimum_size_check: true,
first_item_validation: true,
parenthetical_context_check: true,
sequential_numbering_check: true,
mathematical_context_check: true,
hyphen_context_check: true,
sequential_numbering: SequentialNumberingConfig::default(),
mathematical_context: MathematicalContextConfig::default(),
hyphen_context: HyphenContextConfig::default(),
sequence_pattern_check: false,
content_quality_check: false,
spatial_coherence_check: false,
}
}
}
fn default_max_size() -> usize {
800 }
fn default_size_unit() -> String {
"characters".to_string()
}
fn default_min_split_size_ratio() -> f32 {
0.25 }
fn default_max_iterations() -> usize {
10 }
fn default_split_direction() -> String {
"vertical".to_string() }
impl Default for ListDetectionConfig {
fn default() -> Self {
Self {
enabled: true,
sequence_lookahead_elements: default_sequence_lookahead_elements(),
sequence_boundary_extension: default_sequence_boundary_extension(),
y_tolerance: default_y_tolerance(),
bullet_patterns: default_bullet_patterns(),
numbered_patterns: default_numbered_patterns(),
create_list_containers: true,
preserve_list_items: false,
max_lookahead_elements: default_max_lookahead_elements(),
last_item_boundary_gap: default_last_item_boundary_gap(),
validation: ListValidationConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SizeEnforcerConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_max_size")]
pub max_size: usize,
#[serde(default = "default_size_unit")]
pub size_unit: String,
#[serde(default = "default_true")]
pub preserve_sentences: bool,
#[serde(default = "default_min_split_size_ratio")]
pub min_split_size_ratio: f32,
#[serde(default = "default_true")]
pub recursive: bool,
#[serde(default = "default_max_iterations")]
pub max_iterations: usize,
#[serde(default = "default_split_direction")]
pub split_direction: String,
}
impl Default for SizeEnforcerConfig {
fn default() -> Self {
Self {
enabled: true,
max_size: 800,
size_unit: "characters".to_string(),
preserve_sentences: true,
min_split_size_ratio: 0.25,
recursive: true,
max_iterations: 10,
split_direction: "vertical".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct ConfigManager {
configs: HashMap<DocumentType, ParsingConfig>,
default_config: ParsingConfig,
}
impl ConfigManager {
pub fn new() -> Result<Self> {
let mut manager = Self {
configs: HashMap::new(),
default_config: Self::create_default_generic_config(),
};
manager.load_builtin_configs()?;
Ok(manager)
}
pub fn get_config(&self, doc_type: &DocumentType) -> &ParsingConfig {
self.configs.get(doc_type).unwrap_or(&self.default_config)
}
pub fn load_config_from_file(&mut self, path: &str) -> Result<()> {
let content = fs::read_to_string(path)?;
let config: ParsingConfig = serde_yaml::from_str(&content)?;
self.configs.insert(config.document_type.clone(), config);
Ok(())
}
fn load_builtin_configs(&mut self) -> Result<()> {
let generic_config = Self::create_default_generic_config();
self.configs.insert(DocumentType::Generic, generic_config);
let academic_config = ParsingConfig {
document_type: DocumentType::AcademicPaper,
section_and_hierarchy: SectionAndHierarchyConfig {
large_header_threshold: 0.8, medium_header_threshold: 0.4,
small_header_threshold: 0.15,
min_header_size: 10.0,
use_bold_indicator: true,
bold_size_strict: true,
max_depth: 4,
font_size_tolerance: 0.1,
enforce_max_depth: true,
starting_section_level: 1,
pattern_detection: PatternDetectionConfig::default(),
},
spatial_clustering: SpatialClusteringConfig {
enabled: true,
enable_paragraph_merging: true,
enable_spatial_adjacency: false,
min_line_height: 9.0, vertical_gap_threshold_multiplier: 1.2, horizontal_alignment_tolerance: 8.0, line_grouping_tolerance: 0.25, sections: ElementClusteringConfig {
min_segment_size: 50, max_segment_size: 500, },
paragraphs: ElementClusteringConfig {
min_segment_size: 200, max_segment_size: 12000, },
},
section_patterns: vec![
"abstract".to_string(),
"introduction".to_string(),
"methodology".to_string(),
"results".to_string(),
"discussion".to_string(),
"conclusion".to_string(),
"references".to_string(),
],
include_raw_tika: false, pipeline: PipelineConfig::default(),
list_detection: ListDetectionConfig::default(),
size_enforcer: SizeEnforcerConfig::default(), minimal_parse: false,
};
self.configs
.insert(DocumentType::AcademicPaper, academic_config);
let legal_config = ParsingConfig {
document_type: DocumentType::LegalContract,
section_and_hierarchy: SectionAndHierarchyConfig {
large_header_threshold: 0.6,
medium_header_threshold: 0.3,
small_header_threshold: 0.1,
min_header_size: 9.0,
use_bold_indicator: true,
bold_size_strict: true,
max_depth: 5,
font_size_tolerance: 0.1,
enforce_max_depth: true,
starting_section_level: 1,
pattern_detection: PatternDetectionConfig::default(),
},
spatial_clustering: SpatialClusteringConfig {
enabled: true,
enable_paragraph_merging: true,
enable_spatial_adjacency: false,
min_line_height: 8.5,
vertical_gap_threshold_multiplier: 0.6, horizontal_alignment_tolerance: 12.0, line_grouping_tolerance: 0.2, sections: ElementClusteringConfig {
min_segment_size: 30, max_segment_size: 200, },
paragraphs: ElementClusteringConfig {
min_segment_size: 50, max_segment_size: 5000, },
},
section_patterns: vec![
"article".to_string(),
"section".to_string(),
"clause".to_string(),
"whereas".to_string(),
"terms".to_string(),
"conditions".to_string(),
],
include_raw_tika: false, pipeline: PipelineConfig::default(),
list_detection: ListDetectionConfig::default(),
size_enforcer: SizeEnforcerConfig::default(), minimal_parse: false,
};
self.configs
.insert(DocumentType::LegalContract, legal_config);
Ok(())
}
fn create_default_generic_config() -> ParsingConfig {
ParsingConfig {
document_type: DocumentType::Generic,
section_and_hierarchy: SectionAndHierarchyConfig::default(),
spatial_clustering: SpatialClusteringConfig {
enabled: true, enable_paragraph_merging: true, enable_spatial_adjacency: false, min_line_height: 8.0, vertical_gap_threshold_multiplier: 0.8, horizontal_alignment_tolerance: 10.0, line_grouping_tolerance: 0.3, sections: ElementClusteringConfig {
min_segment_size: 20, max_segment_size: 300, },
paragraphs: ElementClusteringConfig {
min_segment_size: 100, max_segment_size: 8000, },
},
section_patterns: vec![
"chapter".to_string(),
"section".to_string(),
"part".to_string(),
"overview".to_string(),
"summary".to_string(),
"background".to_string(),
"principles".to_string(),
"approach".to_string(),
],
include_raw_tika: false, pipeline: PipelineConfig::default(),
list_detection: ListDetectionConfig::default(),
size_enforcer: SizeEnforcerConfig::default(), minimal_parse: false,
}
}
}
impl Default for ConfigManager {
fn default() -> Self {
Self::new().expect("Failed to create default ConfigManager")
}
}
impl ParsingConfig {
pub fn load_from_file(path: &str) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
let config: ParsingConfig = serde_yaml::from_str(&content)?;
Ok(config)
}
pub fn load_with_fallback(path: Option<&str>) -> Self {
match path {
Some(p) => Self::load_from_file(p).unwrap_or_else(|_| {
eprintln!("⚠️ Failed to load config from {}, using defaults", p);
Self::default()
}),
None => Self::default(),
}
}
}
impl Default for ParsingConfig {
fn default() -> Self {
Self {
document_type: DocumentType::Generic,
section_and_hierarchy: SectionAndHierarchyConfig::default(),
spatial_clustering: SpatialClusteringConfig {
enabled: true,
enable_paragraph_merging: true,
enable_spatial_adjacency: false,
min_line_height: 8.0,
vertical_gap_threshold_multiplier: 0.8,
horizontal_alignment_tolerance: 10.0,
line_grouping_tolerance: 0.3,
sections: ElementClusteringConfig {
min_segment_size: 20,
max_segment_size: 300,
},
paragraphs: ElementClusteringConfig {
min_segment_size: 100,
max_segment_size: 8000,
},
},
section_patterns: vec![],
include_raw_tika: false,
pipeline: PipelineConfig::default(),
list_detection: ListDetectionConfig::default(),
size_enforcer: SizeEnforcerConfig::default(),
minimal_parse: false,
}
}
}