markdown_ppp/ast_transform/
query.rs

1//! Query API for finding elements in AST
2//!
3//! This module provides query methods for finding elements in the AST that match certain conditions.
4//!
5//! # Example
6//!
7//! ```rust
8//! use markdown_ppp::ast::*;
9//! use markdown_ppp::ast_transform::Query;
10//!
11//! let doc = Document {
12//!     blocks: vec![
13//!         Block::Paragraph(vec![
14//!             Inline::Text("hello".to_string()),
15//!             Inline::Autolink("https://example.com".to_string()),
16//!         ]),
17//!     ],
18//! };
19//!
20//! // Find all autolinks
21//! let autolinks = doc.find_all_inlines(|inline| {
22//!     matches!(inline, Inline::Autolink(_))
23//! });
24//! assert_eq!(autolinks.len(), 1);
25//!
26//! // Count text nodes
27//! let text_count = doc.count_inlines(|inline| {
28//!     matches!(inline, Inline::Text(_))
29//! });
30//! assert_eq!(text_count, 1);
31//! ```
32
33use crate::ast::*;
34
35/// Query trait for finding elements in AST structures
36pub trait Query {
37    /// Find all inline elements matching a predicate
38    fn find_all_inlines<F>(&self, predicate: F) -> Vec<&Inline>
39    where
40        F: Fn(&Inline) -> bool;
41
42    /// Find all block elements matching a predicate
43    fn find_all_blocks<F>(&self, predicate: F) -> Vec<&Block>
44    where
45        F: Fn(&Block) -> bool;
46
47    /// Find the first inline element matching a predicate
48    fn find_first_inline<F>(&self, predicate: F) -> Option<&Inline>
49    where
50        F: Fn(&Inline) -> bool;
51
52    /// Find the first block element matching a predicate
53    fn find_first_block<F>(&self, predicate: F) -> Option<&Block>
54    where
55        F: Fn(&Block) -> bool;
56
57    /// Count inline elements matching a predicate
58    fn count_inlines<F>(&self, predicate: F) -> usize
59    where
60        F: Fn(&Inline) -> bool,
61    {
62        self.find_all_inlines(predicate).len()
63    }
64
65    /// Count block elements matching a predicate
66    fn count_blocks<F>(&self, predicate: F) -> usize
67    where
68        F: Fn(&Block) -> bool,
69    {
70        self.find_all_blocks(predicate).len()
71    }
72
73    /// Check if any inline element matches a predicate
74    fn any_inline<F>(&self, predicate: F) -> bool
75    where
76        F: Fn(&Inline) -> bool,
77    {
78        self.find_first_inline(predicate).is_some()
79    }
80
81    /// Check if any block element matches a predicate
82    fn any_block<F>(&self, predicate: F) -> bool
83    where
84        F: Fn(&Block) -> bool,
85    {
86        self.find_first_block(predicate).is_some()
87    }
88}
89
90impl Query for Document {
91    fn find_all_inlines<F>(&self, predicate: F) -> Vec<&Inline>
92    where
93        F: Fn(&Inline) -> bool,
94    {
95        let mut results = Vec::new();
96        for block in &self.blocks {
97            results.extend(block.find_all_inlines(&predicate));
98        }
99        results
100    }
101
102    fn find_all_blocks<F>(&self, predicate: F) -> Vec<&Block>
103    where
104        F: Fn(&Block) -> bool,
105    {
106        let mut results = Vec::new();
107        for block in &self.blocks {
108            results.extend(block.find_all_blocks(&predicate));
109        }
110        results
111    }
112
113    fn find_first_inline<F>(&self, predicate: F) -> Option<&Inline>
114    where
115        F: Fn(&Inline) -> bool,
116    {
117        for block in &self.blocks {
118            if let Some(inline) = block.find_first_inline(&predicate) {
119                return Some(inline);
120            }
121        }
122        None
123    }
124
125    fn find_first_block<F>(&self, predicate: F) -> Option<&Block>
126    where
127        F: Fn(&Block) -> bool,
128    {
129        for block in &self.blocks {
130            if let Some(found) = block.find_first_block(&predicate) {
131                return Some(found);
132            }
133        }
134        None
135    }
136}
137
138impl Query for Block {
139    fn find_all_inlines<F>(&self, predicate: F) -> Vec<&Inline>
140    where
141        F: Fn(&Inline) -> bool,
142    {
143        let mut results = Vec::new();
144        collect_inlines_from_block(self, &predicate, &mut results);
145        results
146    }
147
148    fn find_all_blocks<F>(&self, predicate: F) -> Vec<&Block>
149    where
150        F: Fn(&Block) -> bool,
151    {
152        let mut results = Vec::new();
153        collect_blocks_from_block(self, &predicate, &mut results);
154        results
155    }
156
157    fn find_first_inline<F>(&self, predicate: F) -> Option<&Inline>
158    where
159        F: Fn(&Inline) -> bool,
160    {
161        find_first_inline_in_block(self, &predicate)
162    }
163
164    fn find_first_block<F>(&self, predicate: F) -> Option<&Block>
165    where
166        F: Fn(&Block) -> bool,
167    {
168        find_first_block_in_block(self, &predicate)
169    }
170}
171
172impl Query for Vec<Inline> {
173    fn find_all_inlines<F>(&self, predicate: F) -> Vec<&Inline>
174    where
175        F: Fn(&Inline) -> bool,
176    {
177        let mut results = Vec::new();
178        for inline in self {
179            collect_inlines_from_inline(inline, &predicate, &mut results);
180        }
181        results
182    }
183
184    fn find_all_blocks<F>(&self, _predicate: F) -> Vec<&Block>
185    where
186        F: Fn(&Block) -> bool,
187    {
188        Vec::new() // Inline elements don't contain blocks
189    }
190
191    fn find_first_inline<F>(&self, predicate: F) -> Option<&Inline>
192    where
193        F: Fn(&Inline) -> bool,
194    {
195        for inline in self {
196            if let Some(found) = find_first_inline_in_inline(inline, &predicate) {
197                return Some(found);
198            }
199        }
200        None
201    }
202
203    fn find_first_block<F>(&self, _predicate: F) -> Option<&Block>
204    where
205        F: Fn(&Block) -> bool,
206    {
207        None // Inline elements don't contain blocks
208    }
209}
210
211// Helper functions for recursive collection
212
213fn collect_inlines_from_block<'a, F>(block: &'a Block, predicate: &F, results: &mut Vec<&'a Inline>)
214where
215    F: Fn(&Inline) -> bool,
216{
217    match block {
218        Block::Paragraph(inlines) => {
219            for inline in inlines {
220                collect_inlines_from_inline(inline, predicate, results);
221            }
222        }
223        Block::Heading(heading) => {
224            for inline in &heading.content {
225                collect_inlines_from_inline(inline, predicate, results);
226            }
227        }
228        Block::BlockQuote(blocks) => {
229            for block in blocks {
230                collect_inlines_from_block(block, predicate, results);
231            }
232        }
233        Block::List(list) => {
234            for item in &list.items {
235                for block in &item.blocks {
236                    collect_inlines_from_block(block, predicate, results);
237                }
238            }
239        }
240        Block::Table(table) => {
241            for row in &table.rows {
242                for cell in row {
243                    for inline in cell {
244                        collect_inlines_from_inline(inline, predicate, results);
245                    }
246                }
247            }
248        }
249        Block::FootnoteDefinition(footnote) => {
250            for block in &footnote.blocks {
251                collect_inlines_from_block(block, predicate, results);
252            }
253        }
254        Block::GitHubAlert(alert) => {
255            for block in &alert.blocks {
256                collect_inlines_from_block(block, predicate, results);
257            }
258        }
259        Block::Definition(def) => {
260            for inline in &def.label {
261                collect_inlines_from_inline(inline, predicate, results);
262            }
263        }
264        _ => {} // Terminal blocks
265    }
266}
267
268fn collect_inlines_from_inline<'a, F>(
269    inline: &'a Inline,
270    predicate: &F,
271    results: &mut Vec<&'a Inline>,
272) where
273    F: Fn(&Inline) -> bool,
274{
275    if predicate(inline) {
276        results.push(inline);
277    }
278
279    match inline {
280        Inline::Emphasis(inlines) | Inline::Strong(inlines) | Inline::Strikethrough(inlines) => {
281            for inline in inlines {
282                collect_inlines_from_inline(inline, predicate, results);
283            }
284        }
285        Inline::Link(link) => {
286            for inline in &link.children {
287                collect_inlines_from_inline(inline, predicate, results);
288            }
289        }
290        Inline::LinkReference(link_ref) => {
291            for inline in &link_ref.label {
292                collect_inlines_from_inline(inline, predicate, results);
293            }
294            for inline in &link_ref.text {
295                collect_inlines_from_inline(inline, predicate, results);
296            }
297        }
298        _ => {} // Terminal inlines
299    }
300}
301
302fn collect_blocks_from_block<'a, F>(block: &'a Block, predicate: &F, results: &mut Vec<&'a Block>)
303where
304    F: Fn(&Block) -> bool,
305{
306    if predicate(block) {
307        results.push(block);
308    }
309
310    match block {
311        Block::BlockQuote(blocks) => {
312            for block in blocks {
313                collect_blocks_from_block(block, predicate, results);
314            }
315        }
316        Block::List(list) => {
317            for item in &list.items {
318                for block in &item.blocks {
319                    collect_blocks_from_block(block, predicate, results);
320                }
321            }
322        }
323        Block::FootnoteDefinition(footnote) => {
324            for block in &footnote.blocks {
325                collect_blocks_from_block(block, predicate, results);
326            }
327        }
328        Block::GitHubAlert(alert) => {
329            for block in &alert.blocks {
330                collect_blocks_from_block(block, predicate, results);
331            }
332        }
333        _ => {} // Terminal or inline-containing blocks
334    }
335}
336
337fn find_first_inline_in_block<'a, F>(block: &'a Block, predicate: &F) -> Option<&'a Inline>
338where
339    F: Fn(&Inline) -> bool,
340{
341    match block {
342        Block::Paragraph(inlines) => {
343            for inline in inlines {
344                if let Some(found) = find_first_inline_in_inline(inline, predicate) {
345                    return Some(found);
346                }
347            }
348        }
349        Block::Heading(heading) => {
350            for inline in &heading.content {
351                if let Some(found) = find_first_inline_in_inline(inline, predicate) {
352                    return Some(found);
353                }
354            }
355        }
356        Block::BlockQuote(blocks) => {
357            for block in blocks {
358                if let Some(found) = find_first_inline_in_block(block, predicate) {
359                    return Some(found);
360                }
361            }
362        }
363        Block::List(list) => {
364            for item in &list.items {
365                for block in &item.blocks {
366                    if let Some(found) = find_first_inline_in_block(block, predicate) {
367                        return Some(found);
368                    }
369                }
370            }
371        }
372        Block::Table(table) => {
373            for row in &table.rows {
374                for cell in row {
375                    for inline in cell {
376                        if let Some(found) = find_first_inline_in_inline(inline, predicate) {
377                            return Some(found);
378                        }
379                    }
380                }
381            }
382        }
383        Block::FootnoteDefinition(footnote) => {
384            for block in &footnote.blocks {
385                if let Some(found) = find_first_inline_in_block(block, predicate) {
386                    return Some(found);
387                }
388            }
389        }
390        Block::GitHubAlert(alert) => {
391            for block in &alert.blocks {
392                if let Some(found) = find_first_inline_in_block(block, predicate) {
393                    return Some(found);
394                }
395            }
396        }
397        Block::Definition(def) => {
398            for inline in &def.label {
399                if let Some(found) = find_first_inline_in_inline(inline, predicate) {
400                    return Some(found);
401                }
402            }
403        }
404        _ => {} // Terminal blocks
405    }
406    None
407}
408
409fn find_first_inline_in_inline<'a, F>(inline: &'a Inline, predicate: &F) -> Option<&'a Inline>
410where
411    F: Fn(&Inline) -> bool,
412{
413    if predicate(inline) {
414        return Some(inline);
415    }
416
417    match inline {
418        Inline::Emphasis(inlines) | Inline::Strong(inlines) | Inline::Strikethrough(inlines) => {
419            for inline in inlines {
420                if let Some(found) = find_first_inline_in_inline(inline, predicate) {
421                    return Some(found);
422                }
423            }
424        }
425        Inline::Link(link) => {
426            for inline in &link.children {
427                if let Some(found) = find_first_inline_in_inline(inline, predicate) {
428                    return Some(found);
429                }
430            }
431        }
432        Inline::LinkReference(link_ref) => {
433            for inline in &link_ref.label {
434                if let Some(found) = find_first_inline_in_inline(inline, predicate) {
435                    return Some(found);
436                }
437            }
438            for inline in &link_ref.text {
439                if let Some(found) = find_first_inline_in_inline(inline, predicate) {
440                    return Some(found);
441                }
442            }
443        }
444        _ => {} // Terminal inlines
445    }
446    None
447}
448
449fn find_first_block_in_block<'a, F>(block: &'a Block, predicate: &F) -> Option<&'a Block>
450where
451    F: Fn(&Block) -> bool,
452{
453    if predicate(block) {
454        return Some(block);
455    }
456
457    match block {
458        Block::BlockQuote(blocks) => {
459            for block in blocks {
460                if let Some(found) = find_first_block_in_block(block, predicate) {
461                    return Some(found);
462                }
463            }
464        }
465        Block::List(list) => {
466            for item in &list.items {
467                for block in &item.blocks {
468                    if let Some(found) = find_first_block_in_block(block, predicate) {
469                        return Some(found);
470                    }
471                }
472            }
473        }
474        Block::FootnoteDefinition(footnote) => {
475            for block in &footnote.blocks {
476                if let Some(found) = find_first_block_in_block(block, predicate) {
477                    return Some(found);
478                }
479            }
480        }
481        Block::GitHubAlert(alert) => {
482            for block in &alert.blocks {
483                if let Some(found) = find_first_block_in_block(block, predicate) {
484                    return Some(found);
485                }
486            }
487        }
488        _ => {} // Terminal or inline-containing blocks
489    }
490    None
491}