use std::fmt;
use rich::{Style, Text};
use rich_plugin_api::PluginError;
pub use rich_plugin_api::TextTransform;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TransformError {
message: String,
}
impl TransformError {
pub fn new(message: impl Into<String>) -> Self {
TransformError {
message: message.into(),
}
}
pub fn message(&self) -> &str {
&self.message
}
}
impl fmt::Display for TransformError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for TransformError {}
impl From<PluginError> for TransformError {
fn from(error: PluginError) -> Self {
TransformError::new(error.to_string())
}
}
pub trait Transform<T>: Send + Sync {
fn apply(&self, input: T) -> Result<T, TransformError>;
}
impl<X: TextTransform + ?Sized> Transform<Text> for X {
fn apply(&self, input: Text) -> Result<Text, TransformError> {
Ok(self.transform(input)?)
}
}
pub fn from_fn<T, F>(f: F) -> impl Transform<T>
where
F: Fn(T) -> Result<T, TransformError> + Send + Sync,
{
FromFn(f)
}
struct FromFn<F>(F);
impl<T, F> Transform<T> for FromFn<F>
where
F: Fn(T) -> Result<T, TransformError> + Send + Sync,
{
fn apply(&self, input: T) -> Result<T, TransformError> {
(self.0)(input)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PipelineError {
pub stage: String,
pub error: TransformError,
}
impl fmt::Display for PipelineError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.stage, self.error)
}
}
impl std::error::Error for PipelineError {}
pub struct Pipeline<T> {
stages: Vec<(String, Box<dyn Transform<T>>)>,
}
impl<T> Default for Pipeline<T> {
fn default() -> Self {
Pipeline { stages: Vec::new() }
}
}
impl<T> fmt::Debug for Pipeline<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Pipeline")
.field("stages", &self.names())
.finish()
}
}
impl<T> Pipeline<T> {
pub fn new() -> Self {
Self::default()
}
pub fn then(mut self, name: impl Into<String>, transform: impl Transform<T> + 'static) -> Self {
self.push(name, Box::new(transform));
self
}
pub fn push(&mut self, name: impl Into<String>, transform: Box<dyn Transform<T>>) {
self.stages.push((name.into(), transform));
}
pub fn names(&self) -> Vec<&str> {
self.stages.iter().map(|(name, _)| name.as_str()).collect()
}
pub fn len(&self) -> usize {
self.stages.len()
}
pub fn is_empty(&self) -> bool {
self.stages.is_empty()
}
pub fn apply(&self, input: T) -> Result<T, PipelineError> {
self.stages
.iter()
.try_fold(input, |value, (stage, transform)| {
transform.apply(value).map_err(|error| PipelineError {
stage: stage.clone(),
error,
})
})
}
}
impl<T> Transform<T> for Pipeline<T> {
fn apply(&self, input: T) -> Result<T, TransformError> {
Pipeline::apply(self, input).map_err(|error| TransformError::new(error.to_string()))
}
}
#[derive(Clone, Debug)]
pub struct KeepLines {
regex: fancy_regex::Regex,
invert: bool,
}
impl KeepLines {
pub fn new(pattern: &str) -> Result<Self, TransformError> {
let regex = fancy_regex::Regex::new(pattern)
.map_err(|e| TransformError::new(format!("invalid pattern {pattern:?}: {e}")))?;
Ok(KeepLines {
regex,
invert: false,
})
}
pub fn invert(mut self, invert: bool) -> Self {
self.invert = invert;
self
}
}
impl TextTransform for KeepLines {
fn transform(&self, text: Text) -> Result<Text, PluginError> {
if text.plain().is_empty() {
return Ok(text);
}
let mut kept = text.blank_copy();
for line in text.split("\n", true, false) {
let content = line.plain().strip_suffix('\n').unwrap_or(line.plain());
let matched = self
.regex
.is_match(content)
.map_err(|e| PluginError::Other(format!("filter failed: {e}")))?;
if matched != self.invert {
kept = kept.append_text(&line);
}
}
if !text.plain().ends_with('\n') && kept.plain().ends_with('\n') {
kept.right_crop(1);
}
Ok(kept)
}
}
#[derive(Clone, Debug)]
pub struct HighlightMatches {
regex: fancy_regex::Regex,
style: Style,
}
impl HighlightMatches {
pub fn new(pattern: &str, style: Style) -> Result<Self, TransformError> {
let regex = fancy_regex::Regex::new(pattern)
.map_err(|e| TransformError::new(format!("invalid pattern {pattern:?}: {e}")))?;
Ok(HighlightMatches { regex, style })
}
}
impl TextTransform for HighlightMatches {
fn transform(&self, mut text: Text) -> Result<Text, PluginError> {
let mut ranges = Vec::new();
for found in self.regex.find_iter(text.plain()) {
let found = found.map_err(|e| PluginError::Other(format!("highlight failed: {e}")))?;
ranges.push(found.range());
}
for range in ranges {
text.stylize(self.style.clone(), range.start, range.end);
}
Ok(text)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keep_lines_keeps_endings_and_styles() {
let mut text = Text::new("one\ntwo\nthree");
text.stylize(Style::parse("bold").unwrap(), 4, 7);
let kept = KeepLines::new("t").unwrap().transform(text).unwrap();
assert_eq!(kept.plain(), "two\nthree");
assert_eq!(kept.spans().len(), 1);
assert_eq!((kept.spans()[0].start, kept.spans()[0].end), (0, 3));
let dropped = KeepLines::new("t")
.unwrap()
.invert(true)
.transform(Text::new("one\ntwo\n"))
.unwrap();
assert_eq!(dropped.plain(), "one\n");
let first = KeepLines::new("a")
.unwrap()
.transform(Text::new("a\nb"))
.unwrap();
assert_eq!(first.plain(), "a");
assert_eq!(
KeepLines::new("x")
.unwrap()
.transform(Text::new("a\nb"))
.unwrap()
.plain(),
""
);
assert!(KeepLines::new("(").is_err());
}
#[test]
fn highlight_matches_styles_each_match() {
let text = HighlightMatches::new("o+", Style::parse("red").unwrap())
.unwrap()
.transform(Text::new("foo boo"))
.unwrap();
let ranges: Vec<_> = text.spans().iter().map(|s| (s.start, s.end)).collect();
assert_eq!(ranges, [(1, 3), (5, 7)]);
}
#[test]
fn highlight_matches_styles_whole_matches_only() {
let text = HighlightMatches::new("(?P<blink>ERR)OR", Style::parse("reverse").unwrap())
.unwrap()
.transform(Text::new("an ERROR"))
.unwrap();
assert_eq!(text.spans().len(), 1);
assert_eq!((text.spans()[0].start, text.spans()[0].end), (3, 8));
}
#[test]
fn highlight_matches_reports_the_backtrack_limit_like_keep_lines() {
let input = || Text::new("a".repeat(30));
let pattern = r"^(a|a)*\1b";
assert!(KeepLines::new(pattern).unwrap().transform(input()).is_err());
assert!(
HighlightMatches::new(pattern, Style::parse("reverse").unwrap())
.unwrap()
.transform(input())
.is_err()
);
}
#[test]
fn a_pipeline_runs_in_order_and_names_the_failing_stage() {
let pipeline: Pipeline<Vec<u8>> = Pipeline::new()
.then(
"push",
from_fn(|mut v: Vec<u8>| {
v.push(1);
Ok(v)
}),
)
.then(
"double",
from_fn(|v: Vec<u8>| Ok(v.into_iter().map(|x| x * 2).collect())),
);
assert_eq!(pipeline.apply(vec![3]).unwrap(), [6, 2]);
let failing = pipeline.then("fail", from_fn(|_: Vec<u8>| Err(TransformError::new("no"))));
let error = failing.apply(Vec::new()).unwrap_err();
assert_eq!(error.stage, "fail");
assert_eq!(error.to_string(), "fail: no");
}
}