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)]
29pub enum BlockQuoteKind {
30 Note,
31 Abstract,
32 Info,
33 Todo,
34 Tip,
35 Success,
36 Question,
37 Warning,
38 Failure,
39 Danger,
40 Bug,
41 Example,
42 Quote,
43}
44
45impl From<pulldown_cmark::BlockQuoteKind> for BlockQuoteKind {
46 fn from(value: pulldown_cmark::BlockQuoteKind) -> Self {
47 use pulldown_cmark::BlockQuoteKind as Gfm;
48 match value {
49 Gfm::Note => BlockQuoteKind::Note,
50 Gfm::Tip | Gfm::Important => BlockQuoteKind::Tip,
51 Gfm::Warning | Gfm::Caution => BlockQuoteKind::Warning,
52 }
53 }
54}
55
56impl BlockQuoteKind {
57 fn from_name(name: &str) -> Self {
60 use BlockQuoteKind::*;
61 match name.trim().to_ascii_lowercase().as_str() {
62 "abstract" | "summary" | "tldr" => Abstract,
63 "info" => Info,
64 "todo" => Todo,
65 "tip" | "hint" | "important" => Tip,
66 "success" | "check" | "done" => Success,
67 "question" | "help" | "faq" => Question,
68 "warning" | "caution" | "attention" => Warning,
69 "failure" | "fail" | "missing" => Failure,
70 "danger" | "error" => Danger,
71 "bug" => Bug,
72 "example" => Example,
73 "quote" | "cite" => Quote,
74 _ => Note,
75 }
76 }
77}
78
79pub struct CalloutMarker {
80 pub kind: BlockQuoteKind,
81 pub title: Option<String>,
82}
83
84pub fn parse_callout_marker(line: &str) -> Option<CalloutMarker> {
88 let rest = line.trim_start().strip_prefix("[!")?;
89 let close = rest.find(']')?;
90 let kind = BlockQuoteKind::from_name(&rest[..close]);
91 let tail = &rest[close + 1..];
92 let title = tail.strip_prefix(['-', '+']).unwrap_or(tail).trim();
93 Some(CalloutMarker {
94 kind,
95 title: (!title.is_empty()).then(|| title.to_string()),
96 })
97}
98
99#[derive(Clone, Debug, PartialEq)]
101pub enum ItemKind {
102 Ordered(u64),
104 Unordered,
106}
107
108#[derive(Clone, Debug, PartialEq)]
110pub enum TaskKind {
111 Checked,
113 Unchecked,
115 LooselyChecked,
118}
119
120#[derive(Clone, Copy, Debug, PartialEq)]
122pub enum Alignment {
123 None,
125 Left,
127 Center,
129 Right,
131}
132
133impl From<pulldown_cmark::Alignment> for Alignment {
134 fn from(value: pulldown_cmark::Alignment) -> Self {
135 match value {
136 pulldown_cmark::Alignment::None => Alignment::None,
137 pulldown_cmark::Alignment::Left => Alignment::Left,
138 pulldown_cmark::Alignment::Center => Alignment::Center,
139 pulldown_cmark::Alignment::Right => Alignment::Right,
140 }
141 }
142}
143
144pub type SourceRange<Idx> = std::ops::Range<Idx>;
145
146#[derive(Clone, Debug, PartialEq)]
148pub enum Node {
149 Heading {
150 level: HeadingLevel,
151 text: RichText,
152 source_range: SourceRange<usize>,
153 },
154 Paragraph {
155 text: RichText,
156 source_range: SourceRange<usize>,
157 },
158 CodeBlock {
159 lang: Option<String>,
160 text: RichText,
161 source_range: SourceRange<usize>,
162 },
163 BlockQuote {
164 kind: Option<BlockQuoteKind>,
165 title: Option<String>,
168 nodes: Vec<Node>,
169 source_range: SourceRange<usize>,
170 },
171 List {
172 nodes: Vec<Node>,
173 source_range: SourceRange<usize>,
174 },
175 Item {
176 kind: ItemKind,
177 nodes: Vec<Node>,
178 source_range: SourceRange<usize>,
179 },
180 Task {
181 kind: TaskKind,
182 nodes: Vec<Node>,
183 source_range: SourceRange<usize>,
184 },
185 Table {
188 alignments: Vec<Alignment>,
189 head: Vec<RichText>,
190 rows: Vec<Vec<RichText>>,
191 source_range: SourceRange<usize>,
192 },
193}
194
195impl Node {
196 pub fn source_range(&self) -> &SourceRange<usize> {
197 match self {
198 Self::Heading { source_range, .. }
199 | Self::CodeBlock { source_range, .. }
200 | Self::Paragraph { source_range, .. }
201 | Self::List { source_range, .. }
202 | Self::BlockQuote { source_range, .. }
203 | Self::Item { source_range, .. }
204 | Self::Task { source_range, .. }
205 | Self::Table { source_range, .. } => source_range,
206 }
207 }
208
209 pub fn set_source_range(&mut self, new_range: SourceRange<usize>) {
210 match self {
211 Self::Heading { source_range, .. }
212 | Self::CodeBlock { source_range, .. }
213 | Self::Paragraph { source_range, .. }
214 | Self::List { source_range, .. }
215 | Self::BlockQuote { source_range, .. }
216 | Self::Item { source_range, .. }
217 | Self::Task { source_range, .. }
218 | Self::Table { source_range, .. } => *source_range = new_range,
219 }
220 }
221
222 pub fn rich_text(&self) -> Option<&RichText> {
223 match self {
224 Self::Heading { text, .. }
225 | Self::Paragraph { text, .. }
226 | Self::CodeBlock { text, .. } => Some(text),
227 _ => None,
228 }
229 }
230
231 pub fn children_as_mut(&mut self) -> Option<&mut Vec<Self>> {
232 match self {
233 Self::List { nodes, .. }
234 | Self::Item { nodes, .. }
235 | Self::Task { nodes, .. }
236 | Self::BlockQuote { nodes, .. } => Some(nodes),
237 _ => None,
238 }
239 }
240}
241
242pub fn nodes_to_sexp(nodes: &[Node], indent_level: usize) -> String {
243 nodes
244 .iter()
245 .map(|node| node_to_sexp(node, indent_level))
246 .collect::<Vec<_>>()
247 .join("\n")
248}
249
250pub fn node_to_sexp(node: &Node, indent_level: usize) -> String {
251 let indent_increment = 2;
252
253 match node {
254 Node::Heading {
255 level,
256 text,
257 source_range,
258 } => {
259 format!(
260 "{:indent$}(heading {:?} @{:?}\n{})",
261 "",
262 level,
263 source_range,
264 rich_text_to_sexp(text, indent_level + indent_increment),
265 indent = indent_level
266 )
267 }
268 Node::Paragraph { text, source_range } => {
269 format!(
270 "{:indent$}(paragraph @{:?}\n{})",
271 "",
272 source_range,
273 rich_text_to_sexp(text, indent_level + indent_increment),
274 indent = indent_level
275 )
276 }
277 Node::BlockQuote {
278 kind,
279 title,
280 nodes,
281 source_range,
282 } => {
283 format!(
284 "{:indent$}(blockquote {:?} {:?} @{:?}\n{})",
285 "",
286 kind,
287 title,
288 source_range,
289 nodes_to_sexp(nodes, indent_level + indent_increment),
290 indent = indent_level
291 )
292 }
293 Node::CodeBlock {
294 lang,
295 text,
296 source_range,
297 } => {
298 format!(
299 "{:indent$}(codeblock {} @{:?}\n{})",
300 "",
301 lang.clone().unwrap_or(String::new()),
302 source_range,
303 rich_text_to_sexp(text, indent_level + indent_increment),
304 indent = indent_level,
305 )
306 }
307 Node::List {
308 nodes,
309 source_range,
310 } => {
311 format!(
312 "{:indent$}(list @{:?}\n{})",
313 "",
314 source_range,
315 nodes_to_sexp(nodes, indent_level + indent_increment),
316 indent = indent_level
317 )
318 }
319 Node::Item {
320 kind,
321 nodes,
322 source_range,
323 } => {
324 format!(
325 "{:indent$}(item {:?} @{:?}\n{})",
326 "",
327 kind,
328 source_range,
329 nodes_to_sexp(nodes, indent_level + indent_increment),
330 indent = indent_level
331 )
332 }
333 Node::Task {
334 kind,
335 nodes,
336 source_range,
337 } => {
338 format!(
339 "{:indent$}(task {:?} @{:?}\n{})",
340 "",
341 kind,
342 source_range,
343 nodes_to_sexp(nodes, indent_level + indent_increment),
344 indent = indent_level
345 )
346 }
347 Node::Table {
348 alignments,
349 head,
350 rows,
351 source_range,
352 } => {
353 let cells = |row: &[RichText]| {
354 row.iter()
355 .map(|cell| format!("\"{cell}\""))
356 .collect::<Vec<_>>()
357 .join(" ")
358 };
359 let inner_indent = indent_level + indent_increment;
360 let body = rows
361 .iter()
362 .map(|row| format!("{:inner_indent$}(row {})", "", cells(row)))
363 .collect::<Vec<_>>()
364 .join("\n");
365 format!(
366 "{:indent$}(table {:?} @{:?}\n{:inner_indent$}(head {})\n{})",
367 "",
368 alignments,
369 source_range,
370 "",
371 cells(head),
372 body,
373 indent = indent_level,
374 )
375 }
376 }
377}
378
379pub fn rich_text_to_sexp(rich_text: &RichText, indent_level: usize) -> String {
380 rich_text
381 .segments()
382 .iter()
383 .map(|segment| match &segment.style {
384 Some(style) => format!(
385 "{:indent$}({} \"{}\")",
386 "",
387 style,
388 segment,
389 indent = indent_level
390 ),
391 None => format!("{:indent$}\"{}\"", "", segment, indent = indent_level),
392 })
393 .collect::<Vec<_>>()
394 .join("\n")
395}