1#[cfg(feature = "gfm")]
6use crate::ast::TableAlignment;
7use crate::ast::{CodeBlockType, CustomNode, HeadingType, ListItem, Node};
8use crate::error::{WriteError, WriteResult};
9use crate::options::WriterOptions;
10use log;
11use std::fmt::{self};
12
13use super::processors::{
14 BlockNodeProcessor, CustomNodeProcessor, InlineNodeProcessor, NodeProcessor,
15};
16
17#[derive(Debug)]
21pub struct CommonMarkWriter {
22 options: WriterOptions,
23 buffer: String,
24}
25
26impl CommonMarkWriter {
27 pub fn new() -> Self {
40 Self::with_options(WriterOptions::default())
41 }
42
43 pub fn with_options(options: WriterOptions) -> Self {
64 Self {
65 options,
66 buffer: String::new(),
67 }
68 }
69
70 pub(crate) fn is_strict_mode(&self) -> bool {
72 self.options.strict
73 }
74
75 fn apply_prefix(&self, content: &str, prefix: &str, first_line_prefix: Option<&str>) -> String {
87 if content.is_empty() {
88 return String::new();
89 }
90
91 let mut result = String::new();
92 let lines: Vec<&str> = content.lines().collect();
93
94 if !lines.is_empty() {
95 let actual_prefix = first_line_prefix.unwrap_or(prefix);
96 result.push_str(actual_prefix);
97 result.push_str(lines[0]);
98 }
99
100 for line in &lines[1..] {
101 result.push('\n');
102 result.push_str(prefix);
103 result.push_str(line);
104 }
105
106 result
107 }
108
109 pub fn write(&mut self, node: &Node) -> WriteResult<()> {
129 if let Node::Custom(_) = node {
130 return CustomNodeProcessor.process(self, node);
131 }
132
133 if node.is_block() {
134 BlockNodeProcessor.process(self, node)
135 } else if node.is_inline() {
136 InlineNodeProcessor.process(self, node)
137 } else {
138 log::warn!("Unsupported node type encountered and skipped: {:?}", node);
139 Ok(())
140 }
141 }
142
143 #[allow(clippy::borrowed_box)]
145 pub(crate) fn write_custom_node(&mut self, node: &Box<dyn CustomNode>) -> WriteResult<()> {
146 node.write(self)
147 }
148
149 pub(crate) fn get_context_for_node(&self, node: &Node) -> String {
151 match node {
152 Node::Text(_) => "Text".to_string(),
153 Node::Emphasis(_) => "Emphasis".to_string(),
154 Node::Strong(_) => "Strong".to_string(),
155 #[cfg(feature = "gfm")]
156 Node::Strikethrough(_) => "Strikethrough".to_string(),
157 Node::InlineCode(_) => "InlineCode".to_string(),
158 Node::Link { .. } => "Link content".to_string(),
159 Node::Image { .. } => "Image alt text".to_string(),
160 Node::HtmlElement(_) => "HtmlElement content".to_string(),
161 Node::Custom(_) => "Custom node".to_string(),
162 _ => "Unknown inline element".to_string(),
163 }
164 }
165
166 pub(crate) fn check_no_newline(&self, node: &Node, context: &str) -> WriteResult<()> {
168 if Self::node_contains_newline(node) {
169 if self.is_strict_mode() {
170 return Err(WriteError::NewlineInInlineElement(context.to_string()));
171 } else {
172 log::warn!(
173 "Newline character found in inline element '{}', but non-strict mode allows it (output may be affected).",
174 context
175 );
176 }
177 }
178 Ok(())
179 }
180
181 fn node_contains_newline(node: &Node) -> bool {
183 match node {
184 Node::Text(s) | Node::InlineCode(s) => s.contains('\n'),
185 Node::Emphasis(children) | Node::Strong(children) => {
186 children.iter().any(Self::node_contains_newline)
187 }
188 #[cfg(feature = "gfm")]
189 Node::Strikethrough(children) => children.iter().any(Self::node_contains_newline),
190 Node::HtmlElement(element) => element.children.iter().any(Self::node_contains_newline),
191 Node::Link { content, .. } => content.iter().any(Self::node_contains_newline),
192 Node::Image { alt, .. } => alt.iter().any(Self::node_contains_newline),
193 Node::SoftBreak | Node::HardBreak => true,
194 Node::Custom(_) => false,
196 _ => false,
197 }
198 }
199
200 pub(crate) fn write_text_content(&mut self, content: &str) -> WriteResult<()> {
202 if self.options.escape_special_chars {
203 let escaped = content
204 .replace('\\', "\\\\")
205 .replace('*', "\\*")
206 .replace('_', "\\_")
207 .replace('[', "\\[")
208 .replace(']', "\\]")
209 .replace('<', "\\<")
210 .replace('>', "\\>")
211 .replace('`', "\\`");
212
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<String>,
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<String>,
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<String>,
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(),
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(),
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<String>,
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 {
842 if element.tag.contains('<') || element.tag.contains('>') {
844 return Err(WriteError::InvalidHtmlTag(element.tag.clone()));
845 }
846
847 for attr in &element.attributes {
849 if attr.name.contains('<') || attr.name.contains('>') {
850 return Err(WriteError::InvalidHtmlAttribute(attr.name.clone()));
851 }
852 }
853 }
854
855 use crate::writer::html::{HtmlWriter, HtmlWriterOptions};
856
857 let html_options = HtmlWriterOptions {
859 strict: self.options.strict,
861 code_block_language_class_prefix: Some("language-".to_string()),
863 #[cfg(feature = "gfm")]
864 enable_gfm: self.options.enable_gfm,
865 #[cfg(feature = "gfm")]
866 gfm_disallowed_html_tags: self.options.gfm_disallowed_html_tags.clone(),
867 };
868
869 let mut html_writer = HtmlWriter::with_options(html_options);
870
871 html_writer.write_node(&Node::HtmlElement(element.clone()))?;
872
873 let html_output = html_writer.into_string();
875
876 self.write_str(&html_output)
878 }
879
880 pub fn into_string(self) -> String {
896 self.buffer
897 }
898
899 pub fn write_str(&mut self, s: &str) -> WriteResult<()> {
903 self.buffer.push_str(s);
904 Ok(())
905 }
906
907 pub fn write_char(&mut self, c: char) -> WriteResult<()> {
911 self.buffer.push(c);
912 Ok(())
913 }
914 pub(crate) fn ensure_trailing_newline(&mut self) -> WriteResult<()> {
918 if !self.buffer.ends_with('\n') {
919 self.write_char('\n')?;
920 }
921 Ok(())
922 }
923
924 pub(crate) fn write_emphasis(&mut self, content: &[Node]) -> WriteResult<()> {
926 let delimiter = self.options.emphasis_char.to_string();
927 self.write_delimited(content, &delimiter)
928 }
929
930 pub(crate) fn write_strong(&mut self, content: &[Node]) -> WriteResult<()> {
932 let char = self.options.strong_char;
933 let delimiter = format!("{}{}", char, char);
934 self.write_delimited(content, &delimiter)
935 }
936
937 #[cfg(feature = "gfm")]
939 pub(crate) fn write_strikethrough(&mut self, content: &[Node]) -> WriteResult<()> {
940 if !self.options.enable_gfm || !self.options.gfm_strikethrough {
941 for node in content.iter() {
943 self.write(node)?;
944 }
945 return Ok(());
946 }
947
948 self.write_delimited(content, "~~")
950 }
951}
952
953impl Default for CommonMarkWriter {
954 fn default() -> Self {
955 Self::new()
956 }
957}
958
959impl fmt::Display for Node {
961 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
962 let mut writer = CommonMarkWriter::new();
963 match writer.write(self) {
964 Ok(_) => write!(f, "{}", writer.into_string()),
965 Err(e) => write!(f, "Error writing Node: {}", e),
966 }
967 }
968}
969
970