at main 259 lines 7.6 kB view raw
1use super::types::{FacetFeature, NormalizedFacet}; 2use super::FacetOutput; 3use std::cmp::Ordering; 4 5#[derive(Debug, Clone)] 6struct FacetEvent<'a> { 7 pos: usize, 8 is_start: bool, 9 feature: FacetFeature<'a>, 10 facet_idx: usize, 11} 12 13impl<'a> FacetEvent<'a> { 14 fn start(pos: usize, feature: FacetFeature<'a>, facet_idx: usize) -> Self { 15 Self { 16 pos, 17 is_start: true, 18 feature, 19 facet_idx, 20 } 21 } 22 23 fn end(pos: usize, feature: FacetFeature<'a>, facet_idx: usize) -> Self { 24 Self { 25 pos, 26 is_start: false, 27 feature, 28 facet_idx, 29 } 30 } 31} 32 33impl<'a> PartialEq for FacetEvent<'a> { 34 fn eq(&self, other: &Self) -> bool { 35 self.pos == other.pos && self.is_start == other.is_start 36 } 37} 38 39impl<'a> Eq for FacetEvent<'a> {} 40 41impl<'a> PartialOrd for FacetEvent<'a> { 42 fn partial_cmp(&self, other: &Self) -> Option<Ordering> { 43 Some(self.cmp(other)) 44 } 45} 46 47impl<'a> Ord for FacetEvent<'a> { 48 fn cmp(&self, other: &Self) -> Ordering { 49 match self.pos.cmp(&other.pos) { 50 Ordering::Equal => { 51 // At same position: ends before starts for proper nesting 52 match (self.is_start, other.is_start) { 53 (false, true) => Ordering::Less, 54 (true, false) => Ordering::Greater, 55 _ => Ordering::Equal, 56 } 57 } 58 ord => ord, 59 } 60 } 61} 62 63pub fn process_faceted_text<'a, O: FacetOutput>( 64 text: &'a str, 65 facets: &[NormalizedFacet<'a>], 66 output: &mut O, 67) -> Result<(), O::Error> { 68 let mut events: Vec<FacetEvent<'a>> = Vec::new(); 69 70 for (idx, facet) in facets.iter().enumerate() { 71 if facet.index.is_empty() { 72 continue; 73 } 74 for feature in &facet.features { 75 events.push(FacetEvent::start(facet.index.start(), feature.clone(), idx)); 76 events.push(FacetEvent::end(facet.index.end(), feature.clone(), idx)); 77 } 78 } 79 80 events.sort(); 81 82 // Track active features in a stack: (feature, facet_idx) 83 let mut active_stack: Vec<(FacetFeature<'a>, usize)> = Vec::new(); 84 let mut last_pos = 0; 85 86 for event in events { 87 let pos = event.pos.min(text.len()); 88 89 // Write text up to this position 90 if pos > last_pos { 91 if let Some(segment) = text.get(last_pos..pos) { 92 output.write_text(segment)?; 93 } 94 last_pos = pos; 95 } 96 97 if event.is_start { 98 output.start_feature(&event.feature)?; 99 active_stack.push((event.feature, event.facet_idx)); 100 } else { 101 // Find the feature in the stack that matches this end event 102 let close_from = active_stack 103 .iter() 104 .rposition(|(f, idx)| *idx == event.facet_idx && feature_matches(f, &event.feature)); 105 106 if let Some(close_idx) = close_from { 107 // Close features from top down to the one we need to close 108 let to_reopen: Vec<_> = active_stack.drain(close_idx..).collect(); 109 110 // Close all features we're draining (in reverse order) 111 for (f, _) in to_reopen.iter().rev() { 112 output.end_feature(f)?; 113 } 114 115 // Reopen features that aren't the one we're closing (skip first which is the one we're closing) 116 for (f, idx) in to_reopen.into_iter().skip(1) { 117 output.start_feature(&f)?; 118 active_stack.push((f, idx)); 119 } 120 } 121 } 122 } 123 124 // Write remaining text 125 if last_pos < text.len() { 126 output.write_text(&text[last_pos..])?; 127 } 128 129 // Close any remaining open features 130 for (feature, _) in active_stack.into_iter().rev() { 131 output.end_feature(&feature)?; 132 } 133 134 Ok(()) 135} 136 137fn feature_matches(a: &FacetFeature<'_>, b: &FacetFeature<'_>) -> bool { 138 std::mem::discriminant(a) == std::mem::discriminant(b) 139} 140 141#[cfg(test)] 142mod tests { 143 use super::*; 144 use crate::facet::types::ByteRange; 145 146 struct TestOutput { 147 buffer: String, 148 } 149 150 impl TestOutput { 151 fn new() -> Self { 152 Self { 153 buffer: String::new(), 154 } 155 } 156 } 157 158 impl FacetOutput for TestOutput { 159 type Error = std::fmt::Error; 160 161 fn write_text(&mut self, text: &str) -> Result<(), Self::Error> { 162 self.buffer.push_str(text); 163 Ok(()) 164 } 165 166 fn start_feature(&mut self, feature: &FacetFeature<'_>) -> Result<(), Self::Error> { 167 match feature { 168 FacetFeature::Bold => self.buffer.push_str("<b>"), 169 FacetFeature::Italic => self.buffer.push_str("<i>"), 170 FacetFeature::Link { uri } => { 171 self.buffer.push_str(&format!("<a href=\"{}\">", uri)) 172 } 173 _ => self.buffer.push_str("<?>"), 174 } 175 Ok(()) 176 } 177 178 fn end_feature(&mut self, feature: &FacetFeature<'_>) -> Result<(), Self::Error> { 179 match feature { 180 FacetFeature::Bold => self.buffer.push_str("</b>"), 181 FacetFeature::Italic => self.buffer.push_str("</i>"), 182 FacetFeature::Link { .. } => self.buffer.push_str("</a>"), 183 _ => self.buffer.push_str("</?>"), 184 } 185 Ok(()) 186 } 187 } 188 189 #[test] 190 fn test_simple_bold() { 191 let text = "hello world"; 192 let facets = vec![NormalizedFacet { 193 index: ByteRange::new(0, 5), 194 features: vec![FacetFeature::Bold], 195 }]; 196 197 let mut output = TestOutput::new(); 198 process_faceted_text(text, &facets, &mut output).unwrap(); 199 200 assert_eq!(output.buffer, "<b>hello</b> world"); 201 } 202 203 #[test] 204 fn test_overlapping_facets() { 205 // "bold and italic just italic" 206 // ^^^^^^^^^^^^^ <- bold (0-13) 207 // ^^^^^^^^^^^^^^^^^^^^^^^ <- italic (5-27) 208 let text = "bold and italic just italic"; 209 let facets = vec![ 210 NormalizedFacet { 211 index: ByteRange::new(0, 15), 212 features: vec![FacetFeature::Bold], 213 }, 214 NormalizedFacet { 215 index: ByteRange::new(5, 27), 216 features: vec![FacetFeature::Italic], 217 }, 218 ]; 219 220 let mut output = TestOutput::new(); 221 process_faceted_text(text, &facets, &mut output).unwrap(); 222 223 // Should properly nest: <b>bold <i>and italic</i></b><i> just italic</i> 224 assert_eq!( 225 output.buffer, 226 "<b>bold <i>and italic</i></b><i> just italic</i>" 227 ); 228 } 229 230 #[test] 231 fn test_no_facets() { 232 let text = "plain text"; 233 let facets: Vec<NormalizedFacet> = vec![]; 234 235 let mut output = TestOutput::new(); 236 process_faceted_text(text, &facets, &mut output).unwrap(); 237 238 assert_eq!(output.buffer, "plain text"); 239 } 240 241 #[test] 242 fn test_link_facet() { 243 let text = "click here for more"; 244 let facets = vec![NormalizedFacet { 245 index: ByteRange::new(6, 10), 246 features: vec![FacetFeature::Link { 247 uri: "https://example.com", 248 }], 249 }]; 250 251 let mut output = TestOutput::new(); 252 process_faceted_text(text, &facets, &mut output).unwrap(); 253 254 assert_eq!( 255 output.buffer, 256 "click <a href=\"https://example.com\">here</a> for more" 257 ); 258 } 259}