uqa_analysis/
char_filter.rs1use std::collections::BTreeMap;
10
11use serde::{Deserialize, Serialize};
12
13use crate::error::AnalysisResult;
14use crate::FilteredText;
15use uqa_core::memory::MemoryBudget;
16
17mod compiled;
18mod replacement;
19mod stream;
20pub(crate) use compiled::PreparedCharFilter;
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(tag = "type", rename_all = "snake_case")]
24pub enum CharFilter {
25 #[serde(rename = "html_strip", alias = "h_t_m_l_strip")]
28 HTMLStrip,
29 Mapping {
30 mapping: BTreeMap<String, String>,
31 },
32 PatternReplace {
33 pattern: String,
34 #[serde(default)]
35 replacement: String,
36 },
37}
38
39impl CharFilter {
40 pub fn validate(&self) -> AnalysisResult<()> {
42 match self {
43 CharFilter::PatternReplace { .. } => self.prepare().map(|_| ()),
44 _ => Ok(()),
45 }
46 }
47
48 pub fn filter(&self, text: &str) -> AnalysisResult<String> {
49 Ok(self.filter_with_offsets(text)?.into_string())
50 }
51
52 pub fn filter_with_offsets<'a>(&self, text: &'a str) -> AnalysisResult<FilteredText<'a>> {
54 self.filter_mapped(FilteredText::new(text))
55 }
56
57 pub fn filter_mapped<'a>(&self, text: FilteredText<'a>) -> AnalysisResult<FilteredText<'a>> {
59 self.prepare()?.filter_mapped(text)
60 }
61
62 pub fn filter_with_offsets_budgeted<'a>(
81 &self,
82 text: &'a str,
83 budget: &MemoryBudget,
84 poll: &mut dyn FnMut() -> AnalysisResult<()>,
85 ) -> AnalysisResult<FilteredText<'a>> {
86 self.filter_mapped_budgeted(FilteredText::new(text), budget, poll)
87 }
88
89 pub fn filter_mapped_budgeted<'a>(
91 &self,
92 text: FilteredText<'a>,
93 budget: &MemoryBudget,
94 poll: &mut dyn FnMut() -> AnalysisResult<()>,
95 ) -> AnalysisResult<FilteredText<'a>> {
96 poll()?;
97 self.prepare()?.filter_mapped_budgeted(text, budget, poll)
98 }
99}
100
101const HTML_ENTITIES: &[(&str, &str)] = &[
102 ("&", "&"),
103 ("<", "<"),
104 (">", ">"),
105 (""", "\""),
106 ("'", "'"),
107 ("'", "'"),
108 (" ", " "),
109];
110
111fn mapping_longest_first(m: &BTreeMap<String, String>) -> Vec<(String, String)> {
114 let mut entries: Vec<(String, String)> =
115 m.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
116 entries.sort_by(|a, b| b.0.len().cmp(&a.0.len()).then_with(|| a.0.cmp(&b.0)));
117 entries
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn html_strip_removes_tags_and_decodes_entities() {
126 let f = CharFilter::HTMLStrip;
127 assert_eq!(
128 f.filter("<p>hello & world</p>").unwrap(),
129 " hello & world ".to_string()
130 );
131 }
132
133 #[test]
134 fn mapping_replaces_longest_first() {
135 let mut m = BTreeMap::new();
140 m.insert("aa".to_string(), "X".to_string());
141 m.insert("a".to_string(), "Y".to_string());
142 let f = CharFilter::Mapping { mapping: m };
143 assert_eq!(f.filter("aab").unwrap(), "Xb");
144
145 assert_eq!(f.filter("aba").unwrap(), "YbY");
148 }
149
150 #[test]
151 fn pattern_replace_uses_regex() {
152 let f = CharFilter::PatternReplace {
153 pattern: r"\d+".to_string(),
154 replacement: "#".to_string(),
155 };
156 assert_eq!(f.filter("a1b22c").unwrap(), "a#b#c");
157 }
158}