rusty_rich/
highlighter.rs1use regex::Regex;
8use crate::style::Style;
9use crate::text::Text;
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 RegexHighlighter {
63 pub fn new() -> Self {
65 Self { rules: Vec::new() }
66 }
67
68 pub fn add_rule(&mut self, pattern: &str, style: Style) -> Result<(), regex::Error> {
69 let re = Regex::new(pattern)?;
70 self.rules.push((re, style));
71 Ok(())
72 }
73}
74
75impl Highlighter for RegexHighlighter {
76 fn highlight(&self, text: &Text) -> Text {
77 let mut result = text.clone();
78 for (re, style) in &self.rules {
79 let plain = result.plain.clone();
80 let mut new_text = Text::new("");
81 let mut last_end = 0usize;
82
83 for m in re.find_iter(&plain) {
84 if m.start() > last_end {
86 new_text.append(&plain[last_end..m.start()], None);
87 }
88 new_text.append_styled(m.as_str(), style.clone());
90 last_end = m.end();
91 }
92 if last_end < plain.len() {
94 new_text.append(&plain[last_end..], None);
95 }
96 result = new_text;
97 }
98 result
99 }
100}
101
102#[derive(Debug, Clone)]
108pub struct ReprHighlighter {
109 highlighter: Option<Box<RegexHighlighter>>,
110}
111
112impl ReprHighlighter {
113 pub fn new() -> Self {
115 let mut rh = RegexHighlighter::new();
117
118 let _ = rh.add_rule(
120 r"https?://[^\s)\]}>]+",
121 Style::new()
122 .color(crate::color::Color::parse("bright_blue").unwrap())
123 .underline(true),
124 );
125
126 let _ = rh.add_rule(
128 r"(?<!\w)(-?\d+\.?\d*(?:e[+-]?\d+)?)(?!\w)",
129 Style::new()
130 .color(crate::color::Color::parse("cyan").unwrap())
131 .bold(true),
132 );
133
134 let _ = rh.add_rule(
136 r"(?<!\w)(?:/[\w.-]+)+/?(?!\w)",
137 Style::new()
138 .color(crate::color::Color::parse("magenta").unwrap()),
139 );
140
141 let _ = rh.add_rule(
143 r#""(?:[^"\\]|\\.)*""#,
144 Style::new()
145 .color(crate::color::Color::parse("green").unwrap()),
146 );
147 let _ = rh.add_rule(
148 r"'(?:[^'\\]|\\.)*'",
149 Style::new()
150 .color(crate::color::Color::parse("green").unwrap()),
151 );
152
153 Self {
154 highlighter: Some(Box::new(rh)),
155 }
156 }
157
158 pub fn highlight_str(&self, text: &str) -> Text {
160 let t = Text::new(text);
161 if let Some(ref h) = self.highlighter {
162 h.highlight(&t)
163 } else {
164 t
165 }
166 }
167}
168
169#[derive(Debug, Clone)]
179pub struct ISO8601Highlighter {
180 highlighter: RegexHighlighter,
181}
182
183impl ISO8601Highlighter {
184 pub fn new() -> Self {
186 let mut h = RegexHighlighter::new();
187 let _ = h.add_rule(
189 r"\b\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?\b",
190 Style::new()
191 .color(crate::color::Color::parse("bright_yellow").unwrap())
192 .bold(true),
193 );
194 Self { highlighter: h }
195 }
196
197 pub fn highlight_str(&self, text: &str) -> Text {
199 let t = Text::new(text);
200 self.highlighter.highlight(&t)
201 }
202}
203
204impl Highlighter for ISO8601Highlighter {
205 fn highlight(&self, text: &Text) -> Text {
206 self.highlighter.highlight(text)
207 }
208}
209
210#[derive(Debug, Clone)]
217pub struct JSONHighlighter {
218 highlighter: RegexHighlighter,
219}
220
221impl JSONHighlighter {
222 pub fn new() -> Self {
224 let mut h = RegexHighlighter::new();
225
226 let _ = h.add_rule(
228 r#""(?:[^"\\]|\\.)*"\s*:"#,
229 Style::new()
230 .color(crate::color::Color::parse("bright_cyan").unwrap()),
231 );
232
233 let _ = h.add_rule(
235 r#""(?:[^"\\]|\\.)*""#,
236 Style::new()
237 .color(crate::color::Color::parse("green").unwrap()),
238 );
239
240 let _ = h.add_rule(
242 r"(?<!\w)-?\d+\.?\d*(?:[eE][+-]?\d+)?(?!\w)",
243 Style::new()
244 .color(crate::color::Color::parse("bright_yellow").unwrap()),
245 );
246
247 let _ = h.add_rule(
249 r"\b(?:true|false|null)\b",
250 Style::new()
251 .color(crate::color::Color::parse("magenta").unwrap())
252 .bold(true),
253 );
254
255 let _ = h.add_rule(
257 r"[{}\[\]]",
258 Style::new()
259 .color(crate::color::Color::parse("white").unwrap())
260 .bold(true),
261 );
262
263 Self { highlighter: h }
264 }
265
266 pub fn highlight_str(&self, text: &str) -> Text {
268 let t = Text::new(text);
269 self.highlighter.highlight(&t)
270 }
271}
272
273impl Highlighter for JSONHighlighter {
274 fn highlight(&self, text: &Text) -> Text {
275 self.highlighter.highlight(text)
276 }
277}
278
279#[derive(Debug, Clone)]
286pub struct PathHighlighter {
287 highlighter: RegexHighlighter,
288}
289
290impl PathHighlighter {
291 pub fn new() -> Self {
293 let mut h = RegexHighlighter::new();
294
295 let _ = h.add_rule(
297 r"(?:\w:)?(?:[/\\][\w.\-]+)+(?:\.\w+)?(?::\d+(?::\d+)?)?",
298 Style::new()
299 .color(crate::color::Color::parse("bright_magenta").unwrap()),
300 );
301
302 Self { highlighter: h }
303 }
304
305 pub fn highlight_str(&self, text: &str) -> Text {
307 let t = Text::new(text);
308 self.highlighter.highlight(&t)
309 }
310}
311
312impl Highlighter for PathHighlighter {
313 fn highlight(&self, text: &Text) -> Text {
314 self.highlighter.highlight(text)
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn test_null_highlighter() {
324 let h = NullHighlighter;
325 let t = Text::new("hello");
326 let result = h.highlight(&t);
327 assert_eq!(result.plain, "hello");
328 }
329
330 #[test]
331 fn test_repr_highlighter_numbers() {
332 let h = ReprHighlighter::new();
333 let result = h.highlight_str("num=42");
334 assert!(!result.plain.is_empty());
337 }
338
339 #[test]
340 fn test_iso8601_highlighter() {
341 let h = ISO8601Highlighter::new();
342 let result = h.highlight_str("2024-01-15T10:30:00Z");
343 assert!(!result.plain.is_empty());
344 }
345
346 #[test]
347 fn test_json_highlighter() {
348 let h = JSONHighlighter::new();
349 let result = h.highlight_str(r#"{"key": "value", "num": 42, "flag": true}"#);
350 assert!(!result.plain.is_empty());
351 }
352
353 #[test]
354 fn test_path_highlighter() {
355 let h = PathHighlighter::new();
356 let result = h.highlight_str("src/main.rs:42");
357 assert!(!result.plain.is_empty());
358 }
359}