oak_markdown/builder/
mod.rs1use crate::{ast::*, language::MarkdownLanguage, parser::MarkdownParser};
2use oak_core::{Builder, BuilderCache, GreenNode, OakError, Parser, RedNode, RedTree, SourceText, TextEdit, source::Source};
3
4#[derive(Clone)]
6pub struct MarkdownBuilder<'config> {
7 config: &'config MarkdownLanguage,
9}
10
11impl<'config> MarkdownBuilder<'config> {
12 pub fn new(config: &'config MarkdownLanguage) -> Self {
14 Self { config }
15 }
16
17 fn build_root(&self, green_tree: &GreenNode<MarkdownLanguage>, source: &SourceText) -> Result<MarkdownRoot, OakError> {
19 let red_root = RedNode::new(green_tree, 0);
20
21 let mut blocks = Vec::new();
22 for child in red_root.children() {
23 if let RedTree::Node(node) = child {
24 if let Some(block) = self.build_block(node, source) {
25 blocks.push(block);
26 }
27 }
28 }
29
30 Ok(MarkdownRoot { blocks })
31 }
32
33 fn build_block(&self, node: RedNode<MarkdownLanguage>, source: &SourceText) -> Option<Block> {
35 use crate::kind::MarkdownSyntaxKind::*;
36
37 let kind = node.green.kind;
38 match kind {
39 Heading1 | Heading2 | Heading3 | Heading4 | Heading5 | Heading6 => {
40 let level = match kind {
41 Heading1 => 1,
42 Heading2 => 2,
43 Heading3 => 3,
44 Heading4 => 4,
45 Heading5 => 5,
46 Heading6 => 6,
47 _ => unreachable!(),
48 };
49 let text = source.get_text_in(node.span());
50 let content = text.trim_start_matches('#').trim_start().to_string();
51 Some(Block::Heading(crate::ast::Heading { level, content, span: node.span() }))
52 }
53 Paragraph => Some(Block::Paragraph(crate::ast::Paragraph { content: source.get_text_in(node.span()).to_string(), span: node.span() })),
54 CodeBlock => {
55 let mut language = None;
56 let mut content = String::new();
57
58 for child in node.children() {
59 match child {
60 RedTree::Leaf(leaf) => {
61 if leaf.kind == CodeLanguage {
62 language = Some(source.get_text_in(leaf.span).trim().to_string());
63 }
64 else if leaf.kind != CodeFence {
65 content.push_str(&source.get_text_in(leaf.span));
66 }
67 }
68 RedTree::Node(child_node) => {
69 for sub_child in child_node.children() {
71 if let RedTree::Leaf(sub_leaf) = sub_child {
72 if sub_leaf.kind == CodeLanguage {
73 language = Some(source.get_text_in(sub_leaf.span).trim().to_string());
74 }
75 else if sub_leaf.kind != CodeFence {
76 content.push_str(&source.get_text_in(sub_leaf.span));
77 }
78 }
79 else if let RedTree::Node(sub_node) = sub_child {
80 content.push_str(&source.get_text_in(sub_node.span()));
81 }
82 }
83 }
84 }
85 }
86
87 Some(Block::CodeBlock(crate::ast::CodeBlock { language, content: content.trim().to_string(), span: node.span() }))
88 }
89 UnorderedList | OrderedList => {
90 let mut items = Vec::new();
91 for child in node.children() {
92 if let RedTree::Node(child_node) = child {
93 if child_node.green.kind == ListItem {
94 items.push(self.build_list_item(child_node, source));
95 }
96 }
97 }
98 Some(Block::List(crate::ast::List { is_ordered: kind == OrderedList, items, span: node.span() }))
99 }
100 Blockquote => {
101 let mut content_text = String::new();
102 for child in node.children() {
103 match child {
104 RedTree::Leaf(leaf) => {
105 if leaf.kind != BlockquoteMarker {
106 content_text.push_str(&source.get_text_in(leaf.span));
107 }
108 }
109 RedTree::Node(child_node) => {
110 content_text.push_str(&source.get_text_in(child_node.span()));
111 }
112 }
113 }
114
115 Some(Block::Blockquote(crate::ast::Blockquote { content: vec![Block::Paragraph(crate::ast::Paragraph { content: content_text.trim().to_string(), span: node.span() })], span: node.span() }))
117 }
118 HorizontalRule => Some(Block::HorizontalRule(crate::ast::HorizontalRule { span: node.span() })),
119 Table => {
120 let text = source.get_text_in(node.span());
121 let lines: Vec<&str> = text.lines().collect();
122 if lines.is_empty() {
123 return None;
124 }
125
126 let parse_row = |line: &str| -> crate::ast::TableRow {
127 let cells = line
128 .split('|')
129 .filter(|s| !s.trim().is_empty())
130 .map(|s| crate::ast::TableCell {
131 content: s.trim().to_string(),
132 span: node.span(), })
134 .collect();
135 crate::ast::TableRow { cells, span: node.span() }
136 };
137
138 let header = parse_row(lines[0]);
139 let mut rows = Vec::new();
140 for line in lines.iter().skip(1) {
141 if line.contains("---") {
142 continue;
143 }
144 if line.trim().is_empty() {
145 continue;
146 }
147 rows.push(parse_row(line));
148 }
149
150 Some(Block::Table(crate::ast::Table { header, rows, span: node.span() }))
151 }
152 HtmlTag => {
153 None
155 }
156 _ => None,
157 }
158 }
159
160 fn build_list_item(&self, node: RedNode<MarkdownLanguage>, source: &SourceText) -> crate::ast::ListItem {
161 let mut content = Vec::new();
162 for child in node.children() {
163 if let RedTree::Node(child_node) = child {
164 if let Some(block) = self.build_block(child_node, source) {
165 content.push(block);
166 }
167 }
168 }
169
170 if content.is_empty() {
172 let text = source.get_text_in(node.span()).to_string();
173 if !text.trim().is_empty() {
174 let display_text = if text.starts_with("- ") || text.starts_with("* ") {
176 text[2..].to_string()
177 }
178 else if text.len() > 3 && text.chars().next().unwrap().is_ascii_digit() && text.contains(". ") {
179 if let Some(pos) = text.find(". ") { text[pos + 2..].to_string() } else { text }
181 }
182 else {
183 text
184 };
185
186 content.push(crate::ast::Block::Paragraph(crate::ast::Paragraph { content: display_text.trim().to_string(), span: node.span() }));
187 }
188 }
189
190 crate::ast::ListItem { content, is_task: false, is_checked: None, span: node.span() }
191 }
192}
193
194impl<'config> Builder<MarkdownLanguage> for MarkdownBuilder<'config> {
195 fn build<'a, S: Source + ?Sized>(&self, source: &S, edits: &[TextEdit], _cache: &'a mut impl BuilderCache<MarkdownLanguage>) -> oak_core::builder::BuildOutput<MarkdownLanguage> {
196 let parser = MarkdownParser::new(self.config);
197 let mut parse_session = oak_core::parser::session::ParseSession::<MarkdownLanguage>::default();
198 let parse_result = parser.parse(source, edits, &mut parse_session);
199
200 match parse_result.result {
201 Ok(green_tree) => {
202 let source_text = SourceText::new(source.get_text_in((0..source.length()).into()).into_owned());
203 match self.build_root(green_tree, &source_text) {
204 Ok(ast_root) => oak_core::OakDiagnostics { result: Ok(ast_root), diagnostics: parse_result.diagnostics },
205 Err(build_error) => {
206 let mut diagnostics = parse_result.diagnostics;
207 diagnostics.push(build_error.clone());
208 oak_core::OakDiagnostics { result: Err(build_error), diagnostics }
209 }
210 }
211 }
212 Err(parse_error) => oak_core::OakDiagnostics { result: Err(parse_error), diagnostics: parse_result.diagnostics },
213 }
214 }
215}