md-parser-kma 0.1.0

A simple parser that converts basic Markdown syntax (headers, bold, italic, lists) into corresponding HTML tags
Documentation
use pest::Parser;
use pest_derive::Parser;
use thiserror::Error;

#[derive(Parser)]
#[grammar = "grammar.pest"]
pub struct MarkdownParser;

#[derive(Error, Debug)]
pub enum MarkdownError {
    #[error("Parse error at {line}:{column}: {source}")]
    ParseError {
        line: usize,
        column: usize,
        #[source]
        source: Box<pest::error::Error<Rule>>,
    },
    #[error("Invalid structure in {rule}: expected {expected} but got {actual}")]
    InvalidStructure {
        rule: String,
        expected: String,
        actual: String,
    },
}

pub fn parse_to_html(markdown: &str) -> Result<String, MarkdownError> {
    match MarkdownParser::parse(Rule::document, markdown) {
        Ok(pairs) => {
            let mut html = String::new();
            for pair in pairs {
                html.push_str(&convert_pair_to_html(pair)?);
            }
            Ok(html)
        }
        Err(e) => match e.line_col {
            pest::error::LineColLocation::Pos((line, column)) => Err(MarkdownError::ParseError {
                line,
                column,
                source: Box::new(e),
            }),
            pest::error::LineColLocation::Span((start_line, start_col), (_end_line, _end_col)) => {
                Err(MarkdownError::ParseError {
                    line: start_line,
                    column: start_col,
                    source: Box::new(e),
                })
            }
        },
    }
}

fn convert_inner_to_html(pair: pest::iterators::Pair<Rule>) -> Result<String, MarkdownError> {
    let mut html = String::new();
    for child in pair.into_inner() {
        html.push_str(&convert_pair_to_html(child)?);
    }
    Ok(html)
}

fn convert_pair_to_html(pair: pest::iterators::Pair<Rule>) -> Result<String, MarkdownError> {
    match pair.as_rule() {
        Rule::document | Rule::block | Rule::paragraph_line | Rule::line_content => {
            convert_inner_to_html(pair)
        }

        Rule::header => {
            let mut inner = pair.into_inner();
            let header_start = inner.next().unwrap();
            let line_content = inner.next().unwrap();

            let level = header_start.as_str().trim().len();
            let content = convert_pair_to_html(line_content)?;

            Ok(format!("<h{}>{}</h{}>\n", level, content, level))
        }
        Rule::unordered_list => {
            let items = convert_inner_to_html(pair)?;
            Ok(format!("<ul>\n{}\n</ul>\n", items.trim()))
        }
        Rule::ordered_list => {
            let items = convert_inner_to_html(pair)?;
            Ok(format!("<ol>\n{}\n</ol>\n", items.trim()))
        }
        Rule::unordered_list_point | Rule::ordered_list_point => {
            let content = convert_inner_to_html(pair)?;
            Ok(format!("<li>{}</li>\n", content.trim()))
        }
        Rule::paragraph => {
            let content = convert_inner_to_html(pair)?;
            Ok(format!("<p>{}</p>\n", content.trim()))
        }
        Rule::bold_italic => {
            let inner_html = convert_inner_to_html(pair)?;
            Ok(format!("<strong><em>{}</em></strong>", inner_html))
        }
        Rule::bold => {
            let inner_html = convert_inner_to_html(pair)?;
            Ok(format!("<strong>{}</strong>", inner_html))
        }
        Rule::italic => {
            let inner_html = convert_inner_to_html(pair)?;
            Ok(format!("<em>{}</em>", inner_html))
        }
        Rule::char => Ok(pair.as_str().to_string()),
        Rule::WHITESPACE => Ok(pair.as_str().to_string()),
        _ => convert_inner_to_html(pair),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_simple_header() {
        let markdown = "# Hello World\n";
        let html = parse_to_html(markdown).unwrap();
        assert_eq!(html, "<h1>Hello World</h1>\n");
    }

    #[test]
    fn test_parse_bold_text() {
        let markdown = "This is **bold** text\n";
        let html = parse_to_html(markdown).unwrap();
        assert_eq!(html, "<p>This is <strong>bold</strong> text</p>\n");
    }

    #[test]
    fn test_parse_italic_text() {
        let markdown = "This is *italic* text\n";
        let html = parse_to_html(markdown).unwrap();
        assert_eq!(html, "<p>This is <em>italic</em> text</p>\n");
    }

    #[test]
    fn test_parse_unordered_list() {
        let markdown = "- Point 1\n- Point 2\n";
        let html = parse_to_html(markdown).unwrap();
        assert!(html.contains("<ul>"));
        assert!(html.contains("</ul>"));
        assert!(html.contains("<li>Point 1</li>"));
        assert!(html.contains("<li>Point 2</li>"));
    }

    #[test]
    fn test_parse_ordered_list() {
        let markdown = "1. First point\n2. Second point\n";
        let html = parse_to_html(markdown).unwrap();
        assert!(html.contains("<ol>"));
        assert!(html.contains("</ol>"));
        assert!(html.contains("<li>First point</li>"));
        assert!(html.contains("<li>Second point</li>"));
    }

    #[test]
    fn test_parse_bold_italic_combined() {
        let markdown = "This is ***bold and italic*** text\n";
        let html = parse_to_html(markdown).unwrap();
        assert!(html.contains("<strong><em>bold and italic</em></strong>"));
    }
}