mdl-0.13.0/0000755000004100000410000000000014507243662012410 5ustar www-datawww-datamdl-0.13.0/bin/0000755000004100000410000000000014507243662013160 5ustar www-datawww-datamdl-0.13.0/bin/mdl0000755000004100000410000000032314507243662013660 0ustar www-datawww-data#!/usr/bin/env ruby begin require 'mdl' rescue LoadError # For running in development without bundler $LOAD_PATH << File.expand_path('../lib', File.dirname(__FILE__)) require 'mdl' end MarkdownLint.run mdl-0.13.0/mdl.gemspec0000644000004100000410000000241014507243662014526 0ustar www-datawww-datalib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'mdl/version' Gem::Specification.new do |spec| spec.name = 'mdl' spec.version = MarkdownLint::VERSION spec.authors = ['Mark Harrison'] spec.email = ['mark@mivok.net'] spec.summary = 'Markdown lint tool' spec.description = 'Style checker/lint tool for markdown files' spec.homepage = 'https://github.com/markdownlint/markdownlint' spec.license = 'MIT' spec.metadata['rubygems_mfa_required'] = 'true' spec.files = %w{LICENSE.txt Gemfile} + Dir.glob('*.gemspec') + Dir.glob('lib/**/*') spec.bindir = 'bin' spec.executables = %w{mdl} spec.require_paths = ['lib'] spec.required_ruby_version = '>= 2.7' spec.add_dependency 'kramdown', '~> 2.3' spec.add_dependency 'kramdown-parser-gfm', '~> 1.1' spec.add_dependency 'mixlib-cli', '~> 2.1', '>= 2.1.1' spec.add_dependency 'mixlib-config', '>= 2.2.1', '< 4' spec.add_dependency 'mixlib-shellout' spec.add_development_dependency 'bundler', '>= 1.12', '< 3' spec.add_development_dependency 'minitest', '~> 5.9' spec.add_development_dependency 'pry', '~> 0.10' spec.add_development_dependency 'rake', '>= 11.2', '< 14' spec.add_development_dependency 'rubocop', '~> 1.28.1' end mdl-0.13.0/lib/0000755000004100000410000000000014507243662013156 5ustar www-datawww-datamdl-0.13.0/lib/mdl.rb0000644000004100000410000001204514507243662014261 0ustar www-datawww-datarequire_relative 'mdl/formatters/sarif' require_relative 'mdl/cli' require_relative 'mdl/config' require_relative 'mdl/doc' require_relative 'mdl/kramdown_parser' require_relative 'mdl/ruleset' require_relative 'mdl/style' require_relative 'mdl/version' require 'kramdown' require 'mixlib/shellout' # Primary MDL container module MarkdownLint def self.run(argv = ARGV) cli = MarkdownLint::CLI.new cli.run(argv) ruleset = RuleSet.new ruleset.load_default unless Config[:skip_default_ruleset] Config[:rulesets]&.each do |r| ruleset.load(r) end rules = ruleset.rules Style.load(Config[:style], rules) # Rule option filter if Config[:rules] unless Config[:rules][:include].empty? rules.select! do |r, v| Config[:rules][:include].include?(r) or !(Config[:rules][:include] & v.aliases).empty? end end unless Config[:rules][:exclude].empty? rules.select! do |r, v| !Config[:rules][:exclude].include?(r) and (Config[:rules][:exclude] & v.aliases).empty? end end end # Tag option filter if Config[:tags] rules.reject! { |_r, v| (v.tags & Config[:tags][:include]).empty? } \ unless Config[:tags][:include].empty? rules.select! { |_r, v| (v.tags & Config[:tags][:exclude]).empty? } \ unless Config[:tags][:exclude].empty? end if Config[:list_rules] puts 'Enabled rules:' rules.each do |id, rule| if Config[:verbose] puts "#{id} (#{rule.aliases.join(', ')}) [#{rule.tags.join(', ')}] " + "- #{rule.description}" elsif Config[:show_aliases] puts "#{rule.aliases.first || id} - #{rule.description}" else puts "#{id} - #{rule.description}" end end exit 0 end # Recurse into directories cli.cli_arguments.each_with_index do |filename, i| if Dir.exist?(filename) if Config[:git_recurse] Dir.chdir(filename) do cli.cli_arguments[i] = Mixlib::ShellOut.new("git ls-files '*.md' '*.markdown'") .run_command.stdout.lines .map { |m| File.join(filename, m.strip) } end else cli.cli_arguments[i] = Dir["#{filename}/**/*.{md,markdown}"] end end end cli.cli_arguments.flatten! status = 0 results = [] docs_to_print = [] cli.cli_arguments.each do |filename| puts "Checking #{filename}..." if Config[:verbose] unless filename == '-' || File.exist?(filename) warn( "#{Errno::ENOENT}: No such file or directory - #{filename}", ) exit 3 end doc = Doc.new_from_file(filename, Config[:ignore_front_matter]) filename = '(stdin)' if filename == '-' if Config[:show_kramdown_warnings] status = 2 unless doc.parsed.warnings.empty? doc.parsed.warnings.each do |w| puts "#{filename}: Kramdown Warning: #{w}" end end rules.sort.each do |id, rule| puts "Processing rule #{id}" if Config[:verbose] error_lines = rule.check.call(doc) next if error_lines.nil? || error_lines.empty? status = 1 error_lines.each do |line| line += doc.offset # Correct line numbers for any yaml front matter if Config[:json] || Config[:sarif] results << { 'filename' => filename, 'line' => line, 'rule' => id, 'aliases' => rule.aliases, 'description' => rule.description, 'docs' => rule.docs_url, } else linked_id = linkify(printable_id(rule), rule.docs_url) puts "#{filename}:#{line}: #{linked_id} " + rule.description.to_s end end # If we're not in JSON or SARIF mode (URLs are in the object), and we # cannot make real links (checking if we have a TTY is an OK heuristic # for that) then, instead of making the output ugly with long URLs, we # print them at the end. And of course we only want to print each URL # once. if !Config[:json] && !Config[:sarif] && !$stdout.tty? && !docs_to_print.include?(rule) docs_to_print << rule end end end if Config[:json] require 'json' puts JSON.generate(results) elsif Config[:sarif] puts SarifFormatter.generate(rules, results) elsif docs_to_print.any? puts "\nFurther documentation is available for these failures:" docs_to_print.each do |rule| puts " - #{printable_id(rule)}: #{rule.docs_url}" end end exit status end def self.printable_id(rule) return rule.aliases.first if Config[:show_aliases] && rule.aliases.any? rule.id end # Creates hyperlinks in terminal emulators, if available: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda def self.linkify(text, url) return text unless $stdout.tty? && url "\e]8;;#{url}\e\\#{text}\e]8;;\e\\" end end mdl-0.13.0/lib/mdl/0000755000004100000410000000000014507243662013732 5ustar www-datawww-datamdl-0.13.0/lib/mdl/formatters/0000755000004100000410000000000014507243662016120 5ustar www-datawww-datamdl-0.13.0/lib/mdl/formatters/sarif.rb0000644000004100000410000000524214507243662017554 0ustar www-datawww-datarequire 'json' module MarkdownLint # SARIF formatter # # @see https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html class SarifFormatter class << self def generate(rules, results) matched_rules_id = results.map { |result| result['rule'] }.uniq matched_rules = rules.select { |id, _| matched_rules_id.include?(id) } JSON.generate(generate_sarif(matched_rules, results)) end def generate_sarif(rules, results) { :'$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 => generate_sarif_rules(rules), }, }, :results => generate_sarif_results(rules, results), } ], } end def generate_sarif_rules(rules) rules.map do |id, rule| { :id => id, :name => rule.aliases.first.split('-').map(&:capitalize).join, :defaultConfiguration => { :level => 'note', }, :properties => { :description => rule.description, :tags => rule.tags, :queryURI => rule.docs_url, }, :shortDescription => { :text => rule.description, }, :fullDescription => { :text => rule.description, }, :helpUri => rule.docs_url, :help => { :text => "More info: #{rule.docs_url}", :markdown => "[More info](#{rule.docs_url})", }, } end end def generate_sarif_results(rules, results) results.map do |result| { :ruleId => result['rule'], :ruleIndex => rules.find_index { |id, _| id == result['rule'] }, :message => { :text => "#{result['rule']} - #{result['description']}", }, :locations => [ { :physicalLocation => { :artifactLocation => { :uri => result['filename'], :uriBaseId => '%SRCROOT%', }, :region => { :startLine => result['line'], }, }, } ], } end end end end end mdl-0.13.0/lib/mdl/version.rb0000644000004100000410000000006414507243662015744 0ustar www-datawww-datamodule MarkdownLint VERSION = '0.13.0'.freeze end mdl-0.13.0/lib/mdl/kramdown_parser.rb0000644000004100000410000000127714507243662017464 0ustar www-datawww-data# Modified version of the kramdown parser to add in features/changes # appropriate for markdownlint, but which don't make sense to try to put # upstream. require 'kramdown/parser/gfm' module Kramdown module Parser # modified parser class - see comment above class MarkdownLint < Kramdown::Parser::Kramdown def initialize(source, options) super i = @block_parsers.index(:codeblock_fenced) @block_parsers.delete(:codeblock_fenced) @block_parsers.insert(i, :codeblock_fenced_gfm) end # Regular kramdown parser, but with GFM style fenced code blocks FENCED_CODEBLOCK_MATCH = Kramdown::Parser::GFM::FENCED_CODEBLOCK_MATCH end end end mdl-0.13.0/lib/mdl/styles/0000755000004100000410000000000014507243662015255 5ustar www-datawww-datamdl-0.13.0/lib/mdl/styles/default.rb0000644000004100000410000000024414507243662017226 0ustar www-datawww-dataall exclude_rule 'fenced-code-language' # Fenced code blocks should have a language exclude_rule 'first-line-h1' # First line in file should be a top level header mdl-0.13.0/lib/mdl/styles/all.rb0000644000004100000410000000000414507243662016344 0ustar www-datawww-dataall mdl-0.13.0/lib/mdl/styles/relaxed.rb0000644000004100000410000000066114507243662017231 0ustar www-datawww-dataall exclude_tag :whitespace exclude_tag :line_length exclude_rule 'MD006' # Lists at beginning of line exclude_rule 'MD007' # List indentation exclude_rule 'MD033' # Inline HTML exclude_rule 'MD034' # Bare URL used exclude_rule 'MD040' # Fenced code blocks should have a language specified exclude_rule 'MD041' # First line in file should be a top level header exclude_rule 'MD047' # File should end with a single newline character mdl-0.13.0/lib/mdl/styles/cirosantilli.rb0000644000004100000410000000100414507243662020271 0ustar www-datawww-data# Enforce the style guide at https://cirosantilli.com/markdown-style-guide all rule 'MD003', :style => :atx rule 'MD004', :style => :dash rule 'MD007', :indent => 4 rule 'MD030', :ul_multi => 3, :ol_multi => 2 rule 'MD035', :style => '---' # Inline HTML - this isn't forbidden by the style guide, and raw HTML use is # explicitly mentioned in the 'email automatic links' section. exclude_rule 'MD033' # File should end with a single newline character # this isn't forbidden by the style guide exclude_rule 'MD047' mdl-0.13.0/lib/mdl/style.rb0000644000004100000410000000362414507243662015424 0ustar www-datawww-datarequire 'set' module MarkdownLint # defines a style class Style attr_reader :rules def initialize(all_rules) @tagged_rules = {} @aliases = {} all_rules.each do |id, r| r.tags.each do |t| @tagged_rules[t] ||= Set.new @tagged_rules[t] << id end r.aliases.each do |a| @aliases[a] = id end end @all_rules = all_rules @rules = Set.new end def all @rules.merge(@all_rules.keys) end def rule(id, params = {}) if block_given? raise '"rule" does not take a block. Should this definition go in a ' + 'ruleset instead?' end id = @aliases[id] if @aliases[id] raise "No such rule: #{id}" unless @all_rules[id] @rules << id @all_rules[id].params(params) end def exclude_rule(id) id = @aliases[id] if @aliases[id] @rules.delete(id) end def tag(tag) @rules.merge(@tagged_rules[tag]) end def exclude_tag(tag) @rules.subtract(@tagged_rules[tag]) end def self.load(style_file, rules) unless style_file.include?('/') || style_file.end_with?('.rb') tmp = File.expand_path("../styles/#{style_file}.rb", __FILE__) unless File.exist?(tmp) warn "#{style_file} does not appear to be a built-in style." + ' If you meant to pass in your own style file, it must contain' + " a '/' or end in '.rb'. See https://github.com/markdownlint/" + 'markdownlint/blob/main/docs/configuration.md' exit(1) end style_file = tmp end unless File.exist?(style_file) warn "Style '#{style_file}' does not exist." exit(1) end style = new(rules) style.instance_eval(File.read(style_file), style_file) rules.select! { |r| style.rules.include?(r) } style end end end mdl-0.13.0/lib/mdl/doc.rb0000644000004100000410000002307714507243662015035 0ustar www-datawww-datarequire 'kramdown' require_relative 'kramdown_parser' module MarkdownLint ## # Representation of the markdown document passed to rule checks class Doc ## # A list of raw markdown source lines. Note that the list is 0-indexed, # while line numbers in the parsed source are 1-indexed, so you need to # subtract 1 from a line number to get the correct line. The element_line* # methods take care of this for you. attr_reader :lines, :parsed, :elements, :offset ## # A Kramdown::Document object containing the parsed markdown document. ## # A list of top level Kramdown::Element objects from the parsed document. ## # The line number offset which is greater than zero when the # markdown file contains YAML front matter that should be ignored. ## # Create a new document given a string containing the markdown source def initialize(text, ignore_front_matter = false) regex = /^---\n(.*?)---\n\n?/m if ignore_front_matter && regex.match(text) @offset = regex.match(text).to_s.split("\n").length text.sub!(regex, '') else @offset = 0 end # The -1 is to cause split to preserve an extra entry in the array so we # can tell if there's a final newline in the file or not. @lines = text.split(/\R/, -1) @parsed = Kramdown::Document.new(text, :input => 'MarkdownLint') @elements = @parsed.root.children add_annotations(@elements) end ## # Alternate 'constructor' passing in a filename def self.new_from_file(filename, ignore_front_matter = false) if filename == '-' new($stdin.read, ignore_front_matter) else new(File.read(filename, :encoding => 'UTF-8'), ignore_front_matter) end end ## # Find all elements of a given type, returning their options hash. The # options hash has most of the useful data about an element and often you # can just use this in your rules. # # # Returns [ { :location => 1, :element_level => 2 }, ... ] # elements = find_type(:li) # # If +nested+ is set to false, this returns only top level elements of a # given type. def find_type(type, nested = true) find_type_elements(type, nested).map(&:options) end ## # Find all elements of a given type, returning a list of the element # objects themselves. # # Instead of a single type, a list of types can be provided instead to # find all types. # # If +nested+ is set to false, this returns only top level elements of a # given type. def find_type_elements(type, nested = true, elements = @elements) results = [] type = [type] if type.instance_of?(Symbol) elements.each do |e| results.push(e) if type.include?(e.type) if nested && !e.children.empty? results.concat(find_type_elements(type, nested, e.children)) end end results end ## # A variation on find_type_elements that allows you to skip drilling down # into children of specific element types. # # Instead of a single type, a list of types can be provided instead to # find all types. # # Unlike find_type_elements, this method will always search for nested # elements, and skip the element types given to nested_except. def find_type_elements_except( type, nested_except = [], elements = @elements ) results = [] type = [type] if type.instance_of?(Symbol) nested_except = [nested_except] if nested_except.instance_of?(Symbol) elements.each do |e| results.push(e) if type.include?(e.type) next if nested_except.include?(e.type) || e.children.empty? results.concat( find_type_elements_except(type, nested_except, e.children), ) end results end ## # Returns the line number a given element is located on in the source # file. You can pass in either an element object or an options hash here. def element_linenumber(element) element = element.options if element.is_a?(Kramdown::Element) element[:location] end ## # Returns the actual source line for a given element. You can pass in an # element object or an options hash here. This is useful if you need to # examine the source line directly for your rule to make use of # information that isn't present in the parsed document. def element_line(element) @lines[element_linenumber(element) - 1] end ## # Returns a list of line numbers for all elements passed in. You can pass # in a list of element objects or a list of options hashes here. def element_linenumbers(elements) elements.map { |e| element_linenumber(e) } end ## # Returns the actual source lines for a list of elements. You can pass in # a list of elements objects or a list of options hashes here. def element_lines(elements) elements.map { |e| element_line(e) } end ## # Returns the header 'style' - :atx (hashes at the beginning), :atx_closed # (atx header style, but with hashes at the end of the line also), :setext # (underlined). You can pass in the element object or an options hash # here. def header_style(header) if header.type != :header raise 'header_style called with non-header element' end line = element_line(header) if line.start_with?('#') if line.strip.end_with?('#') :atx_closed else :atx end else :setext end end ## # Returns the list style for a list: :asterisk, :plus, :dash, :ordered or # :ordered_paren depending on which symbol is used to denote the list # item. You can pass in either the element itself or an options hash here. def list_style(item) raise 'list_style called with non-list element' if item.type != :li line = element_line(item).strip.gsub(/^>\s+/, '') if line.start_with?('*') :asterisk elsif line.start_with?('+') :plus elsif line.start_with?('-') :dash elsif line.match('[0-9]+\.') :ordered elsif line.match('[0-9]+\)') :ordered_paren else :unknown end end ## # Returns how much a given line is indented. Hard tabs are treated as an # indent of 8 spaces. You need to pass in the raw string here. def indent_for(line) line.match(/^\s*/)[0].gsub("\t", ' ' * 8).length end ## # Returns line numbers for lines that match the given regular expression def matching_lines(regex) @lines.each_with_index.select { |text, _linenum| regex.match(text) } .map do |i| i[1] + 1 end end ## # Returns line numbers for lines that match the given regular expression. # Only considers text inside of 'text' elements (i.e. regular markdown # text and not code/links or other elements). def matching_text_element_lines(regex, exclude_nested = [:a]) matches = [] find_type_elements_except(:text, exclude_nested).each do |e| first_line = e.options[:location] # We'll error out if kramdown doesn't have location information for # the current element. It's better to just not match in these cases # rather than crash. next if first_line.nil? lines = e.value.split("\n") lines.each_with_index do |l, i| matches << (first_line + i) if regex.match(l) end end matches end ## # Extracts the text from an element whose children consist of text # elements and other things def extract_text(element, prefix = '', restore_whitespace = true) quotes = { :rdquo => '"', :ldquo => '"', :lsquo => "'", :rsquo => "'", } # If anything goes amiss here, e.g. unknown type, then nil will be # returned and we'll just not catch that part of the text, which seems # like a sensible failure mode. lines = element.children.map do |e| if e.type == :text e.value elsif %i{strong em p codespan}.include?(e.type) extract_text(e, prefix, restore_whitespace).join("\n") elsif e.type == :smart_quote quotes[e.value] end end.join.split("\n") # Text blocks have whitespace stripped, so we need to add it back in at # the beginning. Because this might be in something like a blockquote, # we optionally strip off a prefix given to the function. lines[0] = element_line(element).sub(prefix, '') if restore_whitespace lines end ## # Returns the element as plaintext def extract_as_text(element) quotes = { :rdquo => '"', :ldquo => '"', :lsquo => "'", :rsquo => "'", } # If anything goes amiss here, e.g. unknown type, then nil will be # returned and we'll just not catch that part of the text, which seems # like a sensible failure mode. element.children.map do |e| if e.type == :text || e.type == :codespan e.value elsif %i{strong em p a}.include?(e.type) extract_as_text(e).join("\n") elsif e.type == :smart_quote quotes[e.value] end end.join.split("\n") end private ## # Adds a 'level' and 'parent' option to all elements to show how nested they # are def add_annotations(elements, level = 1, parent = nil) elements.each do |e| e.options[:element_level] = level e.options[:parent] = parent add_annotations(e.children, level + 1, e) end end end end mdl-0.13.0/lib/mdl/cli.rb0000644000004100000410000001275314507243662015036 0ustar www-datawww-datarequire 'mixlib/cli' require 'pathname' module MarkdownLint # Our Mixlib::CLI class class CLI include Mixlib::CLI CONFIG_FILE = '.mdlrc'.freeze banner "Usage: #{File.basename($PROGRAM_NAME)} [options] [FILE.md|DIR ...]" option :show_aliases, :short => '-a', :long => '--[no-]show-aliases', :description => 'Show rule alias instead of rule ID when viewing rules', :boolean => true option :config_file, :short => '-c', :long => '--config FILE', :description => 'The configuration file to use', :default => CONFIG_FILE.to_s option :verbose, :short => '-v', :long => '--[no-]verbose', :description => 'Increase verbosity', :boolean => true option :ignore_front_matter, :short => '-i', :long => '--[no-]ignore-front-matter', :boolean => true, :description => 'Ignore YAML front matter' option :show_kramdown_warnings, :short => '-w', :long => '--[no-]warnings', :description => 'Show kramdown warnings', :boolean => true option :tags, :short => '-t', :long => '--tags TAG1,TAG2', :description => 'Only process rules with these tags', :proc => proc { |v| toggle_list(v, true) } option :rules, :short => '-r', :long => '--rules RULE1,RULE2', :description => 'Only process these rules', :proc => proc { |v| toggle_list(v) } option :style, :short => '-s', :long => '--style STYLE', :description => 'Load the given style' option :list_rules, :short => '-l', :long => '--list-rules', :boolean => true, :description => "Don't process any files, just list enabled rules" option :git_recurse, :short => '-g', :long => '--git-recurse', :boolean => true, :description => 'Only process files known to git when given a directory' option :rulesets, :short => '-u', :long => '--rulesets RULESET1,RULESET2', :proc => proc { |v| v.split(',') }, :description => 'Specify additional ruleset files to load' option :skip_default_ruleset, :short => '-d', :long => '--skip-default-ruleset', :boolean => true, :description => "Don't load the default markdownlint ruleset" option :help, :on => :tail, :short => '-h', :long => '--help', :description => 'Show this message', :boolean => true, :show_options => true, :exit => 0 option :version, :on => :tail, :short => '-V', :long => '--version', :description => 'Show version', :boolean => true, :proc => proc { puts MarkdownLint::VERSION }, :exit => 0 option :json, :short => '-j', :long => '--json', :description => 'JSON output', :boolean => true option :sarif, :short => '-S', :long => '--sarif', :description => 'SARIF output', :boolean => true def run(argv = ARGV) parse_options(argv) # Load the config file if it's present filename = CLI.probe_config_file(config[:config_file]) # Only fall back to ~/.mdlrc if we are using the default value for -c if filename.nil? && (config[:config_file] == CONFIG_FILE) filename = File.expand_path("~/#{CONFIG_FILE}") end if !filename.nil? && File.exist?(filename) MarkdownLint::Config.from_file(filename.to_s) puts "Loaded config from #{filename}" if config[:verbose] end # Put values in the config file MarkdownLint::Config.merge!(config) # Set the correct format for any rules/tags configuration loaded from # the config file. Ideally this would probably be done as part of the # config class itself rather than here. unless MarkdownLint::Config[:rules].nil? MarkdownLint::Config[:rules] = CLI.toggle_list( MarkdownLint::Config[:rules], ) end unless MarkdownLint::Config[:tags].nil? MarkdownLint::Config[:tags] = CLI.toggle_list( MarkdownLint::Config[:tags], true ) end # Read from stdin if we didn't provide a filename cli_arguments << '-' if cli_arguments.empty? && !config[:list_rules] end def self.toggle_list(parts, to_sym = false) parts = parts.split(',') if parts.instance_of?(String) if parts.instance_of?(Array) inc = parts.reject { |p| p.start_with?('~') } exc = parts.select { |p| p.start_with?('~') }.map { |p| p[1..] } if to_sym inc.map!(&:to_sym) exc.map!(&:to_sym) end { :include => inc, :exclude => exc } else # We already converted the string into a list of include/exclude # pairs, so just return as is parts end end def self.probe_config_file(path) expanded_path = File.expand_path(path) return expanded_path if File.exist?(expanded_path) # Look for a file up from the working dir Pathname.new(expanded_path).ascend do |p| next unless p.directory? config_file = p.join(CONFIG_FILE) return config_file if File.exist?(config_file) end nil end end end mdl-0.13.0/lib/mdl/rules.rb0000644000004100000410000006565714507243662015434 0ustar www-datawww-datadocs do |id, description| url_hash = [id.downcase, description.downcase.gsub(/[^a-z]+/, '-')].join('---') "https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md##{url_hash}" end rule 'MD001', 'Header levels should only increment by one level at a time' do tags :headers aliases 'header-increment' check do |doc| headers = doc.find_type(:header) old_level = nil errors = [] headers.each do |h| errors << h[:location] if old_level && (h[:level] > old_level + 1) old_level = h[:level] end errors end end rule 'MD002', 'First header should be a top level header' do tags :headers aliases 'first-header-h1' params :level => 1 check do |doc| first_header = doc.find_type(:header).first if first_header && (first_header[:level] != @params[:level]) [first_header[:location]] end end end rule 'MD003', 'Header style' do # Header styles are things like ### and adding underscores # See https://daringfireball.net/projects/markdown/syntax#header tags :headers aliases 'header-style' # :style can be one of :consistent, :atx, :atx_closed, :setext params :style => :consistent check do |doc| headers = doc.find_type_elements(:header, false) if headers.empty? nil else doc_style = if @params[:style] == :consistent doc.header_style(headers.first) else @params[:style] end if doc_style == :setext_with_atx headers.map do |h| doc.element_linenumber(h) \ unless (doc.header_style(h) == :setext) || \ ((doc.header_style(h) == :atx) && \ (h.options[:level] > 2)) end.compact else headers.map do |h| doc.element_linenumber(h) \ if doc.header_style(h) != doc_style end.compact end end end end rule 'MD004', 'Unordered list style' do tags :bullet, :ul aliases 'ul-style' # :style can be one of :consistent, :asterisk, :plus, :dash, :sublist params :style => :consistent check do |doc| bullets = doc.find_type_elements(:ul).map do |l| doc.find_type_elements(:li, false, l.children) end.flatten if bullets.empty? nil else doc_style = case @params[:style] when :consistent doc.list_style(bullets.first) when :sublist {} else @params[:style] end results = [] bullets.each do |b| if @params[:style] == :sublist level = b.options[:element_level] if doc_style[level] if doc_style[level] != doc.list_style(b) results << doc.element_linenumber(b) end else doc_style[level] = doc.list_style(b) end elsif doc.list_style(b) != doc_style results << doc.element_linenumber(b) end end results.compact end end end rule 'MD005', 'Inconsistent indentation for list items at the same level' do tags :bullet, :ul, :indentation aliases 'list-indent' check do |doc| bullets = doc.find_type(:li) errors = [] indent_levels = [] bullets.each do |b| indent_level = doc.indent_for(doc.element_line(b)) if indent_levels[b[:element_level]].nil? indent_levels[b[:element_level]] = indent_level end if indent_level != indent_levels[b[:element_level]] errors << doc.element_linenumber(b) end end errors end end rule 'MD006', 'Consider starting bulleted lists at the beginning of the line' do # Starting at the beginning of the line means that indentation for each # bullet level can be identical. tags :bullet, :ul, :indentation aliases 'ul-start-left' check do |doc| doc.find_type(:ul, false).reject do |e| doc.indent_for(doc.element_line(e)) == 0 end.map { |e| e[:location] } end end rule 'MD007', 'Unordered list indentation' do tags :bullet, :ul, :indentation aliases 'ul-indent' # Do not default to < 3, see PR#373 or the comments in RULES.md params :indent => 3 check do |doc| errors = [] indents = doc.find_type(:ul).map do |e| [doc.indent_for(doc.element_line(e)), doc.element_linenumber(e)] end curr_indent = indents[0][0] unless indents.empty? indents.each do |indent, linenum| if (indent > curr_indent) && (indent - curr_indent != @params[:indent]) errors << linenum end curr_indent = indent end errors end end rule 'MD009', 'Trailing spaces' do tags :whitespace aliases 'no-trailing-spaces' params :br_spaces => 2 check do |doc| errors = doc.matching_lines(/\s$/) if params[:br_spaces] > 1 errors -= doc.matching_lines(/\S\s{#{params[:br_spaces]}}$/) end errors end end rule 'MD010', 'Hard tabs' do tags :whitespace, :hard_tab aliases 'no-hard-tabs' params :ignore_code_blocks => false check do |doc| # Every line in the document that is part of a code block. Blank lines # inside of a code block are acceptable. codeblock_lines = doc.find_type_elements(:codeblock).map do |e| (doc.element_linenumber(e).. doc.element_linenumber(e) + e.value.lines.count).to_a end.flatten # Check for lines with hard tab hard_tab_lines = doc.matching_lines(/\t/) # Remove lines with hard tabs, if they stem from codeblock hard_tab_lines -= codeblock_lines if params[:ignore_code_blocks] hard_tab_lines end end rule 'MD011', 'Reversed link syntax' do tags :links aliases 'no-reversed-links' check do |doc| doc.matching_text_element_lines(/\([^)]+\)\[[^\]]+\]/) end end rule 'MD012', 'Multiple consecutive blank lines' do tags :whitespace, :blank_lines aliases 'no-multiple-blanks' check do |doc| # Every line in the document that is part of a code block. Blank lines # inside of a code block are acceptable. codeblock_lines = doc.find_type_elements(:codeblock).map do |e| (doc.element_linenumber(e).. doc.element_linenumber(e) + e.value.lines.count).to_a end.flatten blank_lines = doc.matching_lines(/^\s*$/) cons_blank_lines = blank_lines.each_cons(2).select do |p, n| n - p == 1 end.map { |_p, n| n } cons_blank_lines - codeblock_lines end end rule 'MD013', 'Line length' do tags :line_length aliases 'line-length' params :line_length => 80, :ignore_code_blocks => false, :code_blocks => true, :tables => true check do |doc| # Every line in the document that is part of a code block. codeblock_lines = doc.find_type_elements(:codeblock).map do |e| (doc.element_linenumber(e).. doc.element_linenumber(e) + e.value.lines.count).to_a end.flatten # Every line in the document that is part of a table. locations = doc.elements .map { |e| [e.options[:location], e] } .reject { |l, _| l.nil? } table_lines = locations.map.with_index do |(l, e), i| if e.type == :table if i + 1 < locations.size (l..locations[i + 1].first - 1).to_a else (l..doc.lines.count).to_a end end end.flatten overlines = doc.matching_lines(/^.{#{@params[:line_length]}}.*\s/) if !params[:code_blocks] || params[:ignore_code_blocks] overlines -= codeblock_lines unless params[:code_blocks] warn 'MD013 warning: Parameter :code_blocks is deprecated.' warn ' Please replace \":code_blocks => false\" by '\ '\":ignore_code_blocks => true\" in your configuration.' end end overlines -= table_lines unless params[:tables] overlines end end rule 'MD014', 'Dollar signs used before commands without showing output' do tags :code aliases 'commands-show-output' check do |doc| doc.find_type_elements(:codeblock).select do |e| !e.value.empty? && !e.value.split(/\n+/).map { |l| l.match(/^\$\s/) }.include?(nil) end.map { |e| doc.element_linenumber(e) } end end rule 'MD018', 'No space after hash on atx style header' do tags :headers, :atx, :spaces aliases 'no-missing-space-atx' check do |doc| doc.find_type_elements(:header).select do |h| doc.header_style(h) == :atx && doc.element_line(h).match(/^#+[^#\s]/) end.map { |h| doc.element_linenumber(h) } end end rule 'MD019', 'Multiple spaces after hash on atx style header' do tags :headers, :atx, :spaces aliases 'no-multiple-space-atx' check do |doc| doc.find_type_elements(:header).select do |h| doc.header_style(h) == :atx && doc.element_line(h).match(/^#+\s\s/) end.map { |h| doc.element_linenumber(h) } end end rule 'MD020', 'No space inside hashes on closed atx style header' do tags :headers, :atx_closed, :spaces aliases 'no-missing-space-closed-atx' check do |doc| doc.find_type_elements(:header).select do |h| doc.header_style(h) == :atx_closed \ && (doc.element_line(h).match(/^#+[^#\s]/) \ || doc.element_line(h).match(/[^#\s\\]#+$/)) end.map { |h| doc.element_linenumber(h) } end end rule 'MD021', 'Multiple spaces inside hashes on closed atx style header' do tags :headers, :atx_closed, :spaces aliases 'no-multiple-space-closed-atx' check do |doc| doc.find_type_elements(:header).select do |h| doc.header_style(h) == :atx_closed \ && (doc.element_line(h).match(/^#+\s\s/) \ || doc.element_line(h).match(/\s\s#+$/)) end.map { |h| doc.element_linenumber(h) } end end rule 'MD022', 'Headers should be surrounded by blank lines' do tags :headers, :blank_lines aliases 'blanks-around-headers' check do |doc| errors = [] doc.find_type_elements(:header, false).each do |h| header_bad = false linenum = doc.element_linenumber(h) # Check previous line header_bad = true if (linenum > 1) && !doc.lines[linenum - 2].empty? # Check next line next_line_idx = doc.header_style(h) == :setext ? linenum + 1 : linenum next_line = doc.lines[next_line_idx] header_bad = true if !next_line.nil? && !next_line.empty? errors << linenum if header_bad end # Kramdown requires that headers start on a block boundary, so in most # cases it won't pick up a header without a blank line before it. We need # to check regular text and pick out headers ourselves too doc.find_type_elements(:p, false).each do |p| linenum = doc.element_linenumber(p) text = p.children.select { |e| e.type == :text }.map(&:value).join lines = text.split("\n") prev_lines = ['', ''] lines.each do |line| # First look for ATX style headers without blank lines before errors << linenum if line.match(/^\#{1,6}/) && !prev_lines[1].empty? # Next, look for setext style if line.match(/^(-+|=+)\s*$/) && !prev_lines[0].empty? errors << (linenum - 1) end linenum += 1 prev_lines << line prev_lines.shift end end errors.sort end end rule 'MD023', 'Headers must start at the beginning of the line' do tags :headers, :spaces aliases 'header-start-left' check do |doc| errors = [] # The only type of header with spaces actually parsed as such is setext # style where only the text is indented. We check for that first. doc.find_type_elements(:header, false).each do |h| errors << doc.element_linenumber(h) if doc.element_line(h).match(/^\s/) end # Next we have to look for things that aren't parsed as headers because # they start with spaces. doc.find_type_elements(:p, false).each do |p| linenum = doc.element_linenumber(p) lines = doc.extract_text(p) prev_line = '' lines.each do |line| # First look for ATX style headers errors << linenum if line.match(/^\s+\#{1,6}/) # Next, look for setext style if line.match(/^\s+(-+|=+)\s*$/) && !prev_line.empty? errors << (linenum - 1) end linenum += 1 prev_line = line end end errors.sort end end rule 'MD024', 'Multiple headers with the same content' do tags :headers aliases 'no-duplicate-header' params :allow_different_nesting => false check do |doc| headers = doc.find_type(:header) allow_different_nesting = params[:allow_different_nesting] duplicates = headers.select do |h| headers.any? do |e| e[:location] < h[:location] && e[:raw_text] == h[:raw_text] && (allow_different_nesting == false || e[:level] != h[:level]) end end.to_set if allow_different_nesting same_nesting_duplicates = Set.new stack = [] current_level = 0 doc.find_type(:header).each do |header| level = header[:level] text = header[:raw_text] if current_level > level stack.pop elsif current_level < level stack.push([text]) elsif stack.last.include?(text) same_nesting_duplicates.add(header) end current_level = level end duplicates += same_nesting_duplicates end duplicates.map { |h| doc.element_linenumber(h) } end end rule 'MD025', 'Multiple top level headers in the same document' do tags :headers aliases 'single-h1' params :level => 1 check do |doc| headers = doc.find_type(:header, false).select do |h| h[:level] == params[:level] end if !headers.empty? && (doc.element_linenumber(headers[0]) == 1) headers[1..].map { |h| doc.element_linenumber(h) } end end end rule 'MD026', 'Trailing punctuation in header' do tags :headers aliases 'no-trailing-punctuation' params :punctuation => '.,;:!?' check do |doc| doc.find_type(:header).select do |h| h[:raw_text].match(/[#{params[:punctuation]}]$/) end.map do |h| doc.element_linenumber(h) end end end rule 'MD027', 'Multiple spaces after blockquote symbol' do tags :blockquote, :whitespace, :indentation aliases 'no-multiple-space-blockquote' check do |doc| errors = [] doc.find_type_elements(:blockquote).each do |e| linenum = doc.element_linenumber(e) lines = doc.extract_as_text(e) # Handle first line specially as whitespace is stripped from the text # element errors << linenum if doc.element_line(e).match(/^\s*> /) lines.each do |line| errors << linenum if line.start_with?(' ') linenum += 1 end end errors end end rule 'MD028', 'Blank line inside blockquote' do tags :blockquote, :whitespace aliases 'no-blanks-blockquote' check do |doc| def check_blockquote(errors, elements) prev = [nil, nil, nil] elements.each do |e| prev.shift prev << e.type if prev == %i{blockquote blank blockquote} # The current location is the start of the second blockquote, so the # line before will be a blank line in between the two, or at least the # lowest blank line if there are more than one. errors << (e.options[:location] - 1) end check_blockquote(errors, e.children) end end errors = [] check_blockquote(errors, doc.elements) errors end end rule 'MD029', 'Ordered list item prefix' do tags :ol aliases 'ol-prefix' # Style can be :one or :ordered params :style => :one check do |doc| case params[:style] when :ordered doc.find_type_elements(:ol).map do |l| doc.find_type_elements(:li, false, l.children) .map.with_index do |i, idx| unless doc.element_line(i).strip.start_with?("#{idx + 1}. ") doc.element_linenumber(i) end end end.flatten.compact when :one doc.find_type_elements(:ol).map do |l| doc.find_type_elements(:li, false, l.children) end.flatten.map do |i| unless doc.element_line(i).strip.start_with?('1. ') doc.element_linenumber(i) end end.compact end end end rule 'MD030', 'Spaces after list markers' do tags :ol, :ul, :whitespace aliases 'list-marker-space' params :ul_single => 1, :ol_single => 1, :ul_multi => 1, :ol_multi => 1 check do |doc| errors = [] doc.find_type_elements(%i{ul ol}).each do |l| list_type = l.type.to_s items = doc.find_type_elements(:li, false, l.children) # The entire list is to use the multi-paragraph spacing rule if any of # the items in it have multiple paragraphs/other block items. srule = items.map { |i| i.children.length }.max > 1 ? 'multi' : 'single' items.each do |i| line = doc.element_line(i) # See #278 - sometimes we think non-printable characters are list # items even if they are not, so this ignore those and prevents # us from crashing next if line.empty? actual_spaces = line.gsub(/^> /, '').match(/^\s*\S+(\s+)/)[1].length required_spaces = params["#{list_type}_#{srule}".to_sym] errors << doc.element_linenumber(i) if required_spaces != actual_spaces end end errors end end rule 'MD031', 'Fenced code blocks should be surrounded by blank lines' do tags :code, :blank_lines aliases 'blanks-around-fences' check do |doc| errors = [] # Some parsers (including kramdown) have trouble detecting fenced code # blocks without surrounding whitespace, so examine the lines directly. in_code = false fence = nil lines = [''] + doc.lines + [''] lines.each_with_index do |line, linenum| line.strip.match(/^(`{3,}|~{3,})/) unless Regexp.last_match(1) && ( !in_code || (Regexp.last_match(1).slice(0, fence.length) == fence) ) next end fence = in_code ? nil : Regexp.last_match(1) in_code = !in_code if (in_code && !lines[linenum - 1].empty?) || (!in_code && !lines[linenum + 1].empty?) errors << linenum end end errors end end rule 'MD032', 'Lists should be surrounded by blank lines' do tags :bullet, :ul, :ol, :blank_lines aliases 'blanks-around-lists' check do |doc| errors = [] # Some parsers (including kramdown) have trouble detecting lists # without surrounding whitespace, so examine the lines directly. in_list = false in_code = false fence = nil prev_line = '' doc.lines.each_with_index do |line, linenum| next if line.strip == '{:toc}' unless in_code list_marker = line.strip.match(/^([*+\-]|(\d+\.))\s/) if list_marker && !in_list && !prev_line.match(/^($|\s)/) errors << (linenum + 1) elsif !list_marker && in_list && !line.match(/^($|\s)/) errors << linenum end in_list = list_marker end line.strip.match(/^(`{3,}|~{3,})/) if Regexp.last_match(1) && ( !in_code || (Regexp.last_match(1).slice(0, fence.length) == fence) ) fence = in_code ? nil : Regexp.last_match(1) in_code = !in_code in_list = false end prev_line = line end errors.uniq end end rule 'MD033', 'Inline HTML' do tags :html aliases 'no-inline-html' params :allowed_elements => '' check do |doc| doc.element_linenumbers(doc.find_type(:html_element)) allowed = params[:allowed_elements].delete(" \t\r\n").downcase.split(',') errors = doc.find_type_elements(:html_element).reject do |e| allowed.include?(e.value) end doc.element_linenumbers(errors) end end rule 'MD034', 'Bare URL used' do tags :links, :url aliases 'no-bare-urls' check do |doc| doc.matching_text_element_lines(%r{https?://}) end end rule 'MD035', 'Horizontal rule style' do tags :hr aliases 'hr-style' params :style => :consistent check do |doc| hrs = doc.find_type(:hr) if hrs.empty? [] else doc_style = if params[:style] == :consistent doc.element_line(hrs[0]) else params[:style] end doc.element_linenumbers( hrs.reject { |e| doc.element_line(e) == doc_style }, ) end end end rule 'MD036', 'Emphasis used instead of a header' do tags :headers, :emphasis aliases 'no-emphasis-as-header' params :punctuation => '.,;:!?' check do |doc| # We are looking for a paragraph consisting entirely of emphasized # (italic/bold) text. errors = [] doc.find_type_elements(:p, false).each do |p| next if p.children.length > 1 next unless %i{em strong}.include?(p.children[0].type) lines = doc.extract_text(p.children[0], '', false) next if lines.length > 1 next if lines.empty? next if lines[0].match(/[#{params[:punctuation]}]$/) errors << doc.element_linenumber(p) end errors end end rule 'MD037', 'Spaces inside emphasis markers' do tags :whitespace, :emphasis aliases 'no-space-in-emphasis' check do |doc| # Kramdown doesn't parse emphasis with spaces, which means we can just # look for emphasis patterns inside regular text with spaces just inside # them. (doc.matching_text_element_lines(/\s(\*\*?|__?)\s.+\1/) | \ doc.matching_text_element_lines(/(\*\*?|__?).+\s\1\s/)).sort end end rule 'MD038', 'Spaces inside code span elements' do tags :whitespace, :code aliases 'no-space-in-code' check do |doc| # We only want to check single line codespan elements and not fenced code # block that happen to be parsed as code spans. doc.element_linenumbers( doc.find_type_elements(:codespan).select do |i| i.value.match(/(^\s|\s$)/) && !i.value.include?("\n") end, ) end end rule 'MD039', 'Spaces inside link text' do tags :whitespace, :links aliases 'no-space-in-links' check do |doc| doc.element_linenumbers( doc.find_type_elements(:a).reject { |e| e.children.empty? }.select do |e| e.children.first.type == :text && e.children.last.type == :text && ( e.children.first.value.start_with?(' ') || e.children.last.value.end_with?(' ')) end, ) end end rule 'MD040', 'Fenced code blocks should have a language specified' do tags :code, :language aliases 'fenced-code-language' check do |doc| # Kramdown parses code blocks with language settings as code blocks with # the class attribute set to language-languagename. doc.element_linenumbers(doc.find_type_elements(:codeblock).select do |i| !i.attr['class'].to_s.start_with?('language-') && !doc.element_line(i).start_with?(' ') end) end end rule 'MD041', 'First line in file should be a top level header' do tags :headers aliases 'first-line-h1' params :level => 1 check do |doc| first_header = doc.find_type(:header).first [1] if first_header.nil? || (first_header[:location] != 1) \ || (first_header[:level] != params[:level]) end end rule 'MD046', 'Code block style' do tags :code aliases 'code-block-style' params :style => :fenced check do |doc| style = @params[:style] doc.element_linenumbers( doc.find_type_elements(:codeblock).select do |i| # for consistent we determine the first one if style == :consistent style = if doc.element_line(i).start_with?(' ') :indented else :fenced end end if style == :fenced # if our parent is a list or a codeblock, we need to ignore # its spaces, plus 4 more parent = i.options[:parent] ignored_spaces = 0 if parent parent.options.delete(:children) parent.options.delete(:parent) if %i{li codeblock}.include?(parent.type) linenum = doc.element_linenumbers([parent]).first indent = doc.indent_for(doc.lines[linenum - 1]) ignored_spaces = indent + 4 end end start = ' ' * ignored_spaces doc.element_line(i).start_with?("#{start} ") else !doc.element_line(i).start_with?(' ') end end, ) end end rule 'MD047', 'File should end with a single newline character' do tags :blank_lines aliases 'single-trailing-newline' check do |doc| error_lines = [] last_line = doc.lines[-1] error_lines.push(doc.lines.length) unless last_line.nil? || last_line.empty? error_lines end end rule 'MD055', 'Table row doesn\'t begin/end with pipes' do tags :tables aliases 'table-rows-start-and-end-with-pipes' check do |doc| error_lines = [] tables = doc.find_type_elements(:table) lines = doc.lines tables.each do |table| table_pos = table.options[:location] - 1 table_rows = get_table_rows(lines, table_pos) table_rows.each_with_index do |line, index| if line.length < 2 || line[0] != '|' || line[-1] != '|' error_lines << (table_pos + index + 1) end end end error_lines end end rule 'MD056', 'Table has inconsistent number of columns' do tags :tables aliases 'inconsistent-columns-in-table' check do |doc| error_lines = [] tables = doc.find_type_elements(:table) lines = doc.lines tables.each do |table| table_pos = table.options[:location] - 1 table_rows = get_table_rows(lines, table_pos) num_headings = number_of_columns_in_a_table_row(lines[table_pos]) table_rows.each_with_index do |line, index| if number_of_columns_in_a_table_row(line) != num_headings error_lines << (table_pos + index + 1) end end end error_lines end end rule 'MD057', 'Table has missing or invalid header separation (second row)' do tags :tables aliases 'table-invalid-second-row' check do |doc| error_lines = [] tables = doc.find_type_elements(:table) lines = doc.lines tables.each do |table| second_row = '' # line number of table start (1-indexed) # which is equal to second row's index (0-indexed) line_num = table.options[:location] second_row = lines[line_num] if line_num < lines.length # This pattern matches if # 1) The row starts and stops with | characters # 2) Only consists of characters '|', '-', ':' and whitespace # 3) Each section between the separators (i.e. '|') # a) has at least three consecutive dashes # b) can have whitespace at the beginning or the end # c) can have colon before and/or after dashes (for alignment) # Some examples: # |-----|----|-------| --> matches # |:---:|:---|-------| --> matches # | :------: | ----| --> matches # | - - - | - - - | --> does NOT match # |::---| --> does NOT match # |----:|:--|----| --> does NOT match pattern = /^(\|\s*:?-{3,}:?\s*)+\|$/ unless second_row.match(pattern) # Second row is not in the form described by the pattern error_lines << (line_num + 1) end end error_lines end end mdl-0.13.0/lib/mdl/ruleset.rb0000644000004100000410000000734114507243662015747 0ustar www-datawww-datamodule MarkdownLint # defines a single rule class Rule attr_accessor :id, :description def initialize(id, description, fallback_docs: nil, &block) @id = id @description = description @generate_docs = fallback_docs @docs_overridden = false @aliases = [] @tags = [] @params = {} instance_eval(&block) end def check(&block) @check = block unless block.nil? @check end def tags(*tags) @tags = tags.flatten.map(&:to_sym) unless tags.empty? @tags end def aliases(*aliases) @aliases.concat(aliases) @aliases end def params(params = nil) @params.update(params) unless params.nil? @params end def docs(url = nil, &block) if block_given? != url.nil? raise ArgumentError, 'Give either a URL or a block, not both' end raise 'A docs url is already set within this rule' if @docs_overridden @generate_docs = block_given? ? block : lambda { |_, _| url } @docs_overridden = true end def docs_url @generate_docs&.call(id, description) end # This method calculates the number of columns in a table row # # @param [String] table_row A row of the table in question. # @return [Numeric] Number of columns in the row def number_of_columns_in_a_table_row(table_row) columns = table_row.strip.split('|') if columns.empty? # The stripped line consists of zero or more pipe characters # and nothing more. # # Examples of stripped rows: # '||' --> one column # '|||' --> two columns # '|' --> zero columns [0, table_row.count('|') - 1].max else # Number of columns is the number of splited # segments with pipe separator. The first segment # is ignored when it's empty string because # someting like '|1|2|' is split into ['', '1', '2'] # when using split('|') function. # # Some examples: # '|foo|bar|' --> two columns # ' |foo|bar|' --> two columns # '|foo|bar' --> two columns # 'foo|bar' --> two columns columns.size - (columns[0].empty? ? 1 : 0) end end # This method returns all the rows of a table # # @param [Array] lines Lines of a doc as an array # @param [Numeric] pos Position/index of the table in the array # @return [Array] Rows of the table in an array def get_table_rows(lines, pos) table_rows = [] while pos < lines.length line = lines[pos] # If the previous line is a table and the current line # 1) includes pipe character # 2) does not start with code block identifiers # a) >= 4 spaces # b) < 4 spaces and ``` right after # # it is possibly a table row unless line.include?('|') && !line.start_with?(' ') && !line.strip.start_with?('```') break end table_rows << line pos += 1 end table_rows end end # defines a ruleset class RuleSet attr_reader :rules def initialize @rules = {} end def rule(id, description, &block) @rules[id] = Rule.new(id, description, :fallback_docs => @fallback_docs, &block) end def load(rules_file) instance_eval(File.read(rules_file), rules_file) @rules end def docs(url = nil, &block) if block_given? != url.nil? raise ArgumentError, 'Give either a URL or a block, not both' end @fallback_docs = block_given? ? block : lambda { |_, _| url } end def load_default load(File.expand_path('rules.rb', __dir__)) end end end mdl-0.13.0/lib/mdl/config.rb0000644000004100000410000000023514507243662015524 0ustar www-datawww-datarequire 'mixlib/config' module MarkdownLint # our Mixlib::Config class module Config extend Mixlib::Config default :style, 'default' end end mdl-0.13.0/Gemfile0000644000004100000410000000004614507243662013703 0ustar www-datawww-datasource 'https://rubygems.org' gemspec mdl-0.13.0/LICENSE.txt0000644000004100000410000000205614507243662014236 0ustar www-datawww-dataCopyright (c) 2014 Mark Harrison MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.