Skip to main content

oak_markdown/builder/
mod.rs

1use crate::{
2    ast::*,
3    language::MarkdownLanguage,
4    parser::{MarkdownParser, element_type::MarkdownElementType},
5};
6use oak_core::{Builder, BuilderCache, ElementType, GreenNode, OakError, Parser, RedNode, RedTree, SourceText, TextEdit, UniversalElementRole, source::Source};
7
8/// AST builder for the Markdown language.
9#[derive(Clone)]
10pub struct MarkdownBuilder<'config> {
11    /// Language configuration.
12    config: &'config MarkdownLanguage,
13}
14
15#[allow(unused)]
16impl<'config> MarkdownBuilder<'config> {
17    /// Creates a new MarkdownBuilder with the given configuration.
18    pub fn new(config: &'config MarkdownLanguage) -> Self {
19        Self { config }
20    }
21
22    /// Builds the AST root node from the green tree.
23    fn build_root(&self, green_tree: &GreenNode<MarkdownLanguage>, source: &SourceText) -> Result<MarkdownRoot, OakError> {
24        let red_root = RedNode::new(green_tree, 0);
25
26        let mut blocks = Vec::new();
27        for child in red_root.children() {
28            if let RedTree::Node(node) = child {
29                if let Some(block) = self.build_block(node, source) {
30                    blocks.push(block)
31                }
32            }
33        }
34
35        Ok(MarkdownRoot { blocks })
36    }
37
38    /// Builds block-level elements.
39    fn build_block(&self, node: RedNode<MarkdownLanguage>, source: &SourceText) -> Option<Block> {
40        let role = node.kind::<MarkdownElementType>().role();
41        match role {
42            UniversalElementRole::Container => {
43                // Determine if it's a list, quote, or other container
44                None
45            }
46            UniversalElementRole::Statement => {
47                // Handle paragraphs, headings, etc.
48                None
49            }
50            _ => None,
51        }
52    }
53
54    /// Builds inline-level elements.
55    fn build_inline(&self, node: RedNode<MarkdownLanguage>, source: &SourceText) -> Option<Inline> {
56        None
57    }
58
59    fn build_list_item(&self, node: RedNode<MarkdownLanguage>, source: &SourceText) -> crate::ast::ListItem {
60        let mut content = Vec::new();
61        for child in node.children() {
62            if let RedTree::Node(child_node) = child {
63                if let Some(block) = self.build_block(child_node, source) {
64                    content.push(block)
65                }
66            }
67        }
68
69        // If no nested blocks but has text content, wrap it as a paragraph
70        if content.is_empty() {
71            let text = source.get_text_in(node.span()).to_string();
72            if !text.trim().is_empty() {
73                // Simple cleanup: remove possible list marker prefixes
74                let display_text = if text.starts_with("- ") || text.starts_with("* ") {
75                    text[2..].to_string()
76                }
77                else if text.len() > 3 && text.chars().next().unwrap().is_ascii_digit() && text.contains(". ") {
78                    // Handle ordered list markers like "1. "
79                    if let Some(pos) = text.find(". ") { text[pos + 2..].to_string() } else { text }
80                }
81                else {
82                    text
83                };
84
85                content.push(crate::ast::Block::Paragraph(crate::ast::Paragraph { content: display_text.trim().to_string(), span: node.span() }))
86            }
87        }
88
89        crate::ast::ListItem { content, is_task: false, is_checked: None, span: node.span() }
90    }
91}
92
93impl<'config> Builder<MarkdownLanguage> for MarkdownBuilder<'config> {
94    fn build<'a, S: Source + ?Sized>(&self, source: &S, edits: &[TextEdit], _cache: &'a mut impl BuilderCache<MarkdownLanguage>) -> oak_core::builder::BuildOutput<MarkdownLanguage> {
95        let parser = MarkdownParser::new(self.config);
96        let mut parse_session = oak_core::parser::session::ParseSession::<MarkdownLanguage>::default();
97        let parse_result = parser.parse(source, edits, &mut parse_session);
98
99        match parse_result.result {
100            Ok(green_tree) => {
101                let source_text = SourceText::new(source.get_text_in((0..source.length()).into()).into_owned());
102                match self.build_root(green_tree, &source_text) {
103                    Ok(ast_root) => oak_core::OakDiagnostics { result: Ok(ast_root), diagnostics: parse_result.diagnostics },
104                    Err(build_error) => {
105                        let mut diagnostics = parse_result.diagnostics;
106                        diagnostics.push(build_error.clone());
107                        oak_core::OakDiagnostics { result: Err(build_error), diagnostics }
108                    }
109                }
110            }
111            Err(parse_error) => oak_core::OakDiagnostics { result: Err(parse_error), diagnostics: parse_result.diagnostics },
112        }
113    }
114}