├── test
├── rule_tests
│ ├── empty_doc.md
│ ├── first_header_good_atx.md
│ ├── no_first_line_header_style.rb
│ ├── first_header_bad_atx.md
│ ├── no_first_line_top_level_header_style.rb
│ ├── first_header_good_setext.md
│ ├── code_block_fenced_style.rb
│ ├── bulletd_list_sublist_style.rb
│ ├── code_block_indented_style.rb
│ ├── first_header_bad_setext.md
│ ├── code_block_consistency_style.rb
│ ├── consistent_bullet_styles_dash.md
│ ├── consistent_bullet_styles_plus.md
│ ├── header_multiple_toplevel.md
│ ├── incorrect_header_atx_style.rb
│ ├── consistent_bullet_styles_asterisk.md
│ ├── headers_good.md
│ ├── incorrect_header_setext_style.rb
│ ├── no_first_line_header.md
│ ├── no_first_line_top_level_header.md
│ ├── first_line_top_level_header_atx_style.rb
│ ├── first_line_top_level_header_setext_style.rb
│ ├── incorrect_bullet_style_dash.md
│ ├── incorrect_bullet_style_plus.md
│ ├── incorrect_header_atx_closed_style.rb
│ ├── headers_good_setext_with_atx_style.rb
│ ├── hr_style_dashes_style.rb
│ ├── hr_style_long_style.rb
│ ├── hr_style_stars_style.rb
│ ├── incorrect_bullet_style_asterisk.md
│ ├── whitespace_issues.md
│ ├── alternate_top_level_header.md
│ ├── header_trailing_punctuation_customized_style.rb
│ ├── inline_html_style.rb
│ ├── long_lines_100_style.rb
│ ├── trailing_spaces_br_style.rb
│ ├── bulleted_list_2_space_indent_style.rb
│ ├── bulleted_list_4_space_indent.md
│ ├── fenced_code_without_blank_lines_style.rb
│ ├── header_duplicate_content_different_nesting_style.rb
│ ├── header_mutliple_h1_no_toplevel.md
│ ├── atx_header_spacing.md
│ ├── incorrect_bullet_style_dash_style.rb
│ ├── incorrect_bullet_style_plus_style.rb
│ ├── first_line_top_level_header_atx.md
│ ├── hard_tabs_code_blocks_style.rb
│ ├── headers_bad.md
│ ├── headers_good_setext_with_atx.md
│ ├── inconsistent_bullet_indent_same_level.md
│ ├── incorrect_bullet_style_asterisk_style.rb
│ ├── incorrect_header_setext.md
│ ├── ordered_list_item_prefix_ordered_style.rb
│ ├── incorrect_header_atx.md
│ ├── long_lines_code_style.rb
│ ├── mixed_header_types_atx.md
│ ├── mixed_header_types_setext.md
│ ├── default_test_style.rb
│ ├── incorrect_header_atx_closed.md
│ ├── mixed_header_types_atx_closed.md
│ ├── hard_tabs_code_blocks.md
│ ├── alternate_top_level_header_style.rb
│ ├── first_line_top_level_header_setext.md
│ ├── md013_ignore_long_lines_in_code_block_style.rb
│ ├── file_ends_with_single_newline_character_good.md
│ ├── file_ends_with_single_newline_character_bad.md
│ ├── header_duplicate_content.md
│ ├── spaces_after_list_marker_style.rb
│ ├── header_duplicate_content_different_nesting.md
│ ├── inconsistent_bullet_styles_dash.md
│ ├── inconsistent_bullet_styles_plus.md
│ ├── inconsistent_bullet_styles_asterisk.md
│ ├── code_block_consistency.md
│ ├── consecutive_blank_lines.md
│ ├── header_duplicate_content_no_different_nesting.md
│ ├── bulleted_list_2_space_indent.md
│ ├── headers_good_with_issue_numbers.md
│ ├── header_trailing_punctuation.md
│ ├── ordered_list_item_prefix.md
│ ├── ordered_list_item_prefix_ordered.md
│ ├── trailing_spaces_br.md
│ ├── long_lines.md
│ ├── bulleted_list_not_at_beginning_of_line.md
│ ├── reversed_link.md
│ ├── hr_style_stars.md
│ ├── headers_surrounding_space_setext.md
│ ├── hr_style_inconsistent.md
│ ├── links.md
│ ├── hr_style_dashes.md
│ ├── hr_style_long.md
│ ├── code_block_indented.md
│ ├── code_block_fenced.md
│ ├── atx_closed_header_spacing.md
│ ├── header_trailing_punctuation_customized.md
│ ├── headers_surrounding_space_atx.md
│ ├── code_block_dollar_fence.md
│ ├── bulletd_list_sublist.md
│ ├── fix_102_extra_nodes_in_link_text.md
│ ├── spaces_inside_codespan_elements.md
│ ├── md013_ignore_long_lines_in_code_block.md
│ ├── long_lines_100.md
│ ├── fenced_code_without_blank_lines.md
│ ├── fenced_code_blocks.md
│ ├── inline_html.md
│ ├── headers_with_spaces_at_the_beginning.md
│ ├── blockquote_spaces.md
│ ├── spaces_inside_link_text.md
│ ├── blockquote_blank_lines.md
│ ├── fenced_code_blocks_in_lists.md
│ ├── code_block_dollar.md
│ ├── fenced_code_with_nesting.md
│ ├── spaces_inside_emphasis_markers.md
│ ├── lists_without_blank_lines.md
│ ├── spaces_after_list_marker.md
│ ├── emphasis_instead_of_headers.md
│ └── long_lines_code.md
├── fixtures
│ ├── default_mdlrc
│ ├── unprintable_chars
│ │ ├── test2.md
│ │ ├── test1.md
│ │ └── test3.md
│ ├── output
│ │ ├── json
│ │ │ ├── without_matches.json
│ │ │ └── with_matches.json
│ │ └── sarif
│ │ │ ├── without_matches.sarif
│ │ │ └── with_matches.sarif
│ ├── mdlrc_disable_rules
│ ├── mdlrc_disable_tags
│ ├── mdlrc_enable_rules
│ ├── mdlrc_enable_tags
│ ├── dir_with_md_and_markdown
│ │ ├── foo.md
│ │ └── bar.markdown
│ ├── fake_tty.rb
│ ├── my_ruleset.rb
│ ├── docs_ruleset_1.rb
│ ├── front_matter
│ │ ├── jekyll_post.md
│ │ └── jekyll_post_2.md
│ └── docs_ruleset_2.rb
├── setup_tests.rb
├── test_rules.rb
├── test_ruledocs.rb
└── test_cli.rb
├── .mdlrc
├── lib
├── mdl
│ ├── styles
│ │ ├── all.rb
│ │ ├── default.rb
│ │ ├── relaxed.rb
│ │ └── cirosantilli.rb
│ ├── version.rb
│ ├── config.rb
│ ├── kramdown_parser.rb
│ ├── ruleset.rb
│ ├── style.rb
│ ├── formatters
│ │ └── sarif.rb
│ ├── cli.rb
│ ├── doc.rb
│ └── rules.rb
└── mdl.rb
├── Gemfile
├── example
├── new_style_example.rb
└── .mdlrc_example
├── .mdl_style.rb
├── .gitignore
├── Rakefile
├── tools
├── README.md
├── view_markdown.rb
├── docker
│ ├── Dockerfile
│ └── README.md
└── test_location.rb
├── bin
└── mdl
├── .pre-commit-hooks.yaml
├── .github
├── workflows
│ ├── dco.yml
│ └── ci.yml
├── ISSUE_TEMPLATE
│ ├── BUG_TEMPLATE.md
│ ├── ENHANCEMENT_REQUEST.md
│ ├── RULE_REQUEST.md
│ └── DESIGN_PROPOSAL.md
└── PULL_REQUEST_TEMPLATE.md
├── MAINTAINERS.md
├── LICENSE.txt
├── .rubocop.yml
├── mdl.gemspec
├── docs
├── creating_styles.md
├── rolling_a_release.md
├── configuration.md
└── creating_rules.md
├── README.md
├── CONTRIBUTING.md
└── CHANGELOG.md
/test/rule_tests/empty_doc.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.mdlrc:
--------------------------------------------------------------------------------
1 | style '.mdl_style.rb'
2 |
--------------------------------------------------------------------------------
/lib/mdl/styles/all.rb:
--------------------------------------------------------------------------------
1 | all
2 |
--------------------------------------------------------------------------------
/test/fixtures/default_mdlrc:
--------------------------------------------------------------------------------
1 | # Blank .mdlrc
2 |
--------------------------------------------------------------------------------
/test/fixtures/unprintable_chars/test2.md:
--------------------------------------------------------------------------------
1 | A
* B
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | gemspec
3 |
--------------------------------------------------------------------------------
/test/fixtures/output/json/without_matches.json:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/test/rule_tests/first_header_good_atx.md:
--------------------------------------------------------------------------------
1 | # Header
2 |
--------------------------------------------------------------------------------
/test/rule_tests/no_first_line_header_style.rb:
--------------------------------------------------------------------------------
1 | all
2 |
--------------------------------------------------------------------------------
/test/fixtures/unprintable_chars/test1.md:
--------------------------------------------------------------------------------
1 | A
2 |
3 | * B
--------------------------------------------------------------------------------
/test/fixtures/unprintable_chars/test3.md:
--------------------------------------------------------------------------------
1 | A
2 |
3 | B
--------------------------------------------------------------------------------
/test/fixtures/mdlrc_disable_rules:
--------------------------------------------------------------------------------
1 | rules "~MD001", "~MD002"
2 |
--------------------------------------------------------------------------------
/test/fixtures/mdlrc_disable_tags:
--------------------------------------------------------------------------------
1 | tags "ul", "~indentation"
2 |
--------------------------------------------------------------------------------
/test/fixtures/mdlrc_enable_rules:
--------------------------------------------------------------------------------
1 | rules "MD001", "MD002"
2 |
--------------------------------------------------------------------------------
/test/rule_tests/first_header_bad_atx.md:
--------------------------------------------------------------------------------
1 | ## Header {MD002}
2 |
--------------------------------------------------------------------------------
/test/rule_tests/no_first_line_top_level_header_style.rb:
--------------------------------------------------------------------------------
1 | all
2 |
--------------------------------------------------------------------------------
/test/fixtures/mdlrc_enable_tags:
--------------------------------------------------------------------------------
1 | tags "headers", "whitespace"
2 |
--------------------------------------------------------------------------------
/test/rule_tests/first_header_good_setext.md:
--------------------------------------------------------------------------------
1 | Header
2 | ======
3 |
--------------------------------------------------------------------------------
/test/fixtures/dir_with_md_and_markdown/foo.md:
--------------------------------------------------------------------------------
1 | ## Second Header First
2 |
--------------------------------------------------------------------------------
/example/new_style_example.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD013', :line_length => 500
3 |
--------------------------------------------------------------------------------
/test/fixtures/dir_with_md_and_markdown/bar.markdown:
--------------------------------------------------------------------------------
1 | ## Second Header First
2 |
--------------------------------------------------------------------------------
/test/rule_tests/code_block_fenced_style.rb:
--------------------------------------------------------------------------------
1 | rule 'MD046', :style => :fenced
2 |
--------------------------------------------------------------------------------
/lib/mdl/version.rb:
--------------------------------------------------------------------------------
1 | module MarkdownLint
2 | VERSION = '0.13.0'.freeze
3 | end
4 |
--------------------------------------------------------------------------------
/test/rule_tests/bulletd_list_sublist_style.rb:
--------------------------------------------------------------------------------
1 | rule 'MD004', :style => :sublist
2 |
--------------------------------------------------------------------------------
/test/rule_tests/code_block_indented_style.rb:
--------------------------------------------------------------------------------
1 | rule 'MD046', :style => :indented
2 |
--------------------------------------------------------------------------------
/test/rule_tests/first_header_bad_setext.md:
--------------------------------------------------------------------------------
1 | Header {MD002}
2 | --------------
3 |
--------------------------------------------------------------------------------
/test/rule_tests/code_block_consistency_style.rb:
--------------------------------------------------------------------------------
1 | rule 'MD046', :style => :consistent
2 |
--------------------------------------------------------------------------------
/test/rule_tests/consistent_bullet_styles_dash.md:
--------------------------------------------------------------------------------
1 | - Item
2 | - Item
3 | - Item
4 |
--------------------------------------------------------------------------------
/test/rule_tests/consistent_bullet_styles_plus.md:
--------------------------------------------------------------------------------
1 | + Item
2 | + Item
3 | + Item
4 |
--------------------------------------------------------------------------------
/test/rule_tests/header_multiple_toplevel.md:
--------------------------------------------------------------------------------
1 | # Heading 1
2 |
3 | # Heading 2 {MD025}
4 |
--------------------------------------------------------------------------------
/test/rule_tests/incorrect_header_atx_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD003', :style => :atx
3 |
--------------------------------------------------------------------------------
/test/rule_tests/consistent_bullet_styles_asterisk.md:
--------------------------------------------------------------------------------
1 | * Item
2 | * Item
3 | * Item
4 |
--------------------------------------------------------------------------------
/test/rule_tests/headers_good.md:
--------------------------------------------------------------------------------
1 | # Heading 1
2 |
3 | ## Heading 2
4 |
5 | ## Heading 3
6 |
--------------------------------------------------------------------------------
/test/rule_tests/incorrect_header_setext_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD003', :style => :setext
3 |
--------------------------------------------------------------------------------
/test/rule_tests/no_first_line_header.md:
--------------------------------------------------------------------------------
1 | This is a file without a top level header {MD041}
2 |
--------------------------------------------------------------------------------
/test/rule_tests/no_first_line_top_level_header.md:
--------------------------------------------------------------------------------
1 | ## Second level header {MD041} {MD002}
2 |
--------------------------------------------------------------------------------
/example/.mdlrc_example:
--------------------------------------------------------------------------------
1 | style "#{File.dirname(__FILE__)}/{your_markdown_rule_file_path}.rb"
2 |
--------------------------------------------------------------------------------
/test/rule_tests/first_line_top_level_header_atx_style.rb:
--------------------------------------------------------------------------------
1 | # Only test MD041 here
2 | rule 'MD041'
3 |
--------------------------------------------------------------------------------
/test/rule_tests/first_line_top_level_header_setext_style.rb:
--------------------------------------------------------------------------------
1 | # Only test MD041 here
2 | rule 'MD041'
3 |
--------------------------------------------------------------------------------
/test/rule_tests/incorrect_bullet_style_dash.md:
--------------------------------------------------------------------------------
1 | * Item {MD004}
2 | - Item
3 | + Item {MD004}
4 |
--------------------------------------------------------------------------------
/test/rule_tests/incorrect_bullet_style_plus.md:
--------------------------------------------------------------------------------
1 | * Item {MD004}
2 | - Item {MD004}
3 | + Item
4 |
--------------------------------------------------------------------------------
/test/rule_tests/incorrect_header_atx_closed_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD003', :style => :atx_closed
3 |
--------------------------------------------------------------------------------
/test/rule_tests/headers_good_setext_with_atx_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD003', :style => :setext_with_atx
3 |
--------------------------------------------------------------------------------
/test/rule_tests/hr_style_dashes_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD035', :style => '---'
3 | exclude_rule 'MD041'
4 |
--------------------------------------------------------------------------------
/test/rule_tests/hr_style_long_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD035', :style => '_____'
3 | exclude_rule 'MD041'
4 |
--------------------------------------------------------------------------------
/test/rule_tests/hr_style_stars_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD035', :style => '***'
3 | exclude_rule 'MD041'
4 |
--------------------------------------------------------------------------------
/test/rule_tests/incorrect_bullet_style_asterisk.md:
--------------------------------------------------------------------------------
1 | * Item
2 | - Item {MD004}
3 | + Item {MD004}
4 |
--------------------------------------------------------------------------------
/test/rule_tests/whitespace_issues.md:
--------------------------------------------------------------------------------
1 | Some text {MD009}
2 | Some more text {MD010}
3 | Some more text
4 |
--------------------------------------------------------------------------------
/test/rule_tests/alternate_top_level_header.md:
--------------------------------------------------------------------------------
1 | ## A level 2 top level header
2 |
3 | ## Another one {MD025}
4 |
--------------------------------------------------------------------------------
/test/rule_tests/header_trailing_punctuation_customized_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD026', :punctuation => '.,;:!'
3 |
--------------------------------------------------------------------------------
/test/rule_tests/inline_html_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | exclude_rule 'MD046'
3 | rule 'MD033', :allowed_elements => 'br'
4 |
--------------------------------------------------------------------------------
/test/rule_tests/long_lines_100_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD013', :line_length => 100
3 | exclude_rule 'MD041'
4 |
--------------------------------------------------------------------------------
/test/rule_tests/trailing_spaces_br_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD009', :br_spaces => 2
3 | exclude_rule 'MD041'
4 |
--------------------------------------------------------------------------------
/test/rule_tests/bulleted_list_2_space_indent_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD007', :indent => 4
3 | exclude_rule 'MD041'
4 |
--------------------------------------------------------------------------------
/test/rule_tests/bulleted_list_4_space_indent.md:
--------------------------------------------------------------------------------
1 | * Test X
2 | * Test Y {MD007}
3 | * Test Z {MD007}
4 |
--------------------------------------------------------------------------------
/test/rule_tests/fenced_code_without_blank_lines_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | exclude_rule 'MD040'
3 | exclude_rule 'MD041'
4 |
--------------------------------------------------------------------------------
/test/rule_tests/header_duplicate_content_different_nesting_style.rb:
--------------------------------------------------------------------------------
1 | rule 'MD024', :allow_different_nesting => true
2 |
--------------------------------------------------------------------------------
/test/rule_tests/header_mutliple_h1_no_toplevel.md:
--------------------------------------------------------------------------------
1 | Some introductory text
2 |
3 | # Heading 1
4 |
5 | # Heading 2
6 |
--------------------------------------------------------------------------------
/test/rule_tests/atx_header_spacing.md:
--------------------------------------------------------------------------------
1 | #Header 1 {MD018}
2 |
3 | ## Header 2 {MD019}
4 |
5 | ## Header 3 {MD019}
6 |
--------------------------------------------------------------------------------
/test/rule_tests/incorrect_bullet_style_dash_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD004', :style => :dash
3 | exclude_rule 'MD041'
4 |
--------------------------------------------------------------------------------
/test/rule_tests/incorrect_bullet_style_plus_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD004', :style => :plus
3 | exclude_rule 'MD041'
4 |
--------------------------------------------------------------------------------
/test/rule_tests/first_line_top_level_header_atx.md:
--------------------------------------------------------------------------------
1 | # First line is a top level header
2 |
3 | This shouldn't trigger MD041
4 |
--------------------------------------------------------------------------------
/test/rule_tests/hard_tabs_code_blocks_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD010', :ignore_code_blocks => true
3 | exclude_rule 'MD041'
4 |
--------------------------------------------------------------------------------
/test/rule_tests/headers_bad.md:
--------------------------------------------------------------------------------
1 | # Header
2 |
3 | ### Header 3 {MD001}
4 |
5 | ## Header 2
6 |
7 | #### Header 4 {MD001}
8 |
--------------------------------------------------------------------------------
/test/rule_tests/headers_good_setext_with_atx.md:
--------------------------------------------------------------------------------
1 | Header 1
2 | ========
3 |
4 | Header 2
5 | --------
6 |
7 | ### Header 3
8 |
--------------------------------------------------------------------------------
/test/rule_tests/inconsistent_bullet_indent_same_level.md:
--------------------------------------------------------------------------------
1 | * Item
2 | * Item {MD007}
3 | * Item {MD005}
4 | * Item
5 |
--------------------------------------------------------------------------------
/test/rule_tests/incorrect_bullet_style_asterisk_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD004', :style => :asterisk
3 | exclude_rule 'MD041'
4 |
--------------------------------------------------------------------------------
/test/rule_tests/incorrect_header_setext.md:
--------------------------------------------------------------------------------
1 | # Header 1 {MD003} #
2 |
3 | ## Header 2 {MD003}
4 |
5 | Header 3
6 | --------
7 |
--------------------------------------------------------------------------------
/test/rule_tests/ordered_list_item_prefix_ordered_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD029', :style => :ordered
3 | exclude_rule 'MD041'
4 |
--------------------------------------------------------------------------------
/test/rule_tests/incorrect_header_atx.md:
--------------------------------------------------------------------------------
1 | # Header 1 {MD003} #
2 |
3 | ## Header 2
4 |
5 | Header 3 {MD003}
6 | ----------------
7 |
--------------------------------------------------------------------------------
/test/rule_tests/long_lines_code_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD013', :code_blocks => false, :tables => false
3 | exclude_rule 'MD041'
4 |
--------------------------------------------------------------------------------
/test/rule_tests/mixed_header_types_atx.md:
--------------------------------------------------------------------------------
1 | # Header
2 |
3 | ## Header 2 {MD003} ##
4 |
5 | Header 3 {MD003}
6 | ----------------
7 |
--------------------------------------------------------------------------------
/test/rule_tests/mixed_header_types_setext.md:
--------------------------------------------------------------------------------
1 | Header 1
2 | ========
3 |
4 | ## Header 2 {MD003}
5 |
6 | ## Header 3 {MD003} ##
7 |
--------------------------------------------------------------------------------
/test/setup_tests.rb:
--------------------------------------------------------------------------------
1 | require 'bundler/setup'
2 | Bundler.setup
3 |
4 | require_relative '../lib/mdl'
5 | require 'minitest/autorun'
6 |
--------------------------------------------------------------------------------
/test/rule_tests/default_test_style.rb:
--------------------------------------------------------------------------------
1 | # Default style file for rule tests
2 | all
3 |
4 | exclude_rule 'MD041'
5 | # exclude_rule "MD046"
6 |
--------------------------------------------------------------------------------
/test/rule_tests/incorrect_header_atx_closed.md:
--------------------------------------------------------------------------------
1 | # Header 1 #
2 |
3 | ## Header 2 {MD003}
4 |
5 | Header 3 {MD003}
6 | ----------------
7 |
--------------------------------------------------------------------------------
/test/rule_tests/mixed_header_types_atx_closed.md:
--------------------------------------------------------------------------------
1 | # Header 1 #
2 |
3 | ## Header 2 {MD003}
4 |
5 | Header 3 {MD003}
6 | ----------------
7 |
--------------------------------------------------------------------------------
/test/rule_tests/hard_tabs_code_blocks.md:
--------------------------------------------------------------------------------
1 | ```makefile
2 | text after hard tab
3 | ```
4 |
5 | Text before hard tab text after hard tab {MD010}
6 |
--------------------------------------------------------------------------------
/test/rule_tests/alternate_top_level_header_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD002', :level => 2
3 | rule 'MD025', :level => 2
4 | rule 'MD041', :level => 2
5 |
--------------------------------------------------------------------------------
/test/rule_tests/first_line_top_level_header_setext.md:
--------------------------------------------------------------------------------
1 | First line top level header
2 | ===========================
3 |
4 | This shouldn't trigger MD041
5 |
--------------------------------------------------------------------------------
/.mdl_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | # our changelog does this, by design
3 | exclude_rule 'MD024'
4 | # default in next version, remove then
5 | rule 'MD007', :indent => 3
6 |
--------------------------------------------------------------------------------
/test/rule_tests/md013_ignore_long_lines_in_code_block_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD013', :ignore_code_blocks => true, :tables => false
3 | exclude_rule 'MD041'
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Bundler/Ruby
2 | vendor/
3 | .bundle
4 | Gemfile.lock
5 | spec/reports
6 | tmp
7 | *.gem
8 |
9 | # Vim
10 | *.swp
11 |
12 | # Mac
13 | .DS_Store
14 |
--------------------------------------------------------------------------------
/test/rule_tests/file_ends_with_single_newline_character_good.md:
--------------------------------------------------------------------------------
1 | # File ending with a single newline character
2 |
3 | This file ends with a single newline character
4 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler/gem_tasks'
2 | require 'rake/testtask'
3 |
4 | task :default => :test
5 |
6 | Rake::TestTask.new do |t|
7 | t.pattern = 'test/test_*.rb'
8 | end
9 |
--------------------------------------------------------------------------------
/test/rule_tests/file_ends_with_single_newline_character_bad.md:
--------------------------------------------------------------------------------
1 | # File not ending with a single newline character
2 |
3 | This file does not end with a single newline character{MD047}
--------------------------------------------------------------------------------
/test/fixtures/fake_tty.rb:
--------------------------------------------------------------------------------
1 | # A hacky 'ruleset' file which forces an externally run
2 | # mdl instance to believe it's in a TTY, used for testing.
3 | def $stdout.tty?
4 | true
5 | end
6 |
--------------------------------------------------------------------------------
/test/rule_tests/header_duplicate_content.md:
--------------------------------------------------------------------------------
1 | # Header 1
2 |
3 | ## Header 2
4 |
5 | ## Header 1
6 |
7 | ### Header 2
8 |
9 | ## Header 3
10 |
11 | {MD024:5} {MD024:7}
12 |
--------------------------------------------------------------------------------
/test/rule_tests/spaces_after_list_marker_style.rb:
--------------------------------------------------------------------------------
1 | all
2 | rule 'MD007', :indent => 4
3 | rule 'MD030', :ul_multi => 3, :ol_multi => 2
4 | exclude_rule 'MD041'
5 | exclude_rule 'MD046'
6 |
--------------------------------------------------------------------------------
/test/fixtures/my_ruleset.rb:
--------------------------------------------------------------------------------
1 | rule 'MY001', 'Documents must start with Hello World' do
2 | tags :opinionated
3 | check do |doc|
4 | [1] if doc.lines[0] != 'Hello World'
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/test/rule_tests/header_duplicate_content_different_nesting.md:
--------------------------------------------------------------------------------
1 | # Change log
2 |
3 | ## 2.0.0
4 |
5 | ### Bug fixes
6 |
7 | ### Features
8 |
9 | ## 1.0.0
10 |
11 | ### Bug fixes
12 |
--------------------------------------------------------------------------------
/tools/README.md:
--------------------------------------------------------------------------------
1 | # Tools
2 |
3 | This directory contains a few scripts that, while not part of markdownlint
4 | directly, can be useful for inspecting markdown documents and debugging
5 | issues.
6 |
--------------------------------------------------------------------------------
/lib/mdl/styles/default.rb:
--------------------------------------------------------------------------------
1 | all
2 |
3 | exclude_rule 'fenced-code-language' # Fenced code blocks should have a language
4 | exclude_rule 'first-line-h1' # First line in file should be a top level header
5 |
--------------------------------------------------------------------------------
/test/rule_tests/inconsistent_bullet_styles_dash.md:
--------------------------------------------------------------------------------
1 | - Item
2 | * Item {MD004}
3 | + Item {MD004}
4 | - Item
5 |
6 | > - Item
7 | > * Item {MD004}
8 | > + Item {MD004}
9 | > - Item
10 |
--------------------------------------------------------------------------------
/test/rule_tests/inconsistent_bullet_styles_plus.md:
--------------------------------------------------------------------------------
1 | + Item
2 | * Item {MD004}
3 | - Item {MD004}
4 | + Item
5 |
6 | > + Item
7 | > * Item {MD004}
8 | > - Item {MD004}
9 | > + Item
10 |
--------------------------------------------------------------------------------
/test/rule_tests/inconsistent_bullet_styles_asterisk.md:
--------------------------------------------------------------------------------
1 | * Item
2 | + Item {MD004}
3 | - Item {MD004}
4 | * Item
5 |
6 | > * Item
7 | > + Item {MD004}
8 | > - Item {MD004}
9 | > * Item
10 |
--------------------------------------------------------------------------------
/test/rule_tests/code_block_consistency.md:
--------------------------------------------------------------------------------
1 | This is text.
2 |
3 | This is a
4 | code block.
5 |
6 | And here is more text
7 |
8 | ```
9 | and here is a different {MD046:8}
10 | code block
11 | ```
12 |
--------------------------------------------------------------------------------
/test/rule_tests/consecutive_blank_lines.md:
--------------------------------------------------------------------------------
1 | Some text
2 |
3 |
4 | Some text {MD012:3}
5 |
6 | ```fenced
7 | This is a code block
8 |
9 |
10 | with two blank lines in it
11 | ```
12 |
13 | Some more text
14 |
--------------------------------------------------------------------------------
/test/rule_tests/header_duplicate_content_no_different_nesting.md:
--------------------------------------------------------------------------------
1 | # Change log
2 |
3 | ## 2.0.0
4 |
5 | ### Bug fixes
6 |
7 | ### Features
8 |
9 | ## 1.0.0
10 |
11 | ### Bug fixes
12 |
13 | {MD024:11}
14 |
--------------------------------------------------------------------------------
/lib/mdl/config.rb:
--------------------------------------------------------------------------------
1 | require 'mixlib/config'
2 |
3 | module MarkdownLint
4 | # our Mixlib::Config class
5 | module Config
6 | extend Mixlib::Config
7 |
8 | default :style, 'default'
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/test/rule_tests/bulleted_list_2_space_indent.md:
--------------------------------------------------------------------------------
1 | This is a document where the lists are indented by 2 spaces, but the style is
2 | set to 4 space indents for lists:
3 |
4 | * Test X
5 | * Test Y {MD007}
6 | * Test Z {MD007}
7 |
--------------------------------------------------------------------------------
/test/rule_tests/headers_good_with_issue_numbers.md:
--------------------------------------------------------------------------------
1 | # Heading 1
2 |
3 | ## Heading 2
4 |
5 | See the following issues:
6 |
7 | * #1234
8 | * #5678 (and related)
9 | * #5678
10 | * #9101
11 |
12 | ## Heading 3
13 |
--------------------------------------------------------------------------------
/test/rule_tests/header_trailing_punctuation.md:
--------------------------------------------------------------------------------
1 | # Heading 1 {MD026}.
2 |
3 | ## Heading 2 {MD026},
4 |
5 | ## Heading 3 {MD026}!
6 |
7 | ## Heading 4 {MD026}:
8 |
9 | ## Heading 5 {MD026};
10 |
11 | ## Heading 6 {MD026}?
12 |
--------------------------------------------------------------------------------
/test/rule_tests/ordered_list_item_prefix.md:
--------------------------------------------------------------------------------
1 | Good list:
2 |
3 | 1. Do this.
4 | 1. Do that.
5 | 1. ???
6 | 1. Profit!
7 |
8 | Bad list:
9 |
10 | 1. Do this.
11 | 2. Do nothing. {MD029}
12 | 3. ??? {MD029}
13 | 4. Failed! {MD029}
14 |
--------------------------------------------------------------------------------
/test/rule_tests/ordered_list_item_prefix_ordered.md:
--------------------------------------------------------------------------------
1 | Good list:
2 |
3 | 1. Do this.
4 | 2. Do that.
5 | 3. ???
6 | 4. Profit!
7 |
8 | Bad list:
9 |
10 | 1. Do this.
11 | 1. Do nothing. {MD029}
12 | 1. ??? {MD029}
13 | 1. Failed! {MD029}
14 |
--------------------------------------------------------------------------------
/test/rule_tests/trailing_spaces_br.md:
--------------------------------------------------------------------------------
1 | This line has a single trailing space {MD009}
2 | This line has two trailing spaces and should be allowed
3 | This line has three trailing spaces {MD009}
4 | This line has four trailing spaces {MD009}
5 |
--------------------------------------------------------------------------------
/test/rule_tests/long_lines.md:
--------------------------------------------------------------------------------
1 | This is a very very very very very very very very very very very very very very long line {MD013}
2 |
3 | This line however, while very long, doesn't have whitespace after the 80th columnwhichallowsforURLsandotherlongthings.
4 |
--------------------------------------------------------------------------------
/bin/mdl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | require 'mdl'
4 | rescue LoadError
5 | # For running in development without bundler
6 | $LOAD_PATH << File.expand_path('../lib', File.dirname(__FILE__))
7 | require 'mdl'
8 | end
9 |
10 | MarkdownLint.run
11 |
--------------------------------------------------------------------------------
/test/rule_tests/bulleted_list_not_at_beginning_of_line.md:
--------------------------------------------------------------------------------
1 | Some text
2 |
3 | * Item {MD006}
4 | * Item
5 | * Item
6 | * Item
7 | * Item
8 | * Item
9 | * Item
10 |
11 | Some more text
12 |
13 | * Item {MD006}
14 | * Item
15 |
--------------------------------------------------------------------------------
/test/rule_tests/reversed_link.md:
--------------------------------------------------------------------------------
1 | Go to (this website)[http://www.example.com] {MD011} {MD034}
2 |
3 | However, this shouldn't trigger inside code blocks:
4 |
5 | ```fenced
6 | myObj.getFiles("test")[0]
7 | ```
8 |
9 | Nor inline code: `myobj.getFiles("test")[0]`
10 |
--------------------------------------------------------------------------------
/test/rule_tests/hr_style_stars.md:
--------------------------------------------------------------------------------
1 | ***
2 |
3 | * * *
4 |
5 | *****
6 |
7 | ---
8 |
9 | - - -
10 |
11 | -----
12 |
13 | ___
14 |
15 | _ _ _
16 |
17 | _____
18 |
19 | ***
20 |
21 | {MD035:3} {MD035:5} {MD035:7} {MD035:9} {MD035:11} {MD035:13} {MD035:15}
22 | {MD035:17}
23 |
--------------------------------------------------------------------------------
/test/rule_tests/headers_surrounding_space_setext.md:
--------------------------------------------------------------------------------
1 | Header 1
2 | ========
3 |
4 | Header 2 {MD022}
5 | ----------------
6 | Some text
7 | Header 3 {MD022}
8 | ================
9 | Some text
10 | Header 4 {MD022}
11 | ================
12 | Some text
13 |
14 | Header 5
15 | --------
16 |
--------------------------------------------------------------------------------
/test/rule_tests/hr_style_inconsistent.md:
--------------------------------------------------------------------------------
1 | ***
2 |
3 | * * *
4 |
5 | *****
6 |
7 | ---
8 |
9 | - - -
10 |
11 | -----
12 |
13 | ___
14 |
15 | _ _ _
16 |
17 | _____
18 |
19 | ***
20 |
21 | {MD035:3} {MD035:5} {MD035:7} {MD035:9} {MD035:11} {MD035:13} {MD035:15}
22 | {MD035:17}
23 |
--------------------------------------------------------------------------------
/test/rule_tests/links.md:
--------------------------------------------------------------------------------
1 | # Link test
2 |
3 | For more information, please see the
4 | following page: http://www.example.com/ {MD034}
5 | which will tell you all you want to know.
6 |
7 | http://www.google.com/ {MD034}
8 |
9 | This link should be fine:
10 |
--------------------------------------------------------------------------------
/test/rule_tests/hr_style_dashes.md:
--------------------------------------------------------------------------------
1 | ***
2 |
3 | * * *
4 |
5 | *****
6 |
7 | ---
8 |
9 | - - -
10 |
11 | -----
12 |
13 | ___
14 |
15 | _ _ _
16 |
17 | _____
18 |
19 | ***
20 |
21 | {MD035:1} {MD035:3} {MD035:5} {MD035:9} {MD035:11} {MD035:13} {MD035:15}
22 | {MD035:17} {MD035:19}
23 |
--------------------------------------------------------------------------------
/test/rule_tests/hr_style_long.md:
--------------------------------------------------------------------------------
1 | ***
2 |
3 | * * *
4 |
5 | *****
6 |
7 | ---
8 |
9 | - - -
10 |
11 | -----
12 |
13 | ___
14 |
15 | _ _ _
16 |
17 | _____
18 |
19 | ***
20 |
21 | {MD035:1} {MD035:3} {MD035:5} {MD035:7} {MD035:9} {MD035:11} {MD035:13}
22 | {MD035:15} {MD035:19}
23 |
--------------------------------------------------------------------------------
/test/rule_tests/code_block_indented.md:
--------------------------------------------------------------------------------
1 | This is text.
2 |
3 | This is a
4 | code block.
5 |
6 | And here is more text
7 |
8 | ```
9 | This is {MD046:8} also a code block.
10 | ```
11 |
12 | But we'll do another:
13 |
14 | And this
15 | will.
16 |
17 | Final text is here
18 |
--------------------------------------------------------------------------------
/test/fixtures/output/json/with_matches.json:
--------------------------------------------------------------------------------
1 | [{"filename":"(stdin)","line":1,"rule":"MD002","aliases":["first-header-h1"],"description":"First header should be a top level header","docs":"https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md#md002---first-header-should-be-a-top-level-header"}]
2 |
--------------------------------------------------------------------------------
/test/rule_tests/code_block_fenced.md:
--------------------------------------------------------------------------------
1 | This is text.
2 |
3 | This is a {MD046}
4 | code block.
5 |
6 | And here is more text
7 |
8 | ```
9 | This is a code block that won't trigger.
10 | ```
11 |
12 | But we'll do another:
13 |
14 | And this {MD046}
15 | will.
16 |
17 | Final text is here
18 |
--------------------------------------------------------------------------------
/test/rule_tests/atx_closed_header_spacing.md:
--------------------------------------------------------------------------------
1 | #Header 1 {MD020} #
2 |
3 | ## Header 2 {MD020}##
4 |
5 | ##Header 3 {MD020}##
6 |
7 | ## Header 4 {MD021} ##
8 |
9 | ## Header 5 {MD021} ##
10 |
11 | ## Header 6 {MD021} ##
12 |
13 | ## Header 7 {MD021} ##
14 |
15 | ## Header 8 \#
16 |
17 | ## Header 9 \#
18 |
--------------------------------------------------------------------------------
/test/rule_tests/header_trailing_punctuation_customized.md:
--------------------------------------------------------------------------------
1 | # Heading 1 {MD026}.
2 |
3 | ## Heading 2 {MD026},
4 |
5 | ## Heading 3 {MD026}!
6 |
7 | ## Heading 4 {MD026}:
8 |
9 | ## Heading 5 {MD026};
10 |
11 | ## Heading 6?
12 |
13 | The rule has been customized to allow question marks while disallowing
14 | everything else.
15 |
--------------------------------------------------------------------------------
/test/rule_tests/headers_surrounding_space_atx.md:
--------------------------------------------------------------------------------
1 | # Header 1
2 |
3 | ## Header 2 {MD022}
4 | Some text
5 | ## Header 3 {MD022}
6 | Some text
7 | ## Header 4 {MD022}
8 |
9 | ## Header 5
10 |
11 | * This shouldn't trigger MD022, but did because of some bug where we tried to
12 | #catch headers that kramdown didn't parse correctly.
13 |
--------------------------------------------------------------------------------
/test/fixtures/output/sarif/without_matches.sarif:
--------------------------------------------------------------------------------
1 | {"$schema":"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json","version":"2.1.0","runs":[{"tool":{"driver":{"name":"Markdown lint","version":"<%= MarkdownLint::VERSION %>","informationUri":"https://github.com/markdownlint/markdownlint","rules":[]}},"results":[]}]}
2 |
--------------------------------------------------------------------------------
/test/rule_tests/code_block_dollar_fence.md:
--------------------------------------------------------------------------------
1 | # header
2 |
3 | ```fence
4 | $ code
5 | ```
6 |
7 | text
8 |
9 | ```fence
10 | $ code
11 | ```
12 |
13 | text
14 |
15 | ```fence
16 | $ code
17 | $ code
18 | ```
19 |
20 | text
21 |
22 | ```fence
23 | $ code
24 | $ code
25 | ```
26 |
27 | text
28 |
29 | {MD014:3} {MD014:9} {MD014:15} {MD014:22}
30 |
--------------------------------------------------------------------------------
/test/rule_tests/bulletd_list_sublist.md:
--------------------------------------------------------------------------------
1 | This is a document where the lists are consisent style per-sublist
2 |
3 | * stuff
4 | * other stuff
5 | - indented stuff
6 | - more indented stuff
7 | + thing {MD004}
8 | - stuff
9 | + woah
10 | + this
11 | + is
12 | + ok but...
13 | - not this {MD004}
14 | + but this
15 | * thing
16 |
--------------------------------------------------------------------------------
/test/rule_tests/fix_102_extra_nodes_in_link_text.md:
--------------------------------------------------------------------------------
1 | [test _test_ test](www.test.com)
2 | [test `test` test](www.test.com)
3 | [test *test* test](www.test.com)
4 | [test *test* *test* test](www.test.com)
5 | [test *test* *test* *test* test](www.test.com)
6 | [test **test** test](www.test.com)
7 | [test __test__ test](www.test.com)
8 | [this should not raise](www.shouldnotraise.com)
9 |
--------------------------------------------------------------------------------
/test/rule_tests/spaces_inside_codespan_elements.md:
--------------------------------------------------------------------------------
1 | `normal codespan element`
2 |
3 | `codespan element with space inside right ` {MD038}
4 |
5 | We SHOULD have the following two tests marked with MD038 failurs
6 | ` codespan element with space inside left`
7 | ` codespan element with spaces inside ` but
8 | kramdown doesn't see that as a codespans so we can't detect them anymore.
9 |
--------------------------------------------------------------------------------
/tools/view_markdown.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # Quick script for viewing getting at kramdown's and markdownlint's view of a
3 | # markdown file
4 | require 'mdl/doc'
5 | require 'pry'
6 |
7 | doc = MarkdownLint::Doc.new_from_file(ARGV[0])
8 | children = doc.parsed.root.children
9 |
10 | # rubocop:disable Lint/Debugger
11 | binding.pry
12 | # rubocop:enable Lint/Debugger
13 |
--------------------------------------------------------------------------------
/test/fixtures/docs_ruleset_1.rb:
--------------------------------------------------------------------------------
1 | docs 'https://example.com/static-docs'
2 |
3 | rule 'MY002', 'Documents must start with A' do
4 | tags :opinionated
5 | check do |doc|
6 | [1] if doc.lines[0] != 'A'
7 | end
8 | end
9 |
10 | rule 'MY003', 'Documents must start with B' do
11 | tags :opinionated
12 | docs 'https://example.com/override-docs'
13 | check do |doc|
14 | [1] if doc.lines[0] != 'B'
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/test/rule_tests/md013_ignore_long_lines_in_code_block.md:
--------------------------------------------------------------------------------
1 | This is a short line.
2 |
3 | This is a very very very very very very very very very very very very very very very very very very very very long line. {MD013}
4 |
5 | This is a short line.
6 |
7 | ```text
8 | Here is a short line in a code block.
9 | Here is a very very very very very very very very very very very very very very very very very very very long line in a code block.
10 | ```
11 |
--------------------------------------------------------------------------------
/.pre-commit-hooks.yaml:
--------------------------------------------------------------------------------
1 | - id: markdownlint
2 | name: Markdownlint
3 | description: Run markdownlint on your Markdown files
4 | entry: mdl
5 | language: ruby
6 | files: \.(md|mdown|markdown)$
7 |
8 | - id: markdownlint_docker
9 | name: Markdownlint Docker
10 | description: Run markdown lint on your Markdown files using the project docker image
11 | language: docker_image
12 | files: \.(md|mdown|markdown)$
13 | entry: markdownlint/markdownlint
14 |
--------------------------------------------------------------------------------
/test/rule_tests/long_lines_100.md:
--------------------------------------------------------------------------------
1 | This is a very very very very very very very very long line over 80 chars but less than 100
2 |
3 | This is a very very very very very very very very very very long line over 80 chars, and also over 100. {MD013}
4 |
5 | This is a very very very very very very very very very long line that is exactly 100 characters long
6 |
7 | This line however, while very long, doesn't have whitespace after the 100th columnwhichallowsforURLsandotherlongthings.
8 |
--------------------------------------------------------------------------------
/test/rule_tests/fenced_code_without_blank_lines.md:
--------------------------------------------------------------------------------
1 | ```
2 | code at start of file
3 | ```
4 |
5 | text
6 |
7 | ```ruby
8 | code
9 | ```
10 |
11 | text
12 | ``` {MD031}
13 | code
14 | ``` {MD031}
15 | text
16 |
17 | ```
18 | code
19 | ``` {MD031}
20 | text
21 |
22 | text
23 | ``` {MD031}
24 | code
25 | ```
26 |
27 | text
28 |
29 | ```js
30 | code
31 | code
32 | code
33 | ```
34 |
35 | ```html
36 | ```
37 |
38 | text
39 |
40 | ```
41 | code at end of file without newline
42 | ```{MD047}
--------------------------------------------------------------------------------
/.github/workflows/dco.yml:
--------------------------------------------------------------------------------
1 | name: DCO Check
2 | on: [pull_request]
3 |
4 | jobs:
5 | dco_check_job:
6 | runs-on: ubuntu-latest
7 | name: DCO Check
8 | steps:
9 | - name: Get PR Commits
10 | uses: tim-actions/get-pr-commits@master
11 | id: 'get-pr-commits'
12 | with:
13 | token: ${{ secrets.GITHUB_TOKEN }}
14 | - name: DCO Check
15 | uses: tim-actions/dco@master
16 | with:
17 | commits: ${{ steps.get-pr-commits.outputs.commits }}
18 |
--------------------------------------------------------------------------------
/lib/mdl/styles/relaxed.rb:
--------------------------------------------------------------------------------
1 | all
2 | exclude_tag :whitespace
3 | exclude_tag :line_length
4 |
5 | exclude_rule 'MD006' # Lists at beginning of line
6 | exclude_rule 'MD007' # List indentation
7 | exclude_rule 'MD033' # Inline HTML
8 | exclude_rule 'MD034' # Bare URL used
9 | exclude_rule 'MD040' # Fenced code blocks should have a language specified
10 | exclude_rule 'MD041' # First line in file should be a top level header
11 | exclude_rule 'MD047' # File should end with a single newline character
12 |
--------------------------------------------------------------------------------
/test/fixtures/front_matter/jekyll_post.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: Hello World!
4 | category: Meta
5 | tags:
6 | - tag
7 | - another tag
8 | - one more tag
9 | url: http://example.com
10 | excerpt: Hello World! Vestibulum imperdiet adipiscing arcu, quis aliquam dolor condimentum dapibus. Aliquam fermentum leo aliquet quam volutpat et molestie mauris mattis. Suspendisse semper consequat velit in suscipit.
11 | ---
12 | # header1
13 |
14 | This is just a sample post.
15 |
16 | ### offending header3
17 |
--------------------------------------------------------------------------------
/test/fixtures/front_matter/jekyll_post_2.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: Hello World!
4 | category: Meta
5 | tags:
6 | - tag
7 | - another tag
8 | - one more tag
9 | url: http://example.com
10 | excerpt: Hello World! Vestibulum imperdiet adipiscing arcu, quis aliquam dolor condimentum dapibus. Aliquam fermentum leo aliquet quam volutpat et molestie mauris mattis. Suspendisse semper consequat velit in suscipit.
11 | ---
12 |
13 | # header1
14 |
15 | This is just a sample post.
16 |
17 | ### offending header3
18 |
--------------------------------------------------------------------------------
/test/rule_tests/fenced_code_blocks.md:
--------------------------------------------------------------------------------
1 | This is a GFM-style fenced code block:
2 |
3 | ``` bash
4 | #!/bin/bash
5 |
6 | # Print something to stdout:
7 | echo "Hello"
8 | echo "World"
9 | ```
10 |
11 | This is a kramdown-style fenced code block:
12 |
13 | ~~~ bash
14 | #!/bin/bash
15 |
16 | # Print something to stdout:
17 | echo "Hello"
18 | echo "World"
19 | ~~~
20 |
21 | None of the above should trigger any heading related rules.
22 |
23 | ```
24 | Code block without a language specifier
25 | ```
26 |
27 | {MD040:23}
28 |
--------------------------------------------------------------------------------
/test/rule_tests/inline_html.md:
--------------------------------------------------------------------------------
1 | # Regular header
2 |
3 |
Inline HTML Header {MD033}
4 |
5 | More inline HTML {MD033}
6 | but this time on multiple lines
7 |
8 |
9 | This shouldn't trigger as it's inside a code block
10 |
11 | ```text
12 | Neither should this as it's also in a code block
13 | ```
14 |
15 | The rule has been customized to allow some elements while disallowing
16 | everything else.
17 |
18 | Test case for the line break element
19 | present on `allowed_elements` and it should be permitted.
20 |
--------------------------------------------------------------------------------
/test/rule_tests/headers_with_spaces_at_the_beginning.md:
--------------------------------------------------------------------------------
1 | Some text
2 |
3 | # Header 1 {MD023}
4 |
5 | Setext style fully indented {MD023}
6 | ===================================
7 |
8 | Setext style title only indented {MD023}
9 | =========================================
10 |
11 | * Test situations in which MD023 shouldn't be triggered.
12 |
13 | ```rb
14 | # This shouldn't trigger MD023 as it is a code comment.
15 | foo = "And here is some code"
16 | ```
17 |
18 | * This is another case where MD023 shouldn't be triggered
19 | # Test
20 | # Test
21 |
--------------------------------------------------------------------------------
/test/rule_tests/blockquote_spaces.md:
--------------------------------------------------------------------------------
1 | Some text
2 |
3 | > Hello world
4 | > Foo {MD027}
5 | > Bar {MD027}
6 |
7 | This tests other things embedded in the blockquote:
8 |
9 | - foo
10 |
11 | > *Hello world*
12 | > *foo* {MD027}
13 | > **bar** {MD027}
14 | > "Baz" {MD027}
15 | > `qux` {MD027}
16 | > *foo* more text
17 | > **bar** more text
18 | > 'baz' more text
19 | > `qux` more text
20 | > [link](example.com) to site
21 | > [link](#link) {MD027}
22 | >
23 | > - foo
24 |
25 | Test the first line being indented too much:
26 |
27 | > Foo {MD027}
28 | > Bar {MD027}
29 | > Baz
30 |
--------------------------------------------------------------------------------
/lib/mdl/styles/cirosantilli.rb:
--------------------------------------------------------------------------------
1 | # Enforce the style guide at https://cirosantilli.com/markdown-style-guide
2 | all
3 | rule 'MD003', :style => :atx
4 | rule 'MD004', :style => :dash
5 | rule 'MD007', :indent => 4
6 | rule 'MD030', :ul_multi => 3, :ol_multi => 2
7 | rule 'MD035', :style => '---'
8 |
9 | # Inline HTML - this isn't forbidden by the style guide, and raw HTML use is
10 | # explicitly mentioned in the 'email automatic links' section.
11 | exclude_rule 'MD033'
12 |
13 | # File should end with a single newline character
14 | # this isn't forbidden by the style guide
15 | exclude_rule 'MD047'
16 |
--------------------------------------------------------------------------------
/test/rule_tests/spaces_inside_link_text.md:
--------------------------------------------------------------------------------
1 | [](http://bar/)
2 |
3 | [foo](http://bar/)
4 |
5 | ["foo"](http:/bar/)
6 |
7 | [`foo`](http://bar/)
8 |
9 | [*foo*](http://bar/)
10 |
11 | [**foo**](http://bar/)
12 |
13 | [foo "bar"](http:/baz/)
14 |
15 | [foo ](http://bar/) {MD039}
16 |
17 | [ foo](http://bar/) {MD039}
18 |
19 | [ foo ](http://bar/) {MD039}
20 |
21 | [ "foo" ](http://bar/) {MD039}
22 |
23 | [ `foo` ](http://bar/) {MD039}
24 |
25 | [ *foo* ](http://bar/) {MD039}
26 |
27 | The following shouldn't break anything:
28 | [](/images/Screenshot.png)
29 |
--------------------------------------------------------------------------------
/test/rule_tests/blockquote_blank_lines.md:
--------------------------------------------------------------------------------
1 | Some text
2 |
3 | > a quote
4 | > same quote
5 |
6 | > blank line above me
7 |
8 |
9 | > two blank lines above me
10 |
11 | > space above me
12 |
13 | * List with embedded blockquote
14 |
15 | > Test
16 | > Test
17 |
18 | > Test
19 |
20 | * Item 2
21 |
22 | > Test. The blank line below should _not_ trigger MD028 as one blockquote is
23 | > inside the list, and the other is outside it.
24 |
25 | > Test
26 |
27 | Expected errors:
28 |
29 | {MD028:5} {MD028:8} {MD028:10} {MD028:17}
30 | {MD009:10} (trailing space is intentional)
31 | {MD012:8} (multiple blank lines are intentional)
32 |
--------------------------------------------------------------------------------
/tools/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3.16.2
2 | # Standard library gems used by markdownlint, such as 'etc' and 'json', are
3 | # not installed by default in Alpine Linux distros, since the current policy
4 | # is to have small packages with extra functionality (standard library
5 | # included) delivered in individual subpackages. The solution is to install
6 | # ruby + standard library via the 'ruby-full' package.
7 | RUN adduser -h /home/mdl -s /sbin/nologin -D -g mdl mdl && \
8 | apk add --update --no-cache ruby-full && \
9 | gem install mdl --no-document && \
10 | mkdir /data
11 | WORKDIR /data
12 | USER mdl:mdl
13 | ENTRYPOINT ["mdl"]
14 | CMD ["--help"]
15 |
--------------------------------------------------------------------------------
/tools/docker/README.md:
--------------------------------------------------------------------------------
1 | # Docker container for markdownlint
2 |
3 | ## Using the docker image
4 |
5 | To check a single file:
6 |
7 | ```shell
8 | docker run --rm -v ${PWD}:/data markdownlint/markdownlint myfile.md
9 | ```
10 |
11 | Or, to check all files in a directory:
12 |
13 | ```shell
14 | docker run --rm -v ${PWD}:/data markdownlint/markdownlint .
15 | ```
16 |
17 | ## Building from a docker file
18 |
19 | The following will tag and upload a new release. Replace X.Y.Z as appropriate.
20 |
21 | ```shell
22 | podman build -t markdownlint/markdownlint:latest \
23 | -t markdownlint/markdownlint:X.Y.Z .
24 | podman push markdownlint/markdownlint:latest
25 | podman push markdownlint/markdownling:X.Y.Z
26 | ```
27 |
--------------------------------------------------------------------------------
/test/rule_tests/fenced_code_blocks_in_lists.md:
--------------------------------------------------------------------------------
1 | # test doc
2 |
3 | this is some text
4 |
5 | * This is a list item
6 |
7 | ```fenced
8 | this is a code block within the list item.
9 | ```
10 |
11 | with more text here
12 |
13 | * and another list item here
14 |
15 | And another paragraph.
16 |
17 | But this code block {MD046}
18 |
19 | is *NOT* in a list and should error.
20 |
21 | And in addition to that...
22 |
23 | ```text
24 | This code block is both indented
25 | and fenced and should *also* error.
26 | ```
27 |
28 | And finally:
29 |
30 | ```text
31 | This is a code block
32 |
33 | And this is a code block in a code block and should *not* error
34 |
35 | More stuff here
36 | ```
37 |
38 | all
39 |
40 | {MD046:23}
41 |
--------------------------------------------------------------------------------
/MAINTAINERS.md:
--------------------------------------------------------------------------------
1 | # Markdownlint
2 |
3 | We aim to make sure this project has longevity, and to that end we want to make
4 | adding new maintainers a simple process.
5 |
6 | If you would like to be a maintainer, you need to demonstrate some ongoing
7 | contributions - either reviews of PRs or PR contributions. They need not be large.
8 | Then send a PR to add yourself to this list. The existing maintainers will
9 | have a discussion, and assuming there are no objections, you'll be added.
10 |
11 | ## Current Maintainers
12 |
13 | * [Phil Dibowitz](https://github.com/jaymzh)
14 | * [Naomi Reeves](https://github.com/NaomiReeves)
15 | * [Bryan Wann](https://github.com/bwann)
16 |
17 | ## Past Maintainers
18 |
19 | * [psyomn](https://github.com/psyomn)
20 | * [Mark Harrison](https://github.com/mivok)
21 |
--------------------------------------------------------------------------------
/lib/mdl/kramdown_parser.rb:
--------------------------------------------------------------------------------
1 | # Modified version of the kramdown parser to add in features/changes
2 | # appropriate for markdownlint, but which don't make sense to try to put
3 | # upstream.
4 | require 'kramdown/parser/gfm'
5 |
6 | module Kramdown
7 | module Parser
8 | # modified parser class - see comment above
9 | class MarkdownLint < Kramdown::Parser::Kramdown
10 | def initialize(source, options)
11 | super
12 | i = @block_parsers.index(:codeblock_fenced)
13 | @block_parsers.delete(:codeblock_fenced)
14 | @block_parsers.insert(i, :codeblock_fenced_gfm)
15 | end
16 |
17 | # Regular kramdown parser, but with GFM style fenced code blocks
18 | FENCED_CODEBLOCK_MATCH = Kramdown::Parser::GFM::FENCED_CODEBLOCK_MATCH
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/tools/test_location.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # Script for identifying when markdownlint is receiving an incorrect line
3 | # number for an element. It checks all headers, then grabs the lines
4 | # associated with the headers according to markdown and compares the content,
5 | # printing out any that don't match.
6 | require 'mdl/doc'
7 | require 'pry'
8 |
9 | text = File.read(ARGV[0])
10 | unless ARGV[1].nil?
11 | # If we provide a second argument, then start the document from line N of
12 | # the original file.
13 | text = text.split("\n")[ARGV[1].to_i - 1..].join("\n")
14 | end
15 | doc = MarkdownLint::Doc.new(text)
16 | headers = doc.find_type(:header)
17 | bad_headers = headers.select do |e|
18 | doc.element_line(e).nil? || !doc.element_line(e).include?(e[:raw_text])
19 | end
20 | pp bad_headers
21 |
--------------------------------------------------------------------------------
/test/rule_tests/code_block_dollar.md:
--------------------------------------------------------------------------------
1 | The following code block shouldn't have $ before the commands:
2 |
3 | ```bash
4 | $ ls
5 | $ less foo
6 |
7 | $ cat bar
8 | ```
9 |
10 | However the following code block shows output, and $ can be used to
11 | distinguish between command and output:
12 |
13 | ```bash
14 | $ ls
15 | foo bar
16 | $ less foo
17 | Hello world
18 |
19 | $ cat bar
20 | baz
21 | ```
22 |
23 | The following code block uses variable names, and likewise shouldn't fire:
24 |
25 | ```bash
26 | $foo = 'bar';
27 | $baz = 'qux';
28 | ```
29 |
30 | The following code block doesn't have any dollar signs, and shouldn't fire:
31 |
32 | ```bash
33 | ls foo
34 | cat bar
35 | ```
36 |
37 | The following (fenced) code block doesn't have any content at all, and
38 | shouldn't fire:
39 |
40 | ```bash
41 | ```
42 |
43 | {MD014:3}
44 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/BUG_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🐛 Bug Report
3 | about: If something isn't working as expected 🤔.
4 | labels: "bug"
5 | ---
6 | ## Description
7 |
8 |
9 | ### Environment
10 |
11 | **MDL Version**
12 |
13 |
14 | ### Expected Behavior
15 |
16 |
17 | ### Actual Behavior
18 |
19 |
20 | ## Replication Case
21 |
23 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/ENHANCEMENT_REQUEST.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🚀 Enhancement Request
3 | about: I have a suggestion (and may want to implement it 🙂)!
4 | labels: "enhancement"
5 | ---
6 |
7 | ## Describe the Enhancement:
8 |
9 |
10 | ### Impacted Rules:
11 |
12 |
13 | ## Describe the Need:
14 |
15 |
16 | ## Current Alternative
17 |
18 |
19 | ## Can We Help You Implement This?:
20 |
21 |
--------------------------------------------------------------------------------
/test/rule_tests/fenced_code_with_nesting.md:
--------------------------------------------------------------------------------
1 | # header
2 |
3 | text
4 | ```fence {MD031}
5 | code
6 | ``` {MD031}
7 | text
8 | ~~~fence {MD031}
9 | code
10 | ~~~ {MD031}
11 | text
12 | ```fence {MD031}
13 | ~~~fence
14 | code
15 | ~~~
16 | ``` {MD031}
17 | text
18 | ~~~fence {MD031}
19 | ```fence
20 | code
21 | ```
22 | ~~~ {MD031}
23 | text
24 | ```fence {MD031}
25 |
26 | ~~~fence
27 | code
28 | ~~~
29 |
30 | ``` {MD031}
31 | text
32 | ~~~fence {MD031}
33 |
34 | ```fence
35 | code
36 | ```
37 |
38 | ~~~ {MD031}
39 | text
40 | ```fence {MD031}
41 | code
42 | ~~~
43 | ``` {MD031}
44 | text
45 | ~~~fence {MD031}
46 | code
47 | ```
48 | ~~~ {MD031}
49 | text
50 | ````fence {MD031}
51 | ```fence
52 | code
53 | ```
54 | ```` {MD031}
55 | text
56 | ~~~~fence {MD031}
57 | ~~~fence
58 | code
59 | ~~~
60 | ~~~~ {MD031}
61 | text
62 | ````fence {MD031}
63 | ```fence
64 | code
65 | ```
66 | ````` {MD031}
67 | text
68 | ~~~~fence {MD031}
69 | ~~~fence
70 | code
71 | ~~~
72 | ~~~~~ {MD031}
73 | text
74 |
--------------------------------------------------------------------------------
/test/rule_tests/spaces_inside_emphasis_markers.md:
--------------------------------------------------------------------------------
1 | Line with *Normal emphasis*
2 |
3 | Line with **Normal strong**
4 |
5 | Line with _Normal emphasis_
6 |
7 | Line with __Normal strong__
8 |
9 | Broken * emphasis * with spaces in {MD037}
10 |
11 | Broken ** strong ** with spaces in {MD037}
12 |
13 | Broken _ emphasis _ with spaces in {MD037}
14 |
15 | Broken __ strong __ with spaces in {MD037}
16 |
17 | Mixed *ok emphasis* and * broken emphasis * {MD037}
18 |
19 | Mixed **ok strong** and ** broken strong ** {MD037}
20 |
21 | Mixed _ok emphasis_ and _ broken emphasis _ {MD037}
22 |
23 | Mixed __ok strong__ and __ broken strong __ {MD037}
24 |
25 | Mixed *ok emphasis* **ok strong** * broken emphasis * {MD037}
26 |
27 | Multiple * broken emphasis * _ broken emphasis _ {MD037}
28 |
29 | One-sided *broken emphasis * {MD037}
30 |
31 | One-sided * broken emphasis* {MD037}
32 |
33 | Don't _flag on _words with underscores before them.
34 |
35 | The same goes for words* with asterisks* after them.
36 |
--------------------------------------------------------------------------------
/test/rule_tests/lists_without_blank_lines.md:
--------------------------------------------------------------------------------
1 | * list (on first line)
2 |
3 | text
4 |
5 | * list
6 |
7 | text
8 | * list {MD032}
9 | text
10 | + list {MD032}
11 | text
12 | - list {MD032}
13 | text
14 | 1. list {MD032}
15 | text
16 |
17 | * list
18 | * list {MD032}
19 | text
20 |
21 | text
22 | 10. list {MD032}
23 | 20. list
24 |
25 | text
26 |
27 | * list
28 | * list
29 | * list
30 |
31 | text
32 |
33 | * list
34 | with hanging indent
35 | * list
36 | with hanging indent
37 | * list
38 | with hanging indent
39 |
40 | Note: list without hanging indent violates MD032
41 |
42 | * list
43 |
44 | item with blank lines
45 |
46 | * list
47 |
48 | item with blank lines
49 |
50 | text
51 |
52 | ```js
53 | /*
54 | * code block
55 | * not a list
56 | */
57 | ```
58 |
59 | text
60 |
61 | * list {MD032}
62 | ``` {MD031}
63 | code
64 | ```
65 |
66 | text
67 |
68 | ```
69 | code
70 | ``` {MD031}
71 | * list {MD032}
72 |
73 | text
74 |
75 | * list (on last line without newline){MD047}
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Integration
2 | on:
3 | push:
4 | branches: main
5 | pull_request:
6 | branches: main
7 | jobs:
8 | markdown:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout repository
12 | uses: actions/checkout@main
13 | - name: Lint Markdown
14 | uses: actionshub/markdownlint@main
15 | with:
16 | filesToIgnoreRegex: '((test|example|bin)\/.*|docs\/RULES.md)'
17 | ruby:
18 | name: Ruby ${{ matrix.ruby }}
19 | strategy:
20 | fail-fast: false
21 | matrix:
22 | ruby: ['2.7', '3.0', '3.1', '3.2']
23 | runs-on: ubuntu-latest
24 | steps:
25 | - name: Checkout repository
26 | uses: actions/checkout@v2
27 | - name: Setup Ruby
28 | uses: ruby/setup-ruby@v1
29 | with:
30 | ruby-version: ${{ matrix.ruby }}
31 | - name: Install dependencies
32 | run: bundle install
33 | - name: Run rubocop
34 | run: bundle exec rubocop
35 | - name: Run tests
36 | run: bundle exec rake
37 |
--------------------------------------------------------------------------------
/test/fixtures/docs_ruleset_2.rb:
--------------------------------------------------------------------------------
1 | require 'digest/md5'
2 |
3 | docs do |id, description|
4 | "https://example.com/#{id}##{Digest::MD5.hexdigest(description)}"
5 | end
6 |
7 | rule 'MY004', 'Documents must start with C' do
8 | tags :opinionated
9 | check do |doc|
10 | [1] if doc.lines[0] != 'C'
11 | end
12 | end
13 |
14 | rule 'MY005', 'Documents must start with D' do
15 | tags :opinionated
16 | docs 'https://example.com/override-docs'
17 | check do |doc|
18 | [1] if doc.lines[0] != 'D'
19 | end
20 | end
21 |
22 | rule 'MY007', 'Documents must start with F' do
23 | tags :opinionated
24 |
25 | docs do |id, description|
26 | hash = description.downcase.gsub(/[^a-z]+/, '-')
27 | "https://example.com/dynamic-override/#{id}##{hash}"
28 | end
29 |
30 | check do |doc|
31 | [1] if doc.lines[0] != 'F'
32 | end
33 | end
34 |
35 | docs 'https://example.com/later-declaration'
36 |
37 | rule 'MY006', 'Documents must start with E' do
38 | tags :opinionated
39 | check do |doc|
40 | [1] if doc.lines[0] != 'E'
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 Mark Harrison
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/RULE_REQUEST.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 💪 Rule Request
3 | about: I have a suggestion for a new rule for markdownlint (and may want to implement it 🙌)!
4 | labels: "new rule"
5 | ---
6 |
7 | ## New Rule Checklist
8 |
9 | Before suggesting a rule for inclusion please make sure your suggestion meets these criteria for rule built into markdownlint:
10 | - [ ] allows a user to lint for a specific syntax divergence across the multiple flavors and styles of markdown
11 | - [ ] does not dictate any one specific style, but enables an end user to enable, disable, or configure the specific style of enforcement desired
12 |
13 | ## Describe The Rule:
14 |
15 |
16 | ## Why Should This Be Included In Markdownlint?:
17 |
18 |
19 | ## Can We Help You Implement This?:
20 |
21 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | TargetRubyVersion: 2.7
3 | NewCops: enable
4 | Exclude:
5 | - 'omnibus/bin/*'
6 | - 'vendor/**/*'
7 |
8 | Layout/LineLength:
9 | Max: 80
10 |
11 | Metrics:
12 | Enabled: false
13 |
14 | Style/FrozenStringLiteralComment:
15 | EnforcedStyle: never
16 |
17 | Style/LineEndConcatenation:
18 | Enabled: false
19 |
20 | Style/TrailingCommaInHashLiteral:
21 | EnforcedStyleForMultiline: comma
22 |
23 | Style/HashSyntax:
24 | EnforcedStyle: hash_rockets
25 |
26 | Style/PercentLiteralDelimiters:
27 | PreferredDelimiters:
28 | default: '{}'
29 | '%i': '{}'
30 | '%I': '{}'
31 | '%w': '{}'
32 | '%W': '{}'
33 | '%r': '{}'
34 |
35 | Style/TrailingCommaInArguments:
36 | EnforcedStyleForMultiline: comma
37 |
38 | Style/OptionalBooleanParameter:
39 | Enabled: false
40 |
41 | # once we have a customizable line-end concat,
42 | # then this will be be useful
43 | Style/StringConcatenation:
44 | Enabled: false
45 |
46 | Style/Lambda:
47 | EnforcedStyle: lambda
48 |
49 | # we do this a lot. it's very rubyish
50 | Style/MultilineBlockChain:
51 | Enabled: false
52 |
53 | Style/NumericPredicate:
54 | Enabled: false
55 |
--------------------------------------------------------------------------------
/test/rule_tests/spaces_after_list_marker.md:
--------------------------------------------------------------------------------
1 | Normal list
2 |
3 | * Foo
4 | * Bar
5 | * Baz
6 |
7 | List with incorrect spacing
8 |
9 | * Foo {MD030}
10 | * Bar {MD030}
11 | * Baz {MD030}
12 |
13 | List with children:
14 |
15 | * Foo {MD030}
16 | * Bar {MD030}
17 | * Baz
18 |
19 | List with children and correct spacing:
20 |
21 | * Foo
22 | * Bar
23 | * Baz (This sublist has no children)
24 |
25 | List with Multiple paragraphs and correct spacing
26 |
27 | * Foo
28 |
29 | Here is the second paragraph
30 |
31 | * All items in the list need the same indent
32 |
33 | List with multiple paragraphs and incorrect spacing
34 |
35 | * Foo {MD030}
36 |
37 | Here is the second paragraph
38 |
39 | * Bar {MD030}
40 |
41 | List with code blocks:
42 |
43 | * Foo
44 |
45 | Here is some code
46 |
47 | * Bar
48 |
49 | Ordered lists:
50 |
51 | 1. Foo
52 | 1. Bar
53 | 1. Baz
54 |
55 | And with incorrect spacing:
56 |
57 | 1. Foo {MD030}
58 | 1. Bar {MD030}
59 | 1. Baz {MD030}
60 |
61 | Ordered lists with children:
62 |
63 | 1. Foo {MD030}
64 | * Hi
65 | 1. Bar {MD030}
66 | 1. Baz {MD030}
67 |
68 | Ordered lists with children (correct spacing), and with something other than
69 | the first item determining that the entire list has children:
70 |
71 | 1. Foo
72 | 1. Bar
73 | * Hi
74 | 1. Baz
75 |
--------------------------------------------------------------------------------
/test/fixtures/output/sarif/with_matches.sarif:
--------------------------------------------------------------------------------
1 | {"$schema":"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json","version":"2.1.0","runs":[{"tool":{"driver":{"name":"Markdown lint","version":"<%= MarkdownLint::VERSION %>","informationUri":"https://github.com/markdownlint/markdownlint","rules":[{"id":"MD002","name":"FirstHeaderH1","defaultConfiguration":{"level":"note"},"properties":{"description":"First header should be a top level header","tags":["headers"],"queryURI":"https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md#md002---first-header-should-be-a-top-level-header"},"shortDescription":{"text":"First header should be a top level header"},"fullDescription":{"text":"First header should be a top level header"},"helpUri":"https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md#md002---first-header-should-be-a-top-level-header","help":{"text":"More info: https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md#md002---first-header-should-be-a-top-level-header","markdown":"[More info](https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md#md002---first-header-should-be-a-top-level-header)"}}]}},"results":[{"ruleId":"MD002","ruleIndex":0,"message":{"text":"MD002 - First header should be a top level header"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"(stdin)","uriBaseId":"%SRCROOT%"},"region":{"startLine":1}}}]}]}]}
2 |
--------------------------------------------------------------------------------
/mdl.gemspec:
--------------------------------------------------------------------------------
1 | lib = File.expand_path('lib', __dir__)
2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3 | require 'mdl/version'
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = 'mdl'
7 | spec.version = MarkdownLint::VERSION
8 | spec.authors = ['Mark Harrison']
9 | spec.email = ['mark@mivok.net']
10 | spec.summary = 'Markdown lint tool'
11 | spec.description = 'Style checker/lint tool for markdown files'
12 | spec.homepage = 'https://github.com/markdownlint/markdownlint'
13 | spec.license = 'MIT'
14 | spec.metadata['rubygems_mfa_required'] = 'true'
15 |
16 | spec.files = %w{LICENSE.txt Gemfile} + Dir.glob('*.gemspec') +
17 | Dir.glob('lib/**/*')
18 | spec.bindir = 'bin'
19 | spec.executables = %w{mdl}
20 | spec.require_paths = ['lib']
21 |
22 | spec.required_ruby_version = '>= 2.7'
23 |
24 | spec.add_dependency 'kramdown', '~> 2.3'
25 | spec.add_dependency 'kramdown-parser-gfm', '~> 1.1'
26 | spec.add_dependency 'mixlib-cli', '~> 2.1', '>= 2.1.1'
27 | spec.add_dependency 'mixlib-config', '>= 2.2.1', '< 4'
28 | spec.add_dependency 'mixlib-shellout'
29 |
30 | spec.add_development_dependency 'bundler', '>= 1.12', '< 3'
31 | spec.add_development_dependency 'minitest', '~> 5.9'
32 | spec.add_development_dependency 'pry', '~> 0.10'
33 | spec.add_development_dependency 'rake', '>= 11.2', '< 14'
34 | spec.add_development_dependency 'rubocop', '~> 1.28.1'
35 | end
36 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/DESIGN_PROPOSAL.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Design Proposal
3 | about: I have a significant change I would like to propose and discuss before starting
4 | labels: "design proposal"
5 | ---
6 |
7 | ### When a Change Needs a Design Proposal
8 |
9 | A design proposal should be opened any time a change meets one of the following qualifications:
10 |
11 | - Significantly changes the user experience of a project in a way that impacts users.
12 | - Significantly changes the underlying architecture of the project in a way that impacts other developers.
13 | - Changes the development or testing process of the project such as a change of CI systems or test frameworks.
14 |
15 | ### Why We Use This Process
16 |
17 | - Allows all interested parties (including any community member) to discuss large impact changes to a project.
18 | - Serves as a durable paper trail for discussions regarding project architecture.
19 | - Forces design discussions to occur before PRs are created.
20 | - Reduces PR refactoring and rejected PRs.
21 |
22 | ---
23 |
24 |
25 |
26 | ## Motivation
27 |
28 |
33 |
34 | ## Specification
35 |
36 |
37 |
38 | ## Downstream Impact
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 |
4 | ## Related Issues
5 |
6 |
7 |
8 | ## Types of changes
9 |
10 | - [ ] Bug fix (non-breaking change which fixes an issue)
11 | - [ ] New feature (non-breaking change which adds functionality)
12 | - [ ] Breaking change (fix or feature that would cause existing functionality to change)
13 | - [ ] Documentation (non-breaking change that does not add functionality but updates documentation)
14 | - [ ] Chore (non-breaking change that does not add functionality or fix an issue)
15 |
16 | ## Checklist:
17 |
18 |
19 | - [ ] I have read the [**CONTRIBUTING**](https://github.com/markdownlint/markdownlint/blob/main/CONTRIBUTING.md) document.
20 | - [ ] Wrote [good commit messages](https://chris.beams.io/posts/git-commit/)
21 | - [ ] Feature branch is up-to-date with `main`, if not - rebase it
22 | - [ ] Added tests for all new/changed functionality, including tests for positive and negative scenarios
23 | - [ ] The PR relates to *only* one subject with a clear title and description in grammatically correct, complete sentences
24 |
--------------------------------------------------------------------------------
/docs/creating_styles.md:
--------------------------------------------------------------------------------
1 | # Creating styles
2 |
3 | A 'style' in markdownlint is simply a ruby file specifying the list of enabled
4 | and disabled rules, as well as specifying parameters for any rules that need
5 | parameters different than the defaults.
6 |
7 | The various options you can use in a style file are:
8 |
9 | * `all` - include all rules
10 | * `rule` - include a specific rule.
11 |
12 | ```ruby
13 | rule 'MD001'
14 | ```
15 |
16 | * `exclude_rule` - exclude a previously included rule. Used if you want to
17 | include all except for a few rules.
18 |
19 | ```ruby
20 | exclude_rule 'MD000'
21 | ```
22 |
23 | * `tag` - include all rules that are tagged with a specific value
24 |
25 | ```ruby
26 | tag :whitespace
27 | ```
28 |
29 | * `exclude_tag` - exclude all rules tagged with the specified tag
30 |
31 | ```ruby
32 | exclude_tag :line_length
33 | ```
34 |
35 | Note that tags are specified as symbols, and rule names as strings, just as in
36 | the rule definitions themselves.
37 |
38 | The last matching option wins, so you should always put `all` at the top of the
39 | file (if you want to include all rules), then tags (and tag excludes), then
40 | specific rules. In other words, go from least to most specific.
41 |
42 | ## Parameters
43 |
44 | If you specify any parameters after a rule ID, then those values will be used
45 | for the rules instead of the default. You only need to specify parameters for
46 | any values you wish to override. For example, the default values for the
47 | parameters in MD030 (spaces after list markers) are all 1. If you still want
48 | the spaces after the list markers to be 1 in some cases, then you can exclude
49 | those parameters:
50 |
51 | ```ruby
52 | rule 'MD030', :ol_multi => 2, :ul_multi => 3
53 | ```
54 |
55 | Even if a rule is included already by a tag specification (or `all`), it is not
56 | a problem to add a specific `rule` entry in order to set custom parameters, and
57 | is in fact necessary to do so.
58 |
--------------------------------------------------------------------------------
/test/test_rules.rb:
--------------------------------------------------------------------------------
1 | require_relative 'setup_tests'
2 |
3 | class TestRules < Minitest::Test
4 | def get_expected_errors(lines)
5 | # Looks for lines tagged with {MD123} to signify that a rule is expected to
6 | # fire for this line. It also looks for lines tagged with {MD123:1} to
7 | # signify that a rule is expected to fire on another line (the line number
8 | # after the colon).
9 | expected_errors = {}
10 | re = /\{(MD\d+)(?::(\d+))?\}/
11 | lines.each_with_index do |line, num|
12 | m = re.match(line)
13 | while m
14 | expected_errors[m[1]] ||= []
15 | expected_line = if m[2]
16 | m[2].to_i
17 | else
18 | num + 1 # 1 indexed lines
19 | end
20 | expected_errors[m[1]] << expected_line
21 | m = re.match(line, m.end(0))
22 | end
23 | end
24 | expected_errors
25 | end
26 |
27 | def do_lint(filename)
28 | # Check for a test_case_style.rb style file for individual tests
29 | style_file = filename.sub(/.md$/, '_style.rb')
30 | unless File.exist?(style_file)
31 | style_file = "#{File.dirname(filename)}/default_test_style.rb"
32 | end
33 |
34 | ruleset = MarkdownLint::RuleSet.new
35 | ruleset.load_default
36 | rules = ruleset.rules
37 | style = MarkdownLint::Style.load(style_file, rules)
38 | rules.select! { |r| style.rules.include?(r) }
39 |
40 | doc = MarkdownLint::Doc.new(File.read(filename))
41 | expected_errors = get_expected_errors(doc.lines)
42 | actual_errors = {}
43 | rules.sort.each do |id, rule|
44 | error_lines = rule.check.call(doc)
45 | actual_errors[id] = error_lines if error_lines && !error_lines.empty?
46 | end
47 | assert_equal expected_errors, actual_errors
48 | end
49 |
50 | Dir[File.expand_path('rule_tests/*.md', __dir__)].each do |filename|
51 | define_method("test_#{File.basename(filename, '.md')}") do
52 | do_lint(filename)
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/docs/rolling_a_release.md:
--------------------------------------------------------------------------------
1 | # Rolling a new release
2 |
3 | Bump the version. Markdownlint uses semantic versioning. From
4 | :
5 |
6 | * Major version for backwards-incompatible changes
7 | * Minor version for functionality added in a backwards-compatible manner
8 | * Patch version for backwards-compatible bug fixes
9 | * Exception: Versions < 1.0 may introduce backwards-incompatible changes in a
10 | minor version.
11 |
12 | To bump the version, edit `lib/mdl/version.rb` and commit to the main branch.
13 |
14 | Update the changelog:
15 |
16 | * Add a new header and link for the new release, replacing any 'Unreleased'
17 | header.
18 |
19 | ```markdown
20 | ## [v0.2.0] (2015-04-13)
21 |
22 | This goes at the bottom:
23 |
24 | [v0.2.0]: https://github.com/markdownlint/markdownlint/tree/v0.2.0
25 | ```
26 |
27 | * Changelog entries can and should be added in an 'Unreleased' section as
28 | commits are made. However, the following steps can be performed before each
29 | release to catch anything that was missed.
30 | * Add a 'Rules added' section, listing every new rule added for this version.
31 | * Use `git diff v0.1.0..v0.2.0 docs/RULES.md | grep '## MD'` to discover
32 | what these are.
33 | * Search for closed issues:
34 | * Go to
35 | * Search for `closed:>1900-01-01`, changing the date to the date
36 | of the last release.
37 | * From this list of issues, make sections for:
38 | * Added - for new features
39 | * Changed - for changes in existing functionality
40 | * Deprecated - for once-stable features removed in upcoming releases
41 | * Removed - for deprecated features removed in this release
42 | * Fixed - for any bug fixes
43 | * Security - for any security issues
44 |
45 | Next, run `rake release`. This will:
46 |
47 | * Tag vX.Y.Z in git
48 | * Upload the new gem to rubygems.org
49 |
50 | Then `git push --tags upstream` to push the tag.
51 |
52 | Build and push a docker image per the docs in tools/docker/README.md
53 |
54 | Finally, add a new 'Unreleased' section to the changelog for the next release.
55 |
--------------------------------------------------------------------------------
/lib/mdl/ruleset.rb:
--------------------------------------------------------------------------------
1 | module MarkdownLint
2 | # defines a single rule
3 | class Rule
4 | attr_accessor :id, :description
5 |
6 | def initialize(id, description, fallback_docs: nil, &block)
7 | @id = id
8 | @description = description
9 | @generate_docs = fallback_docs
10 | @docs_overridden = false
11 | @aliases = []
12 | @tags = []
13 | @params = {}
14 | instance_eval(&block)
15 | end
16 |
17 | def check(&block)
18 | @check = block unless block.nil?
19 | @check
20 | end
21 |
22 | def tags(*tags)
23 | @tags = tags.flatten.map(&:to_sym) unless tags.empty?
24 | @tags
25 | end
26 |
27 | def aliases(*aliases)
28 | @aliases.concat(aliases)
29 | @aliases
30 | end
31 |
32 | def params(params = nil)
33 | @params.update(params) unless params.nil?
34 | @params
35 | end
36 |
37 | def docs(url = nil, &block)
38 | if block_given? != url.nil?
39 | raise ArgumentError, 'Give either a URL or a block, not both'
40 | end
41 |
42 | raise 'A docs url is already set within this rule' if @docs_overridden
43 |
44 | @generate_docs = block_given? ? block : lambda { |_, _| url }
45 | @docs_overridden = true
46 | end
47 |
48 | def docs_url
49 | @generate_docs&.call(id, description)
50 | end
51 | end
52 |
53 | # defines a ruleset
54 | class RuleSet
55 | attr_reader :rules
56 |
57 | def initialize
58 | @rules = {}
59 | end
60 |
61 | def rule(id, description, &block)
62 | @rules[id] =
63 | Rule.new(id, description, :fallback_docs => @fallback_docs, &block)
64 | end
65 |
66 | def load(rules_file)
67 | instance_eval(File.read(rules_file), rules_file)
68 | @rules
69 | end
70 |
71 | def docs(url = nil, &block)
72 | if block_given? != url.nil?
73 | raise ArgumentError, 'Give either a URL or a block, not both'
74 | end
75 |
76 | @fallback_docs = block_given? ? block : lambda { |_, _| url }
77 | end
78 |
79 | def load_default
80 | load(File.expand_path('rules.rb', __dir__))
81 | end
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/test/rule_tests/emphasis_instead_of_headers.md:
--------------------------------------------------------------------------------
1 | **Section 1: the first section {MD036}**
2 |
3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
4 | incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
5 | nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
6 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore
7 | eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt
8 | in culpa qui officia deserunt mollit anim id est laborum.
9 |
10 | __Section 1.1: another section {MD036}__
11 |
12 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
13 | incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
14 | nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
15 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore
16 | eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt
17 | in culpa qui officia deserunt mollit anim id est laborum.
18 |
19 | *Section 2: yet more sections {MD036}*
20 |
21 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
22 | incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
23 | nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
24 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore
25 | eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt
26 | in culpa qui officia deserunt mollit anim id est laborum.
27 |
28 | _Section 3: oh no more sections {MD036}_
29 |
30 | This is a normal paragraph
31 | **that just happens to have emphasized text in**
32 | even though the emphasized text is on its own line.
33 |
34 | This is another **normal** paragraph with some text in it. This also should
35 | not trigger the rule.
36 |
37 | **This is an entire paragraph that has been emphasized, and shouldn't be
38 | detected as a header because it's on multiple lines**
39 |
40 | **This also shouldn't be detected as a header as it ends in punctuation.**
41 |
42 | **[This as well since it is a link](https://example.com)**
43 |
--------------------------------------------------------------------------------
/test/test_ruledocs.rb:
--------------------------------------------------------------------------------
1 | require_relative 'setup_tests'
2 |
3 | # Ensures there is documentation for every rule, and that the
4 | # descriptions/tags/etc in the rule match those in the documentation
5 | # rubocop:disable Style/ClassVars
6 | class TestRuledocs < Minitest::Test
7 | @@ruleset = MarkdownLint::RuleSet.new
8 | @@ruleset.load_default
9 | @@rules = @@ruleset.rules
10 |
11 | def setup
12 | @ruledocs = load_ruledocs
13 | end
14 |
15 | def load_ruledocs
16 | rules = Hash.new({}) # Default to {} if no docs for the rule
17 | curr_rule = nil
18 | rules_file = File.expand_path('../docs/RULES.md', __dir__)
19 | File.read(rules_file).split("\n").each do |l|
20 | case l
21 | when /^## (MD\d+) - (.*)$/
22 | rules[Regexp.last_match(1)] = {
23 | :description => Regexp.last_match(2), :params => {}
24 | }
25 | curr_rule = Regexp.last_match(1)
26 | when /^Tags: (.*)$/
27 | rules[curr_rule][:tags] = Regexp.last_match(1).split(',').map do |i|
28 | i.strip.to_sym
29 | end
30 | when /^Aliases: (.*)$/
31 | rules[curr_rule][:aliases] = Regexp.last_match(1).split(',')
32 | .map(&:strip)
33 | when /^Parameters: (.*)(\(.*\)?)$/
34 | rules[curr_rule][:params] = Regexp.last_match(1).split(',').map do |i|
35 | i.strip.to_sym
36 | end
37 | end
38 | end
39 | rules
40 | end
41 |
42 | @@rules.each do |id, r|
43 | define_method("test_ruledoc_description_#{id}") do
44 | assert_equal r.description, @ruledocs[id][:description]
45 | end
46 | define_method("test_ruledoc_tags_#{id}") do
47 | assert_equal r.tags, @ruledocs[id][:tags]
48 | end
49 | define_method("test_ruledoc_aliases_#{id}") do
50 | assert_equal r.aliases, @ruledocs[id][:aliases]
51 | end
52 | define_method("test_ruledoc_params_#{id}") do
53 | assert_equal r.params.keys.sort, (@ruledocs[id][:params] || []).sort
54 | end
55 | end
56 |
57 | def test_ruledoc_for_every_rule
58 | # (and vice versa)
59 | assert_equal @@rules.keys, @ruledocs.keys
60 | end
61 | end
62 | # rubocop:enable Style/ClassVars
63 |
--------------------------------------------------------------------------------
/lib/mdl/style.rb:
--------------------------------------------------------------------------------
1 | require 'set'
2 |
3 | module MarkdownLint
4 | # defines a style
5 | class Style
6 | attr_reader :rules
7 |
8 | def initialize(all_rules)
9 | @tagged_rules = {}
10 | @aliases = {}
11 | all_rules.each do |id, r|
12 | r.tags.each do |t|
13 | @tagged_rules[t] ||= Set.new
14 | @tagged_rules[t] << id
15 | end
16 | r.aliases.each do |a|
17 | @aliases[a] = id
18 | end
19 | end
20 | @all_rules = all_rules
21 | @rules = Set.new
22 | end
23 |
24 | def all
25 | @rules.merge(@all_rules.keys)
26 | end
27 |
28 | def rule(id, params = {})
29 | if block_given?
30 | raise '"rule" does not take a block. Should this definition go in a ' +
31 | 'ruleset instead?'
32 | end
33 |
34 | id = @aliases[id] if @aliases[id]
35 | raise "No such rule: #{id}" unless @all_rules[id]
36 |
37 | @rules << id
38 | @all_rules[id].params(params)
39 | end
40 |
41 | def exclude_rule(id)
42 | id = @aliases[id] if @aliases[id]
43 | @rules.delete(id)
44 | end
45 |
46 | def tag(tag)
47 | @rules.merge(@tagged_rules[tag])
48 | end
49 |
50 | def exclude_tag(tag)
51 | @rules.subtract(@tagged_rules[tag])
52 | end
53 |
54 | def self.load(style_file, rules)
55 | unless style_file.include?('/') || style_file.end_with?('.rb')
56 | tmp = File.expand_path("../styles/#{style_file}.rb", __FILE__)
57 | unless File.exist?(tmp)
58 | warn "#{style_file} does not appear to be a built-in style." +
59 | ' If you meant to pass in your own style file, it must contain' +
60 | " a '/' or end in '.rb'. See https://github.com/markdownlint/" +
61 | 'markdownlint/blob/main/docs/configuration.md'
62 | exit(1)
63 | end
64 | style_file = tmp
65 | end
66 |
67 | unless File.exist?(style_file)
68 | warn "Style '#{style_file}' does not exist."
69 | exit(1)
70 | end
71 |
72 | style = new(rules)
73 | style.instance_eval(File.read(style_file), style_file)
74 | rules.select! { |r| style.rules.include?(r) }
75 | style
76 | end
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/test/rule_tests/long_lines_code.md:
--------------------------------------------------------------------------------
1 | This is a short line.
2 |
3 | This is a very very very very very very very very very very very very very very very very very very very very long line. {MD013}
4 |
5 | This is a short line.
6 |
7 | ```text
8 | Here is a short line in a code block.
9 | Here is a very very very very very very very very very very very very very very very very very very very long line in a code block.
10 | ```
11 |
12 | ```text
13 | test
14 | test
15 |
16 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
17 | ```
18 |
19 | This is a short line.
20 |
21 | | First Header | Second Header | Third Header | Fourth Header | Fifth Header | Sixth Header |
22 | | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- |
23 | | Content Cell | Content Cell | Content Cell | Content Cell | Content Cell | Content Cell |
24 | | Content Cell | Content Cell | Content Cell | Content Cell | Content Cell | Content Cell |
25 | | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- |
26 | | Content Cell | Content Cell | Content Cell | Content Cell | Content Cell | Content Cell |
27 | | Content Cell | Content Cell | Content Cell | Content Cell | Content Cell | Content Cell |
28 | | ============= | ============= | ============= | ============= | ============= | ============= |
29 | | Footer Cell | Footer Cell | Footer Cell | Footer Cell | Footer Cell | Footer Cell |
30 | {: rules="groups"}
31 |
32 | This is a very very very very very very very very very very very very very very very very very very very very long line. {MD013}
33 |
34 | Another line.
35 |
36 | | First Header | Second Header | Third Header | Fourth Header | Fifth Header | Sixth Header |
37 | | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- |
38 | | Content Cell | Content Cell | Content Cell | Content Cell | Content Cell | Content Cell |
39 | | Content Cell | Content Cell | Content Cell | Content Cell | Content Cell | Content Cell |
40 | | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- |
41 | | Content Cell | Content Cell | Content Cell | Content Cell | Content Cell | Content Cell |
42 | | Content Cell | Content Cell | Content Cell | Content Cell | Content Cell | Content Cell |
43 | | ============= | ============= | ============= | ============= | ============= | ============= |
44 | | Footer Cell | Footer Cell | Footer Cell | Footer Cell | Footer Cell | Footer Cell |
45 | {: rules="groups"}
46 |
--------------------------------------------------------------------------------
/lib/mdl/formatters/sarif.rb:
--------------------------------------------------------------------------------
1 | require 'json'
2 |
3 | module MarkdownLint
4 | # SARIF formatter
5 | #
6 | # @see https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
7 | class SarifFormatter
8 | class << self
9 | def generate(rules, results)
10 | matched_rules_id = results.map { |result| result['rule'] }.uniq
11 | matched_rules = rules.select { |id, _| matched_rules_id.include?(id) }
12 | JSON.generate(generate_sarif(matched_rules, results))
13 | end
14 |
15 | def generate_sarif(rules, results)
16 | {
17 | :'$schema' => 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
18 | :version => '2.1.0',
19 | :runs => [
20 | {
21 | :tool => {
22 | :driver => {
23 | :name => 'Markdown lint',
24 | :version => MarkdownLint::VERSION,
25 | :informationUri => 'https://github.com/markdownlint/markdownlint',
26 | :rules => generate_sarif_rules(rules),
27 | },
28 | },
29 | :results => generate_sarif_results(rules, results),
30 | }
31 | ],
32 | }
33 | end
34 |
35 | def generate_sarif_rules(rules)
36 | rules.map do |id, rule|
37 | {
38 | :id => id,
39 | :name => rule.aliases.first.split('-').map(&:capitalize).join,
40 | :defaultConfiguration => {
41 | :level => 'note',
42 | },
43 | :properties => {
44 | :description => rule.description,
45 | :tags => rule.tags,
46 | :queryURI => rule.docs_url,
47 | },
48 | :shortDescription => {
49 | :text => rule.description,
50 | },
51 | :fullDescription => {
52 | :text => rule.description,
53 | },
54 | :helpUri => rule.docs_url,
55 | :help => {
56 | :text => "More info: #{rule.docs_url}",
57 | :markdown => "[More info](#{rule.docs_url})",
58 | },
59 | }
60 | end
61 | end
62 |
63 | def generate_sarif_results(rules, results)
64 | results.map do |result|
65 | {
66 | :ruleId => result['rule'],
67 | :ruleIndex => rules.find_index { |id, _| id == result['rule'] },
68 | :message => {
69 | :text => "#{result['rule']} - #{result['description']}",
70 | },
71 | :locations => [
72 | {
73 | :physicalLocation => {
74 | :artifactLocation => {
75 | :uri => result['filename'],
76 | :uriBaseId => '%SRCROOT%',
77 | },
78 | :region => {
79 | :startLine => result['line'],
80 | },
81 | },
82 | }
83 | ],
84 | }
85 | end
86 | end
87 | end
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Markdown lint tool
2 |
3 | [](https://github.com/markdownlint/markdownlint/actions?query=workflow%3A%22Continuous+Integration%22)
4 | [](https://badge.fury.io/rb/mdl)
5 |
6 | A tool to check markdown files and flag style issues.
7 |
8 | ## Installation
9 |
10 | Markdownlint is packaged in some distributions as well as distributed via
11 | RubyGems. Check the list below to see if it's packaged for your distribution,
12 | and if so, feel free to use your distros package manager to install it.
13 |
14 | [](https://repology.org/project/mdl-markdownlint/versions)
15 |
16 | To install from rubygems, run:
17 |
18 | ```shell
19 | gem install mdl
20 | ```
21 |
22 | Alternatively you can build it from source:
23 |
24 | ```shell
25 | git clone https://github.com/markdownlint/markdownlint
26 | cd markdownlint
27 | rake install
28 | ```
29 |
30 | Note that you will need [rake](https://github.com/ruby/rake)
31 | (`gem install rake`) and [bundler](https://github.com/bundler/bundler)
32 | (`gem install bundler`) in order to build from source.
33 |
34 | ## Usage
35 |
36 | To have markdownlint check your markdown files, simply run `mdl` with the
37 | filenames as a parameter:
38 |
39 | ```shell
40 | mdl README.md
41 | ```
42 |
43 | Markdownlint can also take a directory, and it will scan all markdown files
44 | within the directory (and nested directories):
45 |
46 | ```shell
47 | mdl docs/
48 | ```
49 |
50 | If you don't specify a filename, markdownlint will use stdin:
51 |
52 | ```shell
53 | cat foo.md | mdl
54 | ```
55 |
56 | Markdownlint will output a list of issues it finds, and the line number where
57 | the issue is. See [RULES.md](docs/RULES.md) for information on each issue, as
58 | well as how to correct it:
59 |
60 | ```shell
61 | README.md:1: MD013 Line length
62 | README.md:70: MD029 Ordered list item prefix
63 | README.md:71: MD029 Ordered list item prefix
64 | README.md:72: MD029 Ordered list item prefix
65 | README.md:73: MD029 Ordered list item prefix
66 | ```
67 |
68 | Markdownlint has many more options you can pass on the command line, run
69 | `mdl --help` to see what they are, or see the documentation on
70 | [configuring markdownlint](docs/configuration.md).
71 |
72 | ### Styles
73 |
74 | Not everyone writes markdown in the same way, and there are multiple flavors
75 | and styles, each of which are valid. While markdownlint's default settings
76 | will result in markdown files that reflect the author's preferred markdown
77 | authoring preferences, your project may have different guidelines.
78 |
79 | It's not markdownlint's intention to dictate any one specific style, and in
80 | order to support these differing styles and/or preferences, markdownlint
81 | supports what are called 'style files'. A style file is a file describing
82 | which rules markdownlint should enable, and also what settings to apply to
83 | individual rules. For example, rule [MD013](docs/RULES.md#md013---line-length)
84 | checks for long lines, and by default will report an issue for any line longer
85 | than 80 characters. If your project has a different maximum line length limit,
86 | or if you don't want to enforce a line limit at all, then this can be
87 | configured in a style file.
88 |
89 | For more information on creating style files, see the
90 | [creating styles](docs/creating_styles.md) document.
91 |
92 | ### Custom rules and rulesets
93 |
94 | It may be that the rules provided in this project don't cover your stylistic
95 | needs. To account for this, markdownlint supports the creation and use of custom
96 | rules.
97 |
98 | For more information, see the [creating rules](docs/creating_rules.md) document.
99 |
100 | ## Related projects
101 |
102 | - [markdownlint](https://github.com/DavidAnson/markdownlint) - A similar
103 | project, but limited in Node.js
104 | - [markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli) - A CLI
105 | for the above Node.js project
106 | - [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2) - An
107 | alternative CLI for the Node.js project
108 |
109 | ## Contributing
110 |
111 | See [CONTRIBUTING.md](CONTRIBUTING.md) for more information.
112 |
--------------------------------------------------------------------------------
/docs/configuration.md:
--------------------------------------------------------------------------------
1 | # Mdl configuration
2 |
3 | Markdownlint has several options you can configure both on the command line, or
4 | in markdownlint's configuration file: `.mdlrc`, first looked for in the working
5 | directory, then in your home directory. While markdownlint will work perfectly
6 | well out of the box, this page documents some of the options you can change to
7 | suit your needs.
8 |
9 | In general, anything you pass on the command line can also be put into
10 | `~/.mdlrc` with the same option. For example, if you pass `--style foo` on the
11 | command line, you can make this the default by putting `style "foo"` into your
12 | `~/.mdlrc` file.
13 |
14 | ## Configuration options
15 |
16 | ### General options
17 |
18 | Verbose - Print additional information about what markdownlint is doing.
19 |
20 | * Command line: `-v`, `--verbose`
21 | * Config file: `verbose true`
22 | * Default: false
23 |
24 | Show warnings - Kramdown will generate warnings of its own for some issues
25 | found with documents during parsing, and markdownlint can print these out in
26 | addition to using the built in rules. This option enables/disables that
27 | behavior.
28 |
29 | * Command line: `-w`, `--warnings`
30 | * Config file: `show_kramdown_warnings true`
31 | * Default: true
32 |
33 | Recurse using files known to git - When mdl is given a directory name on the
34 | command line, it will recurse into that directory looking for markdown files to
35 | process. If this option is enabled, it will use git to look for files instead,
36 | and ignore any files git doesn't know about.
37 |
38 | * Command line: `-g`, `--git-recurse`
39 | * Config file: `git_recurse true`
40 | * Default: false
41 |
42 | Ignore YAML front matter - If this option is enabled markdownlint will ignore
43 | content within valid [YAML front
44 | matter](https://jekyllrb.com/docs/frontmatter/). Reported line numbers will
45 | still match the file contents but markdownlint will consider the line following
46 | front matter to be the first line.
47 |
48 | * Command line: `-i`, `--ignore-front-matter`
49 | * Config file: `ignore_front_matter true`
50 | * Default: false
51 |
52 | ### Specifying which rules mdl processes
53 |
54 | Tags - Limit the rules mdl enables to those containing the provided tags.
55 |
56 | * Command line: `-t tag1,tag2`, `--tags tag1,tag2`, `-t ~tag1,~tag2`
57 | * Config file: `tags "tag1", "tag2"`
58 | * Default: process all rules (no tag limit)
59 |
60 | Rules - Limit the rules mdl enables to those provided in this option.
61 |
62 | * Command line: `-r MD001,MD002`, `--rules MD001,MD002`, `-r ~MD001,~MD002`
63 | * Config file: `rules "MD001", "MD002"`
64 | * Default: process all rules (no rule limit)
65 |
66 | If a rule or tag ID is preceded by a tilde (`~`), then it _disables_ the
67 | matching rules instead of enabling them, starting with all rules being enabled.
68 |
69 | Note: If both `--rules` and `--tags` are provided, then a given rule has to
70 | both be in the list of enabled rules, as well as be tagged with one of the tags
71 | provided with the `--tags` option. Use the `-l/--list-rules` option to test
72 | this behavior.
73 |
74 | Style - Select which style mdl uses. A 'style' is a file containing a list of
75 | enabled/disable rules, as well as options for some rules that take them. For
76 | example, one style might enforce a line length of 80 characters, while another
77 | might choose 72 characters, and another might have no line length limit at all
78 | (rule MD013).
79 |
80 | * Command line: `-s style_name`, `--style style_name`
81 | * Config file: `style "style_name"`
82 | * Default: Use the style called 'default'
83 |
84 | Note: The value for `style_name` must either end with `.rb` or have `/` in it
85 | in order to tell `mdl` to look for a custom style, and not a built-in style.
86 |
87 | Note: When setting `style` in `mdlrc`, it is highly recommended that either a
88 | fully-qualified path be used, or that the relative values be passed in a form
89 | like `File.join(File.dirname(__FILE__), '.mdl.style')` so that the value is
90 | relative to the `mdlrc` and not the path the user happens to be in.
91 |
92 | Rulesets - Load a custom ruleset file. This option allows you to load custom
93 | rules in addition to those included with markdownlint.
94 |
95 | * Command line: `-u ruleset.rb,ruleset2.rb`, `--rulesets ruleset.rb,ruleset2.rb`
96 | * Config file: `rulesets ['ruleset.rb', 'ruleset2.rb']`
97 | * Default: Don't load any additional rulesets
98 |
99 | No default ruleset - Skip loading the default ruleset file included with
100 | markdownlint. Use this option if you only want to load custom rulesets.
101 |
102 | * Command line: `-d`, `--skip-default-ruleset`
103 | * Config file: `skip_default_ruleset true`
104 | * Default: Load the default ruleset.
105 |
106 | ## Creating your own .mdlrc files
107 |
108 | You can configure `mdl` using your own `.mdlrc` file. You can specify and
109 | command-line option in this file.
110 |
111 | In particular you can specify a `style` file, where you have configured any
112 | rules. Here's a simple example that just points to a style file:
113 |
114 | ```ruby
115 | style "#{File.dirname(__FILE__)}/{your_markdown_rule_file_path}.rb"
116 | ```
117 |
118 | As commented, this path is relative to `.mdlrc` file. You can find a basic
119 | example of `.mdlrc` file [here](../example/.mdlrc_example).
120 |
121 | Then you should create your new [rule](creating_rules.md) or
122 | [style](creating_styles.md).
123 |
124 | You can find a basic example of new style file
125 | [here](../example/new_style_example.rb).
126 |
--------------------------------------------------------------------------------
/lib/mdl.rb:
--------------------------------------------------------------------------------
1 | require_relative 'mdl/formatters/sarif'
2 | require_relative 'mdl/cli'
3 | require_relative 'mdl/config'
4 | require_relative 'mdl/doc'
5 | require_relative 'mdl/kramdown_parser'
6 | require_relative 'mdl/ruleset'
7 | require_relative 'mdl/style'
8 | require_relative 'mdl/version'
9 |
10 | require 'kramdown'
11 | require 'mixlib/shellout'
12 |
13 | # Primary MDL container
14 | module MarkdownLint
15 | def self.run(argv = ARGV)
16 | cli = MarkdownLint::CLI.new
17 | cli.run(argv)
18 | ruleset = RuleSet.new
19 | ruleset.load_default unless Config[:skip_default_ruleset]
20 | Config[:rulesets]&.each do |r|
21 | ruleset.load(r)
22 | end
23 | rules = ruleset.rules
24 | Style.load(Config[:style], rules)
25 | # Rule option filter
26 | if Config[:rules]
27 | unless Config[:rules][:include].empty?
28 | rules.select! do |r, v|
29 | Config[:rules][:include].include?(r) or
30 | !(Config[:rules][:include] & v.aliases).empty?
31 | end
32 | end
33 | unless Config[:rules][:exclude].empty?
34 | rules.select! do |r, v|
35 | !Config[:rules][:exclude].include?(r) and
36 | (Config[:rules][:exclude] & v.aliases).empty?
37 | end
38 | end
39 | end
40 | # Tag option filter
41 | if Config[:tags]
42 | rules.reject! { |_r, v| (v.tags & Config[:tags][:include]).empty? } \
43 | unless Config[:tags][:include].empty?
44 | rules.select! { |_r, v| (v.tags & Config[:tags][:exclude]).empty? } \
45 | unless Config[:tags][:exclude].empty?
46 | end
47 |
48 | if Config[:list_rules]
49 | puts 'Enabled rules:'
50 | rules.each do |id, rule|
51 | if Config[:verbose]
52 | puts "#{id} (#{rule.aliases.join(', ')}) [#{rule.tags.join(', ')}] " +
53 | "- #{rule.description}"
54 | elsif Config[:show_aliases]
55 | puts "#{rule.aliases.first || id} - #{rule.description}"
56 | else
57 | puts "#{id} - #{rule.description}"
58 | end
59 | end
60 | exit 0
61 | end
62 |
63 | # Recurse into directories
64 | cli.cli_arguments.each_with_index do |filename, i|
65 | if Dir.exist?(filename)
66 | if Config[:git_recurse]
67 | Dir.chdir(filename) do
68 | cli.cli_arguments[i] =
69 | Mixlib::ShellOut.new("git ls-files '*.md' '*.markdown'")
70 | .run_command.stdout.lines
71 | .map { |m| File.join(filename, m.strip) }
72 | end
73 | else
74 | cli.cli_arguments[i] = Dir["#{filename}/**/*.{md,markdown}"]
75 | end
76 | end
77 | end
78 | cli.cli_arguments.flatten!
79 |
80 | status = 0
81 | results = []
82 | docs_to_print = []
83 | cli.cli_arguments.each do |filename|
84 | puts "Checking #{filename}..." if Config[:verbose]
85 | unless filename == '-' || File.exist?(filename)
86 | warn(
87 | "#{Errno::ENOENT}: No such file or directory - #{filename}",
88 | )
89 | exit 3
90 | end
91 | doc = Doc.new_from_file(filename, Config[:ignore_front_matter])
92 | filename = '(stdin)' if filename == '-'
93 | if Config[:show_kramdown_warnings]
94 | status = 2 unless doc.parsed.warnings.empty?
95 | doc.parsed.warnings.each do |w|
96 | puts "#{filename}: Kramdown Warning: #{w}"
97 | end
98 | end
99 | rules.sort.each do |id, rule|
100 | puts "Processing rule #{id}" if Config[:verbose]
101 | error_lines = rule.check.call(doc)
102 | next if error_lines.nil? || error_lines.empty?
103 |
104 | status = 1
105 | error_lines.each do |line|
106 | line += doc.offset # Correct line numbers for any yaml front matter
107 | if Config[:json] || Config[:sarif]
108 | results << {
109 | 'filename' => filename,
110 | 'line' => line,
111 | 'rule' => id,
112 | 'aliases' => rule.aliases,
113 | 'description' => rule.description,
114 | 'docs' => rule.docs_url,
115 | }
116 | else
117 | linked_id = linkify(printable_id(rule), rule.docs_url)
118 | puts "#{filename}:#{line}: #{linked_id} " + rule.description.to_s
119 | end
120 | end
121 |
122 | # If we're not in JSON or SARIF mode (URLs are in the object), and we
123 | # cannot make real links (checking if we have a TTY is an OK heuristic
124 | # for that) then, instead of making the output ugly with long URLs, we
125 | # print them at the end. And of course we only want to print each URL
126 | # once.
127 | if !Config[:json] && !Config[:sarif] &&
128 | !$stdout.tty? && !docs_to_print.include?(rule)
129 | docs_to_print << rule
130 | end
131 | end
132 | end
133 |
134 | if Config[:json]
135 | require 'json'
136 | puts JSON.generate(results)
137 | elsif Config[:sarif]
138 | puts SarifFormatter.generate(rules, results)
139 | elsif docs_to_print.any?
140 | puts "\nFurther documentation is available for these failures:"
141 | docs_to_print.each do |rule|
142 | puts " - #{printable_id(rule)}: #{rule.docs_url}"
143 | end
144 | end
145 | exit status
146 | end
147 |
148 | def self.printable_id(rule)
149 | return rule.aliases.first if Config[:show_aliases] && rule.aliases.any?
150 |
151 | rule.id
152 | end
153 |
154 | # Creates hyperlinks in terminal emulators, if available: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
155 | def self.linkify(text, url)
156 | return text unless $stdout.tty? && url
157 |
158 | "\e]8;;#{url}\e\\#{text}\e]8;;\e\\"
159 | end
160 | end
161 |
--------------------------------------------------------------------------------
/docs/creating_rules.md:
--------------------------------------------------------------------------------
1 | # Creating Rules and Rulesets
2 |
3 | If the rules provided in this project don't cover your stylistic needs then you
4 | can create a custom ruleset. A ruleset is a Ruby file that contains rules. Rules
5 | are small pieces of Ruby code; these are [described below](#rule-syntax).
6 |
7 | ## Using custom rules
8 |
9 | When a custom ruleset has been created, the ruleset can be loaded using the
10 | configuration options described in [the configuration
11 | document](./configuration.md) and referred to in the style document.
12 |
13 | As a minimal example: if `foo.rb` is a custom ruleset in the same directory as
14 | `.mdlrc`, and if `foo.rb` contains the custom rules `BAR001` and `BAR002`, then
15 | these can be used by adding the following lines to `.mdlrc`
16 |
17 | ```ruby
18 | rulesets ["#{File.dirname(__FILE__)}/foo.rb"]
19 | style "#{File.dirname(__FILE__)}/baz.rb"
20 | ```
21 |
22 | where `baz.rb` is a style file that contains the contents
23 |
24 | ```ruby
25 | rule "BAR001"
26 | rule "BAR002"
27 | ```
28 |
29 | ## Rule syntax
30 |
31 | Rules are written in ruby, using a rule DSL for defining rules. A rule looks
32 | like:
33 |
34 | ```ruby
35 | rule "MY000", "Rule description" do
36 | tags :foo, :bar
37 | docs 'https://docs.example.org/more/info#MY000'
38 | aliases 'rule-name'
39 | params :style => :foo
40 | check do |doc|
41 | # check code goes here
42 | # return a list of line numbers that break the rule, or an empty list
43 | # (or nil) if there are no problems.
44 | end
45 | end
46 | ```
47 |
48 | The first line specifies the rule name and description. By convention, built in
49 | markdownlint rules use the prefix 'MD' followed by a number to identify rules.
50 | Any custom rules should use an alternate prefix to avoid conflicting with
51 | current or future rules. The description is simply a short description
52 | explaining what the rule is checking, which will be printed alongside the rule
53 | name when rules are triggered.
54 |
55 | Next, the rule's tags are specified. These are simply ruby symbols, and can be
56 | used by a user to limit which rules are checks. For example, if your rule
57 | checks whitespace usage in a document, you can add the `:whitespace` tag, and
58 | users who don't care about whitespace can exclude that tag on the command line
59 | or in style files.
60 |
61 | You can optionally provide a URL with more context on this rule and why it
62 | exists. `markdownlint` links to this URL when this rule fails, which can help
63 | the people receiving a failure understand how to fix it. Docs URLs can also be
64 | specified once for many rules in a ruleset, the built-in rules
65 | [use this feature](
66 | https://github.com/markdownlint/markdownlint/blob/81e99c03f1f096aa200011ff7a1043a6f81167e7/lib/mdl/rules.rb#L1-L5
67 | ) to set a URL dynamically using the rule id and/or description.
68 |
69 | You can also specify aliases for the rule, which can be used to refer to the
70 | rule with a human-readable name rather than MD000. To do this, add then with
71 | the 'aliases' directive. Whenever you refer to a rule, such as for
72 | including/excluding in the configuration or in style files, you can use an
73 | alias for the rule instead of its ID.
74 |
75 | After that, any parameters the rule takes are specified. If your rule checks
76 | for a specific number of things, or if you can envision multiple variants of
77 | the same rule, then you should add parameters to allow your rule to be
78 | customized in a style file. Any parameters specified here are accessible inside
79 | the check itself using `params[:foo]`.
80 |
81 | Finally, the check itself is specified. This is simply a ruby block that should
82 | return a list of line numbers for any issues found. If no line numbers are
83 | found, you can either return an empty list, or nil, whichever is easiest for
84 | your check.
85 |
86 | ### Document objects
87 |
88 | The check takes a single parameter `doc`, which is an object containing a
89 | representation of the markdown document along with several helper functions
90 | used for making rules. The [doc.rb](../lib/mdl/doc.rb) file is documented using
91 | rdoc, and you will want to take a look there to see all the methods you can
92 | use, as well as look at some of the existing rules, but a quick summary is as
93 | follows:
94 |
95 | * `doc` - Object containing a representation of the markdown document
96 | * `doc.lines` - The raw markdown file as an array of lines
97 | * You can also look up a line given an element with
98 | `doc.element_line(element)`
99 | * `doc.parsed` - The kramdown internal representation of the doc. Most of the
100 | time you will want to interact with the parsed version of the document
101 | rather than looking at `doc.lines`.
102 | * `doc.find_type_elements` - A method to find all elements of a given type.
103 | You pass the type as a symbol, such as `:ul` or `:p`. Most element types
104 | match the name of the element in HTML output. This method returns a list of
105 | the matching elements.
106 | * `doc.find_type` - This is like `doc.find_type_elements`, but returns just
107 | the options hashes (see below) for each element. This is useful if you don't
108 | need all the element information, but you do need the line numbers.
109 | * `doc.element_line_number` - Pass in an element (or an options hash), and
110 | this will return the line number for the element. You need to return the
111 | line number in the list of errors.
112 |
113 | ### Element objects
114 |
115 | The document contains an internal representation of the markdown document as
116 | parsed by kramdown. Kramdown's representation of the document is as a tree of
117 | 'element' objects. The following is a quick summary of those objects:
118 |
119 | * element.type - a symbol denoting the type of the element, such as `:li`,
120 | `:p`, `:text`
121 | * element.value - the value of the element. Note that most block level
122 | elements such as paragraphs don't have any value themselves, but have child
123 | text elements containing their contents instead.
124 | * element.children - A list of the element's child elements.
125 | * element.options - A hash containing:
126 | * `:location` - line number of element
127 | * `:element_level` - A value filled in by markdownlint to denote the nesting
128 | level of the element, i.e. how deep in the tree is it.
129 | * Other options that are element type specific.
130 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Markdownlint
2 |
3 | We're glad you want to contribute markdownlint! This document will help answer
4 | common questions you may have during your first contribution. Please, try to
5 | follow these guidelines when you do so.
6 |
7 | ## Issue Reporting
8 |
9 | Not every contribution comes in the form of code. Submitting, confirming, and
10 | triaging issues is an important task for any project. We use GitHub to track
11 | all project issues. If you discover bugs, have ideas for improvements or new
12 | features, please start by [opening an
13 | issue](https://github.com/markdownlint/markdownlint/issues) on this repository.
14 | We use issues to centralize the discussion and agree on a plan of action before
15 | spending time and effort writing code that might not get used.
16 |
17 | ### Submitting An Issue
18 |
19 | * Check that the issue has not already been reported
20 | * Check that the issue has not already been fixed in the latest code
21 | (a.k.a. `main`)
22 | * Select the appropriate issue type and open an issue with a descriptive title
23 | * Be clear, concise, and precise using grammatically correct, complete sentences
24 | in your summary of the problem
25 | * Include the output of `mdl -V` or `mdl --version`
26 | * Include any relevant code in the issue
27 |
28 | ## Code Contributions
29 |
30 | Markdownlint follows a [forking
31 | workflow](https://guides.github.com/activities/forking/), and we have a simple
32 | process for contributions:
33 |
34 | 1. Open an issue on the [project
35 | repository](https://github.com/markdownlint/markdownlint/issues), if
36 | appropriate
37 | 1. If you're adding or making changes to rules, read the [Development
38 | docs](#local-development)
39 | 1. Follow the [forking workflow](https://guides.github.com/activities/forking/)
40 | steps:
41 | 1. Fork the project ( )
42 | 1. Create your feature branch (`git checkout -b my-new-feature`)
43 | 1. Commit your changes (`git commit -am 'Add some feature'`)
44 | 1. Sign your changes (`git commit --amend -s`)
45 | 1. Push to the branch (`git push origin my-new-feature`)
46 | 1. Create a [GitHub Pull
47 | Request](https://help.github.com/articles/about-pull-requests/) for your
48 | change, following all [pull request
49 | requirements](#pull-request-requirements) and any instructions in the pull
50 | request template
51 | 1. Participate in a [Code Review](#code-review-process) with the project
52 | maintainers on the pull request
53 |
54 | ### Your First Code Contribution
55 |
56 | Unsure where to begin contributing to Markdownlint? You can start by looking
57 | through these beginner and help-wanted issues:
58 |
59 | * [Beginner issues](https://github.com/markdownlint/markdownlint/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+sort%3Acomments-desc)
60 | * Issues which should only require a few lines of code, and a test or two.
61 | * [Help wanted issues](https://github.com/markdownlint/markdownlint/issues?q=is%3aissue+is%3aopen+label%3a%22help+wanted%22+sort%3Acomments-desc)
62 | * Issues which should be a bit more involved than beginner issues. Both
63 | issue lists are sorted by total number of comments. While not perfect,
64 | number of comments is a reasonable proxy for impact a given change will
65 | have.
66 |
67 | ### Pull Request Requirements
68 |
69 | Markdownlint strives to ensure high quality for the project. In order to
70 | promote this, we require that all pull requests to meet these specifications:
71 |
72 | * **Tests:** To ensure high quality code and protect against future
73 | regressions, we require tests for all new/changed functionality in
74 | Markdownlint. Test positive and negative scenarios, try to break the new code
75 | now.
76 | * **Green CI Tests:** We use [GitHub
77 | Actions](https://github.com/markdownlint/markdownlint/actions) to test all pull
78 | requests. We require these test runs to succeed on every pull request before
79 | being merged.
80 | * **Signed Commits:** With [Developer Certificates of
81 | Origin](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin), we
82 | want to ascertain your contribution rightfully may enter the project. GitHub
83 | hence [automatically checks](https://github.com/apps/dco) your PR in line as
84 | one prerequisite for a merge. For additional context, see e.g. the discussion
85 | on
86 | [stackoverflow](https://stackoverflow.com/questions/1962094/what-is-the-sign-off-feature-in-git-for)
87 | or the reasoning on
88 | [ProgressChef](https://www.chef.io/blog/introducing-developer-certificate-of-origin).
89 |
90 | ### Code Review Process
91 |
92 | Code review takes place in GitHub pull requests. See [this
93 | article](https://help.github.com/articles/about-pull-requests/) if you're not
94 | familiar with GitHub Pull Requests.
95 |
96 | Once you open a pull request, project maintainers will review your code and
97 | respond to your pull request with any feedback they might have. The process at
98 | this point is as follows:
99 |
100 | 1. A review is required from at least one of the project maintainers. See the
101 | maintainers document for Markdownlint project at
102 | .
103 | 1. Your change will be merged into the project's `main` branch, and all
104 | [commits will be
105 | squashed](https://help.github.com/en/articles/about-pull-request-merges#squash-and-merge-your-pull-request-commits)
106 | during the merge.
107 |
108 | If you would like to learn about when your code will be available in a release
109 | of Markdownlint, read more about [Markdownlint Release
110 | Cycles](#release-cycles).
111 |
112 | ## Releases
113 |
114 | We release Markdownlint as a gem to [Rubygems](https://rubygems.org/gems/mdl)
115 | and maintain a [Dockerfile](https://hub.docker.com/r/mivok/markdownlint)
116 |
117 | Markdownlint follows the [Semantic Versioning](https://semver.org/) standard.
118 | Our standard version numbers look like `X.Y.Z` and translates to:
119 |
120 | * `X` is a major release: has changes that may be incompatible with prior major
121 | releases
122 | * `Y` is a minor release: adds new functionality and bug fixes in a backwards
123 | compatible manner
124 | * `Z` is a patch release: adds backwards compatible bug fixes
125 |
126 | *Exception: Versions < 1.0 may introduce backwards-incompatible changes in a
127 | minor version*
128 |
--------------------------------------------------------------------------------
/lib/mdl/cli.rb:
--------------------------------------------------------------------------------
1 | require 'mixlib/cli'
2 | require 'pathname'
3 |
4 | module MarkdownLint
5 | # Our Mixlib::CLI class
6 | class CLI
7 | include Mixlib::CLI
8 |
9 | CONFIG_FILE = '.mdlrc'.freeze
10 |
11 | banner "Usage: #{File.basename($PROGRAM_NAME)} [options] [FILE.md|DIR ...]"
12 |
13 | option :show_aliases,
14 | :short => '-a',
15 | :long => '--[no-]show-aliases',
16 | :description =>
17 | 'Show rule alias instead of rule ID when viewing rules',
18 | :boolean => true
19 |
20 | option :config_file,
21 | :short => '-c',
22 | :long => '--config FILE',
23 | :description => 'The configuration file to use',
24 | :default => CONFIG_FILE.to_s
25 |
26 | option :verbose,
27 | :short => '-v',
28 | :long => '--[no-]verbose',
29 | :description => 'Increase verbosity',
30 | :boolean => true
31 |
32 | option :ignore_front_matter,
33 | :short => '-i',
34 | :long => '--[no-]ignore-front-matter',
35 | :boolean => true,
36 | :description => 'Ignore YAML front matter'
37 |
38 | option :show_kramdown_warnings,
39 | :short => '-w',
40 | :long => '--[no-]warnings',
41 | :description => 'Show kramdown warnings',
42 | :boolean => true
43 |
44 | option :tags,
45 | :short => '-t',
46 | :long => '--tags TAG1,TAG2',
47 | :description => 'Only process rules with these tags',
48 | :proc => proc { |v| toggle_list(v, true) }
49 |
50 | option :rules,
51 | :short => '-r',
52 | :long => '--rules RULE1,RULE2',
53 | :description => 'Only process these rules',
54 | :proc => proc { |v| toggle_list(v) }
55 |
56 | option :style,
57 | :short => '-s',
58 | :long => '--style STYLE',
59 | :description => 'Load the given style'
60 |
61 | option :list_rules,
62 | :short => '-l',
63 | :long => '--list-rules',
64 | :boolean => true,
65 | :description => "Don't process any files, just list enabled rules"
66 |
67 | option :git_recurse,
68 | :short => '-g',
69 | :long => '--git-recurse',
70 | :boolean => true,
71 | :description =>
72 | 'Only process files known to git when given a directory'
73 |
74 | option :rulesets,
75 | :short => '-u',
76 | :long => '--rulesets RULESET1,RULESET2',
77 | :proc => proc { |v| v.split(',') },
78 | :description => 'Specify additional ruleset files to load'
79 |
80 | option :skip_default_ruleset,
81 | :short => '-d',
82 | :long => '--skip-default-ruleset',
83 | :boolean => true,
84 | :description => "Don't load the default markdownlint ruleset"
85 |
86 | option :help,
87 | :on => :tail,
88 | :short => '-h',
89 | :long => '--help',
90 | :description => 'Show this message',
91 | :boolean => true,
92 | :show_options => true,
93 | :exit => 0
94 |
95 | option :version,
96 | :on => :tail,
97 | :short => '-V',
98 | :long => '--version',
99 | :description => 'Show version',
100 | :boolean => true,
101 | :proc => proc { puts MarkdownLint::VERSION },
102 | :exit => 0
103 |
104 | option :json,
105 | :short => '-j',
106 | :long => '--json',
107 | :description => 'JSON output',
108 | :boolean => true
109 |
110 | option :sarif,
111 | :short => '-S',
112 | :long => '--sarif',
113 | :description => 'SARIF output',
114 | :boolean => true
115 |
116 | def run(argv = ARGV)
117 | parse_options(argv)
118 |
119 | # Load the config file if it's present
120 | filename = CLI.probe_config_file(config[:config_file])
121 |
122 | # Only fall back to ~/.mdlrc if we are using the default value for -c
123 | if filename.nil? && (config[:config_file] == CONFIG_FILE)
124 | filename = File.expand_path("~/#{CONFIG_FILE}")
125 | end
126 |
127 | if !filename.nil? && File.exist?(filename)
128 | MarkdownLint::Config.from_file(filename.to_s)
129 | puts "Loaded config from #{filename}" if config[:verbose]
130 | end
131 |
132 | # Put values in the config file
133 | MarkdownLint::Config.merge!(config)
134 |
135 | # Set the correct format for any rules/tags configuration loaded from
136 | # the config file. Ideally this would probably be done as part of the
137 | # config class itself rather than here.
138 | unless MarkdownLint::Config[:rules].nil?
139 | MarkdownLint::Config[:rules] = CLI.toggle_list(
140 | MarkdownLint::Config[:rules],
141 | )
142 | end
143 | unless MarkdownLint::Config[:tags].nil?
144 | MarkdownLint::Config[:tags] = CLI.toggle_list(
145 | MarkdownLint::Config[:tags], true
146 | )
147 | end
148 |
149 | # Read from stdin if we didn't provide a filename
150 | cli_arguments << '-' if cli_arguments.empty? && !config[:list_rules]
151 | end
152 |
153 | def self.toggle_list(parts, to_sym = false)
154 | parts = parts.split(',') if parts.instance_of?(String)
155 | if parts.instance_of?(Array)
156 | inc = parts.reject { |p| p.start_with?('~') }
157 | exc = parts.select { |p| p.start_with?('~') }.map { |p| p[1..] }
158 | if to_sym
159 | inc.map!(&:to_sym)
160 | exc.map!(&:to_sym)
161 | end
162 | { :include => inc, :exclude => exc }
163 | else
164 | # We already converted the string into a list of include/exclude
165 | # pairs, so just return as is
166 | parts
167 | end
168 | end
169 |
170 | def self.probe_config_file(path)
171 | expanded_path = File.expand_path(path)
172 | return expanded_path if File.exist?(expanded_path)
173 |
174 | # Look for a file up from the working dir
175 | Pathname.new(expanded_path).ascend do |p|
176 | next unless p.directory?
177 |
178 | config_file = p.join(CONFIG_FILE)
179 | return config_file if File.exist?(config_file)
180 | end
181 | nil
182 | end
183 | end
184 | end
185 |
--------------------------------------------------------------------------------
/lib/mdl/doc.rb:
--------------------------------------------------------------------------------
1 | require 'kramdown'
2 | require_relative 'kramdown_parser'
3 |
4 | module MarkdownLint
5 | ##
6 | # Representation of the markdown document passed to rule checks
7 | class Doc
8 | ##
9 | # A list of raw markdown source lines. Note that the list is 0-indexed,
10 | # while line numbers in the parsed source are 1-indexed, so you need to
11 | # subtract 1 from a line number to get the correct line. The element_line*
12 | # methods take care of this for you.
13 |
14 | attr_reader :lines, :parsed, :elements, :offset
15 |
16 | ##
17 | # A Kramdown::Document object containing the parsed markdown document.
18 |
19 | ##
20 | # A list of top level Kramdown::Element objects from the parsed document.
21 |
22 | ##
23 | # The line number offset which is greater than zero when the
24 | # markdown file contains YAML front matter that should be ignored.
25 |
26 | ##
27 | # Create a new document given a string containing the markdown source
28 |
29 | def initialize(text, ignore_front_matter = false)
30 | regex = /^---\n(.*?)---\n\n?/m
31 | if ignore_front_matter && regex.match(text)
32 | @offset = regex.match(text).to_s.split("\n").length
33 | text.sub!(regex, '')
34 | else
35 | @offset = 0
36 | end
37 | # The -1 is to cause split to preserve an extra entry in the array so we
38 | # can tell if there's a final newline in the file or not.
39 | @lines = text.split(/\R/, -1)
40 | @parsed = Kramdown::Document.new(text, :input => 'MarkdownLint')
41 | @elements = @parsed.root.children
42 | add_annotations(@elements)
43 | end
44 |
45 | ##
46 | # Alternate 'constructor' passing in a filename
47 |
48 | def self.new_from_file(filename, ignore_front_matter = false)
49 | if filename == '-'
50 | new($stdin.read, ignore_front_matter)
51 | else
52 | new(File.read(filename, :encoding => 'UTF-8'), ignore_front_matter)
53 | end
54 | end
55 |
56 | ##
57 | # Find all elements of a given type, returning their options hash. The
58 | # options hash has most of the useful data about an element and often you
59 | # can just use this in your rules.
60 | #
61 | # # Returns [ { :location => 1, :element_level => 2 }, ... ]
62 | # elements = find_type(:li)
63 | #
64 | # If +nested+ is set to false, this returns only top level elements of a
65 | # given type.
66 |
67 | def find_type(type, nested = true)
68 | find_type_elements(type, nested).map(&:options)
69 | end
70 |
71 | ##
72 | # Find all elements of a given type, returning a list of the element
73 | # objects themselves.
74 | #
75 | # Instead of a single type, a list of types can be provided instead to
76 | # find all types.
77 | #
78 | # If +nested+ is set to false, this returns only top level elements of a
79 | # given type.
80 |
81 | def find_type_elements(type, nested = true, elements = @elements)
82 | results = []
83 | type = [type] if type.instance_of?(Symbol)
84 | elements.each do |e|
85 | results.push(e) if type.include?(e.type)
86 | if nested && !e.children.empty?
87 | results.concat(find_type_elements(type, nested, e.children))
88 | end
89 | end
90 | results
91 | end
92 |
93 | ##
94 | # A variation on find_type_elements that allows you to skip drilling down
95 | # into children of specific element types.
96 | #
97 | # Instead of a single type, a list of types can be provided instead to
98 | # find all types.
99 | #
100 | # Unlike find_type_elements, this method will always search for nested
101 | # elements, and skip the element types given to nested_except.
102 |
103 | def find_type_elements_except(
104 | type, nested_except = [], elements = @elements
105 | )
106 | results = []
107 | type = [type] if type.instance_of?(Symbol)
108 | nested_except = [nested_except] if nested_except.instance_of?(Symbol)
109 | elements.each do |e|
110 | results.push(e) if type.include?(e.type)
111 | next if nested_except.include?(e.type) || e.children.empty?
112 |
113 | results.concat(
114 | find_type_elements_except(type, nested_except, e.children),
115 | )
116 | end
117 | results
118 | end
119 |
120 | ##
121 | # Returns the line number a given element is located on in the source
122 | # file. You can pass in either an element object or an options hash here.
123 |
124 | def element_linenumber(element)
125 | element = element.options if element.is_a?(Kramdown::Element)
126 | element[:location]
127 | end
128 |
129 | ##
130 | # Returns the actual source line for a given element. You can pass in an
131 | # element object or an options hash here. This is useful if you need to
132 | # examine the source line directly for your rule to make use of
133 | # information that isn't present in the parsed document.
134 |
135 | def element_line(element)
136 | @lines[element_linenumber(element) - 1]
137 | end
138 |
139 | ##
140 | # Returns a list of line numbers for all elements passed in. You can pass
141 | # in a list of element objects or a list of options hashes here.
142 |
143 | def element_linenumbers(elements)
144 | elements.map { |e| element_linenumber(e) }
145 | end
146 |
147 | ##
148 | # Returns the actual source lines for a list of elements. You can pass in
149 | # a list of elements objects or a list of options hashes here.
150 |
151 | def element_lines(elements)
152 | elements.map { |e| element_line(e) }
153 | end
154 |
155 | ##
156 | # Returns the header 'style' - :atx (hashes at the beginning), :atx_closed
157 | # (atx header style, but with hashes at the end of the line also), :setext
158 | # (underlined). You can pass in the element object or an options hash
159 | # here.
160 |
161 | def header_style(header)
162 | if header.type != :header
163 | raise 'header_style called with non-header element'
164 | end
165 |
166 | line = element_line(header)
167 | if line.start_with?('#')
168 | if line.strip.end_with?('#')
169 | :atx_closed
170 | else
171 | :atx
172 | end
173 | else
174 | :setext
175 | end
176 | end
177 |
178 | ##
179 | # Returns the list style for a list: :asterisk, :plus, :dash, :ordered or
180 | # :ordered_paren depending on which symbol is used to denote the list
181 | # item. You can pass in either the element itself or an options hash here.
182 |
183 | def list_style(item)
184 | raise 'list_style called with non-list element' if item.type != :li
185 |
186 | line = element_line(item).strip.gsub(/^>\s+/, '')
187 | if line.start_with?('*')
188 | :asterisk
189 | elsif line.start_with?('+')
190 | :plus
191 | elsif line.start_with?('-')
192 | :dash
193 | elsif line.match('[0-9]+\.')
194 | :ordered
195 | elsif line.match('[0-9]+\)')
196 | :ordered_paren
197 | else
198 | :unknown
199 | end
200 | end
201 |
202 | ##
203 | # Returns how much a given line is indented. Hard tabs are treated as an
204 | # indent of 8 spaces. You need to pass in the raw string here.
205 |
206 | def indent_for(line)
207 | line.match(/^\s*/)[0].gsub("\t", ' ' * 8).length
208 | end
209 |
210 | ##
211 | # Returns line numbers for lines that match the given regular expression
212 |
213 | def matching_lines(regex)
214 | @lines.each_with_index.select { |text, _linenum| regex.match(text) }
215 | .map do |i|
216 | i[1] + 1
217 | end
218 | end
219 |
220 | ##
221 | # Returns line numbers for lines that match the given regular expression.
222 | # Only considers text inside of 'text' elements (i.e. regular markdown
223 | # text and not code/links or other elements).
224 | def matching_text_element_lines(regex, exclude_nested = [:a])
225 | matches = []
226 | find_type_elements_except(:text, exclude_nested).each do |e|
227 | first_line = e.options[:location]
228 | # We'll error out if kramdown doesn't have location information for
229 | # the current element. It's better to just not match in these cases
230 | # rather than crash.
231 | next if first_line.nil?
232 |
233 | lines = e.value.split("\n")
234 | lines.each_with_index do |l, i|
235 | matches << (first_line + i) if regex.match(l)
236 | end
237 | end
238 | matches
239 | end
240 |
241 | ##
242 | # Extracts the text from an element whose children consist of text
243 | # elements and other things
244 |
245 | def extract_text(element, prefix = '', restore_whitespace = true)
246 | quotes = {
247 | :rdquo => '"',
248 | :ldquo => '"',
249 | :lsquo => "'",
250 | :rsquo => "'",
251 | }
252 | # If anything goes amiss here, e.g. unknown type, then nil will be
253 | # returned and we'll just not catch that part of the text, which seems
254 | # like a sensible failure mode.
255 | lines = element.children.map do |e|
256 | if e.type == :text
257 | e.value
258 | elsif %i{strong em p codespan}.include?(e.type)
259 | extract_text(e, prefix, restore_whitespace).join("\n")
260 | elsif e.type == :smart_quote
261 | quotes[e.value]
262 | end
263 | end.join.split("\n")
264 | # Text blocks have whitespace stripped, so we need to add it back in at
265 | # the beginning. Because this might be in something like a blockquote,
266 | # we optionally strip off a prefix given to the function.
267 | lines[0] = element_line(element).sub(prefix, '') if restore_whitespace
268 | lines
269 | end
270 |
271 | ##
272 | # Returns the element as plaintext
273 |
274 | def extract_as_text(element)
275 | quotes = {
276 | :rdquo => '"',
277 | :ldquo => '"',
278 | :lsquo => "'",
279 | :rsquo => "'",
280 | }
281 | # If anything goes amiss here, e.g. unknown type, then nil will be
282 | # returned and we'll just not catch that part of the text, which seems
283 | # like a sensible failure mode.
284 | element.children.map do |e|
285 | if e.type == :text || e.type == :codespan
286 | e.value
287 | elsif %i{strong em p a}.include?(e.type)
288 | extract_as_text(e).join("\n")
289 | elsif e.type == :smart_quote
290 | quotes[e.value]
291 | end
292 | end.join.split("\n")
293 | end
294 |
295 | private
296 |
297 | ##
298 | # Adds a 'level' and 'parent' option to all elements to show how nested they
299 | # are
300 |
301 | def add_annotations(elements, level = 1, parent = nil)
302 | elements.each do |e|
303 | e.options[:element_level] = level
304 | e.options[:parent] = parent
305 | add_annotations(e.children, level + 1, e)
306 | end
307 | end
308 | end
309 | end
310 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## Unreleased
4 |
5 | ### Fixed
6 |
7 | * Fix Markdown lint version in SARIF output test [#469](https://github.com/markdownlint/markdownlint/pull/469)
8 |
9 | ## [v0.13.0] (2023-10-01)
10 |
11 | ### Rules Added
12 |
13 | * MD055 - Tables: Each row must start and end with a `|` [#464](https://github.com/markdownlint/markdownlint/pull/464)
14 | * MD056 - Tables: Number of columns is the same for all
15 | rows [#464](https://github.com/markdownlint/markdownlint/pull/464)
16 | * MD057 - Tables: In the second row every column must have at least `---`,
17 | possibly surrounded with alignment `:` chars [#464](https://github.com/markdownlint/markdownlint/pull/464)
18 |
19 | ### Added
20 |
21 | * Add SARIF output [#459](https://github.com/markdownlint/markdownlint/pull/459)
22 | * Document DCO process better [#448](https://github.com/markdownlint/markdownlint/pull/448)
23 | and [#449](https://github.com/markdownlint/markdownlint/pull/449)
24 |
25 | ### Changed
26 |
27 | * MD009 - Allow exactly 2 trailing spaces by default [#452](https://github.com/markdownlint/markdownlint/pull/452)
28 | * MD033 - Add `allowed_elements` parameter [#450](https://github.com/markdownlint/markdownlint/pull/450)
29 | * Updated build instructions [#431](https://github.com/markdownlint/markdownlint/pull/431)
30 |
31 | ### Fixed
32 |
33 | * MD027 - handle anchor elements correctly [#463](https://github.com/markdownlint/markdownlint/pull/463)
34 | * Fix examples for RULES.md for MD007 [#462](https://github.com/markdownlint/markdownlint/pull/462)
35 | * Fix links to use https instead of http [#447](https://github.com/markdownlint/markdownlint/pull/447)
36 | * Make RULES.md comply with our own rules [#439](https://github.com/markdownlint/markdownlint/pull/439)
37 | * Fix docker builds [#429](https://github.com/markdownlint/markdownlint/pull/429)
38 |
39 | ## [v0.12.0] (2022-10-17)
40 |
41 | ### Rules Added
42 |
43 | * MD047 - File should end with a blank line
44 |
45 | ### Added
46 |
47 | * New 'docs' method on rules to provide a URL and longer description
48 | * `docker_image`-based pre-commit
49 |
50 | ### Changed
51 |
52 | * Changed the default for MD007 to 3 spaces to match minimum spaces for ordered lists
53 | * Added option `:ignore_code_blocks` to rule MD010. If set to true, hard tabs in
54 | code blocks will be ignored.
55 | * Added option `:ignore_code_blocks` to rule MD013. If set to true, hard tabs in
56 | code blocks will be ignored. The option `:code_blocks` has been marked as
57 | deprecated in the documentation. If `:code_blocks` is set to false in the
58 | configuration, a deprecation warning is printed.
59 | * Improved documentation on custom rules and rulesets
60 | * Handle non-printable characters gracefully
61 | * Support configurable sublist styles for MD004
62 |
63 | ### Fixed
64 |
65 | * Fixed directory argument with `--git-recurse`
66 | * Preserve empty lines at the end of a file
67 |
68 | ## [v0.11.0] (2020-08-22)
69 |
70 | ### Fixed
71 |
72 | * Fixed crash when using `-g`
73 | * Fixed missing dependencies in docker image
74 |
75 | ## [v0.10.0] (2020-08-08)
76 |
77 | ### Added
78 |
79 | * More examples of mdlrc and style files
80 | * Added CI for Rubocop and Markdownlint on our own repo
81 |
82 | ### Fixed
83 |
84 | * Update Dockerfile to work with modern mdl
85 | * Update minimum version of kramdown for security advisory
86 | * Update minimum version of rubocop for security advisory
87 |
88 | ## [v0.9.0] (2020-02-21)
89 |
90 | ### Changed
91 |
92 | * Better error messages on missing styles [#302](https://github.com/markdownlint/markdownlint/pull/302)
93 | * Use require_relative to speed up requires [#297](https://github.com/markdownlint/markdownlint/pull/297)
94 | * Bumped alpine version in the Dockerfile [#155](https://github.com/markdownlint/markdownlint/pull/155)
95 |
96 | ### Fixed
97 |
98 | * Fix crash in --json [#286](https://github.com/markdownlint/markdownlint/pull/286)
99 | * Handle codeblocks that are nested [#293](https://github.com/markdownlint/markdownlint/pull/293)
100 | * Fix handling of blockquoted list items [#284](https://github.com/markdownlint/markdownlint/pull/284)
101 |
102 | ## [v0.8.0] (2019-11-08)
103 |
104 | ### Changed
105 |
106 | * Don't ship test / example files in the gem artifact [#282](https://github.com/markdownlint/markdownlint/pull/282)
107 |
108 | ### Fixed
109 |
110 | * Handle newlines on Windows better [#238](https://github.com/markdownlint/markdownlint/pull/238)
111 |
112 | ## [v0.7.0] (2019-10-24)
113 |
114 | ### Added
115 |
116 | * Pull request and issue templates for users and contributors
117 | * Move to kramdown 2
118 | * Handle Kramdown TOC
119 | * Loosen various dependencies and move minimum ruby version to 2.4
120 |
121 | ## [v0.6.0] (2019-10-19)
122 |
123 | ### Added
124 |
125 | * There are now CONTRIBUTING.md and MAINTAINERS.md docs
126 | * There is now a `.pre-commit-hooks.yaml` for those who want to use pre-commit.com
127 |
128 | ### Changed
129 |
130 | * Ignore blank line after front matter [#248](https://github.com/markdownlint/markdownlint/pull/248)
131 | * Only import JSON when necessary [#231](https://github.com/markdownlint/markdownlint/pull/231)
132 | * Use newere mixlib-cli [#265](https://github.com/markdownlint/markdownlint/pull/265)
133 | * Nicer error message when activating nonexistent rule [#246](https://github.com/markdownlint/markdownlint/pull/246)
134 | * Fix documentation on `ignore_front_matter` [#241](https://github.com/markdownlint/markdownlint/pull/241)
135 | * Update docs to use "MY" prefix for custom rule example [#245](https://github.com/markdownlint/markdownlint/pull/245)
136 | * Fix crash in MD039 when the link text is empty [#256](https://github.com/markdownlint/markdownlint/pull/256)
137 | * Reference Node.js markdownlint [#229](https://github.com/markdownlint/markdownlint/pull/229)
138 | * Added table of contents to RULES.md for easier
139 | navigation [#232](https://github.com/markdownlint/markdownlint/pull/232)
140 | * Fix typos in MD046 docs [#219](https://github.com/markdownlint/markdownlint/pull/219)
141 | * Fixed MD036 crash [#222](https://github.com/markdownlint/markdownlint/pull/222)
142 |
143 | ### Removed
144 |
145 | ## [v0.5.0] (2018-07-01)
146 |
147 | ### Added
148 |
149 | * Add md042 to enforce code block style
150 | * JSON formatter/output
151 |
152 | ### Changed
153 |
154 | * Allow different nesting on headers duplication check [#200](https://github.com/markdownlint/markdownlint/pull/200)
155 | * MD036 - Ignore multi-line emphasized paragraphs, and emphasized paragraphs
156 | that end in punctuation [#140](https://github.com/markdownlint/markdownlint/pull/140)
157 |
158 | ### Fixed
159 |
160 | * Fix issue numbers false positives [#168](https://github.com/markdownlint/markdownlint/pull/168)
161 | * Lint MD039 checking for nodes inside link text [#102](https://github.com/markdownlint/markdownlint/issues/102)
162 |
163 | ## [v0.4.0] (2016-08-22)
164 |
165 | ### Added
166 |
167 | * Ignore yaml front matter option [#130](https://github.com/markdownlint/markdownlint/pull/130)
168 | and [#143](https://github.com/markdownlint/markdownlint/pull/143)
169 |
170 | ### Changed
171 |
172 | * Allow top level header rules (MD002, MD025, MD041) to be
173 | configurable [#142](https://github.com/markdownlint/markdownlint/pull/142)
174 |
175 | ### Fixed
176 |
177 | * Read UTF-8 files correctly even if locale is set to
178 | C [#135](https://github.com/markdownlint/markdownlint/pull/135),
179 | [#146](https://github.com/markdownlint/markdownlint/pull/146),
180 | [#147](https://github.com/markdownlint/markdownlint/pull/147)
181 | and [#148](https://github.com/markdownlint/markdownlint/pull/148)
182 | * Fix issues loading .mdlrc
183 | file [#126](https://github.com/markdownlint/markdownlint/pull/126),
184 | [#129](https://github.com/markdownlint/markdownlint/pull/129),
185 | [#133](https://github.com/markdownlint/markdownlint/pull/133)
186 | and [#148](https://github.com/markdownlint/markdownlint/pull/148)
187 | * Fix erroneous triggering of MD022/MD023 in some cases [#144](https://github.com/markdownlint/markdownlint/pull/144)
188 | * Detect codeblock lines correctly when ignoring them [#141](https://github.com/markdownlint/markdownlint/pull/141)
189 |
190 | ## [v0.3.1] (2016-03-20)
191 |
192 | ### Fixed
193 |
194 | * Fix error on starting mdl
195 |
196 | ## [v0.3.0] (2016-03-19)
197 |
198 | ### Rules added
199 |
200 | * MD041 - First line in file should be a top level header
201 |
202 | ### Added
203 |
204 | * You can now load your own custom rules with the `-u` option. See
205 | [rules.rb](https://github.com/markdownlint/markdownlint/blob/main/lib/mdl/rules.rb)
206 | for an example of what a rules file looks like. Use the `-d` option if you
207 | don't want to load markdownlint's default ruleset.
208 | * You can now refer to rules by human-readable/writable aliases, such as
209 | 'ul-style' instead of 'MD004'. See RULES.md for a list of the rule aliases.
210 | You can pass the `-a` option to display rule aliases instead of MDxxx rule
211 | IDs.
212 |
213 | ### Changed
214 |
215 | * MD003 - An additional header style, setext_with_atx, was added to require
216 | setext style headers for levels 1 and 2, but allow atx style headers for
217 | levels 3 and above (i.e. header levels that can't be expressed with setext
218 | style headers)
219 | * MD013 - You now have the option to exclude code blocks and tables from the
220 | line length limit check.
221 |
222 | ### Fixed
223 |
224 | * Crash with MD034 and pipe character [#93](https://github.com/markdownlint/markdownlint/pull/93)
225 | and [#97](https://github.com/markdownlint/markdownlint/pull/97)
226 | * MD031 failed on nested code blocks [#100](https://github.com/markdownlint/markdownlint/pull/100)
227 | and [#109](https://github.com/markdownlint/markdownlint/pull/109)
228 | * MD037 crashes on with underscores [#83](https://github.com/markdownlint/markdownlint/pull/83)
229 | * Regression introduced in v0.2.1 - ignoring rules/tags on the command line
230 | caused a crash [#108](https://github.com/markdownlint/markdownlint/pull/108)
231 | * MD027 false positive when line starts with a backtick [#105](https://github.com/markdownlint/markdownlint/pull/105)
232 |
233 | ### Merged pull requests
234 |
235 | * [Add support for nested code fences to MD031/MD032 - David
236 | Anson](https://github.com/markdownlint/markdownlint/pull/109)
237 | * [Add missing word to description of MD035 in RULES.md - David
238 | Anson](https://github.com/markdownlint/markdownlint/pull/86)
239 | * [Probe for .mdlrc in current and parent directories - Loic
240 | Nageleisen](https://github.com/markdownlint/markdownlint/pull/111)
241 | * [MD013: allow excluding code blocks and tables - Loic
242 | Nageleisen](https://github.com/markdownlint/markdownlint/pull/112)
243 |
244 | ## [v0.2.1] (2015-04-13)
245 |
246 | ### Fixed
247 |
248 | * Incorrect parsing of rules/tags specification in .mdlrc [#81](https://github.com/markdownlint/markdownlint/pull/81)
249 | * Exception on image links with MD039 [#82](https://github.com/markdownlint/markdownlint/pull/82)
250 | * MD037 flags on two words beginning with underscores on the same
251 | line [#83](https://github.com/markdownlint/markdownlint/pull/83)
252 |
253 | ### Known issues
254 |
255 | * Exception on some lines with raw html list items in them [#83](https://github.com/markdownlint/markdownlint/pull/83)
256 |
257 | ## [v0.2.0] (2015-04-13)
258 |
259 | ### Rules added
260 |
261 | * MD033 - Inline HTML
262 | * MD034 - Bare URL used
263 | * MD035 - Horizontal rule style
264 | * MD036 - Emphasis used instead of a header
265 | * MD037 - Spaces inside emphasis markers
266 | * MD038 - Spaces inside code span elements
267 | * MD039 - Spaces inside link text
268 | * MD040 - Fenced code blocks should have a language specified
269 |
270 | ## Added
271 |
272 | * Trailing spaces rule should allow an excemption for deliberate
273 | insertion.
274 | * Rules can be excluded in .mdlrc and on the command line by specifying a rule
275 | as ~MD000.
276 |
277 | ### Merged pull requests
278 |
279 | * [Add parameter (value and default) information to rule documentation. - David Anson](https://github.com/markdownlint/markdownlint/pull/76)
280 |
281 | ## [v0.1.0] (2015-02-22)
282 |
283 | ### Rules added
284 |
285 | * MD031 - Fenced code blocks should be surrounded by blank lines
286 | * MD032 - Lists should be surrounded by blank lines
287 |
288 | ### Fixed
289 |
290 | * MD014 triggers when it shouldn't
291 |
292 | ### Merged pull requests
293 |
294 | * [MD032 - Lists should be surrounded by blank lines - David Anson](https://github.com/markdownlint/markdownlint/pull/70)
295 | * [MD031 - Fenced code blocks should be surrounded by blank lines - David Anson](https://github.com/markdownlint/markdownlint/pull/68)
296 | * [Clarify how to specify your own style - mjankowski](https://github.com/markdownlint/markdownlint/pull/65)
297 | * [Use single quotes to prevent early escaping - highb](https://github.com/markdownlint/markdownlint/pull/64)
298 |
299 | ## [v0.0.1] (2014-09-07)
300 |
301 | ### Rules added
302 |
303 | * MD001 - Header levels should only increment by one level at a time
304 | * MD002 - First header should be a h1 header
305 | * MD003 - Header style
306 | * MD004 - Unordered list style
307 | * MD005 - Inconsistent indentation for list items at the same level
308 | * MD006 - Consider starting bulleted lists at the beginning of the line
309 | * MD007 - Unordered list indentation
310 | * MD009 - Trailing spaces
311 | * MD010 - Hard tabs
312 | * MD011 - Reversed link syntax
313 | * MD012 - Multiple consecutive blank lines
314 | * MD013 - Line length
315 | * MD014 - Dollar signs used before commands without showing output
316 | * MD018 - No space after hash on atx style header
317 | * MD019 - Multiple spaces after hash on atx style header
318 | * MD020 - No space inside hashes on closed atx style header
319 | * MD021 - Multiple spaces inside hashes on closed atx style header
320 | * MD022 - Headers should be surrounded by blank lines
321 | * MD023 - Headers must start at the beginning of the line
322 | * MD024 - Multiple headers with the same content
323 | * MD025 - Multiple top level headers in the same document
324 | * MD026 - Trailing punctuation in header
325 | * MD027 - Multiple spaces after blockquote symbol
326 | * MD028 - Blank line inside blockquote
327 | * MD029 - Ordered list item prefix
328 | * MD030 - Spaces after list markers
329 |
330 | [Unreleased]: https://github.com/markdownlint/markdownlint/tree/main
331 | [v0.13.0]: https://github.com/markdownlint/markdownlint/tree/v0.13.0
332 | [v0.12.0]: https://github.com/markdownlint/markdownlint/tree/v0.12.0
333 | [v0.11.0]: https://github.com/markdownlint/markdownlint/tree/v0.11.0
334 | [v0.10.0]: https://github.com/markdownlint/markdownlint/tree/v0.10.0
335 | [v0.9.0]: https://github.com/markdownlint/markdownlint/tree/v0.9.0
336 | [v0.8.0]: https://github.com/markdownlint/markdownlint/tree/v0.8.0
337 | [v0.7.0]: https://github.com/markdownlint/markdownlint/tree/v0.7.0
338 | [v0.6.0]: https://github.com/markdownlint/markdownlint/tree/v0.6.0
339 | [v0.5.0]: https://github.com/markdownlint/markdownlint/tree/v0.5.0
340 | [v0.4.0]: https://github.com/markdownlint/markdownlint/tree/v0.4.0
341 | [v0.3.1]: https://github.com/markdownlint/markdownlint/tree/v0.3.1
342 | [v0.3.0]: https://github.com/markdownlint/markdownlint/tree/v0.3.0
343 | [v0.2.1]: https://github.com/markdownlint/markdownlint/tree/v0.2.1
344 | [v0.2.0]: https://github.com/markdownlint/markdownlint/tree/v0.2.0
345 | [v0.1.0]: https://github.com/markdownlint/markdownlint/tree/v0.1.0
346 | [v0.0.1]: https://github.com/markdownlint/markdownlint/tree/v0.0.1
347 |
--------------------------------------------------------------------------------
/test/test_cli.rb:
--------------------------------------------------------------------------------
1 | require_relative 'setup_tests'
2 | require 'open3'
3 | require 'set'
4 | require 'erb'
5 | require 'fileutils'
6 | require 'json'
7 |
8 | class TestCli < Minitest::Test
9 | def test_help_text
10 | result = run_cli('--help')
11 | assert_match(/Usage: \S+ \[options\]/, result[:stdout])
12 | assert_equal(0, result[:status])
13 | end
14 |
15 | def test_json_output
16 | result = run_cli_with_input('-j', "# header\n")
17 | assert_ran_ok(result)
18 | expected = File.read('test/fixtures/output/json/without_matches.json')
19 | assert_equal(expected, result[:stdout])
20 | end
21 |
22 | def test_json_output_with_matches
23 | result = run_cli_with_input('-j -r MD002', "## header2\n")
24 | assert_equal(1, result[:status])
25 | assert_equal('', result[:stderr])
26 | expected = File.read('test/fixtures/output/json/with_matches.json')
27 | assert_equal(expected, result[:stdout])
28 | end
29 |
30 | def test_sarif_output
31 | result = run_cli_with_input('-S', "# header\n")
32 | assert_ran_ok(result)
33 | sarif_file = File.read('test/fixtures/output/sarif/without_matches.sarif')
34 | expected = ERB.new(sarif_file).result(binding)
35 | assert_equal(expected, result[:stdout])
36 | end
37 |
38 | def test_sarif_output_with_matches
39 | result = run_cli_with_input('-S -r MD002', "## header2\n")
40 | assert_equal(1, result[:status])
41 | assert_equal('', result[:stderr])
42 | sarif_file = File.read('test/fixtures/output/sarif/with_matches.sarif')
43 | expected = ERB.new(sarif_file).result(binding)
44 | assert_equal(expected, result[:stdout])
45 | end
46 |
47 | def test_default_ruleset_loading
48 | result = run_cli('-l')
49 | assert_ran_ok(result)
50 | assert_rules_enabled(result, ['MD001'])
51 | end
52 |
53 | def test_show_alias_rule_list
54 | result = run_cli('-al')
55 | assert_ran_ok(result)
56 | assert_rules_enabled(result, ['header-increment'])
57 | end
58 |
59 | def test_show_alias_processing_file
60 | result = run_cli_with_input('-a -r MD002', "## header2\n")
61 | assert_equal(1, result[:status])
62 | assert_equal('', result[:stderr])
63 | assert_match(/^\(stdin\):1: first-header-h1/, result[:stdout])
64 | end
65 |
66 | def test_running_on_unicode_input
67 | result = run_cli_with_file_and_ascii_env("## header2 🚀\n")
68 | assert_equal(1, result[:status])
69 | assert_equal('', result[:stderr])
70 | assert_match(/MD002 First header should be a top level header/,
71 | result[:stdout])
72 | end
73 |
74 | def test_skipping_default_ruleset_loading
75 | result = run_cli('-ld')
76 | assert_rules_enabled(result, [], true)
77 | end
78 |
79 | def test_custom_ruleset_loading
80 | my_ruleset = File.expand_path('fixtures/my_ruleset.rb', __dir__)
81 | result = run_cli("-ldu #{my_ruleset}")
82 | assert_rules_enabled(result, ['MY001'], true)
83 | assert_ran_ok(result)
84 | end
85 |
86 | def test_show_alias_rule_without_alias
87 | # Tests that when -a is given, but the rule doesn't have an alias, it
88 | # prints the rule ID instead.
89 | my_ruleset = File.expand_path('fixtures/my_ruleset.rb', __dir__)
90 | result = run_cli("-ladu #{my_ruleset}")
91 | assert_rules_enabled(result, ['MY001'], true)
92 | assert_ran_ok(result)
93 | end
94 |
95 | def test_custom_ruleset_processing_success
96 | my_ruleset = File.expand_path('fixtures/my_ruleset.rb', __dir__)
97 | result = run_cli_with_input("-du #{my_ruleset}", 'Hello World')
98 | assert_equal('', result[:stdout])
99 | assert_ran_ok(result)
100 | end
101 |
102 | def test_custom_ruleset_processing_failure
103 | my_ruleset = File.expand_path('fixtures/my_ruleset.rb', __dir__)
104 | result = run_cli_with_input("-du #{my_ruleset}", 'Goodbye world')
105 | assert_equal(1, result[:status])
106 | assert_match(/^\(stdin\):1: MY001/, result[:stdout])
107 | assert_equal('', result[:stderr])
108 | end
109 |
110 | def test_custom_ruleset_processing_failure_with_show_alias
111 | # The custom rule doesn't have an alias, so the output should be identical
112 | # to that without show_alias enabled.
113 | my_ruleset = File.expand_path('fixtures/my_ruleset.rb', __dir__)
114 | result = run_cli_with_input("-dau #{my_ruleset}", 'Goodbye world')
115 | assert_equal(1, result[:status])
116 | assert_match(/^\(stdin\):1: MY001/, result[:stdout])
117 | assert_equal('', result[:stderr])
118 | end
119 |
120 | def test_custom_ruleset_loading_with_default
121 | my_ruleset = File.expand_path('fixtures/my_ruleset.rb', __dir__)
122 | result = run_cli("-lu #{my_ruleset}")
123 | assert_rules_enabled(result, %w{MD001 MY001})
124 | assert_ran_ok(result)
125 | end
126 |
127 | def test_rule_inclusion_cli
128 | result = run_cli('-r MD001 -l')
129 | assert_rules_enabled(result, ['MD001'], true)
130 | assert_ran_ok(result)
131 | end
132 |
133 | def test_rule_exclusion_cli
134 | result = run_cli('-r ~MD001 -l')
135 | assert_rules_disabled(result, ['MD001'])
136 | assert_ran_ok(result)
137 | end
138 |
139 | def test_rule_inclusion_with_exclusion_cli
140 | result = run_cli('-r ~MD001,MD039 -l')
141 | assert_rules_enabled(result, ['MD039'], true)
142 | assert_ran_ok(result)
143 | end
144 |
145 | def test_tag_inclusion_cli
146 | result = run_cli('-t headers -l')
147 | assert_rules_enabled(result, %w{MD001 MD002 MD003})
148 | assert_rules_disabled(result, %w{MD004 MD005 MD006})
149 | assert_ran_ok(result)
150 | end
151 |
152 | def test_tag_exclusion_cli
153 | result = run_cli('-t ~headers -l')
154 | assert_ran_ok(result)
155 | assert_rules_disabled(result, %w{MD001 MD002 MD003})
156 | assert_rules_enabled(result, %w{MD004 MD005 MD006})
157 | end
158 |
159 | def test_rule_inclusion_config
160 | result = run_cli_with_custom_rc_file('-l', 'mdlrc_enable_rules')
161 | assert_ran_ok(result)
162 | assert_rules_enabled(result, %w{MD001 MD002}, true)
163 | end
164 |
165 | def test_rule_exclusion_config
166 | result = run_cli_with_custom_rc_file('-l', 'mdlrc_disable_rules')
167 | assert_correctly_disabled(result)
168 | end
169 |
170 | def test_mdlrc_loading_from_current_dir_by_default
171 | inside_tmp_dir do |dir|
172 | with_mdlrc('mdlrc_disable_rules', dir) do
173 | result = run_cli_without_rc_flag('-l')
174 | assert_correctly_disabled(result)
175 | end
176 | end
177 | end
178 |
179 | def test_mdlrc_loading_ascends_until_it_finds_an_rc_file
180 | Dir.mktmpdir do |parent_dir|
181 | inside_tmp_dir(parent_dir) do
182 | with_mdlrc('mdlrc_disable_rules', parent_dir) do
183 | result = run_cli_without_rc_flag('-l')
184 | assert_correctly_disabled(result)
185 | end
186 | end
187 | end
188 | end
189 |
190 | def test_tag_inclusion_config
191 | result = run_cli_with_custom_rc_file('-l', 'mdlrc_enable_tags')
192 | assert_ran_ok(result)
193 | assert_rules_enabled(result, %w{MD001 MD002 MD009 MD010})
194 | assert_rules_disabled(result, %w{MD004 MD005})
195 | end
196 |
197 | def test_tag_exclusion_config
198 | result = run_cli_with_custom_rc_file('-l', 'mdlrc_disable_tags')
199 | assert_ran_ok(result)
200 | assert_rules_enabled(result, %w{MD004 MD030 MD032})
201 | assert_rules_disabled(result, %w{MD001 MD005})
202 | end
203 |
204 | def test_rule_inclusion_alias_cli
205 | result = run_cli('-l -r header-increment')
206 | assert_ran_ok(result)
207 | assert_rules_enabled(result, ['MD001'], true)
208 | end
209 |
210 | def test_rule_exclusion_alias_cli
211 | result = run_cli('-l -r ~header-increment')
212 | assert_ran_ok(result)
213 | assert_rules_disabled(result, ['MD001'])
214 | assert_rules_enabled(result, ['MD002'])
215 | end
216 |
217 | def test_directory_scanning
218 | path = File.expand_path(
219 | './fixtures/dir_with_md_and_markdown',
220 | File.dirname(__FILE__),
221 | )
222 | result = run_cli(path.to_s)
223 | lines_output = result[:stdout].lines
224 | interested_lines = lines_output[0...lines_output.index("\n")]
225 | files_with_issues = interested_lines.map { |l| l.split(':')[0] }.sort
226 | assert_equal(files_with_issues, ["#{path}/bar.markdown", "#{path}/foo.md"])
227 | end
228 |
229 | def test_ignore_front_matter
230 | path = File.expand_path('./fixtures/front_matter', File.dirname(__FILE__))
231 | files = ['jekyll_post.md', 'jekyll_post_2.md'].map do |f|
232 | File.join(path, f)
233 | end.join(' ')
234 | result = run_cli("-i -r MD001,MD041,MD034 #{files}")
235 |
236 | expected = <<~OUTPUT
237 | #{path}/jekyll_post.md:16: MD001 Header levels should only increment by one level at a time
238 | #{path}/jekyll_post_2.md:16: MD001 Header levels should only increment by one level at a time
239 |
240 | Further documentation is available for these failures:
241 | - MD001: https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md#md001---header-levels-should-only-increment-by-one-level-at-a-time
242 | OUTPUT
243 |
244 | assert_equal(result[:stdout], expected)
245 | end
246 |
247 | def test_unprintable_chars
248 | path = File.expand_path(
249 | './fixtures/unprintable_chars',
250 | File.dirname(__FILE__),
251 | )
252 | files = (1..3).map do |i|
253 | File.join(path, "test#{i}")
254 | end.join(' ')
255 | result = run_cli(files)
256 |
257 | assert_equal(result[:stdout], '')
258 | end
259 |
260 | def test_printing_url_links_in_tty
261 | result = run_cli_as_tty('-r MD002', "## header2\n")
262 |
263 | link_text = 'MD002'
264 | url = 'https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md#md002---first-header-should-be-a-top-level-header'
265 | expected_link = "\e]8;;#{url}\e\\#{link_text}\e]8;;\e\\"
266 |
267 | expected = <<~OUTPUT
268 | (stdin):1: #{expected_link} First header should be a top level header
269 | OUTPUT
270 |
271 | assert_equal(result[:stdout], expected)
272 | end
273 |
274 | def test_printing_url_links_outside_tty
275 | result = run_cli_with_input('-r MD002', "## header2\n")
276 |
277 | url = 'https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md#md002---first-header-should-be-a-top-level-header'
278 |
279 | expected = <<~OUTPUT
280 | (stdin):1: MD002 First header should be a top level header
281 |
282 | Further documentation is available for these failures:
283 | - MD002: #{url}
284 | OUTPUT
285 |
286 | assert_equal(result[:stdout], expected)
287 | end
288 |
289 | def test_docs_declaration
290 | ruleset1 = File.expand_path('./fixtures/docs_ruleset_1.rb', __dir__)
291 | ruleset2 = File.expand_path('./fixtures/docs_ruleset_2.rb', __dir__)
292 |
293 | result = run_cli_with_input("-d -u #{ruleset1},#{ruleset2}", "!\n")
294 |
295 | expected = <<~OUTPUT
296 | (stdin):1: MY002 Documents must start with A
297 | (stdin):1: MY003 Documents must start with B
298 | (stdin):1: MY004 Documents must start with C
299 | (stdin):1: MY005 Documents must start with D
300 | (stdin):1: MY006 Documents must start with E
301 | (stdin):1: MY007 Documents must start with F
302 |
303 | Further documentation is available for these failures:
304 | - MY002: https://example.com/static-docs
305 | - MY003: https://example.com/override-docs
306 | - MY004: https://example.com/MY004#364fe83d86ba1cdcc2ea87aec2fc5ec0
307 | - MY005: https://example.com/override-docs
308 | - MY006: https://example.com/later-declaration
309 | - MY007: https://example.com/dynamic-override/MY007#documents-must-start-with-f
310 | OUTPUT
311 |
312 | assert_equal(expected, result[:stdout])
313 | end
314 |
315 | def test_graceful_not_found
316 | file_path = 'a/file/that/does/not/exist.md'
317 | dir_path = 'a/folder/that/does/not/exist'
318 |
319 | file_result = run_cli(file_path)
320 | dir_result = run_cli(dir_path)
321 |
322 | file_expected = "Errno::ENOENT: No such file or directory - #{file_path}\n"
323 | dir_expected = "Errno::ENOENT: No such file or directory - #{dir_path}\n"
324 |
325 | # No stdout
326 | assert_equal('', file_result[:stdout])
327 | assert_equal('', dir_result[:stdout])
328 |
329 | # Statuses are 3
330 | assert_equal(3, file_result[:status])
331 | assert_equal(3, dir_result[:status])
332 |
333 | # Check error message is expected
334 | assert_equal(file_expected, file_result[:stderr])
335 | assert_equal(dir_expected, dir_result[:stderr])
336 | end
337 |
338 | private
339 |
340 | def run_cli_with_input(args, stdin)
341 | run_cmd("#{mdl_script} -c #{default_rc_file} #{args}", stdin)
342 | end
343 |
344 | def run_cli_as_tty(args, stdin)
345 | fake_tty = File.expand_path('./fixtures/fake_tty.rb', __dir__)
346 | run_cmd("#{mdl_script} -c #{default_rc_file} -u #{fake_tty} #{args}", stdin)
347 | end
348 |
349 | def run_cli_without_rc_flag(args)
350 | run_cmd("#{mdl_script} #{args}", '')
351 | end
352 |
353 | def run_cli(args)
354 | run_cmd("#{mdl_script} -c #{default_rc_file} #{args}", '')
355 | end
356 |
357 | def run_cli_with_custom_rc_file(args, filename)
358 | run_cmd("#{mdl_script} -c #{fixture_rc(filename)} #{args}", '')
359 | end
360 |
361 | def run_cli_with_file_and_ascii_env(content)
362 | Tempfile.create('foo') do |f|
363 | f.write(content)
364 | f.close
365 |
366 | run_cmd("ruby -E ASCII #{mdl_script} -c #{default_rc_file} #{f.path}", '')
367 | end
368 | end
369 |
370 | def run_cmd(command, stdin)
371 | result = {}
372 | result[:stdout], result[:stderr], result[:status] = \
373 | Open3.capture3('bundle', 'exec', *command.split, :stdin_data => stdin)
374 | result[:status] = result[:status].exitstatus
375 | result
376 | end
377 |
378 | def mdl_script
379 | File.expand_path('../bin/mdl', __dir__)
380 | end
381 |
382 | def fixture_rc(filename)
383 | File.expand_path("../fixtures/#{filename}", __FILE__)
384 | end
385 |
386 | def default_rc_file
387 | fixture_rc('default_mdlrc')
388 | end
389 |
390 | def inside_tmp_dir(base_dir = Dir.tmpdir)
391 | Dir.mktmpdir(nil, base_dir) do |dir|
392 | Dir.chdir(dir) { yield(dir) }
393 | end
394 | end
395 |
396 | def assert_rules_enabled(result, rules, only_these_rules = false)
397 | # Asserts that the given rules are enabled given the output of mdl -l
398 | # If only_these_rules is set, then it asserts that the given rules and no
399 | # others are enabled.
400 | lines = result[:stdout].split("\n")
401 | assert_equal('Enabled rules:', lines.first)
402 | lines.shift
403 | rules = rules.to_set
404 | enabled_rules = lines.map { |l| l.split.first }.to_set
405 | if only_these_rules
406 | assert_equal(rules, enabled_rules)
407 | else
408 | assert_equal(Set.new, rules - enabled_rules)
409 | end
410 | end
411 |
412 | def assert_rules_disabled(result, rules)
413 | # Asserts that the given rules are _not_ enabled given the output of mdl -l
414 | lines = result[:stdout].split("\n")
415 | assert_equal('Enabled rules:', lines.first)
416 | lines.shift
417 | rules = rules.to_set
418 | enabled_rules = lines.map { |l| l.split.first }.to_set
419 | assert_equal(Set.new, rules & enabled_rules)
420 | end
421 |
422 | def assert_ran_ok(result)
423 | assert_equal(0, result[:status])
424 | assert_equal('', result[:stderr])
425 | end
426 |
427 | def assert_correctly_disabled(result)
428 | assert_ran_ok(result)
429 | assert_rules_disabled(result, %w{MD001 MD002})
430 | assert_rules_enabled(result, %w{MD003 MD004})
431 | end
432 |
433 | def with_mdlrc(filename, dest_dir = Dir.pwd)
434 | rc_path = File.join(dest_dir, '.mdlrc')
435 | FileUtils.cp(fixture_rc(filename), rc_path)
436 | yield
437 | ensure
438 | File.delete(rc_path)
439 | end
440 | end
441 |
--------------------------------------------------------------------------------
/lib/mdl/rules.rb:
--------------------------------------------------------------------------------
1 | docs do |id, description|
2 | url_hash = [id.downcase,
3 | description.downcase.gsub(/[^a-z]+/, '-')].join('---')
4 | "https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md##{url_hash}"
5 | end
6 |
7 | rule 'MD001', 'Header levels should only increment by one level at a time' do
8 | tags :headers
9 | aliases 'header-increment'
10 | check do |doc|
11 | headers = doc.find_type(:header)
12 | old_level = nil
13 | errors = []
14 | headers.each do |h|
15 | errors << h[:location] if old_level && (h[:level] > old_level + 1)
16 | old_level = h[:level]
17 | end
18 | errors
19 | end
20 | end
21 |
22 | rule 'MD002', 'First header should be a top level header' do
23 | tags :headers
24 | aliases 'first-header-h1'
25 | params :level => 1
26 | check do |doc|
27 | first_header = doc.find_type(:header).first
28 | if first_header && (first_header[:level] != @params[:level])
29 | [first_header[:location]]
30 | end
31 | end
32 | end
33 |
34 | rule 'MD003', 'Header style' do
35 | # Header styles are things like ### and adding underscores
36 | # See https://daringfireball.net/projects/markdown/syntax#header
37 | tags :headers
38 | aliases 'header-style'
39 | # :style can be one of :consistent, :atx, :atx_closed, :setext
40 | params :style => :consistent
41 | check do |doc|
42 | headers = doc.find_type_elements(:header, false)
43 | if headers.empty?
44 | nil
45 | else
46 | doc_style = if @params[:style] == :consistent
47 | doc.header_style(headers.first)
48 | else
49 | @params[:style]
50 | end
51 | if doc_style == :setext_with_atx
52 | headers.map do |h|
53 | doc.element_linenumber(h) \
54 | unless (doc.header_style(h) == :setext) || \
55 | ((doc.header_style(h) == :atx) && \
56 | (h.options[:level] > 2))
57 | end.compact
58 | else
59 | headers.map do |h|
60 | doc.element_linenumber(h) \
61 | if doc.header_style(h) != doc_style
62 | end.compact
63 | end
64 | end
65 | end
66 | end
67 |
68 | rule 'MD004', 'Unordered list style' do
69 | tags :bullet, :ul
70 | aliases 'ul-style'
71 | # :style can be one of :consistent, :asterisk, :plus, :dash, :sublist
72 | params :style => :consistent
73 | check do |doc|
74 | bullets = doc.find_type_elements(:ul).map do |l|
75 | doc.find_type_elements(:li, false, l.children)
76 | end.flatten
77 | if bullets.empty?
78 | nil
79 | else
80 | doc_style = case @params[:style]
81 | when :consistent
82 | doc.list_style(bullets.first)
83 | when :sublist
84 | {}
85 | else
86 | @params[:style]
87 | end
88 | results = []
89 | bullets.each do |b|
90 | if @params[:style] == :sublist
91 | level = b.options[:element_level]
92 | if doc_style[level]
93 | if doc_style[level] != doc.list_style(b)
94 | results << doc.element_linenumber(b)
95 | end
96 | else
97 | doc_style[level] = doc.list_style(b)
98 | end
99 | elsif doc.list_style(b) != doc_style
100 | results << doc.element_linenumber(b)
101 | end
102 | end
103 | results.compact
104 | end
105 | end
106 | end
107 |
108 | rule 'MD005', 'Inconsistent indentation for list items at the same level' do
109 | tags :bullet, :ul, :indentation
110 | aliases 'list-indent'
111 | check do |doc|
112 | bullets = doc.find_type(:li)
113 | errors = []
114 | indent_levels = []
115 | bullets.each do |b|
116 | indent_level = doc.indent_for(doc.element_line(b))
117 | if indent_levels[b[:element_level]].nil?
118 | indent_levels[b[:element_level]] = indent_level
119 | end
120 | if indent_level != indent_levels[b[:element_level]]
121 | errors << doc.element_linenumber(b)
122 | end
123 | end
124 | errors
125 | end
126 | end
127 |
128 | rule 'MD006', 'Consider starting bulleted lists at the beginning of the line' do
129 | # Starting at the beginning of the line means that indentation for each
130 | # bullet level can be identical.
131 | tags :bullet, :ul, :indentation
132 | aliases 'ul-start-left'
133 | check do |doc|
134 | doc.find_type(:ul, false).reject do |e|
135 | doc.indent_for(doc.element_line(e)) == 0
136 | end.map { |e| e[:location] }
137 | end
138 | end
139 |
140 | rule 'MD007', 'Unordered list indentation' do
141 | tags :bullet, :ul, :indentation
142 | aliases 'ul-indent'
143 | # Do not default to < 3, see PR#373 or the comments in RULES.md
144 | params :indent => 3
145 | check do |doc|
146 | errors = []
147 | indents = doc.find_type(:ul).map do |e|
148 | [doc.indent_for(doc.element_line(e)), doc.element_linenumber(e)]
149 | end
150 | curr_indent = indents[0][0] unless indents.empty?
151 | indents.each do |indent, linenum|
152 | if (indent > curr_indent) && (indent - curr_indent != @params[:indent])
153 | errors << linenum
154 | end
155 | curr_indent = indent
156 | end
157 | errors
158 | end
159 | end
160 |
161 | rule 'MD009', 'Trailing spaces' do
162 | tags :whitespace
163 | aliases 'no-trailing-spaces'
164 | params :br_spaces => 2
165 | check do |doc|
166 | errors = doc.matching_lines(/\s$/)
167 | if params[:br_spaces] > 1
168 | errors -= doc.matching_lines(/\S\s{#{params[:br_spaces]}}$/)
169 | end
170 | errors
171 | end
172 | end
173 |
174 | rule 'MD010', 'Hard tabs' do
175 | tags :whitespace, :hard_tab
176 | aliases 'no-hard-tabs'
177 | params :ignore_code_blocks => false
178 | check do |doc|
179 | # Every line in the document that is part of a code block. Blank lines
180 | # inside of a code block are acceptable.
181 | codeblock_lines = doc.find_type_elements(:codeblock).map do |e|
182 | (doc.element_linenumber(e)..
183 | doc.element_linenumber(e) + e.value.lines.count).to_a
184 | end.flatten
185 |
186 | # Check for lines with hard tab
187 | hard_tab_lines = doc.matching_lines(/\t/)
188 | # Remove lines with hard tabs, if they stem from codeblock
189 | hard_tab_lines -= codeblock_lines if params[:ignore_code_blocks]
190 | hard_tab_lines
191 | end
192 | end
193 |
194 | rule 'MD011', 'Reversed link syntax' do
195 | tags :links
196 | aliases 'no-reversed-links'
197 | check do |doc|
198 | doc.matching_text_element_lines(/\([^)]+\)\[[^\]]+\]/)
199 | end
200 | end
201 |
202 | rule 'MD012', 'Multiple consecutive blank lines' do
203 | tags :whitespace, :blank_lines
204 | aliases 'no-multiple-blanks'
205 | check do |doc|
206 | # Every line in the document that is part of a code block. Blank lines
207 | # inside of a code block are acceptable.
208 | codeblock_lines = doc.find_type_elements(:codeblock).map do |e|
209 | (doc.element_linenumber(e)..
210 | doc.element_linenumber(e) + e.value.lines.count).to_a
211 | end.flatten
212 | blank_lines = doc.matching_lines(/^\s*$/)
213 | cons_blank_lines = blank_lines.each_cons(2).select do |p, n|
214 | n - p == 1
215 | end.map { |_p, n| n }
216 | cons_blank_lines - codeblock_lines
217 | end
218 | end
219 |
220 | rule 'MD013', 'Line length' do
221 | tags :line_length
222 | aliases 'line-length'
223 | params :line_length => 80, :ignore_code_blocks => false, :code_blocks => true,
224 | :tables => true
225 |
226 | check do |doc|
227 | # Every line in the document that is part of a code block.
228 | codeblock_lines = doc.find_type_elements(:codeblock).map do |e|
229 | (doc.element_linenumber(e)..
230 | doc.element_linenumber(e) + e.value.lines.count).to_a
231 | end.flatten
232 | # Every line in the document that is part of a table.
233 | locations = doc.elements
234 | .map { |e| [e.options[:location], e] }
235 | .reject { |l, _| l.nil? }
236 | table_lines = locations.map.with_index do |(l, e), i|
237 | if e.type == :table
238 | if i + 1 < locations.size
239 | (l..locations[i + 1].first - 1).to_a
240 | else
241 | (l..doc.lines.count).to_a
242 | end
243 | end
244 | end.flatten
245 | overlines = doc.matching_lines(/^.{#{@params[:line_length]}}.*\s/)
246 | if !params[:code_blocks] || params[:ignore_code_blocks]
247 | overlines -= codeblock_lines
248 | unless params[:code_blocks]
249 | warn 'MD013 warning: Parameter :code_blocks is deprecated.'
250 | warn ' Please replace \":code_blocks => false\" by '\
251 | '\":ignore_code_blocks => true\" in your configuration.'
252 | end
253 | end
254 | overlines -= table_lines unless params[:tables]
255 | overlines
256 | end
257 | end
258 |
259 | rule 'MD014', 'Dollar signs used before commands without showing output' do
260 | tags :code
261 | aliases 'commands-show-output'
262 | check do |doc|
263 | doc.find_type_elements(:codeblock).select do |e|
264 | !e.value.empty? &&
265 | !e.value.split(/\n+/).map { |l| l.match(/^\$\s/) }.include?(nil)
266 | end.map { |e| doc.element_linenumber(e) }
267 | end
268 | end
269 |
270 | rule 'MD018', 'No space after hash on atx style header' do
271 | tags :headers, :atx, :spaces
272 | aliases 'no-missing-space-atx'
273 | check do |doc|
274 | doc.find_type_elements(:header).select do |h|
275 | doc.header_style(h) == :atx && doc.element_line(h).match(/^#+[^#\s]/)
276 | end.map { |h| doc.element_linenumber(h) }
277 | end
278 | end
279 |
280 | rule 'MD019', 'Multiple spaces after hash on atx style header' do
281 | tags :headers, :atx, :spaces
282 | aliases 'no-multiple-space-atx'
283 | check do |doc|
284 | doc.find_type_elements(:header).select do |h|
285 | doc.header_style(h) == :atx && doc.element_line(h).match(/^#+\s\s/)
286 | end.map { |h| doc.element_linenumber(h) }
287 | end
288 | end
289 |
290 | rule 'MD020', 'No space inside hashes on closed atx style header' do
291 | tags :headers, :atx_closed, :spaces
292 | aliases 'no-missing-space-closed-atx'
293 | check do |doc|
294 | doc.find_type_elements(:header).select do |h|
295 | doc.header_style(h) == :atx_closed \
296 | && (doc.element_line(h).match(/^#+[^#\s]/) \
297 | || doc.element_line(h).match(/[^#\s\\]#+$/))
298 | end.map { |h| doc.element_linenumber(h) }
299 | end
300 | end
301 |
302 | rule 'MD021', 'Multiple spaces inside hashes on closed atx style header' do
303 | tags :headers, :atx_closed, :spaces
304 | aliases 'no-multiple-space-closed-atx'
305 | check do |doc|
306 | doc.find_type_elements(:header).select do |h|
307 | doc.header_style(h) == :atx_closed \
308 | && (doc.element_line(h).match(/^#+\s\s/) \
309 | || doc.element_line(h).match(/\s\s#+$/))
310 | end.map { |h| doc.element_linenumber(h) }
311 | end
312 | end
313 |
314 | rule 'MD022', 'Headers should be surrounded by blank lines' do
315 | tags :headers, :blank_lines
316 | aliases 'blanks-around-headers'
317 | check do |doc|
318 | errors = []
319 | doc.find_type_elements(:header, false).each do |h|
320 | header_bad = false
321 | linenum = doc.element_linenumber(h)
322 | # Check previous line
323 | header_bad = true if (linenum > 1) && !doc.lines[linenum - 2].empty?
324 | # Check next line
325 | next_line_idx = doc.header_style(h) == :setext ? linenum + 1 : linenum
326 | next_line = doc.lines[next_line_idx]
327 | header_bad = true if !next_line.nil? && !next_line.empty?
328 | errors << linenum if header_bad
329 | end
330 | # Kramdown requires that headers start on a block boundary, so in most
331 | # cases it won't pick up a header without a blank line before it. We need
332 | # to check regular text and pick out headers ourselves too
333 | doc.find_type_elements(:p, false).each do |p|
334 | linenum = doc.element_linenumber(p)
335 | text = p.children.select { |e| e.type == :text }.map(&:value).join
336 | lines = text.split("\n")
337 | prev_lines = ['', '']
338 | lines.each do |line|
339 | # First look for ATX style headers without blank lines before
340 | errors << linenum if line.match(/^\#{1,6}/) && !prev_lines[1].empty?
341 | # Next, look for setext style
342 | if line.match(/^(-+|=+)\s*$/) && !prev_lines[0].empty?
343 | errors << (linenum - 1)
344 | end
345 | linenum += 1
346 | prev_lines << line
347 | prev_lines.shift
348 | end
349 | end
350 | errors.sort
351 | end
352 | end
353 |
354 | rule 'MD023', 'Headers must start at the beginning of the line' do
355 | tags :headers, :spaces
356 | aliases 'header-start-left'
357 | check do |doc|
358 | errors = []
359 | # The only type of header with spaces actually parsed as such is setext
360 | # style where only the text is indented. We check for that first.
361 | doc.find_type_elements(:header, false).each do |h|
362 | errors << doc.element_linenumber(h) if doc.element_line(h).match(/^\s/)
363 | end
364 | # Next we have to look for things that aren't parsed as headers because
365 | # they start with spaces.
366 | doc.find_type_elements(:p, false).each do |p|
367 | linenum = doc.element_linenumber(p)
368 | lines = doc.extract_text(p)
369 | prev_line = ''
370 | lines.each do |line|
371 | # First look for ATX style headers
372 | errors << linenum if line.match(/^\s+\#{1,6}/)
373 | # Next, look for setext style
374 | if line.match(/^\s+(-+|=+)\s*$/) && !prev_line.empty?
375 | errors << (linenum - 1)
376 | end
377 | linenum += 1
378 | prev_line = line
379 | end
380 | end
381 | errors.sort
382 | end
383 | end
384 |
385 | rule 'MD024', 'Multiple headers with the same content' do
386 | tags :headers
387 | aliases 'no-duplicate-header'
388 | params :allow_different_nesting => false
389 | check do |doc|
390 | headers = doc.find_type(:header)
391 | allow_different_nesting = params[:allow_different_nesting]
392 |
393 | duplicates = headers.select do |h|
394 | headers.any? do |e|
395 | e[:location] < h[:location] &&
396 | e[:raw_text] == h[:raw_text] &&
397 | (allow_different_nesting == false || e[:level] != h[:level])
398 | end
399 | end.to_set
400 |
401 | if allow_different_nesting
402 | same_nesting_duplicates = Set.new
403 | stack = []
404 | current_level = 0
405 | doc.find_type(:header).each do |header|
406 | level = header[:level]
407 | text = header[:raw_text]
408 |
409 | if current_level > level
410 | stack.pop
411 | elsif current_level < level
412 | stack.push([text])
413 | elsif stack.last.include?(text)
414 | same_nesting_duplicates.add(header)
415 | end
416 |
417 | current_level = level
418 | end
419 |
420 | duplicates += same_nesting_duplicates
421 | end
422 |
423 | duplicates.map { |h| doc.element_linenumber(h) }
424 | end
425 | end
426 |
427 | rule 'MD025', 'Multiple top level headers in the same document' do
428 | tags :headers
429 | aliases 'single-h1'
430 | params :level => 1
431 | check do |doc|
432 | headers = doc.find_type(:header, false).select do |h|
433 | h[:level] == params[:level]
434 | end
435 | if !headers.empty? && (doc.element_linenumber(headers[0]) == 1)
436 | headers[1..].map { |h| doc.element_linenumber(h) }
437 | end
438 | end
439 | end
440 |
441 | rule 'MD026', 'Trailing punctuation in header' do
442 | tags :headers
443 | aliases 'no-trailing-punctuation'
444 | params :punctuation => '.,;:!?'
445 | check do |doc|
446 | doc.find_type(:header).select do |h|
447 | h[:raw_text].match(/[#{params[:punctuation]}]$/)
448 | end.map do |h|
449 | doc.element_linenumber(h)
450 | end
451 | end
452 | end
453 |
454 | rule 'MD027', 'Multiple spaces after blockquote symbol' do
455 | tags :blockquote, :whitespace, :indentation
456 | aliases 'no-multiple-space-blockquote'
457 | check do |doc|
458 | errors = []
459 | doc.find_type_elements(:blockquote).each do |e|
460 | linenum = doc.element_linenumber(e)
461 | lines = doc.extract_as_text(e)
462 | # Handle first line specially as whitespace is stripped from the text
463 | # element
464 | errors << linenum if doc.element_line(e).match(/^\s*> /)
465 | lines.each do |line|
466 | errors << linenum if line.start_with?(' ')
467 | linenum += 1
468 | end
469 | end
470 | errors
471 | end
472 | end
473 |
474 | rule 'MD028', 'Blank line inside blockquote' do
475 | tags :blockquote, :whitespace
476 | aliases 'no-blanks-blockquote'
477 | check do |doc|
478 | def check_blockquote(errors, elements)
479 | prev = [nil, nil, nil]
480 | elements.each do |e|
481 | prev.shift
482 | prev << e.type
483 | if prev == %i{blockquote blank blockquote}
484 | # The current location is the start of the second blockquote, so the
485 | # line before will be a blank line in between the two, or at least the
486 | # lowest blank line if there are more than one.
487 | errors << (e.options[:location] - 1)
488 | end
489 | check_blockquote(errors, e.children)
490 | end
491 | end
492 | errors = []
493 | check_blockquote(errors, doc.elements)
494 | errors
495 | end
496 | end
497 |
498 | rule 'MD029', 'Ordered list item prefix' do
499 | tags :ol
500 | aliases 'ol-prefix'
501 | # Style can be :one or :ordered
502 | params :style => :one
503 | check do |doc|
504 | case params[:style]
505 | when :ordered
506 | doc.find_type_elements(:ol).map do |l|
507 | doc.find_type_elements(:li, false, l.children)
508 | .map.with_index do |i, idx|
509 | unless doc.element_line(i).strip.start_with?("#{idx + 1}. ")
510 | doc.element_linenumber(i)
511 | end
512 | end
513 | end.flatten.compact
514 | when :one
515 | doc.find_type_elements(:ol).map do |l|
516 | doc.find_type_elements(:li, false, l.children)
517 | end.flatten.map do |i|
518 | unless doc.element_line(i).strip.start_with?('1. ')
519 | doc.element_linenumber(i)
520 | end
521 | end.compact
522 | end
523 | end
524 | end
525 |
526 | rule 'MD030', 'Spaces after list markers' do
527 | tags :ol, :ul, :whitespace
528 | aliases 'list-marker-space'
529 | params :ul_single => 1, :ol_single => 1, :ul_multi => 1, :ol_multi => 1
530 | check do |doc|
531 | errors = []
532 | doc.find_type_elements(%i{ul ol}).each do |l|
533 | list_type = l.type.to_s
534 | items = doc.find_type_elements(:li, false, l.children)
535 | # The entire list is to use the multi-paragraph spacing rule if any of
536 | # the items in it have multiple paragraphs/other block items.
537 | srule = items.map { |i| i.children.length }.max > 1 ? 'multi' : 'single'
538 | items.each do |i|
539 | line = doc.element_line(i)
540 | # See #278 - sometimes we think non-printable characters are list
541 | # items even if they are not, so this ignore those and prevents
542 | # us from crashing
543 | next if line.empty?
544 |
545 | actual_spaces = line.gsub(/^> /, '').match(/^\s*\S+(\s+)/)[1].length
546 | required_spaces = params["#{list_type}_#{srule}".to_sym]
547 | errors << doc.element_linenumber(i) if required_spaces != actual_spaces
548 | end
549 | end
550 | errors
551 | end
552 | end
553 |
554 | rule 'MD031', 'Fenced code blocks should be surrounded by blank lines' do
555 | tags :code, :blank_lines
556 | aliases 'blanks-around-fences'
557 | check do |doc|
558 | errors = []
559 | # Some parsers (including kramdown) have trouble detecting fenced code
560 | # blocks without surrounding whitespace, so examine the lines directly.
561 | in_code = false
562 | fence = nil
563 | lines = [''] + doc.lines + ['']
564 | lines.each_with_index do |line, linenum|
565 | line.strip.match(/^(`{3,}|~{3,})/)
566 | unless Regexp.last_match(1) &&
567 | (
568 | !in_code ||
569 | (Regexp.last_match(1).slice(0, fence.length) == fence)
570 | )
571 | next
572 | end
573 |
574 | fence = in_code ? nil : Regexp.last_match(1)
575 | in_code = !in_code
576 | if (in_code && !lines[linenum - 1].empty?) ||
577 | (!in_code && !lines[linenum + 1].empty?)
578 | errors << linenum
579 | end
580 | end
581 | errors
582 | end
583 | end
584 |
585 | rule 'MD032', 'Lists should be surrounded by blank lines' do
586 | tags :bullet, :ul, :ol, :blank_lines
587 | aliases 'blanks-around-lists'
588 | check do |doc|
589 | errors = []
590 | # Some parsers (including kramdown) have trouble detecting lists
591 | # without surrounding whitespace, so examine the lines directly.
592 | in_list = false
593 | in_code = false
594 | fence = nil
595 | prev_line = ''
596 | doc.lines.each_with_index do |line, linenum|
597 | next if line.strip == '{:toc}'
598 |
599 | unless in_code
600 | list_marker = line.strip.match(/^([*+\-]|(\d+\.))\s/)
601 | if list_marker && !in_list && !prev_line.match(/^($|\s)/)
602 | errors << (linenum + 1)
603 | elsif !list_marker && in_list && !line.match(/^($|\s)/)
604 | errors << linenum
605 | end
606 | in_list = list_marker
607 | end
608 | line.strip.match(/^(`{3,}|~{3,})/)
609 | if Regexp.last_match(1) && (
610 | !in_code || (Regexp.last_match(1).slice(0, fence.length) == fence)
611 | )
612 | fence = in_code ? nil : Regexp.last_match(1)
613 | in_code = !in_code
614 | in_list = false
615 | end
616 | prev_line = line
617 | end
618 | errors.uniq
619 | end
620 | end
621 |
622 | rule 'MD033', 'Inline HTML' do
623 | tags :html
624 | aliases 'no-inline-html'
625 | params :allowed_elements => ''
626 | check do |doc|
627 | doc.element_linenumbers(doc.find_type(:html_element))
628 | allowed = params[:allowed_elements].delete(" \t\r\n").downcase.split(',')
629 | errors = doc.find_type_elements(:html_element).reject do |e|
630 | allowed.include?(e.value)
631 | end
632 | doc.element_linenumbers(errors)
633 | end
634 | end
635 |
636 | rule 'MD034', 'Bare URL used' do
637 | tags :links, :url
638 | aliases 'no-bare-urls'
639 | check do |doc|
640 | doc.matching_text_element_lines(%r{https?://})
641 | end
642 | end
643 |
644 | rule 'MD035', 'Horizontal rule style' do
645 | tags :hr
646 | aliases 'hr-style'
647 | params :style => :consistent
648 | check do |doc|
649 | hrs = doc.find_type(:hr)
650 | if hrs.empty?
651 | []
652 | else
653 | doc_style = if params[:style] == :consistent
654 | doc.element_line(hrs[0])
655 | else
656 | params[:style]
657 | end
658 | doc.element_linenumbers(
659 | hrs.reject { |e| doc.element_line(e) == doc_style },
660 | )
661 | end
662 | end
663 | end
664 |
665 | rule 'MD036', 'Emphasis used instead of a header' do
666 | tags :headers, :emphasis
667 | aliases 'no-emphasis-as-header'
668 | params :punctuation => '.,;:!?'
669 | check do |doc|
670 | # We are looking for a paragraph consisting entirely of emphasized
671 | # (italic/bold) text.
672 | errors = []
673 | doc.find_type_elements(:p, false).each do |p|
674 | next if p.children.length > 1
675 | next unless %i{em strong}.include?(p.children[0].type)
676 |
677 | lines = doc.extract_text(p.children[0], '', false)
678 | next if lines.length > 1
679 | next if lines.empty?
680 | next if lines[0].match(/[#{params[:punctuation]}]$/)
681 |
682 | errors << doc.element_linenumber(p)
683 | end
684 | errors
685 | end
686 | end
687 |
688 | rule 'MD037', 'Spaces inside emphasis markers' do
689 | tags :whitespace, :emphasis
690 | aliases 'no-space-in-emphasis'
691 | check do |doc|
692 | # Kramdown doesn't parse emphasis with spaces, which means we can just
693 | # look for emphasis patterns inside regular text with spaces just inside
694 | # them.
695 | (doc.matching_text_element_lines(/\s(\*\*?|__?)\s.+\1/) | \
696 | doc.matching_text_element_lines(/(\*\*?|__?).+\s\1\s/)).sort
697 | end
698 | end
699 |
700 | rule 'MD038', 'Spaces inside code span elements' do
701 | tags :whitespace, :code
702 | aliases 'no-space-in-code'
703 | check do |doc|
704 | # We only want to check single line codespan elements and not fenced code
705 | # block that happen to be parsed as code spans.
706 | doc.element_linenumbers(
707 | doc.find_type_elements(:codespan).select do |i|
708 | i.value.match(/(^\s|\s$)/) && !i.value.include?("\n")
709 | end,
710 | )
711 | end
712 | end
713 |
714 | rule 'MD039', 'Spaces inside link text' do
715 | tags :whitespace, :links
716 | aliases 'no-space-in-links'
717 | check do |doc|
718 | doc.element_linenumbers(
719 | doc.find_type_elements(:a).reject { |e| e.children.empty? }.select do |e|
720 | e.children.first.type == :text && e.children.last.type == :text && (
721 | e.children.first.value.start_with?(' ') ||
722 | e.children.last.value.end_with?(' '))
723 | end,
724 | )
725 | end
726 | end
727 |
728 | rule 'MD040', 'Fenced code blocks should have a language specified' do
729 | tags :code, :language
730 | aliases 'fenced-code-language'
731 | check do |doc|
732 | # Kramdown parses code blocks with language settings as code blocks with
733 | # the class attribute set to language-languagename.
734 | doc.element_linenumbers(doc.find_type_elements(:codeblock).select do |i|
735 | !i.attr['class'].to_s.start_with?('language-') &&
736 | !doc.element_line(i).start_with?(' ')
737 | end)
738 | end
739 | end
740 |
741 | rule 'MD041', 'First line in file should be a top level header' do
742 | tags :headers
743 | aliases 'first-line-h1'
744 | params :level => 1
745 | check do |doc|
746 | first_header = doc.find_type(:header).first
747 | [1] if first_header.nil? || (first_header[:location] != 1) \
748 | || (first_header[:level] != params[:level])
749 | end
750 | end
751 |
752 | rule 'MD046', 'Code block style' do
753 | tags :code
754 | aliases 'code-block-style'
755 | params :style => :fenced
756 | check do |doc|
757 | style = @params[:style]
758 | doc.element_linenumbers(
759 | doc.find_type_elements(:codeblock).select do |i|
760 | # for consistent we determine the first one
761 | if style == :consistent
762 | style = if doc.element_line(i).start_with?(' ')
763 | :indented
764 | else
765 | :fenced
766 | end
767 | end
768 | if style == :fenced
769 | # if our parent is a list or a codeblock, we need to ignore
770 | # its spaces, plus 4 more
771 | parent = i.options[:parent]
772 | ignored_spaces = 0
773 | if parent
774 | parent.options.delete(:children)
775 | parent.options.delete(:parent)
776 | if %i{li codeblock}.include?(parent.type)
777 | linenum = doc.element_linenumbers([parent]).first
778 | indent = doc.indent_for(doc.lines[linenum - 1])
779 | ignored_spaces = indent + 4
780 | end
781 | end
782 | start = ' ' * ignored_spaces
783 | doc.element_line(i).start_with?("#{start} ")
784 | else
785 | !doc.element_line(i).start_with?(' ')
786 | end
787 | end,
788 | )
789 | end
790 | end
791 |
792 | rule 'MD047', 'File should end with a single newline character' do
793 | tags :blank_lines
794 | aliases 'single-trailing-newline'
795 | check do |doc|
796 | error_lines = []
797 | last_line = doc.lines[-1]
798 | error_lines.push(doc.lines.length) unless last_line.nil? || last_line.empty?
799 | error_lines
800 | end
801 | end
802 |
--------------------------------------------------------------------------------