1#[cfg(feature = "gfm")]
6use crate::ast::TableAlignment;
7use crate::ast::{
8 CodeBlockType, CustomNode, CustomNodeWriter, HeadingType, HtmlElement, ListItem, Node,
9};
10use crate::error::{WriteError, WriteResult};
11use crate::options::WriterOptions;
12use log;
13use std::fmt::{self};
14
15use super::processors::{
16 BlockNodeProcessor, CustomNodeProcessor, InlineNodeProcessor, NodeProcessor,
17};
18
19use crate::writer::html;
20
21#[derive(Debug)]
25pub struct CommonMarkWriter {
26 options: WriterOptions,
27 buffer: String,
28}
29
30impl CommonMarkWriter {
31 pub fn new() -> Self {
44 Self::with_options(WriterOptions::default())
45 }
46
47 pub fn with_options(options: WriterOptions) -> Self {
68 Self {
69 options,
70 buffer: String::new(),
71 }
72 }
73
74 pub(crate) fn is_strict_mode(&self) -> bool {
76 self.options.strict
77 }
78
79 fn apply_prefix(&self, content: &str, prefix: &str, first_line_prefix: Option<&str>) -> String {
91 if content.is_empty() {
92 return String::new();
93 }
94
95 let mut result = String::new();
96 let lines: Vec<&str> = content.lines().collect();
97
98 if !lines.is_empty() {
99 let actual_prefix = first_line_prefix.unwrap_or(prefix);
100 result.push_str(actual_prefix);
101 result.push_str(lines[0]);
102 }
103
104 for line in &lines[1..] {
105 result.push('\n');
106 result.push_str(prefix);
107 result.push_str(line);
108 }
109
110 result
111 }
112
113 pub fn write(&mut self, node: &Node) -> WriteResult<()> {
133 if let Node::Custom(_) = node {
134 return CustomNodeProcessor.process(self, node);
135 }
136
137 if node.is_block() {
138 BlockNodeProcessor.process(self, node)
139 } else if node.is_inline() {
140 InlineNodeProcessor.process(self, node)
141 } else {
142 log::warn!("Unsupported node type encountered and skipped: {:?}", node);
143 Ok(())
144 }
145 }
146
147 #[allow(clippy::borrowed_box)]
149 pub(crate) fn write_custom_node(&mut self, node: &Box<dyn CustomNode>) -> WriteResult<()> {
150 node.write(self)
151 }
152
153 pub(crate) fn get_context_for_node(&self, node: &Node) -> String {
155 match node {
156 Node::Text(_) => "Text".to_string(),
157 Node::Emphasis(_) => "Emphasis".to_string(),
158 Node::Strong(_) => "Strong".to_string(),
159 #[cfg(feature = "gfm")]
160 Node::Strikethrough(_) => "Strikethrough".to_string(),
161 Node::InlineCode(_) => "InlineCode".to_string(),
162 Node::Link { .. } => "Link content".to_string(),
163 Node::Image { .. } => "Image alt text".to_string(),
164 Node::HtmlElement(_) => "HtmlElement content".to_string(),
165 Node::Custom(_) => "Custom node".to_string(),
166 _ => "Unknown inline element".to_string(),
167 }
168 }
169
170 pub(crate) fn check_no_newline(&self, node: &Node, context: &str) -> WriteResult<()> {
172 if Self::node_contains_newline(node) {
173 if self.is_strict_mode() {
174 return Err(WriteError::NewlineInInlineElement(context.to_string()));
175 } else {
176 log::warn!(
177 "Newline character found in inline element '{}', but non-strict mode allows it (output may be affected).",
178 context
179 );
180 }
181 }
182 Ok(())
183 }
184
185 fn node_contains_newline(node: &Node) -> bool {
187 match node {
188 Node::Text(s) | Node::InlineCode(s) => s.contains('\n'),
189 Node::Emphasis(children) | Node::Strong(children) => {
190 children.iter().any(Self::node_contains_newline)
191 }
192 #[cfg(feature = "gfm")]
193 Node::Strikethrough(children) => children.iter().any(Self::node_contains_newline),
194 Node::HtmlElement(element) => element.children.iter().any(Self::node_contains_newline),
195 Node::Link { content, .. } => content.iter().any(Self::node_contains_newline),
196 Node::Image { alt, .. } => alt.iter().any(Self::node_contains_newline),
197 Node::SoftBreak | Node::HardBreak => true,
198 Node::Custom(_) => false,
200 _ => false,
201 }
202 }
203
204 pub(crate) fn write_text_content(&mut self, content: &str) -> WriteResult<()> {
206 if self.options.escape_special_chars {
207 let escaped = content
208 .replace('\\', "\\\\")
209 .replace('*', "\\*")
210 .replace('_', "\\_")
211 .replace('[', "\\[")
212 .replace(']', "\\]")
213 .replace('<', "\\<")
214 .replace('>', "\\>")
215 .replace('`', "\\`");
216
217 self.write_str(&escaped)?;
218 } else {
219 self.write_str(content)?;
220 }
221
222 Ok(())
223 }
224
225 pub(crate) fn write_code_content(&mut self, content: &str) -> WriteResult<()> {
227 self.write_char('`')?;
228 self.write_str(content)?;
229 self.write_char('`')?;
230 Ok(())
231 }
232
233 pub(crate) fn write_delimited(&mut self, content: &[Node], delimiter: &str) -> WriteResult<()> {
235 self.write_str(delimiter)?;
236
237 for node in content {
238 self.write(node)?;
239 }
240
241 self.write_str(delimiter)?;
242 Ok(())
243 }
244
245 pub(crate) fn write_document(&mut self, children: &[Node]) -> WriteResult<()> {
247 for (i, child) in children.iter().enumerate() {
248 if i > 0 {
249 self.write_str("\n")?;
250 }
251 self.write(child)?;
252 }
253 Ok(())
254 }
255
256 pub(crate) fn write_heading(
258 &mut self,
259 mut level: u8,
260 content: &[Node],
261 heading_type: &HeadingType,
262 ) -> WriteResult<()> {
263 if level == 0 || level > 6 {
265 if self.is_strict_mode() {
266 return Err(WriteError::InvalidHeadingLevel(level));
267 } else {
268 let original_level = level;
269 level = level.clamp(1, 6); log::warn!(
271 "Invalid heading level: {}. Corrected to {}. Strict mode is off.",
272 original_level,
273 level
274 );
275 }
276 }
277
278 match heading_type {
279 HeadingType::Atx => {
281 for _ in 0..level {
282 self.write_char('#')?;
283 }
284 self.write_char(' ')?;
285
286 for node in content {
287 self.write(node)?;
288 }
289
290 self.write_char('\n')?;
291 }
292
293 HeadingType::Setext => {
294 for node in content {
296 self.write(node)?;
297 }
298 self.write_char('\n')?;
299
300 let underline_char = if level == 1 { '=' } else { '-' };
303
304 let min_len = 3;
307
308 for _ in 0..min_len {
310 self.write_char(underline_char)?;
311 }
312
313 self.write_char('\n')?;
315 }
316 }
317
318 Ok(())
319 }
320
321 pub(crate) fn write_paragraph(&mut self, content: &[Node]) -> WriteResult<()> {
323 for node in content.iter() {
324 self.write(node)?;
325 }
326
327 Ok(())
328 }
329
330 pub(crate) fn write_blockquote(&mut self, content: &[Node]) -> WriteResult<()> {
332 let mut temp_writer = CommonMarkWriter::with_options(self.options.clone());
334
335 for (i, node) in content.iter().enumerate() {
337 if i > 0 {
338 temp_writer.write_char('\n')?;
339 }
340 temp_writer.write(node)?;
342 }
343
344 let all_content = temp_writer.into_string();
346
347 let prefix = "> ";
349 let formatted_content = self.apply_prefix(&all_content, prefix, Some(prefix));
350
351 self.buffer.push_str(&formatted_content);
353 Ok(())
354 }
355
356 pub(crate) fn write_thematic_break(&mut self) -> WriteResult<()> {
358 let char = self.options.thematic_break_char;
359 self.write_str(&format!("{}{}{}", char, char, char))?;
360 Ok(())
361 }
362
363 pub(crate) fn write_code_block(
365 &mut self,
366 language: &Option<String>,
367 content: &str,
368 block_type: &CodeBlockType,
369 ) -> WriteResult<()> {
370 match block_type {
371 CodeBlockType::Indented => {
372 let indent = " ";
373 let indented_content = self.apply_prefix(content, indent, Some(indent));
374 self.buffer.push_str(&indented_content);
375 }
376 CodeBlockType::Fenced => {
377 let max_backticks = content
378 .chars()
379 .fold((0, 0), |(max, current), c| {
380 if c == '`' {
381 (max.max(current + 1), current + 1)
382 } else {
383 (max, 0)
384 }
385 })
386 .0;
387
388 let fence_len = std::cmp::max(max_backticks + 1, 3);
389 let fence = "`".repeat(fence_len);
390
391 self.write_str(&fence)?;
392 if let Some(lang) = language {
393 self.write_str(lang)?;
394 }
395 self.write_char('\n')?;
396
397 self.buffer.push_str(content);
398 if !content.ends_with('\n') {
399 self.write_char('\n')?;
400 }
401
402 self.write_str(&fence)?;
403 }
404 }
405
406 Ok(())
407 }
408
409 pub(crate) fn write_unordered_list(&mut self, items: &[ListItem]) -> WriteResult<()> {
411 let list_marker = self.options.list_marker;
412 let prefix = format!("{} ", list_marker);
413
414 for (i, item) in items.iter().enumerate() {
415 if i > 0 {
416 self.write_char('\n')?;
417 }
418 self.write_list_item(item, &prefix)?;
419 }
420
421 Ok(())
422 }
423
424 pub(crate) fn write_ordered_list(&mut self, start: u32, items: &[ListItem]) -> WriteResult<()> {
426 let mut current_number = start;
428
429 for (i, item) in items.iter().enumerate() {
430 if i > 0 {
431 self.write_char('\n')?;
432 }
433
434 match item {
435 ListItem::Ordered { number, content: _ } => {
437 if let Some(custom_num) = number {
438 let prefix = format!("{}. ", custom_num);
440 self.write_list_item(item, &prefix)?;
441 current_number = custom_num + 1;
443 } else {
444 let prefix = format!("{}. ", current_number);
446 self.write_list_item(item, &prefix)?;
447 current_number += 1;
448 }
449 }
450 _ => {
452 let prefix = format!("{}. ", current_number);
453 self.write_list_item(item, &prefix)?;
454 current_number += 1;
455 }
456 }
457 }
458
459 Ok(())
460 }
461
462 fn write_list_item(&mut self, item: &ListItem, prefix: &str) -> WriteResult<()> {
464 match item {
465 ListItem::Unordered { content } => {
466 self.write_str(prefix)?;
467 self.write_list_item_content(content, prefix.len())?;
468 }
469 ListItem::Ordered { number, content } => {
470 if let Some(num) = number {
471 let custom_prefix = format!("{}. ", num);
472 self.write_str(&custom_prefix)?;
473 self.write_list_item_content(content, custom_prefix.len())?;
474 } else {
475 self.write_str(prefix)?;
476 self.write_list_item_content(content, prefix.len())?;
477 }
478 }
479 #[cfg(feature = "gfm")]
480 ListItem::Task { status, content } => {
481 if self.options.gfm_tasklists {
483 let checkbox = match status {
484 crate::ast::TaskListStatus::Checked => "[x] ",
485 crate::ast::TaskListStatus::Unchecked => "[ ] ",
486 };
487
488 let task_prefix = format!("{}{}", prefix, checkbox);
490 self.write_str(&task_prefix)?;
491 self.write_list_item_content(content, task_prefix.len())?;
492 } else {
493 self.write_str(prefix)?;
495 self.write_list_item_content(content, prefix.len())?;
496 }
497 }
498 }
499
500 Ok(())
501 }
502
503 fn write_list_item_content(&mut self, content: &[Node], prefix_len: usize) -> WriteResult<()> {
505 if content.is_empty() {
506 return Ok(());
507 }
508
509 let mut temp_writer = CommonMarkWriter::with_options(self.options.clone());
510
511 for (i, node) in content.iter().enumerate() {
512 if i > 0 {
513 temp_writer.write_char('\n')?;
514 }
515
516 temp_writer.write(node)?;
517 }
518
519 let all_content = temp_writer.into_string();
520
521 let indent = " ".repeat(prefix_len);
522
523 let formatted_content = self.apply_prefix(&all_content, &indent, Some(""));
524
525 self.buffer.push_str(&formatted_content);
526
527 Ok(())
528 }
529
530 pub(crate) fn write_table(&mut self, headers: &[Node], rows: &[Vec<Node>]) -> WriteResult<()> {
532 self.write_char('|')?;
534 for header in headers {
535 self.check_no_newline(header, "Table Header")?;
536 self.write_char(' ')?;
537 self.write(header)?;
538 self.write_str(" |")?;
539 }
540 self.write_char('\n')?;
541
542 self.write_char('|')?;
544 for _ in 0..headers.len() {
545 self.write_str(" --- |")?;
546 }
547 self.write_char('\n')?;
548
549 for row in rows {
551 self.write_char('|')?;
552 for cell in row {
553 self.check_no_newline(cell, "Table Cell")?;
554 self.write_char(' ')?;
555 self.write(cell)?;
556 self.write_str(" |")?;
557 }
558 self.write_char('\n')?;
559 }
560
561 Ok(())
562 }
563
564 #[cfg(feature = "gfm")]
565 pub(crate) fn write_table_with_alignment(
567 &mut self,
568 headers: &[Node],
569 alignments: &[TableAlignment],
570 rows: &[Vec<Node>],
571 ) -> WriteResult<()> {
572 if !self.options.gfm_tables {
574 return self.write_table(headers, rows);
575 }
576
577 self.write_char('|')?;
579 for header in headers {
580 self.check_no_newline(header, "Table Header")?;
581 self.write_char(' ')?;
582 self.write(header)?;
583 self.write_str(" |")?;
584 }
585 self.write_char('\n')?;
586
587 self.write_char('|')?;
590
591 for i in 0..headers.len() {
593 let alignment = if i < alignments.len() {
594 &alignments[i]
595 } else {
596 &TableAlignment::Center
597 };
598
599 match alignment {
600 TableAlignment::Left => self.write_str(" :--- |")?,
601 TableAlignment::Center => self.write_str(" :---: |")?,
602 TableAlignment::Right => self.write_str(" ---: |")?,
603 TableAlignment::None => self.write_str(" --- |")?,
604 }
605 }
606
607 self.write_char('\n')?;
608
609 for row in rows {
611 self.write_char('|')?;
612 for cell in row {
613 self.check_no_newline(cell, "Table Cell")?;
614 self.write_char(' ')?;
615 self.write(cell)?;
616 self.write_str(" |")?;
617 }
618 self.write_char('\n')?;
619 }
620
621 Ok(())
622 }
623
624 pub(crate) fn write_link(
626 &mut self,
627 url: &str,
628 title: &Option<String>,
629 content: &[Node],
630 ) -> WriteResult<()> {
631 for node in content {
632 self.check_no_newline(node, "Link Text")?;
633 }
634 self.write_char('[')?;
635
636 for node in content {
637 self.write(node)?;
638 }
639
640 self.write_str("](")?;
641 self.write_str(url)?;
642
643 if let Some(title_text) = title {
644 self.write_str(" \"")?;
645 self.write_str(title_text)?;
646 self.write_char('"')?;
647 }
648
649 self.write_char(')')?;
650 Ok(())
651 }
652
653 pub(crate) fn write_image(
655 &mut self,
656 url: &str,
657 title: &Option<String>,
658 alt: &[Node],
659 ) -> WriteResult<()> {
660 for node in alt {
662 self.check_no_newline(node, "Image alt text")?;
663 }
664
665 self.write_str("?;
673 self.write_str(url)?;
674
675 if let Some(title_text) = title {
676 self.write_str(" \"")?;
677 self.write_str(title_text)?;
678 self.write_char('"')?;
679 }
680
681 self.write_char(')')?;
682 Ok(())
683 }
684
685 pub(crate) fn write_soft_break(&mut self) -> WriteResult<()> {
687 self.write_char('\n')?;
688 Ok(())
689 }
690
691 pub(crate) fn write_hard_break(&mut self) -> WriteResult<()> {
693 if self.options.hard_break_spaces {
694 self.write_str(" \n")?;
695 } else {
696 self.write_str("\\\n")?;
697 }
698 Ok(())
699 }
700
701 pub(crate) fn write_html_block(&mut self, content: &str) -> WriteResult<()> {
703 self.buffer.push_str(content);
704
705 Ok(())
706 }
707
708 pub(crate) fn write_autolink(&mut self, url: &str, is_email: bool) -> WriteResult<()> {
710 if url.contains('\n') {
712 if self.is_strict_mode() {
713 return Err(WriteError::NewlineInInlineElement(
714 "Autolink URL".to_string(),
715 ));
716 } else {
717 log::warn!(
718 "Newline character found in autolink URL '{}'. Writing it as is, which might result in an invalid link. Strict mode is off.",
719 url
720 );
721 }
723 }
724
725 self.write_char('<')?;
727
728 if !is_email && !url.contains(':') {
731 self.write_str("https://")?;
733 }
734
735 self.write_str(url)?;
736 self.write_char('>')?;
737
738 Ok(())
739 }
740
741 #[cfg(feature = "gfm")]
743 pub(crate) fn write_extended_autolink(&mut self, url: &str) -> WriteResult<()> {
744 if !self.options.gfm_autolinks {
745 self.write_text_content(url)?;
747 return Ok(());
748 }
749
750 if url.contains('\n') {
752 if self.is_strict_mode() {
753 return Err(WriteError::NewlineInInlineElement(
755 "Extended Autolink URL".to_string(),
756 ));
757 } else {
758 log::warn!(
759 "Newline character found in extended autolink URL '{}'. Writing it as is, which might result in an invalid link. Strict mode is off.",
760 url
761 );
762 }
764 }
765
766 self.write_str(url)?;
768
769 Ok(())
770 }
771
772 pub(crate) fn write_link_reference_definition(
774 &mut self,
775 label: &str,
776 destination: &str,
777 title: &Option<String>,
778 ) -> WriteResult<()> {
779 self.write_char('[')?;
781 self.write_str(label)?;
782 self.write_str("]: ")?;
783 self.write_str(destination)?;
784
785 if let Some(title_text) = title {
786 self.write_str(" \"")?;
787 self.write_str(title_text)?;
788 self.write_char('"')?;
789 }
790
791 Ok(())
792 }
793
794 pub(crate) fn write_reference_link(
796 &mut self,
797 label: &str,
798 content: &[Node],
799 ) -> WriteResult<()> {
800 for node in content {
802 self.check_no_newline(node, "Reference Link Text")?;
803 }
804
805 if content.is_empty() {
808 self.write_char('[')?;
809 self.write_str(label)?;
810 self.write_char(']')?;
811 return Ok(());
812 }
813
814 let is_shortcut =
816 content.len() == 1 && matches!(&content[0], Node::Text(text) if text == label);
817
818 if is_shortcut {
819 self.write_char('[')?;
821 self.write_str(label)?;
822 self.write_char(']')?;
823 } else {
824 self.write_char('[')?;
826
827 for node in content {
828 self.write(node)?;
829 }
830
831 self.write_str("][")?;
832 self.write_str(label)?;
833 self.write_char(']')?;
834 }
835
836 Ok(())
837 }
838
839 pub(crate) fn write_html_element(&mut self, element: &HtmlElement) -> WriteResult<()> {
841 #[cfg(feature = "gfm")]
842 let mut html_render_options = html::HtmlRenderOptions::default();
843 #[cfg(not(feature = "gfm"))]
844 let html_render_options = html::HtmlRenderOptions::default();
845
846 #[cfg(feature = "gfm")]
847 {
848 html_render_options.enable_gfm = self.options.enable_gfm;
849 html_render_options.gfm_disallowed_html_tags =
850 self.options.gfm_disallowed_html_tags.clone();
851 }
852
853 let mut html_output_buffer = Vec::new();
856 let mut temp_html_writer =
857 html::HtmlWriter::new(std::io::Cursor::new(&mut html_output_buffer));
858 let node_to_render = Node::HtmlElement(element.clone());
859
860 temp_html_writer.write_node(&node_to_render, &html_render_options)?;
861 temp_html_writer
862 .flush()
863 .map_err(html::error::HtmlWriteError::Io)?;
864
865 let html_string =
866 String::from_utf8(html_output_buffer).map_err(|utf8_err| WriteError::Custom {
867 message: format!(
868 "HTML output from HtmlWriter was not valid UTF-8: {}",
869 utf8_err
870 ),
871 code: None,
872 })?;
873
874 self.buffer.push_str(&html_string);
875 Ok(())
876 }
877
878 pub fn into_string(self) -> String {
894 self.buffer
895 }
896 pub(crate) fn ensure_trailing_newline(&mut self) -> WriteResult<()> {
900 if !self.buffer.ends_with('\n') {
901 self.write_char('\n')?;
902 }
903 Ok(())
904 }
905
906 pub(crate) fn write_emphasis(&mut self, content: &[Node]) -> WriteResult<()> {
908 let delimiter = self.options.emphasis_char.to_string();
909 self.write_delimited(content, &delimiter)
910 }
911
912 pub(crate) fn write_strong(&mut self, content: &[Node]) -> WriteResult<()> {
914 let char = self.options.strong_char;
915 let delimiter = format!("{}{}", char, char);
916 self.write_delimited(content, &delimiter)
917 }
918
919 #[cfg(feature = "gfm")]
921 pub(crate) fn write_strikethrough(&mut self, content: &[Node]) -> WriteResult<()> {
922 if !self.options.enable_gfm || !self.options.gfm_strikethrough {
923 for node in content.iter() {
925 self.write(node)?;
926 }
927 return Ok(());
928 }
929
930 self.write_delimited(content, "~~")
932 }
933}
934
935impl Default for CommonMarkWriter {
936 fn default() -> Self {
937 Self::new()
938 }
939}
940
941impl fmt::Display for Node {
943 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
944 let mut writer = CommonMarkWriter::new();
945 match writer.write(self) {
946 Ok(_) => write!(f, "{}", writer.into_string()),
947 Err(e) => write!(f, "Error writing Node: {}", e),
948 }
949 }
950}
951
952impl CustomNodeWriter for CommonMarkWriter {
954 fn write_str(&mut self, s: &str) -> fmt::Result {
955 self.buffer.push_str(s);
956 Ok(())
957 }
958
959 fn write_char(&mut self, c: char) -> fmt::Result {
960 self.buffer.push(c);
961 Ok(())
962 }
963}