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    /// Find all links in the document
90    fn find_all_links(&self) -> Vec<&Link> {
91        self.find_all_inlines(|inline| matches!(inline, Inline::Link(_)))
92            .into_iter()
93            .filter_map(|inline| match inline {
94                Inline::Link(link) => Some(link),
95                _ => None,
96            })
97            .collect()
98    }
99
100    /// Find all images in the document
101    fn find_all_images(&self) -> Vec<&Image> {
102        self.find_all_inlines(|inline| matches!(inline, Inline::Image(_)))
103            .into_iter()
104            .filter_map(|inline| match inline {
105                Inline::Image(image) => Some(image),
106                _ => None,
107            })
108            .collect()
109    }
110
111    /// Find all headings in the document
112    fn find_all_headings(&self) -> Vec<&Heading> {
113        self.find_all_blocks(|block| matches!(block, Block::Heading(_)))
114            .into_iter()
115            .filter_map(|block| match block {
116                Block::Heading(heading) => Some(heading),
117                _ => None,
118            })
119            .collect()
120    }
121
122    /// Find all autolinks in the document
123    fn find_all_autolinks(&self) -> Vec<&str> {
124        self.find_all_inlines(|inline| matches!(inline, Inline::Autolink(_)))
125            .into_iter()
126            .filter_map(|inline| match inline {
127                Inline::Autolink(url) => Some(url.as_str()),
128                _ => None,
129            })
130            .collect()
131    }
132
133    /// Find all text nodes in the document
134    fn find_all_text(&self) -> Vec<&str> {
135        self.find_all_inlines(|inline| matches!(inline, Inline::Text(_)))
136            .into_iter()
137            .filter_map(|inline| match inline {
138                Inline::Text(text) => Some(text.as_str()),
139                _ => None,
140            })
141            .collect()
142    }
143
144    /// Find all code spans in the document
145    fn find_all_code_spans(&self) -> Vec<&str> {
146        self.find_all_inlines(|inline| matches!(inline, Inline::Code(_)))
147            .into_iter()
148            .filter_map(|inline| match inline {
149                Inline::Code(code) => Some(code.as_str()),
150                _ => None,
151            })
152            .collect()
153    }
154
155    /// Find all code blocks in the document
156    fn find_all_code_blocks(&self) -> Vec<&CodeBlock> {
157        self.find_all_blocks(|block| matches!(block, Block::CodeBlock(_)))
158            .into_iter()
159            .filter_map(|block| match block {
160                Block::CodeBlock(code_block) => Some(code_block),
161                _ => None,
162            })
163            .collect()
164    }
165
166    /// Find all tables in the document
167    fn find_all_tables(&self) -> Vec<&Table> {
168        self.find_all_blocks(|block| matches!(block, Block::Table(_)))
169            .into_iter()
170            .filter_map(|block| match block {
171                Block::Table(table) => Some(table),
172                _ => None,
173            })
174            .collect()
175    }
176
177    /// Find all lists in the document
178    fn find_all_lists(&self) -> Vec<&List> {
179        self.find_all_blocks(|block| matches!(block, Block::List(_)))
180            .into_iter()
181            .filter_map(|block| match block {
182                Block::List(list) => Some(list),
183                _ => None,
184            })
185            .collect()
186    }
187}
188
189impl Query for Document {
190    fn find_all_inlines<F>(&self, predicate: F) -> Vec<&Inline>
191    where
192        F: Fn(&Inline) -> bool,
193    {
194        let mut results = Vec::new();
195        for block in &self.blocks {
196            results.extend(block.find_all_inlines(&predicate));
197        }
198        results
199    }
200
201    fn find_all_blocks<F>(&self, predicate: F) -> Vec<&Block>
202    where
203        F: Fn(&Block) -> bool,
204    {
205        let mut results = Vec::new();
206        for block in &self.blocks {
207            results.extend(block.find_all_blocks(&predicate));
208        }
209        results
210    }
211
212    fn find_first_inline<F>(&self, predicate: F) -> Option<&Inline>
213    where
214        F: Fn(&Inline) -> bool,
215    {
216        for block in &self.blocks {
217            if let Some(inline) = block.find_first_inline(&predicate) {
218                return Some(inline);
219            }
220        }
221        None
222    }
223
224    fn find_first_block<F>(&self, predicate: F) -> Option<&Block>
225    where
226        F: Fn(&Block) -> bool,
227    {
228        for block in &self.blocks {
229            if let Some(found) = block.find_first_block(&predicate) {
230                return Some(found);
231            }
232        }
233        None
234    }
235}
236
237impl Query for Block {
238    fn find_all_inlines<F>(&self, predicate: F) -> Vec<&Inline>
239    where
240        F: Fn(&Inline) -> bool,
241    {
242        let mut results = Vec::new();
243        collect_inlines_from_block(self, &predicate, &mut results);
244        results
245    }
246
247    fn find_all_blocks<F>(&self, predicate: F) -> Vec<&Block>
248    where
249        F: Fn(&Block) -> bool,
250    {
251        let mut results = Vec::new();
252        collect_blocks_from_block(self, &predicate, &mut results);
253        results
254    }
255
256    fn find_first_inline<F>(&self, predicate: F) -> Option<&Inline>
257    where
258        F: Fn(&Inline) -> bool,
259    {
260        find_first_inline_in_block(self, &predicate)
261    }
262
263    fn find_first_block<F>(&self, predicate: F) -> Option<&Block>
264    where
265        F: Fn(&Block) -> bool,
266    {
267        find_first_block_in_block(self, &predicate)
268    }
269}
270
271impl Query for Vec<Inline> {
272    fn find_all_inlines<F>(&self, predicate: F) -> Vec<&Inline>
273    where
274        F: Fn(&Inline) -> bool,
275    {
276        let mut results = Vec::new();
277        for inline in self {
278            collect_inlines_from_inline(inline, &predicate, &mut results);
279        }
280        results
281    }
282
283    fn find_all_blocks<F>(&self, _predicate: F) -> Vec<&Block>
284    where
285        F: Fn(&Block) -> bool,
286    {
287        Vec::new() // Inline elements don't contain blocks
288    }
289
290    fn find_first_inline<F>(&self, predicate: F) -> Option<&Inline>
291    where
292        F: Fn(&Inline) -> bool,
293    {
294        for inline in self {
295            if let Some(found) = find_first_inline_in_inline(inline, &predicate) {
296                return Some(found);
297            }
298        }
299        None
300    }
301
302    fn find_first_block<F>(&self, _predicate: F) -> Option<&Block>
303    where
304        F: Fn(&Block) -> bool,
305    {
306        None // Inline elements don't contain blocks
307    }
308}
309
310// Helper functions for recursive collection
311
312fn collect_inlines_from_block<'a, F>(block: &'a Block, predicate: &F, results: &mut Vec<&'a Inline>)
313where
314    F: Fn(&Inline) -> bool,
315{
316    match block {
317        Block::Paragraph(inlines) => {
318            for inline in inlines {
319                collect_inlines_from_inline(inline, predicate, results);
320            }
321        }
322        Block::Heading(heading) => {
323            for inline in &heading.content {
324                collect_inlines_from_inline(inline, predicate, results);
325            }
326        }
327        Block::BlockQuote(blocks) => {
328            for block in blocks {
329                collect_inlines_from_block(block, predicate, results);
330            }
331        }
332        Block::List(list) => {
333            for item in &list.items {
334                for block in &item.blocks {
335                    collect_inlines_from_block(block, predicate, results);
336                }
337            }
338        }
339        Block::Table(table) => {
340            for row in &table.rows {
341                for cell in row {
342                    for inline in cell {
343                        collect_inlines_from_inline(inline, predicate, results);
344                    }
345                }
346            }
347        }
348        Block::FootnoteDefinition(footnote) => {
349            for block in &footnote.blocks {
350                collect_inlines_from_block(block, predicate, results);
351            }
352        }
353        Block::GitHubAlert(alert) => {
354            for block in &alert.blocks {
355                collect_inlines_from_block(block, predicate, results);
356            }
357        }
358        Block::Definition(def) => {
359            for inline in &def.label {
360                collect_inlines_from_inline(inline, predicate, results);
361            }
362        }
363        _ => {} // Terminal blocks
364    }
365}
366
367fn collect_inlines_from_inline<'a, F>(
368    inline: &'a Inline,
369    predicate: &F,
370    results: &mut Vec<&'a Inline>,
371) where
372    F: Fn(&Inline) -> bool,
373{
374    if predicate(inline) {
375        results.push(inline);
376    }
377
378    match inline {
379        Inline::Emphasis(inlines) | Inline::Strong(inlines) | Inline::Strikethrough(inlines) => {
380            for inline in inlines {
381                collect_inlines_from_inline(inline, predicate, results);
382            }
383        }
384        Inline::Link(link) => {
385            for inline in &link.children {
386                collect_inlines_from_inline(inline, predicate, results);
387            }
388        }
389        Inline::LinkReference(link_ref) => {
390            for inline in &link_ref.label {
391                collect_inlines_from_inline(inline, predicate, results);
392            }
393            for inline in &link_ref.text {
394                collect_inlines_from_inline(inline, predicate, results);
395            }
396        }
397        _ => {} // Terminal inlines
398    }
399}
400
401fn collect_blocks_from_block<'a, F>(block: &'a Block, predicate: &F, results: &mut Vec<&'a Block>)
402where
403    F: Fn(&Block) -> bool,
404{
405    if predicate(block) {
406        results.push(block);
407    }
408
409    match block {
410        Block::BlockQuote(blocks) => {
411            for block in blocks {
412                collect_blocks_from_block(block, predicate, results);
413            }
414        }
415        Block::List(list) => {
416            for item in &list.items {
417                for block in &item.blocks {
418                    collect_blocks_from_block(block, predicate, results);
419                }
420            }
421        }
422        Block::FootnoteDefinition(footnote) => {
423            for block in &footnote.blocks {
424                collect_blocks_from_block(block, predicate, results);
425            }
426        }
427        Block::GitHubAlert(alert) => {
428            for block in &alert.blocks {
429                collect_blocks_from_block(block, predicate, results);
430            }
431        }
432        _ => {} // Terminal or inline-containing blocks
433    }
434}
435
436fn find_first_inline_in_block<'a, F>(block: &'a Block, predicate: &F) -> Option<&'a Inline>
437where
438    F: Fn(&Inline) -> bool,
439{
440    match block {
441        Block::Paragraph(inlines) => {
442            for inline in inlines {
443                if let Some(found) = find_first_inline_in_inline(inline, predicate) {
444                    return Some(found);
445                }
446            }
447        }
448        Block::Heading(heading) => {
449            for inline in &heading.content {
450                if let Some(found) = find_first_inline_in_inline(inline, predicate) {
451                    return Some(found);
452                }
453            }
454        }
455        Block::BlockQuote(blocks) => {
456            for block in blocks {
457                if let Some(found) = find_first_inline_in_block(block, predicate) {
458                    return Some(found);
459                }
460            }
461        }
462        Block::List(list) => {
463            for item in &list.items {
464                for block in &item.blocks {
465                    if let Some(found) = find_first_inline_in_block(block, predicate) {
466                        return Some(found);
467                    }
468                }
469            }
470        }
471        Block::Table(table) => {
472            for row in &table.rows {
473                for cell in row {
474                    for inline in cell {
475                        if let Some(found) = find_first_inline_in_inline(inline, predicate) {
476                            return Some(found);
477                        }
478                    }
479                }
480            }
481        }
482        Block::FootnoteDefinition(footnote) => {
483            for block in &footnote.blocks {
484                if let Some(found) = find_first_inline_in_block(block, predicate) {
485                    return Some(found);
486                }
487            }
488        }
489        Block::GitHubAlert(alert) => {
490            for block in &alert.blocks {
491                if let Some(found) = find_first_inline_in_block(block, predicate) {
492                    return Some(found);
493                }
494            }
495        }
496        Block::Definition(def) => {
497            for inline in &def.label {
498                if let Some(found) = find_first_inline_in_inline(inline, predicate) {
499                    return Some(found);
500                }
501            }
502        }
503        _ => {} // Terminal blocks
504    }
505    None
506}
507
508fn find_first_inline_in_inline<'a, F>(inline: &'a Inline, predicate: &F) -> Option<&'a Inline>
509where
510    F: Fn(&Inline) -> bool,
511{
512    if predicate(inline) {
513        return Some(inline);
514    }
515
516    match inline {
517        Inline::Emphasis(inlines) | Inline::Strong(inlines) | Inline::Strikethrough(inlines) => {
518            for inline in inlines {
519                if let Some(found) = find_first_inline_in_inline(inline, predicate) {
520                    return Some(found);
521                }
522            }
523        }
524        Inline::Link(link) => {
525            for inline in &link.children {
526                if let Some(found) = find_first_inline_in_inline(inline, predicate) {
527                    return Some(found);
528                }
529            }
530        }
531        Inline::LinkReference(link_ref) => {
532            for inline in &link_ref.label {
533                if let Some(found) = find_first_inline_in_inline(inline, predicate) {
534                    return Some(found);
535                }
536            }
537            for inline in &link_ref.text {
538                if let Some(found) = find_first_inline_in_inline(inline, predicate) {
539                    return Some(found);
540                }
541            }
542        }
543        _ => {} // Terminal inlines
544    }
545    None
546}
547
548fn find_first_block_in_block<'a, F>(block: &'a Block, predicate: &F) -> Option<&'a Block>
549where
550    F: Fn(&Block) -> bool,
551{
552    if predicate(block) {
553        return Some(block);
554    }
555
556    match block {
557        Block::BlockQuote(blocks) => {
558            for block in blocks {
559                if let Some(found) = find_first_block_in_block(block, predicate) {
560                    return Some(found);
561                }
562            }
563        }
564        Block::List(list) => {
565            for item in &list.items {
566                for block in &item.blocks {
567                    if let Some(found) = find_first_block_in_block(block, predicate) {
568                        return Some(found);
569                    }
570                }
571            }
572        }
573        Block::FootnoteDefinition(footnote) => {
574            for block in &footnote.blocks {
575                if let Some(found) = find_first_block_in_block(block, predicate) {
576                    return Some(found);
577                }
578            }
579        }
580        Block::GitHubAlert(alert) => {
581            for block in &alert.blocks {
582                if let Some(found) = find_first_block_in_block(block, predicate) {
583                    return Some(found);
584                }
585            }
586        }
587        _ => {} // Terminal or inline-containing blocks
588    }
589    None
590}