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        self.steps.push(Box::new(move |doc| {
64            crate::ast_transform::Transform::transform_text(doc, f)
65        }));
66        self
67    }
68
69    /// Transform all image URLs
70    pub fn transform_image_urls<F>(mut self, f: F) -> Self
71    where
72        F: Fn(String) -> String + 'static,
73    {
74        self.steps.push(Box::new(move |doc| {
75            crate::ast_transform::Transform::transform_image_urls(doc, f)
76        }));
77        self
78    }
79
80    /// Transform all link URLs
81    pub fn transform_link_urls<F>(mut self, f: F) -> Self
82    where
83        F: Fn(String) -> String + 'static,
84    {
85        self.steps.push(Box::new(move |doc| {
86            crate::ast_transform::Transform::transform_link_urls(doc, f)
87        }));
88        self
89    }
90
91    /// Transform all autolink URLs
92    pub fn transform_autolink_urls<F>(mut self, f: F) -> Self
93    where
94        F: Fn(String) -> String + 'static,
95    {
96        self.steps.push(Box::new(move |doc| {
97            crate::ast_transform::Transform::transform_autolink_urls(doc, f)
98        }));
99        self
100    }
101
102    /// Transform all code spans
103    pub fn transform_code<F>(mut self, f: F) -> Self
104    where
105        F: Fn(String) -> String + 'static,
106    {
107        self.steps.push(Box::new(move |doc| {
108            crate::ast_transform::Transform::transform_code(doc, f)
109        }));
110        self
111    }
112
113    /// Transform all HTML content
114    pub fn transform_html<F>(mut self, f: F) -> Self
115    where
116        F: Fn(String) -> String + 'static,
117    {
118        self.steps.push(Box::new(move |doc| {
119            crate::ast_transform::Transform::transform_html(doc, f)
120        }));
121        self
122    }
123
124    /// Apply a custom transformer
125    pub fn transform_with<T: Transformer + 'static>(mut self, transformer: T) -> Self {
126        self.steps.push(Box::new(move |doc| {
127            crate::ast_transform::Transform::transform_with(doc, transformer)
128        }));
129        self
130    }
131
132    /// Add a custom transformation function
133    pub fn custom<F>(mut self, f: F) -> Self
134    where
135        F: FnOnce(Document) -> Document + 'static,
136    {
137        self.steps.push(Box::new(f));
138        self
139    }
140
141    /// Conditionally apply a sub-pipeline
142    pub fn when<F>(mut self, condition: bool, builder: F) -> Self
143    where
144        F: FnOnce(TransformPipeline) -> TransformPipeline,
145    {
146        if condition {
147            let sub_pipeline = builder(TransformPipeline::new());
148            self.steps
149                .push(Box::new(move |doc| sub_pipeline.apply(doc)));
150        }
151        self
152    }
153
154    /// Apply transformations only if the document matches a predicate
155    pub fn when_doc<P, F>(mut self, predicate: P, builder: F) -> Self
156    where
157        P: Fn(&Document) -> bool + 'static,
158        F: FnOnce(TransformPipeline) -> TransformPipeline + 'static,
159    {
160        self.steps.push(Box::new(move |doc| {
161            if predicate(&doc) {
162                let sub_pipeline = builder(TransformPipeline::new());
163                sub_pipeline.apply(doc)
164            } else {
165                doc
166            }
167        }));
168        self
169    }
170
171    /// Remove empty paragraphs
172    pub fn remove_empty_paragraphs(mut self) -> Self {
173        self.steps.push(Box::new(|doc| {
174            crate::ast_transform::FilterTransform::remove_empty_paragraphs(doc)
175        }));
176        self
177    }
178
179    /// Remove empty text elements
180    pub fn remove_empty_text(mut self) -> Self {
181        self.steps.push(Box::new(|doc| {
182            crate::ast_transform::FilterTransform::remove_empty_text(doc)
183        }));
184        self
185    }
186
187    /// Normalize whitespace
188    pub fn normalize_whitespace(mut self) -> Self {
189        self.steps.push(Box::new(|doc| {
190            crate::ast_transform::FilterTransform::normalize_whitespace(doc)
191        }));
192        self
193    }
194
195    /// Filter blocks by predicate
196    pub fn filter_blocks<F>(mut self, predicate: F) -> Self
197    where
198        F: Fn(&Block) -> bool + 'static,
199    {
200        self.steps.push(Box::new(move |doc| {
201            crate::ast_transform::FilterTransform::filter_blocks(doc, predicate)
202        }));
203        self
204    }
205
206    /// Apply all transformations in the pipeline
207    pub fn apply(self, mut doc: Document) -> Document {
208        for step in self.steps {
209            doc = step(doc);
210        }
211        doc
212    }
213}
214
215impl Default for TransformPipeline {
216    fn default() -> Self {
217        Self::new()
218    }
219}
220
221/// Functional composition helpers
222pub trait PipeExt {
223    /// Apply a function to self (functional pipe operator)
224    fn pipe<F, R>(self, f: F) -> R
225    where
226        F: FnOnce(Self) -> R,
227        Self: Sized,
228    {
229        f(self)
230    }
231
232    /// Compose two functions
233    fn compose<F, G, R>(self, f: F, g: G) -> R
234    where
235        F: FnOnce(Self) -> R,
236        G: FnOnce(R) -> R,
237        Self: Sized,
238    {
239        g(f(self))
240    }
241}
242
243impl PipeExt for Document {}
244
245/// Macro for creating transformation pipelines with a more functional syntax
246///
247/// # Example
248///
249/// ```rust
250/// use markdown_ppp::ast::*;
251/// use markdown_ppp::ast_transform::*;
252/// use markdown_ppp::pipeline;
253///
254/// let original_doc = Document {
255///     blocks: vec![Block::Paragraph(vec![Inline::Text("  hello  ".to_string())])],
256/// };
257///
258/// let result = pipeline! {
259///     original_doc =>
260///     |d: Document| d.transform_text(|s| s.trim().to_string()),
261///     |d: Document| d.normalize_whitespace(),
262/// };
263/// ```
264#[macro_export]
265macro_rules! pipeline {
266    ($doc:expr => $($transform:expr),* $(,)?) => {{
267        let mut doc = $doc;
268        $(
269            doc = $transform(doc);
270        )*
271        doc
272    }};
273}
274
275pub use pipeline;