1mod 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
34use 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::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
165pub 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
194pub 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
211pub 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}