1use super::{ListContext, TableContext};
2use crate::html::state::HtmlState;
3use crate::html::HtmlError;
4use crate::HtmlConfig;
5
6use pulldown_cmark::{
7 Alignment, CodeBlockKind, CowStr, Event, HeadingLevel, LinkType, MetadataBlockKind,
8};
9use pulldown_cmark_escape::{escape_href, escape_html, escape_html_body_text, StrWrite};
10use std::iter::Peekable;
11
12pub trait HtmlWriter<W: StrWrite> {
14 fn write_str(&mut self, s: &str) -> Result<(), HtmlError> {
16 self.get_writer()
17 .write_str(s)
18 .map_err(|_| HtmlError::Write(std::fmt::Error))
19 }
20
21 fn write_attributes(&mut self, element: &str) -> Result<(), HtmlError> {
23 let mut attrs_string = String::new();
24
25 if let Some(attrs) = self.get_config().attributes.element_attributes.get(element) {
26 for (key, value) in attrs {
27 attrs_string.push_str(&format!(" {}=\"{}\"", key, value));
28 }
29 }
30
31 if !attrs_string.is_empty() {
32 self.write_str(&attrs_string)?;
33 }
34 Ok(())
35 }
36
37 fn get_config(&self) -> &HtmlConfig;
38
39 fn get_writer(&mut self) -> &mut W;
40
41 fn get_state(&mut self) -> &mut HtmlState;
42
43 fn is_external_link(&self, url: &str) -> bool {
45 url.starts_with("http://") || url.starts_with("https://")
46 }
47
48 fn start_paragraph(&mut self) -> Result<(), HtmlError> {
49 if !self.get_state().currently_in_footnote {
50 self.write_str("<p")?;
51 self.write_attributes("p")?;
52 self.write_str(">")?;
53 }
54 Ok(())
55 }
56
57 fn end_paragraph(&mut self) -> Result<(), HtmlError> {
58 if !self.get_state().currently_in_footnote {
59 self.write_str("</p>")?;
60 }
61 Ok(())
62 }
63
64 fn start_heading(
65 &mut self,
66 level: HeadingLevel,
67 id: Option<&str>,
68 classes: &[CowStr],
69 attrs: &Vec<(CowStr, Option<CowStr>)>,
70 ) -> Result<(), HtmlError> {
71 let level_num = level as u8;
73 let add_ids = self.get_config().elements.headings.add_ids;
74 let id_prefix = self.get_config().elements.headings.id_prefix.clone();
75 let level_classes = self
76 .get_config()
77 .elements
78 .headings
79 .level_classes
80 .get(&level_num)
81 .cloned();
82
83 self.write_str(&format!("<h{}", level_num))?;
85
86 if add_ids {
88 let heading_id =
89 id.map_or_else(|| format!("{}{}", id_prefix, level_num), |s| s.to_string());
90 self.write_str(" id=\"")?;
91 escape_html(self.get_writer(), &heading_id)
92 .map_err(|_| HtmlError::Write(std::fmt::Error))?;
93 self.write_str("\"")?;
94 self.get_state().heading_stack.push(heading_id);
95 }
96
97 let mut all_classes = Vec::new();
99 if let Some(level_class) = level_classes {
100 all_classes.push(level_class);
101 }
102 all_classes.extend(classes.iter().map(|s| s.to_string()));
103
104 if !all_classes.is_empty() {
105 self.write_str(" class=\"")?;
106 escape_html(self.get_writer(), &all_classes.join(" "))
107 .map_err(|_| HtmlError::Write(std::fmt::Error))?;
108 self.write_str("\"")?;
109 }
110
111 for (key, value) in attrs {
113 self.write_str(" ")?;
114 escape_html(self.get_writer(), key).map_err(|_| HtmlError::Write(std::fmt::Error))?;
115 if let Some(val) = value {
116 self.write_str("=\"")?;
117 escape_html(self.get_writer(), val)
118 .map_err(|_| HtmlError::Write(std::fmt::Error))?;
119 self.write_str("\"")?;
120 }
121 }
122
123 self.write_attributes(&format!("h{}", level_num))?;
125
126 self.write_str(">")
128 }
129 fn end_heading(&mut self, level: HeadingLevel) -> Result<(), HtmlError> {
130 self.write_str(&format!("</{}>", level))
131 }
132
133 fn start_blockquote(&mut self) -> Result<(), HtmlError> {
134 self.write_str("<blockquote")?;
135 self.write_attributes("blockquote")?;
136 self.write_str(">")?;
137 Ok(())
138 }
139
140 fn end_blockquote(&mut self) -> Result<(), HtmlError> {
141 self.write_str("</blockquote>")
142 }
143
144 fn start_code_block(&mut self, kind: CodeBlockKind) -> Result<(), HtmlError> {
145 self.get_state().currently_in_code_block = true;
146 self.write_str("<pre")?;
147 self.write_attributes("pre")?;
148 self.write_str("><code")?;
149
150 match kind {
151 CodeBlockKind::Fenced(info) => {
152 let lang = if info.is_empty() {
153 self.get_config()
154 .elements
155 .code_blocks
156 .default_language
157 .as_deref()
158 } else {
159 Some(&*info)
160 };
161
162 if let Some(lang) = lang {
163 self.write_str(&format!(" class=\"language-{}\"", lang))?;
164 }
165 }
166 CodeBlockKind::Indented => {
167 if let Some(lang) = &self.get_config().elements.code_blocks.default_language {
168 self.write_str(&format!(" class=\"language-{}\"", lang))?;
169 }
170 }
171 }
172
173 self.write_attributes("code")?;
174 self.write_str(">")?;
175 Ok(())
176 }
177
178 fn end_code_block(&mut self) -> Result<(), HtmlError> {
179 self.write_str("</code></pre>")
180 }
181
182 fn start_inline_code(&mut self) -> Result<(), HtmlError> {
183 self.write_str("<code")?;
184 self.write_attributes("code")?;
185 self.write_str(">")?;
186 Ok(())
187 }
188
189 fn end_inline_code(&mut self) -> Result<(), HtmlError> {
190 self.write_str("</code>")
191 }
192
193 fn start_list(&mut self, first_number: Option<u64>) -> Result<(), HtmlError> {
194 match first_number {
195 Some(n) => {
196 self.get_state().numbers.push(n.try_into().unwrap());
197 self.get_state()
198 .list_stack
199 .push(ListContext::Ordered(n.try_into().unwrap()));
200 self.write_str("<ol")?;
201 if n != 1 {
202 self.write_str(&format!(" start=\"{}\"", n))?;
203 }
204 self.write_attributes("ol")?;
205 self.write_str(">")?;
206 }
207 None => {
208 self.get_state().list_stack.push(ListContext::Unordered);
209 self.write_str("<ul")?;
210 self.write_attributes("ul")?;
211 self.write_str(">")?;
212 }
213 }
214 Ok(())
215 }
216
217 fn end_list(&mut self, ordered: bool) -> Result<(), HtmlError> {
218 self.write_str(if ordered { "</ol>" } else { "</ul>" })
219 }
220
221 fn start_list_item(&mut self) -> Result<(), HtmlError> {
222 self.write_str("<li")?;
223 self.write_attributes("li")?;
224 self.write_str(">")
225 }
226
227 fn end_list_item(&mut self) -> Result<(), HtmlError> {
228 self.write_str("</li>")
229 }
230
231 fn start_table(&mut self, alignments: Vec<Alignment>) -> Result<(), HtmlError> {
232 self.get_state().table_state = TableContext::InHeader;
233 self.get_state().table_alignments = alignments;
234 self.write_str("<table")?;
235 self.write_attributes("table")?;
236 self.write_str(">")
237 }
238
239 fn end_table(&mut self) -> Result<(), HtmlError> {
240 self.write_str("</tbody></table>")
241 }
242
243 fn start_table_head(&mut self) -> Result<(), HtmlError> {
244 self.get_state().table_cell_index = 0;
245 self.write_str("<thead><tr>")
246 }
247
248 fn end_table_head(&mut self) -> Result<(), HtmlError> {
249 self.write_str("</tr></thead><tbody>")
250 }
251
252 fn start_table_row(&mut self) -> Result<(), HtmlError> {
253 self.get_state().table_cell_index = 0;
254 if self.get_state().table_state == TableContext::InHeader {
255 self.get_state().table_state = TableContext::InBody;
256 }
257 self.write_str("<tr>")
258 }
259
260 fn end_table_row(&mut self) -> Result<(), HtmlError> {
261 self.write_str("</tr>")
262 }
263
264 fn start_table_cell(&mut self) -> Result<(), HtmlError> {
265 let tag = match self.get_state().table_state {
266 TableContext::InHeader => "th",
267 _ => "td",
268 };
269
270 self.write_str("<")?;
271 self.write_str(tag)?;
272 let idx = self.get_state().table_cell_index;
273 if let Some(alignment) = self.get_state().table_alignments.get(idx) {
274 match alignment {
275 Alignment::Left => self.write_str(" style=\"text-align: left\"")?,
276 Alignment::Center => self.write_str(" style=\"text-align: center\"")?,
277 Alignment::Right => self.write_str(" style=\"text-align: right\"")?,
278 Alignment::None => {}
279 }
280 }
281
282 self.write_attributes(tag)?;
283 self.write_str(">")?;
284
285 self.get_state().table_cell_index += 1;
286 Ok(())
287 }
288
289 fn end_table_cell(&mut self) -> Result<(), HtmlError> {
290 self.write_str("</td>")
291 }
292
293 fn start_emphasis(&mut self) -> Result<(), HtmlError> {
294 self.write_str("<em")?;
295 self.write_attributes("em")?;
296 self.write_str(">")
297 }
298
299 fn end_emphasis(&mut self) -> Result<(), HtmlError> {
300 self.write_str("</em>")
301 }
302
303 fn start_strong(&mut self) -> Result<(), HtmlError> {
304 self.write_str("<strong")?;
305 self.write_attributes("strong")?;
306 self.write_str(">")
307 }
308
309 fn end_strong(&mut self) -> Result<(), HtmlError> {
310 self.write_str("</strong>")
311 }
312
313 fn start_strikethrough(&mut self) -> Result<(), HtmlError> {
314 self.write_str("<del")?;
315 self.write_attributes("del")?;
316 self.write_str(">")
317 }
318
319 fn end_strikethrough(&mut self) -> Result<(), HtmlError> {
320 self.write_str("</del>")
321 }
322
323 fn start_link(
324 &mut self,
325 _link_type: LinkType,
326 dest: &str,
327 title: &str,
328 ) -> Result<(), HtmlError> {
329 self.write_str("<a href=\"")?;
330 escape_href(self.get_writer(), dest).map_err(|_| HtmlError::Write(std::fmt::Error))?;
331
332 if !title.is_empty() {
333 self.write_str("\" title=\"")?;
334 escape_html(self.get_writer(), title).map_err(|_| HtmlError::Write(std::fmt::Error))?;
335 }
336
337 if self.is_external_link(dest) {
338 if self.get_config().elements.links.nofollow_external {
339 self.write_str("\" rel=\"nofollow")?;
340 }
341 if self.get_config().elements.links.open_external_blank {
342 self.write_str("\" target=\"_blank")?;
343 }
344 }
345
346 self.write_str("\"")?;
347 self.write_attributes("a")?;
348 self.write_str(">")
349 }
350
351 fn end_link(&mut self) -> Result<(), HtmlError> {
352 self.write_str("</a>")
353 }
354
355 fn start_image<'a, I>(
356 &mut self,
357 _link_type: LinkType,
358 dest: &str,
359 title: &str,
360 iter: &mut Peekable<I>,
361 ) -> Result<(), HtmlError>
362 where
363 I: Iterator<Item = Event<'a>>,
364 {
365 self.write_str("<img src=\"")?;
366 escape_href(self.get_writer(), dest).map_err(|_| HtmlError::Write(std::fmt::Error))?;
367 self.write_str("\" alt=\"")?;
368
369 let alt_text = self.collect_alt_text(iter);
370 escape_html(self.get_writer(), &alt_text).map_err(|_| HtmlError::Write(std::fmt::Error))?;
371 self.write_str("\"")?;
372
373 if !title.is_empty() {
374 self.write_str(" title=\"")?;
375 escape_html(self.get_writer(), title).map_err(|_| HtmlError::Write(std::fmt::Error))?;
376 self.write_str("\"")?;
377 }
378
379 self.write_attributes("img")?;
380
381 if self.get_config().html.xhtml_style {
382 self.write_str(" />")?;
383 } else {
384 self.write_str(">")?;
385 }
386 Ok(())
387 }
388
389 fn end_image(&mut self) -> Result<(), HtmlError> {
390 Ok(())
391 }
392
393 fn footnote_reference(&mut self, name: &str) -> Result<(), HtmlError> {
394 self.write_str("<sup class=\"footnote-reference\"><a href=\"#")?;
395 self.write_str(name)?;
396 self.write_str("\">")?;
397 self.write_str(name)?;
398 self.write_str("</a></sup>")
399 }
400
401 fn start_footnote_definition(&mut self, name: &str) -> Result<(), HtmlError> {
402 self.write_str("<div class=\"footnote-definition\" id=\"")?;
403 self.write_str(name)?;
404 self.write_str("\"><sup class=\"footnote-definition-label\">")?;
405 self.write_str(name)?;
406 self.get_state().currently_in_footnote = true;
407 self.write_str("</sup>")?;
408
409 Ok(())
410 }
411 fn end_footnote_definition(&mut self) -> Result<(), HtmlError> {
412 self.write_str("</div>")?;
413 self.get_state().currently_in_footnote = false;
414 Ok(())
415 }
416
417 fn task_list_item(&mut self, checked: bool) -> Result<(), HtmlError> {
419 self.write_str("<input type=\"checkbox\" disabled")?;
420 if checked {
421 self.write_str(" checked")?;
422 }
423 self.write_str(">")
424 }
425
426 fn horizontal_rule(&mut self) -> Result<(), HtmlError> {
428 self.write_str("<hr>")
429 }
430
431 fn soft_break(&mut self) -> Result<(), HtmlError> {
432 if self.get_config().html.break_on_newline {
433 self.write_str("<br>")
434 } else {
435 self.write_str("\n")
436 }
437 }
438
439 fn hard_break(&mut self) -> Result<(), HtmlError> {
440 self.write_str("<br>")
441 }
442
443 fn text(&mut self, text: &str) -> Result<(), HtmlError> {
444 if self.get_config().html.escape_html {
445 escape_html_body_text(self.get_writer(), text)
446 .map_err(|_| HtmlError::Write(std::fmt::Error))?;
447 } else {
448 self.write_str(text)?;
449 }
450 Ok(())
451 }
452
453 fn start_definition_list(&mut self) -> Result<(), HtmlError> {
454 self.write_str("<dl")?;
455 self.write_attributes("dl")?;
456 self.write_str(">")
457 }
458
459 fn end_definition_list(&mut self) -> Result<(), HtmlError> {
460 self.write_str("</dl>")
461 }
462
463 fn start_definition_list_title(&mut self) -> Result<(), HtmlError> {
464 self.write_str("<dt")?;
465 self.write_attributes("dt")?;
466 self.write_str(">")
467 }
468
469 fn end_definition_list_title(&mut self) -> Result<(), HtmlError> {
470 self.write_str("</dt>")
471 }
472
473 fn start_definition_list_definition(&mut self) -> Result<(), HtmlError> {
474 self.write_str("<dd")?;
475 self.write_attributes("dd")?;
476 self.write_str(">")
477 }
478
479 fn end_definition_list_definition(&mut self) -> Result<(), HtmlError> {
480 self.write_str("</dd>")
481 }
482
483 fn start_metadata_block(
484 &mut self,
485 _metadata_type: &MetadataBlockKind,
486 ) -> Result<(), HtmlError> {
487 Ok(())
490 }
491 fn end_metadata_block(&mut self) -> Result<(), HtmlError> {
492 Ok(())
495 }
496
497 fn html_raw(&mut self, html: &CowStr) -> Result<(), HtmlError> {
498 self.write_str(html)
499 }
500
501 fn collect_alt_text<'a, I>(&self, iter: &mut Peekable<I>) -> String
502 where
503 I: Iterator<Item = Event<'a>>,
504 {
505 let mut alt = String::new();
506 let mut nest = 0;
507
508 for event in iter.by_ref() {
509 match event {
510 Event::Start(_) => nest += 1,
511 Event::End(_) => {
512 if nest == 0 {
513 break;
514 }
515 nest -= 1;
516 }
517 Event::Text(text) => {
518 alt.push_str(&text);
519 }
520 Event::Code(text) => {
521 alt.push_str(&text);
522 }
523 Event::SoftBreak | Event::HardBreak => {
524 alt.push(' ');
525 }
526 _ => {}
527 }
528 }
529 alt
530 }
531}
532
533pub struct HtmlWriterBase<W: StrWrite> {
535 writer: W,
536 config: HtmlConfig,
537 state: HtmlState,
538}
539
540impl<W: StrWrite> HtmlWriterBase<W> {
541 pub fn new(writer: W, config: HtmlConfig) -> Self {
542 Self {
543 writer,
544 config,
545 state: HtmlState::new(),
546 }
547 }
548}
549
550impl<W: StrWrite> HtmlWriter<W> for HtmlWriterBase<W> {
551 fn get_writer(&mut self) -> &mut W {
552 &mut self.writer
553 }
554
555 fn get_config(&self) -> &HtmlConfig {
556 &self.config
557 }
558
559 fn get_state(&mut self) -> &mut HtmlState {
560 &mut self.state
561 }
562}
563
564#[cfg(test)]
565mod tests {
566
567 use super::*;
568 use pulldown_cmark_escape::FmtWriter;
569
570 struct TestHandler<W: StrWrite> {
571 writer: W,
572 config: HtmlConfig,
573 state: HtmlState,
574 }
575
576 impl<W: StrWrite> TestHandler<W> {
577 fn new(writer: W) -> Self {
578 let mut config = HtmlConfig::default();
579 config.html.break_on_newline = false;
580 Self {
581 writer,
582 config,
583 state: HtmlState::new(),
584 }
585 }
586 }
587
588 impl<W: StrWrite> HtmlWriter<W> for TestHandler<W> {
589 fn get_writer(&mut self) -> &mut W {
590 &mut self.writer
591 }
592 fn get_config(&self) -> &HtmlConfig {
593 &self.config
594 }
595
596 fn get_state(&mut self) -> &mut HtmlState {
597 &mut self.state
598 }
599 }
600
601 #[test]
602 fn test_paragraph() {
603 let mut output = String::new();
604 let mut handler = TestHandler::new(FmtWriter(&mut output));
605 handler.start_paragraph().unwrap();
606 handler.text("Hello world").unwrap();
607 handler.end_paragraph().unwrap();
608 assert_eq!(output, "<p>Hello world</p>");
609 }
610
611 #[test]
612 fn test_blockquote() {
613 let mut output = String::new();
614 let mut handler = TestHandler::new(FmtWriter(&mut output));
615 handler.start_blockquote().unwrap();
616 handler.text("Quote").unwrap();
617 handler.end_blockquote().unwrap();
618 assert_eq!(output, "<blockquote>Quote</blockquote>");
619 }
620
621 #[test]
622 fn test_emphasis() {
623 let mut output = String::new();
624 let mut handler = TestHandler::new(FmtWriter(&mut output));
625 handler.start_emphasis().unwrap();
626 handler.text("emphasized").unwrap();
627 handler.end_emphasis().unwrap();
628 assert_eq!(output, "<em>emphasized</em>");
629 }
630
631 #[test]
632 fn test_strong() {
633 let mut output = String::new();
634 let mut handler = TestHandler::new(FmtWriter(&mut output));
635 handler.start_strong().unwrap();
636 handler.text("bold").unwrap();
637 handler.end_strong().unwrap();
638 assert_eq!(output, "<strong>bold</strong>");
639 }
640
641 #[test]
642 fn test_strikethrough() {
643 let mut output = String::new();
644 let mut handler = TestHandler::new(FmtWriter(&mut output));
645 handler.start_strikethrough().unwrap();
646 handler.text("strike").unwrap();
647 handler.end_strikethrough().unwrap();
648 assert_eq!(output, "<del>strike</del>");
649 }
650
651 #[test]
652 fn test_inline_code() {
653 let mut output = String::new();
654 let mut handler = TestHandler::new(FmtWriter(&mut output));
655 handler.start_inline_code().unwrap();
656 handler.text("code").unwrap();
657 handler.end_inline_code().unwrap();
658 assert_eq!(output, "<code>code</code>");
659 }
660
661 #[test]
662 fn test_line_breaks() {
663 let mut output = String::new();
664 let mut handler = TestHandler::new(FmtWriter(&mut output));
665 handler.soft_break().unwrap();
666 handler.hard_break().unwrap();
667 assert_eq!(output, "\n<br>");
668 }
669
670 #[test]
671 fn test_horizontal_rule() {
672 let mut output = String::new();
673 let mut handler = TestHandler::new(FmtWriter(&mut output));
674 handler.horizontal_rule().unwrap();
675 assert_eq!(output, "<hr>");
676 }
677
678 #[test]
679 fn test_task_list() {
680 let mut output = String::new();
681 let mut handler = TestHandler::new(FmtWriter(&mut output));
682 handler.task_list_item(true).unwrap();
683 handler.text("Done").unwrap();
684
685 assert_eq!(output, "<input type=\"checkbox\" disabled checked>Done");
686
687 let mut output = String::new();
688 let mut handler = TestHandler::new(FmtWriter(&mut output));
689 handler.task_list_item(false).unwrap();
690 handler.text("Todo").unwrap();
691
692 assert_eq!(output, "<input type=\"checkbox\" disabled>Todo");
693 }
694
695 #[test]
696 fn test_footnote_definition() {
697 let mut output = String::new();
698 let mut handler = TestHandler::new(FmtWriter(&mut output));
699 handler.start_footnote_definition("1").unwrap();
700 handler.text("Footnote content").unwrap();
701 handler.end_footnote_definition().unwrap();
702 assert_eq!(
703 output,
704 "<div class=\"footnote-definition\" id=\"1\">\
705 <sup class=\"footnote-definition-label\">1</sup>\
706 Footnote content</div>"
707 );
708 }
709
710 #[test]
711 fn test_list_endings() {
712 let mut output = String::new();
713 let mut handler = TestHandler::new(FmtWriter(&mut output));
714 handler.end_list(true).unwrap();
715 assert_eq!(output, "</ol>");
716
717 let mut output = String::new();
718 let mut handler = TestHandler::new(FmtWriter(&mut output));
719 handler.end_list(false).unwrap();
720 assert_eq!(output, "</ul>");
721 }
722
723 #[test]
724 fn test_table_structure() {
725 let mut output = String::new();
726 let mut handler = TestHandler::new(FmtWriter(&mut output));
727 handler.end_table_head().unwrap();
728 handler.end_table_row().unwrap();
729 handler.end_table_cell().unwrap();
730 handler.end_table().unwrap();
731 assert_eq!(output, "</tr></thead><tbody></tr></td></tbody></table>");
732 }
733}