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//! | `` | `\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}