1use super::processors::{
6 BlockNodeProcessor, CustomNodeProcessor, InlineNodeProcessor, NodeProcessor,
7};
8#[cfg(feature = "gfm")]
9use crate::ast::TableAlignment;
10use crate::ast::{CodeBlockType, CustomNode, HeadingType, ListItem, Node};
11use crate::error::{WriteError, WriteResult};
12use crate::options::WriterOptions;
13use ecow::EcoString;
14use log;
15use std::fmt::{self};
16
17#[derive(Debug)]
21pub struct CommonMarkWriter {
22 pub options: WriterOptions,
24 buffer: EcoString,
26}
27
28impl CommonMarkWriter {
29 pub fn new() -> Self {
42 Self::with_options(WriterOptions::default())
43 }
44
45 pub fn with_options(options: WriterOptions) -> Self {
66 Self {
67 options,
68 buffer: EcoString::new(),
69 }
70 }
71
72 pub(crate) fn is_strict_mode(&self) -> bool {
74 self.options.strict
75 }
76
77 fn apply_prefix(
89 &self,
90 content: &str,
91 prefix: &str,
92 first_line_prefix: Option<&str>,
93 ) -> EcoString {
94 if content.is_empty() {
95 return EcoString::new();
96 }
97
98 let mut result = EcoString::new();
99 let lines: Vec<&str> = content.lines().collect();
100
101 if !lines.is_empty() {
102 let actual_prefix = first_line_prefix.unwrap_or(prefix);
103 result.push_str(actual_prefix);
104 result.push_str(lines[0]);
105 }
106
107 for line in &lines[1..] {
108 result.push('\n');
109 result.push_str(prefix);
110 result.push_str(line);
111 }
112
113 result
114 }
115
116 pub fn write(&mut self, node: &Node) -> WriteResult<()> {
136 if let Node::Custom(_) = node {
137 return CustomNodeProcessor.process(self, node);
138 }
139
140 if node.is_block() {
141 BlockNodeProcessor.process(self, node)
142 } else if node.is_inline() {
143 InlineNodeProcessor.process(self, node)
144 } else {
145 log::warn!("Unsupported node type encountered and skipped: {:?}", node);
146 Ok(())
147 }
148 }
149
150 #[allow(clippy::borrowed_box)]
152 pub(crate) fn write_custom_node(&mut self, node: &Box<dyn CustomNode>) -> WriteResult<()> {
153 node.write(self)
154 }
155
156 pub(crate) fn get_context_for_node(&self, node: &Node) -> EcoString {
158 match node {
159 Node::Text(_) => "Text".into(),
160 Node::Emphasis(_) => "Emphasis".into(),
161 Node::Strong(_) => "Strong".into(),
162 #[cfg(feature = "gfm")]
163 Node::Strikethrough(_) => "Strikethrough".into(),
164 Node::InlineCode(_) => "InlineCode".into(),
165 Node::Link { .. } => "Link content".into(),
166 Node::Image { .. } => "Image alt text".into(),
167 Node::HtmlElement(_) => "HtmlElement content".into(),
168 Node::Custom(_) => "Custom node".into(),
169 _ => "Unknown inline element".into(),
170 }
171 }
172
173 pub(crate) fn check_no_newline(&self, node: &Node, context: &str) -> WriteResult<()> {
175 if Self::node_contains_newline(node) {
176 if self.is_strict_mode() {
177 return Err(WriteError::NewlineInInlineElement(
178 context.to_string().into(),
179 ));
180 } else {
181 log::warn!(
182 "Newline character found in inline element '{}', but non-strict mode allows it (output may be affected).",
183 context
184 );
185 }
186 }
187 Ok(())
188 }
189
190 fn node_contains_newline(node: &Node) -> bool {
192 match node {
193 Node::Text(s) | Node::InlineCode(s) => s.contains('\n'),
194 Node::Emphasis(children) | Node::Strong(children) => {
195 children.iter().any(Self::node_contains_newline)
196 }
197 #[cfg(feature = "gfm")]
198 Node::Strikethrough(children) => children.iter().any(Self::node_contains_newline),
199 Node::HtmlElement(element) => element.children.iter().any(Self::node_contains_newline),
200 Node::Link { content, .. } => content.iter().any(Self::node_contains_newline),
201 Node::Image { alt, .. } => alt.iter().any(Self::node_contains_newline),
202 Node::SoftBreak | Node::HardBreak => true,
203 Node::Custom(_) => false,
205 _ => false,
206 }
207 }
208
209 fn table_contains_block_elements(headers: &[Node], rows: &[Vec<Node>]) -> bool {
211 if headers.iter().any(|node| node.is_block()) {
213 return true;
214 }
215
216 rows.iter()
218 .any(|row| row.iter().any(|node| node.is_block()))
219 }
220
221 pub(crate) fn write_text_content(&mut self, content: &str) -> WriteResult<()> {
223 if self.options.escape_special_chars {
224 let escaped = escape_str::<CommonMarkEscapes>(content);
225 self.write_str(&escaped)?
226 } else {
227 self.write_str(content)?
228 }
229
230 Ok(())
231 }
232
233 pub(crate) fn write_code_content(&mut self, content: &str) -> WriteResult<()> {
235 self.write_char('`')?;
236 self.write_str(content)?;
237 self.write_char('`')?;
238 Ok(())
239 }
240
241 pub(crate) fn write_delimited(&mut self, content: &[Node], delimiter: &str) -> WriteResult<()> {
243 self.write_str(delimiter)?;
244
245 for node in content {
246 self.write(node)?;
247 }
248
249 self.write_str(delimiter)?;
250 Ok(())
251 }
252
253 pub(crate) fn write_document(&mut self, children: &[Node]) -> WriteResult<()> {
255 for (i, child) in children.iter().enumerate() {
256 if i > 0 {
257 self.write_str("\n")?;
258 }
259 self.write(child)?;
260 }
261 Ok(())
262 }
263
264 pub(crate) fn write_heading(
266 &mut self,
267 mut level: u8,
268 content: &[Node],
269 heading_type: &HeadingType,
270 ) -> WriteResult<()> {
271 if level == 0 || level > 6 {
273 if self.is_strict_mode() {
274 return Err(WriteError::InvalidHeadingLevel(level));
275 } else {
276 let original_level = level;
277 level = level.clamp(1, 6); log::warn!(
279 "Invalid heading level: {}. Corrected to {}. Strict mode is off.",
280 original_level,
281 level
282 );
283 }
284 }
285
286 match heading_type {
287 HeadingType::Atx => {
289 for _ in 0..level {
290 self.write_char('#')?;
291 }
292 self.write_char(' ')?;
293
294 for node in content {
295 self.write(node)?;
296 }
297
298 self.write_char('\n')?;
299 }
300
301 HeadingType::Setext => {
302 for node in content {
304 self.write(node)?;
305 }
306 self.write_char('\n')?;
307
308 let underline_char = if level == 1 { '=' } else { '-' };
311
312 let min_len = 3;
315
316 for _ in 0..min_len {
318 self.write_char(underline_char)?;
319 }
320
321 self.write_char('\n')?;
323 }
324 }
325
326 Ok(())
327 }
328
329 pub(crate) fn write_paragraph(&mut self, content: &[Node]) -> WriteResult<()> {
331 if self.options.trim_paragraph_trailing_hard_breaks {
332 let mut last_non_hard_break_index = content.len();
333
334 while last_non_hard_break_index > 0 {
335 if !matches!(content[last_non_hard_break_index - 1], Node::HardBreak) {
336 break;
337 }
338 last_non_hard_break_index -= 1;
339 }
340
341 for node in content.iter().take(last_non_hard_break_index) {
342 self.write(node)?;
343 }
344 } else {
345 for node in content {
346 self.write(node)?;
347 }
348 }
349
350 Ok(())
351 }
352
353 pub(crate) fn write_blockquote(&mut self, content: &[Node]) -> WriteResult<()> {
355 let mut temp_writer = CommonMarkWriter::with_options(self.options.clone());
357
358 for (i, node) in content.iter().enumerate() {
360 if i > 0 {
361 temp_writer.write_char('\n')?;
362 }
363 temp_writer.write(node)?;
365 }
366
367 let all_content = temp_writer.into_string();
369
370 let prefix = "> ";
372 let formatted_content = self.apply_prefix(&all_content, prefix, Some(prefix));
373
374 self.buffer.push_str(&formatted_content);
376 Ok(())
377 }
378
379 pub(crate) fn write_thematic_break(&mut self) -> WriteResult<()> {
381 let char = self.options.thematic_break_char;
382 self.write_str(&format!("{}{}{}", char, char, char))?;
383 Ok(())
384 }
385
386 pub(crate) fn write_code_block(
388 &mut self,
389 language: &Option<EcoString>,
390 content: &str,
391 block_type: &CodeBlockType,
392 ) -> WriteResult<()> {
393 match block_type {
394 CodeBlockType::Indented => {
395 let indent = " ";
396 let indented_content = self.apply_prefix(content, indent, Some(indent));
397 self.buffer.push_str(&indented_content);
398 }
399 CodeBlockType::Fenced => {
400 let max_backticks = content
401 .chars()
402 .fold((0, 0), |(max, current), c| {
403 if c == '`' {
404 (max.max(current + 1), current + 1)
405 } else {
406 (max, 0)
407 }
408 })
409 .0;
410
411 let fence_len = std::cmp::max(max_backticks + 1, 3);
412 let fence = "`".repeat(fence_len);
413
414 self.write_str(&fence)?;
415 if let Some(lang) = language {
416 self.write_str(lang)?;
417 }
418 self.write_char('\n')?;
419
420 self.buffer.push_str(content);
421 if !content.ends_with('\n') {
422 self.write_char('\n')?;
423 }
424
425 self.write_str(&fence)?;
426 }
427 }
428
429 Ok(())
430 }
431
432 pub(crate) fn write_unordered_list(&mut self, items: &[ListItem]) -> WriteResult<()> {
434 let list_marker = self.options.list_marker;
435 let prefix = format!("{} ", list_marker);
436
437 for (i, item) in items.iter().enumerate() {
438 if i > 0 {
439 self.write_char('\n')?;
440 }
441 self.write_list_item(item, &prefix)?;
442 }
443
444 Ok(())
445 }
446
447 pub(crate) fn write_ordered_list(&mut self, start: u32, items: &[ListItem]) -> WriteResult<()> {
449 let mut current_number = start;
451
452 for (i, item) in items.iter().enumerate() {
453 if i > 0 {
454 self.write_char('\n')?;
455 }
456
457 match item {
458 ListItem::Ordered { number, content: _ } => {
460 if let Some(custom_num) = number {
461 let prefix = format!("{}. ", custom_num);
463 self.write_list_item(item, &prefix)?;
464 current_number = custom_num + 1;
466 } else {
467 let prefix = format!("{}. ", current_number);
469 self.write_list_item(item, &prefix)?;
470 current_number += 1;
471 }
472 }
473 _ => {
475 let prefix = format!("{}. ", current_number);
476 self.write_list_item(item, &prefix)?;
477 current_number += 1;
478 }
479 }
480 }
481
482 Ok(())
483 }
484
485 fn write_list_item(&mut self, item: &ListItem, prefix: &str) -> WriteResult<()> {
487 match item {
488 ListItem::Unordered { content } => {
489 self.write_str(prefix)?;
490 self.write_list_item_content(content, prefix.len())?;
491 }
492 ListItem::Ordered { number, content } => {
493 if let Some(num) = number {
494 let custom_prefix = format!("{}. ", num);
495 self.write_str(&custom_prefix)?;
496 self.write_list_item_content(content, custom_prefix.len())?;
497 } else {
498 self.write_str(prefix)?;
499 self.write_list_item_content(content, prefix.len())?;
500 }
501 }
502 #[cfg(feature = "gfm")]
503 ListItem::Task { status, content } => {
504 if self.options.gfm_tasklists {
506 let checkbox = match status {
507 crate::ast::TaskListStatus::Checked => "[x] ",
508 crate::ast::TaskListStatus::Unchecked => "[ ] ",
509 };
510
511 let task_prefix = format!("{}{}", prefix, checkbox);
513 self.write_str(&task_prefix)?;
514 self.write_list_item_content(content, task_prefix.len())?;
515 } else {
516 self.write_str(prefix)?;
518 self.write_list_item_content(content, prefix.len())?;
519 }
520 }
521 }
522
523 Ok(())
524 }
525
526 fn write_list_item_content(&mut self, content: &[Node], prefix_len: usize) -> WriteResult<()> {
528 if content.is_empty() {
529 return Ok(());
530 }
531
532 let mut temp_writer = CommonMarkWriter::with_options(self.options.clone());
533
534 for (i, node) in content.iter().enumerate() {
535 if i > 0 {
536 temp_writer.write_char('\n')?;
537 }
538
539 temp_writer.write(node)?;
540 }
541
542 let all_content = temp_writer.into_string();
543
544 let indent = " ".repeat(prefix_len);
545
546 let formatted_content = self.apply_prefix(&all_content, &indent, Some(""));
547
548 self.buffer.push_str(&formatted_content);
549
550 Ok(())
551 }
552
553 pub(crate) fn write_table(&mut self, headers: &[Node], rows: &[Vec<Node>]) -> WriteResult<()> {
555 if Self::table_contains_block_elements(headers, rows) {
557 if self.is_strict_mode() {
558 return Err(WriteError::InvalidStructure(
560 "Table contains block-level elements which are not allowed in strict mode"
561 .to_string()
562 .into(),
563 ));
564 } else {
565 log::info!(
567 "Table contains block-level elements, falling back to HTML output in soft mode"
568 );
569 return self.write_table_as_html(headers, rows);
570 }
571 }
572
573 self.write_char('|')?;
575 for header in headers {
576 self.check_no_newline(header, "Table Header")?;
577 self.write_char(' ')?;
578 self.write(header)?;
579 self.write_str(" |")?;
580 }
581 self.write_char('\n')?;
582
583 self.write_char('|')?;
585 for _ in 0..headers.len() {
586 self.write_str(" --- |")?;
587 }
588 self.write_char('\n')?;
589
590 for row in rows {
592 self.write_char('|')?;
593 for cell in row {
594 self.check_no_newline(cell, "Table Cell")?;
595 self.write_char(' ')?;
596 self.write(cell)?;
597 self.write_str(" |")?;
598 }
599 self.write_char('\n')?;
600 }
601
602 Ok(())
603 }
604
605 #[cfg(feature = "gfm")]
606 pub(crate) fn write_table_with_alignment(
608 &mut self,
609 headers: &[Node],
610 alignments: &[TableAlignment],
611 rows: &[Vec<Node>],
612 ) -> WriteResult<()> {
613 if !self.options.gfm_tables {
615 return self.write_table(headers, rows);
616 }
617
618 if Self::table_contains_block_elements(headers, rows) {
620 if self.is_strict_mode() {
621 return Err(WriteError::InvalidStructure(
623 "GFM table contains block-level elements which are not allowed in strict mode"
624 .to_string()
625 .into(),
626 ));
627 } else {
628 log::info!("GFM table contains block-level elements, falling back to HTML output in soft mode");
630 return self.write_table_as_html_with_alignment(headers, alignments, rows);
631 }
632 }
633
634 self.write_char('|')?;
636 for header in headers {
637 self.check_no_newline(header, "Table Header")?;
638 self.write_char(' ')?;
639 self.write(header)?;
640 self.write_str(" |")?;
641 }
642 self.write_char('\n')?;
643
644 self.write_char('|')?;
647
648 for i in 0..headers.len() {
650 let alignment = if i < alignments.len() {
651 &alignments[i]
652 } else {
653 &TableAlignment::Center
654 };
655
656 match alignment {
657 TableAlignment::Left => self.write_str(" :--- |")?,
658 TableAlignment::Center => self.write_str(" :---: |")?,
659 TableAlignment::Right => self.write_str(" ---: |")?,
660 TableAlignment::None => self.write_str(" --- |")?,
661 }
662 }
663
664 self.write_char('\n')?;
665
666 for row in rows {
668 self.write_char('|')?;
669 for cell in row {
670 self.check_no_newline(cell, "Table Cell")?;
671 self.write_char(' ')?;
672 self.write(cell)?;
673 self.write_str(" |")?;
674 }
675 self.write_char('\n')?;
676 }
677
678 Ok(())
679 }
680
681 pub(crate) fn write_link(
683 &mut self,
684 url: &str,
685 title: &Option<EcoString>,
686 content: &[Node],
687 ) -> WriteResult<()> {
688 for node in content {
689 self.check_no_newline(node, "Link Text")?;
690 }
691 self.write_char('[')?;
692
693 for node in content {
694 self.write(node)?;
695 }
696
697 self.write_str("](")?;
698 self.write_str(url)?;
699
700 if let Some(title_text) = title {
701 self.write_str(" \"")?;
702 self.write_str(title_text)?;
703 self.write_char('"')?;
704 }
705
706 self.write_char(')')?;
707 Ok(())
708 }
709
710 pub(crate) fn write_image(
712 &mut self,
713 url: &str,
714 title: &Option<EcoString>,
715 alt: &[Node],
716 ) -> WriteResult<()> {
717 for node in alt {
719 self.check_no_newline(node, "Image alt text")?;
720 }
721
722 self.write_str("?;
730 self.write_str(url)?;
731
732 if let Some(title_text) = title {
733 self.write_str(" \"")?;
734 self.write_str(title_text)?;
735 self.write_char('"')?;
736 }
737
738 self.write_char(')')?;
739 Ok(())
740 }
741
742 pub(crate) fn write_soft_break(&mut self) -> WriteResult<()> {
744 self.write_char('\n')?;
745 Ok(())
746 }
747
748 pub(crate) fn write_hard_break(&mut self) -> WriteResult<()> {
750 if self.options.hard_break_spaces {
751 self.write_str(" \n")?;
752 } else {
753 self.write_str("\\\n")?;
754 }
755 Ok(())
756 }
757
758 pub(crate) fn write_html_block(&mut self, content: &str) -> WriteResult<()> {
760 self.buffer.push_str(content);
761
762 Ok(())
763 }
764
765 pub(crate) fn write_autolink(&mut self, url: &str, is_email: bool) -> WriteResult<()> {
767 if url.contains('\n') {
769 if self.is_strict_mode() {
770 return Err(WriteError::NewlineInInlineElement(
771 "Autolink URL".to_string().into(),
772 ));
773 } else {
774 log::warn!(
775 "Newline character found in autolink URL '{}'. Writing it as is, which might result in an invalid link. Strict mode is off.",
776 url
777 );
778 }
780 }
781
782 self.write_char('<')?;
784
785 if !is_email && !url.contains(':') {
788 self.write_str("https://")?;
790 }
791
792 self.write_str(url)?;
793 self.write_char('>')?;
794
795 Ok(())
796 }
797
798 #[cfg(feature = "gfm")]
800 pub(crate) fn write_extended_autolink(&mut self, url: &str) -> WriteResult<()> {
801 if !self.options.gfm_autolinks {
802 self.write_text_content(url)?;
804 return Ok(());
805 }
806
807 if url.contains('\n') {
809 if self.is_strict_mode() {
810 return Err(WriteError::NewlineInInlineElement(
812 "Extended Autolink URL".to_string().into(),
813 ));
814 } else {
815 log::warn!(
816 "Newline character found in extended autolink URL '{}'. Writing it as is, which might result in an invalid link. Strict mode is off.",
817 url
818 );
819 }
821 }
822
823 self.write_str(url)?;
825
826 Ok(())
827 }
828
829 pub(crate) fn write_link_reference_definition(
831 &mut self,
832 label: &str,
833 destination: &str,
834 title: &Option<EcoString>,
835 ) -> WriteResult<()> {
836 self.write_char('[')?;
838 self.write_str(label)?;
839 self.write_str("]: ")?;
840 self.write_str(destination)?;
841
842 if let Some(title_text) = title {
843 self.write_str(" \"")?;
844 self.write_str(title_text)?;
845 self.write_char('"')?;
846 }
847
848 Ok(())
849 }
850
851 pub(crate) fn write_reference_link(
853 &mut self,
854 label: &str,
855 content: &[Node],
856 ) -> WriteResult<()> {
857 for node in content {
859 self.check_no_newline(node, "Reference Link Text")?;
860 }
861
862 if content.is_empty() {
865 self.write_char('[')?;
866 self.write_str(label)?;
867 self.write_char(']')?;
868 return Ok(());
869 }
870
871 let is_shortcut =
873 content.len() == 1 && matches!(&content[0], Node::Text(text) if text == label);
874
875 if is_shortcut {
876 self.write_char('[')?;
878 self.write_str(label)?;
879 self.write_char(']')?;
880 } else {
881 self.write_char('[')?;
883
884 for node in content {
885 self.write(node)?;
886 }
887
888 self.write_str("][")?;
889 self.write_str(label)?;
890 self.write_char(']')?;
891 }
892
893 Ok(())
894 }
895
896 pub(crate) fn write_html_element(
898 &mut self,
899 element: &crate::ast::HtmlElement,
900 ) -> WriteResult<()> {
901 if self.options.strict {
902 if element.tag.contains('<') || element.tag.contains('>') {
903 return Err(WriteError::InvalidHtmlTag(element.tag.clone()));
904 }
905
906 for attr in &element.attributes {
907 if attr.name.contains('<') || attr.name.contains('>') {
908 return Err(WriteError::InvalidHtmlAttribute(attr.name.clone()));
909 }
910 }
911 }
912
913 use crate::writer::html::{HtmlWriter, HtmlWriterOptions};
914
915 let html_options = HtmlWriterOptions {
917 strict: self.options.strict,
919 code_block_language_class_prefix: Some("language-".into()),
921 #[cfg(feature = "gfm")]
922 enable_gfm: self.options.enable_gfm,
923 #[cfg(feature = "gfm")]
924 gfm_disallowed_html_tags: self.options.gfm_disallowed_html_tags.clone(),
925 };
926
927 let mut html_writer = HtmlWriter::with_options(html_options);
928
929 html_writer.write_node(&Node::HtmlElement(element.clone()))?;
930
931 let html_output = html_writer.into_string();
933
934 self.write_str(&html_output)
936 }
937
938 pub fn into_string(self) -> EcoString {
954 self.buffer
955 }
956
957 pub fn write_str(&mut self, s: &str) -> WriteResult<()> {
961 self.buffer.push_str(s);
962 Ok(())
963 }
964
965 pub fn write_char(&mut self, c: char) -> WriteResult<()> {
969 self.buffer.push(c);
970 Ok(())
971 }
972 pub(crate) fn ensure_trailing_newline(&mut self) -> WriteResult<()> {
976 if !self.buffer.ends_with('\n') {
977 self.write_char('\n')?;
978 }
979 Ok(())
980 }
981
982 pub(crate) fn write_emphasis(&mut self, content: &[Node]) -> WriteResult<()> {
984 let delimiter = self.options.emphasis_char.to_string();
985 self.write_delimited(content, &delimiter)
986 }
987
988 pub(crate) fn write_strong(&mut self, content: &[Node]) -> WriteResult<()> {
990 let char = self.options.strong_char;
991 let delimiter = format!("{}{}", char, char);
992 self.write_delimited(content, &delimiter)
993 }
994
995 #[cfg(feature = "gfm")]
997 pub(crate) fn write_strikethrough(&mut self, content: &[Node]) -> WriteResult<()> {
998 if !self.options.enable_gfm || !self.options.gfm_strikethrough {
999 for node in content.iter() {
1001 self.write(node)?;
1002 }
1003 return Ok(());
1004 }
1005
1006 self.write_delimited(content, "~~")
1008 }
1009}
1010
1011impl Default for CommonMarkWriter {
1012 fn default() -> Self {
1013 Self::new()
1014 }
1015}
1016
1017impl fmt::Display for Node {
1019 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1020 let mut writer = CommonMarkWriter::new();
1021 match writer.write(self) {
1022 Ok(_) => write!(f, "{}", writer.into_string()),
1023 Err(e) => write!(f, "Error writing Node: {}", e),
1024 }
1025 }
1026}
1027
1028pub(crate) trait Escapes {
1030 fn str_needs_escaping(s: &str) -> bool;
1032
1033 fn char_needs_escaping(c: char) -> bool;
1035
1036 fn escape_char(c: char) -> Option<&'static str>;
1038}
1039
1040pub(crate) struct CommonMarkEscapes;
1042
1043impl Escapes for CommonMarkEscapes {
1044 fn str_needs_escaping(s: &str) -> bool {
1045 s.chars().any(Self::char_needs_escaping)
1046 }
1047
1048 fn char_needs_escaping(c: char) -> bool {
1049 matches!(c, '\\' | '*' | '_' | '[' | ']' | '<' | '>' | '`')
1050 }
1051
1052 fn escape_char(c: char) -> Option<&'static str> {
1053 match c {
1054 '\\' => Some(r"\\"),
1055 '*' => Some(r"\*"),
1056 '_' => Some(r"\_"),
1057 '[' => Some(r"\["),
1058 ']' => Some(r"\]"),
1059 '<' => Some(r"\<"),
1060 '>' => Some(r"\>"),
1061 '`' => Some(r"\`"),
1062 _ => None,
1063 }
1064 }
1065}
1066
1067pub(crate) struct Escaped<'a, E: Escapes> {
1069 inner: &'a str,
1070 _phantom: std::marker::PhantomData<E>,
1071}
1072
1073impl<'a, E: Escapes> Escaped<'a, E> {
1074 pub fn new(s: &'a str) -> Self {
1076 Self {
1077 inner: s,
1078 _phantom: std::marker::PhantomData,
1079 }
1080 }
1081}
1082
1083impl<E: Escapes> std::fmt::Display for Escaped<'_, E> {
1084 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1085 for c in self.inner.chars() {
1086 if E::char_needs_escaping(c) {
1087 f.write_str(E::escape_char(c).unwrap())?;
1088 } else {
1089 write!(f, "{}", c)?;
1090 }
1091 }
1092 Ok(())
1093 }
1094}
1095
1096impl CommonMarkWriter {
1097 fn write_table_as_html(&mut self, headers: &[Node], rows: &[Vec<Node>]) -> WriteResult<()> {
1099 use crate::writer::html::HtmlWriter;
1100
1101 let mut html_writer = HtmlWriter::new();
1102
1103 let table_node = Node::Table {
1105 headers: headers.to_vec(),
1106 #[cfg(feature = "gfm")]
1107 alignments: vec![],
1108 rows: rows.to_vec(),
1109 };
1110
1111 html_writer.write_node(&table_node).map_err(|_| {
1112 WriteError::HtmlFallbackError("Failed to write table as HTML".to_string().into())
1113 })?;
1114
1115 let html_output = html_writer.into_string();
1116 self.buffer.push_str(&html_output);
1117
1118 Ok(())
1119 }
1120
1121 #[cfg(feature = "gfm")]
1122 fn write_table_as_html_with_alignment(
1124 &mut self,
1125 headers: &[Node],
1126 alignments: &[TableAlignment],
1127 rows: &[Vec<Node>],
1128 ) -> WriteResult<()> {
1129 use crate::writer::html::HtmlWriter;
1130
1131 let mut html_writer = HtmlWriter::new();
1132
1133 let table_node = Node::Table {
1135 headers: headers.to_vec(),
1136 alignments: alignments.to_vec(),
1137 rows: rows.to_vec(),
1138 };
1139
1140 html_writer.write_node(&table_node).map_err(|_| {
1141 WriteError::HtmlFallbackError("Failed to write GFM table as HTML".to_string().into())
1142 })?;
1143
1144 let html_output = html_writer.into_string();
1145 self.buffer.push_str(&html_output);
1146
1147 Ok(())
1148 }
1149}
1150
1151pub(crate) fn escape_str<E: Escapes>(s: &str) -> std::borrow::Cow<'_, str> {
1153 if E::str_needs_escaping(s) {
1154 std::borrow::Cow::Owned(format!("{}", Escaped::<E>::new(s)))
1155 } else {
1156 std::borrow::Cow::Borrowed(s)
1157 }
1158}