diff --git a/changelog.md b/changelog.md index f54fbd95a..bc2f1051d 100644 --- a/changelog.md +++ b/changelog.md @@ -13,6 +13,8 @@ for a version 1.0 release in early 2 - Extension: Strikethrough - [Issue 805](https://github.com/jackdewinter/pymarkdown/issues/805) - Extension: Task List Items +- [Issue 821](https://github.com/jackdewinter/pymarkdown/issues/821) + - Rule MD037 - Added fix options - [Issue 822](https://github.com/jackdewinter/pymarkdown/issues/822) - Rule MD038 - Added fix options - [Issue 823](https://github.com/jackdewinter/pymarkdown/issues/823) diff --git a/docs/rules/rule_md037.md b/docs/rules/rule_md037.md index dc541734d..b2ab618b4 100644 --- a/docs/rules/rule_md037.md +++ b/docs/rules/rule_md037.md @@ -5,6 +5,10 @@ | `md037` | | `no-space-in-emphasis` | +| Autofix Available | +| --- | +| Yes | + ## Summary Spaces inside emphasis markers. @@ -66,3 +70,7 @@ and emphasis sequences. Therefore, text such as `this * is not * emphasis` raised triggered on both the first and the second emphasis characters. This rule looks for scenarios where there are a matched pair of emphasis characters, instead of just looking for those individual characters. + +## Fix Description + +Within the block of emphasized text, any leading and trailing whitespace is removed. diff --git a/publish/coverage.json b/publish/coverage.json index 640ac7aef..5fabfbf03 100644 --- a/publish/coverage.json +++ b/publish/coverage.json @@ -2,12 +2,12 @@ "projectName": "pymarkdown", "reportSource": "pytest", "branchLevel": { - "totalMeasured": 4451, - "totalCovered": 4451 + "totalMeasured": 4467, + "totalCovered": 4467 }, "lineLevel": { - "totalMeasured": 18452, - "totalCovered": 18452 + "totalMeasured": 18480, + "totalCovered": 18480 } } diff --git a/publish/pylint_suppression.json b/publish/pylint_suppression.json index 5fa95eb2c..822cf9705 100644 --- a/publish/pylint_suppression.json +++ b/publish/pylint_suppression.json @@ -317,7 +317,9 @@ }, "pymarkdown/plugins/rule_md_035.py": {}, "pymarkdown/plugins/rule_md_036.py": {}, - "pymarkdown/plugins/rule_md_037.py": {}, + "pymarkdown/plugins/rule_md_037.py": { + "too-many-arguments": 1 + }, "pymarkdown/plugins/rule_md_038.py": {}, "pymarkdown/plugins/rule_md_039.py": {}, "pymarkdown/plugins/rule_md_040.py": {}, @@ -485,7 +487,7 @@ "too-many-instance-attributes": 23, "too-many-public-methods": 4, "too-few-public-methods": 39, - "too-many-arguments": 216, + "too-many-arguments": 217, "too-many-locals": 33, "chained-comparison": 1, "too-many-boolean-expressions": 2, diff --git a/publish/test-results.json b/publish/test-results.json index c62851c2a..5a6a78f00 100644 --- a/publish/test-results.json +++ b/publish/test-results.json @@ -1436,7 +1436,7 @@ }, { "name": "test.rules.test_md037", - "totalTests": 10, + "totalTests": 21, "failedTests": 0, "errorTests": 0, "skippedTests": 0, @@ -1697,6 +1697,14 @@ "errorTests": 0, "skippedTests": 0, "elapsedTimeInMilliseconds": 0 + }, + { + "name": "test.tokens.test_text_markdown_token", + "totalTests": 1, + "failedTests": 0, + "errorTests": 0, + "skippedTests": 0, + "elapsedTimeInMilliseconds": 0 } ] } diff --git a/pymarkdown/plugins/rule_md_037.py b/pymarkdown/plugins/rule_md_037.py index 7f7cf855e..45e7135ad 100644 --- a/pymarkdown/plugins/rule_md_037.py +++ b/pymarkdown/plugins/rule_md_037.py @@ -1,7 +1,7 @@ """ Module to implement a plugin that looks for spaces within emphasis sections. """ -from typing import List, Optional, cast +from typing import List, Optional, Tuple, cast from pymarkdown.plugin_manager.plugin_details import PluginDetails from pymarkdown.plugin_manager.plugin_scan_context import PluginScanContext @@ -43,6 +43,53 @@ def starting_new_file(self) -> None: self.__start_emphasis_token = None self.__emphasis_token_list = [] + # pylint: disable=too-many-arguments + def __fix( + self, + context: PluginScanContext, + start_token: Optional[TextMarkdownToken], + end_token: Optional[TextMarkdownToken], + did_first_start_with_space: bool, + did_last_end_with_space: bool, + ) -> None: + if start_token == end_token: + assert start_token is not None + adjusted_token_text = start_token.token_text + if did_first_start_with_space: + adjusted_token_text = adjusted_token_text.lstrip() + if did_last_end_with_space: + adjusted_token_text = adjusted_token_text.rstrip() + self.register_fix_token_request( + context, + start_token, + "next_token", + "token_text", + adjusted_token_text, + ) + else: + if did_first_start_with_space: + assert start_token is not None + adjusted_token_text = start_token.token_text.lstrip() + self.register_fix_token_request( + context, + start_token, + "next_token", + "token_text", + adjusted_token_text, + ) + if did_last_end_with_space: + assert end_token is not None + adjusted_token_text = end_token.token_text.rstrip() + self.register_fix_token_request( + context, + end_token, + "next_token", + "token_text", + adjusted_token_text, + ) + + # pylint: enable=too-many-arguments + def __handle_emphasis_text( self, context: PluginScanContext, token: MarkdownToken ) -> None: # sourcery skip: extract-method @@ -50,22 +97,40 @@ def __handle_emphasis_text( assert self.__start_emphasis_token is not None if text_token.token_text == self.__start_emphasis_token.token_text: assert self.__emphasis_token_list - did_first_start_with_space = self.__handle_emphasis_text_space_check(0) - did_last_end_with_space = self.__handle_emphasis_text_space_check(-1) + ( + start_token, + did_first_start_with_space, + ) = self.__handle_emphasis_text_space_check(0) + ( + end_token, + did_last_end_with_space, + ) = self.__handle_emphasis_text_space_check(-1) if did_first_start_with_space or did_last_end_with_space: assert self.__start_emphasis_token is not None - self.report_next_token_error(context, self.__start_emphasis_token) + if context.in_fix_mode: + self.__fix( + context, + start_token, + end_token, + did_first_start_with_space, + did_last_end_with_space, + ) + else: + self.report_next_token_error(context, self.__start_emphasis_token) self.__start_emphasis_token = None self.__emphasis_token_list = [] else: self.__emphasis_token_list.append(token) - def __handle_emphasis_text_space_check(self, token_text_index: int) -> bool: + def __handle_emphasis_text_space_check( + self, token_text_index: int + ) -> Tuple[Optional[TextMarkdownToken], bool]: first_capture_token = self.__emphasis_token_list[token_text_index] - assert first_capture_token.is_text + if not first_capture_token.is_text: + return None, False other_text_token = cast(TextMarkdownToken, first_capture_token) - return other_text_token.token_text[token_text_index] == " " + return other_text_token, other_text_token.token_text[token_text_index] == " " def __handle_start_emphasis( self, context: PluginScanContext, token: MarkdownToken diff --git a/pymarkdown/tokens/text_markdown_token.py b/pymarkdown/tokens/text_markdown_token.py index da3f90102..80beb6825 100644 --- a/pymarkdown/tokens/text_markdown_token.py +++ b/pymarkdown/tokens/text_markdown_token.py @@ -3,7 +3,9 @@ """ import logging -from typing import List, Optional, Tuple, cast +from typing import List, Optional, Tuple, Union, cast + +from typing_extensions import override from pymarkdown.general.constants import Constants from pymarkdown.general.parser_helper import ParserHelper @@ -114,6 +116,15 @@ def create_copy(self) -> "TextMarkdownToken": column_number=self.column_number, ) + @override + def _modify_token(self, field_name: str, field_value: Union[str, int]) -> bool: + if field_name == "token_text" and isinstance(field_value, str): + self.__token_text = field_value + self.__compose_extra_data_field() + + return True + return False + def __compose_extra_data_field(self) -> None: """ Compose the object's self.extra_data field from the local object's variables. diff --git a/test/rules/test_md037.py b/test/rules/test_md037.py index 4a6bc31a2..37cf56509 100644 --- a/test/rules/test_md037.py +++ b/test/rules/test_md037.py @@ -3,9 +3,16 @@ """ import os from test.markdown_scanner import MarkdownScanner +from test.utils import ( + assert_file_is_as_expected, + copy_to_temp_file, + create_temporary_configuration_file, +) import pytest +source_path = os.path.join("test", "resources", "rules", "md037") + os.sep + @pytest.mark.rules def test_md037_good_valid_emphasis(): @@ -72,6 +79,57 @@ def test_md037_bad_surrounding_emphasis(): ) +@pytest.mark.rules +def test_md037_bad_surrounding_emphasis_fix(): + """ + Test to make sure this rule does trigger with a document that + contains an inline link with space on the right side of the link label. + """ + + # Arrange + scanner = MarkdownScanner() + with copy_to_temp_file( + source_path + "bad_surrounding_emphasis.md" + ) as temp_source_path: + original_file_contents = """this text * is * in italics + +this text _ is _ in italics + +this text ** is ** in bold + +this text __ is __ in bold +""" + assert_file_is_as_expected(temp_source_path, original_file_contents) + + supplied_arguments = [ + "-x-fix", + "scan", + temp_source_path, + ] + + expected_return_code = 3 + expected_output = f"Fixed: {temp_source_path}" + expected_error = "" + + expected_file_contents = """this text *is* in italics + +this text _is_ in italics + +this text **is** in bold + +this text __is__ in bold +""" + + # Act + execute_results = scanner.invoke_main(arguments=supplied_arguments) + + # Assert + execute_results.assert_results( + expected_output, expected_error, expected_return_code + ) + assert_file_is_as_expected(temp_source_path, expected_file_contents) + + @pytest.mark.rules def test_md037_bad_leading_emphasis(): """ @@ -107,6 +165,55 @@ def test_md037_bad_leading_emphasis(): ) +@pytest.mark.rules +def test_md037_bad_leading_emphasis_fix(): + """ + Test to make sure this rule does trigger with a document that + contains an inline link with space on the right side of the link label. + """ + + # Arrange + scanner = MarkdownScanner() + with copy_to_temp_file(source_path + "bad_leading_emphasis.md") as temp_source_path: + original_file_contents = """this text * is* in italics + +this text _ is_ in italics + +this text ** is** in bold + +this text __ is__ in bold +""" + assert_file_is_as_expected(temp_source_path, original_file_contents) + + supplied_arguments = [ + "-x-fix", + "scan", + temp_source_path, + ] + + expected_return_code = 3 + expected_output = f"Fixed: {temp_source_path}" + expected_error = "" + + expected_file_contents = """this text *is* in italics + +this text _is_ in italics + +this text **is** in bold + +this text __is__ in bold +""" + + # Act + execute_results = scanner.invoke_main(arguments=supplied_arguments) + + # Assert + execute_results.assert_results( + expected_output, expected_error, expected_return_code + ) + assert_file_is_as_expected(temp_source_path, expected_file_contents) + + @pytest.mark.rules def test_md037_bad_trailing_emphasis(): """ @@ -142,6 +249,57 @@ def test_md037_bad_trailing_emphasis(): ) +@pytest.mark.rules +def test_md037_bad_trailing_emphasis_fix(): + """ + Test to make sure this rule does trigger with a document that + contains an inline link with space on the right side of the link label. + """ + + # Arrange + scanner = MarkdownScanner() + with copy_to_temp_file( + source_path + "bad_trailing_emphasis.md" + ) as temp_source_path: + original_file_contents = """this text *is * in italics + +this text _is _ in italics + +this text **is ** in bold + +this text __is __ in bold +""" + assert_file_is_as_expected(temp_source_path, original_file_contents) + + supplied_arguments = [ + "-x-fix", + "scan", + temp_source_path, + ] + + expected_return_code = 3 + expected_output = f"Fixed: {temp_source_path}" + expected_error = "" + + expected_file_contents = """this text *is* in italics + +this text _is_ in italics + +this text **is** in bold + +this text __is__ in bold +""" + + # Act + execute_results = scanner.invoke_main(arguments=supplied_arguments) + + # Assert + execute_results.assert_results( + expected_output, expected_error, expected_return_code + ) + assert_file_is_as_expected(temp_source_path, expected_file_contents) + + @pytest.mark.rules def test_md037_bad_surrounding_emphasis_multiline(): """ @@ -178,6 +336,65 @@ def test_md037_bad_surrounding_emphasis_multiline(): ) +@pytest.mark.rules +def test_md037_bad_surrounding_emphasis_multiline_fix(): + """ + Test to make sure this rule does trigger with a document that + contains an inline link with space on the right side of the link label. + """ + + # Arrange + scanner = MarkdownScanner() + with copy_to_temp_file( + source_path + "bad_surrounding_emphasis_multiline.md" + ) as temp_source_path: + original_file_contents = """this text * is +not * in italics + +this text _ is +not _ in italics + +this text ** is +not ** in bold + +this text __ is +not __ in bold +""" + assert_file_is_as_expected(temp_source_path, original_file_contents) + + supplied_arguments = [ + "-x-fix", + "scan", + temp_source_path, + ] + + expected_return_code = 3 + expected_output = f"Fixed: {temp_source_path}" + expected_error = "" + + expected_file_contents = """this text *is +not* in italics + +this text _is +not_ in italics + +this text **is +not** in bold + +this text __is +not__ in bold +""" + + # Act + execute_results = scanner.invoke_main(arguments=supplied_arguments) + + # Assert + execute_results.assert_results( + expected_output, expected_error, expected_return_code + ) + assert_file_is_as_expected(temp_source_path, expected_file_contents) + + @pytest.mark.rules def test_md037_bad_surrounding_empahsis_setext(): """ @@ -214,6 +431,65 @@ def test_md037_bad_surrounding_empahsis_setext(): ) +@pytest.mark.rules +def test_md037_bad_surrounding_empahsis_setext_fix(): + """ + Test to make sure this rule does trigger with a document that + contains an inline link with space on the right side of the link label. + """ + + # Arrange + scanner = MarkdownScanner() + with copy_to_temp_file( + source_path + "bad_surrounding_empahsis_setext.md" + ) as temp_source_path: + original_file_contents = """this text * is * in italics +=== + +this text _ is _ in italics +--- + +this text ** is ** in bold +--- + +this text __ is __ in bold +--- +""" + assert_file_is_as_expected(temp_source_path, original_file_contents) + + supplied_arguments = [ + "-x-fix", + "scan", + temp_source_path, + ] + + expected_return_code = 3 + expected_output = f"Fixed: {temp_source_path}" + expected_error = "" + + expected_file_contents = """this text *is* in italics +=== + +this text _is_ in italics +--- + +this text **is** in bold +--- + +this text __is__ in bold +--- +""" + + # Act + execute_results = scanner.invoke_main(arguments=supplied_arguments) + + # Assert + execute_results.assert_results( + expected_output, expected_error, expected_return_code + ) + assert_file_is_as_expected(temp_source_path, expected_file_contents) + + @pytest.mark.rules def test_md037_bad_surrounding_empahsis_atx(): """ @@ -250,6 +526,57 @@ def test_md037_bad_surrounding_empahsis_atx(): ) +@pytest.mark.rules +def test_md037_bad_surrounding_empahsis_atx_fix(): + """ + Test to make sure this rule does trigger with a document that + contains an inline link with space on the right side of the link label. + """ + + # Arrange + scanner = MarkdownScanner() + with copy_to_temp_file( + source_path + "bad_surrounding_empahsis_atx.md" + ) as temp_source_path: + original_file_contents = """# this text * is * in italics + +## this text _ is _ in italics + +## this text ** is ** in bold + +## this text __ is __ in bold +""" + assert_file_is_as_expected(temp_source_path, original_file_contents) + + supplied_arguments = [ + "-x-fix", + "scan", + temp_source_path, + ] + + expected_return_code = 3 + expected_output = f"Fixed: {temp_source_path}" + expected_error = "" + + expected_file_contents = """# this text *is* in italics + +## this text _is_ in italics + +## this text **is** in bold + +## this text __is__ in bold +""" + + # Act + execute_results = scanner.invoke_main(arguments=supplied_arguments) + + # Assert + execute_results.assert_results( + expected_output, expected_error, expected_return_code + ) + assert_file_is_as_expected(temp_source_path, expected_file_contents) + + @pytest.mark.rules def test_md037_bad_surrounding_emphasis_containers(): """ @@ -285,6 +612,53 @@ def test_md037_bad_surrounding_emphasis_containers(): ) +@pytest.mark.rules +def test_md037_bad_surrounding_emphasis_containers_fix(): + """ + Test to make sure this rule does trigger with a document that + contains an inline link with space on the right side of the link label. + """ + + # Arrange + scanner = MarkdownScanner() + with copy_to_temp_file( + source_path + "bad_surrounding_emphasis_containers.md" + ) as temp_source_path: + original_file_contents = """1. this is * not in * italics + ++ this is * not in * italics + +> this is * not in * italics +""" + assert_file_is_as_expected(temp_source_path, original_file_contents) + + supplied_arguments = [ + "-x-fix", + "scan", + temp_source_path, + ] + + expected_return_code = 3 + expected_output = f"Fixed: {temp_source_path}" + expected_error = "" + + expected_file_contents = """1. this is *not in* italics + ++ this is *not in* italics + +> this is *not in* italics +""" + + # Act + execute_results = scanner.invoke_main(arguments=supplied_arguments) + + # Assert + execute_results.assert_results( + expected_output, expected_error, expected_return_code + ) + assert_file_is_as_expected(temp_source_path, expected_file_contents) + + @pytest.mark.rules def test_md037_good_emphasis_with_code_span(): """ @@ -345,3 +719,147 @@ def test_md037_good_no_emphasis_but_stars(): execute_results.assert_results( expected_output, expected_error, expected_return_code ) + + +@pytest.mark.rules +def test_md037_bad_surrounding_emphasis_link_surround(): + """ + Test to make sure this rule does trigger with a document that + contains one or two valid emphasis characters surrounded by spaces, + within a single line within a container element. + """ + + # Arrange + scanner = MarkdownScanner() + original_file_contents = """abc * [link](/url) * ghi +""" + with create_temporary_configuration_file( + original_file_contents, file_name_suffix=".md" + ) as temp_source_path: + supplied_arguments = [ + "scan", + temp_source_path, + ] + + expected_return_code = 1 + expected_output = f"{temp_source_path}:1:5: MD037: Spaces inside emphasis markers (no-space-in-emphasis)" + expected_error = "" + + # Act + execute_results = scanner.invoke_main(arguments=supplied_arguments) + + # Assert + execute_results.assert_results( + expected_output, expected_error, expected_return_code + ) + + +@pytest.mark.rules +def test_md037_bad_surrounding_emphasis_link_surround_fix(): + """ + Test to make sure this rule does trigger with a document that + contains an inline link with space on the right side of the link label. + """ + + # Arrange + scanner = MarkdownScanner() + original_file_contents = """abc * [link](/url) * ghi""" + with create_temporary_configuration_file( + original_file_contents, file_name_suffix=".md" + ) as temp_source_path: + supplied_arguments = [ + "-x-fix", + "scan", + temp_source_path, + ] + + expected_return_code = 3 + expected_output = f"Fixed: {temp_source_path}" + expected_error = "" + + expected_file_contents = """abc *[link](/url)* ghi +""" + + # Act + execute_results = scanner.invoke_main(arguments=supplied_arguments) + + # Assert + execute_results.assert_results( + expected_output, expected_error, expected_return_code + ) + assert_file_is_as_expected(temp_source_path, expected_file_contents) + + +@pytest.mark.rules +def test_md037_bad_surrounding_emphasis_link_before_fix(): + """ + Test to make sure this rule does trigger with a document that + contains an inline link with space on the right side of the link label. + """ + + # Arrange + scanner = MarkdownScanner() + original_file_contents = """abc * [link](/url)* ghi +""" + with create_temporary_configuration_file( + original_file_contents, file_name_suffix=".md" + ) as temp_source_path: + supplied_arguments = [ + "--stack-trace", + "-x-fix", + "scan", + temp_source_path, + ] + + expected_return_code = 3 + expected_output = f"Fixed: {temp_source_path}" + expected_error = "" + + expected_file_contents = """abc *[link](/url)* ghi +""" + + # Act + execute_results = scanner.invoke_main(arguments=supplied_arguments) + + # Assert + execute_results.assert_results( + expected_output, expected_error, expected_return_code + ) + assert_file_is_as_expected(temp_source_path, expected_file_contents) + + +@pytest.mark.rules +def test_md037_bad_surrounding_emphasis_link_after_fix(): + """ + Test to make sure this rule does trigger with a document that + contains an inline link with space on the right side of the link label. + """ + + # Arrange + scanner = MarkdownScanner() + original_file_contents = """abc *[link](/url) * ghi +""" + with create_temporary_configuration_file( + original_file_contents, file_name_suffix=".md" + ) as temp_source_path: + supplied_arguments = [ + "-x-fix", + "scan", + temp_source_path, + ] + + expected_return_code = 3 + expected_output = f"Fixed: {temp_source_path}" + expected_error = "" + + expected_file_contents = """abc *[link](/url)* ghi +""" + + # Act + execute_results = scanner.invoke_main(arguments=supplied_arguments) + + # Assert + execute_results.assert_results( + expected_output, expected_error, expected_return_code + ) + assert_file_is_as_expected(temp_source_path, expected_file_contents) diff --git a/test/tokens/test_text_markdown_token.py b/test/tokens/test_text_markdown_token.py new file mode 100644 index 000000000..b422816d6 --- /dev/null +++ b/test/tokens/test_text_markdown_token.py @@ -0,0 +1,24 @@ +from test.tokens.mock_plugin_modify_context import MockPluginModifyContext + +from pymarkdown.tokens.text_markdown_token import TextMarkdownToken + + +def test_text_markdown_token_modify_with_bad_name(): + """ + Test to make sure that try to change this token with a bad name fails. + """ + + # Arrange + modification_context = MockPluginModifyContext() + original_token = TextMarkdownToken( + token_text="bob", + extracted_whitespace="", + line_number=1, + column_number=1, + ) + + # Act + did_modify = original_token.modify_token(modification_context, "bad_name", "") + + # Assert + assert not did_modify