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 pub options: WriterOptions,
24 buffer: String,
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: String::new(),
69 }
70 }
71
72 pub(crate) fn is_strict_mode(&self) -> bool {
74 self.options.strict
75 }
76
77 fn apply_prefix(&self, content: &str, prefix: &str, first_line_prefix: Option<&str>) -> String {
89 if content.is_empty() {
90 return String::new();
91 }
92
93 let mut result = String::new();
94 let lines: Vec<&str> = content.lines().collect();
95
96 if !lines.is_empty() {
97 let actual_prefix = first_line_prefix.unwrap_or(prefix);
98 result.push_str(actual_prefix);
99 result.push_str(lines[0]);
100 }
101
102 for line in &lines[1..] {
103 result.push('\n');
104 result.push_str(prefix);
105 result.push_str(line);
106 }
107
108 result
109 }
110
111 pub fn write(&mut self, node: &Node) -> WriteResult<()> {
131 if let Node::Custom(_) = node {
132 return CustomNodeProcessor.process(self, node);
133 }
134
135 if node.is_block() {
136 BlockNodeProcessor.process(self, node)
137 } else if node.is_inline() {
138 InlineNodeProcessor.process(self, node)
139 } else {
140 log::warn!("Unsupported node type encountered and skipped: {:?}", node);
141 Ok(())
142 }
143 }
144
145 #[allow(clippy::borrowed_box)]
147 pub(crate) fn write_custom_node(&mut self, node: &Box<dyn CustomNode>) -> WriteResult<()> {
148 node.write(self)
149 }
150
151 pub(crate) fn get_context_for_node(&self, node: &Node) -> String {
153 match node {
154 Node::Text(_) => "Text".to_string(),
155 Node::Emphasis(_) => "Emphasis".to_string(),
156 Node::Strong(_) => "Strong".to_string(),
157 #[cfg(feature = "gfm")]
158 Node::Strikethrough(_) => "Strikethrough".to_string(),
159 Node::InlineCode(_) => "InlineCode".to_string(),
160 Node::Link { .. } => "Link content".to_string(),
161 Node::Image { .. } => "Image alt text".to_string(),
162 Node::HtmlElement(_) => "HtmlElement content".to_string(),
163 Node::Custom(_) => "Custom node".to_string(),
164 _ => "Unknown inline element".to_string(),
165 }
166 }
167
168 pub(crate) fn check_no_newline(&self, node: &Node, context: &str) -> WriteResult<()> {
170 if Self::node_contains_newline(node) {
171 if self.is_strict_mode() {
172 return Err(WriteError::NewlineInInlineElement(context.to_string()));
173 } else {
174 log::warn!(
175 "Newline character found in inline element '{}', but non-strict mode allows it (output may be affected).",
176 context
177 );
178 }
179 }
180 Ok(())
181 }
182
183 fn node_contains_newline(node: &Node) -> bool {
185 match node {
186 Node::Text(s) | Node::InlineCode(s) => s.contains('\n'),
187 Node::Emphasis(children) | Node::Strong(children) => {
188 children.iter().any(Self::node_contains_newline)
189 }
190 #[cfg(feature = "gfm")]
191 Node::Strikethrough(children) => children.iter().any(Self::node_contains_newline),
192 Node::HtmlElement(element) => element.children.iter().any(Self::node_contains_newline),
193 Node::Link { content, .. } => content.iter().any(Self::node_contains_newline),
194 Node::Image { alt, .. } => alt.iter().any(Self::node_contains_newline),
195 Node::SoftBreak | Node::HardBreak => true,
196 Node::Custom(_) => false,
198 _ => false,
199 }
200 }
201
202 pub(crate) fn write_text_content(&mut self, content: &str) -> WriteResult<()> {
204 if self.options.escape_special_chars {
205 let escaped = content
206 .replace('\\', "\\\\")
207 .replace('*', "\\*")
208 .replace('_', "\\_")
209 .replace('[', "\\[")
210 .replace(']', "\\]")
211 .replace('<', "\\<")
212 .replace('>', "\\>")
213 .replace('`', "\\`");
214
215 self.write_str(&escaped)?;
216 } else {
217 self.write_str(content)?;
218 }
219
220 Ok(())
221 }
222
223 pub(crate) fn write_code_content(&mut self, content: &str) -> WriteResult<()> {
225 self.write_char('`')?;
226 self.write_str(content)?;
227 self.write_char('`')?;
228 Ok(())
229 }
230
231 pub(crate) fn write_delimited(&mut self, content: &[Node], delimiter: &str) -> WriteResult<()> {
233 self.write_str(delimiter)?;
234
235 for node in content {
236 self.write(node)?;
237 }
238
239 self.write_str(delimiter)?;
240 Ok(())
241 }
242
243 pub(crate) fn write_document(&mut self, children: &[Node]) -> WriteResult<()> {
245 for (i, child) in children.iter().enumerate() {
246 if i > 0 {
247 self.write_str("\n")?;
248 }
249 self.write(child)?;
250 }
251 Ok(())
252 }
253
254 pub(crate) fn write_heading(
256 &mut self,
257 mut level: u8,
258 content: &[Node],
259 heading_type: &HeadingType,
260 ) -> WriteResult<()> {
261 if level == 0 || level > 6 {
263 if self.is_strict_mode() {
264 return Err(WriteError::InvalidHeadingLevel(level));
265 } else {
266 let original_level = level;
267 level = level.clamp(1, 6); log::warn!(
269 "Invalid heading level: {}. Corrected to {}. Strict mode is off.",
270 original_level,
271 level
272 );
273 }
274 }
275
276 match heading_type {
277 HeadingType::Atx => {
279 for _ in 0..level {
280 self.write_char('#')?;
281 }
282 self.write_char(' ')?;
283
284 for node in content {
285 self.write(node)?;
286 }
287
288 self.write_char('\n')?;
289 }
290
291 HeadingType::Setext => {
292 for node in content {
294 self.write(node)?;
295 }
296 self.write_char('\n')?;
297
298 let underline_char = if level == 1 { '=' } else { '-' };
301
302 let min_len = 3;
305
306 for _ in 0..min_len {
308 self.write_char(underline_char)?;
309 }
310
311 self.write_char('\n')?;
313 }
314 }
315
316 Ok(())
317 }
318
319 pub(crate) fn write_paragraph(&mut self, content: &[Node]) -> WriteResult<()> {
321 for node in content.iter() {
322 self.write(node)?;
323 }
324
325 Ok(())
326 }
327
328 pub(crate) fn write_blockquote(&mut self, content: &[Node]) -> WriteResult<()> {
330 let mut temp_writer = CommonMarkWriter::with_options(self.options.clone());
332
333 for (i, node) in content.iter().enumerate() {
335 if i > 0 {
336 temp_writer.write_char('\n')?;
337 }
338 temp_writer.write(node)?;
340 }
341
342 let all_content = temp_writer.into_string();
344
345 let prefix = "> ";
347 let formatted_content = self.apply_prefix(&all_content, prefix, Some(prefix));
348
349 self.buffer.push_str(&formatted_content);
351 Ok(())
352 }
353
354 pub(crate) fn write_thematic_break(&mut self) -> WriteResult<()> {
356 let char = self.options.thematic_break_char;
357 self.write_str(&format!("{}{}{}", char, char, char))?;
358 Ok(())
359 }
360
361 pub(crate) fn write_code_block(
363 &mut self,
364 language: &Option<String>,
365 content: &str,
366 block_type: &CodeBlockType,
367 ) -> WriteResult<()> {
368 match block_type {
369 CodeBlockType::Indented => {
370 let indent = " ";
371 let indented_content = self.apply_prefix(content, indent, Some(indent));
372 self.buffer.push_str(&indented_content);
373 }
374 CodeBlockType::Fenced => {
375 let max_backticks = content
376 .chars()
377 .fold((0, 0), |(max, current), c| {
378 if c == '`' {
379 (max.max(current + 1), current + 1)
380 } else {
381 (max, 0)
382 }
383 })
384 .0;
385
386 let fence_len = std::cmp::max(max_backticks + 1, 3);
387 let fence = "`".repeat(fence_len);
388
389 self.write_str(&fence)?;
390 if let Some(lang) = language {
391 self.write_str(lang)?;
392 }
393 self.write_char('\n')?;
394
395 self.buffer.push_str(content);
396 if !content.ends_with('\n') {
397 self.write_char('\n')?;
398 }
399
400 self.write_str(&fence)?;
401 }
402 }
403
404 Ok(())
405 }
406
407 pub(crate) fn write_unordered_list(&mut self, items: &[ListItem]) -> WriteResult<()> {
409 let list_marker = self.options.list_marker;
410 let prefix = format!("{} ", list_marker);
411
412 for (i, item) in items.iter().enumerate() {
413 if i > 0 {
414 self.write_char('\n')?;
415 }
416 self.write_list_item(item, &prefix)?;
417 }
418
419 Ok(())
420 }
421
422 pub(crate) fn write_ordered_list(&mut self, start: u32, items: &[ListItem]) -> WriteResult<()> {
424 let mut current_number = start;
426
427 for (i, item) in items.iter().enumerate() {
428 if i > 0 {
429 self.write_char('\n')?;
430 }
431
432 match item {
433 ListItem::Ordered { number, content: _ } => {
435 if let Some(custom_num) = number {
436 let prefix = format!("{}. ", custom_num);
438 self.write_list_item(item, &prefix)?;
439 current_number = custom_num + 1;
441 } else {
442 let prefix = format!("{}. ", current_number);
444 self.write_list_item(item, &prefix)?;
445 current_number += 1;
446 }
447 }
448 _ => {
450 let prefix = format!("{}. ", current_number);
451 self.write_list_item(item, &prefix)?;
452 current_number += 1;
453 }
454 }
455 }
456
457 Ok(())
458 }
459
460 fn write_list_item(&mut self, item: &ListItem, prefix: &str) -> WriteResult<()> {
462 match item {
463 ListItem::Unordered { content } => {
464 self.write_str(prefix)?;
465 self.write_list_item_content(content, prefix.len())?;
466 }
467 ListItem::Ordered { number, content } => {
468 if let Some(num) = number {
469 let custom_prefix = format!("{}. ", num);
470 self.write_str(&custom_prefix)?;
471 self.write_list_item_content(content, custom_prefix.len())?;
472 } else {
473 self.write_str(prefix)?;
474 self.write_list_item_content(content, prefix.len())?;
475 }
476 }
477 #[cfg(feature = "gfm")]
478 ListItem::Task { status, content } => {
479 if self.options.gfm_tasklists {
481 let checkbox = match status {
482 crate::ast::TaskListStatus::Checked => "[x] ",
483 crate::ast::TaskListStatus::Unchecked => "[ ] ",
484 };
485
486 let task_prefix = format!("{}{}", prefix, checkbox);
488 self.write_str(&task_prefix)?;
489 self.write_list_item_content(content, task_prefix.len())?;
490 } else {
491 self.write_str(prefix)?;
493 self.write_list_item_content(content, prefix.len())?;
494 }
495 }
496 }
497
498 Ok(())
499 }
500
501 fn write_list_item_content(&mut self, content: &[Node], prefix_len: usize) -> WriteResult<()> {
503 if content.is_empty() {
504 return Ok(());
505 }
506
507 let mut temp_writer = CommonMarkWriter::with_options(self.options.clone());
508
509 for (i, node) in content.iter().enumerate() {
510 if i > 0 {
511 temp_writer.write_char('\n')?;
512 }
513
514 temp_writer.write(node)?;
515 }
516
517 let all_content = temp_writer.into_string();
518
519 let indent = " ".repeat(prefix_len);
520
521 let formatted_content = self.apply_prefix(&all_content, &indent, Some(""));
522
523 self.buffer.push_str(&formatted_content);
524
525 Ok(())
526 }
527
528 pub(crate) fn write_table(&mut self, headers: &[Node], rows: &[Vec<Node>]) -> WriteResult<()> {
530 self.write_char('|')?;
532 for header in headers {
533 self.check_no_newline(header, "Table Header")?;
534 self.write_char(' ')?;
535 self.write(header)?;
536 self.write_str(" |")?;
537 }
538 self.write_char('\n')?;
539
540 self.write_char('|')?;
542 for _ in 0..headers.len() {
543 self.write_str(" --- |")?;
544 }
545 self.write_char('\n')?;
546
547 for row in rows {
549 self.write_char('|')?;
550 for cell in row {
551 self.check_no_newline(cell, "Table Cell")?;
552 self.write_char(' ')?;
553 self.write(cell)?;
554 self.write_str(" |")?;
555 }
556 self.write_char('\n')?;
557 }
558
559 Ok(())
560 }
561
562 #[cfg(feature = "gfm")]
563 pub(crate) fn write_table_with_alignment(
565 &mut self,
566 headers: &[Node],
567 alignments: &[TableAlignment],
568 rows: &[Vec<Node>],
569 ) -> WriteResult<()> {
570 if !self.options.gfm_tables {
572 return self.write_table(headers, rows);
573 }
574
575 self.write_char('|')?;
577 for header in headers {
578 self.check_no_newline(header, "Table Header")?;
579 self.write_char(' ')?;
580 self.write(header)?;
581 self.write_str(" |")?;
582 }
583 self.write_char('\n')?;
584
585 self.write_char('|')?;
588
589 for i in 0..headers.len() {
591 let alignment = if i < alignments.len() {
592 &alignments[i]
593 } else {
594 &TableAlignment::Center
595 };
596
597 match alignment {
598 TableAlignment::Left => self.write_str(" :--- |")?,
599 TableAlignment::Center => self.write_str(" :---: |")?,
600 TableAlignment::Right => self.write_str(" ---: |")?,
601 TableAlignment::None => self.write_str(" --- |")?,
602 }
603 }
604
605 self.write_char('\n')?;
606
607 for row in rows {
609 self.write_char('|')?;
610 for cell in row {
611 self.check_no_newline(cell, "Table Cell")?;
612 self.write_char(' ')?;
613 self.write(cell)?;
614 self.write_str(" |")?;
615 }
616 self.write_char('\n')?;
617 }
618
619 Ok(())
620 }
621
622 pub(crate) fn write_link(
624 &mut self,
625 url: &str,
626 title: &Option<String>,
627 content: &[Node],
628 ) -> WriteResult<()> {
629 for node in content {
630 self.check_no_newline(node, "Link Text")?;
631 }
632 self.write_char('[')?;
633
634 for node in content {
635 self.write(node)?;
636 }
637
638 self.write_str("](")?;
639 self.write_str(url)?;
640
641 if let Some(title_text) = title {
642 self.write_str(" \"")?;
643 self.write_str(title_text)?;
644 self.write_char('"')?;
645 }
646
647 self.write_char(')')?;
648 Ok(())
649 }
650
651 pub(crate) fn write_image(
653 &mut self,
654 url: &str,
655 title: &Option<String>,
656 alt: &[Node],
657 ) -> WriteResult<()> {
658 for node in alt {
660 self.check_no_newline(node, "Image alt text")?;
661 }
662
663 self.write_str("?;
671 self.write_str(url)?;
672
673 if let Some(title_text) = title {
674 self.write_str(" \"")?;
675 self.write_str(title_text)?;
676 self.write_char('"')?;
677 }
678
679 self.write_char(')')?;
680 Ok(())
681 }
682
683 pub(crate) fn write_soft_break(&mut self) -> WriteResult<()> {
685 self.write_char('\n')?;
686 Ok(())
687 }
688
689 pub(crate) fn write_hard_break(&mut self) -> WriteResult<()> {
691 if self.options.hard_break_spaces {
692 self.write_str(" \n")?;
693 } else {
694 self.write_str("\\\n")?;
695 }
696 Ok(())
697 }
698
699 pub(crate) fn write_html_block(&mut self, content: &str) -> WriteResult<()> {
701 self.buffer.push_str(content);
702
703 Ok(())
704 }
705
706 pub(crate) fn write_autolink(&mut self, url: &str, is_email: bool) -> WriteResult<()> {
708 if url.contains('\n') {
710 if self.is_strict_mode() {
711 return Err(WriteError::NewlineInInlineElement(
712 "Autolink URL".to_string(),
713 ));
714 } else {
715 log::warn!(
716 "Newline character found in autolink URL '{}'. Writing it as is, which might result in an invalid link. Strict mode is off.",
717 url
718 );
719 }
721 }
722
723 self.write_char('<')?;
725
726 if !is_email && !url.contains(':') {
729 self.write_str("https://")?;
731 }
732
733 self.write_str(url)?;
734 self.write_char('>')?;
735
736 Ok(())
737 }
738
739 #[cfg(feature = "gfm")]
741 pub(crate) fn write_extended_autolink(&mut self, url: &str) -> WriteResult<()> {
742 if !self.options.gfm_autolinks {
743 self.write_text_content(url)?;
745 return Ok(());
746 }
747
748 if url.contains('\n') {
750 if self.is_strict_mode() {
751 return Err(WriteError::NewlineInInlineElement(
753 "Extended Autolink URL".to_string(),
754 ));
755 } else {
756 log::warn!(
757 "Newline character found in extended autolink URL '{}'. Writing it as is, which might result in an invalid link. Strict mode is off.",
758 url
759 );
760 }
762 }
763
764 self.write_str(url)?;
766
767 Ok(())
768 }
769
770 pub(crate) fn write_link_reference_definition(
772 &mut self,
773 label: &str,
774 destination: &str,
775 title: &Option<String>,
776 ) -> WriteResult<()> {
777 self.write_char('[')?;
779 self.write_str(label)?;
780 self.write_str("]: ")?;
781 self.write_str(destination)?;
782
783 if let Some(title_text) = title {
784 self.write_str(" \"")?;
785 self.write_str(title_text)?;
786 self.write_char('"')?;
787 }
788
789 Ok(())
790 }
791
792 pub(crate) fn write_reference_link(
794 &mut self,
795 label: &str,
796 content: &[Node],
797 ) -> WriteResult<()> {
798 for node in content {
800 self.check_no_newline(node, "Reference Link Text")?;
801 }
802
803 if content.is_empty() {
806 self.write_char('[')?;
807 self.write_str(label)?;
808 self.write_char(']')?;
809 return Ok(());
810 }
811
812 let is_shortcut =
814 content.len() == 1 && matches!(&content[0], Node::Text(text) if text == label);
815
816 if is_shortcut {
817 self.write_char('[')?;
819 self.write_str(label)?;
820 self.write_char(']')?;
821 } else {
822 self.write_char('[')?;
824
825 for node in content {
826 self.write(node)?;
827 }
828
829 self.write_str("][")?;
830 self.write_str(label)?;
831 self.write_char(']')?;
832 }
833
834 Ok(())
835 }
836
837 pub(crate) fn write_html_element(
839 &mut self,
840 element: &crate::ast::HtmlElement,
841 ) -> WriteResult<()> {
842 if self.options.strict {
843 if element.tag.contains('<') || element.tag.contains('>') {
844 return Err(WriteError::InvalidHtmlTag(element.tag.clone()));
845 }
846
847 for attr in &element.attributes {
848 if attr.name.contains('<') || attr.name.contains('>') {
849 return Err(WriteError::InvalidHtmlAttribute(attr.name.clone()));
850 }
851 }
852 }
853
854 use crate::writer::html::{HtmlWriter, HtmlWriterOptions};
855
856 let html_options = HtmlWriterOptions {
858 strict: self.options.strict,
860 code_block_language_class_prefix: Some("language-".to_string()),
862 #[cfg(feature = "gfm")]
863 enable_gfm: self.options.enable_gfm,
864 #[cfg(feature = "gfm")]
865 gfm_disallowed_html_tags: self.options.gfm_disallowed_html_tags.clone(),
866 };
867
868 let mut html_writer = HtmlWriter::with_options(html_options);
869
870 html_writer.write_node(&Node::HtmlElement(element.clone()))?;
871
872 let html_output = html_writer.into_string();
874
875 self.write_str(&html_output)
877 }
878
879 pub fn into_string(self) -> String {
895 self.buffer
896 }
897
898 pub fn write_str(&mut self, s: &str) -> WriteResult<()> {
902 self.buffer.push_str(s);
903 Ok(())
904 }
905
906 pub fn write_char(&mut self, c: char) -> WriteResult<()> {
910 self.buffer.push(c);
911 Ok(())
912 }
913 pub(crate) fn ensure_trailing_newline(&mut self) -> WriteResult<()> {
917 if !self.buffer.ends_with('\n') {
918 self.write_char('\n')?;
919 }
920 Ok(())
921 }
922
923 pub(crate) fn write_emphasis(&mut self, content: &[Node]) -> WriteResult<()> {
925 let delimiter = self.options.emphasis_char.to_string();
926 self.write_delimited(content, &delimiter)
927 }
928
929 pub(crate) fn write_strong(&mut self, content: &[Node]) -> WriteResult<()> {
931 let char = self.options.strong_char;
932 let delimiter = format!("{}{}", char, char);
933 self.write_delimited(content, &delimiter)
934 }
935
936 #[cfg(feature = "gfm")]
938 pub(crate) fn write_strikethrough(&mut self, content: &[Node]) -> WriteResult<()> {
939 if !self.options.enable_gfm || !self.options.gfm_strikethrough {
940 for node in content.iter() {
942 self.write(node)?;
943 }
944 return Ok(());
945 }
946
947 self.write_delimited(content, "~~")
949 }
950}
951
952impl Default for CommonMarkWriter {
953 fn default() -> Self {
954 Self::new()
955 }
956}
957
958impl fmt::Display for Node {
960 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
961 let mut writer = CommonMarkWriter::new();
962 match writer.write(self) {
963 Ok(_) => write!(f, "{}", writer.into_string()),
964 Err(e) => write!(f, "Error writing Node: {}", e),
965 }
966 }
967}
968
969