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 pub(crate) fn write_text_content(&mut self, content: &str) -> WriteResult<()> {
211 if self.options.escape_special_chars {
212 let escaped = escape_str::<CommonMarkEscapes>(content);
213 self.write_str(&escaped)?
214 } else {
215 self.write_str(content)?
216 }
217
218 Ok(())
219 }
220
221 pub(crate) fn write_code_content(&mut self, content: &str) -> WriteResult<()> {
223 self.write_char('`')?;
224 self.write_str(content)?;
225 self.write_char('`')?;
226 Ok(())
227 }
228
229 pub(crate) fn write_delimited(&mut self, content: &[Node], delimiter: &str) -> WriteResult<()> {
231 self.write_str(delimiter)?;
232
233 for node in content {
234 self.write(node)?;
235 }
236
237 self.write_str(delimiter)?;
238 Ok(())
239 }
240
241 pub(crate) fn write_document(&mut self, children: &[Node]) -> WriteResult<()> {
243 for (i, child) in children.iter().enumerate() {
244 if i > 0 {
245 self.write_str("\n")?;
246 }
247 self.write(child)?;
248 }
249 Ok(())
250 }
251
252 pub(crate) fn write_heading(
254 &mut self,
255 mut level: u8,
256 content: &[Node],
257 heading_type: &HeadingType,
258 ) -> WriteResult<()> {
259 if level == 0 || level > 6 {
261 if self.is_strict_mode() {
262 return Err(WriteError::InvalidHeadingLevel(level));
263 } else {
264 let original_level = level;
265 level = level.clamp(1, 6); log::warn!(
267 "Invalid heading level: {}. Corrected to {}. Strict mode is off.",
268 original_level,
269 level
270 );
271 }
272 }
273
274 match heading_type {
275 HeadingType::Atx => {
277 for _ in 0..level {
278 self.write_char('#')?;
279 }
280 self.write_char(' ')?;
281
282 for node in content {
283 self.write(node)?;
284 }
285
286 self.write_char('\n')?;
287 }
288
289 HeadingType::Setext => {
290 for node in content {
292 self.write(node)?;
293 }
294 self.write_char('\n')?;
295
296 let underline_char = if level == 1 { '=' } else { '-' };
299
300 let min_len = 3;
303
304 for _ in 0..min_len {
306 self.write_char(underline_char)?;
307 }
308
309 self.write_char('\n')?;
311 }
312 }
313
314 Ok(())
315 }
316
317 pub(crate) fn write_paragraph(&mut self, content: &[Node]) -> WriteResult<()> {
319 for node in content.iter() {
320 self.write(node)?;
321 }
322
323 Ok(())
324 }
325
326 pub(crate) fn write_blockquote(&mut self, content: &[Node]) -> WriteResult<()> {
328 let mut temp_writer = CommonMarkWriter::with_options(self.options.clone());
330
331 for (i, node) in content.iter().enumerate() {
333 if i > 0 {
334 temp_writer.write_char('\n')?;
335 }
336 temp_writer.write(node)?;
338 }
339
340 let all_content = temp_writer.into_string();
342
343 let prefix = "> ";
345 let formatted_content = self.apply_prefix(&all_content, prefix, Some(prefix));
346
347 self.buffer.push_str(&formatted_content);
349 Ok(())
350 }
351
352 pub(crate) fn write_thematic_break(&mut self) -> WriteResult<()> {
354 let char = self.options.thematic_break_char;
355 self.write_str(&format!("{}{}{}", char, char, char))?;
356 Ok(())
357 }
358
359 pub(crate) fn write_code_block(
361 &mut self,
362 language: &Option<EcoString>,
363 content: &str,
364 block_type: &CodeBlockType,
365 ) -> WriteResult<()> {
366 match block_type {
367 CodeBlockType::Indented => {
368 let indent = " ";
369 let indented_content = self.apply_prefix(content, indent, Some(indent));
370 self.buffer.push_str(&indented_content);
371 }
372 CodeBlockType::Fenced => {
373 let max_backticks = content
374 .chars()
375 .fold((0, 0), |(max, current), c| {
376 if c == '`' {
377 (max.max(current + 1), current + 1)
378 } else {
379 (max, 0)
380 }
381 })
382 .0;
383
384 let fence_len = std::cmp::max(max_backticks + 1, 3);
385 let fence = "`".repeat(fence_len);
386
387 self.write_str(&fence)?;
388 if let Some(lang) = language {
389 self.write_str(lang)?;
390 }
391 self.write_char('\n')?;
392
393 self.buffer.push_str(content);
394 if !content.ends_with('\n') {
395 self.write_char('\n')?;
396 }
397
398 self.write_str(&fence)?;
399 }
400 }
401
402 Ok(())
403 }
404
405 pub(crate) fn write_unordered_list(&mut self, items: &[ListItem]) -> WriteResult<()> {
407 let list_marker = self.options.list_marker;
408 let prefix = format!("{} ", list_marker);
409
410 for (i, item) in items.iter().enumerate() {
411 if i > 0 {
412 self.write_char('\n')?;
413 }
414 self.write_list_item(item, &prefix)?;
415 }
416
417 Ok(())
418 }
419
420 pub(crate) fn write_ordered_list(&mut self, start: u32, items: &[ListItem]) -> WriteResult<()> {
422 let mut current_number = start;
424
425 for (i, item) in items.iter().enumerate() {
426 if i > 0 {
427 self.write_char('\n')?;
428 }
429
430 match item {
431 ListItem::Ordered { number, content: _ } => {
433 if let Some(custom_num) = number {
434 let prefix = format!("{}. ", custom_num);
436 self.write_list_item(item, &prefix)?;
437 current_number = custom_num + 1;
439 } else {
440 let prefix = format!("{}. ", current_number);
442 self.write_list_item(item, &prefix)?;
443 current_number += 1;
444 }
445 }
446 _ => {
448 let prefix = format!("{}. ", current_number);
449 self.write_list_item(item, &prefix)?;
450 current_number += 1;
451 }
452 }
453 }
454
455 Ok(())
456 }
457
458 fn write_list_item(&mut self, item: &ListItem, prefix: &str) -> WriteResult<()> {
460 match item {
461 ListItem::Unordered { content } => {
462 self.write_str(prefix)?;
463 self.write_list_item_content(content, prefix.len())?;
464 }
465 ListItem::Ordered { number, content } => {
466 if let Some(num) = number {
467 let custom_prefix = format!("{}. ", num);
468 self.write_str(&custom_prefix)?;
469 self.write_list_item_content(content, custom_prefix.len())?;
470 } else {
471 self.write_str(prefix)?;
472 self.write_list_item_content(content, prefix.len())?;
473 }
474 }
475 #[cfg(feature = "gfm")]
476 ListItem::Task { status, content } => {
477 if self.options.gfm_tasklists {
479 let checkbox = match status {
480 crate::ast::TaskListStatus::Checked => "[x] ",
481 crate::ast::TaskListStatus::Unchecked => "[ ] ",
482 };
483
484 let task_prefix = format!("{}{}", prefix, checkbox);
486 self.write_str(&task_prefix)?;
487 self.write_list_item_content(content, task_prefix.len())?;
488 } else {
489 self.write_str(prefix)?;
491 self.write_list_item_content(content, prefix.len())?;
492 }
493 }
494 }
495
496 Ok(())
497 }
498
499 fn write_list_item_content(&mut self, content: &[Node], prefix_len: usize) -> WriteResult<()> {
501 if content.is_empty() {
502 return Ok(());
503 }
504
505 let mut temp_writer = CommonMarkWriter::with_options(self.options.clone());
506
507 for (i, node) in content.iter().enumerate() {
508 if i > 0 {
509 temp_writer.write_char('\n')?;
510 }
511
512 temp_writer.write(node)?;
513 }
514
515 let all_content = temp_writer.into_string();
516
517 let indent = " ".repeat(prefix_len);
518
519 let formatted_content = self.apply_prefix(&all_content, &indent, Some(""));
520
521 self.buffer.push_str(&formatted_content);
522
523 Ok(())
524 }
525
526 pub(crate) fn write_table(&mut self, headers: &[Node], rows: &[Vec<Node>]) -> WriteResult<()> {
528 self.write_char('|')?;
530 for header in headers {
531 self.check_no_newline(header, "Table Header")?;
532 self.write_char(' ')?;
533 self.write(header)?;
534 self.write_str(" |")?;
535 }
536 self.write_char('\n')?;
537
538 self.write_char('|')?;
540 for _ in 0..headers.len() {
541 self.write_str(" --- |")?;
542 }
543 self.write_char('\n')?;
544
545 for row in rows {
547 self.write_char('|')?;
548 for cell in row {
549 self.check_no_newline(cell, "Table Cell")?;
550 self.write_char(' ')?;
551 self.write(cell)?;
552 self.write_str(" |")?;
553 }
554 self.write_char('\n')?;
555 }
556
557 Ok(())
558 }
559
560 #[cfg(feature = "gfm")]
561 pub(crate) fn write_table_with_alignment(
563 &mut self,
564 headers: &[Node],
565 alignments: &[TableAlignment],
566 rows: &[Vec<Node>],
567 ) -> WriteResult<()> {
568 if !self.options.gfm_tables {
570 return self.write_table(headers, rows);
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('|')?;
586
587 for i in 0..headers.len() {
589 let alignment = if i < alignments.len() {
590 &alignments[i]
591 } else {
592 &TableAlignment::Center
593 };
594
595 match alignment {
596 TableAlignment::Left => self.write_str(" :--- |")?,
597 TableAlignment::Center => self.write_str(" :---: |")?,
598 TableAlignment::Right => self.write_str(" ---: |")?,
599 TableAlignment::None => self.write_str(" --- |")?,
600 }
601 }
602
603 self.write_char('\n')?;
604
605 for row in rows {
607 self.write_char('|')?;
608 for cell in row {
609 self.check_no_newline(cell, "Table Cell")?;
610 self.write_char(' ')?;
611 self.write(cell)?;
612 self.write_str(" |")?;
613 }
614 self.write_char('\n')?;
615 }
616
617 Ok(())
618 }
619
620 pub(crate) fn write_link(
622 &mut self,
623 url: &str,
624 title: &Option<EcoString>,
625 content: &[Node],
626 ) -> WriteResult<()> {
627 for node in content {
628 self.check_no_newline(node, "Link Text")?;
629 }
630 self.write_char('[')?;
631
632 for node in content {
633 self.write(node)?;
634 }
635
636 self.write_str("](")?;
637 self.write_str(url)?;
638
639 if let Some(title_text) = title {
640 self.write_str(" \"")?;
641 self.write_str(title_text)?;
642 self.write_char('"')?;
643 }
644
645 self.write_char(')')?;
646 Ok(())
647 }
648
649 pub(crate) fn write_image(
651 &mut self,
652 url: &str,
653 title: &Option<EcoString>,
654 alt: &[Node],
655 ) -> WriteResult<()> {
656 for node in alt {
658 self.check_no_newline(node, "Image alt text")?;
659 }
660
661 self.write_str("?;
669 self.write_str(url)?;
670
671 if let Some(title_text) = title {
672 self.write_str(" \"")?;
673 self.write_str(title_text)?;
674 self.write_char('"')?;
675 }
676
677 self.write_char(')')?;
678 Ok(())
679 }
680
681 pub(crate) fn write_soft_break(&mut self) -> WriteResult<()> {
683 self.write_char('\n')?;
684 Ok(())
685 }
686
687 pub(crate) fn write_hard_break(&mut self) -> WriteResult<()> {
689 if self.options.hard_break_spaces {
690 self.write_str(" \n")?;
691 } else {
692 self.write_str("\\\n")?;
693 }
694 Ok(())
695 }
696
697 pub(crate) fn write_html_block(&mut self, content: &str) -> WriteResult<()> {
699 self.buffer.push_str(content);
700
701 Ok(())
702 }
703
704 pub(crate) fn write_autolink(&mut self, url: &str, is_email: bool) -> WriteResult<()> {
706 if url.contains('\n') {
708 if self.is_strict_mode() {
709 return Err(WriteError::NewlineInInlineElement(
710 "Autolink URL".to_string().into(),
711 ));
712 } else {
713 log::warn!(
714 "Newline character found in autolink URL '{}'. Writing it as is, which might result in an invalid link. Strict mode is off.",
715 url
716 );
717 }
719 }
720
721 self.write_char('<')?;
723
724 if !is_email && !url.contains(':') {
727 self.write_str("https://")?;
729 }
730
731 self.write_str(url)?;
732 self.write_char('>')?;
733
734 Ok(())
735 }
736
737 #[cfg(feature = "gfm")]
739 pub(crate) fn write_extended_autolink(&mut self, url: &str) -> WriteResult<()> {
740 if !self.options.gfm_autolinks {
741 self.write_text_content(url)?;
743 return Ok(());
744 }
745
746 if url.contains('\n') {
748 if self.is_strict_mode() {
749 return Err(WriteError::NewlineInInlineElement(
751 "Extended Autolink URL".to_string().into(),
752 ));
753 } else {
754 log::warn!(
755 "Newline character found in extended autolink URL '{}'. Writing it as is, which might result in an invalid link. Strict mode is off.",
756 url
757 );
758 }
760 }
761
762 self.write_str(url)?;
764
765 Ok(())
766 }
767
768 pub(crate) fn write_link_reference_definition(
770 &mut self,
771 label: &str,
772 destination: &str,
773 title: &Option<EcoString>,
774 ) -> WriteResult<()> {
775 self.write_char('[')?;
777 self.write_str(label)?;
778 self.write_str("]: ")?;
779 self.write_str(destination)?;
780
781 if let Some(title_text) = title {
782 self.write_str(" \"")?;
783 self.write_str(title_text)?;
784 self.write_char('"')?;
785 }
786
787 Ok(())
788 }
789
790 pub(crate) fn write_reference_link(
792 &mut self,
793 label: &str,
794 content: &[Node],
795 ) -> WriteResult<()> {
796 for node in content {
798 self.check_no_newline(node, "Reference Link Text")?;
799 }
800
801 if content.is_empty() {
804 self.write_char('[')?;
805 self.write_str(label)?;
806 self.write_char(']')?;
807 return Ok(());
808 }
809
810 let is_shortcut =
812 content.len() == 1 && matches!(&content[0], Node::Text(text) if text == label);
813
814 if is_shortcut {
815 self.write_char('[')?;
817 self.write_str(label)?;
818 self.write_char(']')?;
819 } else {
820 self.write_char('[')?;
822
823 for node in content {
824 self.write(node)?;
825 }
826
827 self.write_str("][")?;
828 self.write_str(label)?;
829 self.write_char(']')?;
830 }
831
832 Ok(())
833 }
834
835 pub(crate) fn write_html_element(
837 &mut self,
838 element: &crate::ast::HtmlElement,
839 ) -> WriteResult<()> {
840 if self.options.strict {
841 if element.tag.contains('<') || element.tag.contains('>') {
842 return Err(WriteError::InvalidHtmlTag(element.tag.clone()));
843 }
844
845 for attr in &element.attributes {
846 if attr.name.contains('<') || attr.name.contains('>') {
847 return Err(WriteError::InvalidHtmlAttribute(attr.name.clone()));
848 }
849 }
850 }
851
852 use crate::writer::html::{HtmlWriter, HtmlWriterOptions};
853
854 let html_options = HtmlWriterOptions {
856 strict: self.options.strict,
858 code_block_language_class_prefix: Some("language-".into()),
860 #[cfg(feature = "gfm")]
861 enable_gfm: self.options.enable_gfm,
862 #[cfg(feature = "gfm")]
863 gfm_disallowed_html_tags: self.options.gfm_disallowed_html_tags.clone(),
864 };
865
866 let mut html_writer = HtmlWriter::with_options(html_options);
867
868 html_writer.write_node(&Node::HtmlElement(element.clone()))?;
869
870 let html_output = html_writer.into_string();
872
873 self.write_str(&html_output)
875 }
876
877 pub fn into_string(self) -> EcoString {
893 self.buffer
894 }
895
896 pub fn write_str(&mut self, s: &str) -> WriteResult<()> {
900 self.buffer.push_str(s);
901 Ok(())
902 }
903
904 pub fn write_char(&mut self, c: char) -> WriteResult<()> {
908 self.buffer.push(c);
909 Ok(())
910 }
911 pub(crate) fn ensure_trailing_newline(&mut self) -> WriteResult<()> {
915 if !self.buffer.ends_with('\n') {
916 self.write_char('\n')?;
917 }
918 Ok(())
919 }
920
921 pub(crate) fn write_emphasis(&mut self, content: &[Node]) -> WriteResult<()> {
923 let delimiter = self.options.emphasis_char.to_string();
924 self.write_delimited(content, &delimiter)
925 }
926
927 pub(crate) fn write_strong(&mut self, content: &[Node]) -> WriteResult<()> {
929 let char = self.options.strong_char;
930 let delimiter = format!("{}{}", char, char);
931 self.write_delimited(content, &delimiter)
932 }
933
934 #[cfg(feature = "gfm")]
936 pub(crate) fn write_strikethrough(&mut self, content: &[Node]) -> WriteResult<()> {
937 if !self.options.enable_gfm || !self.options.gfm_strikethrough {
938 for node in content.iter() {
940 self.write(node)?;
941 }
942 return Ok(());
943 }
944
945 self.write_delimited(content, "~~")
947 }
948}
949
950impl Default for CommonMarkWriter {
951 fn default() -> Self {
952 Self::new()
953 }
954}
955
956impl fmt::Display for Node {
958 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
959 let mut writer = CommonMarkWriter::new();
960 match writer.write(self) {
961 Ok(_) => write!(f, "{}", writer.into_string()),
962 Err(e) => write!(f, "Error writing Node: {}", e),
963 }
964 }
965}
966
967pub(crate) trait Escapes {
969 fn str_needs_escaping(s: &str) -> bool;
971
972 fn char_needs_escaping(c: char) -> bool;
974
975 fn escape_char(c: char) -> Option<&'static str>;
977}
978
979pub(crate) struct CommonMarkEscapes;
981
982impl Escapes for CommonMarkEscapes {
983 fn str_needs_escaping(s: &str) -> bool {
984 s.chars().any(Self::char_needs_escaping)
985 }
986
987 fn char_needs_escaping(c: char) -> bool {
988 matches!(c, '\\' | '*' | '_' | '[' | ']' | '<' | '>' | '`')
989 }
990
991 fn escape_char(c: char) -> Option<&'static str> {
992 match c {
993 '\\' => Some(r"\\"),
994 '*' => Some(r"\*"),
995 '_' => Some(r"\_"),
996 '[' => Some(r"\["),
997 ']' => Some(r"\]"),
998 '<' => Some(r"\<"),
999 '>' => Some(r"\>"),
1000 '`' => Some(r"\`"),
1001 _ => None,
1002 }
1003 }
1004}
1005
1006pub(crate) struct Escaped<'a, E: Escapes> {
1008 inner: &'a str,
1009 _phantom: std::marker::PhantomData<E>,
1010}
1011
1012impl<'a, E: Escapes> Escaped<'a, E> {
1013 pub fn new(s: &'a str) -> Self {
1015 Self {
1016 inner: s,
1017 _phantom: std::marker::PhantomData,
1018 }
1019 }
1020}
1021
1022impl<E: Escapes> std::fmt::Display for Escaped<'_, E> {
1023 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1024 for c in self.inner.chars() {
1025 if E::char_needs_escaping(c) {
1026 f.write_str(E::escape_char(c).unwrap())?;
1027 } else {
1028 write!(f, "{}", c)?;
1029 }
1030 }
1031 Ok(())
1032 }
1033}
1034
1035pub(crate) fn escape_str<E: Escapes>(s: &str) -> std::borrow::Cow<'_, str> {
1037 if E::str_needs_escaping(s) {
1038 std::borrow::Cow::Owned(format!("{}", Escaped::<E>::new(s)))
1039 } else {
1040 std::borrow::Cow::Borrowed(s)
1041 }
1042}