markdown_ppp/latex_printer/
mod.rs

1//! LaTeX printer for Markdown AST
2//!
3//! This module provides functionality to render a Markdown Abstract Syntax Tree (AST)
4//! into LaTeX format. The printer supports full CommonMark + GitHub Flavored Markdown
5//! features and offers configurable output styles.
6//!
7//! # Features
8//!
9//! - **Full AST coverage**: All block and inline elements from CommonMark + GFM
10//! - **Configurable table styles**: `tabular`, `longtabu`, `booktabs`
11//! - **Configurable code styles**: `verbatim`, `listings`, `minted`
12//! - **Proper LaTeX escaping**: All special characters are properly escaped
13//! - **GitHub extensions**: Alerts, task lists, footnotes, strikethrough
14//! - **Width control**: Configurable line width for pretty-printing
15//!
16//! # Basic Usage
17//!
18//! ```rust
19//! use markdown_ppp::ast::*;
20//! use markdown_ppp::latex_printer::{render_latex, config::Config};
21//!
22//! let doc = Document {
23//!     blocks: vec![
24//!         Block::Heading(Heading {
25//!             kind: HeadingKind::Atx(1),
26//!             content: vec![Inline::Text("Hello LaTeX".to_string())],
27//!         }),
28//!         Block::Paragraph(vec![
29//!             Inline::Text("This is ".to_string()),
30//!             Inline::Strong(vec![Inline::Text("bold".to_string())]),
31//!             Inline::Text(" and ".to_string()),
32//!             Inline::Emphasis(vec![Inline::Text("italic".to_string())]),
33//!             Inline::Text(" text with special chars: $100 & 50%.".to_string()),
34//!         ]),
35//!     ],
36//! };
37//!
38//! let latex = render_latex(&doc, Config::default());
39//! // Produces:
40//! // \section{Hello LaTeX}
41//! //
42//! // This is \textbf{bold} and \textit{italic} text with special chars: \$100 \& 50\%.
43//! ```
44//!
45//! # Advanced Configuration
46//!
47//! ```rust
48//! # use markdown_ppp::ast::*;
49//! # use markdown_ppp::latex_printer::{render_latex, config::*};
50//! let config = Config::default()
51//!     .with_width(120)
52//!     .with_table_style(TableStyle::Booktabs)
53//!     .with_code_block_style(CodeBlockStyle::Minted);
54//!
55//! # let doc = Document { blocks: vec![] };
56//! let latex = render_latex(&doc, config);
57//! ```
58//!
59//! # LaTeX Element Mappings
60//!
61//! | Markdown          | LaTeX                                |
62//! |-------------------|--------------------------------------|
63//! | `# Heading`       | `\section{Heading}`                 |
64//! | `**bold**`        | `\textbf{bold}`                     |
65//! | `*italic*`        | `\textit{italic}`                   |
66//! | `~~strike~~`      | `\sout{strike}`                     |
67//! | `` `code` ``      | `\texttt{code}`                     |
68//! | `> quote`         | `\begin{quote}...\end{quote}`       |
69//! | `- list`          | `\begin{itemize}...\end{itemize}`   |
70//! | `1. ordered`      | `\begin{enumerate}...\end{enumerate}` |
71//! | `[link](url)`     | `\href{url}{link}`                  |
72//! | `![img](url)`     | `\includegraphics{url}`             |
73//! | Tables            | `\begin{tabular}...` (configurable) |
74//! | Code blocks       | `\begin{verbatim}...` (configurable) |
75
76mod block;
77pub mod config;
78mod inline;
79mod table;
80pub mod util;
81
82#[cfg(test)]
83mod tests;
84
85use crate::ast::*;
86use pretty::{Arena, DocBuilder};
87use std::{collections::HashMap, rc::Rc};
88
89/// Internal state for LaTeX rendering
90///
91/// This structure holds the rendering context including the pretty-printer arena,
92/// configuration, and pre-processed indices for footnotes and link definitions.
93pub(crate) struct State<'a> {
94    arena: Arena<'a>,
95    config: crate::latex_printer::config::Config,
96    /// Mapping of footnote labels to their indices in the footnote list.
97    footnote_index: HashMap<String, usize>,
98    /// Mapping of link labels to their definitions.
99    link_definitions: HashMap<Vec<Inline>, LinkDefinition>,
100}
101
102impl State<'_> {
103    /// Create a new rendering state
104    ///
105    /// This processes the AST to build indices for footnotes and link definitions,
106    /// which are needed for proper cross-referencing during rendering.
107    pub fn new(config: crate::latex_printer::config::Config, ast: &Document) -> Self {
108        let (footnote_index, link_definitions) = get_indices(ast);
109        let arena = Arena::new();
110        Self {
111            arena,
112            config,
113            footnote_index,
114            link_definitions,
115        }
116    }
117
118    /// Get the numeric index for a footnote label
119    ///
120    /// Returns `None` if the footnote is not defined in the document.
121    pub fn get_footnote_index(&self, label: &str) -> Option<&usize> {
122        self.footnote_index.get(label)
123    }
124
125    /// Get the link definition for a reference link
126    ///
127    /// Returns `None` if the link reference is not defined in the document.
128    pub fn get_link_definition(&self, label: &Vec<Inline>) -> Option<&LinkDefinition> {
129        self.link_definitions.get(label)
130    }
131}
132
133/// Render the given Markdown AST to LaTeX
134///
135/// This is the main entry point for LaTeX rendering. It takes a parsed Markdown
136/// document and configuration, then produces LaTeX source code.
137///
138/// # Arguments
139///
140/// * `ast` - The parsed Markdown document as an AST
141/// * `config` - Configuration for rendering (table styles, code styles, width, etc.)
142///
143/// # Returns
144///
145/// LaTeX source code as a string. This will be a document fragment suitable
146/// for inclusion in a larger LaTeX document, not a complete document with
147/// `\documentclass` etc.
148///
149/// # Examples
150///
151/// ```rust
152/// use markdown_ppp::ast::*;
153/// use markdown_ppp::latex_printer::{render_latex, config::Config};
154///
155/// let doc = Document {
156///     blocks: vec![
157///         Block::Paragraph(vec![
158///             Inline::Text("Visit ".to_string()),
159///             Inline::Link(Link {
160///                 destination: "https://example.com".to_string(),
161///                 title: None,
162///                 children: vec![Inline::Text("this link".to_string())],
163///             }),
164///             Inline::Text(" for more info.".to_string()),
165///         ]),
166///         Block::List(List {
167///             kind: ListKind::Bullet(ListBulletKind::Star),
168///             items: vec![ListItem {
169///                 task: Some(TaskState::Complete),
170///                 blocks: vec![Block::Paragraph(vec![
171///                     Inline::Strong(vec![Inline::Text("Bold".to_string())]),
172///                     Inline::Text(" item with special chars: $100 & 50%".to_string()),
173///                 ])],
174///             }],
175///         }),
176///     ],
177/// };
178///
179/// let latex = render_latex(&doc, Config::default());
180/// // Produces LaTeX with proper escaping and formatting:
181/// // Visit \href{https://example.com}{this link} for more info.
182/// //
183/// // \begin{itemize}
184/// // \item $\boxtimes$ \textbf{Bold} item with special chars: \$100 \& 50\%
185/// // \end{itemize}
186/// ```
187///
188/// # LaTeX Packages Required
189///
190/// The generated LaTeX may require these packages depending on features used:
191///
192/// - `hyperref` - for links (`\href`)
193/// - `graphicx` - for images (`\includegraphics`)
194/// - `ulem` - for strikethrough (`\sout`)
195/// - `booktabs` - if using booktabs table style
196/// - `longtabu` - if using longtabu table style
197/// - `listings` - if using listings code style
198/// - `minted` - if using minted code style
199pub fn render_latex(ast: &Document, config: crate::latex_printer::config::Config) -> String {
200    let state = Rc::new(State::new(config, ast));
201    let doc = ast.to_doc(&state);
202
203    let mut buf = Vec::new();
204    doc.render(state.config.width, &mut buf).unwrap();
205    String::from_utf8(buf).unwrap()
206}
207
208/// Internal trait for converting AST nodes to pretty-printer documents
209///
210/// This trait is implemented by all AST node types and provides the core
211/// rendering logic for each element type.
212trait ToDoc<'a> {
213    /// Convert this AST node to a pretty-printer document
214    fn to_doc(&self, state: &'a State<'a>) -> DocBuilder<'a, Arena<'a>, ()>;
215}
216
217impl<'a> ToDoc<'a> for Document {
218    fn to_doc(&self, state: &'a State<'a>) -> DocBuilder<'a, Arena<'a>, ()> {
219        self.blocks.to_doc(state)
220    }
221}
222
223/// Extract footnote and link definition indices from the document
224///
225/// This function performs a pre-processing pass over the AST to:
226/// 1. Assign numeric indices to footnote definitions (1, 2, 3, ...)
227/// 2. Collect link definitions for reference link resolution
228///
229/// Returns a tuple of (footnote_index, link_definitions) where:
230/// - footnote_index maps footnote labels to their numeric indices
231/// - link_definitions maps link labels to their full definitions
232fn get_indices(ast: &Document) -> (HashMap<String, usize>, HashMap<Vec<Inline>, LinkDefinition>) {
233    let mut footnote_index = HashMap::new();
234    let mut link_definitions = HashMap::new();
235    let mut footnote_counter = 1;
236
237    fn process_blocks(
238        blocks: &[Block],
239        footnote_index: &mut HashMap<String, usize>,
240        link_definitions: &mut HashMap<Vec<Inline>, LinkDefinition>,
241        footnote_counter: &mut usize,
242    ) {
243        for block in blocks {
244            match block {
245                Block::FootnoteDefinition(def) => {
246                    footnote_index.insert(def.label.clone(), *footnote_counter);
247                    *footnote_counter += 1;
248                }
249                Block::Definition(def) => {
250                    link_definitions.insert(def.label.clone(), def.clone());
251                }
252                Block::List(list) => {
253                    for item in &list.items {
254                        process_blocks(
255                            &item.blocks,
256                            footnote_index,
257                            link_definitions,
258                            footnote_counter,
259                        );
260                    }
261                }
262                Block::BlockQuote(blocks) => {
263                    process_blocks(blocks, footnote_index, link_definitions, footnote_counter);
264                }
265                Block::GitHubAlert(alert) => {
266                    process_blocks(
267                        &alert.blocks,
268                        footnote_index,
269                        link_definitions,
270                        footnote_counter,
271                    );
272                }
273                _ => {}
274            }
275        }
276    }
277
278    process_blocks(
279        &ast.blocks,
280        &mut footnote_index,
281        &mut link_definitions,
282        &mut footnote_counter,
283    );
284
285    (footnote_index, link_definitions)
286}