rusty_rich/
highlighter.rs1use crate::style::Style;
8use crate::text::Text;
9use regex::Regex;
10
11pub trait Highlighter {
17 fn highlight(&self, text: &Text) -> Text;
19}
20
21pub struct NullHighlighter;
27
28impl Highlighter for NullHighlighter {
29 fn highlight(&self, text: &Text) -> Text {
30 text.clone()
31 }
32}
33
34pub struct RegexHighlighter {
40 rules: Vec<(Regex, Style)>,
41}
42
43impl std::fmt::Debug for RegexHighlighter {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 f.debug_struct("RegexHighlighter")
46 .field("rule_count", &self.rules.len())
47 .finish()
48 }
49}
50
51impl Clone for RegexHighlighter {
52 fn clone(&self) -> Self {
53 let mut cloned = Self::new();
55 for (re, style) in &self.rules {
56 cloned.rules.push((re.clone(), style.clone()));
57 }
58 cloned
59 }
60}
61
62impl Default for RegexHighlighter {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68impl RegexHighlighter {
69 pub fn new() -> Self {
71 Self { rules: Vec::new() }
72 }
73
74 pub fn add_rule(&mut self, pattern: &str, style: Style) -> Result<(), regex::Error> {
75 let re = Regex::new(pattern)?;
76 self.rules.push((re, style));
77 Ok(())
78 }
79}
80
81impl Highlighter for RegexHighlighter {
82 fn highlight(&self, text: &Text) -> Text {
83 let mut result = text.clone();
84 for (re, style) in &self.rules {
85 let plain = result.plain.clone();
86 let mut new_text = Text::new("");
87 let mut last_end = 0usize;
88
89 for m in re.find_iter(&plain) {
90 if m.start() > last_end {
92 new_text.append(&plain[last_end..m.start()], None);
93 }
94 new_text.append_styled(m.as_str(), style.clone());
96 last_end = m.end();
97 }
98 if last_end < plain.len() {
100 new_text.append(&plain[last_end..], None);
101 }
102 result = new_text;
103 }
104 result
105 }
106}
107
108#[derive(Debug, Clone)]
114pub struct ReprHighlighter {
115 highlighter: Option<Box<RegexHighlighter>>,
116}
117
118impl Default for ReprHighlighter {
119 fn default() -> Self {
120 Self::new()
121 }
122}
123
124impl ReprHighlighter {
125 pub fn new() -> Self {
127 let mut rh = RegexHighlighter::new();
129
130 let _ = rh.add_rule(
132 r"https?://[^\s)\]}>]+",
133 Style::new()
134 .color(crate::color::Color::parse("bright_blue").unwrap())
135 .underline(true),
136 );
137
138 let _ = rh.add_rule(
140 r"(?<!\w)(-?\d+\.?\d*(?:e[+-]?\d+)?)(?!\w)",
141 Style::new()
142 .color(crate::color::Color::parse("cyan").unwrap())
143 .bold(true),
144 );
145
146 let _ = rh.add_rule(
148 r"(?<!\w)(?:/[\w.-]+)+/?(?!\w)",
149 Style::new().color(crate::color::Color::parse("magenta").unwrap()),
150 );
151
152 let _ = rh.add_rule(
154 r#""(?:[^"\\]|\\.)*""#,
155 Style::new().color(crate::color::Color::parse("green").unwrap()),
156 );
157 let _ = rh.add_rule(
158 r"'(?:[^'\\]|\\.)*'",
159 Style::new().color(crate::color::Color::parse("green").unwrap()),
160 );
161
162 Self {
163 highlighter: Some(Box::new(rh)),
164 }
165 }
166
167 pub fn highlight_str(&self, text: &str) -> Text {
169 let t = Text::new(text);
170 if let Some(ref h) = self.highlighter {
171 h.highlight(&t)
172 } else {
173 t
174 }
175 }
176}
177
178#[derive(Debug, Clone)]
188pub struct ISO8601Highlighter {
189 highlighter: RegexHighlighter,
190}
191
192impl Default for ISO8601Highlighter {
193 fn default() -> Self {
194 Self::new()
195 }
196}
197
198impl ISO8601Highlighter {
199 pub fn new() -> Self {
201 let mut h = RegexHighlighter::new();
202 let _ = h.add_rule(
204 r"\b\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?\b",
205 Style::new()
206 .color(crate::color::Color::parse("bright_yellow").unwrap())
207 .bold(true),
208 );
209 Self { highlighter: h }
210 }
211
212 pub fn highlight_str(&self, text: &str) -> Text {
214 let t = Text::new(text);
215 self.highlighter.highlight(&t)
216 }
217}
218
219impl Highlighter for ISO8601Highlighter {
220 fn highlight(&self, text: &Text) -> Text {
221 self.highlighter.highlight(text)
222 }
223}
224
225#[derive(Debug, Clone)]
232pub struct JSONHighlighter {
233 highlighter: RegexHighlighter,
234}
235
236impl Default for JSONHighlighter {
237 fn default() -> Self {
238 Self::new()
239 }
240}
241
242impl JSONHighlighter {
243 pub fn new() -> Self {
245 let mut h = RegexHighlighter::new();
246
247 let _ = h.add_rule(
249 r#""(?:[^"\\]|\\.)*"\s*:"#,
250 Style::new().color(crate::color::Color::parse("bright_cyan").unwrap()),
251 );
252
253 let _ = h.add_rule(
255 r#""(?:[^"\\]|\\.)*""#,
256 Style::new().color(crate::color::Color::parse("green").unwrap()),
257 );
258
259 let _ = h.add_rule(
261 r"(?<!\w)-?\d+\.?\d*(?:[eE][+-]?\d+)?(?!\w)",
262 Style::new().color(crate::color::Color::parse("bright_yellow").unwrap()),
263 );
264
265 let _ = h.add_rule(
267 r"\b(?:true|false|null)\b",
268 Style::new()
269 .color(crate::color::Color::parse("magenta").unwrap())
270 .bold(true),
271 );
272
273 let _ = h.add_rule(
275 r"[{}\[\]]",
276 Style::new()
277 .color(crate::color::Color::parse("white").unwrap())
278 .bold(true),
279 );
280
281 Self { highlighter: h }
282 }
283
284 pub fn highlight_str(&self, text: &str) -> Text {
286 let t = Text::new(text);
287 self.highlighter.highlight(&t)
288 }
289}
290
291impl Highlighter for JSONHighlighter {
292 fn highlight(&self, text: &Text) -> Text {
293 self.highlighter.highlight(text)
294 }
295}
296
297#[derive(Debug, Clone)]
304pub struct PathHighlighter {
305 highlighter: RegexHighlighter,
306}
307
308impl Default for PathHighlighter {
309 fn default() -> Self {
310 Self::new()
311 }
312}
313
314impl PathHighlighter {
315 pub fn new() -> Self {
317 let mut h = RegexHighlighter::new();
318
319 let _ = h.add_rule(
321 r"(?:\w:)?(?:[/\\][\w.\-]+)+(?:\.\w+)?(?::\d+(?::\d+)?)?",
322 Style::new().color(crate::color::Color::parse("bright_magenta").unwrap()),
323 );
324
325 Self { highlighter: h }
326 }
327
328 pub fn highlight_str(&self, text: &str) -> Text {
330 let t = Text::new(text);
331 self.highlighter.highlight(&t)
332 }
333}
334
335impl Highlighter for PathHighlighter {
336 fn highlight(&self, text: &Text) -> Text {
337 self.highlighter.highlight(text)
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn test_null_highlighter() {
347 let h = NullHighlighter;
348 let t = Text::new("hello");
349 let result = h.highlight(&t);
350 assert_eq!(result.plain, "hello");
351 }
352
353 #[test]
354 fn test_repr_highlighter_numbers() {
355 let h = ReprHighlighter::new();
356 let result = h.highlight_str("num=42");
357 assert!(!result.plain.is_empty());
360 }
361
362 #[test]
363 fn test_iso8601_highlighter() {
364 let h = ISO8601Highlighter::new();
365 let result = h.highlight_str("2024-01-15T10:30:00Z");
366 assert!(!result.plain.is_empty());
367 }
368
369 #[test]
370 fn test_json_highlighter() {
371 let h = JSONHighlighter::new();
372 let result = h.highlight_str(r#"{"key": "value", "num": 42, "flag": true}"#);
373 assert!(!result.plain.is_empty());
374 }
375
376 #[test]
377 fn test_path_highlighter() {
378 let h = PathHighlighter::new();
379 let result = h.highlight_str("src/main.rs:42");
380 assert!(!result.plain.is_empty());
381 }
382}