marco-core 1.1.0

nom-based Markdown parser, HTML renderer, and intelligence features (highlights, diagnostics, completions) for the Marco editor.
Documentation
//! Extended tab-block parser - converts grammar output to AST nodes.
//!
//! Converts the grammar-level tab block into:
//! - `NodeKind::TabGroup`
//! - `NodeKind::TabItem { title }`
//!
//! Each tab panel's raw markdown is recursively parsed as block nodes.

use super::shared::{opt_span, opt_span_range, GrammarSpan};
use crate::grammar::blocks::marco_tab_blocks::MarcoTabBlock;
use crate::parser::ast::{Document, Node, NodeKind};

/// Parse an extended tab block into an AST node.
///
/// # Arguments
/// * `block` - Grammar output for the full tab container
/// * `full_start` / `full_end` - Spans covering the entire matched container
/// * `depth` - Recursion depth
/// * `parse_blocks_fn` - Parser callback for parsing each tab panel body
pub fn parse_marco_tab_block<F>(
    block: MarcoTabBlock<'_>,
    full_start: GrammarSpan<'_>,
    full_end: GrammarSpan<'_>,
    depth: usize,
    mut parse_blocks_fn: F,
) -> Result<Node, Box<dyn std::error::Error>>
where
    F: FnMut(&str, usize) -> Result<Document, Box<dyn std::error::Error>>,
{
    let group_span = opt_span_range(full_start, full_end);

    let mut group = Node {
        kind: NodeKind::TabGroup,
        span: group_span,
        children: Vec::new(),
    };

    for item in block.items {
        let body = item.content.fragment();

        let panel_doc = match parse_blocks_fn(body, depth + 1) {
            Ok(doc) => doc,
            Err(e) => {
                log::warn!("Failed to parse tab panel content: {}", e);
                Document::new()
            }
        };

        group.children.push(Node {
            kind: NodeKind::TabItem { title: item.title },
            // The grammar currently only provides a precise span for the panel body.
            span: opt_span(item.content),
            children: panel_doc.children,
        });
    }

    Ok(group)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::grammar::shared::Span;

    fn mock_parse_blocks(
        input: &str,
        _depth: usize,
    ) -> Result<Document, Box<dyn std::error::Error>> {
        let mut doc = Document::new();
        if !input.trim().is_empty() {
            doc.children.push(Node {
                kind: NodeKind::Text(input.to_string()),
                span: None,
                children: Vec::new(),
            });
        }
        Ok(doc)
    }

    #[test]
    fn smoke_test_parse_marco_tab_block_builds_ast() {
        let raw = ":::tab\n@tab One\nHello\n@tab Two\nWorld\n:::\n";
        let span = Span::new(raw);
        let (rest, block) = crate::grammar::blocks::marco_tab_blocks::marco_tab_block(span)
            .expect("grammar parse failed");

        let node = parse_marco_tab_block(block, Span::new(raw), rest, 0, mock_parse_blocks)
            .expect("parser failed");

        assert!(matches!(node.kind, NodeKind::TabGroup));
        assert_eq!(node.children.len(), 2);
        assert!(matches!(node.children[0].kind, NodeKind::TabItem { .. }));
    }
}