Repository: jackdewinter/pymarkdown

Issue:
heading-style "consistent" does not select setext_with_atx
Hi @jackdewinter , I tried pymarkdownlint on a project where people use either setext_with_atx or atx style.

**Config:**

```
{
        "md003": {
            "style" : "consistent"
        }
}
```

**Problem:**

On this document, I get a warning `md003 " Heading style should be consistent throughout the document. [Expected: setext; Actual: atx] (heading-style,header-style)"`:

```md
Title
=====

Section 1
---------

### Subsection

Some text...
```

markdownlint has the same issue, but my point here is:

I would be very glad if rule md003 would detect the setext_with_atx style, because there is simply no other way to have headings of level 3 and below than with ATX (unlike in restructuredText).

Implementation is probably simple: If you encounter setext, just ignore headings L3+ (it can only be ATX, after all), or switch to setext_with_atx the first time the parser encounters a L3 ATX heading.

Discussion:

Patch:
diff --git a/changelog.md b/changelog.md
index edf5c5fd..c0f3ee0a 100644
--- a/changelog.md
+++ b/changelog.md
@@ -8,11 +8,13 @@
 
 ### Changed
 
-- None
+- [Changed - Issue 154](https://github.com/jackdewinter/pymarkdown/issues/154)
+  - rule md003 - added configuration `allow-setext-update`
 
 ### Fixed
 
-- None
+- [Fixed - Issue 95](https://github.com/jackdewinter/pymarkdown/issues/95)
+  - parser: with certain cases involving a new list item, can start a container in the middle of line
 
 ## Version 0.9.5 - Date: 2022-02-07
 
diff --git a/docs/rules/rule_md003.md b/docs/rules/rule_md003.md
index 2dfc4b47..87027ad6 100644
--- a/docs/rules/rule_md003.md
+++ b/docs/rules/rule_md003.md
@@ -38,8 +38,8 @@ SetExt Heading
 ### Correct Scenarios
 
 This rule does not trigger when a consistent heading style is used within
-the document.  By default, the heading style is decided upon encountering
-the first heading element in the document:
+the document.  The default style `consistent` decides the heading style upon
+encountering the first heading element in the document.  In this example:
 
 ```Markdown
 # ATX style H1
@@ -47,7 +47,9 @@ the first heading element in the document:
 ## ATX style H2
 ```
 
-Configuration may be used to specify an exact heading style to be used within
+the heading style that would be decided on is `atx`.
+
+Configuration may be used to specify a specific heading style to be used within
 the document.  This is extremely useful for the `setext` style, which is limited
 by Markdown to only 2 levels.  The `setext_with_atx` and `setext_with_atx_closed`
 styles can be used to specify that for levels 3 and higher, the `atx` and
@@ -66,6 +68,16 @@ Setext style H2
 ### ATX style H3
 ```
 
+#### Allowing Auto-Detection of `setext_with_atx`
+
+Using the default style of `consistent` to auto-detect the `setext_with_atx` style
+is problematic, as it appears first as the `setext` style.  The `allow-setext-update`
+configuration value was added to address this issue.  If this configuration setting
+is enabled with the previous example, the rule will still detect the `setext` style
+based on the first SetExt Heading element.  However, when a level 3 (or higher)
+Atx Heading element is encounted and this configuration setting is enabled, it will
+switch to the `setext_with_atx` style.
+
 ## Configuration
 
 | Prefixes |
@@ -78,6 +90,7 @@ Setext style H2
 | -- | -- | -- | -- |
 | `enabled` | `boolean` | `True` | Whether the plugin rule is enabled. |
 | `style` | string (see below) | `consistent` | Style of headings expected in the document. |
+| `allow-setext-update` | boolean | `False` | If `style` is `consistent` and the document started off as `setext`, allow an upgrade to `setext_with_atx` if a level 3 Atx Header or higher is observed. |
 
 Valid heading styles:
 
@@ -94,3 +107,4 @@ Valid heading styles:
 
 This rule is largely inspired by the MarkdownLint rule
 [MD003](https://github.com/DavidAnson/markdownlint/blob/master/doc/Rules.md#md003---heading-style).
+The `allow-setext-update` configuration value was added due to a [user request](https://github.com/jackdewinter/pymarkdown/issues/154).
diff --git a/publish/coverage.json b/publish/coverage.json
index ae2c7d17..04045d65 100644
--- a/publish/coverage.json
+++ b/publish/coverage.json
@@ -2,12 +2,12 @@
     "projectName": "pymarkdown",
     "reportSource": "pytest",
     "branchLevel": {
-        "totalMeasured": 3026,
-        "totalCovered": 3026
+        "totalMeasured": 3032,
+        "totalCovered": 3032
     },
     "lineLevel": {
-        "totalMeasured": 10576,
-        "totalCovered": 10576
+        "totalMeasured": 10592,
+        "totalCovered": 10592
     }
 }
 
diff --git a/pymarkdown/plugins/rule_md_003.py b/pymarkdown/plugins/rule_md_003.py
index 289511de..62ea4ac4 100644
--- a/pymarkdown/plugins/rule_md_003.py
+++ b/pymarkdown/plugins/rule_md_003.py
@@ -33,6 +33,7 @@ class RuleMd003(RulePlugin):
         super().__init__()
         self.__style_type = None
         self.__actual_style_type = None
+        self.__allow_consistent_setext_update = None
 
     def get_details(self):
         """
@@ -63,6 +64,14 @@ class RuleMd003(RulePlugin):
             default_value=RuleMd003.__consistent_style,
             valid_value_fn=self.__validate_configuration_style,
         )
+        if self.__style_type == RuleMd003.__consistent_style:
+            self.__allow_consistent_setext_update = (
+                self.plugin_configuration.get_boolean_property(
+                    "allow-setext-update", default_value=False
+                )
+            )
+        else:
+            self.__allow_consistent_setext_update = False
 
     def starting_new_file(self):
         """
@@ -72,6 +81,47 @@ class RuleMd003(RulePlugin):
         if self.__style_type != RuleMd003.__consistent_style:
             self.__actual_style_type = self.__style_type
 
+    def __handle_simple_styles(self, heading_style_type, is_heading_level_1_or_2):
+        is_heading_bad = bool(heading_style_type != self.__actual_style_type)
+
+        if (
+            is_heading_bad
+            and self.__allow_consistent_setext_update
+            and self.__actual_style_type == RuleMd003.__setext_style
+            and heading_style_type == RuleMd003.__atx_style
+            and not is_heading_level_1_or_2
+        ):
+
+            is_heading_bad = False
+            self.__actual_style_type = RuleMd003.__setext_with_atx_style
+
+        expected_style_type = self.__actual_style_type
+        return is_heading_bad, expected_style_type
+
+    def __handle_complex_styles(self, heading_style_type, is_heading_level_1_or_2):
+
+        is_heading_bad, expected_style_type = False, None
+        if self.__actual_style_type == RuleMd003.__setext_with_atx_style:
+            base_atx_style = RuleMd003.__atx_style
+        else:
+            assert self.__actual_style_type == RuleMd003.__setext_with_atx_closed_style
+            base_atx_style = RuleMd003.__atx_closed_style
+        if not (
+            (is_heading_level_1_or_2 and heading_style_type == RuleMd003.__setext_style)
+            or (not is_heading_level_1_or_2 and heading_style_type == base_atx_style)
+        ):
+            is_heading_bad = True
+            expected_style_type = (
+                RuleMd003.__setext_style if is_heading_level_1_or_2 else base_atx_style
+            )
+            if (
+                expected_style_type == RuleMd003.__setext_style
+                and is_heading_level_1_or_2
+                and self.__actual_style_type == RuleMd003.__setext_with_atx_style
+            ):
+                expected_style_type = RuleMd003.__setext_with_atx_style
+        return is_heading_bad, expected_style_type
+
     def next_token(self, context, token):
         """
         Event that a new token is being processed.
@@ -86,34 +136,13 @@ class RuleMd003(RulePlugin):
             if not self.__actual_style_type:
                 self.__actual_style_type = heading_style_type
             elif self.__actual_style_type in RuleMd003.__simple_styles:
-                is_heading_bad = bool(heading_style_type != self.__actual_style_type)
-                expected_style_type = self.__actual_style_type
+                is_heading_bad, expected_style_type = self.__handle_simple_styles(
+                    heading_style_type, is_heading_level_1_or_2
+                )
             else:
-
-                if self.__actual_style_type == RuleMd003.__setext_with_atx_style:
-                    base_atx_style = RuleMd003.__atx_style
-                else:
-                    assert (
-                        self.__actual_style_type
-                        == RuleMd003.__setext_with_atx_closed_style
-                    )
-                    base_atx_style = RuleMd003.__atx_closed_style
-                if not (
-                    (
-                        is_heading_level_1_or_2
-                        and heading_style_type == RuleMd003.__setext_style
-                    )
-                    or (
-                        not is_heading_level_1_or_2
-                        and heading_style_type == base_atx_style
-                    )
-                ):
-                    is_heading_bad = True
-                    expected_style_type = (
-                        RuleMd003.__setext_style
-                        if is_heading_level_1_or_2
-                        else base_atx_style
-                    )
+                is_heading_bad, expected_style_type = self.__handle_complex_styles(
+                    heading_style_type, is_heading_level_1_or_2
+                )
 
             if is_heading_bad:
                 extra_data = (


Test patch:
diff --git a/publish/test-results.json b/publish/test-results.json
index 1a9d89d9..39fff94a 100644
--- a/publish/test-results.json
+++ b/publish/test-results.json
@@ -804,7 +804,7 @@
         },
         {
             "name": "test.test_md003",
-            "totalTests": 38,
+            "totalTests": 41,
             "failedTests": 0,
             "errorTests": 0,
             "skippedTests": 0,
diff --git a/test/resources/rules/md003/headings_setext_with_level_2_atx.md b/test/resources/rules/md003/headings_setext_with_level_2_atx.md
new file mode 100644
index 00000000..5bc2f596
--- /dev/null
+++ b/test/resources/rules/md003/headings_setext_with_level_2_atx.md
@@ -0,0 +1,7 @@
+Heading 1
+=========
+
+Heading 2
+---------
+
+## Heading 2
diff --git a/test/resources/rules/md003/headings_setext_with_level_3_then_level_2_atx.md b/test/resources/rules/md003/headings_setext_with_level_3_then_level_2_atx.md
new file mode 100644
index 00000000..5a215ee2
--- /dev/null
+++ b/test/resources/rules/md003/headings_setext_with_level_3_then_level_2_atx.md
@@ -0,0 +1,9 @@
+Heading 1
+=========
+
+Heading 2
+---------
+
+### Heading 3
+
+## Heading 2 Again
diff --git a/test/test_md003.py b/test/test_md003.py
index df2bba4d..e043cd08 100644
--- a/test/test_md003.py
+++ b/test/test_md003.py
@@ -3,11 +3,10 @@ Module to provide tests related to the MD003 rule.
 """
 import os
 from test.markdown_scanner import MarkdownScanner
+from test.utils import write_temporary_configuration
 
 import pytest
 
-from .utils import write_temporary_configuration
-
 # pylint: disable=too-many-lines
 
 
@@ -32,7 +31,8 @@ def test_md003_bad_configuration_style():
     expected_output = ""
     expected_error = (
         "BadPluginError encountered while configuring plugins:\n"
-        + "The value for property 'plugins.md003.style' is not valid: Allowable values: ['consistent', 'atx', 'atx_closed', 'setext', 'setext_with_atx', 'setext_with_atx_closed']"
+        + "The value for property 'plugins.md003.style' is not valid: Allowable values: "
+        + "['consistent', 'atx', 'atx_closed', 'setext', 'setext_with_atx', 'setext_with_atx_closed']"
     )
 
     # Act
@@ -136,7 +136,8 @@ def test_md003_good_consistent_headings_setext():
 
 CONSISTENT_SETEXT_WITH_ATX_HEADINGS_SAMPLE_OUTPUT = (
     "test/resources/rules/md003/headings_setext_with_atx.md:7:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: setext; Actual: atx] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: setext; Actual: atx] (heading-style,header-style)\n"
 )
 
 
@@ -168,9 +169,138 @@ def test_md003_bad_consistent_headings_setext_with_atx():
     )
 
 
+@pytest.mark.rules
+def test_md003_bad_consistent_headings_setext_with_atx_and_allow_config():
+    """
+    Test to make sure this rule does trigger with a document that
+    only contains SetExt Headings for the first two levels and
+    Atx Headings after that.  Also, turn on the `allow-setext-update`
+    configuration to allow the discovered `setext` to be upgraded to
+    `atx` if possible.
+    """
+
+    # Arrange
+    scanner = MarkdownScanner()
+    supplied_configuration = {"plugins": {"md003": {"allow-setext-update": True}}}
+    configuration_file = None
+    try:
+        configuration_file = write_temporary_configuration(supplied_configuration)
+        supplied_arguments = [
+            "-c",
+            configuration_file,
+            "--strict-config",
+            "scan",
+            "test/resources/rules/md003/headings_setext_with_atx.md",
+        ]
+
+        expected_return_code = 0
+        expected_output = ""
+        expected_error = ""
+
+        # Act
+        execute_results = scanner.invoke_main(arguments=supplied_arguments)
+
+        # Assert
+        execute_results.assert_results(
+            expected_output, expected_error, expected_return_code
+        )
+    finally:
+        if configuration_file and os.path.exists(configuration_file):
+            os.remove(configuration_file)
+
+
+@pytest.mark.rules
+def test_md003_bad_consistent_headings_setext_with_level_2_atx_and_allow_config():
+    """
+    Test to make sure this rule does trigger with a document that
+    only contains SetExt Headings for the first two levels and
+    Atx Headings after that.  Also, turn on the `allow-setext-update`
+    configuration to allow the discovered `setext` to be upgraded to
+    `atx` if possible.
+    """
+
+    # Arrange
+    scanner = MarkdownScanner()
+    supplied_configuration = {"plugins": {"md003": {"allow-setext-update": True}}}
+    configuration_file = None
+    try:
+        configuration_file = write_temporary_configuration(supplied_configuration)
+        supplied_arguments = [
+            "-c",
+            configuration_file,
+            "--strict-config",
+            "scan",
+            "test/resources/rules/md003/headings_setext_with_level_2_atx.md",
+        ]
+
+        expected_return_code = 1
+        expected_output = (
+            "test/resources/rules/md003/headings_setext_with_level_2_atx.md:7:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: setext; Actual: atx] (heading-style,header-style)"
+        )
+        expected_error = ""
+
+        # Act
+        execute_results = scanner.invoke_main(arguments=supplied_arguments)
+
+        # Assert
+        execute_results.assert_results(
+            expected_output, expected_error, expected_return_code
+        )
+    finally:
+        if configuration_file and os.path.exists(configuration_file):
+            os.remove(configuration_file)
+
+
+@pytest.mark.rules
+def test_md003_bad_consistent_headings_setext_with_level_3_then_level_2_atx_and_allow_config():
+    """
+    Test to make sure this rule does trigger with a document that
+    only contains SetExt Headings for the first two levels and
+    Atx Headings after that.  Also, turn on the `allow-setext-update`
+    configuration to allow the discovered `setext` to be upgraded to
+    `atx` if possible.
+    """
+
+    # Arrange
+    scanner = MarkdownScanner()
+    supplied_configuration = {"plugins": {"md003": {"allow-setext-update": True}}}
+    configuration_file = None
+    try:
+        configuration_file = write_temporary_configuration(supplied_configuration)
+        supplied_arguments = [
+            "-c",
+            configuration_file,
+            "--strict-config",
+            "scan",
+            "test/resources/rules/md003/headings_setext_with_level_3_then_level_2_atx.md",
+        ]
+
+        expected_return_code = 1
+        expected_output = (
+            "test/resources/rules/md003/headings_setext_with_level_3_then_level_2_atx.md:9:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: setext_with_atx; Actual: atx] (heading-style,header-style)"
+        )
+        expected_error = ""
+
+        # Act
+        execute_results = scanner.invoke_main(arguments=supplied_arguments)
+
+        # Assert
+        execute_results.assert_results(
+            expected_output, expected_error, expected_return_code
+        )
+    finally:
+        if configuration_file and os.path.exists(configuration_file):
+            os.remove(configuration_file)
+
+
 CONSISTENT_SETEXT_WITH_ATX_CLOSED_HEADINGS_SAMPLE_OUTPUT = (
     "test/resources/rules/md003/headings_setext_with_atx_closed.md:7:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: setext; Actual: atx_closed] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: setext; Actual: atx_closed] (heading-style,header-style)\n"
 )
 
 
@@ -220,6 +350,15 @@ def test_md003_consistent_all_samples():
         + CONSISTENT_SETEXT_HEADINGS_SAMPLE_OUTPUT
         + CONSISTENT_SETEXT_WITH_ATX_HEADINGS_SAMPLE_OUTPUT
         + CONSISTENT_SETEXT_WITH_ATX_CLOSED_HEADINGS_SAMPLE_OUTPUT
+        + "test/resources/rules/md003/headings_setext_with_level_2_atx.md:7:1: "
+        + "MD003: Heading style should be consistent throughout the document. "
+        + "[Expected: setext; Actual: atx] (heading-style,header-style)\n"
+        + "test/resources/rules/md003/headings_setext_with_level_3_then_level_2_atx.md:7:1: "
+        + "MD003: Heading style should be consistent throughout the document. "
+        + "[Expected: setext; Actual: atx] (heading-style,header-style)\n"
+        + "test/resources/rules/md003/headings_setext_with_level_3_then_level_2_atx.md:9:1: "
+        + "MD003: Heading style should be consistent throughout the document. "
+        + "[Expected: setext; Actual: atx] (heading-style,header-style)"
     )
     expected_error = ""
 
@@ -308,9 +447,11 @@ def test_md003_good_atx_headings_atxx():
 
 ATX_ATX_CLOSED_HEADINGS_SAMPLE_OUTPUT = (
     "test/resources/rules/md003/headings_atx_closed.md:1:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: atx; Actual: atx_closed] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: atx; Actual: atx_closed] (heading-style,header-style)\n"
     + "test/resources/rules/md003/headings_atx_closed.md:3:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: atx; Actual: atx_closed] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: atx; Actual: atx_closed] (heading-style,header-style)\n"
 )
 
 
@@ -352,9 +493,11 @@ def test_md003_bad_atx_headings_atx_closed():
 
 ATX_SETEXT_HEADINGS_SAMPLE_OUTPUT = (
     "test/resources/rules/md003/headings_setext.md:2:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: atx; Actual: setext] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: atx; Actual: setext] (heading-style,header-style)\n"
     + "test/resources/rules/md003/headings_setext.md:5:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: atx; Actual: setext] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: atx; Actual: setext] (heading-style,header-style)\n"
 )
 
 
@@ -396,9 +539,11 @@ def test_md003_bad_atx_headings_setext():
 
 ATX_SETEXT_WITH_ATX_HEADINGS_SAMPLE_OUTPUT = (
     "test/resources/rules/md003/headings_setext_with_atx.md:2:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: atx; Actual: setext] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: atx; Actual: setext] (heading-style,header-style)\n"
     + "test/resources/rules/md003/headings_setext_with_atx.md:5:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: atx; Actual: setext] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: atx; Actual: setext] (heading-style,header-style)\n"
 )
 
 
@@ -440,11 +585,14 @@ def test_md003_bad_atx_headings_setext_with_atx():
 
 ATX_SETEXT_WITH_ATX_CLOSED_HEADINGS_SAMPLE_OUTPUT = (
     "test/resources/rules/md003/headings_setext_with_atx_closed.md:2:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: atx; Actual: setext] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: atx; Actual: setext] (heading-style,header-style)\n"
     + "test/resources/rules/md003/headings_setext_with_atx_closed.md:5:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: atx; Actual: setext] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: atx; Actual: setext] (heading-style,header-style)\n"
     + "test/resources/rules/md003/headings_setext_with_atx_closed.md:7:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: atx; Actual: atx_closed] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: atx; Actual: atx_closed] (heading-style,header-style)\n"
 )
 
 
@@ -511,6 +659,18 @@ def test_md003_atx_all_samples():
             + ATX_SETEXT_HEADINGS_SAMPLE_OUTPUT
             + ATX_SETEXT_WITH_ATX_HEADINGS_SAMPLE_OUTPUT
             + ATX_SETEXT_WITH_ATX_CLOSED_HEADINGS_SAMPLE_OUTPUT
+            + "test/resources/rules/md003/headings_setext_with_level_2_atx.md:2:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: atx; Actual: setext] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_setext_with_level_2_atx.md:5:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: atx; Actual: setext] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_setext_with_level_3_then_level_2_atx.md:2:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: atx; Actual: setext] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_setext_with_level_3_then_level_2_atx.md:5:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: atx; Actual: setext] (heading-style,header-style)"
         )
         expected_error = ""
 
@@ -528,9 +688,11 @@ def test_md003_atx_all_samples():
 
 ATXCLOSED_ATX_HEADINGS_SAMPLE_OUTPUT = (
     "test/resources/rules/md003/headings_atx.md:1:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: atx_closed; Actual: atx] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: atx_closed; Actual: atx] (heading-style,header-style)\n"
     + "test/resources/rules/md003/headings_atx.md:3:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: atx_closed; Actual: atx] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: atx_closed; Actual: atx] (heading-style,header-style)\n"
 )
 
 
@@ -611,9 +773,11 @@ def test_md003_good_atxclosed_headings_atx_closed():
 
 ATXCLOSED_SETEXT_HEADINGS_SAMPLE_OUTPUT = (
     "test/resources/rules/md003/headings_setext.md:2:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: atx_closed; Actual: setext] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: atx_closed; Actual: setext] (heading-style,header-style)\n"
     + "test/resources/rules/md003/headings_setext.md:5:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: atx_closed; Actual: setext] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: atx_closed; Actual: setext] (heading-style,header-style)\n"
 )
 
 
@@ -655,11 +819,14 @@ def test_md003_bad_atxclosed_headings_setext():
 
 ATXCLOSED_SETEXT_WITH_ATX_HEADINGS_SAMPLE_OUTPUT = (
     "test/resources/rules/md003/headings_setext_with_atx.md:2:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: atx_closed; Actual: setext] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: atx_closed; Actual: setext] (heading-style,header-style)\n"
     + "test/resources/rules/md003/headings_setext_with_atx.md:5:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: atx_closed; Actual: setext] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: atx_closed; Actual: setext] (heading-style,header-style)\n"
     + "test/resources/rules/md003/headings_setext_with_atx.md:7:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: atx_closed; Actual: atx] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: atx_closed; Actual: atx] (heading-style,header-style)\n"
 )
 
 
@@ -701,9 +868,11 @@ def test_md003_bad_atxclosed_headings_setext_with_atx():
 
 ATXCLOSED_SETEXT_WITH_ATX_CLOSED_HEADINGS_SAMPLE_OUTPUT = (
     "test/resources/rules/md003/headings_setext_with_atx_closed.md:2:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: atx_closed; Actual: setext] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: atx_closed; Actual: setext] (heading-style,header-style)\n"
     + "test/resources/rules/md003/headings_setext_with_atx_closed.md:5:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: atx_closed; Actual: setext] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: atx_closed; Actual: setext] (heading-style,header-style)\n"
 )
 
 
@@ -770,6 +939,27 @@ def test_md003_atxclosed_all_samples():
             + ATXCLOSED_SETEXT_HEADINGS_SAMPLE_OUTPUT
             + ATXCLOSED_SETEXT_WITH_ATX_HEADINGS_SAMPLE_OUTPUT
             + ATXCLOSED_SETEXT_WITH_ATX_CLOSED_HEADINGS_SAMPLE_OUTPUT
+            + "test/resources/rules/md003/headings_setext_with_level_2_atx.md:2:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: atx_closed; Actual: setext] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_setext_with_level_2_atx.md:5:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: atx_closed; Actual: setext] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_setext_with_level_2_atx.md:7:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: atx_closed; Actual: atx] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_setext_with_level_3_then_level_2_atx.md:2:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: atx_closed; Actual: setext] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_setext_with_level_3_then_level_2_atx.md:5:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: atx_closed; Actual: setext] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_setext_with_level_3_then_level_2_atx.md:7:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: atx_closed; Actual: atx] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_setext_with_level_3_then_level_2_atx.md:9:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: atx_closed; Actual: atx] (heading-style,header-style)"
         )
         expected_error = ""
 
@@ -787,9 +977,11 @@ def test_md003_atxclosed_all_samples():
 
 SETEXT_ATX_HEADINGS_SAMPLE_OUTPUT = (
     "test/resources/rules/md003/headings_atx.md:1:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: setext; Actual: atx] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: setext; Actual: atx] (heading-style,header-style)\n"
     + "test/resources/rules/md003/headings_atx.md:3:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: setext; Actual: atx] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: setext; Actual: atx] (heading-style,header-style)\n"
 )
 
 
@@ -831,9 +1023,11 @@ def test_md003_bad_setext_headings_atx():
 
 SETEXT_ATX_CLOSED_HEADINGS_SAMPLE_OUTPUT = (
     "test/resources/rules/md003/headings_atx_closed.md:1:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: setext; Actual: atx_closed] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: setext; Actual: atx_closed] (heading-style,header-style)\n"
     + "test/resources/rules/md003/headings_atx_closed.md:3:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: setext; Actual: atx_closed] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: setext; Actual: atx_closed] (heading-style,header-style)\n"
 )
 
 
@@ -914,7 +1108,8 @@ def test_md003_good_setext_headings_setext():
 
 SETEXT_SETEXT_WITH_ATX_HEADINGS_SAMPLE_OUTPUT = (
     "test/resources/rules/md003/headings_setext_with_atx.md:7:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: setext; Actual: atx] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: setext; Actual: atx] (heading-style,header-style)\n"
 )
 
 
@@ -956,7 +1151,8 @@ def test_md003_bad_setext_headings_setext_with_atx():
 
 SETEXT_SETEXT_WITH_ATX_CLOSED_HEADINGS_SAMPLE_OUTPUT = (
     "test/resources/rules/md003/headings_setext_with_atx_closed.md:7:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: setext; Actual: atx_closed] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: setext; Actual: atx_closed] (heading-style,header-style)\n"
 )
 
 
@@ -1023,6 +1219,15 @@ def test_md003_setext_all_samples():
             + SETEXT_SETEXT_HEADINGS_SAMPLE_OUTPUT
             + SETEXT_SETEXT_WITH_ATX_HEADINGS_SAMPLE_OUTPUT
             + SETEXT_SETEXT_WITH_ATX_CLOSED_HEADINGS_SAMPLE_OUTPUT
+            + "test/resources/rules/md003/headings_setext_with_level_2_atx.md:7:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: setext; Actual: atx] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_setext_with_level_3_then_level_2_atx.md:7:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: setext; Actual: atx] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_setext_with_level_3_then_level_2_atx.md:9:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: setext; Actual: atx] (heading-style,header-style)"
         )
         expected_error = ""
 
@@ -1040,9 +1245,11 @@ def test_md003_setext_all_samples():
 
 SETEXT_WITH_ATX_ATX_HEADINGS_SAMPLE_OUTPUT = (
     "test/resources/rules/md003/headings_atx.md:1:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: setext; Actual: atx] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: setext; Actual: atx] (heading-style,header-style)\n"
     + "test/resources/rules/md003/headings_atx.md:3:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: setext; Actual: atx] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: setext; Actual: atx] (heading-style,header-style)\n"
 )
 
 
@@ -1067,7 +1274,14 @@ def test_md003_bad_setext_with_atx_headings_atx():
         ]
 
         expected_return_code = 1
-        expected_output = SETEXT_WITH_ATX_ATX_HEADINGS_SAMPLE_OUTPUT
+        expected_output = (
+            "test/resources/rules/md003/headings_atx.md:1:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: setext_with_atx; Actual: atx] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_atx.md:3:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: setext_with_atx; Actual: atx] (heading-style,header-style)"
+        )
         expected_error = ""
 
         # Act
@@ -1084,9 +1298,11 @@ def test_md003_bad_setext_with_atx_headings_atx():
 
 SETEXT_WITH_ATX_ATX_CLOSED_HEADINGS_SAMPLE_OUTPUT = (
     "test/resources/rules/md003/headings_atx_closed.md:1:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: setext; Actual: atx_closed] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: setext; Actual: atx_closed] (heading-style,header-style)\n"
     + "test/resources/rules/md003/headings_atx_closed.md:3:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: setext; Actual: atx_closed] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: setext; Actual: atx_closed] (heading-style,header-style)\n"
 )
 
 
@@ -1111,7 +1327,14 @@ def test_md003_bad_setext_with_atx_headings_atx_closed():
         ]
 
         expected_return_code = 1
-        expected_output = SETEXT_WITH_ATX_ATX_CLOSED_HEADINGS_SAMPLE_OUTPUT
+        expected_output = (
+            "test/resources/rules/md003/headings_atx_closed.md:1:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: setext_with_atx; Actual: atx_closed] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_atx_closed.md:3:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: setext_with_atx; Actual: atx_closed] (heading-style,header-style)"
+        )
         expected_error = ""
 
         # Act
@@ -1206,7 +1429,8 @@ def test_md003_good_setext_with_atx_headings_setext_with_atx():
 
 SETEXT_WITH_ATX_SETEXT_WITH_ATX_CLOSED_HEADINGS_SAMPLE_OUTPUT = (
     "test/resources/rules/md003/headings_setext_with_atx_closed.md:7:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: atx; Actual: atx_closed] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: atx; Actual: atx_closed] (heading-style,header-style)\n"
 )
 
 
@@ -1268,11 +1492,27 @@ def test_md003_setext_with_atx_all_samples():
 
         expected_return_code = 1
         expected_output = (
-            SETEXT_WITH_ATX_ATX_HEADINGS_SAMPLE_OUTPUT
-            + SETEXT_WITH_ATX_ATX_CLOSED_HEADINGS_SAMPLE_OUTPUT
-            + SETEXT_WITH_ATX_SETEXT_HEADINGS_SAMPLE_OUTPUT
-            + SETEXT_WITH_ATX_SETEXT_WITH_ATX_HEADINGS_SAMPLE_OUTPUT
-            + SETEXT_WITH_ATX_SETEXT_WITH_ATX_CLOSED_HEADINGS_SAMPLE_OUTPUT
+            "test/resources/rules/md003/headings_atx.md:1:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: setext_with_atx; Actual: atx] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_atx.md:3:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: setext_with_atx; Actual: atx] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_atx_closed.md:1:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: setext_with_atx; Actual: atx_closed] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_atx_closed.md:3:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: setext_with_atx; Actual: atx_closed] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_setext_with_atx_closed.md:7:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: atx; Actual: atx_closed] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_setext_with_level_2_atx.md:7:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: setext_with_atx; Actual: atx] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_setext_with_level_3_then_level_2_atx.md:9:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: setext_with_atx; Actual: atx] (heading-style,header-style)"
         )
         expected_error = ""
 
@@ -1290,9 +1530,11 @@ def test_md003_setext_with_atx_all_samples():
 
 SETEXT_WITH_ATX_CLOSED_ATX_HEADINGS_SAMPLE_OUTPUT = (
     "test/resources/rules/md003/headings_atx.md:1:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: setext; Actual: atx] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: setext; Actual: atx] (heading-style,header-style)\n"
     + "test/resources/rules/md003/headings_atx.md:3:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: setext; Actual: atx] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: setext; Actual: atx] (heading-style,header-style)\n"
 )
 
 
@@ -1334,9 +1576,11 @@ def test_md003_bad_setext_with_atx_closed_headings_atx():
 
 SETEXT_WITH_ATX_CLOSED_ATX_CLOSED_HEADINGS_SAMPLE_OUTPUT = (
     "test/resources/rules/md003/headings_atx_closed.md:1:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: setext; Actual: atx_closed] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: setext; Actual: atx_closed] (heading-style,header-style)\n"
     + "test/resources/rules/md003/headings_atx_closed.md:3:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: setext; Actual: atx_closed] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: setext; Actual: atx_closed] (heading-style,header-style)\n"
 )
 
 
@@ -1417,7 +1661,8 @@ def test_md003_good_setext_with_atx_closed_headings_setext():
 
 SETEXT_WITH_ATX_CLOSED_SETEXT_WITH_ATX_HEADINGS_SAMPLE_OUTPUT = (
     "test/resources/rules/md003/headings_setext_with_atx.md:7:1: "
-    + "MD003: Heading style should be consistent throughout the document. [Expected: atx_closed; Actual: atx] (heading-style,header-style)\n"
+    + "MD003: Heading style should be consistent throughout the document. "
+    + "[Expected: atx_closed; Actual: atx] (heading-style,header-style)\n"
 )
 
 
@@ -1525,6 +1770,15 @@ def test_md003_setext_with_atx_closed_all_samples():
             + SETEXT_WITH_ATX_CLOSED_SETEXT_HEADINGS_SAMPLE_OUTPUT
             + SETEXT_WITH_ATX_CLOSED_SETEXT_WITH_ATX_HEADINGS_SAMPLE_OUTPUT
             + SETEXT_WITH_ATX_CLOSED_SETEXT_WITH_ATX_CLOSED_HEADINGS_SAMPLE_OUTPUT
+            + "test/resources/rules/md003/headings_setext_with_level_2_atx.md:7:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: setext; Actual: atx] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_setext_with_level_3_then_level_2_atx.md:7:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: atx_closed; Actual: atx] (heading-style,header-style)\n"
+            + "test/resources/rules/md003/headings_setext_with_level_3_then_level_2_atx.md:9:1: "
+            + "MD003: Heading style should be consistent throughout the document. "
+            + "[Expected: setext; Actual: atx] (heading-style,header-style)"
         )
         expected_error = ""