Handling of numbered markdown lists.

Fixes issue #5416
diff --git a/src/comment.rs b/src/comment.rs
index bc0e877..85918ec 100644
--- a/src/comment.rs
+++ b/src/comment.rs
@@ -432,12 +432,18 @@
 
 /// Block that is formatted as an item.
 ///
-/// An item starts with either a star `*` a dash `-` a greater-than `>` or a plus '+'.
+/// An item starts with either a star `*`, a dash `-`, a greater-than `>`, a plus '+', or a number
+/// `12.` or `34)` (with at most 2 digits). An item represents CommonMark's ["list
+/// items"](https://spec.commonmark.org/0.30/#list-items) and/or ["block
+/// quotes"](https://spec.commonmark.org/0.30/#block-quotes), but note that only a subset of
+/// CommonMark is recognized - see the doc comment of [`ItemizedBlock::get_marker_length`] for more
+/// details.
+///
 /// Different level of indentation are handled by shrinking the shape accordingly.
 struct ItemizedBlock {
     /// the lines that are identified as part of an itemized block
     lines: Vec<String>,
-    /// the number of characters (typically whitespaces) up to the item sigil
+    /// the number of characters (typically whitespaces) up to the item marker
     indent: usize,
     /// the string that marks the start of an item
     opener: String,
@@ -446,37 +452,70 @@
 }
 
 impl ItemizedBlock {
-    /// Returns `true` if the line is formatted as an item
-    fn is_itemized_line(line: &str) -> bool {
-        let trimmed = line.trim_start();
+    /// Checks whether the `trimmed` line includes an item marker. Returns `None` if there is no
+    /// marker. Returns the length of the marker (in bytes) if one is present. Note that the length
+    /// includes the whitespace that follows the marker, for example the marker in `"* list item"`
+    /// has the length of 2.
+    ///
+    /// This function recognizes item markers that correspond to CommonMark's
+    /// ["bullet list marker"](https://spec.commonmark.org/0.30/#bullet-list-marker),
+    /// ["block quote marker"](https://spec.commonmark.org/0.30/#block-quote-marker), and/or
+    /// ["ordered list marker"](https://spec.commonmark.org/0.30/#ordered-list-marker).
+    ///
+    /// Compared to CommonMark specification, the number of digits that are allowed in an ["ordered
+    /// list marker"](https://spec.commonmark.org/0.30/#ordered-list-marker) is more limited (to at
+    /// most 2 digits). Limiting the length of the marker helps reduce the risk of recognizing
+    /// arbitrary numbers as markers. See also
+    /// <https://talk.commonmark.org/t/blank-lines-before-lists-revisited/1990> which gives the
+    /// following example where a number (i.e. "1868") doesn't signify an ordered list:
+    /// ```md
+    /// The Captain died in
+    /// 1868. He wes buried in...
+    /// ```
+    fn get_marker_length(trimmed: &str) -> Option<usize> {
+        // https://spec.commonmark.org/0.30/#bullet-list-marker or
+        // https://spec.commonmark.org/0.30/#block-quote-marker
         let itemized_start = ["* ", "- ", "> ", "+ "];
-        itemized_start.iter().any(|s| trimmed.starts_with(s))
+        if itemized_start.iter().any(|s| trimmed.starts_with(s)) {
+            return Some(2); // All items in `itemized_start` have length 2.
+        }
+
+        // https://spec.commonmark.org/0.30/#ordered-list-marker, where at most 2 digits are
+        // allowed.
+        for suffix in [". ", ") "] {
+            if let Some((prefix, _)) = trimmed.split_once(suffix) {
+                if prefix.len() <= 2 && prefix.chars().all(|c| char::is_ascii_digit(&c)) {
+                    return Some(prefix.len() + suffix.len());
+                }
+            }
+        }
+
+        None // No markers found.
     }
 
-    /// 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;
+    /// Creates a new `ItemizedBlock` described with the given `line`.
+    /// Returns `None` if `line` doesn't start an item.
+    fn new(line: &str) -> Option<ItemizedBlock> {
+        let marker_length = ItemizedBlock::get_marker_length(line.trim_start())?;
+        let space_to_marker = line.chars().take_while(|c| c.is_whitespace()).count();
+        let mut indent = space_to_marker + marker_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 th new line_start. We update the indent because it effects the max width
+            // 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 {
+        Some(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.
@@ -495,7 +534,7 @@
     /// 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_marker_length(line.trim_start()).is_none()
             && self.indent <= line.chars().take_while(|c| c.is_whitespace()).count()
         {
             self.lines.push(line.to_string());
@@ -766,10 +805,11 @@
         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 {
@@ -2020,4 +2060,96 @@
 "#;
         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("12. foo", "foo", 4, "12. ", "    ");
+        run_test("1) foo", "foo", 3, "1) ", "   ");
+        run_test("12) foo", "foo", 4, "12) ", "    ");
+
+        run_test("    - foo", "foo", 6, "    - ", "      ");
+
+        // https://spec.commonmark.org/0.30 says: "A start number may begin with 0s":
+        run_test("0. foo", "foo", 3, "0. ", "   ");
+        run_test("01. foo", "foo", 4, "01. ", "    ");
+    }
+
+    #[test]
+    fn test_itemized_block_nonobvious_markers_are_rejected() {
+        let test_inputs = vec![
+            // Non-numeric item markers (e.g. `a.` or `iv.`) are not allowed by
+            // https://spec.commonmark.org/0.30/#ordered-list-marker. We also note that allowing
+            // them would risk misidentifying regular words as item markers. See also the
+            // discussion in https://talk.commonmark.org/t/blank-lines-before-lists-revisited/1990
+            "word.  rest of the paragraph.",
+            "a.  maybe this is a list item?  maybe not?",
+            "iv.  maybe this is a list item?  maybe not?",
+            // Numbers with 3 or more digits are not recognized as item markers, to avoid
+            // formatting the following example as a list:
+            //
+            // ```
+            // The Captain died in
+            // 1868. He was buried in...
+            // ```
+            "123.  only 2-digit numbers are recognized as item markers.",
+            // Parens:
+            "123)  giving some coverage to parens as well.",
+            "a)  giving some coverage to parens as well.",
+            // https://spec.commonmark.org/0.30 says that "at least one space or tab is needed
+            // between the list marker and any following content":
+            "1.Not a list item.",
+            "1.2.3. Not a list item.",
+            "1)Not a list item.",
+            "-Not a list item.",
+            "+Not a list item.",
+            "+1 not a list item.",
+            // https://spec.commonmark.org/0.30 says: "A start number may not be negative":
+            "-1. Not a list item.",
+            "-1 Not a list item.",
+        ];
+        for line in test_inputs.iter() {
+            let maybe_block = ItemizedBlock::new(line);
+            assert!(
+                maybe_block.is_none(),
+                "The following line shouldn't be classified as a list item: {}",
+                line
+            );
+        }
+    }
 }
diff --git a/tests/source/itemized-blocks/no_wrap.rs b/tests/source/itemized-blocks/no_wrap.rs
index a7b6a10..e5699e7 100644
--- a/tests/source/itemized-blocks/no_wrap.rs
+++ b/tests/source/itemized-blocks/no_wrap.rs
@@ -1,7 +1,7 @@
 // rustfmt-normalize_comments: true
 // rustfmt-format_code_in_doc_comments: true
 
-//! This is a list:
+//! This is an itemized markdown list (see also issue #3224):
 //!  * Outer
 //!  * Outer
 //!   * Inner
@@ -13,6 +13,40 @@
 //! - 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 markdown list (see also issue #5416):
+//! 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
+//!    1. 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 line
+//! 4. Last item
+//!
+//! Using the ')' instead of '.' character after the number:
+//! 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
+//!
+//! Deep list that mixes various bullet and number formats:
+//! 1. First level with a long long long long long long long long long long long long long long
+//!    long long long line
+//! 2. First level with another very long long long long long long long long long long long long
+//!    long long long line
+//!     * Second level with a long long long long long long long long long long long long long
+//!       long long long line
+//!     * Second level with another very long long long long long long long long long long long
+//!       long long long line
+//!         1) Third level with a long long long long long long long long long long long long long
+//!            long long long line
+//!         2) Third level with another very long long long long long long long long long long
+//!            long long long long line
+//!             - Forth level with a long long long long long long long long long long long long
+//!               long long long long line
+//!             - Forth level with another very long long long long long long long long long long
+//!               long long long long line
+//!         3) One more item at the third level
+//!         4) Last item of the third level
+//!     * Last item of second level
+//! 3. Last item of first level
 
 /// 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 955cc69..768461a 100644
--- a/tests/source/itemized-blocks/wrap.rs
+++ b/tests/source/itemized-blocks/wrap.rs
@@ -2,7 +2,7 @@
 // rustfmt-format_code_in_doc_comments: true
 // rustfmt-max_width: 50
 
-//! This is a list:
+//! This is an itemized markdown list (see also issue #3224):
 //!  * Outer
 //!  * Outer
 //!   * Inner
@@ -14,6 +14,40 @@
 //! - 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 markdown list (see also issue #5416):
+//! 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
+//!    1. 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 line
+//! 4. Last item
+//!
+//! Using the ')' instead of '.' character after the number:
+//! 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
+//!
+//! Deep list that mixes various bullet and number formats:
+//! 1. First level with a long long long long long long long long long long long long long long
+//!    long long long line
+//! 2. First level with another very long long long long long long long long long long long long
+//!    long long long line
+//!     * Second level with a long long long long long long long long long long long long long
+//!       long long long line
+//!     * Second level with another very long long long long long long long long long long long
+//!       long long long line
+//!         1) Third level with a long long long long long long long long long long long long long
+//!            long long long line
+//!         2) Third level with another very long long long long long long long long long long
+//!            long long long long line
+//!             - Forth level with a long long long long long long long long long long long long
+//!               long long long long line
+//!             - Forth level with another very long long long long long long long long long long
+//!               long long long long line
+//!         3) One more item at the third level
+//!         4) Last item of the third level
+//!     * Last item of second level
+//! 3. Last item of first level
 
 // 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 de88563..86818b4 100644
--- a/tests/target/itemized-blocks/no_wrap.rs
+++ b/tests/target/itemized-blocks/no_wrap.rs
@@ -1,7 +1,7 @@
 // rustfmt-normalize_comments: true
 // rustfmt-format_code_in_doc_comments: true
 
-//! This is a list:
+//! This is an itemized markdown list (see also issue #3224):
 //!  * Outer
 //!  * Outer
 //!   * Inner
@@ -13,6 +13,40 @@
 //! - 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 markdown list (see also issue #5416):
+//! 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
+//!    1. 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 line
+//! 4. Last item
+//!
+//! Using the ')' instead of '.' character after the number:
+//! 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
+//!
+//! Deep list that mixes various bullet and number formats:
+//! 1. First level with a long long long long long long long long long long long long long long
+//!    long long long line
+//! 2. First level with another very long long long long long long long long long long long long
+//!    long long long line
+//!     * Second level with a long long long long long long long long long long long long long
+//!       long long long line
+//!     * Second level with another very long long long long long long long long long long long
+//!       long long long line
+//!         1) Third level with a long long long long long long long long long long long long long
+//!            long long long line
+//!         2) Third level with another very long long long long long long long long long long
+//!            long long long long line
+//!             - Forth level with a long long long long long long long long long long long long
+//!               long long long long line
+//!             - Forth level with another very long long long long long long long long long long
+//!               long long long long line
+//!         3) One more item at the third level
+//!         4) Last item of the third level
+//!     * Last item of second level
+//! 3. Last item of first level
 
 /// 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 a490730..4826590 100644
--- a/tests/target/itemized-blocks/wrap.rs
+++ b/tests/target/itemized-blocks/wrap.rs
@@ -2,7 +2,8 @@
 // rustfmt-format_code_in_doc_comments: true
 // rustfmt-max_width: 50
 
-//! This is a list:
+//! This is an itemized markdown list (see also
+//! issue #3224):
 //!  * Outer
 //!  * Outer
 //!   * Inner
@@ -23,6 +24,65 @@
 //!   is white
 //! - when the log level is trace, the whole line
 //!   is gray ("bright black")
+//!
+//! This is a numbered markdown list (see also
+//! issue #5416):
+//! 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
+//!    1. 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 line
+//! 4. Last item
+//!
+//! Using the ')' instead of '.' character after
+//! the number:
+//! 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
+//!
+//! Deep list that mixes various bullet and number
+//! formats:
+//! 1. First level with a long long long long long
+//!    long long long long long long long long
+//!    long long long long line
+//! 2. First level with another very long long
+//!    long long long long long long long long
+//!    long long long long long line
+//!     * Second level with a long long long long
+//!       long long long long long long long long
+//!       long long long long line
+//!     * Second level with another very long long
+//!       long long long long long long long long
+//!       long long long long line
+//!         1) Third level with a long long long
+//!            long long long long long long long
+//!            long long long long long long line
+//!         2) Third level with another very long
+//!            long long long long long long long
+//!            long long long long long long line
+//!             - Forth level with a long long
+//!               long long long long long long
+//!               long long long long long long
+//!               long long line
+//!             - Forth level with another very
+//!               long long long long long long
+//!               long long long long long long
+//!               long long line
+//!         3) One more item at the third level
+//!         4) Last item of the third level
+//!     * Last item of second level
+//! 3. Last item of first level
 
 // This example shows how to configure fern to
 // output really nicely colored logs