Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 77 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ unicode-width = "0.1.10"

[dev-dependencies]
dprint-development = "0.10.1"
quickcheck = "1.0.3"
quickcheck_macros = "1.1.0"
serde_json = { version = "1.0" }
23 changes: 21 additions & 2 deletions src/generation/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,10 +304,29 @@ fn gen_block_quote(block_quote: &BlockQuote, context: &mut Context) -> PrintItem
context.mark_in_block_quotes(|context, block_quote_count| {
let mut items = PrintItems::new();

let children_items = gen_nodes(&block_quote.children, context);
if children_items.is_empty() {
let newline_count = context.get_new_lines_in_range(block_quote.range.start, block_quote.range.end);
for i in 0..std::cmp::max(1, newline_count) {
if i > 0 {
items.push_signal(Signal::NewLine);
}
// We use a condition here primarily to prevent a parent
// `gen_block_quote` loop from wrapping this string with
// another layer of ">".
items.push_condition(if_true(
"angleBracketIfStartOfLine",
condition_resolvers::is_start_of_line(),
ir_helpers::gen_from_string(&">".repeat(block_quote_count)),
));
}
return items;
}

// add a > for any string that is on the start of a line
// Note: This is extremely hacky
let mut indent_level = 0;
for print_item in gen_nodes(&block_quote.children, context).iter() {
for print_item in children_items.iter() {
match print_item {
PrintItem::String(text) => {
items.push_condition(if_true(
Expand Down Expand Up @@ -488,7 +507,7 @@ fn gen_str(text: &str, context: &mut Context) -> PrintItems {
}

pub fn add_char(&mut self, character: char) {
if character == '\n' || character == ' ' {
if utils::is_commonmark_whitespace(character) {
if self.context.configuration.text_wrap == TextWrap::Maintain && character == '\n' {
self.newline();
} else {
Expand Down
10 changes: 10 additions & 0 deletions src/generation/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ fn is_space_tab_or_newline(c: char) -> bool {
matches!(c, ' ' | '\t' | '\r' | '\n')
}

/// Returns true if the character is a whitespace character according
/// to the CommonMark spec.
///
/// See <https://spec.commonmark.org/0.30/#whitespace-character>
///
/// Note: This is stricter than Rust's `char::is_whitespace`.
pub fn is_commonmark_whitespace(c: char) -> bool {
matches!(c, ' ' | '\t' | '\r' | '\n' | '\u{0b}' | '\u{0c}')
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
77 changes: 77 additions & 0 deletions tests/fuzz_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use dprint_plugin_markdown::configuration::ConfigurationBuilder;
use dprint_plugin_markdown::format_text;
use quickcheck::TestResult;
use quickcheck_macros::quickcheck;

#[quickcheck]
#[ignore] // Due to randomness, this can lead to sporadic errors.
fn check_idempotency(input: String) -> TestResult {
if !input.chars().all(|c| c.is_ascii_graphic() || c == '\n' || c == '\r') {
// Non-graphical characters (like NUL bytes) are known to break
// idempotency.
return TestResult::discard();
}

TestResult::from_bool(is_idempotent(&input))
}

#[test]
fn test_simple() {
assert!(is_idempotent("Lorem ipsum\n\n\nFoo bar"));
}

#[test]
fn test_list() {
assert!(is_idempotent("* Lorem ipsum\n* Foo bar"));
}

#[test]
fn test_tab() {
assert!(is_idempotent("\t"));
}

#[test]
#[should_panic]
fn test_vertical_tab() {
assert!(is_idempotent("*\u{000b}"));
}

#[test]
fn test_nul_tab() {
assert!(is_idempotent("\0\t\0"));
}

#[test]
fn test_paragraph_blockquote() {
assert!(is_idempotent("Lorem ipsum.\n>"));
}

/// Can `input` be format twice and give the same result?
fn is_idempotent(input: &str) -> bool {
let config = ConfigurationBuilder::new().build();

let format = |input| format_text(input, &config, |_, _, _| Ok(None));

let Ok(Some(text1)) = format(input) else {
// Input was already formatted, or we cannot format this input.
return true;
};

match format(&text1) {
Ok(None) => true, // Reached fix point => success.
Ok(Some(text2)) => {
// Second pass changed something => failure.
eprintln!("Input: {input:?}");
eprintln!("Pass 1: {text1:?}");
eprintln!("Pass 2: {text2:?}");
false
}
Err(err) => {
// Second pass failed => failure.
eprintln!("Input: {input:?}");
eprintln!("Pass 1: {text1:?}");
eprintln!("Pass 2: {err}");
false
}
}
}
46 changes: 46 additions & 0 deletions tests/specs/BlockQuotes/Empty.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
!! Empty blockquote
>

[expect]
>

!! Empty blockquote with newline
>
>

[expect]
>
>

!! Empty blockquote with trailing whitespace
>
>

[expect]
>
>

!! Nested empty blockquote
>>
>>

[expect]
>>
>>

!! Nested empty blockquote with space between quotes
> >
> >

[expect]
>>
>>

!! Empty blockquote after paragraph
Paragraph
>

[expect]
Paragraph

>