pulldown_html_ext/html/
mod.rs

1//! HTML rendering functionality for Markdown content.
2//!
3//! This module provides configurable HTML rendering capabilities built on top
4//! of pulldown-cmark's event model. It supports customized rendering of HTML
5//! elements, attribute handling, and state management during rendering.
6
7mod config;
8mod default;
9mod error;
10mod state;
11mod writer;
12
13#[cfg(feature = "syntect")]
14mod syntect;
15#[cfg(feature = "syntect")]
16pub use self::syntect::{
17    push_html_with_highlighting, SyntectConfig, SyntectConfigStyle, SyntectWriter,
18};
19use pulldown_cmark::{Event, Tag, TagEnd};
20use pulldown_cmark_escape::{FmtWriter, IoWriter, StrWrite};
21use std::iter::Peekable;
22
23pub use self::config::{
24    AttributeMappings, CodeBlockOptions, ElementOptions, HeadingOptions, HtmlConfig, HtmlOptions,
25    LinkOptions,
26};
27pub use self::default::{DefaultHtmlWriter, HtmlWriterBase};
28pub use self::error::HtmlError;
29pub use self::state::{HtmlState, ListContext, TableContext};
30pub use self::writer::HtmlWriter;
31
32pub type Result<T> = std::result::Result<T, HtmlError>;
33
34/// Core renderer that processes Markdown events into HTML
35use std::marker::PhantomData;
36
37pub struct HtmlRenderer<W: StrWrite, H: HtmlWriter<W>> {
38    pub(crate) writer: H,
39    _phantom: PhantomData<W>,
40}
41
42impl<W: StrWrite, H: HtmlWriter<W>> HtmlRenderer<W, H> {
43    pub fn new(writer: H) -> Self {
44        Self {
45            writer,
46            _phantom: PhantomData,
47        }
48    }
49
50    pub fn run<'a, I>(&mut self, iter: I) -> Result<()>
51    where
52        I: Iterator<Item = Event<'a>>,
53    {
54        let mut iter = iter.peekable();
55        while let Some(event) = iter.next() {
56            match event {
57                Event::Start(tag) => self.handle_start(&mut iter, tag)?,
58                Event::End(tag) => self.handle_end(tag)?,
59                Event::Text(text) => self.writer.text(&text)?,
60                Event::Code(text) => self.handle_inline_code(&text)?,
61                Event::Html(html) => self.writer.write_str(&html)?,
62                Event::SoftBreak => self.writer.soft_break()?,
63                Event::HardBreak => self.writer.hard_break()?,
64                Event::Rule => self.writer.horizontal_rule()?,
65                Event::FootnoteReference(name) => self.writer.footnote_reference(&name)?,
66                Event::TaskListMarker(checked) => self.writer.task_list_item(checked)?,
67                Event::InlineMath(_) | Event::DisplayMath(_) | Event::InlineHtml(_) => todo!(),
68            }
69        }
70        Ok(())
71    }
72
73    fn handle_start<'a, I>(
74        &mut self,
75        iter: &mut Peekable<I>,
76        tag: pulldown_cmark::Tag<'a>,
77    ) -> Result<()>
78    where
79        I: Iterator<Item = Event<'a>>,
80    {
81        match tag {
82            Tag::Paragraph => self.writer.start_paragraph()?,
83            Tag::Heading {
84                level,
85                id,
86                classes,
87                attrs,
88            } => self
89                .writer
90                .start_heading(level, id.as_deref(), &classes, &attrs)?,
91            Tag::BlockQuote(_) => self.writer.start_blockquote()?,
92            Tag::CodeBlock(kind) => self.writer.start_code_block(kind)?,
93            Tag::List(start) => self.writer.start_list(start)?,
94            Tag::Item => self.writer.start_list_item()?,
95            Tag::FootnoteDefinition(name) => self.writer.start_footnote_definition(&name)?,
96            Tag::Table(alignments) => self.writer.start_table(alignments)?,
97            Tag::TableHead => self.writer.start_table_head()?,
98            Tag::TableRow => self.writer.start_table_row()?,
99            Tag::TableCell => self.writer.start_table_cell()?,
100            Tag::Emphasis => self.writer.start_emphasis()?,
101            Tag::Strong => self.writer.start_strong()?,
102            Tag::Strikethrough => self.writer.start_strikethrough()?,
103            Tag::Link {
104                link_type,
105                dest_url,
106                title,
107                id: _,
108            } => self.writer.start_link(link_type, &dest_url, &title)?,
109            Tag::Image {
110                link_type,
111                dest_url,
112                title,
113                id: _,
114            } => self
115                .writer
116                .start_image(link_type, &dest_url, &title, iter)?,
117
118            Tag::DefinitionList => self.writer.start_definition_list()?,
119            Tag::DefinitionListTitle => self.writer.start_definition_list_title()?,
120            Tag::DefinitionListDefinition => self.writer.start_definition_list_definition()?,
121
122            Tag::MetadataBlock(kind) => self.writer.start_metadata_block(&kind)?,
123            Tag::HtmlBlock => (),
124        }
125        Ok(())
126    }
127
128    fn handle_end(&mut self, tag: TagEnd) -> Result<()> {
129        match tag {
130            TagEnd::Paragraph => self.writer.end_paragraph()?,
131            TagEnd::Heading(level) => self.writer.end_heading(level)?,
132            TagEnd::BlockQuote(_) => self.writer.end_blockquote()?,
133            TagEnd::CodeBlock => self.writer.end_code_block()?,
134            TagEnd::List(b) => self.writer.end_list(b)?,
135            // TagEnd::List(None) => self.writer.end_list(false)?,
136            TagEnd::Item => self.writer.end_list_item()?,
137            TagEnd::FootnoteDefinition => self.writer.end_footnote_definition()?,
138            TagEnd::Table => self.writer.end_table()?,
139            TagEnd::TableHead => self.writer.end_table_head()?,
140            TagEnd::TableRow => self.writer.end_table_row()?,
141            TagEnd::TableCell => self.writer.end_table_cell()?,
142            TagEnd::Emphasis => self.writer.end_emphasis()?,
143            TagEnd::Strong => self.writer.end_strong()?,
144            TagEnd::Strikethrough => self.writer.end_strikethrough()?,
145            TagEnd::Link {} => self.writer.end_link()?,
146            TagEnd::Image {} => self.writer.end_image()?,
147            TagEnd::DefinitionList => self.writer.end_definition_list()?,
148            TagEnd::DefinitionListTitle => self.writer.end_definition_list_title()?,
149            TagEnd::DefinitionListDefinition => self.writer.end_definition_list_title()?,
150
151            TagEnd::MetadataBlock(_) => self.writer.end_metadata_block()?,
152            TagEnd::HtmlBlock => (),
153        }
154        Ok(())
155    }
156
157    fn handle_inline_code(&mut self, text: &str) -> Result<()> {
158        self.writer.start_inline_code()?;
159        self.writer.text(text)?;
160        self.writer.end_inline_code()?;
161        Ok(())
162    }
163}
164
165/// Renders markdown events to HTML and appends to the provided string
166///
167/// # Arguments
168///
169/// * `output` - String buffer to append the HTML output to
170/// * `iter` - Iterator of markdown events to process
171/// * `config` - Configuration for HTML rendering
172///
173/// # Example
174///
175/// ```rust
176/// use pulldown_cmark::Parser;
177/// use pulldown_html_ext::{HtmlConfig, push_html};
178///
179/// let markdown = "# Hello\n* Item 1\n* Item 2";
180/// let parser = Parser::new(markdown);
181/// let mut output = String::new();
182/// let config = HtmlConfig::default();
183///
184/// push_html(&mut output, parser, &config).unwrap();
185/// assert!(output.contains("<h1"));
186/// ```
187pub fn push_html<'a, I>(output: &mut String, iter: I, config: &HtmlConfig) -> Result<()>
188where
189    I: Iterator<Item = Event<'a>>,
190{
191    write_html_fmt(output, iter, config)
192}
193
194/// Renders markdown events to HTML using a fmt::Write implementation
195///
196/// # Arguments
197///
198/// * `writer` - Any type implementing fmt::Write
199/// * `iter` - Iterator of markdown events to process
200/// * `config` - Configuration for HTML rendering
201pub fn write_html_fmt<'a, W, I>(writer: W, iter: I, config: &HtmlConfig) -> Result<()>
202where
203    W: std::fmt::Write,
204    I: Iterator<Item = Event<'a>>,
205{
206    let writer = DefaultHtmlWriter::new(FmtWriter(writer), config.clone());
207    let mut renderer = HtmlRenderer::new(writer);
208    renderer.run(iter)
209}
210
211/// Renders markdown events to HTML using an io::Write implementation
212///
213/// # Arguments
214///
215/// * `writer` - Any type implementing io::Write
216/// * `iter` - Iterator of markdown events to process
217/// * `config` - Configuration for HTML rendering
218pub fn write_html_io<'a, W, I>(writer: W, iter: I, config: &HtmlConfig) -> Result<()>
219where
220    W: std::io::Write,
221    I: Iterator<Item = Event<'a>>,
222{
223    let writer = DefaultHtmlWriter::new(IoWriter(writer), config.clone());
224    let mut renderer = HtmlRenderer::new(writer);
225    renderer.run(iter)
226}
227
228#[cfg(test)]
229mod tests_mod {
230    use super::*;
231    use html_compare_rs::assert_html_eq;
232    use pulldown_cmark::Parser;
233
234    #[test]
235    fn test_push_html() {
236        let markdown = "# Hello\n\nThis is a test.";
237        let parser = Parser::new(markdown);
238        let mut output = String::new();
239        let config = HtmlConfig::default();
240
241        push_html(&mut output, parser, &config).unwrap();
242
243        assert_html_eq!(
244            output,
245            r#"<h1 id="heading-1">Hello</h1><p>This is a test.</p>"#
246        );
247    }
248
249    #[test]
250    fn test_write_html_fmt() {
251        let markdown = "# Test\n* Item 1\n* Item 2";
252        let parser = Parser::new(markdown);
253        let mut output = String::new();
254        let config = HtmlConfig::default();
255
256        write_html_fmt(&mut output, parser, &config).unwrap();
257
258        assert_html_eq!(
259            output,
260            r#"<h1 id="heading-1">Test</h1><ul><li>Item 1</li><li>Item 2</li></ul>"#
261        );
262    }
263
264    #[test]
265    fn test_write_html_io() {
266        let markdown = "# Test";
267        let parser = Parser::new(markdown);
268        let mut output = Vec::new();
269        let config = HtmlConfig::default();
270
271        write_html_io(&mut output, parser, &config).unwrap();
272
273        let result = String::from_utf8(output).unwrap();
274        assert_html_eq!(result, r#"<h1 id="heading-1">Test</h1>"#);
275    }
276
277    #[test]
278    fn test_with_syntax_highlighting() {
279        let markdown = "```rust\nfn main() {\n    println!(\"Hello\");\n}\n```";
280        let parser = Parser::new(markdown);
281        let mut output = String::new();
282        let config = HtmlConfig::default();
283
284        push_html(&mut output, parser, &config).unwrap();
285
286        assert!(output.contains(r#"<code class="language-rust">"#));
287        assert!(output.contains("println"));
288    }
289}