markdown_ppp/ast_transform/
convenience.rs

1//! Convenience methods for common AST transformations
2//!
3//! This module provides high-level convenience methods for common transformation
4//! tasks. These methods are implemented as extension traits on AST types, making
5//! them easy to use in a fluent style.
6//!
7//! # Example
8//!
9//! ```rust
10//! use markdown_ppp::ast::*;
11//! use markdown_ppp::ast_transform::{Transform, FilterTransform};
12//!
13//! let doc = Document {
14//!     blocks: vec![Block::Paragraph(vec![Inline::Text("  hello  ".to_string())])],
15//! };
16//!
17//! let result = doc
18//!     .transform_text(|text| text.trim().to_string())
19//!     .normalize_whitespace()
20//!     .remove_empty_text();
21//! ```
22
23use super::transformer::Transformer;
24use crate::ast::*;
25
26/// High-level transformation methods for common use cases
27pub trait Transform {
28    /// Transform all text elements with a function
29    ///
30    /// # Example
31    ///
32    /// ```rust
33    /// use markdown_ppp::ast::*;
34    /// use markdown_ppp::ast_transform::Transform;
35    ///
36    /// let doc = Document {
37    ///     blocks: vec![Block::Paragraph(vec![Inline::Text("hello".to_string())])],
38    /// };
39    /// let result = doc.transform_text(|text| text.to_uppercase());
40    /// ```
41    fn transform_text<F>(self, f: F) -> Self
42    where
43        F: Fn(String) -> String;
44
45    /// Transform all image URLs with a function
46    ///
47    /// # Example
48    ///
49    /// ```rust
50    /// use markdown_ppp::ast::*;
51    /// use markdown_ppp::ast_transform::Transform;
52    ///
53    /// let doc = Document {
54    ///     blocks: vec![Block::Paragraph(vec![Inline::Image(Image {
55    ///         destination: "/image.jpg".to_string(),
56    ///         title: None,
57    ///         alt: "test".to_string(),
58    ///     })])],
59    /// };
60    /// let result = doc.transform_image_urls(|url| {
61    ///     format!("https://cdn.example.com{}", url)
62    /// });
63    /// ```
64    fn transform_image_urls<F>(self, f: F) -> Self
65    where
66        F: Fn(String) -> String;
67
68    /// Transform all link URLs with a function
69    ///
70    /// # Example
71    ///
72    /// ```rust
73    /// use markdown_ppp::ast::*;
74    /// use markdown_ppp::ast_transform::Transform;
75    ///
76    /// let doc = Document {
77    ///     blocks: vec![Block::Paragraph(vec![Inline::Link(Link {
78    ///         destination: "http://example.com".to_string(),
79    ///         title: None,
80    ///         children: vec![Inline::Text("link".to_string())],
81    ///     })])],
82    /// };
83    /// let result = doc.transform_link_urls(|url| {
84    ///     url.replace("http://", "https://")
85    /// });
86    /// ```
87    fn transform_link_urls<F>(self, f: F) -> Self
88    where
89        F: Fn(String) -> String;
90
91    /// Transform all autolink URLs with a function
92    fn transform_autolink_urls<F>(self, f: F) -> Self
93    where
94        F: Fn(String) -> String;
95
96    /// Transform all code spans with a function
97    fn transform_code<F>(self, f: F) -> Self
98    where
99        F: Fn(String) -> String;
100
101    /// Transform all HTML content with a function
102    fn transform_html<F>(self, f: F) -> Self
103    where
104        F: Fn(String) -> String;
105
106    /// Apply a custom transformer
107    fn transform_with<T: Transformer>(self, transformer: T) -> Self;
108
109    /// Transform conditionally based on a document predicate
110    fn transform_if_doc<P, F>(self, predicate: P, transform: F) -> Self
111    where
112        P: Fn(&Self) -> bool,
113        F: FnOnce(Self) -> Self,
114        Self: Sized;
115}
116
117impl Transform for Document {
118    fn transform_text<F>(self, f: F) -> Self
119    where
120        F: Fn(String) -> String,
121    {
122        let mut transformer = TextTransformer::new(f);
123        transformer.transform_document(self)
124    }
125
126    fn transform_image_urls<F>(self, f: F) -> Self
127    where
128        F: Fn(String) -> String,
129    {
130        let mut transformer = ImageUrlTransformer::new(f);
131        transformer.transform_document(self)
132    }
133
134    fn transform_link_urls<F>(self, f: F) -> Self
135    where
136        F: Fn(String) -> String,
137    {
138        let mut transformer = LinkUrlTransformer::new(f);
139        transformer.transform_document(self)
140    }
141
142    fn transform_autolink_urls<F>(self, f: F) -> Self
143    where
144        F: Fn(String) -> String,
145    {
146        let mut transformer = AutolinkTransformer::new(f);
147        transformer.transform_document(self)
148    }
149
150    fn transform_code<F>(self, f: F) -> Self
151    where
152        F: Fn(String) -> String,
153    {
154        let mut transformer = CodeTransformer::new(f);
155        transformer.transform_document(self)
156    }
157
158    fn transform_html<F>(self, f: F) -> Self
159    where
160        F: Fn(String) -> String,
161    {
162        let mut transformer = HtmlTransformer::new(f);
163        transformer.transform_document(self)
164    }
165
166    fn transform_with<T: Transformer>(self, mut transformer: T) -> Self {
167        transformer.transform_document(self)
168    }
169
170    fn transform_if_doc<P, F>(self, predicate: P, transform: F) -> Self
171    where
172        P: Fn(&Self) -> bool,
173        F: FnOnce(Self) -> Self,
174    {
175        if predicate(&self) {
176            transform(self)
177        } else {
178            self
179        }
180    }
181}
182
183// Internal transformer implementations
184
185struct TextTransformer<F> {
186    func: F,
187}
188
189impl<F> TextTransformer<F>
190where
191    F: Fn(String) -> String,
192{
193    fn new(func: F) -> Self {
194        Self { func }
195    }
196}
197
198impl<F> Transformer for TextTransformer<F>
199where
200    F: Fn(String) -> String,
201{
202    fn transform_inline(&mut self, inline: Inline) -> Inline {
203        match inline {
204            Inline::Text(text) => Inline::Text((self.func)(text)),
205            other => self.walk_transform_inline(other),
206        }
207    }
208}
209
210struct ImageUrlTransformer<F> {
211    func: F,
212}
213
214impl<F> ImageUrlTransformer<F>
215where
216    F: Fn(String) -> String,
217{
218    fn new(func: F) -> Self {
219        Self { func }
220    }
221}
222
223impl<F> Transformer for ImageUrlTransformer<F>
224where
225    F: Fn(String) -> String,
226{
227    fn transform_inline(&mut self, inline: Inline) -> Inline {
228        match inline {
229            Inline::Image(mut image) => {
230                image.destination = (self.func)(image.destination);
231                Inline::Image(image)
232            }
233            other => self.walk_transform_inline(other),
234        }
235    }
236}
237
238struct LinkUrlTransformer<F> {
239    func: F,
240}
241
242impl<F> LinkUrlTransformer<F>
243where
244    F: Fn(String) -> String,
245{
246    fn new(func: F) -> Self {
247        Self { func }
248    }
249}
250
251impl<F> Transformer for LinkUrlTransformer<F>
252where
253    F: Fn(String) -> String,
254{
255    fn transform_inline(&mut self, inline: Inline) -> Inline {
256        match inline {
257            Inline::Link(mut link) => {
258                link.destination = (self.func)(link.destination);
259                link.children = link
260                    .children
261                    .into_iter()
262                    .map(|child| self.transform_inline(child))
263                    .collect();
264                Inline::Link(link)
265            }
266            other => self.walk_transform_inline(other),
267        }
268    }
269}
270
271struct AutolinkTransformer<F> {
272    func: F,
273}
274
275impl<F> AutolinkTransformer<F>
276where
277    F: Fn(String) -> String,
278{
279    fn new(func: F) -> Self {
280        Self { func }
281    }
282}
283
284impl<F> Transformer for AutolinkTransformer<F>
285where
286    F: Fn(String) -> String,
287{
288    fn transform_inline(&mut self, inline: Inline) -> Inline {
289        match inline {
290            Inline::Autolink(url) => Inline::Autolink((self.func)(url)),
291            other => self.walk_transform_inline(other),
292        }
293    }
294}
295
296struct CodeTransformer<F> {
297    func: F,
298}
299
300impl<F> CodeTransformer<F>
301where
302    F: Fn(String) -> String,
303{
304    fn new(func: F) -> Self {
305        Self { func }
306    }
307}
308
309impl<F> Transformer for CodeTransformer<F>
310where
311    F: Fn(String) -> String,
312{
313    fn transform_inline(&mut self, inline: Inline) -> Inline {
314        match inline {
315            Inline::Code(code) => Inline::Code((self.func)(code)),
316            other => self.walk_transform_inline(other),
317        }
318    }
319}
320
321struct HtmlTransformer<F> {
322    func: F,
323}
324
325impl<F> HtmlTransformer<F>
326where
327    F: Fn(String) -> String,
328{
329    fn new(func: F) -> Self {
330        Self { func }
331    }
332}
333
334impl<F> Transformer for HtmlTransformer<F>
335where
336    F: Fn(String) -> String,
337{
338    fn transform_inline(&mut self, inline: Inline) -> Inline {
339        match inline {
340            Inline::Html(html) => Inline::Html((self.func)(html)),
341            other => self.walk_transform_inline(other),
342        }
343    }
344
345    fn transform_block(&mut self, block: Block) -> Block {
346        match block {
347            Block::HtmlBlock(html) => Block::HtmlBlock((self.func)(html)),
348            other => self.walk_transform_block(other),
349        }
350    }
351}
352
353/// Additional utility methods for filtering and common operations
354pub trait FilterTransform {
355    /// Remove empty paragraphs
356    fn remove_empty_paragraphs(self) -> Self;
357
358    /// Remove empty text elements
359    fn remove_empty_text(self) -> Self;
360
361    /// Normalize whitespace in text elements
362    fn normalize_whitespace(self) -> Self;
363
364    /// Remove specific block types
365    fn filter_blocks<F>(self, predicate: F) -> Self
366    where
367        F: Fn(&Block) -> bool;
368}
369
370impl FilterTransform for Document {
371    fn remove_empty_paragraphs(mut self) -> Self {
372        self.blocks
373            .retain(|block| !matches!(block, Block::Paragraph(inlines) if inlines.is_empty()));
374        self
375    }
376
377    fn remove_empty_text(self) -> Self {
378        let mut transformer = EmptyTextRemover;
379        transformer.transform_document(self)
380    }
381
382    fn normalize_whitespace(self) -> Self {
383        self.transform_text(|text| text.split_whitespace().collect::<Vec<_>>().join(" "))
384    }
385
386    fn filter_blocks<F>(mut self, predicate: F) -> Self
387    where
388        F: Fn(&Block) -> bool,
389    {
390        self.blocks.retain(|block| predicate(block));
391        self
392    }
393}
394
395struct EmptyTextRemover;
396
397impl Transformer for EmptyTextRemover {
398    fn transform_inline(&mut self, inline: Inline) -> Inline {
399        match inline {
400            Inline::Text(text) if text.trim().is_empty() => Inline::Empty,
401            other => self.walk_transform_inline(other),
402        }
403    }
404}