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}