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 pub fn then(mut self, name: impl Into<String>, transform: impl Transform<T> + 'static) -> Self {
140 self.push(name, Box::new(transform));
141 self
142 }
143
144 pub fn push(&mut self, name: impl Into<String>, transform: Box<dyn Transform<T>>) {
146 self.stages.push((name.into(), transform));
147 }
148
149 pub fn names(&self) -> Vec<&str> {
151 self.stages.iter().map(|(name, _)| name.as_str()).collect()
152 }
153
154 pub fn len(&self) -> usize {
155 self.stages.len()
156 }
157
158 pub fn is_empty(&self) -> bool {
159 self.stages.is_empty()
160 }
161
162 pub fn apply(&self, input: T) -> Result<T, PipelineError> {
164 self.stages
165 .iter()
166 .try_fold(input, |value, (stage, transform)| {
167 transform.apply(value).map_err(|error| PipelineError {
168 stage: stage.clone(),
169 error,
170 })
171 })
172 }
173}
174
175impl<T> Transform<T> for Pipeline<T> {
177 fn apply(&self, input: T) -> Result<T, TransformError> {
178 Pipeline::apply(self, input).map_err(|error| TransformError::new(error.to_string()))
179 }
180}
181
182#[derive(Clone, Debug)]
186pub struct KeepLines {
187 regex: fancy_regex::Regex,
188 invert: bool,
189}
190
191impl KeepLines {
192 pub fn new(pattern: &str) -> Result<Self, TransformError> {
194 let regex = fancy_regex::Regex::new(pattern)
195 .map_err(|e| TransformError::new(format!("invalid pattern {pattern:?}: {e}")))?;
196 Ok(KeepLines {
197 regex,
198 invert: false,
199 })
200 }
201
202 pub fn invert(mut self, invert: bool) -> Self {
204 self.invert = invert;
205 self
206 }
207}
208
209impl TextTransform for KeepLines {
210 fn transform(&self, text: Text) -> Result<Text, PluginError> {
211 if text.plain().is_empty() {
212 return Ok(text);
213 }
214 let mut kept = text.blank_copy();
215 for line in text.split("\n", true, false) {
216 let content = line.plain().strip_suffix('\n').unwrap_or(line.plain());
217 let matched = self
218 .regex
219 .is_match(content)
220 .map_err(|e| PluginError::Other(format!("filter failed: {e}")))?;
221 if matched != self.invert {
222 kept = kept.append_text(&line);
223 }
224 }
225 if !text.plain().ends_with('\n') && kept.plain().ends_with('\n') {
227 kept.right_crop(1);
228 }
229 Ok(kept)
230 }
231}
232
233#[derive(Clone, Debug)]
239pub struct HighlightMatches {
240 regex: fancy_regex::Regex,
241 style: Style,
242}
243
244impl HighlightMatches {
245 pub fn new(pattern: &str, style: Style) -> Result<Self, TransformError> {
246 let regex = fancy_regex::Regex::new(pattern)
247 .map_err(|e| TransformError::new(format!("invalid pattern {pattern:?}: {e}")))?;
248 Ok(HighlightMatches { regex, style })
249 }
250}
251
252impl TextTransform for HighlightMatches {
253 fn transform(&self, mut text: Text) -> Result<Text, PluginError> {
254 let mut ranges = Vec::new();
255 for found in self.regex.find_iter(text.plain()) {
256 let found = found.map_err(|e| PluginError::Other(format!("highlight failed: {e}")))?;
257 ranges.push(found.range());
258 }
259 for range in ranges {
261 text.stylize(self.style.clone(), range.start, range.end);
262 }
263 Ok(text)
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn keep_lines_keeps_endings_and_styles() {
273 let mut text = Text::new("one\ntwo\nthree");
274 text.stylize(Style::parse("bold").unwrap(), 4, 7);
275 let kept = KeepLines::new("t").unwrap().transform(text).unwrap();
276 assert_eq!(kept.plain(), "two\nthree");
277 assert_eq!(kept.spans().len(), 1);
278 assert_eq!((kept.spans()[0].start, kept.spans()[0].end), (0, 3));
279
280 let dropped = KeepLines::new("t")
281 .unwrap()
282 .invert(true)
283 .transform(Text::new("one\ntwo\n"))
284 .unwrap();
285 assert_eq!(dropped.plain(), "one\n");
286 let first = KeepLines::new("a")
287 .unwrap()
288 .transform(Text::new("a\nb"))
289 .unwrap();
290 assert_eq!(first.plain(), "a");
291 assert_eq!(
292 KeepLines::new("x")
293 .unwrap()
294 .transform(Text::new("a\nb"))
295 .unwrap()
296 .plain(),
297 ""
298 );
299 assert!(KeepLines::new("(").is_err());
300 }
301
302 #[test]
303 fn highlight_matches_styles_each_match() {
304 let text = HighlightMatches::new("o+", Style::parse("red").unwrap())
305 .unwrap()
306 .transform(Text::new("foo boo"))
307 .unwrap();
308 let ranges: Vec<_> = text.spans().iter().map(|s| (s.start, s.end)).collect();
309 assert_eq!(ranges, [(1, 3), (5, 7)]);
310 }
311
312 #[test]
313 fn highlight_matches_styles_whole_matches_only() {
314 let text = HighlightMatches::new("(?P<blink>ERR)OR", Style::parse("reverse").unwrap())
316 .unwrap()
317 .transform(Text::new("an ERROR"))
318 .unwrap();
319 assert_eq!(text.spans().len(), 1);
320 assert_eq!((text.spans()[0].start, text.spans()[0].end), (3, 8));
321 }
322
323 #[test]
324 fn highlight_matches_reports_the_backtrack_limit_like_keep_lines() {
325 let input = || Text::new("a".repeat(30));
326 let pattern = r"^(a|a)*\1b";
327 assert!(KeepLines::new(pattern).unwrap().transform(input()).is_err());
328 assert!(
329 HighlightMatches::new(pattern, Style::parse("reverse").unwrap())
330 .unwrap()
331 .transform(input())
332 .is_err()
333 );
334 }
335
336 #[test]
337 fn a_pipeline_runs_in_order_and_names_the_failing_stage() {
338 let pipeline: Pipeline<Vec<u8>> = Pipeline::new()
339 .then(
340 "push",
341 from_fn(|mut v: Vec<u8>| {
342 v.push(1);
343 Ok(v)
344 }),
345 )
346 .then(
347 "double",
348 from_fn(|v: Vec<u8>| Ok(v.into_iter().map(|x| x * 2).collect())),
349 );
350 assert_eq!(pipeline.apply(vec![3]).unwrap(), [6, 2]);
351
352 let failing = pipeline.then("fail", from_fn(|_: Vec<u8>| Err(TransformError::new("no"))));
353 let error = failing.apply(Vec::new()).unwrap_err();
354 assert_eq!(error.stage, "fail");
355 assert_eq!(error.to_string(), "fail: no");
356 }
357}