From cac5fae376021bdff619de8315d8947b0c047836 Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Thu, 30 Jun 2022 00:19:18 +0000 Subject: [PATCH] Handling of numbered markdown lists. Fixes issue #5416 --- src/comment.rs | 133 ++++++++++++++++++------ tests/source/itemized-blocks/no_wrap.rs | 8 ++ tests/source/itemized-blocks/wrap.rs | 8 ++ tests/target/itemized-blocks/no_wrap.rs | 8 ++ tests/target/itemized-blocks/wrap.rs | 12 +++ 5 files changed, 135 insertions(+), 34 deletions(-) diff --git a/src/comment.rs b/src/comment.rs index 4d565afc1e0..873be95378f 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -432,7 +432,9 @@ impl CodeBlockAttribute { /// Block that is formatted as an item. /// -/// An item starts with either a star `*` a dash `-` or a greater-than `>`. +/// An item starts with either a star `*`, a dash `-`, a greater-than `>`, +/// a letter `a.` or `a)`, or a number `123.` or `123)`. +/// /// Different level of indentation are handled by shrinking the shape accordingly. struct ItemizedBlock { /// the lines that are identified as part of an itemized block @@ -446,36 +448,53 @@ struct ItemizedBlock { } impl ItemizedBlock { - /// Returns `true` if the line is formatted as an item - fn is_itemized_line(line: &str) -> bool { - let trimmed = line.trim_start(); - trimmed.starts_with("* ") || trimmed.starts_with("- ") || trimmed.starts_with("> ") - } - - /// Creates a new ItemizedBlock described with the given line. - /// The `is_itemized_line` needs to be called first. - fn new(line: &str) -> ItemizedBlock { - let space_to_sigil = line.chars().take_while(|c| c.is_whitespace()).count(); - // +2 = '* ', which will add the appropriate amount of whitespace to keep itemized - // content formatted correctly. - let mut indent = space_to_sigil + 2; - let mut line_start = " ".repeat(indent); - - // Markdown blockquote start with a "> " - if line.trim_start().starts_with(">") { - // remove the original +2 indent because there might be multiple nested block quotes - // and it's easier to reason about the final indent by just taking the length - // of th new line_start. We update the indent because it effects the max width - // of each formatted line. - line_start = itemized_block_quote_start(line, line_start, 2); - indent = line_start.len(); + /// Returns the sigil's (e.g. "- ", "* ", or "1. ") length or None of there is no sigil. + fn get_sigil_length(trimmed: &str) -> Option { + if trimmed.starts_with("* ") || trimmed.starts_with("- ") || trimmed.starts_with("> ") { + return Some(2); } - ItemizedBlock { - lines: vec![line[indent..].to_string()], - indent, - opener: line[..indent].to_string(), - line_start, + + for suffix in [". ", ") "] { + if let Some((prefix, _)) = trimmed.split_once(suffix) { + for prefix_predicate in [ + char::is_ascii_digit, + char::is_ascii_lowercase, + char::is_ascii_uppercase, + ] { + if prefix.chars().all(|c| prefix_predicate(&c)) { + return Some(prefix.len() + suffix.len()); + } + } + } } + + None + } + + /// Creates a new ItemizedBlock described with the given `line` + /// or None if `line` doesn't start an item. + fn new(line: &str) -> Option { + ItemizedBlock::get_sigil_length(line.trim_start()).map(|sigil_length| { + let space_to_sigil = line.chars().take_while(|c| c.is_whitespace()).count(); + let mut indent = space_to_sigil + sigil_length; + let mut line_start = " ".repeat(indent); + + // Markdown blockquote start with a "> " + if line.trim_start().starts_with(">") { + // remove the original +2 indent because there might be multiple nested block quotes + // and it's easier to reason about the final indent by just taking the length + // of the new line_start. We update the indent because it effects the max width + // of each formatted line. + line_start = itemized_block_quote_start(line, line_start, 2); + indent = line_start.len(); + } + ItemizedBlock { + lines: vec![line[indent..].to_string()], + indent, + opener: line[..indent].to_string(), + line_start, + } + }) } /// Returns a `StringFormat` used for formatting the content of an item. @@ -494,7 +513,7 @@ impl ItemizedBlock { /// Returns `true` if the line is part of the current itemized block. /// If it is, then it is added to the internal lines list. fn add_line(&mut self, line: &str) -> bool { - if !ItemizedBlock::is_itemized_line(line) + if ItemizedBlock::get_sigil_length(line.trim_start()).is_none() && self.indent <= line.chars().take_while(|c| c.is_whitespace()).count() { self.lines.push(line.to_string()); @@ -765,10 +784,11 @@ impl<'a> CommentRewrite<'a> { self.item_block = None; if let Some(stripped) = line.strip_prefix("```") { self.code_block_attr = Some(CodeBlockAttribute::new(stripped)) - } else if self.fmt.config.wrap_comments() && ItemizedBlock::is_itemized_line(line) { - let ib = ItemizedBlock::new(line); - self.item_block = Some(ib); - return false; + } else if self.fmt.config.wrap_comments() { + if let Some(ib) = ItemizedBlock::new(line) { + self.item_block = Some(ib); + return false; + } } if self.result == self.opener { @@ -2004,4 +2024,49 @@ fn main() { "#; assert_eq!(s, filter_normal_code(s_with_comment)); } + + #[test] + fn test_itemized_block_first_line_handling() { + fn run_test( + test_input: &str, + expected_line: &str, + expected_indent: usize, + expected_opener: &str, + expected_line_start: &str, + ) { + let block = ItemizedBlock::new(test_input).unwrap(); + assert_eq!(1, block.lines.len(), "test_input: {:?}", test_input); + assert_eq!( + expected_line, &block.lines[0], + "test_input: {:?}", + test_input + ); + assert_eq!( + expected_indent, block.indent, + "test_input: {:?}", + test_input + ); + assert_eq!( + expected_opener, &block.opener, + "test_input: {:?}", + test_input + ); + assert_eq!( + expected_line_start, &block.line_start, + "test_input: {:?}", + test_input + ); + } + + run_test("- foo", "foo", 2, "- ", " "); + run_test("* foo", "foo", 2, "* ", " "); + run_test("> foo", "foo", 2, "> ", "> "); + + run_test("1. foo", "foo", 3, "1. ", " "); + run_test("a) foo", "foo", 3, "a) ", " "); + run_test("A) foo", "foo", 3, "A) ", " "); + + run_test("123. foo", "foo", 5, "123. ", " "); + run_test(" - foo", "foo", 6, " - ", " "); + } } diff --git a/tests/source/itemized-blocks/no_wrap.rs b/tests/source/itemized-blocks/no_wrap.rs index a7b6a10a010..cdcea3e57e8 100644 --- a/tests/source/itemized-blocks/no_wrap.rs +++ b/tests/source/itemized-blocks/no_wrap.rs @@ -13,6 +13,14 @@ //! - when the log level is info, the level name is green and the rest of the line is white //! - when the log level is debug, the whole line is white //! - when the log level is trace, the whole line is gray ("bright black") +//! +//! This is a numbered list: +//! 1. Long long long long long long long long long long long long long long long long long line +//! 2. Another very long long long long long long long long long long long long long long long line +//! 3. Nested list +//! A. foo +//! B. bar +//! 4. Last item /// All the parameters ***except for `from_theater`*** should be inserted as sent by the remote /// theater, i.e., as passed to [`Theater::send`] on the remote actor: diff --git a/tests/source/itemized-blocks/wrap.rs b/tests/source/itemized-blocks/wrap.rs index 955cc698b79..d4424b3f97a 100644 --- a/tests/source/itemized-blocks/wrap.rs +++ b/tests/source/itemized-blocks/wrap.rs @@ -14,6 +14,14 @@ //! - when the log level is info, the level name is green and the rest of the line is white //! - when the log level is debug, the whole line is white //! - when the log level is trace, the whole line is gray ("bright black") +//! +//! This is a numbered list: +//! 1. Long long long long long long long long long long long long long long long long long line +//! 2. Another very long long long long long long long long long long long long long long long line +//! 3. Nested list +//! A. foo +//! B. bar +//! 4. Last item // This example shows how to configure fern to output really nicely colored logs // - when the log level is error, the whole line is red diff --git a/tests/target/itemized-blocks/no_wrap.rs b/tests/target/itemized-blocks/no_wrap.rs index de885638272..b0ff95e0816 100644 --- a/tests/target/itemized-blocks/no_wrap.rs +++ b/tests/target/itemized-blocks/no_wrap.rs @@ -13,6 +13,14 @@ //! - when the log level is info, the level name is green and the rest of the line is white //! - when the log level is debug, the whole line is white //! - when the log level is trace, the whole line is gray ("bright black") +//! +//! This is a numbered list: +//! 1. Long long long long long long long long long long long long long long long long long line +//! 2. Another very long long long long long long long long long long long long long long long line +//! 3. Nested list +//! A. foo +//! B. bar +//! 4. Last item /// All the parameters ***except for `from_theater`*** should be inserted as sent by the remote /// theater, i.e., as passed to [`Theater::send`] on the remote actor: diff --git a/tests/target/itemized-blocks/wrap.rs b/tests/target/itemized-blocks/wrap.rs index a4907303c9e..9616047c3a5 100644 --- a/tests/target/itemized-blocks/wrap.rs +++ b/tests/target/itemized-blocks/wrap.rs @@ -23,6 +23,18 @@ //! is white //! - when the log level is trace, the whole line //! is gray ("bright black") +//! +//! This is a numbered list: +//! 1. Long long long long long long long long +//! long long long long long long long long +//! long line +//! 2. Another very long long long long long long +//! long long long long long long long long +//! long line +//! 3. Nested list +//! A. foo +//! B. bar +//! 4. Last item // This example shows how to configure fern to // output really nicely colored logs