1use pulldown_cmark::MetadataBlockKind;
2
3use super::*;
4
5pub struct MarkdownFormatter {
11 code_block_formatter: CodeBlockFormatter,
12 config: Config,
13}
14
15impl MarkdownFormatter {
16 pub fn format(self, input: &str) -> Result<String, std::fmt::Error> {
27 self.format_with_paragraph_and_html_block_formatter::<Paragraph, PreservingHtmlBlock>(input)
28 }
29
30 pub fn format_with_paragraph_and_html_block_formatter<P, H>(
44 self,
45 input: &str,
46 ) -> Result<String, std::fmt::Error>
47 where
48 P: ParagraphFormatter,
49 H: ParagraphFormatter,
50 {
51 let mut callback = |broken_link| {
53 tracing::trace!("found boken link: {broken_link:?}");
54 Some(("".into(), "".into()))
55 };
56
57 let mut options = Options::all();
58 options.remove(Options::ENABLE_SMART_PUNCTUATION);
59
60 let parser = Parser::new_with_broken_link_callback(input, options, Some(&mut callback));
61
62 let is_false_title = |input: &str, span: Range<usize>| {
70 input[span.end..]
71 .chars()
72 .take_while(|c| *c != '\n')
73 .any(|c| !c.is_whitespace())
74 };
75
76 let reference_links = parser
77 .reference_definitions()
78 .iter()
79 .sorted_by(|(_, link_a), (_, link_b)| {
80 link_b.span.start.cmp(&link_a.span.start)
83 })
84 .map(|(link_lable, link_def)| {
86 let (dest, title, span) = (&link_def.dest, &link_def.title, &link_def.span);
87 let full_link = &input[span.clone()];
88 if title.is_some() && is_false_title(input, span.clone()) {
89 let end = input[span.clone()]
90 .find(dest.as_ref())
91 .map(|idx| idx + dest.len())
92 .unwrap_or(span.end);
93 return (
94 link_lable.to_string(),
95 dest.to_string(),
96 None,
97 span.start..end,
98 );
99 }
100
101 if let Some((url, title)) = links::recover_escaped_link_destination_and_title(
102 full_link,
103 link_lable,
104 title.is_some(),
105 ) {
106 (link_lable.to_string(), url, title, span.clone())
107 } else {
108 (
110 link_lable.to_string(),
111 dest.to_string(),
112 title.clone().map(|s| (s.to_string(), '"')),
113 span.clone(),
114 )
115 }
116 })
117 .collect::<Vec<_>>();
118
119 let iter = parser.into_offset_iter().all_loose_lists();
120
121 let fmt_state = <FormatState<_, _, P, H>>::new_with_paragraph_and_html_block_formatter(
122 input,
123 self.config,
124 self.code_block_formatter,
125 iter,
126 reference_links,
127 );
128 fmt_state.format()
129 }
130
131 pub(crate) fn new(code_block_formatter: CodeBlockFormatter, config: Config) -> Self {
138 Self {
139 code_block_formatter,
140 config,
141 }
142 }
143}
144
145type ReferenceLinkDefinition = (String, String, Option<(String, char)>, Range<usize>);
146
147pub(crate) struct FormatState<'i, F, I, P, H>
148where
149 I: Iterator,
150{
151 input: &'i str,
153 pub(crate) last_was_softbreak: bool,
154 events: Peekable<I>,
156 rewrite_buffer: String,
157 code_block_buffer: String,
159 indentation: Vec<Cow<'static, str>>,
166 nested_context: Vec<Tag<'i>>,
168 reference_links: Vec<ReferenceLinkDefinition>,
177 setext_header: Option<&'i str>,
183 header_id_and_classes: Option<(Option<CowStr<'i>>, Vec<CowStr<'i>>)>,
185 needs_indent: bool,
187 table_state: Option<TableState<'i>>,
188 last_position: usize,
189 code_block_formatter: F,
190 trim_link_or_image_start: bool,
191 paragraph: Option<P>,
193 html_block: Option<H>,
195 force_rewrite_buffer: bool,
197 config: Config,
199}
200
201impl<'i, F, I, P, H> Write for FormatState<'i, F, I, P, H>
204where
205 I: Iterator<Item = (Event<'i>, std::ops::Range<usize>)>,
206 P: ParagraphFormatter,
207 H: ParagraphFormatter,
208{
209 fn write_str(&mut self, text: &str) -> std::fmt::Result {
210 if let Some(writer) = self.current_buffer() {
211 tracing::trace!(text, "write_str");
212 writer.write_str(text)?
213 }
214 Ok(())
215 }
216
217 fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::fmt::Result {
218 if let Some(writer) = self.current_buffer() {
219 writer.write_fmt(args)?
220 }
221 Ok(())
222 }
223}
224
225impl<'i, F, I, P, H> FormatState<'i, F, I, P, H>
226where
227 I: Iterator<Item = (Event<'i>, std::ops::Range<usize>)>,
228 P: ParagraphFormatter,
229 H: ParagraphFormatter,
230{
231 fn peek(&mut self) -> Option<&Event<'i>> {
233 self.events.peek().map(|(e, _)| e)
234 }
235
236 fn peek_with_range(&mut self) -> Option<(&Event, &Range<usize>)> {
238 self.events.peek().map(|(e, r)| (e, r))
239 }
240
241 fn is_next_end_event(&mut self) -> bool {
243 matches!(self.peek(), Some(Event::End(_)))
244 }
245
246 fn check_needs_indent(&mut self, event: &Event<'i>) {
248 self.needs_indent = match self.peek() {
249 Some(Event::Start(_) | Event::Rule | Event::Html(_) | Event::End(TagEnd::Item)) => true,
250 Some(Event::End(TagEnd::BlockQuote)) => matches!(event, Event::End(_)),
251 Some(Event::Text(_)) => matches!(event, Event::End(_) | Event::Start(Tag::Item)),
252 _ => matches!(event, Event::Rule),
253 };
254 }
255
256 fn in_fenced_code_block(&self) -> bool {
258 matches!(
259 self.nested_context.last(),
260 Some(Tag::CodeBlock(CodeBlockKind::Fenced(_)))
261 )
262 }
263
264 fn in_indented_code_block(&self) -> bool {
266 matches!(
267 self.nested_context.last(),
268 Some(Tag::CodeBlock(CodeBlockKind::Indented))
269 )
270 }
271
272 fn in_html_block(&self) -> bool {
274 self.html_block.is_some()
275 }
276
277 fn in_table_header(&self) -> bool {
279 self.nested_context
280 .iter()
281 .rfind(|tag| **tag == Tag::TableHead)
282 .is_some()
283 }
284
285 fn in_table_row(&self) -> bool {
287 self.nested_context
288 .iter()
289 .rfind(|tag| **tag == Tag::TableRow)
290 .is_some()
291 }
292
293 fn in_link_or_image(&self) -> bool {
295 matches!(
296 self.nested_context.last(),
297 Some(Tag::Link { .. } | Tag::Image { .. })
298 )
299 }
300
301 fn in_paragraph(&self) -> bool {
304 self.paragraph.is_some()
305 }
306
307 fn is_nested(&self) -> bool {
309 !self.nested_context.is_empty()
310 }
311
312 fn indentation_len(&self) -> usize {
314 self.indentation.iter().map(|i| i.len()).sum()
315 }
316
317 fn current_buffer(&mut self) -> Option<&mut dyn std::fmt::Write> {
321 if self.force_rewrite_buffer {
322 tracing::trace!("rewrite_buffer");
323 Some(&mut self.rewrite_buffer)
324 } else if self.in_fenced_code_block() || self.in_indented_code_block() {
325 tracing::trace!("code_block_buffer");
326 Some(&mut self.code_block_buffer)
327 } else if self.in_html_block() {
328 tracing::trace!("html_block");
329 self.html_block
330 .as_mut()
331 .map(|h| h as &mut dyn std::fmt::Write)
332 } else if self.in_table_header() || self.in_table_row() {
333 tracing::trace!("table_state");
334 self.table_state
335 .as_mut()
336 .map(|s| s as &mut dyn std::fmt::Write)
337 } else if self.in_paragraph() {
338 tracing::trace!("paragraph");
339 self.paragraph
340 .as_mut()
341 .map(|p| p as &mut dyn std::fmt::Write)
342 } else {
343 tracing::trace!("rewrite_buffer");
344 Some(&mut self.rewrite_buffer)
345 }
346 }
347
348 fn is_current_buffer_empty(&self) -> bool {
350 if self.in_fenced_code_block() || self.in_indented_code_block() {
351 self.code_block_buffer.is_empty()
352 } else if self.in_html_block() {
353 self.html_block.as_ref().is_some_and(|h| h.is_empty())
354 } else if self.in_table_header() || self.in_table_row() {
355 self.table_state.as_ref().is_some_and(|s| s.is_empty())
356 } else if self.in_paragraph() {
357 self.paragraph.as_ref().is_some_and(|p| p.is_empty())
358 } else {
359 self.rewrite_buffer.is_empty()
360 }
361 }
362
363 fn count_newlines(&self, range: &Range<usize>) -> usize {
364 if self.last_position == range.start {
365 return 0;
366 }
367
368 let snippet = if self.last_position < range.start {
369 &self.input[self.last_position..range.start]
371 } else {
372 self.input[self.last_position..range.end].trim_end_matches('\n')
374 };
375
376 snippet.bytes().filter(|b| *b == b'\n').count()
377 }
378
379 fn write_indentation(&mut self, trim_trailing_whiltespace: bool) -> std::fmt::Result {
380 let last_non_complete_whitespace_indent = self
381 .indentation
382 .iter()
383 .rposition(|indent| !indent.chars().all(char::is_whitespace));
384
385 let position = if trim_trailing_whiltespace {
386 let Some(position) = last_non_complete_whitespace_indent else {
387 return Ok(());
389 };
390 position
391 } else {
392 self.indentation.len()
393 };
394
395 let indentation = std::mem::take(&mut self.indentation);
397
398 for (i, indent) in indentation.iter().take(position + 1).enumerate() {
399 let is_last = i == position;
400
401 if is_last && trim_trailing_whiltespace {
402 self.write_str(indent.trim())?;
403 } else {
404 self.write_str(indent)?;
405 }
406 }
407 self.indentation = indentation;
409 Ok(())
410 }
411
412 fn write_newlines(&mut self, max_newlines: usize) -> std::fmt::Result {
413 self.write_newlines_inner(max_newlines, false)
414 }
415
416 fn write_newlines_no_trailing_whitespace(&mut self, max_newlines: usize) -> std::fmt::Result {
417 self.write_newlines_inner(max_newlines, true)
418 }
419
420 fn write_newlines_inner(
421 &mut self,
422 max_newlines: usize,
423 always_trim_trailing_whitespace: bool,
424 ) -> std::fmt::Result {
425 if self.is_current_buffer_empty() {
426 return Ok(());
427 }
428 let newlines = self
429 .rewrite_buffer
430 .chars()
431 .rev()
432 .take_while(|c| *c == '\n')
433 .count();
434
435 let nested = self.is_nested();
436 let newlines_to_write = max_newlines.saturating_sub(newlines);
437 let next_is_end_event = self.is_next_end_event();
438 tracing::trace!(newlines, nested, newlines_to_write, next_is_end_event);
439
440 for i in 0..newlines_to_write {
441 let is_last = i == newlines_to_write - 1;
442
443 writeln!(self)?;
444
445 if nested {
446 self.write_indentation(!is_last || always_trim_trailing_whitespace)?;
447 }
448 }
449 if !nested {
450 self.write_indentation(next_is_end_event || always_trim_trailing_whitespace)?;
451 }
452 Ok(())
453 }
454
455 fn write_reference_link_definition_inner(
456 &mut self,
457 label: &str,
458 dest: &str,
459 title: Option<&(String, char)>,
460 ) -> std::fmt::Result {
461 let dest = links::format_link_url(dest, true);
463 self.write_newlines(1)?;
464 if let Some((title, quote)) = title {
465 write!(self, r#"[{}]: {dest} {quote}{title}{quote}"#, label.trim())?;
466 } else {
467 write!(self, "[{}]: {dest}", label.trim())?;
468 }
469 Ok(())
470 }
471
472 fn rewrite_reference_link_definitions(&mut self, range: &Range<usize>) -> std::fmt::Result {
473 if self.reference_links.is_empty() {
474 return Ok(());
475 }
476 let mut reference_links = std::mem::take(&mut self.reference_links);
478
479 loop {
480 match reference_links.last() {
481 Some((_, _, _, link_range)) if link_range.start > range.start => {
482 break;
484 }
485 None => break,
486 _ => {}
487 }
488
489 let (label, dest, title, link_range) = reference_links.pop().expect("we have a value");
490 let newlines = self.count_newlines(&link_range);
491 self.write_newlines(newlines)?;
492 self.write_reference_link_definition_inner(&label, &dest, title.as_ref())?;
493 self.last_position = link_range.end;
494 self.needs_indent = true;
495 }
496
497 self.reference_links = reference_links;
499 Ok(())
500 }
501
502 fn rewrite_final_reference_links(mut self) -> Result<String, std::fmt::Error> {
504 let reference_links = std::mem::take(&mut self.reference_links);
506
507 for (label, dest, title, range) in reference_links.into_iter().rev() {
509 let newlines = self.count_newlines(&range);
510 self.write_newlines(newlines)?;
511
512 self.write_reference_link_definition_inner(&label, &dest, title.as_ref())?;
514 self.last_position = range.end
515 }
516 Ok(self.rewrite_buffer)
517 }
518
519 fn join_with_indentation(
520 &mut self,
521 buffer: &str,
522 start_with_indentation: bool,
523 ) -> std::fmt::Result {
524 self.force_rewrite_buffer = true;
525 let mut lines = buffer.trim_end().lines().peekable();
526 while let Some(line) = lines.next() {
527 let is_last = lines.peek().is_none();
528 let is_next_empty = lines
529 .peek()
530 .map(|l| l.trim().is_empty())
531 .unwrap_or_default();
532
533 if start_with_indentation {
534 self.write_indentation(line.trim().is_empty())?;
535 }
536
537 if !line.trim().is_empty() {
538 self.write_str(line)?;
539 }
540
541 if !is_last {
542 writeln!(self)?;
543 }
544
545 if !is_last && !start_with_indentation {
546 self.write_indentation(is_next_empty)?;
547 }
548 }
549 self.force_rewrite_buffer = false;
550 Ok(())
551 }
552}
553
554impl<'i, F, I, P, H> FormatState<'i, F, I, P, H>
555where
556 I: Iterator<Item = (Event<'i>, std::ops::Range<usize>)>,
557 P: ParagraphFormatter,
558{
559}
560
561#[allow(dead_code)] impl<'i, F, I> FormatState<'i, F, I, Paragraph, PreservingHtmlBlock>
563where
564 F: Fn(&str, String) -> String,
565 I: Iterator<Item = (Event<'i>, std::ops::Range<usize>)>,
566{
567 pub(crate) fn new(
568 input: &'i str,
569 config: Config,
570 code_block_formatter: F,
571 iter: I,
572 reference_links: Vec<ReferenceLinkDefinition>,
573 ) -> Self {
574 Self::new_with_paragraph_and_html_block_formatter(
575 input,
576 config,
577 code_block_formatter,
578 iter,
579 reference_links,
580 )
581 }
582}
583
584impl<'i, F, I, P, H> FormatState<'i, F, I, P, H>
585where
586 F: Fn(&str, String) -> String,
587 I: Iterator<Item = (Event<'i>, std::ops::Range<usize>)>,
588 P: ParagraphFormatter,
589 H: ParagraphFormatter,
590{
591 pub(crate) fn new_with_paragraph_and_html_block_formatter(
592 input: &'i str,
593 config: Config,
594 code_block_formatter: F,
595 iter: I,
596 reference_links: Vec<ReferenceLinkDefinition>,
597 ) -> Self {
598 Self {
599 input,
600 last_was_softbreak: false,
601 events: iter.peekable(),
602 rewrite_buffer: String::with_capacity(input.len() * 2),
603 code_block_buffer: String::with_capacity(256),
604 indentation: vec![],
607 nested_context: vec![],
608 reference_links,
609 setext_header: None,
610 header_id_and_classes: None,
611 needs_indent: false,
612 table_state: None,
613 last_position: 0,
614 code_block_formatter,
615 trim_link_or_image_start: false,
616 paragraph: None,
617 html_block: None,
618 force_rewrite_buffer: false,
619 config,
620 }
621 }
622
623 fn format_code_buffer(&mut self, info_string: Option<&str>) -> String {
624 let code_block_buffer = std::mem::take(&mut self.code_block_buffer);
626
627 let Some(info_string) = info_string else {
628 return code_block_buffer;
630 };
631
632 (self.code_block_formatter)(info_string, code_block_buffer)
634 }
635
636 fn write_code_block_buffer(&mut self, info_string: Option<&str>) -> std::fmt::Result {
637 let code = self.format_code_buffer(info_string);
638
639 if code.trim().is_empty() && info_string.is_some() {
640 return Ok(());
643 }
644
645 self.join_with_indentation(&code, info_string.is_some())?;
646
647 if info_string.is_some() {
648 writeln!(self)?
650 }
651
652 Ok(())
653 }
654
655 fn formatter_width(&self) -> Option<usize> {
656 self.config
657 .max_width
658 .map(|w| w.saturating_sub(self.indentation_len()))
659 }
660
661 pub fn format(mut self) -> Result<String, std::fmt::Error> {
663 while let Some((event, range)) = self.events.next() {
664 tracing::debug!(?event, ?range);
665 let mut last_position = self.input[..range.end]
666 .bytes()
667 .rposition(|b| !b.is_ascii_whitespace())
668 .unwrap_or(0);
669
670 match event {
671 Event::Start(tag) => {
672 self.rewrite_reference_link_definitions(&range)?;
673 last_position = range.start;
674 self.start_tag(tag.clone(), range)?;
675 }
676 Event::End(ref tag) => {
677 self.end_tag(*tag, range)?;
678 self.check_needs_indent(&event);
679 }
680 Event::Text(ref parsed_text)
682 | Event::InlineMath(ref parsed_text)
683 | Event::DisplayMath(ref parsed_text) => {
684 last_position = range.end;
685 let starts_with_escape = self.input[..range.start].ends_with('\\');
686 let newlines = self.count_newlines(&range);
687 let text_from_source = &self.input[range];
688 let mut text = if text_from_source.is_empty() {
689 parsed_text.as_ref()
692 } else {
693 text_from_source
694 };
695
696 if self.in_html_block() {
697 text = text.trim_start_matches(' ');
698 }
699
700 if self.in_link_or_image() && self.trim_link_or_image_start {
701 text = text.trim_start();
703 self.trim_link_or_image_start = false
705 }
706
707 if matches!(
708 self.peek(),
709 Some(Event::End(TagEnd::Link { .. } | TagEnd::Image { .. }))
710 ) {
711 text = text.trim_end();
712 }
713
714 if self.needs_indent {
715 self.write_newlines(newlines)?;
716 self.needs_indent = false;
717 }
718
719 if starts_with_escape || self.needs_escape(text) {
720 write!(self, "\\{text}")?;
722 } else {
723 write!(self, "{text}")?;
724 }
725 self.check_needs_indent(&event);
726 }
727 Event::Code(_) => {
728 write!(self, "{}", &self.input[range])?;
729 }
730 Event::SoftBreak => {
731 last_position = range.end;
732 if self.in_link_or_image() {
733 let next_is_end = matches!(
734 self.peek(),
735 Some(Event::End(TagEnd::Link { .. } | TagEnd::Image { .. }))
736 );
737 if self.trim_link_or_image_start || next_is_end {
738 self.trim_link_or_image_start = false
739 } else {
740 write!(self, " ")?;
741 }
742 } else {
743 write!(self, "{}", &self.input[range])?;
744
745 if !self.in_paragraph() {
747 self.write_indentation(false)?;
748 }
749
750 self.last_was_softbreak = true;
751 }
752 }
753 Event::HardBreak => {
754 write!(self, "{}", &self.input[range])?;
755 }
756 Event::Html(_) => {
757 let html = &self.input[range].trim_start_matches(' ');
760 write!(self, "{}", html)?; self.check_needs_indent(&event);
762 }
763 Event::InlineHtml(_) => {
764 let newlines = self.count_newlines(&range);
765 if self.needs_indent {
766 self.write_newlines(newlines)?;
767 }
768 write!(self, "{}", &self.input[range].trim_end_matches('\n'))?;
769 self.check_needs_indent(&event);
770 }
771 Event::Rule => {
772 let newlines = self.count_newlines(&range);
773 self.write_newlines(newlines)?;
774 write!(self, "{}", &self.input[range])?;
775 self.check_needs_indent(&event)
776 }
777 Event::FootnoteReference(text) => {
778 write!(self, "[^{text}]")?;
779 }
780 Event::TaskListMarker(done) => {
781 if done {
782 write!(self, "[x] ")?;
783 } else {
784 write!(self, "[ ] ")?;
785 }
786 }
787 }
788 self.last_position = last_position
789 }
790 debug_assert!(self.nested_context.is_empty());
791 let trailing_newline = self.input.ends_with('\n');
792 self.rewrite_final_reference_links().map(|mut output| {
793 if trailing_newline {
794 output.push('\n');
795 }
796 output
797 })
798 }
799
800 fn start_tag(&mut self, tag: Tag<'i>, range: Range<usize>) -> std::fmt::Result {
801 match tag {
802 Tag::Paragraph => {
803 if self.needs_indent {
804 let newlines = self.count_newlines(&range);
805 self.write_newlines(newlines)?;
806 self.needs_indent = false;
807 }
808 self.nested_context.push(tag);
809 let capacity = (range.end - range.start) * 2;
810 let width = self.formatter_width();
811 self.paragraph = Some(P::new(width, capacity));
812 }
813 Tag::Heading {
814 level, id, classes, ..
815 } => {
816 self.header_id_and_classes = Some((id, classes));
817 if self.needs_indent {
818 let newlines = self.count_newlines(&range);
819 self.write_newlines(newlines)?;
820 self.needs_indent = false;
821 }
822 let full_header = self.input[range].trim();
823
824 if full_header.contains('\n') && full_header.ends_with(['=', '-']) {
825 let header_marker = full_header.split('\n').last().unwrap().trim();
828 self.setext_header.replace(header_marker);
829 return Ok(());
831 }
832
833 let header = match level {
834 HeadingLevel::H1 => "# ",
835 HeadingLevel::H2 => "## ",
836 HeadingLevel::H3 => "### ",
837 HeadingLevel::H4 => "#### ",
838 HeadingLevel::H5 => "##### ",
839 HeadingLevel::H6 => "###### ",
840 };
841
842 let empty_header = full_header
843 .trim_start_matches(header)
844 .trim_end_matches(|c: char| c.is_whitespace() || matches!(c, '#' | '\\'))
845 .is_empty();
846
847 if empty_header {
848 write!(self, "{}", header.trim())?;
849 } else {
850 write!(self, "{header}")?;
851 }
852 }
853 Tag::BlockQuote(_) => {
854 if let Some(indent) = self.indentation.last_mut() {
858 if indent == "> " {
859 *indent = ">".into()
860 }
861 }
862
863 let newlines = self.count_newlines(&range);
864 if self.needs_indent {
865 self.write_newlines(newlines)?;
866 self.needs_indent = false;
867 }
868
869 self.nested_context.push(tag);
870
871 match self.peek_with_range().map(|(e, r)| (e.clone(), r.clone())) {
872 Some((Event::End(TagEnd::BlockQuote), _)) => {
873 write!(self, ">")?;
875 self.indentation.push(">".into());
876
877 let snippet = &self.input[range].trim_end();
878 let newlines = snippet.bytes().filter(|b| matches!(b, b'\n')).count();
879 self.write_newlines(newlines)?;
880 }
881 Some((Event::Start(Tag::BlockQuote(_)), next_range)) => {
882 write!(self, ">")?;
885 self.indentation.push(">".into());
886
887 let snippet = &self.input[range.start..next_range.start];
890 let newlines = snippet.bytes().filter(|b| matches!(b, b'\n')).count();
891 self.write_newlines(newlines)?;
892 }
893 Some((_, next_range)) => {
894 let snippet = &self.input[range.start..next_range.start];
897 let newlines = snippet.bytes().filter(|b| matches!(b, b'\n')).count();
898
899 self.indentation.push("> ".into());
900 if newlines > 0 {
901 write!(self, ">")?;
902 self.write_newlines(newlines)?;
903 } else {
904 write!(self, "> ")?;
905 }
906 }
907 None => {
908 unreachable!("At the very least we'd expect an `End(BlockQuote)` event.");
910 }
911 }
912 }
913 Tag::CodeBlock(ref kind) => {
914 let newlines = self.count_newlines(&range);
915 if self.needs_indent && newlines > 0 {
916 self.write_newlines(newlines)?;
917 self.needs_indent = false;
918 }
919 match kind {
920 CodeBlockKind::Fenced(info_string) => {
921 rewrite_marker(self.input, &range, self)?;
922
923 if info_string.is_empty() {
924 writeln!(self)?;
925 self.nested_context.push(tag);
926 return Ok(());
927 }
928
929 let starts_with_space = self.input[range.clone()]
930 .trim_start_matches(['`', '~'])
931 .starts_with(char::is_whitespace);
932
933 let info_string = self.input[range]
934 .lines()
935 .next()
936 .unwrap_or_else(|| info_string.as_ref())
937 .trim_start_matches(['`', '~'])
938 .trim();
939
940 if starts_with_space {
941 writeln!(self, " {info_string}")?;
942 } else {
943 writeln!(self, "{info_string}")?;
944 }
945 }
946 CodeBlockKind::Indented => {
947 let indentation = " ";
949
950 if !matches!(self.peek(), Some(Event::End(TagEnd::CodeBlock))) {
951 self.write_str(indentation)?;
953 }
954
955 self.indentation.push(indentation.into());
956 }
957 }
958 self.nested_context.push(tag);
959 }
960 Tag::List(_) => {
961 if self.needs_indent {
962 let newlines = self.count_newlines(&range);
963 self.write_newlines(newlines)?;
964 self.needs_indent = false;
965 }
966
967 self.nested_context.push(tag);
972 }
973 Tag::Item => {
974 let newlines = self.count_newlines(&range);
975 if self.needs_indent && newlines > 0 {
976 self.write_newlines(newlines)?;
977 }
978
979 let empty_list_item = match self.events.peek() {
980 Some((Event::End(TagEnd::Item), _)) => true,
981 Some((_, next_range)) => {
982 let snippet = &self.input[range.start..next_range.start];
983 snippet.bytes().filter(|b| matches!(b, b'\n')).count() > 0
991 }
992 None => false,
993 };
994
995 self.needs_indent = empty_list_item;
998
999 let list_marker = self
1000 .config
1001 .list_marker(&self.input[range.clone()])
1002 .expect("Should be able to parse a list marker");
1003 tracing::debug!(?list_marker, source = &self.input[range]);
1004 let marker_char = list_marker.marker_char();
1011 match &list_marker {
1012 ListMarker::Ordered { number, .. } if empty_list_item => {
1013 let zero_padding = list_marker.zero_padding();
1014 write!(self, "{zero_padding}{number}{marker_char}")?;
1015 }
1016 ListMarker::Ordered { number, .. } => {
1017 let zero_padding = list_marker.zero_padding();
1018 write!(self, "{zero_padding}{number}{marker_char} ")?;
1019 }
1020 ListMarker::Unordered(_) if empty_list_item => {
1021 write!(self, "{marker_char}")?;
1022 }
1023 ListMarker::Unordered(_) => {
1024 write!(self, "{marker_char} ")?;
1025 }
1026 }
1027
1028 self.nested_context.push(tag);
1029 self.indentation.push(
1032 self.config
1033 .fixed_indentation
1034 .clone()
1035 .unwrap_or_else(|| list_marker.indentation()),
1036 );
1037 }
1041 Tag::FootnoteDefinition(label) => {
1042 write!(self, "[^{label}]: ")?;
1043 }
1044 Tag::Emphasis => {
1045 rewrite_marker_with_limit(self.input, &range, self, Some(1))?;
1046 }
1047 Tag::Strong => {
1048 rewrite_marker_with_limit(self.input, &range, self, Some(2))?;
1049 }
1050 Tag::Strikethrough => {
1051 rewrite_marker(self.input, &range, self)?;
1052 }
1053 Tag::Link { link_type, .. } => {
1054 let newlines = self.count_newlines(&range);
1055 if self.needs_indent && newlines > 0 {
1056 self.write_newlines(newlines)?;
1057 self.needs_indent = false;
1058 }
1059
1060 let email_or_auto = matches!(link_type, LinkType::Email | LinkType::Autolink);
1061 let opener = if email_or_auto { "<" } else { "[" };
1062 self.write_str(opener)?;
1063 self.nested_context.push(tag);
1064
1065 if matches!(self.peek(), Some(Event::Text(_) | Event::SoftBreak)) {
1066 self.trim_link_or_image_start = true
1067 }
1068 }
1069 Tag::Image { .. } => {
1070 let newlines = self.count_newlines(&range);
1071 if self.needs_indent && newlines > 0 {
1072 self.write_newlines(newlines)?;
1073 self.needs_indent = false;
1074 }
1075
1076 write!(self, "![")?;
1077 self.nested_context.push(tag);
1078
1079 if matches!(self.peek(), Some(Event::Text(_) | Event::SoftBreak)) {
1080 self.trim_link_or_image_start = true
1081 }
1082 }
1083 Tag::Table(ref alignment) => {
1084 if self.needs_indent {
1085 let newlines = self.count_newlines(&range);
1086 self.write_newlines(newlines)?;
1087 self.needs_indent = false;
1088 }
1089 self.table_state.replace(TableState::new(alignment.clone()));
1090 write!(self, "|")?;
1091 self.indentation.push("|".into());
1092 self.nested_context.push(tag);
1093 }
1094 Tag::TableHead => {
1095 self.nested_context.push(tag);
1096 }
1097 Tag::TableRow => {
1098 self.nested_context.push(tag);
1099 if let Some(state) = self.table_state.as_mut() {
1100 state.push_row()
1101 }
1102 }
1103 Tag::TableCell => {
1104 if !matches!(self.peek(), Some(Event::End(TagEnd::TableCell))) {
1105 return Ok(());
1106 }
1107
1108 if let Some(state) = self.table_state.as_mut() {
1109 state.write(String::new().into());
1110 }
1111 }
1112 Tag::HtmlBlock => {
1113 if matches!(self.events.peek(), Some((Event::End(TagEnd::Paragraph), _))) {
1114 tracing::debug!("HTML block start before paragraph end.");
1115 let (_, range) = self.events.next().expect("We peeked.");
1119 self.end_tag(TagEnd::Paragraph, range)?;
1120 }
1121
1122 let capacity = (range.end - range.start) * 2;
1123 let width = self.formatter_width();
1124 let mut html_block = H::new(width, capacity);
1125 let newlines = self.count_newlines(&range);
1126 tracing::trace!(newlines);
1127 for _ in 0..newlines {
1128 html_block.write_char('\n')?;
1129 }
1130 self.html_block = Some(html_block);
1131 }
1132 Tag::MetadataBlock(kind) => {
1133 self.write_metadata_block_separator(&kind, range)?;
1134 }
1135 }
1136 Ok(())
1137 }
1138
1139 fn end_tag(&mut self, tag: TagEnd, range: Range<usize>) -> std::fmt::Result {
1140 match tag {
1141 TagEnd::Paragraph => {
1142 let popped_tag = self.nested_context.pop();
1143 debug_assert_eq!(popped_tag, Some(Tag::Paragraph));
1144
1145 if let Some(p) = self.paragraph.take() {
1146 self.join_with_indentation(&p.into_buffer(), false)?;
1147 }
1148 }
1149 TagEnd::Heading(_) => {
1150 let (fragment_identifier, classes) = self
1151 .header_id_and_classes
1152 .take()
1153 .expect("Should have pushed a header tag");
1154 match (fragment_identifier, classes.is_empty()) {
1155 (Some(id), false) => {
1156 let classes = rewirte_header_classes(classes)?;
1157 write!(self, " {{#{id}{classes}}}")?;
1158 }
1159 (Some(id), true) => {
1160 write!(self, " {{#{id}}}")?;
1161 }
1162 (None, false) => {
1163 let classes = rewirte_header_classes(classes)?;
1164 write!(self, " {{{}}}", classes.trim())?;
1165 }
1166 (None, true) => {}
1167 }
1168
1169 if let Some(marker) = self.setext_header.take() {
1170 self.write_newlines(1)?;
1171 write!(self, "{marker}")?;
1172 }
1173 }
1174 TagEnd::BlockQuote => {
1175 let newlines = self.count_newlines(&range);
1176 if self.needs_indent && newlines > 0 {
1177 if let Some(last) = self.indentation.last_mut() {
1179 *last = ">".into()
1181 }
1182 self.write_newlines(newlines)?;
1183 }
1184 let popped_tag = self.nested_context.pop();
1185 debug_assert_eq!(popped_tag.unwrap().to_end(), tag);
1186
1187 let popped_indentation = self
1188 .indentation
1189 .pop()
1190 .expect("we pushed a blockquote marker in start_tag");
1191 if let Some(indentation) = self.indentation.last_mut() {
1192 if indentation == ">" {
1193 *indentation = popped_indentation
1194 }
1195 }
1196 }
1197 TagEnd::CodeBlock => {
1198 let popped_tag = self.nested_context.pop();
1199 let Some(Tag::CodeBlock(kind)) = &popped_tag else {
1200 unreachable!("Should have pushed a code block start tag");
1201 };
1202
1203 match kind {
1204 CodeBlockKind::Fenced(info_string) => {
1205 self.write_code_block_buffer(Some(info_string))?;
1206 self.write_indentation(false)?;
1208 rewrite_marker(self.input, &range, self)?;
1209 }
1210 CodeBlockKind::Indented => {
1211 self.write_code_block_buffer(None)?;
1213
1214 let popped_indentation = self
1215 .indentation
1216 .pop()
1217 .expect("we added 4 spaces in start_tag");
1218 debug_assert_eq!(popped_indentation, " ");
1219 }
1220 }
1221 }
1222 TagEnd::List(_) => {
1223 let popped_tag = self.nested_context.pop();
1224 debug_assert_eq!(popped_tag.unwrap().to_end(), tag);
1225 if let Some(Event::Start(Tag::CodeBlock(CodeBlockKind::Indented))) = self.peek() {
1235 self.write_newlines(1)?;
1236 writeln!(self, "<!-- Don't absorb code block into list -->")?;
1237 write!(self, "<!-- Consider a fenced code block instead -->")?;
1238 };
1239 }
1240 TagEnd::Item => {
1241 let newlines = self.count_newlines(&range);
1242 if self.needs_indent && newlines > 0 {
1243 self.write_newlines_no_trailing_whitespace(newlines)?;
1244 }
1245 let popped_tag = self.nested_context.pop();
1246 debug_assert_eq!(popped_tag.unwrap().to_end(), tag);
1247 let popped_indentation = self.indentation.pop();
1248 debug_assert!(popped_indentation.is_some());
1249
1250 self.needs_indent = matches!(self.peek(), Some(Event::Start(Tag::Item)));
1252 }
1253 TagEnd::FootnoteDefinition => {}
1254 TagEnd::Emphasis => {
1255 rewrite_marker_with_limit(self.input, &range, self, Some(1))?;
1256 }
1257 TagEnd::Strong => {
1258 rewrite_marker_with_limit(self.input, &range, self, Some(2))?;
1259 }
1260 TagEnd::Strikethrough => {
1261 rewrite_marker(self.input, &range, self)?;
1262 }
1263 TagEnd::Link | TagEnd::Image => {
1264 let popped_tag = self
1265 .nested_context
1266 .pop()
1267 .expect("Should have pushed a start tag.");
1268 debug_assert_eq!(popped_tag.to_end(), tag);
1269 let (link_type, url, title) = match popped_tag {
1270 Tag::Link {
1271 link_type,
1272 dest_url,
1273 title,
1274 ..
1275 }
1276 | Tag::Image {
1277 link_type,
1278 dest_url,
1279 title,
1280 ..
1281 } => (link_type, dest_url, title),
1282 _ => unreachable!("Should reach the end of a corresponding tag."),
1283 };
1284
1285 let text = &self.input[range.clone()];
1286
1287 match link_type {
1288 LinkType::Inline => {
1289 if let Some((source_url, title_and_quote)) =
1290 crate::links::find_inline_url_and_title(text)
1291 {
1292 self.write_inline_link(&source_url, title_and_quote)?;
1293 } else {
1294 let title = if title.is_empty() {
1295 None
1296 } else {
1297 Some((title, '"'))
1298 };
1299 self.write_inline_link(&url, title)?;
1300 }
1301 }
1302 LinkType::Reference | LinkType::ReferenceUnknown => {
1303 let label = crate::links::find_reference_link_label(text);
1304 write!(self, "][{label}]")?;
1305 }
1306 LinkType::Collapsed | LinkType::CollapsedUnknown => write!(self, "][]")?,
1307 LinkType::Shortcut | LinkType::ShortcutUnknown => write!(self, "]")?,
1308 LinkType::Autolink | LinkType::Email => write!(self, ">")?,
1309 }
1310 }
1311 TagEnd::Table => {
1312 let popped_tag = self.nested_context.pop();
1313 debug_assert_eq!(popped_tag.unwrap().to_end(), tag);
1314 if let Some(state) = self.table_state.take() {
1315 self.join_with_indentation(&state.format()?, false)?;
1316 }
1317 let popped_indentation = self.indentation.pop().expect("we added `|` in start_tag");
1318 debug_assert_eq!(popped_indentation, "|");
1319 }
1320 TagEnd::TableRow | TagEnd::TableHead => {
1321 let popped_tag = self.nested_context.pop();
1322 debug_assert_eq!(popped_tag.unwrap().to_end(), tag);
1323 }
1324 TagEnd::TableCell => {
1325 if let Some(state) = self.table_state.as_mut() {
1326 state.increment_col_index()
1328 }
1329 }
1330 TagEnd::HtmlBlock => {
1331 let newlines = self.count_newlines(&range);
1332 self.write_newlines(newlines)?;
1333 if let Some(h) = self.html_block.take() {
1334 self.join_with_indentation(&h.into_buffer(), false)?;
1335 }
1336 }
1337 TagEnd::MetadataBlock(kind) => {
1338 self.write_metadata_block_separator(&kind, range)?;
1339 }
1340 }
1341 Ok(())
1342 }
1343
1344 fn write_metadata_block_separator(
1345 &mut self,
1346 kind: &MetadataBlockKind,
1347 range: Range<usize>,
1348 ) -> std::fmt::Result {
1349 let newlines = self.count_newlines(&range);
1350 self.write_newlines(newlines)?;
1351 let marker = match kind {
1352 MetadataBlockKind::YamlStyle => "---",
1353 MetadataBlockKind::PlusesStyle => "+++",
1354 };
1355 writeln!(self, "{marker}")
1356 }
1357}
1358
1359fn find_marker<'i, P>(input: &'i str, range: &Range<usize>, predicate: P) -> &'i str
1362where
1363 P: FnMut(char) -> bool,
1364{
1365 let end = if let Some(position) = input[range.start..].chars().position(predicate) {
1366 range.start + position
1367 } else {
1368 range.end
1369 };
1370 &input[range.start..end]
1371}
1372
1373fn rewrite_marker_with_limit<W: std::fmt::Write>(
1375 input: &str,
1376 range: &Range<usize>,
1377 writer: &mut W,
1378 size_limit: Option<usize>,
1379) -> std::fmt::Result {
1380 let marker_char = input[range.start..].chars().next().unwrap();
1381 let marker = find_marker(input, range, |c| c != marker_char);
1382 if let Some(mark_max_width) = size_limit {
1383 write!(writer, "{}", &marker[..mark_max_width])
1384 } else {
1385 write!(writer, "{marker}")
1386 }
1387}
1388
1389fn rewrite_marker<W: std::fmt::Write>(
1391 input: &str,
1392 range: &Range<usize>,
1393 writer: &mut W,
1394) -> std::fmt::Result {
1395 rewrite_marker_with_limit(input, range, writer, None)
1396}
1397
1398fn rewirte_header_classes(classes: Vec<CowStr>) -> Result<String, std::fmt::Error> {
1400 let item_len = classes.iter().map(|i| i.len()).sum::<usize>();
1401 let capacity = item_len + classes.len() * 2;
1402 let mut result = String::with_capacity(capacity);
1403 for class in classes {
1404 write!(result, " .{class}")?;
1405 }
1406 Ok(result)
1407}