Skip to content

Commit 7c98421

Browse files
author
tjk
committed
Reorganize create to better align naming with pulldown-cmark
1 parent f17a1f4 commit 7c98421

File tree

8 files changed

+752
-416
lines changed

8 files changed

+752
-416
lines changed
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
use crate::config::HtmlConfig;
2-
use crate::renderer_state::RendererState;
3-
use crate::tag_handler::HtmlWriter;
1+
use crate::html::config::HtmlConfig;
2+
use crate::html::state::HtmlState;
3+
use crate::html::writer::HtmlWriter;
44

55
pub struct DefaultHtmlWriter<'a> {
6-
pub(crate) state: RendererState,
6+
pub(crate) state: HtmlState,
77
pub(crate) config: &'a HtmlConfig,
88
pub(crate) output: &'a mut String,
99
}
1010

1111
impl<'a> DefaultHtmlWriter<'a> {
1212
pub fn new(output: &'a mut String, config: &'a HtmlConfig) -> Self {
1313
Self {
14-
state: RendererState::new(),
14+
state: HtmlState::new(),
1515
config,
1616
output,
1717
}
@@ -27,7 +27,7 @@ impl<'a> HtmlWriter for DefaultHtmlWriter<'a> {
2727
self.output
2828
}
2929

30-
fn get_state(&mut self) -> &mut RendererState {
30+
fn get_state(&mut self) -> &mut HtmlState {
3131
&mut self.state
3232
}
3333
}

lib/src/html/mod.rs

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
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+
7+
mod config;
8+
mod default;
9+
mod state;
10+
mod writer;
11+
12+
use pulldown_cmark::{Event, Parser};
13+
use std::iter::Peekable;
14+
15+
pub use self::config::{
16+
AttributeMappings, CodeBlockOptions, ElementOptions, HeadingOptions, HtmlConfig, HtmlOptions,
17+
LinkOptions,
18+
};
19+
pub use self::default::DefaultHtmlWriter;
20+
pub use self::state::{HtmlState, ListContext, TableContext};
21+
pub use self::writer::HtmlWriter;
22+
23+
/// Core renderer that processes Markdown events into HTML
24+
pub struct HtmlRenderer<W: HtmlWriter> {
25+
writer: W,
26+
}
27+
28+
impl<W: HtmlWriter> HtmlRenderer<W> {
29+
/// Create a new renderer with the given HTML writer
30+
pub fn new(writer: W) -> Self {
31+
Self { writer }
32+
}
33+
34+
/// Process the event stream and generate HTML output
35+
pub fn run<'a, I>(&mut self, iter: I)
36+
where
37+
I: Iterator<Item = Event<'a>>,
38+
{
39+
let mut iter = iter.peekable();
40+
while let Some(event) = iter.next() {
41+
match event {
42+
Event::Start(tag) => self.handle_start(&mut iter, tag),
43+
Event::End(tag) => self.handle_end(tag),
44+
Event::Text(text) => self.writer.text(&text),
45+
Event::Code(text) => self.handle_inline_code(&text),
46+
Event::Html(html) => self.writer.html_raw(&html),
47+
Event::SoftBreak => self.writer.soft_break(),
48+
Event::HardBreak => self.writer.hard_break(),
49+
Event::Rule => self.writer.horizontal_rule(),
50+
Event::FootnoteReference(name) => self.writer.footnote_reference(&name),
51+
Event::TaskListMarker(checked) => self.writer.task_list_item(checked),
52+
}
53+
}
54+
}
55+
56+
/// Handle start tags with potential lookahead
57+
fn handle_start<'a, I>(&mut self, iter: &mut Peekable<I>, tag: pulldown_cmark::Tag<'a>)
58+
where
59+
I: Iterator<Item = Event<'a>>,
60+
{
61+
match tag {
62+
pulldown_cmark::Tag::Paragraph => self.writer.start_paragraph(),
63+
pulldown_cmark::Tag::Heading(level, id, classes) => {
64+
self.writer.start_heading(level, id, classes)
65+
}
66+
pulldown_cmark::Tag::BlockQuote => self.writer.start_blockquote(),
67+
pulldown_cmark::Tag::CodeBlock(kind) => self.writer.start_code_block(kind),
68+
pulldown_cmark::Tag::List(start) => self.writer.start_list(start),
69+
pulldown_cmark::Tag::Item => self.writer.start_list_item(),
70+
pulldown_cmark::Tag::FootnoteDefinition(name) => {
71+
self.writer.start_footnote_definition(&name)
72+
}
73+
pulldown_cmark::Tag::Table(alignments) => self.writer.start_table(alignments),
74+
pulldown_cmark::Tag::TableHead => self.writer.start_table_head(),
75+
pulldown_cmark::Tag::TableRow => self.writer.start_table_row(),
76+
pulldown_cmark::Tag::TableCell => self.writer.start_table_cell(),
77+
pulldown_cmark::Tag::Emphasis => self.writer.start_emphasis(),
78+
pulldown_cmark::Tag::Strong => self.writer.start_strong(),
79+
pulldown_cmark::Tag::Strikethrough => self.writer.start_strikethrough(),
80+
pulldown_cmark::Tag::Link(link_type, dest, title) => {
81+
self.writer.start_link(link_type, &dest, &title)
82+
}
83+
pulldown_cmark::Tag::Image(link_type, dest, title) => {
84+
self.writer.start_image(link_type, &dest, &title, iter)
85+
}
86+
}
87+
}
88+
89+
/// Handle end tags
90+
fn handle_end(&mut self, tag: pulldown_cmark::Tag) {
91+
match tag {
92+
pulldown_cmark::Tag::Paragraph => self.writer.end_paragraph(),
93+
pulldown_cmark::Tag::Heading(level, ..) => self.writer.end_heading(level),
94+
pulldown_cmark::Tag::BlockQuote => self.writer.end_blockquote(),
95+
pulldown_cmark::Tag::CodeBlock(_) => self.writer.end_code_block(),
96+
pulldown_cmark::Tag::List(Some(_)) => self.writer.end_list(true),
97+
pulldown_cmark::Tag::List(None) => self.writer.end_list(false),
98+
pulldown_cmark::Tag::Item => self.writer.end_list_item(),
99+
pulldown_cmark::Tag::FootnoteDefinition(_) => self.writer.end_footnote_definition(),
100+
pulldown_cmark::Tag::Table(_) => self.writer.end_table(),
101+
pulldown_cmark::Tag::TableHead => self.writer.end_table_head(),
102+
pulldown_cmark::Tag::TableRow => self.writer.end_table_row(),
103+
pulldown_cmark::Tag::TableCell => self.writer.end_table_cell(),
104+
pulldown_cmark::Tag::Emphasis => self.writer.end_emphasis(),
105+
pulldown_cmark::Tag::Strong => self.writer.end_strong(),
106+
pulldown_cmark::Tag::Strikethrough => self.writer.end_strikethrough(),
107+
pulldown_cmark::Tag::Link(..) => self.writer.end_link(),
108+
pulldown_cmark::Tag::Image(..) => self.writer.end_image(),
109+
}
110+
}
111+
112+
/// Handle inline code elements
113+
fn handle_inline_code(&mut self, text: &str) {
114+
self.writer.start_inline_code();
115+
self.writer.text(text);
116+
self.writer.end_inline_code();
117+
}
118+
}
119+
120+
/// Convert Markdown to HTML using the default writer
121+
pub fn push_html(markdown: &str, config: &HtmlConfig) -> String {
122+
let mut output = String::new();
123+
let parser = Parser::new(markdown);
124+
let writer = DefaultHtmlWriter::new(&mut output, config);
125+
let mut renderer = HtmlRenderer::new(writer);
126+
renderer.run(parser);
127+
output
128+
}
129+
130+
/// Create a custom HTML renderer with a specific writer implementation
131+
pub fn create_html_renderer<W: HtmlWriter>(writer: W) -> HtmlRenderer<W> {
132+
HtmlRenderer::new(writer)
133+
}
134+
135+
#[cfg(test)]
136+
mod tests {
137+
138+
use crate::html::{HtmlConfig, HtmlRenderer};
139+
use crate::DefaultHtmlWriter;
140+
use html_compare_rs::{assert_html_eq, presets::markdown};
141+
use pulldown_cmark::{Options, Parser};
142+
143+
fn push_html_with_config(input: &str, config: &HtmlConfig) -> String {
144+
let mut output = String::new();
145+
let handler = DefaultHtmlWriter::new(&mut output, config);
146+
let mut renderer = HtmlRenderer::new(handler);
147+
renderer.run(Parser::new_ext(input, Options::all()));
148+
output
149+
}
150+
151+
fn push_html(input: &str) -> String {
152+
push_html_with_config(input, &HtmlConfig::default())
153+
}
154+
155+
#[test]
156+
fn test_basic_text_rendering() {
157+
assert_html_eq!(
158+
push_html("Hello, world!"),
159+
"<p>Hello, world!</p>",
160+
markdown()
161+
);
162+
}
163+
164+
#[test]
165+
fn test_emphasis_and_strong() {
166+
assert_html_eq!(
167+
push_html("*italic* and **bold** text"),
168+
"<p><em>italic</em> and <strong>bold</strong> text</p>",
169+
markdown()
170+
);
171+
}
172+
173+
#[test]
174+
fn test_nested_formatting() {
175+
assert_html_eq!(
176+
push_html("***bold italic*** and **bold *italic* mix**"),
177+
"<p><em><strong>bold italic</strong></em> and <strong>bold <em>italic</em> mix</strong></p>",
178+
markdown()
179+
);
180+
}
181+
182+
#[test]
183+
fn test_headings() {
184+
let input = "# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6";
185+
assert_html_eq!(
186+
push_html(input),
187+
"<h1 id=\"heading-1\">H1</h1>\
188+
<h2 id=\"heading-2\">H2</h2>\
189+
<h3 id=\"heading-3\">H3</h3>\
190+
<h4 id=\"heading-4\">H4</h4>\
191+
<h5 id=\"heading-5\">H5</h5>\
192+
<h6 id=\"heading-6\">H6</h6>",
193+
markdown()
194+
);
195+
}
196+
197+
#[test]
198+
fn test_lists() {
199+
let input = "- Item 1\n- Item 2\n - Nested 1\n - Nested 2\n- Item 3";
200+
assert_html_eq!(
201+
push_html(input),
202+
"<ul><li>Item 1</li>\
203+
<li>Item 2\
204+
<ul><li>Nested 1</li>\
205+
<li>Nested 2</li></ul></li>\
206+
<li>Item 3</li></ul>",
207+
markdown()
208+
);
209+
}
210+
211+
#[test]
212+
fn test_ordered_lists() {
213+
let input = "1. First\n2. Second\n 1. Nested\n 2. Items\n3. Third";
214+
assert_html_eq!(
215+
push_html(input),
216+
"<ol><li>First</li>\
217+
<li>Second\
218+
<ol><li>Nested</li>\
219+
<li>Items</li></ol></li>\
220+
<li>Third</li></ol>",
221+
markdown()
222+
);
223+
}
224+
225+
#[test]
226+
fn test_code_blocks() {
227+
let input = "```rust\nfn main() {\n println!(\"Hello\");\n}\n```";
228+
assert_html_eq!(
229+
push_html(input),
230+
"<pre><code class=\"language-rust\">fn main() {\n println!(\"Hello\");\n}</code></pre>",
231+
markdown()
232+
);
233+
}
234+
235+
#[test]
236+
fn test_inline_code() {
237+
assert_html_eq!(
238+
push_html("Use the `println!` macro"),
239+
"<p>Use the <code>println!</code> macro</p>",
240+
markdown()
241+
);
242+
}
243+
244+
#[test]
245+
fn test_blockquotes() {
246+
let input = "> First level\n>> Second level\n\n> Back to first";
247+
assert_html_eq!(
248+
push_html(input),
249+
"<blockquote><p>First level</p><blockquote><p>Second level</p></blockquote></blockquote><blockquote><p>Back to first</p></blockquote>",
250+
markdown()
251+
);
252+
}
253+
254+
#[test]
255+
fn test_links() {
256+
assert_html_eq!(
257+
push_html("[Example](https://example.com \"Title\")"),
258+
r#"<p><a href="https://example.com" title="Title" rel="nofollow" target="_blank">Example</a></p>"#,
259+
markdown()
260+
);
261+
}
262+
263+
#[test]
264+
fn test_images() {
265+
assert_html_eq!(
266+
push_html("![Alt text](image.jpg \"Image title\")"),
267+
"<p><img src=\"image.jpg\" alt=\"Alt text\" title=\"Image title\"></p>",
268+
markdown()
269+
);
270+
}
271+
272+
#[test]
273+
fn test_tables() {
274+
let input = "| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |";
275+
assert_html_eq!(
276+
push_html(input),
277+
"<table><thead><tr><th>Header 1</th><th>Header 2</th></tr></thead>\
278+
<tbody><tr><td>Cell 1</td><td>Cell 2</td></tr></tbody></table>",
279+
markdown()
280+
);
281+
}
282+
283+
#[test]
284+
fn test_task_lists() {
285+
let input = "- [ ] Unchecked\n- [x] Checked";
286+
assert_html_eq!(
287+
push_html(input),
288+
"<ul><li><input type=\"checkbox\" disabled>Unchecked</li>\
289+
<li><input type=\"checkbox\" disabled checked>Checked</li></ul>",
290+
markdown()
291+
);
292+
}
293+
294+
#[test]
295+
fn test_strikethrough() {
296+
assert_html_eq!(
297+
push_html("~~struck through~~"),
298+
"<p><del>struck through</del></p>",
299+
markdown()
300+
);
301+
}
302+
303+
#[test]
304+
fn test_horizontal_rule() {
305+
assert_html_eq!(push_html("---"), "<hr>", markdown());
306+
}
307+
308+
#[test]
309+
fn test_mixed_content() {
310+
let input = "# Title\n\
311+
Some *formatted* text with `code`.\n\n\
312+
> A quote with **bold**\n\n\
313+
- List item 1\n\
314+
- List item 2\n\n\
315+
```\nCode block\n```";
316+
317+
assert_html_eq!(
318+
push_html(input),
319+
"<h1 id=\"heading-1\">Title</h1>\
320+
<p>Some <em>formatted</em> text with <code>code</code>.</p>\
321+
<blockquote><p>A quote with <strong>bold</strong></p></blockquote>\
322+
<ul><li>List item 1</li><li>List item 2</li></ul>\
323+
<pre><code>Code block</code></pre>",
324+
markdown()
325+
);
326+
}
327+
328+
#[test]
329+
#[ignore = "Fix/implement escape_html option"]
330+
fn test_escaped_html() {
331+
let mut config = HtmlConfig::default();
332+
config.html.escape_html = true;
333+
334+
assert_html_eq!(
335+
push_html_with_config("This is <em>HTML</em> content", &config),
336+
"<p>This is &lt;em&gt;HTML&lt;/em&gt; content</p>",
337+
markdown()
338+
);
339+
}
340+
341+
#[test]
342+
fn test_footnotes() {
343+
let input = "Text with a footnote[^1].\n\n[^1]: Footnote content.";
344+
assert_html_eq!(
345+
push_html(input),
346+
"<p>Text with a footnote<sup class=\"footnote-reference\"><a href=\"#1\">1</a></sup>.</p>\
347+
<div class=\"footnote-definition\" id=\"1\"><sup class=\"footnote-definition-label\">1</sup>Footnote content.</div>",
348+
markdown()
349+
);
350+
}
351+
352+
#[test]
353+
fn test_line_breaks() {
354+
assert_html_eq!(
355+
push_html("Line 1 \nLine 2"),
356+
"<p>Line 1<br>Line 2</p>",
357+
markdown()
358+
);
359+
}
360+
}

0 commit comments

Comments
 (0)