Skip to main content

basalt_tui/note_editor/
ast.rs

1use crate::note_editor::rich_text::RichText;
2
3#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
4pub enum HeadingLevel {
5    H1 = 1,
6    H2,
7    H3,
8    H4,
9    H5,
10    H6,
11}
12
13impl From<pulldown_cmark::HeadingLevel> for HeadingLevel {
14    fn from(value: pulldown_cmark::HeadingLevel) -> Self {
15        match value {
16            pulldown_cmark::HeadingLevel::H1 => HeadingLevel::H1,
17            pulldown_cmark::HeadingLevel::H2 => HeadingLevel::H2,
18            pulldown_cmark::HeadingLevel::H3 => HeadingLevel::H3,
19            pulldown_cmark::HeadingLevel::H4 => HeadingLevel::H4,
20            pulldown_cmark::HeadingLevel::H5 => HeadingLevel::H5,
21            pulldown_cmark::HeadingLevel::H6 => HeadingLevel::H6,
22        }
23    }
24}
25
26#[derive(Clone, Debug, PartialEq)]
27pub enum BlockQuoteKind {
28    Note,
29    Tip,
30    Important,
31    Warning,
32    Caution,
33}
34
35impl From<pulldown_cmark::BlockQuoteKind> for BlockQuoteKind {
36    fn from(value: pulldown_cmark::BlockQuoteKind) -> Self {
37        match value {
38            pulldown_cmark::BlockQuoteKind::Tip => BlockQuoteKind::Tip,
39            pulldown_cmark::BlockQuoteKind::Note => BlockQuoteKind::Note,
40            pulldown_cmark::BlockQuoteKind::Warning => BlockQuoteKind::Warning,
41            pulldown_cmark::BlockQuoteKind::Caution => BlockQuoteKind::Caution,
42            pulldown_cmark::BlockQuoteKind::Important => BlockQuoteKind::Important,
43        }
44    }
45}
46
47/// Denotes whether a list is ordered or unordered.
48#[derive(Clone, Debug, PartialEq)]
49pub enum ItemKind {
50    /// An ordered list item (e.g., `1. item`), storing the numeric index.
51    Ordered(u64),
52    /// An unordered list item (e.g., `- item`).
53    Unordered,
54}
55
56/// Represents the variant of a list or task item (checked, unchecked, etc.).
57#[derive(Clone, Debug, PartialEq)]
58pub enum TaskKind {
59    /// A checkbox item that is marked as done using `- [x]`.
60    Checked,
61    /// A checkbox item that is unchecked using `- [ ]`.
62    Unchecked,
63    /// A checkbox item that is checked, but not explicitly recognized as
64    /// `Checked` (e.g., `- [?]`).
65    LooselyChecked,
66}
67
68pub type SourceRange<Idx> = std::ops::Range<Idx>;
69
70/// The Markdown AST node enumeration.
71#[derive(Clone, Debug, PartialEq)]
72pub enum Node {
73    Heading {
74        level: HeadingLevel,
75        text: RichText,
76        source_range: SourceRange<usize>,
77    },
78    Paragraph {
79        text: RichText,
80        source_range: SourceRange<usize>,
81    },
82    CodeBlock {
83        lang: Option<String>,
84        text: RichText,
85        source_range: SourceRange<usize>,
86    },
87    BlockQuote {
88        kind: Option<BlockQuoteKind>,
89        nodes: Vec<Node>,
90        source_range: SourceRange<usize>,
91    },
92    List {
93        nodes: Vec<Node>,
94        source_range: SourceRange<usize>,
95    },
96    Item {
97        kind: ItemKind,
98        nodes: Vec<Node>,
99        source_range: SourceRange<usize>,
100    },
101    Task {
102        kind: TaskKind,
103        nodes: Vec<Node>,
104        source_range: SourceRange<usize>,
105    },
106}
107
108impl Node {
109    pub fn source_range(&self) -> &SourceRange<usize> {
110        match self {
111            Self::Heading { source_range, .. }
112            | Self::CodeBlock { source_range, .. }
113            | Self::Paragraph { source_range, .. }
114            | Self::List { source_range, .. }
115            | Self::BlockQuote { source_range, .. }
116            | Self::Item { source_range, .. }
117            | Self::Task { source_range, .. } => source_range,
118        }
119    }
120
121    pub fn set_source_range(&mut self, new_range: SourceRange<usize>) {
122        match self {
123            Self::Heading { source_range, .. }
124            | Self::CodeBlock { source_range, .. }
125            | Self::Paragraph { source_range, .. }
126            | Self::List { source_range, .. }
127            | Self::BlockQuote { source_range, .. }
128            | Self::Item { source_range, .. }
129            | Self::Task { source_range, .. } => *source_range = new_range,
130        }
131    }
132
133    pub fn rich_text(&self) -> Option<&RichText> {
134        match self {
135            Self::Heading { text, .. }
136            | Self::Paragraph { text, .. }
137            | Self::CodeBlock { text, .. } => Some(text),
138            _ => None,
139        }
140    }
141}
142
143pub fn nodes_to_sexp(nodes: &[Node], indent_level: usize) -> String {
144    nodes
145        .iter()
146        .map(|node| node_to_sexp(node, indent_level))
147        .collect::<Vec<_>>()
148        .join("\n")
149}
150
151pub fn node_to_sexp(node: &Node, indent_level: usize) -> String {
152    let indent_increment = 2;
153
154    match node {
155        Node::Heading {
156            level,
157            text,
158            source_range,
159        } => {
160            format!(
161                "{:indent$}(heading {:?} @{:?}\n{})",
162                "",
163                level,
164                source_range,
165                rich_text_to_sexp(text, indent_level + indent_increment),
166                indent = indent_level
167            )
168        }
169        Node::Paragraph { text, source_range } => {
170            format!(
171                "{:indent$}(paragraph @{:?}\n{})",
172                "",
173                source_range,
174                rich_text_to_sexp(text, indent_level + indent_increment),
175                indent = indent_level
176            )
177        }
178        Node::BlockQuote {
179            kind,
180            nodes,
181            source_range,
182        } => {
183            format!(
184                "{:indent$}(blockquote {:?} @{:?}\n{})",
185                "",
186                kind,
187                source_range,
188                nodes_to_sexp(nodes, indent_level + indent_increment),
189                indent = indent_level
190            )
191        }
192        Node::CodeBlock {
193            lang,
194            text,
195            source_range,
196        } => {
197            format!(
198                "{:indent$}(codeblock {} @{:?}\n{})",
199                "",
200                lang.clone().unwrap_or(String::new()),
201                source_range,
202                rich_text_to_sexp(text, indent_level + indent_increment),
203                indent = indent_level,
204            )
205        }
206        Node::List {
207            nodes,
208            source_range,
209        } => {
210            format!(
211                "{:indent$}(list @{:?}\n{})",
212                "",
213                source_range,
214                nodes_to_sexp(nodes, indent_level + indent_increment),
215                indent = indent_level
216            )
217        }
218        Node::Item {
219            kind,
220            nodes,
221            source_range,
222        } => {
223            format!(
224                "{:indent$}(item {:?} @{:?}\n{})",
225                "",
226                kind,
227                source_range,
228                nodes_to_sexp(nodes, indent_level + indent_increment),
229                indent = indent_level
230            )
231        }
232        Node::Task {
233            kind,
234            nodes,
235            source_range,
236        } => {
237            format!(
238                "{:indent$}(task {:?} @{:?}\n{})",
239                "",
240                kind,
241                source_range,
242                nodes_to_sexp(nodes, indent_level + indent_increment),
243                indent = indent_level
244            )
245        }
246    }
247}
248
249pub fn rich_text_to_sexp(rich_text: &RichText, indent_level: usize) -> String {
250    rich_text
251        .segments()
252        .iter()
253        .map(|segment| match &segment.style {
254            Some(style) => format!(
255                "{:indent$}({} \"{}\")",
256                "",
257                style,
258                segment,
259                indent = indent_level
260            ),
261            None => format!("{:indent$}\"{}\"", "", segment, indent = indent_level),
262        })
263        .collect::<Vec<_>>()
264        .join("\n")
265}