markdown_ppp/ast_transform/
pipeline.rs

1//! Pipeline builder for composing transformations
2//!
3//! This module provides the TransformPipeline builder for chaining multiple
4//! transformations together. Pipelines support conditional logic, custom
5//! transformations, and functional composition patterns.
6//!
7//! # Example
8//!
9//! ```rust
10//! use markdown_ppp::ast::*;
11//! use markdown_ppp::ast_transform::TransformPipeline;
12//!
13//! let doc = Document {
14//!     blocks: vec![Block::Paragraph(vec![Inline::Text("  hello  ".to_string())])],
15//! };
16//!
17//! let result = TransformPipeline::new()
18//!     .transform_text(|s| s.trim().to_string())
19//!     .normalize_whitespace()
20//!     .when(true, |pipeline| {
21//!         pipeline.transform_text(|s| s.to_uppercase())
22//!     })
23//!     .apply(doc);
24//! ```
25
26use super::transformer::Transformer;
27use crate::ast::*;
28
29/// Builder for creating transformation pipelines
30///
31/// Allows chaining multiple transformations together with conditional logic.
32///
33/// # Example
34///
35/// ```rust
36/// use markdown_ppp::ast::*;
37/// use markdown_ppp::ast_transform::TransformPipeline;
38///
39/// let doc = Document {
40///     blocks: vec![Block::Paragraph(vec![Inline::Text("  hello  ".to_string())])],
41/// };
42///
43/// let result = TransformPipeline::new()
44///     .transform_text(|s| s.trim().to_string())
45///     .transform_image_urls(|url| format!("https://cdn.example.com{}", url))
46///     .apply(doc);
47/// ```
48pub struct TransformPipeline {
49    steps: Vec<Box<dyn FnOnce(Document) -> Document>>,
50}
51
52impl TransformPipeline {
53    /// Create a new empty pipeline
54    pub fn new() -> Self {
55        Self { steps: Vec::new() }
56    }
57
58    /// Transform all text elements
59    pub fn transform_text<F>(mut self, f: F) -> Self
60    where
61        F: Fn(String) -> String + 'static,
62    {
63        use crate::ast_transform::Transform;
64        self.steps.push(Box::new(move |doc| doc.transform_text(f)));
65        self
66    }
67
68    /// Transform all image URLs
69    pub fn transform_image_urls<F>(mut self, f: F) -> Self
70    where
71        F: Fn(String) -> String + 'static,
72    {
73        use crate::ast_transform::Transform;
74        self.steps
75            .push(Box::new(move |doc| doc.transform_image_urls(f)));
76        self
77    }
78
79    /// Transform all link URLs
80    pub fn transform_link_urls<F>(mut self, f: F) -> Self
81    where
82        F: Fn(String) -> String + 'static,
83    {
84        use crate::ast_transform::Transform;
85        self.steps
86            .push(Box::new(move |doc| doc.transform_link_urls(f)));
87        self
88    }
89
90    /// Transform all autolink URLs
91    pub fn transform_autolink_urls<F>(mut self, f: F) -> Self
92    where
93        F: Fn(String) -> String + 'static,
94    {
95        use crate::ast_transform::Transform;
96        self.steps
97            .push(Box::new(move |doc| doc.transform_autolink_urls(f)));
98        self
99    }
100
101    /// Transform all code spans
102    pub fn transform_code<F>(mut self, f: F) -> Self
103    where
104        F: Fn(String) -> String + 'static,
105    {
106        use crate::ast_transform::Transform;
107        self.steps.push(Box::new(move |doc| doc.transform_code(f)));
108        self
109    }
110
111    /// Transform all HTML content
112    pub fn transform_html<F>(mut self, f: F) -> Self
113    where
114        F: Fn(String) -> String + 'static,
115    {
116        use crate::ast_transform::Transform;
117        self.steps.push(Box::new(move |doc| doc.transform_html(f)));
118        self
119    }
120
121    /// Apply a custom transformer
122    pub fn transform_with<T: Transformer + 'static>(mut self, transformer: T) -> Self {
123        use crate::ast_transform::Transform;
124        self.steps
125            .push(Box::new(move |doc| doc.transform_with(transformer)));
126        self
127    }
128
129    /// Add a custom transformation function
130    pub fn custom<F>(mut self, f: F) -> Self
131    where
132        F: FnOnce(Document) -> Document + 'static,
133    {
134        self.steps.push(Box::new(f));
135        self
136    }
137
138    /// Conditionally apply a sub-pipeline
139    pub fn when<F>(mut self, condition: bool, builder: F) -> Self
140    where
141        F: FnOnce(TransformPipeline) -> TransformPipeline,
142    {
143        if condition {
144            let sub_pipeline = builder(TransformPipeline::new());
145            self.steps
146                .push(Box::new(move |doc| sub_pipeline.apply(doc)));
147        }
148        self
149    }
150
151    /// Apply transformations only if the document matches a predicate
152    pub fn when_doc<P, F>(mut self, predicate: P, builder: F) -> Self
153    where
154        P: Fn(&Document) -> bool + 'static,
155        F: FnOnce(TransformPipeline) -> TransformPipeline + 'static,
156    {
157        self.steps.push(Box::new(move |doc| {
158            if predicate(&doc) {
159                let sub_pipeline = builder(TransformPipeline::new());
160                sub_pipeline.apply(doc)
161            } else {
162                doc
163            }
164        }));
165        self
166    }
167
168    /// Remove empty paragraphs
169    pub fn remove_empty_paragraphs(mut self) -> Self {
170        use crate::ast_transform::FilterTransform;
171        self.steps
172            .push(Box::new(|doc| doc.remove_empty_paragraphs()));
173        self
174    }
175
176    /// Remove empty text elements
177    pub fn remove_empty_text(mut self) -> Self {
178        use crate::ast_transform::FilterTransform;
179        self.steps.push(Box::new(|doc| doc.remove_empty_text()));
180        self
181    }
182
183    /// Normalize whitespace
184    pub fn normalize_whitespace(mut self) -> Self {
185        use crate::ast_transform::FilterTransform;
186        self.steps.push(Box::new(|doc| doc.normalize_whitespace()));
187        self
188    }
189
190    /// Filter blocks by predicate
191    pub fn filter_blocks<F>(mut self, predicate: F) -> Self
192    where
193        F: Fn(&Block) -> bool + 'static,
194    {
195        use crate::ast_transform::FilterTransform;
196        self.steps
197            .push(Box::new(move |doc| doc.filter_blocks(predicate)));
198        self
199    }
200
201    /// Apply all transformations in the pipeline
202    pub fn apply(self, mut doc: Document) -> Document {
203        for step in self.steps {
204            doc = step(doc);
205        }
206        doc
207    }
208}
209
210impl Default for TransformPipeline {
211    fn default() -> Self {
212        Self::new()
213    }
214}
215
216/// Functional composition helpers
217pub trait PipeExt {
218    /// Apply a function to self (functional pipe operator)
219    fn pipe<F, R>(self, f: F) -> R
220    where
221        F: FnOnce(Self) -> R,
222        Self: Sized,
223    {
224        f(self)
225    }
226}
227
228impl PipeExt for Document {}