Skip to main content

html_to_markdown_rs/options/
preprocessing.rs

1//! HTML preprocessing configuration options.
2//!
3//! This module provides configuration for document cleanup before conversion,
4//! including preset levels and granular control over element removal.
5
6use crate::options::validation::normalize_token;
7
8/// HTML preprocessing aggressiveness level.
9///
10/// Controls the extent of cleanup performed before conversion. Higher levels remove more elements.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum PreprocessingPreset {
13    /// Minimal cleanup. Remove only essential noise (scripts, styles).
14    Minimal,
15    /// Standard cleanup. Default. Removes navigation, forms, and other auxiliary content.
16    #[default]
17    Standard,
18    /// Aggressive cleanup. Remove extensive non-content elements and structure.
19    Aggressive,
20}
21
22impl PreprocessingPreset {
23    /// Parse a preprocessing preset from a string.
24    ///
25    /// Accepts "minimal", "aggressive", or defaults to Standard.
26    /// Input is normalized (lowercased, alphanumeric only).
27    #[must_use]
28    #[cfg_attr(alef, alef(skip))]
29    pub fn parse(value: &str) -> Self {
30        match normalize_token(value).as_str() {
31            "minimal" => Self::Minimal,
32            "aggressive" => Self::Aggressive,
33            _ => Self::Standard,
34        }
35    }
36}
37
38/// HTML preprocessing options for document cleanup before conversion.
39#[derive(Debug, Clone)]
40#[cfg_attr(
41    any(feature = "serde", feature = "metadata"),
42    derive(serde::Serialize, serde::Deserialize)
43)]
44#[cfg_attr(any(feature = "serde", feature = "metadata"), serde(default, deny_unknown_fields))]
45pub struct PreprocessingOptions {
46    /// Enable HTML preprocessing globally
47    pub enabled: bool,
48
49    /// Preprocessing preset level (Minimal, Standard, Aggressive)
50    pub preset: PreprocessingPreset,
51
52    /// Remove navigation elements (nav, breadcrumbs, menus, sidebars)
53    pub remove_navigation: bool,
54
55    /// Remove form elements (forms, inputs, buttons, etc.)
56    pub remove_forms: bool,
57}
58
59/// Partial update for `PreprocessingOptions`.
60///
61/// This struct uses `Option<T>` to represent optional fields that can be selectively updated.
62/// Only specified fields (Some values) will override existing options; None values leave the
63/// corresponding fields unchanged when applied via [`PreprocessingOptions::apply_update`].
64#[derive(Debug, Clone, Default)]
65#[cfg_attr(
66    any(feature = "serde", feature = "metadata"),
67    derive(serde::Serialize, serde::Deserialize)
68)]
69#[cfg_attr(any(feature = "serde", feature = "metadata"), serde(deny_unknown_fields))]
70pub struct PreprocessingOptionsUpdate {
71    /// Optional global preprocessing enablement override
72    pub enabled: Option<bool>,
73
74    /// Optional preprocessing preset level override (Minimal, Standard, Aggressive)
75    pub preset: Option<PreprocessingPreset>,
76
77    /// Optional navigation element removal override (nav, breadcrumbs, menus, sidebars)
78    pub remove_navigation: Option<bool>,
79
80    /// Optional form element removal override (forms, inputs, buttons, etc.)
81    pub remove_forms: Option<bool>,
82}
83
84impl Default for PreprocessingOptions {
85    fn default() -> Self {
86        Self {
87            enabled: true,
88            preset: PreprocessingPreset::default(),
89            remove_navigation: true,
90            remove_forms: true,
91        }
92    }
93}
94
95impl PreprocessingOptions {
96    /// Apply a partial update to these preprocessing options.
97    ///
98    /// Any specified fields in the update will override the current values.
99    /// Unspecified fields (None) are left unchanged.
100    ///
101    /// # Arguments
102    ///
103    /// * `update` - Partial preprocessing options update
104    #[allow(clippy::needless_pass_by_value)]
105    #[cfg_attr(alef, alef(skip))]
106    pub const fn apply_update(&mut self, update: PreprocessingOptionsUpdate) {
107        if let Some(enabled) = update.enabled {
108            self.enabled = enabled;
109        }
110        if let Some(preset) = update.preset {
111            self.preset = preset;
112        }
113        if let Some(remove_navigation) = update.remove_navigation {
114            self.remove_navigation = remove_navigation;
115        }
116        if let Some(remove_forms) = update.remove_forms {
117            self.remove_forms = remove_forms;
118        }
119    }
120
121    /// Create new preprocessing options from a partial update.
122    ///
123    /// Creates a new `PreprocessingOptions` struct with defaults, then applies the update.
124    /// Fields not specified in the update keep their default values.
125    ///
126    /// # Arguments
127    ///
128    /// * `update` - Partial preprocessing options update
129    ///
130    /// # Returns
131    ///
132    /// New `PreprocessingOptions` with specified updates applied to defaults
133    #[must_use]
134    #[cfg_attr(alef, alef(skip))]
135    pub fn from_update(update: PreprocessingOptionsUpdate) -> Self {
136        let mut options = Self::default();
137        options.apply_update(update);
138        options
139    }
140}
141
142impl From<PreprocessingOptionsUpdate> for PreprocessingOptions {
143    fn from(update: PreprocessingOptionsUpdate) -> Self {
144        Self::from_update(update)
145    }
146}
147
148#[cfg(any(feature = "serde", feature = "metadata"))]
149mod serde_impls {
150    use super::PreprocessingPreset;
151    use serde::{Deserialize, Serializer};
152
153    impl<'de> Deserialize<'de> for PreprocessingPreset {
154        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
155        where
156            D: serde::Deserializer<'de>,
157        {
158            let value = String::deserialize(deserializer)?;
159            Ok(Self::parse(&value))
160        }
161    }
162
163    impl serde::Serialize for PreprocessingPreset {
164        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
165        where
166            S: Serializer,
167        {
168            let s = match self {
169                Self::Minimal => "minimal",
170                Self::Standard => "standard",
171                Self::Aggressive => "aggressive",
172            };
173            serializer.serialize_str(s)
174        }
175    }
176}
177
178#[cfg(all(test, any(feature = "serde", feature = "metadata")))]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_preprocessing_options_serde() {
184        let options = PreprocessingOptions {
185            enabled: true,
186            preset: PreprocessingPreset::Aggressive,
187            remove_navigation: false,
188            ..Default::default()
189        };
190
191        // Serialize to JSON
192        let json = serde_json::to_string(&options).expect("Failed to serialize");
193
194        // Deserialize back
195        let deserialized: PreprocessingOptions = serde_json::from_str(&json).expect("Failed to deserialize");
196
197        // Verify values
198        assert!(deserialized.enabled);
199        assert_eq!(deserialized.preset, PreprocessingPreset::Aggressive);
200        assert!(!deserialized.remove_navigation);
201    }
202}