use std::fmt::Write as _;
use pulldown_cmark::{Alignment, CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
#[must_use]
pub fn format(input: &str) -> String {
if input.trim().is_empty() {
return String::new();
}
let mut state = FormatterState::new();
let events: Vec<Event<'_>> = Parser::new_ext(input, mk_options()).collect();
let lookahead: Vec<bool> = (0..events.len())
.map(|i| matches!(events.get(i + 1), Some(Event::Start(Tag::List(None)))))
.collect();
let next_text_char: Vec<Option<char>> = (0..events.len())
.map(|i| match events.get(i + 1) {
Some(Event::Text(t)) => t.chars().next(),
_ => None,
})
.collect();
for ((event, next_is_ul), next_char) in events.into_iter().zip(lookahead).zip(next_text_char) {
state.next_is_unordered_list = next_is_ul;
state.next_text_char = next_char;
state.process(event);
}
state.finish()
}
fn mk_options() -> Options {
Options::ENABLE_TABLES
| Options::ENABLE_FOOTNOTES
| Options::ENABLE_STRIKETHROUGH
| Options::ENABLE_TASKLISTS
| Options::ENABLE_HEADING_ATTRIBUTES
}
#[allow(clippy::struct_excessive_bools)] struct FormatterState {
out: String,
needs_blank: bool,
list_depth: usize,
list_starts: Vec<Option<u64>>,
in_tight_item: bool,
bq_depth: usize,
inline: String,
in_code_block: bool,
code_block_indent: String,
list_item_widths: Vec<usize>,
link_stack: Vec<(String, String)>,
next_is_unordered_list: bool,
next_text_char: Option<char>,
table_alignments: Vec<Alignment>,
table_head_cells: Vec<String>,
table_data_rows: Vec<Vec<String>>,
current_row_cells: Vec<String>,
in_table_head: bool,
}
impl FormatterState {
fn new() -> Self {
Self {
out: String::new(),
needs_blank: false,
list_depth: 0,
list_starts: Vec::new(),
in_tight_item: false,
bq_depth: 0,
inline: String::new(),
in_code_block: false,
code_block_indent: String::new(),
list_item_widths: Vec::new(),
link_stack: Vec::new(),
next_is_unordered_list: false,
next_text_char: None,
table_alignments: Vec::new(),
table_head_cells: Vec::new(),
table_data_rows: Vec::new(),
current_row_cells: Vec::new(),
in_table_head: false,
}
}
fn process(&mut self, event: Event<'_>) {
match event {
Event::Start(tag) => self.on_start(tag),
Event::End(tag) => self.on_end(tag),
Event::Text(t) => self.on_text(&t),
Event::Code(c) => self.emit_inline_code(&c),
Event::Html(h) => {
self.out.push_str(&h);
}
Event::InlineHtml(h) => {
self.inline.push_str(&h);
}
Event::SoftBreak => {
self.inline.push('\n');
}
Event::HardBreak => {
self.inline.push_str("\\\n");
}
Event::Rule => {
self.emit_blank_if_needed();
self.write_bq_prefix();
self.out.push_str("---\n");
self.needs_blank = true;
}
Event::FootnoteReference(label) => {
write!(self.inline, "[^{label}]").expect("writing to String is infallible");
}
Event::TaskListMarker(checked) => {
if checked {
self.inline.push_str("[x] ");
} else {
self.inline.push_str("[ ] ");
}
}
_ => {}
}
}
#[allow(clippy::too_many_lines)] fn on_start(&mut self, tag: Tag<'_>) {
match tag {
Tag::Paragraph => {
if self.list_depth == 0 {
self.emit_blank_if_needed();
}
self.in_tight_item = false;
}
Tag::Heading { .. } => {
self.emit_blank_if_needed();
}
Tag::CodeBlock(kind) => {
self.emit_blank_if_needed();
let lang = match kind {
CodeBlockKind::Fenced(lang) => lang.into_string().replace('\\', "\\\\"),
CodeBlockKind::Indented => String::new(),
};
let fence_indent = self.list_continuation_prefix();
let content_indent = if self.in_tight_item {
let marker_width = self.list_item_widths.last().copied().unwrap_or(0);
" ".repeat(marker_width + fence_indent.len())
} else {
fence_indent.clone()
};
let was_tight = self.in_tight_item;
self.in_tight_item = false;
self.code_block_indent = content_indent;
if !was_tight {
self.write_bq_prefix();
}
self.out.push_str(&fence_indent);
self.out.push_str("```");
self.out.push_str(&lang);
self.out.push('\n');
self.in_code_block = true;
}
Tag::List(start) => {
self.list_item_widths.push(0);
if self.list_depth == 0 {
self.emit_blank_if_needed();
} else {
self.needs_blank = false;
if self.in_tight_item && !self.inline.is_empty() {
let text = std::mem::take(&mut self.inline);
let prefix = " ".repeat(self.list_depth);
self.flush_inline_text(&text, &prefix);
self.in_tight_item = false;
} else if self.in_tight_item {
self.out.push('\n');
self.in_tight_item = false;
}
}
self.list_depth += 1;
self.list_starts.push(start.map(|_| 1u64));
}
Tag::Item => {
if self.list_depth > 0 {
self.emit_blank_if_needed();
}
self.in_tight_item = true;
let indent = " ".repeat(self.list_depth.saturating_sub(1));
let marker = match self.list_starts.last_mut() {
Some(Some(n)) => {
let s = format!("{indent}{n}. ");
*n += 1;
s
}
_ => format!("{indent}- "),
};
if let Some(w) = self.list_item_widths.last_mut() {
*w = marker.len();
}
self.write_bq_prefix();
self.out.push_str(&marker);
}
Tag::Emphasis => self.inline.push('*'),
Tag::Strong => self.inline.push_str("**"),
Tag::Strikethrough => self.inline.push_str("~~"),
Tag::Link {
dest_url, title, ..
} => {
self.link_stack
.push((dest_url.into_string(), title.into_string()));
self.inline.push('[');
}
Tag::Image {
dest_url, title, ..
} => {
self.link_stack
.push((dest_url.into_string(), title.into_string()));
self.inline.push_str("![");
}
Tag::HtmlBlock => {
self.emit_blank_if_needed();
}
Tag::BlockQuote(_) => {
self.emit_blank_if_needed();
self.bq_depth += 1;
}
Tag::FootnoteDefinition(label) => {
self.emit_blank_if_needed();
self.write_bq_prefix();
write!(self.out, "[^{label}]: ").expect("writing to String is infallible");
}
Tag::Table(alignments) => {
self.emit_blank_if_needed();
self.table_alignments.clone_from(&alignments);
self.table_head_cells = Vec::new();
self.table_data_rows = Vec::new();
self.current_row_cells = Vec::new();
self.in_table_head = false;
}
Tag::TableHead => {
self.in_table_head = true;
}
Tag::TableRow => {
self.current_row_cells = Vec::new();
}
_ => {}
}
}
#[allow(clippy::too_many_lines)] fn on_end(&mut self, tag: TagEnd) {
match tag {
TagEnd::Paragraph => {
let text = std::mem::take(&mut self.inline);
if !text.trim().is_empty() {
if self.list_depth == 0 {
self.write_bq_prefix();
}
let prefix = " ".repeat(self.list_depth);
self.flush_inline_text(&text, &prefix);
self.needs_blank = true;
}
self.in_tight_item = false;
}
TagEnd::Heading(level) => {
let text = std::mem::take(&mut self.inline);
let hashes = "#".repeat(level as usize);
self.write_bq_prefix();
let heading_raw = collapse_heading_breaks(&text);
let heading_text = heading_raw.trim();
writeln!(self.out, "{hashes} {heading_text}").expect("writing to String is infallible");
self.needs_blank = true;
}
TagEnd::CodeBlock => {
if !self.out.ends_with('\n') {
self.out.push('\n');
}
self.write_bq_prefix();
self.out.push_str(&self.code_block_indent.clone());
self.out.push_str("```\n");
self.in_code_block = false;
self.code_block_indent = String::new();
self.needs_blank = true;
}
TagEnd::List(_) => {
self.list_depth -= 1;
self.list_starts.pop();
self.list_item_widths.pop();
if self.list_depth == 0 {
if self.next_is_unordered_list {
self.needs_blank = false;
self.out.push_str("\n<!---->\n");
self.needs_blank = true;
} else {
self.needs_blank = true;
}
}
}
TagEnd::Item
if self.in_tight_item => {
let text = std::mem::take(&mut self.inline);
if text.is_empty() {
self.out.push('\n');
} else {
let prefix = " ".repeat(self.list_depth);
self.flush_inline_text(&text, &prefix);
}
self.in_tight_item = false;
}
TagEnd::Emphasis => self.inline.push('*'),
TagEnd::Strong => self.inline.push_str("**"),
TagEnd::Strikethrough => self.inline.push_str("~~"),
TagEnd::Link | TagEnd::Image => {
if let Some((dest, title)) = self.link_stack.pop() {
if title.is_empty() {
write!(self.inline, "]({dest})").expect("writing to String is infallible");
} else {
write!(self.inline, "]({dest} \"{title}\")").expect("writing to String is infallible");
}
}
}
TagEnd::HtmlBlock => {
if !self.out.ends_with('\n') {
self.out.push('\n');
}
self.needs_blank = true;
}
TagEnd::BlockQuote(_) => {
self.bq_depth -= 1;
self.needs_blank = true;
}
TagEnd::FootnoteDefinition => {
let text = std::mem::take(&mut self.inline);
self.flush_inline_text(&text, "");
self.needs_blank = true;
}
TagEnd::TableCell => {
let cell = std::mem::take(&mut self.inline);
self.current_row_cells.push(cell);
}
TagEnd::TableHead => {
if self.table_head_cells.is_empty() {
self.table_head_cells = std::mem::take(&mut self.current_row_cells);
}
self.in_table_head = false;
}
TagEnd::TableRow => {
let row = std::mem::take(&mut self.current_row_cells);
if self.in_table_head {
self.table_head_cells = row;
} else {
self.table_data_rows.push(row);
}
}
TagEnd::Table => {
let head = std::mem::take(&mut self.table_head_cells);
let rows = std::mem::take(&mut self.table_data_rows);
let aligns = std::mem::take(&mut self.table_alignments);
self.write_bq_prefix();
self.out.push_str("| ");
self.out.push_str(&head.join(" | "));
self.out.push_str(" |\n");
self.write_bq_prefix();
self.out.push_str("| ");
let seps: Vec<&str> = aligns
.iter()
.map(|a| match a {
Alignment::Left => ":---",
Alignment::Right => "---:",
Alignment::Center => ":---:",
Alignment::None => "---",
})
.collect();
self.out.push_str(&seps.join(" | "));
self.out.push_str(" |\n");
for row in rows {
self.write_bq_prefix();
self.out.push_str("| ");
self.out.push_str(&row.join(" | "));
self.out.push_str(" |\n");
}
self.needs_blank = true;
}
_ => {}
}
}
fn on_text(&mut self, text: &str) {
if self.in_code_block {
let bq = "> ".repeat(self.bq_depth);
if bq.is_empty() && self.code_block_indent.is_empty() {
self.out.push_str(text);
} else {
for line in text.split_inclusive('\n') {
self.out.push_str(&bq);
self.out.push_str(&self.code_block_indent);
self.out.push_str(line);
}
}
} else {
let text = &*text.replace("\r\n", "\n").replace('\r', "\n");
let prev_inline_char = self.inline.chars().next_back();
let chars: Vec<char> = text.chars().collect();
let mut s = String::with_capacity(text.len() + 4);
for (i, &ch) in chars.iter().enumerate() {
match ch {
'\\' => s.push_str("\\\\"),
'`' => s.push_str("\\`"),
'<' => s.push_str("\\<"),
'_' | '~' => {
let prev = if i > 0 {
chars.get(i - 1).copied()
} else {
prev_inline_char
};
let next = chars.get(i + 1).copied().or(if i + 1 == chars.len() {
self.next_text_char
} else {
None
});
if prev.is_some_and(char::is_alphanumeric)
&& next.is_some_and(char::is_alphanumeric)
{
s.push(ch);
} else {
s.push('\\');
s.push(ch);
}
}
_ => s.push(ch),
}
}
self.inline.push_str(&s);
}
}
fn emit_inline_code(&mut self, code: &str) {
let max_run = code.chars().fold((0usize, 0usize), |(max, cur), ch| {
if ch == '`' {
(max.max(cur + 1), cur + 1)
} else {
(max, 0)
}
});
let delim = "`".repeat(max_run.0 + 1);
let needs_space = code.starts_with('`') || code.ends_with('`');
self.inline.push_str(&delim);
if needs_space {
self.inline.push(' ');
}
self.inline.push_str(code);
if needs_space {
self.inline.push(' ');
}
self.inline.push_str(&delim);
}
fn list_continuation_prefix(&self) -> String {
" ".repeat(self.list_item_widths.last().copied().unwrap_or(0))
}
fn emit_blank_if_needed(&mut self) {
if self.needs_blank && !self.out.is_empty() {
if self.bq_depth > 0 {
self.out.push_str(&">".repeat(self.bq_depth));
}
self.out.push('\n');
}
self.needs_blank = false;
}
fn write_bq_prefix(&mut self) {
self.out.push_str(&"> ".repeat(self.bq_depth));
}
fn flush_inline_text(&mut self, text: &str, continuation_prefix: &str) {
let text = {
let s = text.trim_end_matches(|c: char| c != '\n' && c.is_whitespace());
if let Some(stripped) = s.strip_suffix('\n') {
let run = stripped.chars().rev().take_while(|&c| c == '\\').count();
if run % 2 == 1 {
&stripped[..stripped.len() - 1]
} else {
text
}
} else {
text
}
};
let bq = "> ".repeat(self.bq_depth);
let mut lines = text.split('\n').peekable();
if let Some(first) = lines.next() {
if self.bq_depth > 0 && (self.out.ends_with('\n') || self.out.is_empty()) {
self.out.push_str(&bq);
}
if needs_line_escape(first, false) {
self.out.push_str(&escape_line(first));
} else {
self.out.push_str(first);
}
self.out.push('\n');
}
while let Some(line) = lines.next() {
if lines.peek().is_none() && line.is_empty() {
break;
}
if line.trim_end().is_empty() {
continue;
}
self.out.push_str(continuation_prefix);
self.out.push_str(&bq);
if needs_line_escape(line, true) {
self.out.push_str(&escape_line(line));
} else {
self.out.push_str(line);
}
self.out.push('\n');
}
}
fn finish(mut self) -> String {
let s = std::mem::take(&mut self.out);
let mut result: Vec<&str> = Vec::new();
let mut prev_blank = false;
for line in s.lines() {
let line = line.trim_end();
if line.is_empty() {
if !prev_blank {
result.push(line);
}
prev_blank = true;
} else {
result.push(line);
prev_blank = false;
}
}
let start = result
.iter()
.position(|l| !l.is_empty())
.unwrap_or(result.len());
let joined = result
.get(start..)
.expect("start bounded by result.len()")
.join("\n");
let trimmed = joined.trim_end_matches('\n');
if trimmed.is_empty() {
return String::new();
}
format!("{trimmed}\n")
}
}
fn collapse_heading_breaks(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\\' {
let mut run = 1usize;
while chars.peek() == Some(&'\\') {
chars.next();
run += 1;
}
let is_hard_break = run % 2 == 1 && chars.peek() == Some(&'\n');
let content_backslashes = if is_hard_break { run - 1 } else { run };
for _ in 0..content_backslashes {
out.push('\\');
}
} else if ch == '\n' {
out.push(' ');
} else {
out.push(ch);
}
}
out
}
fn escape_line(line: &str) -> String {
let digits_len = line.chars().take_while(char::is_ascii_digit).count();
if digits_len > 0 {
format!("{}\\{}", &line[..digits_len], &line[digits_len..])
} else {
format!("\\{line}")
}
}
fn needs_line_escape(line: &str, is_continuation: bool) -> bool {
let line = line.trim_end();
if line.is_empty() {
return false;
}
if is_continuation {
let trimmed = line.trim_end_matches([' ', '\t']);
if !trimmed.is_empty()
&& (trimmed.chars().all(|c| c == '=') || trimmed.chars().all(|c| c == '-'))
{
return true;
}
}
if is_continuation {
let digits_len = line.chars().take_while(char::is_ascii_digit).count();
if digits_len > 0 {
let rest = &line[digits_len..];
if let Some(after) = rest.strip_prefix(['.', ')'])
&& (after.is_empty() || after.starts_with([' ', '\t']))
&& &line[..digits_len] != "1"
{
return false;
}
}
}
!matches!(
Parser::new_ext(line, mk_options()).next(),
Some(Event::Start(Tag::Paragraph))
)
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_formats_to(input: &str, expected: &str) {
let got = format(input);
assert_eq!(
got, expected,
"format(input) did not match expected.\nInput:\n{input}\nExpected:\n{expected}\nGot:\n{got}"
);
assert_eq!(
format(expected),
expected,
"format(expected) != expected — already-canonical content must be unchanged.\nExpected:\n{expected}"
);
}
#[test]
fn test_empty_input() {
assert_eq!(format(""), "");
assert_eq!(format(" "), "");
assert_eq!(format("\n\n"), "");
}
#[test]
fn test_simple_paragraph() {
assert_eq!(format("Hello, world."), "Hello, world.\n");
}
#[test]
fn test_atx_heading() {
assert_eq!(format("# Heading 1"), "# Heading 1\n");
assert_eq!(format("## Heading 2"), "## Heading 2\n");
assert_eq!(format("###### Heading 6"), "###### Heading 6\n");
}
#[test]
fn test_heading_and_paragraph() {
let input = "# Title\n\nSome text.";
let output = format(input);
assert_eq!(output, "# Title\n\nSome text.\n");
}
#[test]
fn test_multiple_paragraphs() {
let input = "First paragraph.\n\nSecond paragraph.";
let output = format(input);
assert_eq!(output, "First paragraph.\n\nSecond paragraph.\n");
}
#[test]
fn test_fenced_code_block() {
let input = "```rust\nlet x = 1;\n```";
let output = format(input);
assert_eq!(output, "```rust\nlet x = 1;\n```\n");
}
#[test]
fn test_code_block_no_lang() {
let input = "```\ncode here\n```";
let output = format(input);
assert_eq!(output, "```\ncode here\n```\n");
}
#[test]
fn test_unordered_list() {
let input = "- Item 1\n- Item 2\n- Item 3";
let output = format(input);
assert_eq!(output, "- Item 1\n- Item 2\n- Item 3\n");
}
#[test]
fn test_ordered_list() {
let input = "1. First\n2. Second\n3. Third";
let output = format(input);
assert_eq!(output, "1. First\n2. Second\n3. Third\n");
}
#[test]
fn test_ordered_list_all_ones_renumbered() {
assert_formats_to(
"1. First\n1. Second\n1. Third",
"1. First\n2. Second\n3. Third\n",
);
}
#[test]
fn test_ordered_list_non_one_start_renumbered() {
assert_formats_to(
"3. First\n5. Second\n9. Third",
"1. First\n2. Second\n3. Third\n",
);
}
#[test]
fn test_bold_italic_inline() {
assert_eq!(format("**bold** and *italic*"), "**bold** and *italic*\n");
}
#[test]
fn test_inline_code() {
assert_eq!(format("Use `foo()` here."), "Use `foo()` here.\n");
}
#[test]
fn test_link() {
let input = "[text](https://example.com)";
let output = format(input);
assert_eq!(output, "[text](https://example.com)\n");
}
#[test]
fn test_image() {
let input = "";
let output = format(input);
assert_eq!(output, "\n");
}
#[test]
fn test_blank_line_between_heading_and_code() {
let input = "# Heading\n\n```\ncode\n```";
let output = format(input);
assert_eq!(output, "# Heading\n\n```\ncode\n```\n");
}
#[test]
fn test_blank_line_between_list_and_paragraph() {
let input = "- item\n\nAfter list.";
let output = format(input);
assert_eq!(output, "- item\n\nAfter list.\n");
}
#[test]
fn test_nested_list() {
let input = "- Item 1\n - Nested\n- Item 2";
let output = format(input);
assert_eq!(output, "- Item 1\n - Nested\n- Item 2\n");
}
#[test]
fn test_strikethrough() {
assert_eq!(format("~~struck~~"), "~~struck~~\n");
}
#[test]
fn test_setext_headings_to_atx() {
assert_formats_to("Heading 1\n=========", "# Heading 1\n");
assert_formats_to("Heading 2\n---------", "## Heading 2\n");
}
#[test]
fn test_setext_heading_hard_break_not_leaked() {
assert_formats_to("\\\r¡\r=", "# ¡\n");
}
#[test]
fn test_closed_atx_stripped() {
assert_formats_to("## Heading ##", "## Heading\n");
assert_formats_to("# Title #", "# Title\n");
}
#[test]
fn test_multiple_spaces_after_hash_collapsed() {
assert_formats_to("# Heading", "# Heading\n");
assert_formats_to("## Wide", "## Wide\n");
}
#[test]
fn test_heading_literal_backslash_idempotent() {
assert_formats_to("#\t0\\\ra", "# 0\\\\ a\n");
assert_formats_to("# a\\b", "# a\\\\b\n");
}
#[test]
fn test_heading_hard_break_collapses() {
assert_formats_to("# a\\\nb", "# a\\\\\n\nb\n");
}
#[test]
fn test_intraword_tilde_across_event_split() {
assert_formats_to("Ⓐ~A", "Ⓐ~A\n");
assert_formats_to("Ⓐ~", "Ⓐ\\~\n");
assert_formats_to("~A", "\\~A\n");
}
#[test]
fn test_literal_angle_bracket_escaped() {
assert_formats_to("<#\\@a>", "\\<#@a>\n");
assert_formats_to("x<y", "x\\<y\n");
assert_formats_to(
"<https://example.com>",
"[https://example.com](https://example.com)\n",
);
assert_formats_to("<div>hi</div>", "<div>hi</div>\n");
}
#[test]
fn test_collapse_heading_breaks_unit() {
assert_eq!(collapse_heading_breaks("a\nb"), "a b");
assert_eq!(collapse_heading_breaks("a\\\nb"), "a b");
assert_eq!(collapse_heading_breaks("a\\\\\nb"), "a\\\\ b");
assert_eq!(collapse_heading_breaks("a\\\\b"), "a\\\\b");
}
#[test]
fn test_multiple_blank_lines_collapsed() {
assert_formats_to("First.\n\n\n\nSecond.", "First.\n\nSecond.\n");
}
#[test]
fn test_list_markers_to_dash() {
assert_formats_to("* Item 1\n* Item 2", "- Item 1\n- Item 2\n");
assert_formats_to("+ Item 1\n+ Item 2", "- Item 1\n- Item 2\n");
}
#[test]
fn test_emphasis_to_asterisk() {
assert_formats_to("_italic_", "*italic*\n");
assert_formats_to("__bold__", "**bold**\n");
}
#[test]
fn test_tilde_fence_to_backtick() {
assert_formats_to("~~~rust\ncode\n~~~", "```rust\ncode\n```\n");
assert_formats_to("~~~\ncode\n~~~", "```\ncode\n```\n");
}
#[test]
fn test_all_hr_styles_to_dashes() {
assert_formats_to("***", "---\n");
assert_formats_to("___", "---\n");
assert_formats_to("* * *", "---\n");
assert_formats_to("- - -", "---\n");
assert_formats_to("_ _ _", "---\n");
}
#[test]
fn test_hard_line_break_becomes_backslash() {
assert_formats_to("foo \nbar", "foo\\\nbar\n");
}
#[test]
fn test_simple_table() {
let input = "| A | B |\n| --- | --- |\n| 1 | 2 |\n| 3 | 4 |\n";
let output = format(input);
assert_eq!(output, "| A | B |\n| --- | --- |\n| 1 | 2 |\n| 3 | 4 |\n");
}
#[test]
fn test_table_no_leading_pipes() {
assert_formats_to(
"A | B\n--- | ---\n1 | 2\n",
"| A | B |\n| --- | --- |\n| 1 | 2 |\n",
);
}
#[test]
fn test_table_idempotent() {
let input = "| A | B |\n| --- | --- |\n| 1 | 2 |\n";
let once = format(input);
let twice = format(&once);
assert_eq!(once, twice);
}
#[test]
fn test_table_with_inline_formatting() {
let input = "| **bold** | `code` |\n| --- | --- |\n| *em* | plain |\n";
let output = format(input);
assert_eq!(
output,
"| **bold** | `code` |\n| --- | --- |\n| *em* | plain |\n"
);
}
#[test]
fn test_table_followed_by_paragraph() {
let input = "| A | B |\n| --- | --- |\n| 1 | 2 |\n\nSome text.\n";
let output = format(input);
assert_eq!(
output,
"| A | B |\n| --- | --- |\n| 1 | 2 |\n\nSome text.\n"
);
}
#[test]
fn test_escaped_list_marker_in_paragraph() {
let once = format("\\*");
let twice = format(&once);
assert_eq!(once, twice, "idempotency: escaped asterisk");
let once = format("\\-");
let twice = format(&once);
assert_eq!(once, twice, "idempotency: escaped dash");
}
#[test]
fn test_setext_heading_with_leading_vt() {
let once = format("\u{b}¡\r=");
let twice = format(&once);
assert_eq!(once, twice, "idempotency: setext heading with leading VT");
}
#[test]
fn test_escaped_heading_in_paragraph() {
let once = format("\\# not a heading");
let twice = format(&once);
assert_eq!(once, twice, "idempotency: escaped hash");
}
#[test]
fn test_ordered_list_with_code_block() {
let canonical = "1. **Enable rule:**\n\n ```toml\n enabled = false\n ```\n\n2. **Another item:**\n\n ```toml\n line_length = 100\n ```\n";
assert_formats_to(
"1. **Enable rule:**\n\n ```toml\n enabled = false\n ```\n\n1. **Another item:**\n\n ```toml\n line_length = 100\n ```\n",
canonical,
);
}
#[test]
fn test_unordered_list_with_code_block() {
let canonical = "- **Item:**\n\n ```toml\n enabled = false\n ```\n";
assert_formats_to(canonical, canonical);
}
#[test]
fn test_tight_list_item_code_block_only() {
let canonical = "- ```\n ¡\n ```\n";
assert_formats_to(canonical, canonical);
}
#[test]
fn test_setext_underline_in_paragraph_continuation() {
let once = format("a\r\t=");
let twice = format(&once);
assert_eq!(
once, twice,
"idempotency: setext-underline-like continuation"
);
let once = format("a\r\t--");
let twice = format(&once);
assert_eq!(once, twice, "idempotency: setext h2 continuation");
}
#[test]
fn test_backtick_in_text_escaped() {
let once = format("\\`\r`");
let twice = format(&once);
assert_eq!(once, twice, "idempotency: lone backticks in text");
}
#[test]
fn test_empty_list_items_idempotent() {
let once = format("*\r*\t");
let twice = format(&once);
assert_eq!(once, twice, "idempotency: empty tight list items");
}
#[test]
fn test_html_block_with_cr_content_idempotent() {
let once = format("<?>\r\\");
let twice = format(&once);
assert_eq!(once, twice, "idempotency: HTML block with CR content");
}
#[test]
fn test_list_marker_with_trailing_unicode_whitespace_idempotent() {
assert_formats_to("*\u{85}\u{b}", "\\*\n");
}
#[test]
fn test_blockquote_nel_idempotent() {
let once = format(">\u{85}");
let twice = format(&once);
assert_eq!(once, twice, "idempotency: blockquote + NEL");
}
#[test]
fn test_trailing_backslash_in_paragraph_not_doubled() {
assert_formats_to("¡\\\t\r\x0B", "¡\\\\\n");
}
#[test]
fn test_hard_break_followed_by_vt_in_paragraph() {
let once = format("\\\r\u{b}\r¡");
let twice = format(&once);
assert_eq!(once, twice, "idempotency: hard-break + VT continuation");
}
#[test]
fn test_code_fence_info_backslash_idempotent() {
let once = format("```\\\r!");
let twice = format(&once);
assert_eq!(
once, twice,
"idempotency: code fence info string with backslash"
);
}
}