Skip to main content

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    sequence::terminated,
66    Parser,
67};
68use std::rc::Rc;
69
70/// Parser state containing configuration and shared context
71///
72/// This structure holds the parser configuration and provides shared state
73/// during the parsing process. It's designed to be cloned cheaply using
74/// reference counting for the configuration.
75///
76/// # Examples
77///
78/// ```rust
79/// use markdown_ppp::parser::{MarkdownParserState, config::MarkdownParserConfig};
80///
81/// // Create with default configuration
82/// let state = MarkdownParserState::new();
83///
84/// // Create with custom configuration
85/// let config = MarkdownParserConfig::default();
86/// let state = MarkdownParserState::with_config(config);
87/// ```
88/// Note: This struct is marked `#[non_exhaustive]` to allow adding new fields
89/// in future versions without breaking existing code.
90#[non_exhaustive]
91pub struct MarkdownParserState {
92    /// The parser configuration (reference-counted for efficient cloning)
93    pub config: Rc<MarkdownParserConfig>,
94    /// Whether we are parsing content extracted from a container block (list item, blockquote, etc.)
95    /// When true, fenced code blocks should not strip additional indentation from their content.
96    /// This field is for internal use only.
97    pub(crate) is_nested_block_context: bool,
98    /// Current nesting depth (container blocks + inline elements with nested content).
99    /// Checked against `config.max_nesting_depth` at every `block`/`inline` entry.
100    pub(crate) depth: usize,
101    /// Nesting depth of link labels (`[a [b [c]]]`). Tracked separately because a
102    /// shortcut/collapsed `LinkReference` stores the label content twice, so the AST
103    /// doubles at every level; see `link_util::MAX_LINK_LABEL_DEPTH`.
104    pub(crate) link_label_depth: usize,
105    /// Delimiter index of the slice currently parsed by `inline_many0`/`inline_many1`
106    /// with this state. Nested inline content is parsed with a deeper state and gets
107    /// its own index.
108    pub(crate) inline_index: std::cell::RefCell<Option<Rc<inline::index::InlineIndex>>>,
109}
110
111impl MarkdownParserState {
112    /// Create a new parser state with default configuration
113    ///
114    /// # Examples
115    ///
116    /// ```rust
117    /// use markdown_ppp::parser::MarkdownParserState;
118    ///
119    /// let state = MarkdownParserState::new();
120    /// ```
121    pub fn new() -> Self {
122        Self::default()
123    }
124
125    /// Create a new parser state with the given configuration
126    ///
127    /// # Arguments
128    ///
129    /// * `config` - The parser configuration to use
130    ///
131    /// # Examples
132    ///
133    /// ```rust
134    /// use markdown_ppp::parser::{MarkdownParserState, config::MarkdownParserConfig};
135    ///
136    /// let config = MarkdownParserConfig::default();
137    /// let state = MarkdownParserState::with_config(config);
138    /// ```
139    pub fn with_config(config: MarkdownParserConfig) -> Self {
140        Self {
141            config: Rc::new(config),
142            is_nested_block_context: false,
143            depth: 0,
144            link_label_depth: 0,
145            inline_index: Default::default(),
146        }
147    }
148
149    /// Create a nested parser state for parsing content extracted from container blocks
150    ///
151    /// This method creates a new state that shares the same configuration, marks
152    /// the parsing context as nested and increments the nesting depth. The flag prevents
153    /// double-stripping of indentation when parsing fenced code blocks inside list items,
154    /// blockquotes, etc.
155    pub(crate) fn nested(&self) -> Self {
156        Self {
157            config: self.config.clone(),
158            is_nested_block_context: true,
159            depth: self.depth + 1,
160            link_label_depth: self.link_label_depth,
161            inline_index: Default::default(),
162        }
163    }
164
165    /// Create a state one nesting level deeper, for parsing the content of inline
166    /// elements (emphasis, strikethrough, ...). Does not touch the block context flag.
167    pub(crate) fn deeper(&self) -> Self {
168        Self {
169            config: self.config.clone(),
170            is_nested_block_context: self.is_nested_block_context,
171            depth: self.depth + 1,
172            link_label_depth: self.link_label_depth,
173            inline_index: Default::default(),
174        }
175    }
176
177    /// Like [`Self::deeper`], additionally counting one level of link label nesting.
178    pub(crate) fn deeper_link_label(&self) -> Self {
179        Self {
180            link_label_depth: self.link_label_depth + 1,
181            ..self.deeper()
182        }
183    }
184
185    /// Fail with an unrecoverable `TooLarge` error when the nesting depth exceeds
186    /// `config.max_nesting_depth`. `Failure` (not `Error`) is used so that `alt`,
187    /// `many*`, `not` and `opt` propagate it instead of trying alternatives.
188    pub(crate) fn check_depth<'a>(&self, input: &'a str) -> nom::IResult<&'a str, ()> {
189        if self.depth > self.config.max_nesting_depth {
190            Err(nom::Err::Failure(nom::error::Error::new(
191                input,
192                nom::error::ErrorKind::TooLarge,
193            )))
194        } else {
195            Ok((input, ()))
196        }
197    }
198}
199
200impl Default for MarkdownParserState {
201    fn default() -> Self {
202        Self::with_config(MarkdownParserConfig::default())
203    }
204}
205
206/// Parse a Markdown string into an Abstract Syntax Tree (AST)
207///
208/// This is the main entry point for parsing Markdown text. It processes the input
209/// according to the CommonMark specification with GitHub Flavored Markdown extensions,
210/// returning a fully-typed AST that can be manipulated, analyzed, or rendered.
211///
212/// # Arguments
213///
214/// * `state` - Parser state containing configuration options
215/// * `input` - The Markdown text to parse
216///
217/// # Returns
218///
219/// Returns a `Result` containing either:
220/// - `Ok(Document)` - Successfully parsed AST document
221/// - `Err(nom::Err)` - Parse error with position and context information
222///
223/// # Examples
224///
225/// Basic parsing:
226/// ```rust
227/// use markdown_ppp::parser::{parse_markdown, MarkdownParserState};
228///
229/// let state = MarkdownParserState::new();
230/// let result = parse_markdown(state, "# Hello\n\nWorld!");
231///
232/// match result {
233///     Ok(doc) => println!("Parsed {} blocks", doc.blocks.len()),
234///     Err(e) => eprintln!("Parse error: {:?}", e),
235/// }
236/// ```
237///
238/// With custom configuration:
239/// ```rust
240/// use markdown_ppp::parser::{parse_markdown, MarkdownParserState};
241/// use markdown_ppp::parser::config::*;
242///
243/// let config = MarkdownParserConfig::default()
244///     .with_block_thematic_break_behavior(ElementBehavior::Skip);
245/// let state = MarkdownParserState::with_config(config);
246///
247/// let doc = parse_markdown(state, "---\n\nContent").unwrap();
248/// ```
249///
250/// # Errors
251///
252/// Returns a parse error if the input contains invalid Markdown syntax
253/// that cannot be recovered from. Most malformed Markdown is handled
254/// gracefully according to CommonMark's error handling rules.
255///
256/// Returns `nom::Err::Failure` with [`nom::error::ErrorKind::TooLarge`] when the
257/// nesting depth of the document exceeds
258/// [`MarkdownParserConfig::with_max_nesting_depth`]. This protects against
259/// adversarial input such as thousands of nested `>` markers.
260pub fn parse_markdown(
261    state: MarkdownParserState,
262    input: &str,
263) -> Result<Document, nom::Err<nom::error::Error<&str>>> {
264    let empty_lines = many0(alt((space1, line_ending)));
265    let mut parser = terminated(
266        many0(crate::parser::blocks::block(Rc::new(state))),
267        (empty_lines, eof),
268    );
269    let (_, blocks) = parser.parse(input)?;
270
271    let blocks = blocks.into_iter().flatten().collect();
272
273    Ok(Document { blocks })
274}