pulldown_html_ext/
lib.rs

1//! pulldown-html-ext
2//!
3//! A configurable Markdown to HTML renderer built on top of pulldown-cmark.
4//!
5//! # Documentation
6//!
7//! - [API Documentation](https://docs.rs/pulldown-html-ext)
8//! - [User Guide](https://systemsoverload.github.io/pulldown-html-ext)
9//! - [Examples](https://systemsoverload.github.io/pulldown-html-ext/examples)
10//!
11//! # Quick Start
12//!
13//! ```rust
14//! use pulldown_html_ext::{HtmlConfig, push_html};
15//! use pulldown_cmark::Parser;
16//!
17//! let config = HtmlConfig::default();
18//! let markdown = "# Hello\nThis is *markdown*";
19//! let parser = Parser::new(markdown);
20//! let mut output = String::new();
21//! let html = push_html(&mut output, parser, &config).unwrap();
22//! ```
23//!
24//! Custom rendering with a custom writer:
25//! ```rust
26//! use pulldown_html_ext::{HtmlConfig, HtmlWriter, HtmlState, HtmlRenderer};
27//! use pulldown_cmark_escape::{StrWrite, FmtWriter};
28//!
29//! struct CustomWriter<W: StrWrite> {
30//!     writer: W,
31//!     config: HtmlConfig,
32//!     state: HtmlState,
33//! }
34//!
35//! impl<W: StrWrite> CustomWriter<W> {
36//!     fn new(writer: W, config: HtmlConfig) -> Self {
37//!         Self {
38//!             writer,
39//!             config,
40//!             state: HtmlState::new(),
41//!         }
42//!     }
43//! }
44//!
45//! impl<W: StrWrite> HtmlWriter<W> for CustomWriter<W> {
46//!     fn get_writer(&mut self) -> &mut W {
47//!         &mut self.writer
48//!     }
49//!
50//!     fn get_config(&self) -> &HtmlConfig {
51//!         &self.config
52//!     }
53//!
54//!     fn get_state(&mut self) -> &mut HtmlState {
55//!         &mut self.state
56//!     }
57//! }
58//!
59//! let mut output = String::new();
60//! let writer = CustomWriter::new(
61//!     FmtWriter(&mut output),
62//!     HtmlConfig::default()
63//! );
64//! let mut renderer = HtmlRenderer::new(writer);
65//!
66//! // Use the renderer with a parser
67//! use pulldown_cmark::Parser;
68//! let markdown = "# Hello\nThis is *markdown*";
69//! let parser = Parser::new(markdown);
70//! renderer.run(parser);
71//!
72//! assert!(output.contains("<h1"));
73//! ```
74
75mod html;
76pub mod utils;
77pub use html::{
78    push_html, push_html_with_highlighting, write_html_fmt, write_html_io, AttributeMappings,
79    CodeBlockOptions, DefaultHtmlWriter, ElementOptions, HeadingOptions, HtmlConfig, HtmlError,
80    HtmlOptions, HtmlRenderer, HtmlState, HtmlWriter, HtmlWriterBase, LinkOptions, SyntectConfig,
81    SyntectConfigStyle, SyntectWriter,
82};
83pub use pulldown_html_ext_derive::html_writer;
84
85#[cfg(test)]
86mod tests_lib {
87    use super::*;
88    use pulldown_cmark::Parser;
89    use std::collections::HashMap;
90
91    #[test]
92    fn test_basic_markdown() {
93        let config = HtmlConfig::default();
94        let markdown = "# Hello\nThis is a test.";
95        let parser = Parser::new(markdown);
96        let mut output = String::new();
97
98        push_html(&mut output, parser, &config).unwrap();
99
100        assert!(output.contains("<h1"));
101        assert!(output.contains("Hello"));
102        assert!(output.contains("<p>"));
103        assert!(output.contains("This is a test."));
104    }
105
106    #[test]
107    fn test_custom_heading_classes() {
108        let mut config = HtmlConfig::default();
109        config.elements.headings.level_classes = {
110            let mut map = HashMap::new();
111            map.insert(1, "title".to_string());
112            map.insert(2, "subtitle".to_string());
113            map
114        };
115
116        let markdown = "# Main Title\n## Subtitle";
117        let parser = Parser::new(markdown);
118        let mut output = String::new();
119
120        push_html(&mut output, parser, &config).unwrap();
121
122        assert!(output.contains(r#"<h1 id="heading-1" class="title""#));
123        assert!(output.contains(r#"<h2 id="heading-2" class="subtitle""#));
124    }
125
126    #[test]
127    fn test_code_blocks() {
128        let mut config = HtmlConfig::default();
129        config.elements.code_blocks.default_language = Some("text".to_string());
130
131        let markdown = "```python\nprint('hello')\n```";
132        let parser = Parser::new(markdown);
133        let mut output = String::new();
134
135        push_html(&mut output, parser, &config).unwrap();
136        assert!(output.contains(r#"<code class="language-python">"#));
137
138        let markdown = "```\nplain text\n```";
139        let parser = Parser::new(markdown);
140        let mut output = String::new();
141
142        push_html(&mut output, parser, &config).unwrap();
143        assert!(output.contains(r#"<code class="language-text">"#));
144    }
145
146    #[test]
147    fn test_external_links() {
148        let mut config = HtmlConfig::default();
149        config.elements.links.nofollow_external = true;
150        config.elements.links.open_external_blank = true;
151
152        let markdown = "[External](https://example.com)";
153        let parser = Parser::new(markdown);
154        let mut output = String::new();
155
156        push_html(&mut output, parser, &config).unwrap();
157        assert!(output.contains(r#"rel="nofollow""#));
158        assert!(output.contains(r#"target="_blank""#));
159
160        let markdown = "[Internal](/local)";
161        let parser = Parser::new(markdown);
162        let mut output = String::new();
163
164        push_html(&mut output, parser, &config).unwrap();
165        assert!(!output.contains(r#"rel="nofollow""#));
166        assert!(!output.contains(r#"target="_blank""#));
167    }
168
169    #[test]
170    fn test_html_options() {
171        let mut config = HtmlConfig::default();
172        config.html.escape_html = true;
173        config.html.break_on_newline = false;
174        config.html.xhtml_style = true;
175
176        let markdown = "Test & test\nNew line";
177        let parser = Parser::new(markdown);
178        let mut output = String::new();
179
180        push_html(&mut output, parser, &config).unwrap();
181        assert!(output.contains("&amp;"));
182        assert!(!output.contains("<br"));
183    }
184
185    #[test]
186    fn test_custom_parser_options() {
187        use pulldown_cmark::{Options, Parser};
188
189        let mut options = Options::empty();
190        options.insert(Options::ENABLE_STRIKETHROUGH);
191
192        let markdown = "~~strikethrough~~";
193        let parser = Parser::new_ext(markdown, options);
194        let mut output = String::new();
195        let config = HtmlConfig::default();
196
197        push_html(&mut output, parser, &config).unwrap();
198        assert!(output.contains("<del>"));
199        assert!(output.contains("</del>"));
200    }
201
202    #[test]
203    fn test_streaming_parser() {
204        let config = HtmlConfig::default();
205        let mut output = String::new();
206
207        // Simulate streaming input by creating multiple parsers
208        let chunk1 = "# Title\n";
209        let chunk2 = "Paragraph 1\n";
210        let chunk3 = "* List item";
211
212        let parser1 = Parser::new(chunk1);
213        push_html(&mut output, parser1, &config).unwrap();
214
215        let parser2 = Parser::new(chunk2);
216        push_html(&mut output, parser2, &config).unwrap();
217
218        let parser3 = Parser::new(chunk3);
219        push_html(&mut output, parser3, &config).unwrap();
220
221        assert!(output.contains("<h1"));
222        assert!(output.contains("<p>"));
223        assert!(output.contains("<li>"));
224    }
225}