cdoc_parser/ast/
parser.rs

1use crate::ast::*;
2
3use crate::raw;
4use crate::raw::{Child, ComposedMarkdown, Special};
5use anyhow::anyhow;
6use cowstr::ToCowStr;
7use pulldown_cmark::{Event, HeadingLevel, Parser as MdParser, Tag};
8use regex::Regex;
9use std::str::FromStr;
10
11pub(crate) enum InnerContent {
12    Blocks(Vec<Block>),
13    Inlines(Vec<Inline>),
14}
15
16impl InnerContent {
17    pub(crate) fn into_blocks(self) -> Vec<Block> {
18        if let InnerContent::Blocks(b) = self {
19            b
20        } else {
21            panic!("Expected blocks")
22        }
23    }
24
25    pub(crate) fn into_inlines(self) -> Vec<Inline> {
26        if let InnerContent::Inlines(i) = self {
27            i
28        } else {
29            panic!("Expected inlines")
30        }
31    }
32
33    pub(crate) fn blocks_mut(&mut self) -> anyhow::Result<&mut Vec<Block>> {
34        if let InnerContent::Blocks(b) = self {
35            Ok(b)
36        } else {
37            Err(anyhow!("Expected block element"))
38        }
39    }
40
41    #[allow(unused)]
42    fn inlines_mut(&mut self) -> anyhow::Result<&mut Vec<Inline>> {
43        if let InnerContent::Inlines(i) = self {
44            Ok(i)
45        } else {
46            Err(anyhow!("Expected inline element"))
47        }
48    }
49
50    pub(crate) fn push_inline(&mut self, item: Inline) {
51        match self {
52            InnerContent::Blocks(b) => b.push(Block::Plain(vec![item])),
53            InnerContent::Inlines(i) => i.push(item),
54        }
55    }
56}
57
58impl From<raw::Value> for Value {
59    fn from(value: raw::Value) -> Self {
60        match value {
61            raw::Value::Flag(f) => Value::Flag(f),
62            raw::Value::Content(c) => Value::Content(ComposedMarkdown::from(c).into()),
63            raw::Value::String(s) => Value::String(s),
64        }
65    }
66}
67
68impl From<raw::Parameter> for Parameter {
69    fn from(value: raw::Parameter) -> Self {
70        Parameter {
71            key: value.key,
72            value: value.value.into(),
73            span: value.span,
74        }
75    }
76}
77
78// impl From<raw::Reference> for Reference {
79//     fn from(value: raw::Reference) -> Self {
80//         match value {
81//             raw::Reference::Math(s) => Reference::Math(s),
82//             raw::Reference::Code(s) => Reference::Code(s),
83//             raw::Reference::Command(name, val) => Reference::Command {
84//                 function: name,
85//                 parameters: val.into_iter().map(|p| p.into()).collect(),
86//             },
87//         }
88//     }
89// }
90
91impl From<Child> for Inline {
92    fn from(value: Child) -> Self {
93        match value.elem {
94            Special::Math { inner, is_block } => Inline::Math(Math {
95                label: value.label,
96                source: inner,
97                display_block: is_block,
98                span: value.span,
99            }),
100            Special::CodeBlock {
101                inner, attributes, ..
102            } => Inline::CodeBlock(CodeBlock {
103                label: value.label,
104                source: inner,
105                attributes,
106                display_cell: false,
107                global_idx: value.identifier,
108                span: value.span,
109            }),
110            Special::CodeInline { inner } => Inline::Code(inner),
111            Special::Command {
112                function,
113                parameters,
114                body,
115            } => {
116                let parameters = parameters.into_iter().map(|p| p.into()).collect();
117                let body = body.map(|b| ComposedMarkdown::from(b).into());
118
119                Inline::Command(Command {
120                    function,
121                    label: value.label,
122                    parameters,
123                    body,
124                    span: value.span,
125                    global_idx: value.identifier,
126                })
127            }
128            Special::Verbatim { inner } => Inline::Text(inner),
129        }
130    }
131}
132
133impl From<ComposedMarkdown> for Vec<Block> {
134    fn from(composed: ComposedMarkdown) -> Self {
135        let parser: MdParser = MdParser::new(&composed.src);
136        let r = Regex::new(r"elem-([0-9]+)").expect("invalid regex expression");
137        let mut inners = vec![InnerContent::Blocks(Vec::new())];
138
139        for event in parser {
140            match event {
141                Event::Start(t) => match t {
142                    Tag::Paragraph
143                    | Tag::Heading(_, _, _)
144                    | Tag::BlockQuote
145                    | Tag::CodeBlock(_)
146                    | Tag::TableHead
147                    | Tag::TableRow
148                    | Tag::TableCell
149                    | Tag::Emphasis
150                    | Tag::Strong
151                    | Tag::Strikethrough
152                    | Tag::Image(_, _, _) => inners.push(InnerContent::Inlines(Vec::new())),
153                    Tag::Link(_, _, _) => inners.push(InnerContent::Inlines(Vec::new())),
154                    Tag::List(_) | Tag::Item | Tag::Table(_) | Tag::FootnoteDefinition(_) => {
155                        inners.push(InnerContent::Blocks(Vec::new()))
156                    }
157                },
158                Event::End(t) => {
159                    let inner = inners.pop().expect("No inner content");
160                    match t {
161                        Tag::Paragraph => inners
162                            .last_mut()
163                            .unwrap()
164                            .blocks_mut()
165                            .expect("for paragraph")
166                            .push(Block::Paragraph(inner.into_inlines())),
167                        Tag::Heading(lvl, id, classes) => inners
168                            .last_mut()
169                            .unwrap()
170                            .blocks_mut()
171                            .expect("for heading")
172                            .push(Block::Heading {
173                                lvl: heading_to_lvl(lvl),
174                                id: id.map(|s| s.into()),
175                                classes: classes.into_iter().map(|s| s.into()).collect(),
176                                inner: inner.into_inlines(),
177                            }),
178                        Tag::BlockQuote => inners
179                            .last_mut()
180                            .unwrap()
181                            .blocks_mut()
182                            .expect("for blockquote")
183                            .push(Block::BlockQuote(inner.into_inlines())),
184                        Tag::List(idx) => inners
185                            .last_mut()
186                            .unwrap()
187                            .blocks_mut()
188                            .expect("for list")
189                            .push(Block::List(idx, inner.into_blocks())),
190                        Tag::Item => inners
191                            .last_mut()
192                            .unwrap()
193                            .blocks_mut()
194                            .expect("for item")
195                            .push(Block::ListItem(inner.into_blocks())),
196                        Tag::Emphasis => {
197                            let src = inner.into_inlines();
198
199                            inners
200                                .last_mut()
201                                .unwrap()
202                                .push_inline(Inline::Styled(src, Style::Emphasis))
203                        }
204                        Tag::Strong => inners
205                            .last_mut()
206                            .unwrap()
207                            .push_inline(Inline::Styled(inner.into_inlines(), Style::Strong)),
208                        Tag::Strikethrough => inners.last_mut().unwrap().push_inline(
209                            Inline::Styled(inner.into_inlines(), Style::Strikethrough),
210                        ),
211                        Tag::Link(tp, url, alt) => {
212                            inners.last_mut().unwrap().push_inline(Inline::Link(
213                                tp,
214                                url.to_cowstr(),
215                                alt.to_cowstr(),
216                                inner.into_inlines(),
217                            ))
218                        }
219                        Tag::Image(tp, url, alt) => {
220                            inners.last_mut().unwrap().push_inline(Inline::Image(
221                                tp,
222                                url.to_cowstr(),
223                                alt.to_cowstr(),
224                                inner.into_inlines(),
225                            ))
226                        }
227                        _ => {} // TODO: Implement rest
228                    }
229                }
230                Event::Html(src) => {
231                    let is_insert = r.captures(src.as_ref()).and_then(|c| c.get(1));
232
233                    if let Some(match_) = is_insert {
234                        let idx = usize::from_str(match_.as_str()).unwrap();
235                        let elem = composed.children[idx].clone();
236                        inners.last_mut().unwrap().push_inline(elem.into());
237                    } else {
238                        inners
239                            .last_mut()
240                            .unwrap()
241                            .push_inline(Inline::Html(src.to_cowstr()));
242                    }
243                }
244                other => {
245                    let inner = match other {
246                        Event::Text(s) => Inline::Text(s.to_cowstr()),
247                        Event::Code(s) => Inline::Code(s.to_cowstr()),
248                        Event::SoftBreak => Inline::SoftBreak,
249                        Event::HardBreak => Inline::HardBreak,
250                        Event::Rule => Inline::Rule,
251                        _ => unreachable!(),
252                    };
253
254                    let c = inners.last_mut().unwrap();
255                    c.push_inline(inner);
256                }
257            }
258        }
259        let b = inners.remove(0).into_blocks();
260        b.clone()
261    }
262}
263
264fn heading_to_lvl(value: HeadingLevel) -> u8 {
265    match value {
266        HeadingLevel::H1 => 1,
267        HeadingLevel::H2 => 2,
268        HeadingLevel::H3 => 3,
269        HeadingLevel::H4 => 4,
270        HeadingLevel::H5 => 5,
271        HeadingLevel::H6 => 6,
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use crate::ast;
278    use crate::ast::Block::ListItem;
279    use crate::ast::{Block, Command, Inline, Math, Parameter, Style, Value};
280    use crate::code_ast::types::{CodeContent, CodeElem};
281    use crate::common::Span;
282    use crate::raw::{parse_to_doc, ComposedMarkdown, Element, ElementInfo, Special};
283
284    use pulldown_cmark::LinkType;
285
286    #[test]
287    fn simple_command() {
288        let stuff = vec![
289            ElementInfo {
290                element: Element::Markdown("regular stuff ".into()),
291                span: Span::new(0, 0),
292            },
293            ElementInfo {
294                element: Element::Special(
295                    None,
296                    Special::Command {
297                        function: "func".into(),
298                        parameters: vec![],
299                        body: Some(vec![ElementInfo {
300                            element: Element::Markdown("x".into()),
301                            span: Span::new(0, 0),
302                        }]),
303                    },
304                ),
305                span: Span::new(0, 0),
306            },
307        ];
308
309        let composed = ComposedMarkdown::from(stuff);
310        let doc = Vec::from(composed);
311
312        let expected = vec![Block::Paragraph(vec![
313            Inline::Text("regular stuff ".into()),
314            Inline::Command(Command {
315                function: "func".into(),
316                label: None,
317                parameters: vec![],
318                body: Some(vec![Block::Paragraph(vec![Inline::Text("x".into())])]),
319                span: Span::new(0, 0),
320                global_idx: 0,
321            }),
322        ])];
323
324        assert_eq!(expected, doc);
325    }
326
327    #[test]
328    fn markdown_elements() {
329        let input = include_str!("../../resources/tests/markdown_elems.md");
330        let input_doc = parse_to_doc(input).expect("rawdoc parse error");
331        let composed = ComposedMarkdown::from(input_doc.src);
332        let output_doc = Vec::from(composed);
333
334        let expected = vec![
335            Block::Heading {
336                lvl: 1,
337                id: None,
338                classes: vec![],
339                inner: vec![Inline::Text("Heading".into())],
340            },
341            Block::Heading {
342                lvl: 2,
343                id: None,
344                classes: vec![],
345                inner: vec![Inline::Text("Subheading".into())],
346            },
347            Block::List(
348                None,
349                vec![
350                    ListItem(vec![Block::Plain(vec![Inline::Text(
351                        "unordered list".into(),
352                    )])]),
353                    ListItem(vec![Block::Plain(vec![Inline::Text("item 2".into())])]),
354                ],
355            ),
356            Block::List(
357                Some(1),
358                vec![
359                    ListItem(vec![Block::Plain(vec![Inline::Text(
360                        "ordered list".into(),
361                    )])]),
362                    ListItem(vec![Block::Plain(vec![Inline::Text("item 2".into())])]),
363                ],
364            ),
365            Block::Paragraph(vec![
366                Inline::Link(
367                    LinkType::Inline,
368                    "path/is/here".into(),
369                    "".into(),
370                    vec![Inline::Text("link".into())],
371                ),
372                Inline::SoftBreak,
373                Inline::Image(
374                    LinkType::Inline,
375                    "path/is/here".into(),
376                    "".into(),
377                    vec![Inline::Text("image".into())],
378                ),
379            ]),
380            Block::Paragraph(vec![
381                Inline::Styled(vec![Inline::Text("emph".into())], Style::Emphasis),
382                Inline::SoftBreak,
383                Inline::Styled(vec![Inline::Text("strong".into())], Style::Strong),
384            ]),
385            Block::Plain(vec![Inline::Code("code inline".into())]),
386            Block::Plain(vec![Inline::CodeBlock(ast::CodeBlock {
387                label: None,
388                source: CodeContent {
389                    blocks: vec![CodeElem::Src("\ncode block\n\n".to_string())],
390                    meta: Default::default(),
391                    hash: 8014072465408005981,
392                },
393
394                display_cell: false,
395                global_idx: 0,
396                span: Span::new(180, 198),
397                attributes: vec![],
398            })]),
399            Block::Plain(vec![Inline::Math(Math {
400                label: None,
401                source: "math inline".into(),
402                display_block: false,
403                span: Span::new(200, 213),
404            })]),
405            Block::Plain(vec![Inline::Math(Math {
406                label: None,
407                source: "\nmath block\n".into(),
408                display_block: true,
409                span: Span::new(215, 231),
410            })]),
411        ];
412
413        assert_eq!(expected, output_doc);
414    }
415
416    #[test]
417    fn commands() {
418        let input = include_str!("../../resources/tests/commands.md");
419        let input_doc = parse_to_doc(input).expect("rawdoc parse error");
420        let composed = ComposedMarkdown::from(input_doc.src);
421        let output_doc = Vec::from(composed);
422
423        let expected = vec![
424            Block::Plain(vec![Inline::Command(Command {
425                function: "func".into(),
426                label: None,
427                parameters: vec![],
428                body: None,
429                span: Span::new(0, 5),
430                global_idx: 0,
431            })]),
432            Block::Plain(vec![Inline::Command(Command {
433                function: "func_param".into(),
434                label: None,
435                parameters: vec![
436                    Parameter {
437                        key: None,
438                        value: Value::String("p1".into()),
439                        span: Span::new(19, 21),
440                    },
441                    Parameter {
442                        key: Some("x".into()),
443                        value: Value::String("p2".into()),
444                        span: Span::new(23, 27),
445                    },
446                ],
447                body: None,
448                span: Span::new(7, 28),
449
450                global_idx: 1,
451            })]),
452            Block::Plain(vec![Inline::Command(Command {
453                function: "func_body".into(),
454                label: None,
455                parameters: vec![],
456                body: Some(vec![Block::Paragraph(vec![Inline::Text(
457                    "hello there".into(),
458                )])]),
459                span: Span::new(30, 55),
460                global_idx: 2,
461            })]),
462            Block::Plain(vec![Inline::Command(Command {
463                function: "func_all".into(),
464                label: None,
465                parameters: vec![
466                    Parameter {
467                        key: None,
468                        value: Value::String("p1".into()),
469                        span: Span::new(67, 69),
470                    },
471                    Parameter {
472                        key: Some("x".into()),
473                        value: Value::String("p2".into()),
474                        span: Span::new(71, 75),
475                    },
476                ],
477                body: Some(vec![Block::Paragraph(vec![Inline::Text(
478                    "hello there".into(),
479                )])]),
480                span: Span::new(57, 91),
481                global_idx: 3,
482            })]),
483            Block::Plain(vec![Inline::Command(Command {
484                function: "func_inner".into(),
485                label: None,
486                parameters: vec![],
487                body: Some(vec![
488                    Block::Plain(vec![Inline::Code("#func".into())]),
489                    Block::Plain(vec![Inline::Command(Command {
490                        function: "inner".into(),
491                        label: None,
492                        parameters: vec![],
493                        body: Some(vec![Block::Plain(vec![Inline::Math(Math {
494                            label: None,
495                            source: "math".into(),
496                            display_block: false,
497                            span: Span::new(122, 128),
498                        })])]),
499                        span: Span::new(114, 130),
500                        global_idx: 0,
501                    })]),
502                ]),
503                span: Span::new(93, 132),
504                global_idx: 4,
505            })]),
506        ];
507
508        assert_eq!(expected, output_doc);
509    }
510}