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 std::fmt::{self};
13
14use super::processors::{
15 BlockNodeProcessor, CustomNodeProcessor, InlineNodeProcessor, NodeProcessor,
16};
17
18#[derive(Debug)]
22pub struct CommonMarkWriter {
23 options: WriterOptions,
24 buffer: String,
25}
26
27impl CommonMarkWriter {
28 pub fn new() -> Self {
41 Self::with_options(WriterOptions::default())
42 }
43
44 pub fn with_options(options: WriterOptions) -> Self {
65 Self {
66 options,
67 buffer: String::new(),
68 }
69 }
70
71 pub(crate) fn is_strict_mode(&self) -> bool {
73 self.options.strict
74 }
75
76 fn apply_prefix(&self, content: &str, prefix: &str, first_line_prefix: Option<&str>) -> String {
88 if content.is_empty() {
89 return String::new();
90 }
91
92 let mut result = String::new();
93 let lines: Vec<&str> = content.lines().collect();
94
95 if !lines.is_empty() {
96 let actual_prefix = first_line_prefix.unwrap_or(prefix);
97 result.push_str(actual_prefix);
98 result.push_str(lines[0]);
99 }
100
101 for line in &lines[1..] {
102 result.push('\n');
103 result.push_str(prefix);
104 result.push_str(line);
105 }
106
107 result
108 }
109
110 pub fn write(&mut self, node: &Node) -> WriteResult<()> {
130 if let Node::Custom(_) = node {
131 return CustomNodeProcessor.process(self, node);
132 }
133
134 if node.is_block() {
135 BlockNodeProcessor.process(self, node)
136 } else if node.is_inline() {
137 InlineNodeProcessor.process(self, node)
138 } else {
139 Err(WriteError::UnsupportedNodeType)
141 }
142 }
143
144 #[allow(clippy::borrowed_box)]
146 pub(crate) fn write_custom_node(&mut self, node: &Box<dyn CustomNode>) -> WriteResult<()> {
147 node.write(self)
148 }
149
150 pub(crate) fn get_context_for_node(&self, node: &Node) -> String {
152 match node {
153 Node::Text(_) => "Text".to_string(),
154 Node::Emphasis(_) => "Emphasis".to_string(),
155 Node::Strong(_) => "Strong".to_string(),
156 #[cfg(feature = "gfm")]
157 Node::Strikethrough(_) => "Strikethrough".to_string(),
158 Node::InlineCode(_) => "InlineCode".to_string(),
159 Node::Link { .. } => "Link content".to_string(),
160 Node::Image { .. } => "Image alt text".to_string(),
161 Node::HtmlElement(_) => "HtmlElement content".to_string(),
162 Node::Custom(_) => "Custom node".to_string(),
163 _ => "Unknown inline element".to_string(),
164 }
165 }
166
167 pub(crate) fn check_no_newline(&self, node: &Node, context: &str) -> WriteResult<()> {
169 if Self::node_contains_newline(node) {
170 return Err(WriteError::NewlineInInlineElement(context.to_string()));
171 }
172 Ok(())
173 }
174
175 fn node_contains_newline(node: &Node) -> bool {
177 match node {
178 Node::Text(s) | Node::InlineCode(s) => s.contains('\n'),
179 Node::Emphasis(children) | Node::Strong(children) => {
180 children.iter().any(Self::node_contains_newline)
181 }
182 #[cfg(feature = "gfm")]
183 Node::Strikethrough(children) => children.iter().any(Self::node_contains_newline),
184 Node::HtmlElement(element) => element.children.iter().any(Self::node_contains_newline),
185 Node::Link { content, .. } => content.iter().any(Self::node_contains_newline),
186 Node::Image { alt, .. } => alt.iter().any(Self::node_contains_newline),
187 Node::SoftBreak | Node::HardBreak => true,
188 Node::Custom(_) => false,
190 _ => false,
191 }
192 }
193
194 pub(crate) fn write_text_content(&mut self, content: &str) -> WriteResult<()> {
196 let escaped = content
197 .replace('\\', "\\\\")
198 .replace('*', "\\*")
199 .replace('_', "\\_")
200 .replace('[', "\\[")
201 .replace(']', "\\]")
202 .replace('<', "\\<")
203 .replace('>', "\\>")
204 .replace('`', "\\`");
205
206 self.write_str(&escaped)?;
207 Ok(())
208 }
209
210 pub(crate) fn write_code_content(&mut self, content: &str) -> WriteResult<()> {
212 self.write_char('`')?;
213 self.write_str(content)?;
214 self.write_char('`')?;
215 Ok(())
216 }
217
218 pub(crate) fn write_delimited(&mut self, content: &[Node], delimiter: &str) -> WriteResult<()> {
220 self.write_str(delimiter)?;
221
222 for node in content {
223 self.write(node)?;
224 }
225
226 self.write_str(delimiter)?;
227 Ok(())
228 }
229
230 pub(crate) fn write_document(&mut self, children: &[Node]) -> WriteResult<()> {
232 for (i, child) in children.iter().enumerate() {
233 if i > 0 {
234 self.write_str("\n")?;
235 }
236 self.write(child)?;
237 }
238 Ok(())
239 }
240
241 pub(crate) fn write_heading(
243 &mut self,
244 level: u8,
245 content: &[Node],
246 heading_type: &HeadingType,
247 ) -> WriteResult<()> {
248 if level == 0 || level > 6 {
250 return Err(WriteError::InvalidHeadingLevel(level));
251 }
252
253 match heading_type {
254 HeadingType::Atx => {
256 for _ in 0..level {
257 self.write_char('#')?;
258 }
259 self.write_char(' ')?;
260
261 for node in content {
262 self.write(node)?;
263 }
264
265 self.write_char('\n')?;
266 }
267
268 HeadingType::Setext => {
269 for node in content {
271 self.write(node)?;
272 }
273 self.write_char('\n')?;
274
275 let underline_char = if level == 1 { '=' } else { '-' };
278
279 let min_len = 3;
282
283 for _ in 0..min_len {
285 self.write_char(underline_char)?;
286 }
287
288 self.write_char('\n')?;
290 }
291 }
292
293 Ok(())
294 }
295
296 pub(crate) fn write_paragraph(&mut self, content: &[Node]) -> WriteResult<()> {
298 for node in content.iter() {
299 self.write(node)?;
300 }
301
302 Ok(())
303 }
304
305 pub(crate) fn write_blockquote(&mut self, content: &[Node]) -> WriteResult<()> {
307 let mut temp_writer = CommonMarkWriter::with_options(self.options.clone());
309
310 for (i, node) in content.iter().enumerate() {
312 if i > 0 {
313 temp_writer.write_char('\n')?;
314 }
315 temp_writer.write(node)?;
317 }
318
319 let all_content = temp_writer.into_string();
321
322 let prefix = "> ";
324 let formatted_content = self.apply_prefix(&all_content, prefix, Some(prefix));
325
326 self.buffer.push_str(&formatted_content);
328 Ok(())
329 }
330
331 pub(crate) fn write_thematic_break(&mut self) -> WriteResult<()> {
333 self.write_str("---")?;
334 Ok(())
335 }
336
337 pub(crate) fn write_code_block(
339 &mut self,
340 language: &Option<String>,
341 content: &str,
342 block_type: &CodeBlockType,
343 ) -> WriteResult<()> {
344 match block_type {
345 CodeBlockType::Indented => {
346 let indent = " ";
347 let indented_content = self.apply_prefix(content, indent, Some(indent));
348 self.buffer.push_str(&indented_content);
349 }
350 CodeBlockType::Fenced => {
351 let max_backticks = content
352 .chars()
353 .fold((0, 0), |(max, current), c| {
354 if c == '`' {
355 (max.max(current + 1), current + 1)
356 } else {
357 (max, 0)
358 }
359 })
360 .0;
361
362 let fence_len = std::cmp::max(max_backticks + 1, 3);
363 let fence = "`".repeat(fence_len);
364
365 self.write_str(&fence)?;
366 if let Some(lang) = language {
367 self.write_str(lang)?;
368 }
369 self.write_char('\n')?;
370
371 self.buffer.push_str(content);
372 if !content.ends_with('\n') {
373 self.write_char('\n')?;
374 }
375
376 self.write_str(&fence)?;
377 }
378 }
379
380 Ok(())
381 }
382
383 pub(crate) fn write_unordered_list(&mut self, items: &[ListItem]) -> WriteResult<()> {
385 for (i, item) in items.iter().enumerate() {
386 if i > 0 {
387 self.write_char('\n')?;
388 }
389 self.write_list_item(item, "- ")?;
390 }
391
392 Ok(())
393 }
394
395 pub(crate) fn write_ordered_list(&mut self, start: u32, items: &[ListItem]) -> WriteResult<()> {
397 let mut current_number = start;
399
400 for (i, item) in items.iter().enumerate() {
401 if i > 0 {
402 self.write_char('\n')?;
403 }
404
405 match item {
406 ListItem::Ordered { number, content: _ } => {
408 if let Some(custom_num) = number {
409 let prefix = format!("{}. ", custom_num);
411 self.write_list_item(item, &prefix)?;
412 current_number = custom_num + 1;
414 } else {
415 let prefix = format!("{}. ", current_number);
417 self.write_list_item(item, &prefix)?;
418 current_number += 1;
419 }
420 }
421 _ => {
423 let prefix = format!("{}. ", current_number);
424 self.write_list_item(item, &prefix)?;
425 current_number += 1;
426 }
427 }
428 }
429
430 Ok(())
431 }
432
433 fn write_list_item(&mut self, item: &ListItem, prefix: &str) -> WriteResult<()> {
435 match item {
436 ListItem::Unordered { content } => {
437 self.write_str(prefix)?;
438 self.write_list_item_content(content, prefix.len())?;
439 }
440 ListItem::Ordered { number, content } => {
441 if let Some(num) = number {
442 let custom_prefix = format!("{}. ", num);
443 self.write_str(&custom_prefix)?;
444 self.write_list_item_content(content, custom_prefix.len())?;
445 } else {
446 self.write_str(prefix)?;
447 self.write_list_item_content(content, prefix.len())?;
448 }
449 }
450 #[cfg(feature = "gfm")]
451 ListItem::Task { status, content } => {
452 if self.options.gfm_tasklists {
454 let checkbox = match status {
455 crate::ast::TaskListStatus::Checked => "[x] ",
456 crate::ast::TaskListStatus::Unchecked => "[ ] ",
457 };
458
459 let task_prefix = format!("{}{}", prefix, checkbox);
461 self.write_str(&task_prefix)?;
462 self.write_list_item_content(content, task_prefix.len())?;
463 } else {
464 self.write_str(prefix)?;
466 self.write_list_item_content(content, prefix.len())?;
467 }
468 }
469 }
470
471 Ok(())
472 }
473
474 fn write_list_item_content(&mut self, content: &[Node], prefix_len: usize) -> WriteResult<()> {
476 if content.is_empty() {
477 return Ok(());
478 }
479
480 let mut temp_writer = CommonMarkWriter::with_options(self.options.clone());
481
482 for (i, node) in content.iter().enumerate() {
483 if i > 0 {
484 temp_writer.write_char('\n')?;
485 }
486
487 temp_writer.write(node)?;
488 }
489
490 let all_content = temp_writer.into_string();
491
492 let indent = " ".repeat(prefix_len);
493
494 let formatted_content = self.apply_prefix(&all_content, &indent, Some(""));
495
496 self.buffer.push_str(&formatted_content);
497
498 Ok(())
499 }
500
501 pub(crate) fn write_table(&mut self, headers: &[Node], rows: &[Vec<Node>]) -> WriteResult<()> {
503 self.write_char('|')?;
505 for header in headers {
506 self.check_no_newline(header, "Table Header")?;
507 self.write_char(' ')?;
508 self.write(header)?;
509 self.write_str(" |")?;
510 }
511 self.write_char('\n')?;
512
513 self.write_char('|')?;
515 for _ in 0..headers.len() {
516 self.write_str(" --- |")?;
517 }
518 self.write_char('\n')?;
519
520 for row in rows {
522 self.write_char('|')?;
523 for cell in row {
524 self.check_no_newline(cell, "Table Cell")?;
525 self.write_char(' ')?;
526 self.write(cell)?;
527 self.write_str(" |")?;
528 }
529 self.write_char('\n')?;
530 }
531
532 Ok(())
533 }
534
535 #[cfg(feature = "gfm")]
536 pub(crate) fn write_table_with_alignment(
538 &mut self,
539 headers: &[Node],
540 alignments: &[TableAlignment],
541 rows: &[Vec<Node>],
542 ) -> WriteResult<()> {
543 if !self.options.gfm_tables {
545 return self.write_table(headers, rows);
546 }
547
548 self.write_char('|')?;
550 for header in headers {
551 self.check_no_newline(header, "Table Header")?;
552 self.write_char(' ')?;
553 self.write(header)?;
554 self.write_str(" |")?;
555 }
556 self.write_char('\n')?;
557
558 self.write_char('|')?;
560
561 for i in 0..headers.len() {
563 let alignment = if i < alignments.len() {
564 &alignments[i]
565 } else {
566 &TableAlignment::Center
567 };
568
569 match alignment {
570 TableAlignment::Left => self.write_str(" :--- |")?,
571 TableAlignment::Center => self.write_str(" :---: |")?,
572 TableAlignment::Right => self.write_str(" ---: |")?,
573 TableAlignment::None => self.write_str(" --- |")?,
574 }
575 }
576
577 self.write_char('\n')?;
578
579 for row in rows {
581 self.write_char('|')?;
582 for cell in row {
583 self.check_no_newline(cell, "Table Cell")?;
584 self.write_char(' ')?;
585 self.write(cell)?;
586 self.write_str(" |")?;
587 }
588 self.write_char('\n')?;
589 }
590
591 Ok(())
592 }
593
594 pub(crate) fn write_link(
596 &mut self,
597 url: &str,
598 title: &Option<String>,
599 content: &[Node],
600 ) -> WriteResult<()> {
601 for node in content {
602 self.check_no_newline(node, "Link Text")?;
603 }
604 self.write_char('[')?;
605
606 for node in content {
607 self.write(node)?;
608 }
609
610 self.write_str("](")?;
611 self.write_str(url)?;
612
613 if let Some(title_text) = title {
614 self.write_str(" \"")?;
615 self.write_str(title_text)?;
616 self.write_char('"')?;
617 }
618
619 self.write_char(')')?;
620 Ok(())
621 }
622
623 pub(crate) fn write_image(
625 &mut self,
626 url: &str,
627 title: &Option<String>,
628 alt: &[Node],
629 ) -> WriteResult<()> {
630 for node in alt {
632 self.check_no_newline(node, "Image alt text")?;
633 }
634
635 self.write_str("?;
643 self.write_str(url)?;
644
645 if let Some(title_text) = title {
646 self.write_str(" \"")?;
647 self.write_str(title_text)?;
648 self.write_char('"')?;
649 }
650
651 self.write_char(')')?;
652 Ok(())
653 }
654
655 pub(crate) fn write_soft_break(&mut self) -> WriteResult<()> {
657 self.write_char('\n')?;
658 Ok(())
659 }
660
661 pub(crate) fn write_hard_break(&mut self) -> WriteResult<()> {
663 if self.options.hard_break_spaces {
664 self.write_str(" \n")?;
665 } else {
666 self.write_str("\\\n")?;
667 }
668 Ok(())
669 }
670
671 pub(crate) fn write_html_block(&mut self, content: &str) -> WriteResult<()> {
673 self.buffer.push_str(content);
674
675 Ok(())
676 }
677
678 pub(crate) fn write_autolink(&mut self, url: &str, is_email: bool) -> WriteResult<()> {
680 if url.contains('\n') {
682 return Err(WriteError::NewlineInInlineElement(
683 "Autolink URL".to_string(),
684 ));
685 }
686
687 self.write_char('<')?;
689
690 if !is_email && !url.contains(':') {
693 self.write_str("https://")?;
695 }
696
697 self.write_str(url)?;
698 self.write_char('>')?;
699
700 Ok(())
701 }
702
703 #[cfg(feature = "gfm")]
705 pub(crate) fn write_extended_autolink(&mut self, url: &str) -> WriteResult<()> {
706 if !self.options.gfm_autolinks {
707 self.write_text_content(url)?;
709 return Ok(());
710 }
711
712 if url.contains('\n') {
714 return Err(WriteError::NewlineInInlineElement(
715 "Extended Autolink URL".to_string(),
716 ));
717 }
718
719 self.write_str(url)?;
721
722 Ok(())
723 }
724
725 pub(crate) fn write_link_reference_definition(
727 &mut self,
728 label: &str,
729 destination: &str,
730 title: &Option<String>,
731 ) -> WriteResult<()> {
732 self.write_char('[')?;
734 self.write_str(label)?;
735 self.write_str("]: ")?;
736 self.write_str(destination)?;
737
738 if let Some(title_text) = title {
739 self.write_str(" \"")?;
740 self.write_str(title_text)?;
741 self.write_char('"')?;
742 }
743
744 Ok(())
745 }
746
747 pub(crate) fn write_reference_link(
749 &mut self,
750 label: &str,
751 content: &[Node],
752 ) -> WriteResult<()> {
753 for node in content {
755 self.check_no_newline(node, "Reference Link Text")?;
756 }
757
758 if content.is_empty() {
761 self.write_char('[')?;
762 self.write_str(label)?;
763 self.write_char(']')?;
764 return Ok(());
765 }
766
767 let is_shortcut =
769 content.len() == 1 && matches!(&content[0], Node::Text(text) if text == label);
770
771 if is_shortcut {
772 self.write_char('[')?;
774 self.write_str(label)?;
775 self.write_char(']')?;
776 } else {
777 self.write_char('[')?;
779
780 for node in content {
781 self.write(node)?;
782 }
783
784 self.write_str("][")?;
785 self.write_str(label)?;
786 self.write_char(']')?;
787 }
788
789 Ok(())
790 }
791
792 pub(crate) fn write_html_element(&mut self, element: &HtmlElement) -> WriteResult<()> {
794 #[cfg(feature = "gfm")]
796 if self.options.enable_gfm
797 && self
798 .options
799 .gfm_disallowed_html_tags
800 .iter()
801 .any(|tag| tag.eq_ignore_ascii_case(&element.tag))
802 {
803 self.write_str("<")?;
805 self.write_str(&element.tag)?;
806
807 for attr in &element.attributes {
808 self.write_char(' ')?;
809 self.write_str(&attr.name)?;
810 self.write_str("=\"")?;
811 self.write_str(&escape_attribute_value(&attr.value))?;
812 self.write_char('"')?;
813 }
814
815 if element.self_closing {
816 self.write_str(" />")?;
817 return Ok(());
818 }
819
820 self.write_str(">")?;
821
822 for child in &element.children {
823 self.write(child)?;
824 }
825
826 self.write_str("</")?;
827 self.write_str(&element.tag)?;
828 self.write_str(">")?;
829 return Ok(());
830 }
831
832 if !is_safe_tag_name(&element.tag) {
834 return Err(WriteError::InvalidHtmlTag(element.tag.clone()));
835 }
836
837 self.write_char('<')?;
839 self.write_str(&element.tag)?;
840
841 for attr in &element.attributes {
842 if !is_safe_attribute_name(&attr.name) {
844 return Err(WriteError::InvalidHtmlAttribute(attr.name.clone()));
845 }
846
847 self.write_char(' ')?;
848 self.write_str(&attr.name)?;
849 self.write_str("=\"")?;
850 self.write_str(&escape_attribute_value(&attr.value))?;
851 self.write_char('"')?;
852 }
853
854 if element.self_closing {
855 self.write_str(" />")?;
856 return Ok(());
857 }
858
859 self.write_char('>')?;
860
861 for child in &element.children {
862 self.write(child)?;
864 }
865
866 self.write_str("</")?;
867 self.write_str(&element.tag)?;
868 self.write_char('>')?;
869 Ok(())
870 }
871
872 pub fn into_string(self) -> String {
888 self.buffer
889 }
890 pub(crate) fn ensure_trailing_newline(&mut self) -> WriteResult<()> {
894 if !self.buffer.ends_with('\n') {
895 self.write_char('\n')?;
896 }
897 Ok(())
898 }
899
900 #[cfg(feature = "gfm")]
902 pub(crate) fn write_strikethrough(&mut self, content: &[Node]) -> WriteResult<()> {
903 if !self.options.enable_gfm || !self.options.gfm_strikethrough {
904 for node in content.iter() {
906 self.write(node)?;
907 }
908 return Ok(());
909 }
910
911 self.write_delimited(content, "~~")
913 }
914}
915
916impl Default for CommonMarkWriter {
917 fn default() -> Self {
918 Self::new()
919 }
920}
921
922impl fmt::Display for Node {
924 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
925 let mut writer = CommonMarkWriter::new();
926 match writer.write(self) {
927 Ok(_) => write!(f, "{}", writer.into_string()),
928 Err(e) => write!(f, "Error writing Node: {}", e),
929 }
930 }
931}
932
933impl CustomNodeWriter for CommonMarkWriter {
935 fn write_str(&mut self, s: &str) -> fmt::Result {
936 self.buffer.push_str(s);
937 Ok(())
938 }
939
940 fn write_char(&mut self, c: char) -> fmt::Result {
941 self.buffer.push(c);
942 Ok(())
943 }
944}
945fn is_safe_tag_name(tag: &str) -> bool {
951 !tag.is_empty()
952 && tag
953 .chars()
954 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == ':' || c == '-')
955}
956
957fn is_safe_attribute_name(name: &str) -> bool {
961 !name.is_empty()
962 && name
963 .chars()
964 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == ':' || c == '-' || c == '.')
965}
966
967fn escape_attribute_value(value: &str) -> String {
971 value
972 .replace('&', "&")
973 .replace('"', """)
974 .replace('\'', "'")
975 .replace('<', "<")
976 .replace('>', ">")
977}