1#![deny(rust_2018_idioms)]
2
3use std::{
4 borrow::{Borrow, Cow},
5 collections::HashSet,
6 fmt,
7 ops::Range,
8};
9
10use pulldown_cmark::{Alignment as TableAlignment, BlockQuoteKind, Event, LinkType, MetadataBlockKind, Tag, TagEnd};
11
12mod source_range;
13mod text_modifications;
14
15pub use source_range::{
16 cmark_resume_with_source_range, cmark_resume_with_source_range_and_options, cmark_with_source_range,
17 cmark_with_source_range_and_options,
18};
19use text_modifications::*;
20
21#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
24pub enum Alignment {
25 None,
26 Left,
27 Center,
28 Right,
29}
30
31impl<'a> From<&'a TableAlignment> for Alignment {
32 fn from(s: &'a TableAlignment) -> Self {
33 match *s {
34 TableAlignment::None => Self::None,
35 TableAlignment::Left => Self::Left,
36 TableAlignment::Center => Self::Center,
37 TableAlignment::Right => Self::Right,
38 }
39 }
40}
41
42#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
43pub enum CodeBlockKind {
44 Indented,
45 Fenced,
46}
47
48#[derive(Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
52#[non_exhaustive]
53pub struct State<'a> {
54 pub newlines_before_start: usize,
56 pub list_stack: Vec<Option<u64>>,
58 pub padding: Vec<Cow<'a, str>>,
61 pub table_alignments: Vec<Alignment>,
63 pub table_headers: Vec<String>,
65 pub text_for_header: Option<String>,
67 pub code_block: Option<CodeBlockKind>,
69 pub last_was_text_without_trailing_newline: bool,
71 pub last_was_paragraph_start: bool,
73 pub next_is_link_like: bool,
75 pub link_stack: Vec<LinkCategory<'a>>,
77 pub image_stack: Vec<ImageLink<'a>>,
79 pub current_heading: Option<Heading<'a>>,
81 pub in_table_cell: bool,
83
84 pub current_shortcut_text: Option<String>,
86 pub shortcuts: Vec<(String, String, String)>,
88 pub last_event_end_index: usize,
93}
94
95#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
96pub enum LinkCategory<'a> {
97 AngleBracketed,
98 Reference {
99 uri: Cow<'a, str>,
100 title: Cow<'a, str>,
101 id: Cow<'a, str>,
102 },
103 Collapsed {
104 uri: Cow<'a, str>,
105 title: Cow<'a, str>,
106 },
107 Shortcut {
108 uri: Cow<'a, str>,
109 title: Cow<'a, str>,
110 },
111 Other {
112 uri: Cow<'a, str>,
113 title: Cow<'a, str>,
114 },
115}
116
117#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
118pub enum ImageLink<'a> {
119 Reference {
120 uri: Cow<'a, str>,
121 title: Cow<'a, str>,
122 id: Cow<'a, str>,
123 },
124 Collapsed {
125 uri: Cow<'a, str>,
126 title: Cow<'a, str>,
127 },
128 Shortcut {
129 uri: Cow<'a, str>,
130 title: Cow<'a, str>,
131 },
132 Other {
133 uri: Cow<'a, str>,
134 title: Cow<'a, str>,
135 },
136}
137
138#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
139pub struct Heading<'a> {
140 id: Option<Cow<'a, str>>,
141 classes: Vec<Cow<'a, str>>,
142 attributes: Vec<(Cow<'a, str>, Option<Cow<'a, str>>)>,
143}
144
145pub const DEFAULT_CODE_BLOCK_TOKEN_COUNT: usize = 3;
147
148#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
155pub struct Options<'a> {
156 pub newlines_after_headline: usize,
157 pub newlines_after_paragraph: usize,
158 pub newlines_after_codeblock: usize,
159 pub newlines_after_htmlblock: usize,
160 pub newlines_after_table: usize,
161 pub newlines_after_rule: usize,
162 pub newlines_after_list: usize,
163 pub newlines_after_blockquote: usize,
164 pub newlines_after_rest: usize,
165 pub newlines_after_metadata: usize,
167 pub code_block_token_count: usize,
172 pub code_block_token: char,
173 pub list_token: char,
174 pub ordered_list_token: char,
175 pub increment_ordered_list_bullets: bool,
176 pub emphasis_token: char,
177 pub strong_token: &'a str,
178}
179
180const DEFAULT_OPTIONS: Options<'_> = Options {
181 newlines_after_headline: 2,
182 newlines_after_paragraph: 2,
183 newlines_after_codeblock: 2,
184 newlines_after_htmlblock: 1,
185 newlines_after_table: 2,
186 newlines_after_rule: 2,
187 newlines_after_list: 2,
188 newlines_after_blockquote: 2,
189 newlines_after_rest: 1,
190 newlines_after_metadata: 1,
191 code_block_token_count: 4,
192 code_block_token: '`',
193 list_token: '*',
194 ordered_list_token: '.',
195 increment_ordered_list_bullets: false,
196 emphasis_token: '*',
197 strong_token: "**",
198};
199
200impl Default for Options<'_> {
201 fn default() -> Self {
202 DEFAULT_OPTIONS
203 }
204}
205
206impl Options<'_> {
207 pub fn special_characters(&self) -> Cow<'static, str> {
208 const BASE: &str = "#\\_*<>`|[]";
210 if DEFAULT_OPTIONS.code_block_token == self.code_block_token
211 && DEFAULT_OPTIONS.list_token == self.list_token
212 && DEFAULT_OPTIONS.emphasis_token == self.emphasis_token
213 && DEFAULT_OPTIONS.strong_token == self.strong_token
214 {
215 BASE.into()
216 } else {
217 let mut s = String::from(BASE);
218 s.push(self.code_block_token);
219 s.push(self.list_token);
220 s.push(self.emphasis_token);
221 s.push_str(self.strong_token);
222 s.into()
223 }
224 }
225}
226
227#[derive(Debug)]
230pub enum Error {
231 FormatFailed(fmt::Error),
232 UnexpectedEvent,
233}
234
235impl fmt::Display for Error {
236 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237 match self {
238 Self::FormatFailed(e) => e.fmt(f),
239 Self::UnexpectedEvent => f.write_str("Unexpected event while reconstructing Markdown"),
240 }
241 }
242}
243
244impl std::error::Error for Error {}
245
246impl From<fmt::Error> for Error {
247 fn from(e: fmt::Error) -> Self {
248 Self::FormatFailed(e)
249 }
250}
251
252pub fn cmark<'a, I, E, F>(events: I, mut formatter: F) -> Result<State<'a>, Error>
254where
255 I: Iterator<Item = E>,
256 E: Borrow<Event<'a>>,
257 F: fmt::Write,
258{
259 cmark_with_options(events, &mut formatter, Default::default())
260}
261
262pub fn cmark_resume<'a, I, E, F>(events: I, formatter: F, state: Option<State<'a>>) -> Result<State<'a>, Error>
264where
265 I: Iterator<Item = E>,
266 E: Borrow<Event<'a>>,
267 F: fmt::Write,
268{
269 cmark_resume_with_options(events, formatter, state, Options::default())
270}
271
272pub fn cmark_with_options<'a, I, E, F>(events: I, mut formatter: F, options: Options<'_>) -> Result<State<'a>, Error>
274where
275 I: Iterator<Item = E>,
276 E: Borrow<Event<'a>>,
277 F: fmt::Write,
278{
279 let state = cmark_resume_with_options(events, &mut formatter, Default::default(), options)?;
280 state.finalize(formatter)
281}
282
283pub fn cmark_resume_with_options<'a, I, E, F>(
302 events: I,
303 mut formatter: F,
304 state: Option<State<'a>>,
305 options: Options<'_>,
306) -> Result<State<'a>, Error>
307where
308 I: Iterator<Item = E>,
309 E: Borrow<Event<'a>>,
310 F: fmt::Write,
311{
312 let mut state = state.unwrap_or_default();
313 let mut events = events.peekable();
314 while let Some(event) = events.next() {
315 state.next_is_link_like = matches!(
316 events.peek().map(Borrow::borrow),
317 Some(
318 Event::Start(Tag::Link { .. } | Tag::Image { .. } | Tag::FootnoteDefinition(..))
319 | Event::FootnoteReference(..)
320 )
321 );
322 cmark_resume_one_event(event, &mut formatter, &mut state, &options)?;
323 }
324 Ok(state)
325}
326
327fn cmark_resume_one_event<'a, E, F>(
328 event: E,
329 formatter: &mut F,
330 state: &mut State<'a>,
331 options: &Options<'_>,
332) -> Result<(), Error>
333where
334 E: Borrow<Event<'a>>,
335 F: fmt::Write,
336{
337 use pulldown_cmark::{Event::*, Tag::*};
338
339 let last_was_text_without_trailing_newline = state.last_was_text_without_trailing_newline;
340 state.last_was_text_without_trailing_newline = false;
341 let last_was_paragraph_start = state.last_was_paragraph_start;
342 state.last_was_paragraph_start = false;
343
344 let res = match event.borrow() {
345 Rule => {
346 consume_newlines(formatter, state)?;
347 state.set_minimum_newlines_before_start(options.newlines_after_rule);
348 formatter.write_str("---")
349 }
350 Code(text) => {
351 if let Some(shortcut_text) = state.current_shortcut_text.as_mut() {
352 shortcut_text.push('`');
353 shortcut_text.push_str(text);
354 shortcut_text.push('`');
355 }
356 if let Some(text_for_header) = state.text_for_header.as_mut() {
357 text_for_header.push('`');
358 text_for_header.push_str(text);
359 text_for_header.push('`');
360 }
361
362 let text = if state.in_table_cell {
371 Cow::Owned(text.replace('|', "\\|"))
372 } else {
373 Cow::Borrowed(text.as_ref())
374 };
375
376 if text.chars().all(|ch| ch == ' ') {
379 write!(formatter, "`{text}`")
380 } else {
381 let backticks = Repeated('`', max_consecutive_chars(&text, '`') + 1);
384 let space = match text.as_bytes() {
385 &[b'`', ..] | &[.., b'`'] => " ", &[b' ', .., b' '] => " ", _ => "", };
389 write!(formatter, "{backticks}{space}{text}{space}{backticks}")
390 }
391 }
392 Start(tag) => {
393 if let List(list_type) = tag {
394 state.list_stack.push(*list_type);
395 if state.list_stack.len() > 1 {
396 state.set_minimum_newlines_before_start(options.newlines_after_rest);
397 }
398 }
399 let consumed_newlines = state.newlines_before_start != 0;
400 consume_newlines(formatter, state)?;
401 match tag {
402 Item => {
403 state.last_was_paragraph_start = true;
405 match state.list_stack.last_mut() {
406 Some(inner) => {
407 state.padding.push(list_item_padding_of(*inner));
408 match inner {
409 Some(n) => {
410 let bullet_number = *n;
411 if options.increment_ordered_list_bullets {
412 *n += 1;
413 }
414 write!(formatter, "{}{} ", bullet_number, options.ordered_list_token)
415 }
416 None => write!(formatter, "{} ", options.list_token),
417 }
418 }
419 None => Ok(()),
420 }
421 }
422 Table(alignments) => {
423 state.table_alignments = alignments.iter().map(From::from).collect();
424 Ok(())
425 }
426 TableHead => Ok(()),
427 TableRow => Ok(()),
428 TableCell => {
429 state.text_for_header = Some(String::new());
430 state.in_table_cell = true;
431 formatter.write_char('|')
432 }
433 Link {
434 link_type,
435 dest_url,
436 title,
437 id,
438 } => {
439 state.link_stack.push(match link_type {
440 LinkType::Autolink | LinkType::Email => {
441 formatter.write_char('<')?;
442 LinkCategory::AngleBracketed
443 }
444 LinkType::Reference => {
445 formatter.write_char('[')?;
446 LinkCategory::Reference {
447 uri: dest_url.clone().into(),
448 title: title.clone().into(),
449 id: id.clone().into(),
450 }
451 }
452 LinkType::Collapsed => {
453 state.current_shortcut_text = Some(String::new());
454 formatter.write_char('[')?;
455 LinkCategory::Collapsed {
456 uri: dest_url.clone().into(),
457 title: title.clone().into(),
458 }
459 }
460 LinkType::Shortcut => {
461 state.current_shortcut_text = Some(String::new());
462 formatter.write_char('[')?;
463 LinkCategory::Shortcut {
464 uri: dest_url.clone().into(),
465 title: title.clone().into(),
466 }
467 }
468 _ => {
469 formatter.write_char('[')?;
470 LinkCategory::Other {
471 uri: dest_url.clone().into(),
472 title: title.clone().into(),
473 }
474 }
475 });
476 Ok(())
477 }
478 Image {
479 link_type,
480 dest_url,
481 title,
482 id,
483 } => {
484 state.image_stack.push(match link_type {
485 LinkType::Reference => ImageLink::Reference {
486 uri: dest_url.clone().into(),
487 title: title.clone().into(),
488 id: id.clone().into(),
489 },
490 LinkType::Collapsed => {
491 state.current_shortcut_text = Some(String::new());
492 ImageLink::Collapsed {
493 uri: dest_url.clone().into(),
494 title: title.clone().into(),
495 }
496 }
497 LinkType::Shortcut => {
498 state.current_shortcut_text = Some(String::new());
499 ImageLink::Shortcut {
500 uri: dest_url.clone().into(),
501 title: title.clone().into(),
502 }
503 }
504 _ => ImageLink::Other {
505 uri: dest_url.clone().into(),
506 title: title.clone().into(),
507 },
508 });
509 formatter.write_str("![")
510 }
511 Emphasis => formatter.write_char(options.emphasis_token),
512 Strong => formatter.write_str(options.strong_token),
513 FootnoteDefinition(name) => {
514 state.padding.push(" ".into());
515 write!(formatter, "[^{name}]: ")
516 }
517 Paragraph => {
518 state.last_was_paragraph_start = true;
519 Ok(())
520 }
521 Heading {
522 level,
523 id,
524 classes,
525 attrs,
526 } => {
527 if state.current_heading.is_some() {
528 return Err(Error::UnexpectedEvent);
529 }
530 state.current_heading = Some(self::Heading {
531 id: id.as_ref().map(|id| id.clone().into()),
532 classes: classes.iter().map(|class| class.clone().into()).collect(),
533 attributes: attrs
534 .iter()
535 .map(|(k, v)| (k.clone().into(), v.as_ref().map(|val| val.clone().into())))
536 .collect(),
537 });
538 write!(formatter, "{} ", Repeated('#', *level as usize))
540 }
541 BlockQuote(kind) => {
542 let every_line_padding = " > ";
543 let first_line_padding = kind
544 .map(|kind| match kind {
545 BlockQuoteKind::Note => " > [!NOTE]",
546 BlockQuoteKind::Tip => " > [!TIP]",
547 BlockQuoteKind::Important => " > [!IMPORTANT]",
548 BlockQuoteKind::Warning => " > [!WARNING]",
549 BlockQuoteKind::Caution => " > [!CAUTION]",
550 })
551 .unwrap_or(every_line_padding);
552 state.newlines_before_start = 1;
553
554 if !consumed_newlines {
558 write_padded_newline(formatter, state)?;
559 }
560 formatter.write_str(first_line_padding)?;
561 state.padding.push(every_line_padding.into());
562 Ok(())
563 }
564 CodeBlock(pulldown_cmark::CodeBlockKind::Indented) => {
565 state.code_block = Some(CodeBlockKind::Indented);
566 state.padding.push(" ".into());
567 if consumed_newlines {
568 formatter.write_str(" ")
569 } else {
570 write_padded_newline(formatter, &state)
571 }
572 }
573 CodeBlock(pulldown_cmark::CodeBlockKind::Fenced(info)) => {
574 state.code_block = Some(CodeBlockKind::Fenced);
575 if !consumed_newlines {
576 write_padded_newline(formatter, &state)?;
577 }
578
579 let fence = Repeated(options.code_block_token, options.code_block_token_count);
580 write!(formatter, "{fence}{info}")?;
581 write_padded_newline(formatter, &state)
582 }
583 HtmlBlock => Ok(()),
584 MetadataBlock(MetadataBlockKind::YamlStyle) => formatter.write_str("---\n"),
585 MetadataBlock(MetadataBlockKind::PlusesStyle) => formatter.write_str("+++\n"),
586 List(_) => Ok(()),
587 Strikethrough => formatter.write_str("~~"),
588 DefinitionList => Ok(()),
589 DefinitionListTitle => {
590 state.set_minimum_newlines_before_start(options.newlines_after_rest);
591 Ok(())
592 }
593 DefinitionListDefinition => {
594 let every_line_padding = " ";
595 let first_line_padding = ": ";
596
597 padding(formatter, &state.padding).and(formatter.write_str(first_line_padding))?;
598 state.padding.push(every_line_padding.into());
599 Ok(())
600 }
601 Superscript => formatter.write_str("<sup>"),
602 Subscript => formatter.write_str("<sub>"),
603 }
604 }
605 End(tag) => match tag {
606 TagEnd::Link => match if let Some(link_cat) = state.link_stack.pop() {
607 link_cat
608 } else {
609 return Err(Error::UnexpectedEvent);
610 } {
611 LinkCategory::AngleBracketed => formatter.write_char('>'),
612 LinkCategory::Reference { uri, title, id } => {
613 state
614 .shortcuts
615 .push((id.to_string(), uri.to_string(), title.to_string()));
616 formatter.write_str("][")?;
617 formatter.write_str(&id)?;
618 formatter.write_char(']')
619 }
620 LinkCategory::Collapsed { uri, title } => {
621 if let Some(shortcut_text) = state.current_shortcut_text.take() {
622 state
623 .shortcuts
624 .push((shortcut_text, uri.to_string(), title.to_string()));
625 }
626 formatter.write_str("][]")
627 }
628 LinkCategory::Shortcut { uri, title } => {
629 if let Some(shortcut_text) = state.current_shortcut_text.take() {
630 state
631 .shortcuts
632 .push((shortcut_text, uri.to_string(), title.to_string()));
633 }
634 formatter.write_char(']')
635 }
636 LinkCategory::Other { uri, title } => close_link(&uri, &title, formatter, LinkType::Inline),
637 },
638 TagEnd::Image => match if let Some(img_link) = state.image_stack.pop() {
639 img_link
640 } else {
641 return Err(Error::UnexpectedEvent);
642 } {
643 ImageLink::Reference { uri, title, id } => {
644 state
645 .shortcuts
646 .push((id.to_string(), uri.to_string(), title.to_string()));
647 formatter.write_str("][")?;
648 formatter.write_str(&id)?;
649 formatter.write_char(']')
650 }
651 ImageLink::Collapsed { uri, title } => {
652 if let Some(shortcut_text) = state.current_shortcut_text.take() {
653 state
654 .shortcuts
655 .push((shortcut_text, uri.to_string(), title.to_string()));
656 }
657 formatter.write_str("][]")
658 }
659 ImageLink::Shortcut { uri, title } => {
660 if let Some(shortcut_text) = state.current_shortcut_text.take() {
661 state
662 .shortcuts
663 .push((shortcut_text, uri.to_string(), title.to_string()));
664 }
665 formatter.write_char(']')
666 }
667 ImageLink::Other { uri, title } => {
668 close_link(uri.as_ref(), title.as_ref(), formatter, LinkType::Inline)
669 }
670 },
671 TagEnd::Emphasis => formatter.write_char(options.emphasis_token),
672 TagEnd::Strong => formatter.write_str(options.strong_token),
673 TagEnd::Heading(_) => {
674 let Some(self::Heading {
675 id,
676 classes,
677 attributes,
678 }) = state.current_heading.take()
679 else {
680 return Err(Error::UnexpectedEvent);
681 };
682 let emit_braces = id.is_some() || !classes.is_empty() || !attributes.is_empty();
683 if emit_braces {
684 formatter.write_str(" {")?;
685 }
686 if let Some(id_str) = id {
687 formatter.write_char(' ')?;
688 formatter.write_char('#')?;
689 formatter.write_str(&id_str)?;
690 }
691 for class in &classes {
692 formatter.write_char(' ')?;
693 formatter.write_char('.')?;
694 formatter.write_str(class)?;
695 }
696 for (key, val) in &attributes {
697 formatter.write_char(' ')?;
698 formatter.write_str(key)?;
699 if let Some(val) = val {
700 formatter.write_char('=')?;
701 formatter.write_str(val)?;
702 }
703 }
704 if emit_braces {
705 formatter.write_char(' ')?;
706 formatter.write_char('}')?;
707 }
708 state.set_minimum_newlines_before_start(options.newlines_after_headline);
709 Ok(())
710 }
711 TagEnd::Paragraph => {
712 state.set_minimum_newlines_before_start(options.newlines_after_paragraph);
713 Ok(())
714 }
715 TagEnd::CodeBlock => {
716 state.set_minimum_newlines_before_start(options.newlines_after_codeblock);
717 if last_was_text_without_trailing_newline {
718 write_padded_newline(formatter, &state)?;
719 }
720 match state.code_block {
721 Some(CodeBlockKind::Fenced) => {
722 let fence = Repeated(options.code_block_token, options.code_block_token_count);
723 write!(formatter, "{fence}")?;
724 }
725 Some(CodeBlockKind::Indented) => {
726 state.padding.pop();
727 }
728 None => {}
729 }
730 state.code_block = None;
731 Ok(())
732 }
733 TagEnd::HtmlBlock => {
734 state.set_minimum_newlines_before_start(options.newlines_after_htmlblock);
735 Ok(())
736 }
737 TagEnd::MetadataBlock(MetadataBlockKind::PlusesStyle) => {
738 state.set_minimum_newlines_before_start(options.newlines_after_metadata);
739 formatter.write_str("+++\n")
740 }
741 TagEnd::MetadataBlock(MetadataBlockKind::YamlStyle) => {
742 state.set_minimum_newlines_before_start(options.newlines_after_metadata);
743 formatter.write_str("---\n")
744 }
745 TagEnd::Table => {
746 state.set_minimum_newlines_before_start(options.newlines_after_table);
747 state.table_alignments.clear();
748 state.table_headers.clear();
749 Ok(())
750 }
751 TagEnd::TableCell => {
752 state
753 .table_headers
754 .push(state.text_for_header.take().unwrap_or_default());
755 state.in_table_cell = false;
756 Ok(())
757 }
758 t @ (TagEnd::TableRow | TagEnd::TableHead) => {
759 state.set_minimum_newlines_before_start(options.newlines_after_rest);
760 formatter.write_char('|')?;
761
762 if let TagEnd::TableHead = t {
763 write_padded_newline(formatter, &state)?;
764 for (alignment, name) in state.table_alignments.iter().zip(state.table_headers.iter()) {
765 formatter.write_char('|')?;
766 let min_width = match alignment {
771 Alignment::None => 1,
773 Alignment::Left | Alignment::Right => 2,
775 Alignment::Center => 3,
777 };
778 let length = name.chars().count().max(min_width);
779 let last_minus_one = length.saturating_sub(1);
780 for c in 0..length {
781 formatter.write_char(
782 if (c == 0 && (alignment == &Alignment::Center || alignment == &Alignment::Left))
783 || (c == last_minus_one
784 && (alignment == &Alignment::Center || alignment == &Alignment::Right))
785 {
786 ':'
787 } else {
788 '-'
789 },
790 )?;
791 }
792 }
793 formatter.write_char('|')?;
794 }
795 Ok(())
796 }
797 TagEnd::Item => {
798 state.padding.pop();
799 state.set_minimum_newlines_before_start(options.newlines_after_rest);
800 Ok(())
801 }
802 TagEnd::List(_) => {
803 state.list_stack.pop();
804 if state.list_stack.is_empty() {
805 state.set_minimum_newlines_before_start(options.newlines_after_list);
806 }
807 Ok(())
808 }
809 TagEnd::BlockQuote(_) => {
810 state.padding.pop();
811
812 state.set_minimum_newlines_before_start(options.newlines_after_blockquote);
813
814 Ok(())
815 }
816 TagEnd::FootnoteDefinition => {
817 state.padding.pop();
818 Ok(())
819 }
820 TagEnd::Strikethrough => formatter.write_str("~~"),
821 TagEnd::DefinitionList => {
822 state.set_minimum_newlines_before_start(options.newlines_after_list);
823 Ok(())
824 }
825 TagEnd::DefinitionListTitle => formatter.write_char('\n'),
826 TagEnd::DefinitionListDefinition => {
827 state.padding.pop();
828 write_padded_newline(formatter, &state)
829 }
830 TagEnd::Superscript => formatter.write_str("</sup>"),
831 TagEnd::Subscript => formatter.write_str("</sub>"),
832 },
833 HardBreak => formatter.write_str(" ").and(write_padded_newline(formatter, &state)),
834 SoftBreak => write_padded_newline(formatter, &state),
835 Text(text) => {
836 let mut text = &text[..];
837 if let Some(shortcut_text) = state.current_shortcut_text.as_mut() {
838 shortcut_text.push_str(text);
839 }
840 if let Some(text_for_header) = state.text_for_header.as_mut() {
841 text_for_header.push_str(text);
842 }
843 consume_newlines(formatter, state)?;
844 if last_was_paragraph_start {
845 if text.starts_with('\t') {
846 formatter.write_str("	")?;
847 text = &text[1..];
848 } else if text.starts_with(' ') {
849 formatter.write_str(" ")?;
850 text = &text[1..];
851 }
852 }
853 state.last_was_text_without_trailing_newline = !text.ends_with('\n');
854 let escaped_text = escape_special_characters(text, state, options);
855 print_text_without_trailing_newline(&escaped_text, formatter, &state)
856 }
857 InlineHtml(text) => {
858 consume_newlines(formatter, state)?;
859 print_text_without_trailing_newline(text, formatter, &state)
860 }
861 Html(text) => {
862 let mut lines = text.split('\n');
863 if let Some(line) = lines.next() {
864 formatter.write_str(line)?;
865 }
866 for line in lines {
867 write_padded_newline(formatter, &state)?;
868 formatter.write_str(line)?;
869 }
870 Ok(())
871 }
872 FootnoteReference(name) => write!(formatter, "[^{name}]"),
873 TaskListMarker(checked) => {
874 let check = if *checked { "x" } else { " " };
875 write!(formatter, "[{check}] ")
876 }
877 InlineMath(text) => write!(formatter, "${text}$"),
878 DisplayMath(text) => write!(formatter, "$${text}$$"),
879 };
880
881 Ok(res?)
882}
883
884impl State<'_> {
885 pub fn finalize<F>(mut self, mut formatter: F) -> Result<Self, Error>
886 where
887 F: fmt::Write,
888 {
889 if self.shortcuts.is_empty() {
890 return Ok(self);
891 }
892
893 formatter.write_str("\n")?;
894 let mut written_shortcuts = HashSet::new();
895 for shortcut in self.shortcuts.drain(..) {
896 if written_shortcuts.contains(&shortcut) {
897 continue;
898 }
899 write!(formatter, "\n[{}", shortcut.0)?;
900 close_link(&shortcut.1, &shortcut.2, &mut formatter, LinkType::Shortcut)?;
901 written_shortcuts.insert(shortcut);
902 }
903 Ok(self)
904 }
905
906 pub fn is_in_code_block(&self) -> bool {
907 self.code_block.is_some()
908 }
909
910 fn set_minimum_newlines_before_start(&mut self, option_value: usize) {
913 if self.newlines_before_start < option_value {
914 self.newlines_before_start = option_value
915 }
916 }
917}
918
919pub fn calculate_code_block_token_count<'a, I, E>(events: I) -> Option<usize>
943where
944 I: IntoIterator<Item = E>,
945 E: Borrow<Event<'a>>,
946{
947 let mut in_codeblock = false;
948 let mut max_token_count = 0;
949
950 let mut token_count = 0;
953 let mut prev_token_char = None;
954 for event in events {
955 match event.borrow() {
956 Event::Start(Tag::CodeBlock(_)) => {
957 in_codeblock = true;
958 }
959 Event::End(TagEnd::CodeBlock) => {
960 in_codeblock = false;
961 prev_token_char = None;
962 }
963 Event::Text(x) if in_codeblock => {
964 for c in x.chars() {
965 let prev_token = prev_token_char.take();
966 if c == '`' || c == '~' {
967 prev_token_char = Some(c);
968 if Some(c) == prev_token {
969 token_count += 1;
970 } else {
971 max_token_count = max_token_count.max(token_count);
972 token_count = 1;
973 }
974 }
975 }
976 }
977 _ => prev_token_char = None,
978 }
979 }
980
981 max_token_count = max_token_count.max(token_count);
982 (max_token_count >= 3).then_some(max_token_count + 1)
983}