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 = if let Some(ref custom_options) = self.options.html_writer_options {
916 custom_options.clone()
917 } else {
918 HtmlWriterOptions {
919 strict: self.options.strict,
920 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
928 let mut html_writer = HtmlWriter::with_options(html_options);
929
930 html_writer.write_node(&Node::HtmlElement(element.clone()))?;
931
932 let html_output = html_writer.into_string();
934
935 self.write_str(&html_output)
937 }
938
939 pub fn into_string(self) -> EcoString {
955 self.buffer
956 }
957
958 pub fn write_str(&mut self, s: &str) -> WriteResult<()> {
962 self.buffer.push_str(s);
963 Ok(())
964 }
965
966 pub fn write_char(&mut self, c: char) -> WriteResult<()> {
970 self.buffer.push(c);
971 Ok(())
972 }
973 pub(crate) fn ensure_trailing_newline(&mut self) -> WriteResult<()> {
977 if !self.buffer.ends_with('\n') {
978 self.write_char('\n')?;
979 }
980 Ok(())
981 }
982
983 pub(crate) fn write_emphasis(&mut self, content: &[Node]) -> WriteResult<()> {
985 let delimiter = self.options.emphasis_char.to_string();
986 self.write_delimited(content, &delimiter)
987 }
988
989 pub(crate) fn write_strong(&mut self, content: &[Node]) -> WriteResult<()> {
991 let char = self.options.strong_char;
992 let delimiter = format!("{}{}", char, char);
993 self.write_delimited(content, &delimiter)
994 }
995
996 #[cfg(feature = "gfm")]
998 pub(crate) fn write_strikethrough(&mut self, content: &[Node]) -> WriteResult<()> {
999 if !self.options.enable_gfm || !self.options.gfm_strikethrough {
1000 for node in content.iter() {
1002 self.write(node)?;
1003 }
1004 return Ok(());
1005 }
1006
1007 self.write_delimited(content, "~~")
1009 }
1010}
1011
1012impl Default for CommonMarkWriter {
1013 fn default() -> Self {
1014 Self::new()
1015 }
1016}
1017
1018impl fmt::Display for Node {
1020 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1021 let mut writer = CommonMarkWriter::new();
1022 match writer.write(self) {
1023 Ok(_) => write!(f, "{}", writer.into_string()),
1024 Err(e) => write!(f, "Error writing Node: {}", e),
1025 }
1026 }
1027}
1028
1029pub(crate) trait Escapes {
1031 fn str_needs_escaping(s: &str) -> bool;
1033
1034 fn char_needs_escaping(c: char) -> bool;
1036
1037 fn escape_char(c: char) -> Option<&'static str>;
1039}
1040
1041pub(crate) struct CommonMarkEscapes;
1043
1044impl Escapes for CommonMarkEscapes {
1045 fn str_needs_escaping(s: &str) -> bool {
1046 s.chars().any(Self::char_needs_escaping)
1047 }
1048
1049 fn char_needs_escaping(c: char) -> bool {
1050 matches!(c, '\\' | '*' | '_' | '[' | ']' | '<' | '>' | '`')
1051 }
1052
1053 fn escape_char(c: char) -> Option<&'static str> {
1054 match c {
1055 '\\' => Some(r"\\"),
1056 '*' => Some(r"\*"),
1057 '_' => Some(r"\_"),
1058 '[' => Some(r"\["),
1059 ']' => Some(r"\]"),
1060 '<' => Some(r"\<"),
1061 '>' => Some(r"\>"),
1062 '`' => Some(r"\`"),
1063 _ => None,
1064 }
1065 }
1066}
1067
1068pub(crate) struct Escaped<'a, E: Escapes> {
1070 inner: &'a str,
1071 _phantom: std::marker::PhantomData<E>,
1072}
1073
1074impl<'a, E: Escapes> Escaped<'a, E> {
1075 pub fn new(s: &'a str) -> Self {
1077 Self {
1078 inner: s,
1079 _phantom: std::marker::PhantomData,
1080 }
1081 }
1082}
1083
1084impl<E: Escapes> std::fmt::Display for Escaped<'_, E> {
1085 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1086 for c in self.inner.chars() {
1087 if E::char_needs_escaping(c) {
1088 f.write_str(E::escape_char(c).unwrap())?;
1089 } else {
1090 write!(f, "{}", c)?;
1091 }
1092 }
1093 Ok(())
1094 }
1095}
1096
1097impl CommonMarkWriter {
1098 fn write_table_as_html(&mut self, headers: &[Node], rows: &[Vec<Node>]) -> WriteResult<()> {
1100 use crate::writer::html::HtmlWriter;
1101
1102 let mut html_writer = HtmlWriter::new();
1103
1104 let table_node = Node::Table {
1106 headers: headers.to_vec(),
1107 #[cfg(feature = "gfm")]
1108 alignments: vec![],
1109 rows: rows.to_vec(),
1110 };
1111
1112 html_writer.write_node(&table_node).map_err(|_| {
1113 WriteError::HtmlFallbackError("Failed to write table as HTML".to_string().into())
1114 })?;
1115
1116 let html_output = html_writer.into_string();
1117 self.buffer.push_str(&html_output);
1118
1119 Ok(())
1120 }
1121
1122 #[cfg(feature = "gfm")]
1123 fn write_table_as_html_with_alignment(
1125 &mut self,
1126 headers: &[Node],
1127 alignments: &[TableAlignment],
1128 rows: &[Vec<Node>],
1129 ) -> WriteResult<()> {
1130 use crate::writer::html::HtmlWriter;
1131
1132 let mut html_writer = HtmlWriter::new();
1133
1134 let table_node = Node::Table {
1136 headers: headers.to_vec(),
1137 alignments: alignments.to_vec(),
1138 rows: rows.to_vec(),
1139 };
1140
1141 html_writer.write_node(&table_node).map_err(|_| {
1142 WriteError::HtmlFallbackError("Failed to write GFM table as HTML".to_string().into())
1143 })?;
1144
1145 let html_output = html_writer.into_string();
1146 self.buffer.push_str(&html_output);
1147
1148 Ok(())
1149 }
1150}
1151
1152pub(crate) fn escape_str<E: Escapes>(s: &str) -> std::borrow::Cow<'_, str> {
1154 if E::str_needs_escaping(s) {
1155 std::borrow::Cow::Owned(format!("{}", Escaped::<E>::new(s)))
1156 } else {
1157 std::borrow::Cow::Borrowed(s)
1158 }
1159}