Skip to main content

marco_core/intelligence/markdown/
blocks.rs

1//! Block-level markdown grammar/parsing namespace for intelligence.
2//!
3//! Current implementation reuses parser/grammar blocks from `crate::parser`.
4
5use super::ast::{is_block_kind, Document, Node, NodeKind};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8/// Block-level semantic categories for AST nodes.
9pub enum BlockCategory {
10    /// Heading block.
11    Heading,
12    /// Paragraph block.
13    Paragraph,
14    /// Code block.
15    CodeBlock,
16    /// Thematic break block.
17    ThematicBreak,
18    /// List container block.
19    List,
20    /// List item block.
21    ListItem,
22    /// Definition list block.
23    DefinitionList,
24    /// Definition term block.
25    DefinitionTerm,
26    /// Definition description block.
27    DefinitionDescription,
28    /// Task checkbox block marker.
29    TaskCheckbox,
30    /// Blockquote block.
31    Blockquote,
32    /// Admonition block.
33    Admonition,
34    /// Tab group block.
35    TabGroup,
36    /// Tab item block.
37    TabItem,
38    /// Slider deck block.
39    SliderDeck,
40    /// Slide block.
41    Slide,
42    /// Table block.
43    Table,
44    /// Table row block.
45    TableRow,
46    /// Table cell block.
47    TableCell,
48    /// HTML block.
49    HtmlBlock,
50    /// Footnote definition block.
51    FootnoteDefinition,
52    /// Mermaid diagram block.
53    MermaidDiagram,
54}
55
56/// Classify a node kind as a block category when applicable.
57pub fn classify_block_kind(kind: &NodeKind) -> Option<BlockCategory> {
58    match kind {
59        NodeKind::Heading { .. } => Some(BlockCategory::Heading),
60        NodeKind::Paragraph => Some(BlockCategory::Paragraph),
61        NodeKind::CodeBlock { .. } => Some(BlockCategory::CodeBlock),
62        NodeKind::ThematicBreak => Some(BlockCategory::ThematicBreak),
63        NodeKind::List { .. } => Some(BlockCategory::List),
64        NodeKind::ListItem => Some(BlockCategory::ListItem),
65        NodeKind::DefinitionList => Some(BlockCategory::DefinitionList),
66        NodeKind::DefinitionTerm => Some(BlockCategory::DefinitionTerm),
67        NodeKind::DefinitionDescription => Some(BlockCategory::DefinitionDescription),
68        NodeKind::TaskCheckbox { .. } => Some(BlockCategory::TaskCheckbox),
69        NodeKind::Blockquote => Some(BlockCategory::Blockquote),
70        NodeKind::Admonition { .. } => Some(BlockCategory::Admonition),
71        NodeKind::TabGroup => Some(BlockCategory::TabGroup),
72        NodeKind::TabItem { .. } => Some(BlockCategory::TabItem),
73        NodeKind::SliderDeck { .. } => Some(BlockCategory::SliderDeck),
74        NodeKind::Slide { .. } => Some(BlockCategory::Slide),
75        NodeKind::Table { .. } => Some(BlockCategory::Table),
76        NodeKind::TableRow { .. } => Some(BlockCategory::TableRow),
77        NodeKind::TableCell { .. } => Some(BlockCategory::TableCell),
78        NodeKind::HtmlBlock { .. } => Some(BlockCategory::HtmlBlock),
79        NodeKind::FootnoteDefinition { .. } => Some(BlockCategory::FootnoteDefinition),
80        NodeKind::MermaidDiagram { .. } => Some(BlockCategory::MermaidDiagram),
81        _ => None,
82    }
83}
84
85/// Returns `true` when the node is considered block-level.
86pub fn is_block_node(node: &Node) -> bool {
87    is_block_kind(&node.kind)
88}
89
90/// Iterate top-level block nodes in document order.
91pub fn top_level_blocks(document: &Document) -> impl Iterator<Item = &Node> {
92    document.children.iter().filter(|node| is_block_node(node))
93}
94
95/// Collect all block nodes recursively in pre-order.
96pub fn collect_block_nodes<'a>(nodes: &'a [Node], out: &mut Vec<&'a Node>) {
97    for node in nodes {
98        if is_block_node(node) {
99            out.push(node);
100        }
101        if !node.children.is_empty() {
102            collect_block_nodes(&node.children, out);
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn smoke_test_classify_block_kind_basic() {
113        assert_eq!(
114            classify_block_kind(&NodeKind::Paragraph),
115            Some(BlockCategory::Paragraph)
116        );
117        assert_eq!(classify_block_kind(&NodeKind::Text("x".to_string())), None);
118    }
119
120    #[test]
121    fn smoke_test_collect_block_nodes_recursive() {
122        let doc = Document {
123            children: vec![Node {
124                kind: NodeKind::Paragraph,
125                span: None,
126                children: vec![Node {
127                    kind: NodeKind::Link {
128                        url: "https://example.com".to_string(),
129                        title: None,
130                    },
131                    span: None,
132                    children: vec![],
133                }],
134            }],
135            ..Default::default()
136        };
137
138        let mut blocks = Vec::new();
139        collect_block_nodes(&doc.children, &mut blocks);
140
141        assert_eq!(blocks.len(), 1);
142        assert!(matches!(blocks[0].kind, NodeKind::Paragraph));
143    }
144}