markdown_ppp/parser/mod.rs
1//! Markdown parser for CommonMark + GitHub Flavored Markdown (GFM)
2//!
3//! This module provides a comprehensive parser for Markdown documents following the
4//! CommonMark specification with GitHub Flavored Markdown extensions. The parser
5//! converts raw Markdown text into a fully-typed Abstract Syntax Tree (AST).
6//!
7//! # Features
8//!
9//! - **CommonMark compliance**: Full support for CommonMark 1.0 specification
10//! - **GitHub extensions**: Tables, task lists, strikethrough, autolinks, footnotes, alerts
11//! - **Configurable parsing**: Control which elements to parse, skip, or transform
12//! - **Custom parsers**: Register custom block and inline element parsers
13//! - **Error handling**: Comprehensive error reporting with nom-based parsing
14//!
15//! # Basic Usage
16//!
17//! ```rust
18//! use markdown_ppp::parser::{parse_markdown, MarkdownParserState};
19//!
20//! let state = MarkdownParserState::new();
21//! let input = "# Hello World\n\nThis is **bold** text.";
22//!
23//! match parse_markdown(state, input) {
24//! Ok(document) => {
25//! println!("Parsed {} blocks", document.blocks.len());
26//! }
27//! Err(err) => {
28//! eprintln!("Parse error: {:?}", err);
29//! }
30//! }
31//! ```
32//!
33//! # Configuration
34//!
35//! The parser behavior can be extensively customized using configuration:
36//!
37//! ```rust
38//! use markdown_ppp::parser::{MarkdownParserState, config::*};
39//!
40//! let config = MarkdownParserConfig::default()
41//! .with_block_thematic_break_behavior(ElementBehavior::Skip)
42//! .with_inline_emphasis_behavior(ElementBehavior::Parse);
43//!
44//! let state = MarkdownParserState::with_config(config);
45//! ```
46
47mod blocks;
48
49/// Configuration options for Markdown parsing behavior.
50pub mod config;
51mod inline;
52mod link_util;
53mod util;
54
55#[cfg(test)]
56mod tests;
57
58use crate::ast::Document;
59use crate::parser::config::MarkdownParserConfig;
60use nom::{
61 branch::alt,
62 character::complete::{line_ending, space1},
63 combinator::eof,
64 multi::many0,
65 Parser,
66};
67use std::rc::Rc;
68
69/// Parser state containing configuration and shared context
70///
71/// This structure holds the parser configuration and provides shared state
72/// during the parsing process. It's designed to be cloned cheaply using
73/// reference counting for the configuration.
74///
75/// # Examples
76///
77/// ```rust
78/// use markdown_ppp::parser::{MarkdownParserState, config::MarkdownParserConfig};
79///
80/// // Create with default configuration
81/// let state = MarkdownParserState::new();
82///
83/// // Create with custom configuration
84/// let config = MarkdownParserConfig::default();
85/// let state = MarkdownParserState::with_config(config);
86/// ```
87/// Note: This struct is marked `#[non_exhaustive]` to allow adding new fields
88/// in future versions without breaking existing code.
89#[non_exhaustive]
90pub struct MarkdownParserState {
91 /// The parser configuration (reference-counted for efficient cloning)
92 pub config: Rc<MarkdownParserConfig>,
93 /// Whether we are parsing content extracted from a container block (list item, blockquote, etc.)
94 /// When true, fenced code blocks should not strip additional indentation from their content.
95 /// This field is for internal use only.
96 pub(crate) is_nested_block_context: bool,
97 /// Current nesting depth (container blocks + inline elements with nested content).
98 /// Checked against `config.max_nesting_depth` at every `block`/`inline` entry.
99 pub(crate) depth: usize,
100 /// Nesting depth of link labels (`[a [b [c]]]`). Tracked separately because a
101 /// shortcut/collapsed `LinkReference` stores the label content twice, so the AST
102 /// doubles at every level; see `link_util::MAX_LINK_LABEL_DEPTH`.
103 pub(crate) link_label_depth: usize,
104 /// Delimiter index of the slice currently parsed by `inline_many0`/`inline_many1`
105 /// with this state. Nested inline content is parsed with a deeper state and gets
106 /// its own index.
107 pub(crate) inline_index: std::cell::RefCell<Option<Rc<inline::index::InlineIndex>>>,
108}
109
110impl MarkdownParserState {
111 /// Create a new parser state with default configuration
112 ///
113 /// # Examples
114 ///
115 /// ```rust
116 /// use markdown_ppp::parser::MarkdownParserState;
117 ///
118 /// let state = MarkdownParserState::new();
119 /// ```
120 pub fn new() -> Self {
121 Self::default()
122 }
123
124 /// Create a new parser state with the given configuration
125 ///
126 /// # Arguments
127 ///
128 /// * `config` - The parser configuration to use
129 ///
130 /// # Examples
131 ///
132 /// ```rust
133 /// use markdown_ppp::parser::{MarkdownParserState, config::MarkdownParserConfig};
134 ///
135 /// let config = MarkdownParserConfig::default();
136 /// let state = MarkdownParserState::with_config(config);
137 /// ```
138 pub fn with_config(config: MarkdownParserConfig) -> Self {
139 Self {
140 config: Rc::new(config),
141 is_nested_block_context: false,
142 depth: 0,
143 link_label_depth: 0,
144 inline_index: Default::default(),
145 }
146 }
147
148 /// Create a nested parser state for parsing content extracted from container blocks
149 ///
150 /// This method creates a new state that shares the same configuration, marks
151 /// the parsing context as nested and increments the nesting depth. The flag prevents
152 /// double-stripping of indentation when parsing fenced code blocks inside list items,
153 /// blockquotes, etc.
154 pub(crate) fn nested(&self) -> Self {
155 Self {
156 config: self.config.clone(),
157 is_nested_block_context: true,
158 depth: self.depth + 1,
159 link_label_depth: self.link_label_depth,
160 inline_index: Default::default(),
161 }
162 }
163
164 /// Create a state one nesting level deeper, for parsing the content of inline
165 /// elements (emphasis, strikethrough, ...). Does not touch the block context flag.
166 pub(crate) fn deeper(&self) -> Self {
167 Self {
168 config: self.config.clone(),
169 is_nested_block_context: self.is_nested_block_context,
170 depth: self.depth + 1,
171 link_label_depth: self.link_label_depth,
172 inline_index: Default::default(),
173 }
174 }
175
176 /// Like [`Self::deeper`], additionally counting one level of link label nesting.
177 pub(crate) fn deeper_link_label(&self) -> Self {
178 Self {
179 link_label_depth: self.link_label_depth + 1,
180 ..self.deeper()
181 }
182 }
183
184 /// Fail with an unrecoverable `TooLarge` error when the nesting depth exceeds
185 /// `config.max_nesting_depth`. `Failure` (not `Error`) is used so that `alt`,
186 /// `many*`, `not` and `opt` propagate it instead of trying alternatives.
187 pub(crate) fn check_depth<'a>(&self, input: &'a str) -> nom::IResult<&'a str, ()> {
188 if self.depth > self.config.max_nesting_depth {
189 Err(nom::Err::Failure(nom::error::Error::new(
190 input,
191 nom::error::ErrorKind::TooLarge,
192 )))
193 } else {
194 Ok((input, ()))
195 }
196 }
197}
198
199impl Default for MarkdownParserState {
200 fn default() -> Self {
201 Self::with_config(MarkdownParserConfig::default())
202 }
203}
204
205/// Parse a Markdown string into an Abstract Syntax Tree (AST)
206///
207/// This is the main entry point for parsing Markdown text. It processes the input
208/// according to the CommonMark specification with GitHub Flavored Markdown extensions,
209/// returning a fully-typed AST that can be manipulated, analyzed, or rendered.
210///
211/// # Arguments
212///
213/// * `state` - Parser state containing configuration options
214/// * `input` - The Markdown text to parse
215///
216/// # Returns
217///
218/// Returns a `Result` containing either:
219/// - `Ok(Document)` - Successfully parsed AST document
220/// - `Err(nom::Err)` - Parse error with position and context information
221///
222/// # Examples
223///
224/// Basic parsing:
225/// ```rust
226/// use markdown_ppp::parser::{parse_markdown, MarkdownParserState};
227///
228/// let state = MarkdownParserState::new();
229/// let result = parse_markdown(state, "# Hello\n\nWorld!");
230///
231/// match result {
232/// Ok(doc) => println!("Parsed {} blocks", doc.blocks.len()),
233/// Err(e) => eprintln!("Parse error: {:?}", e),
234/// }
235/// ```
236///
237/// With custom configuration:
238/// ```rust
239/// use markdown_ppp::parser::{parse_markdown, MarkdownParserState};
240/// use markdown_ppp::parser::config::*;
241///
242/// let config = MarkdownParserConfig::default()
243/// .with_block_thematic_break_behavior(ElementBehavior::Skip);
244/// let state = MarkdownParserState::with_config(config);
245///
246/// let doc = parse_markdown(state, "---\n\nContent").unwrap();
247/// ```
248///
249/// # Errors
250///
251/// Returns a parse error if the input contains invalid Markdown syntax
252/// that cannot be recovered from. Most malformed Markdown is handled
253/// gracefully according to CommonMark's error handling rules.
254///
255/// Returns `nom::Err::Failure` with [`nom::error::ErrorKind::TooLarge`] when the
256/// nesting depth of the document exceeds
257/// [`MarkdownParserConfig::with_max_nesting_depth`]. This protects against
258/// adversarial input such as thousands of nested `>` markers.
259pub fn parse_markdown(
260 state: MarkdownParserState,
261 input: &str,
262) -> Result<Document, nom::Err<nom::error::Error<&str>>> {
263 let (rest, blocks) = crate::parser::blocks::blocks_many0(Rc::new(state), input)?;
264 let empty_lines = many0(alt((space1, line_ending)));
265 let (_, _) = (empty_lines, eof).parse(rest)?;
266
267 Ok(Document { blocks })
268}