Rust LogoMarkdown Parser

A Markdown Parser is a software component or library designed to convert text written in Markdown syntax into another format, most commonly HTML, but also Abstract Syntax Trees (ASTs), LaTeX, or plain text. Markdown is a lightweight markup language that allows plain text to be written using an easy-to-read, easy-to-write syntax, which can then be converted into structurally valid HTML or other formats.

The primary purpose of a Markdown parser is to bridge the gap between human-readable, plain-text content and structured, renderable output. It enables developers and content creators to write documents in a simple, expressive format without worrying about complex HTML tags, while still producing web-friendly or print-ready results.

The process of parsing Markdown typically involves several stages:

1. Lexing (or Tokenization): In this initial phase, the input Markdown text is broken down into a stream of tokens. Each token represents a basic unit of the Markdown syntax, such as a heading marker (`#`), a word, a bold marker (``), a list item bullet (`-`), or a newline character. For example, `Hello` might be tokenized as `BOLD_START`, `TEXT("Hello")`, `BOLD_END`.

2. Parsing (or Syntactic Analysis): The stream of tokens is then analyzed to determine the grammatical structure of the Markdown document. This stage builds a hierarchical representation of the document, often in the form of an Abstract Syntax Tree (AST). The AST represents the logical structure of the document, showing how different elements (like headings, paragraphs, lists, bold text) relate to each other. For instance, a paragraph containing bold text would be represented as a `Paragraph` node with a child `Bold` node, which in turn has a child `Text` node.

3. Rendering (or Transformation): Finally, the AST is traversed, and each node is converted into the desired output format. If the target is HTML, a `Heading` node might be converted into an `<h1>` or `<h2>` tag, a `Paragraph` node into a `<p>` tag, and a `Bold` node into a `<strong>` tag. This stage involves serializing the AST into the target string format.

Key Challenges in Markdown Parsing:

* Ambiguity: Markdown syntax can sometimes be ambiguous. For example, `*` can denote an italicized word or a list item bullet, depending on context. Parsers must implement rules to disambiguate these situations.
* Nesting: Handling nested structures (e.g., a bold word inside an italic phrase, a list within a blockquote) correctly is complex and requires careful state management during parsing.
* Different Flavors: Various "flavors" of Markdown exist (e.g., CommonMark, GitHub Flavored Markdown, MultiMarkdown), each with slight differences in syntax and supported features. A robust parser often needs to adhere to a specific standard or offer configuration options for different flavors.
* Performance: For large documents, the parsing process needs to be efficient to avoid performance bottlenecks.
* HTML Escaping: Ensuring that user-provided text content within Markdown (e.g., a `<` character in a paragraph) is properly HTML-escaped to prevent XSS vulnerabilities.

Applications of Markdown Parsers:

* Content Management Systems (CMS): Allow users to write content in Markdown, which is then rendered as HTML for web display.
* Static Site Generators (SSGs): Tools like Hugo or Jekyll process Markdown files into static HTML websites.
* Documentation Tools: Many project documentation systems use Markdown as their primary input format.
* Rich Text Editors: Many "what you see is what you get" (WYSIWYG) editors internally use Markdown as an intermediate representation.
* API Documentation: Tools like Postman or Swagger often use Markdown for describing APIs.

The provided Rust example demonstrates a very simplified Markdown parser. It focuses on identifying basic block-level elements (headings, paragraphs) and simple inline elements (bold, italic) and converting them into an Abstract Syntax Tree (AST), which is then rendered into HTML. A real-world Markdown parser would be significantly more complex, handling a much wider array of Markdown features, edge cases, and adhering to standards like CommonMark.

Example Code

use std::str::Chars; 
use std::iter::Peekable; 

// Define AST nodes for inline content
#[derive(Debug, PartialEq)] 
enum InlineNode {
    Text(String),
    Bold(String),
    Italic(String),
}

// Define AST nodes for block content
#[derive(Debug, PartialEq)] 
enum BlockNode {
    Heading(u8, Vec<InlineNode>), // level (1-6), content
    Paragraph(Vec<InlineNode>),
    // For simplicity, other block nodes like lists, blockquotes, code blocks are omitted
    // in this example's AST and parsing logic.
}

// Helper function to escape HTML special characters
fn escape_html(s: &str) -> String {
    s.replace("&", "&")
     .replace("<", "<")
     .replace(">", ">")
     .replace("\"", """)
     .replace("'", "'")
}

// Parses inline Markdown syntax into a vector of InlineNodes.
// This is a simplified version and does not handle nesting (e.g., bold inside italic)
// or complex edge cases (e.g., escaped markers).
fn parse_inline(text: &str) -> Vec<InlineNode> {
    let mut nodes = Vec::new();
    let mut chars = text.chars().peekable();
    let mut current_text_buffer = String::new();

    while let Some(c) = chars.next() {
        match c {
            '*' => {
                if let Some('*') = chars.peek() { // Possible bold ""
                    chars.next(); // Consume the second '*'
                    if !current_text_buffer.is_empty() {
                        nodes.push(InlineNode::Text(escape_html(¤t_text_buffer)));
                        current_text_buffer.clear();
                    }
                    let mut bold_content = String::new();
                    let mut found_closing = false;
                    while let Some(bc) = chars.next() {
                        if bc == '*' {
                            if let Some('*') = chars.peek() {
                                chars.next(); // Consume second '*'
                                nodes.push(InlineNode::Bold(escape_html(&bold_content)));
                                found_closing = true;
                                break;
                            } else {
                                bold_content.push(bc); // Single '*' inside bold, treat as text
                            }
                        }
                        else {
                            bold_content.push(bc);
                        }
                    }
                    if !found_closing {
                        // Unmatched "", treat original "" and content as text
                        nodes.push(InlineNode::Text(escape_html(&format!("{}", bold_content))));
                    }
                } else { // Possible italic "*"
                    if !current_text_buffer.is_empty() {
                        nodes.push(InlineNode::Text(escape_html(¤t_text_buffer)));
                        current_text_buffer.clear();
                    }
                    let mut italic_content = String::new();
                    let mut found_closing = false;
                    while let Some(ic) = chars.next() {
                        if ic == '*' {
                            nodes.push(InlineNode::Italic(escape_html(&italic_content)));
                            found_closing = true;
                            break;
                        } else {
                            italic_content.push(ic);
                        }
                    }
                    if !found_closing {
                        // Unmatched "*", treat original "*" and content as text
                        nodes.push(InlineNode::Text(escape_html(&format!("*{}", italic_content))));
                    }
                }
            },
            _ => current_text_buffer.push(c),
        }
    }

    if !current_text_buffer.is_empty() {
        nodes.push(InlineNode::Text(escape_html(¤t_text_buffer)));
    }
    nodes
}

struct MarkdownParser;

impl MarkdownParser {
    // Parses a Markdown string into a vector of BlockNodes (AST).
    fn parse(markdown: &str) -> Vec<BlockNode> {
        let mut ast = Vec::new();
        let mut lines = markdown.lines().peekable();

        while let Some(line) = lines.next() {
            if line.starts_with("#") {
                let mut level = 0;
                for c in line.chars() {
                    if c == '#' {
                        level += 1;
                    } else if c == ' ' {
                        break;
                    } else {
                        level = 0; // Not a valid heading if non-'#' char before space
                        break;
                    }
                }
                // Ensure valid heading level (1-6) and there's content after the `#`s and space
                if level > 0 && level <= 6 && line.len() > level && line.chars().nth(level) == Some(' ') {
                    let content = &line[level + 1..];
                    ast.push(BlockNode::Heading(level, parse_inline(content.trim())));
                } else {
                    // If it started with # but wasn't a valid heading, treat as paragraph
                    ast.push(BlockNode::Paragraph(parse_inline(line)));
                }
            } else if line.trim().is_empty() {
                // Ignore empty lines between blocks
                continue;
            } else {
                // Default to paragraph for any other non-empty line
                ast.push(BlockNode::Paragraph(parse_inline(line)));
            }
        }
        ast
    }

    // Renders the AST into an HTML string.
    fn render_html(ast: &[BlockNode]) -> String {
        let mut html = String::new();
        for node in ast {
            match node {
                BlockNode::Heading(level, inline_nodes) => {
                    html.push_str(&format!("<h{}>", level));
                    html.push_str(&Self::render_inline_html(inline_nodes));
                    html.push_str(&format!("</h{}>\n", level));
                }
                BlockNode::Paragraph(inline_nodes) => {
                    html.push_str("<p>");
                    html.push_str(&Self::render_inline_html(inline_nodes));
                    html.push_str("</p>\n");
                }
            }
        }
        html
    }

    // Renders a vector of InlineNodes into an HTML string.
    fn render_inline_html(inline_nodes: &[InlineNode]) -> String {
        let mut html = String::new();
        for node in inline_nodes {
            match node {
                InlineNode::Text(text) => html.push_str(text), // Text is already escaped during parsing
                InlineNode::Bold(text) => html.push_str(&format!("<strong>{}</strong>", text)),
                InlineNode::Italic(text) => html.push_str(&format!("<em>{}</em>", text)),
            }
        }
        html
    }
}

fn main() {
    let markdown_input = r#"
# Welcome to My Document

This is a simple paragraph with *some* italics and bold text.

 A Subheading

Another paragraph here.
"#;

    let ast = MarkdownParser::parse(markdown_input);
    println!("--- AST ---");
    println!("{:#?}", ast);

    let html_output = MarkdownParser::render_html(&ast);
    println!("\n--- HTML Output ---");
    println!("{}", html_output);
}