Skip to main content

md_parser_kma/
lib.rs

1use pest::Parser;
2use pest_derive::Parser;
3use thiserror::Error;
4
5#[derive(Parser)]
6#[grammar = "grammar.pest"]
7pub struct MarkdownParser;
8
9#[derive(Error, Debug)]
10pub enum MarkdownError {
11    #[error("Parse error at {line}:{column}: {source}")]
12    ParseError {
13        line: usize,
14        column: usize,
15        #[source]
16        source: Box<pest::error::Error<Rule>>,
17    },
18    #[error("Invalid structure in {rule}: expected {expected} but got {actual}")]
19    InvalidStructure {
20        rule: String,
21        expected: String,
22        actual: String,
23    },
24}
25
26pub fn parse_to_html(markdown: &str) -> Result<String, MarkdownError> {
27    match MarkdownParser::parse(Rule::document, markdown) {
28        Ok(pairs) => {
29            let mut html = String::new();
30            for pair in pairs {
31                html.push_str(&convert_pair_to_html(pair)?);
32            }
33            Ok(html)
34        }
35        Err(e) => match e.line_col {
36            pest::error::LineColLocation::Pos((line, column)) => Err(MarkdownError::ParseError {
37                line,
38                column,
39                source: Box::new(e),
40            }),
41            pest::error::LineColLocation::Span((start_line, start_col), (_end_line, _end_col)) => {
42                Err(MarkdownError::ParseError {
43                    line: start_line,
44                    column: start_col,
45                    source: Box::new(e),
46                })
47            }
48        },
49    }
50}
51
52fn convert_inner_to_html(pair: pest::iterators::Pair<Rule>) -> Result<String, MarkdownError> {
53    let mut html = String::new();
54    for child in pair.into_inner() {
55        html.push_str(&convert_pair_to_html(child)?);
56    }
57    Ok(html)
58}
59
60fn convert_pair_to_html(pair: pest::iterators::Pair<Rule>) -> Result<String, MarkdownError> {
61    match pair.as_rule() {
62        Rule::document | Rule::block | Rule::paragraph_line | Rule::line_content => {
63            convert_inner_to_html(pair)
64        }
65
66        Rule::header => {
67            let mut inner = pair.into_inner();
68            let header_start = inner.next().unwrap();
69            let line_content = inner.next().unwrap();
70
71            let level = header_start.as_str().trim().len();
72            let content = convert_pair_to_html(line_content)?;
73
74            Ok(format!("<h{}>{}</h{}>\n", level, content, level))
75        }
76        Rule::unordered_list => {
77            let items = convert_inner_to_html(pair)?;
78            Ok(format!("<ul>\n{}\n</ul>\n", items.trim()))
79        }
80        Rule::ordered_list => {
81            let items = convert_inner_to_html(pair)?;
82            Ok(format!("<ol>\n{}\n</ol>\n", items.trim()))
83        }
84        Rule::unordered_list_point | Rule::ordered_list_point => {
85            let content = convert_inner_to_html(pair)?;
86            Ok(format!("<li>{}</li>\n", content.trim()))
87        }
88        Rule::paragraph => {
89            let content = convert_inner_to_html(pair)?;
90            Ok(format!("<p>{}</p>\n", content.trim()))
91        }
92        Rule::bold_italic => {
93            let inner_html = convert_inner_to_html(pair)?;
94            Ok(format!("<strong><em>{}</em></strong>", inner_html))
95        }
96        Rule::bold => {
97            let inner_html = convert_inner_to_html(pair)?;
98            Ok(format!("<strong>{}</strong>", inner_html))
99        }
100        Rule::italic => {
101            let inner_html = convert_inner_to_html(pair)?;
102            Ok(format!("<em>{}</em>", inner_html))
103        }
104        Rule::char => Ok(pair.as_str().to_string()),
105        Rule::WHITESPACE => Ok(pair.as_str().to_string()),
106        _ => convert_inner_to_html(pair),
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_parse_simple_header() {
116        let markdown = "# Hello World\n";
117        let html = parse_to_html(markdown).unwrap();
118        assert_eq!(html, "<h1>Hello World</h1>\n");
119    }
120
121    #[test]
122    fn test_parse_bold_text() {
123        let markdown = "This is **bold** text\n";
124        let html = parse_to_html(markdown).unwrap();
125        assert_eq!(html, "<p>This is <strong>bold</strong> text</p>\n");
126    }
127
128    #[test]
129    fn test_parse_italic_text() {
130        let markdown = "This is *italic* text\n";
131        let html = parse_to_html(markdown).unwrap();
132        assert_eq!(html, "<p>This is <em>italic</em> text</p>\n");
133    }
134
135    #[test]
136    fn test_parse_unordered_list() {
137        let markdown = "- Point 1\n- Point 2\n";
138        let html = parse_to_html(markdown).unwrap();
139        assert!(html.contains("<ul>"));
140        assert!(html.contains("</ul>"));
141        assert!(html.contains("<li>Point 1</li>"));
142        assert!(html.contains("<li>Point 2</li>"));
143    }
144
145    #[test]
146    fn test_parse_ordered_list() {
147        let markdown = "1. First point\n2. Second point\n";
148        let html = parse_to_html(markdown).unwrap();
149        assert!(html.contains("<ol>"));
150        assert!(html.contains("</ol>"));
151        assert!(html.contains("<li>First point</li>"));
152        assert!(html.contains("<li>Second point</li>"));
153    }
154
155    #[test]
156    fn test_parse_bold_italic_combined() {
157        let markdown = "This is ***bold and italic*** text\n";
158        let html = parse_to_html(markdown).unwrap();
159        assert!(html.contains("<strong><em>bold and italic</em></strong>"));
160    }
161}