1use std::fmt;
31
32use rich::{Style, Text};
33use rich_plugin_api::PluginError;
34pub use rich_plugin_api::TextTransform;
35
36#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct TransformError {
39 message: String,
40}
41
42impl TransformError {
43 pub fn new(message: impl Into<String>) -> Self {
44 TransformError {
45 message: message.into(),
46 }
47 }
48
49 pub fn message(&self) -> &str {
50 &self.message
51 }
52}
53
54impl fmt::Display for TransformError {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 f.write_str(&self.message)
57 }
58}
59
60impl std::error::Error for TransformError {}
61
62impl From<PluginError> for TransformError {
63 fn from(error: PluginError) -> Self {
64 TransformError::new(error.to_string())
65 }
66}
67
68pub trait Transform<T>: Send + Sync {
70 fn apply(&self, input: T) -> Result<T, TransformError>;
71}
72
73impl<X: TextTransform + ?Sized> Transform<Text> for X {
74 fn apply(&self, input: Text) -> Result<Text, TransformError> {
75 Ok(self.transform(input)?)
76 }
77}
78
79pub fn from_fn<T, F>(f: F) -> impl Transform<T>
81where
82 F: Fn(T) -> Result<T, TransformError> + Send + Sync,
83{
84 FromFn(f)
85}
86
87struct FromFn<F>(F);
88
89impl<T, F> Transform<T> for FromFn<F>
90where
91 F: Fn(T) -> Result<T, TransformError> + Send + Sync,
92{
93 fn apply(&self, input: T) -> Result<T, TransformError> {
94 (self.0)(input)
95 }
96}
97
98#[derive(Clone, Debug, PartialEq, Eq)]
100pub struct PipelineError {
101 pub stage: String,
103 pub error: TransformError,
104}
105
106impl fmt::Display for PipelineError {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 write!(f, "{}: {}", self.stage, self.error)
109 }
110}
111
112impl std::error::Error for PipelineError {}
113
114pub struct Pipeline<T> {
116 stages: Vec<(String, Box<dyn Transform<T>>)>,
117}
118
119impl<T> Default for Pipeline<T> {
120 fn default() -> Self {
121 Pipeline { stages: Vec::new() }
122 }
123}
124
125impl<T> fmt::Debug for Pipeline<T> {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 f.debug_struct("Pipeline")
128 .field("stages", &self.names())
129 .finish()
130 }
131}
132
133impl<T> Pipeline<T> {
134 pub fn new() -> Self {
135 Self::default()
136 }
137
138 #[must_use]
140 pub fn then(mut self, name: impl Into<String>, transform: impl Transform<T> + 'static) -> Self {
141 self.push(name, Box::new(transform));
142 self
143 }
144
145 pub fn push(&mut self, name: impl Into<String>, transform: Box<dyn Transform<T>>) {
147 self.stages.push((name.into(), transform));
148 }
149
150 pub fn names(&self) -> Vec<&str> {
152 self.stages.iter().map(|(name, _)| name.as_str()).collect()
153 }
154
155 pub fn len(&self) -> usize {
156 self.stages.len()
157 }
158
159 pub fn is_empty(&self) -> bool {
160 self.stages.is_empty()
161 }
162
163 pub fn apply(&self, input: T) -> Result<T, PipelineError> {
165 self.stages
166 .iter()
167 .try_fold(input, |value, (stage, transform)| {
168 transform.apply(value).map_err(|error| PipelineError {
169 stage: stage.clone(),
170 error,
171 })
172 })
173 }
174}
175
176impl<T> Transform<T> for Pipeline<T> {
178 fn apply(&self, input: T) -> Result<T, TransformError> {
179 Pipeline::apply(self, input).map_err(|error| TransformError::new(error.to_string()))
180 }
181}
182
183#[derive(Clone, Debug)]
199pub struct Pattern {
200 regex: fancy_regex::Regex,
201}
202
203impl Pattern {
204 pub fn new(pattern: &str) -> Result<Self, TransformError> {
206 let regex = fancy_regex::Regex::new(pattern)
207 .map_err(|e| TransformError::new(format!("invalid pattern {pattern:?}: {e}")))?;
208 Ok(Pattern { regex })
209 }
210
211 pub fn is_match(&self, text: &str) -> bool {
215 self.regex.is_match(text).unwrap_or(false)
216 }
217
218 pub fn try_is_match(&self, text: &str) -> Result<bool, TransformError> {
220 self.regex
221 .is_match(text)
222 .map_err(|e| TransformError::new(format!("match failed: {e}")))
223 }
224
225 pub fn as_str(&self) -> &str {
227 self.regex.as_str()
228 }
229}
230
231#[derive(Clone, Debug)]
235pub struct KeepLines {
236 regex: fancy_regex::Regex,
237 invert: bool,
238}
239
240impl KeepLines {
241 pub fn new(pattern: &str) -> Result<Self, TransformError> {
243 let regex = fancy_regex::Regex::new(pattern)
244 .map_err(|e| TransformError::new(format!("invalid pattern {pattern:?}: {e}")))?;
245 Ok(KeepLines {
246 regex,
247 invert: false,
248 })
249 }
250
251 #[must_use]
253 pub fn invert(mut self, invert: bool) -> Self {
254 self.invert = invert;
255 self
256 }
257}
258
259impl TextTransform for KeepLines {
260 fn transform(&self, text: Text) -> Result<Text, PluginError> {
261 if text.plain().is_empty() {
262 return Ok(text);
263 }
264 let mut kept = text.blank_copy();
265 for line in text.split("\n", true, false) {
266 let content = line.plain().strip_suffix('\n').unwrap_or(line.plain());
267 let matched = self
268 .regex
269 .is_match(content)
270 .map_err(|e| PluginError::Other(format!("filter failed: {e}")))?;
271 if matched != self.invert {
272 kept = kept.append_text(&line);
273 }
274 }
275 if !text.plain().ends_with('\n') && kept.plain().ends_with('\n') {
277 kept.right_crop(1);
278 }
279 Ok(kept)
280 }
281}
282
283#[derive(Clone, Debug)]
289pub struct HighlightMatches {
290 regex: fancy_regex::Regex,
291 style: Style,
292}
293
294impl HighlightMatches {
295 pub fn new(pattern: &str, style: Style) -> Result<Self, TransformError> {
296 let regex = fancy_regex::Regex::new(pattern)
297 .map_err(|e| TransformError::new(format!("invalid pattern {pattern:?}: {e}")))?;
298 Ok(HighlightMatches { regex, style })
299 }
300}
301
302impl TextTransform for HighlightMatches {
303 fn transform(&self, mut text: Text) -> Result<Text, PluginError> {
304 let mut ranges = Vec::new();
305 for found in self.regex.find_iter(text.plain()) {
306 let found = found.map_err(|e| PluginError::Other(format!("highlight failed: {e}")))?;
307 ranges.push(found.range());
308 }
309 for range in ranges {
311 text.stylize(self.style.clone(), range.start, range.end);
312 }
313 Ok(text)
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn keep_lines_keeps_endings_and_styles() {
323 let mut text = Text::new("one\ntwo\nthree");
324 text.stylize(Style::parse("bold").unwrap(), 4, 7);
325 let kept = KeepLines::new("t").unwrap().transform(text).unwrap();
326 assert_eq!(kept.plain(), "two\nthree");
327 assert_eq!(kept.spans().len(), 1);
328 assert_eq!((kept.spans()[0].start, kept.spans()[0].end), (0, 3));
329
330 let dropped = KeepLines::new("t")
331 .unwrap()
332 .invert(true)
333 .transform(Text::new("one\ntwo\n"))
334 .unwrap();
335 assert_eq!(dropped.plain(), "one\n");
336 let first = KeepLines::new("a")
337 .unwrap()
338 .transform(Text::new("a\nb"))
339 .unwrap();
340 assert_eq!(first.plain(), "a");
341 assert_eq!(
342 KeepLines::new("x")
343 .unwrap()
344 .transform(Text::new("a\nb"))
345 .unwrap()
346 .plain(),
347 ""
348 );
349 assert!(KeepLines::new("(").is_err());
350 }
351
352 #[test]
353 fn a_pattern_matches_plain_text_as_keep_lines_does() {
354 let pattern = Pattern::new(r"(?<=a)b|^c").unwrap();
355 assert!(pattern.is_match("ab"));
356 assert!(pattern.is_match("cat"));
357 assert!(!pattern.is_match("b c"));
358 assert_eq!(pattern.try_is_match("xab"), Ok(true));
359 let err = Pattern::new("[").unwrap_err();
360 assert!(err.message().starts_with("invalid pattern"), "{err}");
361 }
362
363 #[test]
364 fn highlight_matches_styles_each_match() {
365 let text = HighlightMatches::new("o+", Style::parse("red").unwrap())
366 .unwrap()
367 .transform(Text::new("foo boo"))
368 .unwrap();
369 let ranges: Vec<_> = text.spans().iter().map(|s| (s.start, s.end)).collect();
370 assert_eq!(ranges, [(1, 3), (5, 7)]);
371 }
372
373 #[test]
374 fn highlight_matches_styles_whole_matches_only() {
375 let text = HighlightMatches::new("(?P<blink>ERR)OR", Style::parse("reverse").unwrap())
377 .unwrap()
378 .transform(Text::new("an ERROR"))
379 .unwrap();
380 assert_eq!(text.spans().len(), 1);
381 assert_eq!((text.spans()[0].start, text.spans()[0].end), (3, 8));
382 }
383
384 #[test]
385 fn highlight_matches_reports_the_backtrack_limit_like_keep_lines() {
386 let input = || Text::new("a".repeat(30));
387 let pattern = r"^(a|a)*\1b";
388 assert!(KeepLines::new(pattern).unwrap().transform(input()).is_err());
389 assert!(
390 HighlightMatches::new(pattern, Style::parse("reverse").unwrap())
391 .unwrap()
392 .transform(input())
393 .is_err()
394 );
395 }
396
397 #[test]
398 fn a_pipeline_runs_in_order_and_names_the_failing_stage() {
399 let pipeline: Pipeline<Vec<u8>> = Pipeline::new()
400 .then(
401 "push",
402 from_fn(|mut v: Vec<u8>| {
403 v.push(1);
404 Ok(v)
405 }),
406 )
407 .then(
408 "double",
409 from_fn(|v: Vec<u8>| Ok(v.into_iter().map(|x| x * 2).collect())),
410 );
411 assert_eq!(pipeline.apply(vec![3]).unwrap(), [6, 2]);
412
413 let failing = pipeline.then("fail", from_fn(|_: Vec<u8>| Err(TransformError::new("no"))));
414 let error = failing.apply(Vec::new()).unwrap_err();
415 assert_eq!(error.stage, "fail");
416 assert_eq!(error.to_string(), "fail: no");
417 }
418}