diff --git a/bears/markdown/MarkdownBear.py b/bears/markdown/MarkdownBear.py index 4cd376fa62..5fc1658baa 100644 --- a/bears/markdown/MarkdownBear.py +++ b/bears/markdown/MarkdownBear.py @@ -66,7 +66,27 @@ def create_arguments(filename, file, config_file, horizontal_rule_spaces: bool=False, horizontal_rule_repeat: int=3, max_line_length: int=None, - check_links: bool=False): + check_links: bool=True, + blockquote_indentation: int=2, + enforce_checkbox_content_indentation: bool=True, + code_block_style: str='consistent', + enforce_labels_at_eof: bool=True, + first_heading_level: int=None, + enforce_heading_level_increment: bool=False, + max_heading_length: int=60, + prohibit_duplicate_definitions: bool=True, + prohibit_duplicate_headings_in_section: bool=True, + prohibit_duplicate_headings: bool=True, + prohibit_empty_url: bool=True, + prohibit_irregular_chars_filename: + str='\\.a-zA-Z0-9-_', + prohibit_punctuations_in_heading: str='.,;:!?', + prohibit_html: bool=True, + prohibit_shortcut_reference_image: bool=True, + prohibit_shortcut_reference_link: bool=True, + use_spaces: bool=True, + check_undefined_references: bool=True, + check_unused_definition: bool=True): """ :param bullets: Character to use for bullets in lists. Can be "-", "*" or "+". @@ -111,6 +131,121 @@ def create_arguments(filename, file, config_file, The maximum line length allowed. :param check_links: Checks if links to headings and files in markdown are valid. + :param blockquote_indentation: + Warn when blockquotes are either indented too much or too little. + :param enforce_checkbox_content_indentation: + Warn when list item checkboxes are followed by unnecessary + whitespace. + :param code_block_style: + Warn when code-blocks do not adhere to a given style. Can be + ``consistent``, ``fenced``, or ``indented``. The default value, + ``consistent``, detects the first used code-block style, and will + warn when a subsequent code-block uses a different style. + :param enforce_labels_at_eof: + Warn when definitions are not placed at the end of the file. + For example: If set to ``True``, this will throw a warning:: + + Paragraph. + [example]: http://example.com "Example Domain" + Another paragraph. + + :param first_heading_level: + Warn when the first heading has a level other than a specified + value. + For example: If set to ``2``, this will throw a warning:: + + # Bravo + Paragraph. + + :param enforce_heading_level_increment: + Warn when headings increment with more than 1 level at a time. + For example: If set to ``True``, prefer this:: + + # Alpha + ## Bravo + + over this:: + + # Alpha + ### Bravo + + :param max_heading_length: + The maximum heading length allowed. Ignores markdown syntax, only + checks the plain text content. + :param prohibit_duplicate_definitions: + Warn when duplicate definitions are found. + For example: If set to ``True``, this will throw a warning:: + + [foo]: bar + [foo]: qux + + :param prohibit_duplicate_headings_in_section: + Warn when duplicate headings are found, but only when on the same + level, “in” the same section. + For example: If set to ``True``, this will throw a warning:: + + ## Foxtrot + ### Golf + ### Golf + + :param prohibit_duplicate_headings: + Warn when duplicate headings are found. + For example: If set to ``True``, this will throw a warning:: + + # Foo + ## Foo + ## [Foo](http://foo.com/bar) + + :param prohibit_empty_url: + Warn for empty URLs in links and images. + For example: If set to ``True``, this will throw a warning:: + + [golf](). + ![hotel](). + + :param prohibit_irregular_chars_filename: + Warn when file names contain irregular characters: characters other + than alpha-numericals, dashes, dots (full-stops) and underscores. + Can take ``RegExp`` or ``string``. Any match by the given + expression triggers a warning. + :param prohibit_punctuations_in_heading: + Warn when a heading ends with a group of characters. Can take a + ``string`` that contains the group of characters. + :param prohibit_html: + Warn when HTML elements are used. Ignores comments, because they + are used remark, because markdown doesn’t have native comments. + For example: If set to ``True``, this will throw a warning:: + +

Hello

+ + :param prohibit_shortcut_reference_image: + Warn when shortcut reference images are used. + For example: If set to ``True``, this will throw a warning:: + + ![foo] + [foo]: http://foo.bar/baz.png + + :param prohibit_shortcut_reference_link: + Warn when shortcut reference links are used. + For example: If set to ``True``, this will throw a warning:: + + [foo] + [foo]: http://foo.bar/baz + + :param use_spaces: + Warn when tabs are used instead of spaces. + :param check_undefined_references: + Warn when references to undefined definitions are found. + For example: If set to ``True``, this will throw a warning:: + + [bar][] + + :param check_unused_definition: + Warn when unused definitions are found. + For example: If set to ``True``, this will throw a warning:: + + [bar]: https://example.com + """ remark_configs = { 'bullet': bullets, # - or * @@ -130,7 +265,28 @@ def create_arguments(filename, file, config_file, 'ruleSpaces': horizontal_rule_spaces, # Bool 'ruleRepetition': horizontal_rule_repeat, # int } - remark_lint_configs = {} + remark_lint_configs = { + 'blockquoteIndentation': blockquote_indentation, + 'checkboxContentIndent': enforce_checkbox_content_indentation, + 'codeBlockStyle': code_block_style, + 'finalDefinition': enforce_labels_at_eof, + 'firstHeadingLevel': first_heading_level, + 'headingIncrement': enforce_heading_level_increment, + 'maximumHeadingLength': max_heading_length, + 'noDuplicateDefinitions': prohibit_duplicate_definitions, + 'noDuplicateHeadingsInSection': + prohibit_duplicate_headings_in_section, + 'noDuplicateHeadings': prohibit_duplicate_headings, + 'noEmptyURL': prohibit_empty_url, + 'noFileNameIrregularCharacters': prohibit_irregular_chars_filename, + 'noHeadingPunctuation': prohibit_punctuations_in_heading, + 'noHTML': prohibit_html, + 'noShortcutReferenceImage': prohibit_shortcut_reference_image, + 'noShortcutReferenceLink': prohibit_shortcut_reference_link, + 'noTabs': use_spaces, + 'noUndefinedReferences': check_undefined_references, + 'noUnusedDefinitions': check_unused_definition, + } if max_line_length: remark_lint_configs['maximumLineLength'] = max_line_length diff --git a/tests/markdown/MarkdownBearTest.py b/tests/markdown/MarkdownBearTest.py index 40c0005306..ecde659339 100644 --- a/tests/markdown/MarkdownBearTest.py +++ b/tests/markdown/MarkdownBearTest.py @@ -32,6 +32,55 @@ Read more [This link exists](#world). """ +blockquote_indentation_file = """> Hello + +Paragraph. +""" + +checkbox_content_indentation_file = """Some line. + +- [x] List item +""" + +labels_at_eof_unused_definition_file = """Paragraph. + +[example]: http://example.com "Example Domain" + +Another paragraph. +""" + +first_heading_level_file = """# Bravo + +Paragraph. +""" + +heading_level_increment_file = """# Charlie + +### Delta +""" + +empty_url_file = """[golf](<>). + +![hotel](<>). +""" + +duplicate_heading_file = """## Foxtrot + +### Golf + +### Golf +""" + +punctuation_in_heading_file = """# Hello: + +# Hello? + +# Hello! +""" + +html_file = """

Hello

+""" + MarkdownBearTest = verify_local_bear(MarkdownBear, valid_files=(test_file2,), invalid_files=(test_file1,)) @@ -80,3 +129,94 @@ def test_valid_link(self): with prepare_file(content, None) as (file, fname): with execute_bear(self.uut, fname, file) as results: self.assertEqual(results, []) + + def test_blockquote_indentation(self): + content = blockquote_indentation_file.splitlines() + with prepare_file(content, None) as (file, fname): + with execute_bear(self.uut, fname, file) as results: + self.assertEqual(results[0].message, + 'Remove 1 space between blockquote and ' + 'content') + self.assertEqual(results[0].severity, RESULT_SEVERITY.NORMAL) + + def test_checkbox_content_indentation(self): + content = checkbox_content_indentation_file.splitlines() + with prepare_file(content, None) as (file, fname): + with execute_bear(self.uut, fname, file) as results: + self.assertEqual(results[0].message, + 'Checkboxes should be followed by a single ' + 'character') + self.assertEqual(results[0].severity, RESULT_SEVERITY.NORMAL) + + def test_codeblock_style_unused_definition(self): + content = labels_at_eof_unused_definition_file.splitlines() + with prepare_file(content, None) as (file, fname): + with execute_bear(self.uut, fname, file) as results: + self.assertEqual(results[0].message, + 'Move definitions to the end of the file ' + '(after the node at line `5`)') + self.assertEqual(results[0].severity, RESULT_SEVERITY.NORMAL) + self.assertEqual(results[1].message, + 'Found unused definition') + self.assertEqual(results[1].severity, RESULT_SEVERITY.NORMAL) + + def test_first_heading_level(self): + content = first_heading_level_file.splitlines() + self.section.append(Setting('first_heading_level', 2)) + with prepare_file(content, None) as (file, fname): + with execute_bear(self.uut, fname, file) as results: + self.assertEqual(results[0].message, + 'First heading level should be `2`') + self.assertEqual(results[0].severity, RESULT_SEVERITY.NORMAL) + + def test_heading_level_increment(self): + content = heading_level_increment_file.splitlines() + self.section.append(Setting('enforce_heading_level_increment', True)) + with prepare_file(content, None) as (file, fname): + with execute_bear(self.uut, fname, file) as results: + self.assertEqual(results[0].message, + 'Heading levels should increment by one ' + 'level at a time') + self.assertEqual(results[0].severity, RESULT_SEVERITY.NORMAL) + + def test_empty_url(self): + content = empty_url_file.splitlines() + with prepare_file(content, None) as (file, fname): + with execute_bear(self.uut, fname, file) as results: + self.assertEqual(results[0].message, + 'Don’t use links without URL') + self.assertEqual(results[0].severity, RESULT_SEVERITY.NORMAL) + self.assertEqual(results[1].message, + 'Don’t use images without URL') + self.assertEqual(results[1].severity, RESULT_SEVERITY.NORMAL) + + def test_duplicate_headings(self): + content = duplicate_heading_file.splitlines() + with prepare_file(content, None) as (file, fname): + with execute_bear(self.uut, fname, file) as results: + self.assertEqual(results[0].message, + 'Do not use headings with similar content ' + 'per section (3:1)') + self.assertEqual(results[0].severity, RESULT_SEVERITY.NORMAL) + + def test_punctuations_in_heading(self): + content = punctuation_in_heading_file.splitlines() + with prepare_file(content, None) as (file, fname): + with execute_bear(self.uut, fname, file) as results: + self.assertEqual(results[0].message, + 'Don’t add a trailing `:` to headings') + self.assertEqual(results[0].severity, RESULT_SEVERITY.NORMAL) + self.assertEqual(results[1].message, + 'Don’t add a trailing `?` to headings') + self.assertEqual(results[1].severity, RESULT_SEVERITY.NORMAL) + self.assertEqual(results[2].message, + 'Don’t add a trailing `!` to headings') + self.assertEqual(results[2].severity, RESULT_SEVERITY.NORMAL) + + def test_html_in_markdown(self): + content = html_file.splitlines() + with prepare_file(content, None) as (file, fname): + with execute_bear(self.uut, fname, file) as results: + self.assertEqual(results[0].message, + 'Do not use HTML in markdown') + self.assertEqual(results[0].severity, RESULT_SEVERITY.NORMAL)