rubocop-spellcheck 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Spellcheck
5
+ # Masks Asciidoctor inline syntax without substituting attributes or generating HTML.
6
+ class AsciiDocInline
7
+ def initialize(source, positions)
8
+ @source = source.dup
9
+ @positions = positions
10
+ end
11
+
12
+ def span
13
+ mask_code
14
+ mask_macros
15
+ mask_quotes
16
+ decoded_span
17
+ end
18
+
19
+ private
20
+
21
+ def decoded_span
22
+ text = +''
23
+ positions = []
24
+ @source.to_enum(:scan, Markdown::PIECE).each do
25
+ decoded, mapped = decoded_piece(Regexp.last_match)
26
+ text << decoded
27
+ positions.concat(mapped)
28
+ end
29
+ Span.new(text, positions)
30
+ end
31
+
32
+ def decoded_piece(match)
33
+ decoded = Markdown.decode(match[0])
34
+ range = [@positions.fetch(match.begin(0)).first, @positions.fetch(match.end(0) - 1).last]
35
+ [decoded, Array.new(decoded.length, range)]
36
+ end
37
+
38
+ def mask_code
39
+ mask_matches(::Asciidoctor::InlinePassMacroRx)
40
+ mask_matches(::Asciidoctor::InlinePassRx[false][2], 6)
41
+ mask_matches(::Asciidoctor::InlineStemMacroRx)
42
+ mask_matches(::Asciidoctor::InlineKbdBtnMacroRx)
43
+ mask_matches(::Asciidoctor::InlineImageMacroRx)
44
+ mask_matches(::Asciidoctor::InlineIndextermMacroRx)
45
+ mask_matches(::Asciidoctor::InlineMenuMacroRx)
46
+ end
47
+
48
+ def mask_macros
49
+ keep_capture(::Asciidoctor::InlineLinkMacroRx, 3)
50
+ keep_capture(::Asciidoctor::InlineLinkRx, 5)
51
+ keep_capture(/<<[^>,]+(?:,([^>]+))?>>|xref:[^\s\[]+\[([^\]]*)\]/, 1, 2)
52
+ keep_capture(::Asciidoctor::InlineFootnoteMacroRx, 3)
53
+ mask_matches(::Asciidoctor::InlineAnchorRx)
54
+ mask_matches(::Asciidoctor::AttributeReferenceRx)
55
+ end
56
+
57
+ def mask_quotes
58
+ ::Asciidoctor::QUOTE_SUBS[false].each do |type, scope, pattern|
59
+ capture = scope == :constrained ? 3 : 2
60
+ type == :monospaced ? mask_matches(pattern) : keep_capture(pattern, capture)
61
+ end
62
+ end
63
+
64
+ def mask_matches(pattern, capture = 0)
65
+ @source.dup.to_enum(:scan, pattern).each do
66
+ match = Regexp.last_match
67
+ mask(match.begin(capture), match.end(capture))
68
+ end
69
+ end
70
+
71
+ def keep_capture(pattern, *captures)
72
+ @source.dup.to_enum(:scan, pattern).each do
73
+ match = Regexp.last_match
74
+ capture = captures.find { |index| match[index] }
75
+ if capture
76
+ mask(match.begin(0), match.begin(capture))
77
+ mask(match.end(capture), match.end(0))
78
+ else
79
+ mask(match.begin(0), match.end(0))
80
+ end
81
+ end
82
+ end
83
+
84
+ def mask(first, last)
85
+ @source[first...last] = @source[first...last].gsub(/[^\n]/, ' ')
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Spellcheck
5
+ # Cell line numbers can repeat in CSV tables; locate cells in source order within their table.
6
+ class AsciiDocTable
7
+ def initialize(buffer, table)
8
+ @buffer = buffer
9
+ @cursor = buffer.line_range(table.source_location.lineno).end_pos + 1
10
+ @last = table_end(table)
11
+ end
12
+
13
+ def span(cell)
14
+ match = locate(cell.source)
15
+ first = @cursor + match.begin(0)
16
+ @cursor += match.end(0)
17
+ positions = match[0].length.times.map { |index| [first + index, first + index + 1] }
18
+ AsciiDocInline.new(match[0], positions).span
19
+ end
20
+
21
+ private
22
+
23
+ def table_end(table)
24
+ lines = @buffer.source_lines
25
+ line = table.source_location.lineno
26
+ delimiter = lines.fetch(line - 1)
27
+ last = (line...lines.length).find { |index| lines[index] == delimiter }
28
+ last ? @buffer.line_range(last + 1).begin_pos : @buffer.source.length
29
+ end
30
+
31
+ def locate(text)
32
+ match = pattern(text).match(@buffer.source[@cursor...@last])
33
+ return match if match
34
+
35
+ raise RuboCop::ValidationError, "Cannot map AsciiDoc table cell in #{@buffer.name}: #{text.inspect}"
36
+ end
37
+
38
+ def pattern(text)
39
+ Regexp.new(text.each_char.map do |character|
40
+ case character
41
+ when '|' then /\\?\|/.source
42
+ when '"' then /"{1,2}/.source
43
+ else Regexp.escape(character)
44
+ end
45
+ end.join)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -41,6 +41,12 @@ module RuboCop
41
41
  end.uniq.first(3)
42
42
  end
43
43
 
44
+ def correct?(word)
45
+ return @verdicts[word] if @verdicts.key?(word)
46
+
47
+ @verdicts[word] = @checkers.any? { |checker| checker.correct?(word) }
48
+ end
49
+
44
50
  private
45
51
 
46
52
  def tokens(checker, text)
@@ -128,12 +134,6 @@ module RuboCop
128
134
 
129
135
  word
130
136
  end
131
-
132
- def correct?(word)
133
- return @verdicts[word] if @verdicts.key?(word)
134
-
135
- @verdicts[word] = @checkers.any? { |checker| checker.correct?(word) }
136
- end
137
137
  end
138
138
  end
139
139
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kotoshu/algorithms/edit_distance'
4
+
5
+ module RuboCop
6
+ module Spellcheck
7
+ # Restricts unsafe corrections to single-edit lowercase and title-case typos.
8
+ class Correction
9
+ WORD = /\A[\p{L}\p{M}]+(?:['’][\p{L}\p{M}]+)*\z/
10
+
11
+ def self.standalone?(text, position, length)
12
+ before = text[0...position]
13
+ after = text[(position + length)..]
14
+ !before.match?(/[\p{L}\p{M}\p{N}_](?:[.-])?\z/) &&
15
+ !after.match?(/\A(?:[.-])?[\p{L}\p{M}\p{N}_]/)
16
+ end
17
+
18
+ def self.whole_word?(positions, position, length)
19
+ first = positions.fetch(position).first
20
+ last = positions.fetch(position + length - 1).last
21
+ (position.zero? || positions[position - 1].last <= first) &&
22
+ (position + length == positions.length || positions[position + length].first >= last)
23
+ end
24
+
25
+ def initialize(word, suggestions)
26
+ @word = word
27
+ @suggestions = suggestions
28
+ end
29
+
30
+ def replacement
31
+ replacement_word
32
+ end
33
+
34
+ def replacement_word
35
+ return if @suggestions.empty? || !correctable_case?
36
+
37
+ suggestion = @suggestions.first.downcase
38
+ return unless WORD.match?(suggestion)
39
+
40
+ return unless Kotoshu::Algorithms::EditDistance.distance_with_threshold(@word.downcase, suggestion, 1) == 1
41
+
42
+ @word == @word.downcase ? suggestion : suggestion.capitalize
43
+ end
44
+
45
+ def replacement_in(source, first, last, checker:)
46
+ word = replacement_word
47
+ return unless word && checker.correct?(word)
48
+
49
+ replacement if Correction.standalone?(source, first, last - first)
50
+ end
51
+
52
+ def message
53
+ message = "Misspelled word: `#{@word}`."
54
+ return message if @suggestions.empty?
55
+
56
+ "#{message} Did you mean #{@suggestions.map { |suggestion| "`#{suggestion}`" }.join(', ')}?"
57
+ end
58
+
59
+ private
60
+
61
+ def correctable_case?
62
+ @word == @word.downcase || (@word == @word.capitalize && @word != @word.upcase)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -7,12 +7,47 @@ module RuboCop
7
7
  module Spellcheck
8
8
  # Reads YAML string values without constructing application objects.
9
9
  class FrontMatter
10
+ # Retains scalar syntax at extraction time rather than in the reporting code.
11
+ class Span < Spellcheck::Span
12
+ def initialize(text, positions, style)
13
+ super(text, positions)
14
+ @style = style
15
+ end
16
+
17
+ def correction(word, suggestions)
18
+ Correction.new(word, suggestions, @style)
19
+ end
20
+ end
21
+
22
+ # Applies YAML quoting and scalar-type constraints to a spelling replacement.
23
+ class Correction < Spellcheck::Correction
24
+ def initialize(word, suggestions, style)
25
+ super(word, suggestions)
26
+ @style = style
27
+ end
28
+
29
+ def replacement
30
+ suggestion = super
31
+ return unless suggestion
32
+
33
+ case @style
34
+ when Psych::Nodes::Scalar::SINGLE_QUOTED
35
+ suggestion.gsub("'", "''")
36
+ when Psych::Nodes::Scalar::PLAIN
37
+ suggestion if Psych::ScalarScanner.new(Psych::ClassLoader.new).tokenize(suggestion).is_a?(String)
38
+ else
39
+ suggestion
40
+ end
41
+ end
42
+ end
43
+
10
44
  HEADER = /\A(?:\uFEFF)?---[ \t]*\n.*?^(?:---|\.\.\.)[ \t]*(?:\n|\z)/m
11
45
  ESCAPE = /\\(?:U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|.)/m
12
46
 
13
- def initialize(buffer)
47
+ def initialize(buffer, config: {})
14
48
  @buffer = buffer
15
49
  @scanner = Psych::ScalarScanner.new(Psych::ClassLoader.new)
50
+ @paths = FrontMatterPaths.new(config)
16
51
  end
17
52
 
18
53
  def each_span
@@ -33,14 +68,21 @@ module RuboCop
33
68
  raise RuboCop::ValidationError, "Invalid YAML front matter in #{@buffer.name}: #{e.message}"
34
69
  end
35
70
 
36
- def string_values(node, &block)
71
+ def string_values(node, path = [], &block)
37
72
  case node
38
73
  when Psych::Nodes::Mapping
39
- node.children.each_slice(2) { |_key, value| string_values(value, &block) }
74
+ mapping_values(node, path, &block)
40
75
  when Psych::Nodes::Sequence
41
- node.children.each { |value| string_values(value, &block) }
76
+ node.children.each { |value| string_values(value, path, &block) }
42
77
  when Psych::Nodes::Scalar
43
- yield node if string_scalar?(node)
78
+ yield node if @paths.include?(path) && string_scalar?(node)
79
+ end
80
+ end
81
+
82
+ def mapping_values(node, path, &block)
83
+ node.children.each_slice(2) do |key, value|
84
+ name = key.is_a?(Psych::Nodes::Scalar) ? key.value : nil
85
+ string_values(value, [*path, name], &block)
44
86
  end
45
87
  end
46
88
 
@@ -60,7 +102,8 @@ module RuboCop
60
102
  last = position(node.end_line, node.end_column)
61
103
  raw = @buffer.source[first...last]
62
104
  prefix = raw[/\A(?:(?:&\S+|!\S+)[ \t]+)+/].to_s.length
63
- map_pieces(raw[prefix..], node.style, first + prefix)
105
+ style = node.style
106
+ Span.new(*map_pieces(raw[prefix..], style, first + prefix), style)
64
107
  end
65
108
 
66
109
  def map_pieces(raw, style, first)
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Spellcheck
5
+ # Selects YAML values by exact dotted paths, including selected descendants.
6
+ class FrontMatterPaths
7
+ def initialize(config)
8
+ @include = paths(config.fetch('Include', nil), 'Include', nullable: true)
9
+ @exclude = paths(config.fetch('Exclude', []), 'Exclude')
10
+ end
11
+
12
+ def include?(path)
13
+ (@include.nil? || matches?(@include, path)) && !matches?(@exclude, path)
14
+ end
15
+
16
+ private
17
+
18
+ def paths(value, setting, nullable: false)
19
+ return if nullable && value.nil?
20
+ return value.map { |path| path.split('.') } if valid?(value)
21
+
22
+ raise RuboCop::ValidationError,
23
+ "Spellcheck/Spelling Markdown.FrontMatter.#{setting} must be a list of nonempty dotted paths."
24
+ end
25
+
26
+ def valid?(value)
27
+ value.is_a?(Array) && value.all? do |path|
28
+ path.is_a?(String) && !path.empty? && path.split('.', -1).none?(&:empty?)
29
+ end
30
+ end
31
+
32
+ def matches?(patterns, path)
33
+ patterns.any? { |pattern| path.first(pattern.length) == pattern }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -11,10 +11,20 @@ module RuboCop
11
11
  PIECE = /#{ENTITY}|\\[[:punct:]]|./m
12
12
  LIQUID = /\{\{.*?\}\}|\{%.*?%\}/m
13
13
 
14
+ attr_reader :buffer, :directives
15
+
14
16
  def self.file?(path)
15
17
  path && EXTENSIONS.include?(File.extname(path).downcase)
16
18
  end
17
19
 
20
+ def self.decode(piece)
21
+ return piece.delete_prefix('\\') if piece.start_with?('\\')
22
+ return piece unless ENTITY.match?(piece)
23
+
24
+ document = Commonmarker.parse(piece, options: { parse: { smart: false } })
25
+ document.walk.select { |node| node.type == :text }.map(&:string_content).join
26
+ end
27
+
18
28
  def initialize(buffer)
19
29
  @buffer = buffer
20
30
  @lines = buffer.source.lines
@@ -23,10 +33,7 @@ module RuboCop
23
33
  def each_span
24
34
  return enum_for(__method__) unless block_given?
25
35
 
26
- document = Commonmarker.parse(parser_source, options: {
27
- parse: { smart: false },
28
- extension: { shortcodes: false, footnotes: true }
29
- })
36
+ document = parsed_document
30
37
  document.walk do |node|
31
38
  next unless node.type == :text
32
39
 
@@ -35,8 +42,29 @@ module RuboCop
35
42
  end
36
43
  end
37
44
 
45
+ def each_ruby_span(version, config:, &)
46
+ parsed_document.walk do |node|
47
+ next unless node.type == :code_block && node.fence_info.to_s.split.first == 'ruby'
48
+
49
+ MarkdownRuby.new(self, node, version, config: config).each_span(&)
50
+ end
51
+ end
52
+
53
+ def parser_lines
54
+ @parser_lines ||= parser_source.lines(chomp: true)
55
+ end
56
+
38
57
  private
39
58
 
59
+ def parsed_document
60
+ @parsed_document ||= Commonmarker.parse(parser_source, options: {
61
+ parse: { smart: false },
62
+ extension: { shortcodes: false, footnotes: true }
63
+ }).tap do |document|
64
+ @directives = MarkdownDirectives.new(@buffer, document)
65
+ end
66
+ end
67
+
40
68
  def parser_source
41
69
  without_front_matter.gsub(LIQUID) do |expression|
42
70
  expression.gsub(/[^\n]/) { |character| 'x' * character.bytesize }
@@ -62,12 +90,12 @@ module RuboCop
62
90
  end
63
91
 
64
92
  def decode_text(raw, first)
65
- raw = without_liquid(raw)
93
+ raw = without_liquid(@directives.mask(raw, first))
66
94
  text = +''
67
95
  positions = []
68
96
  raw.to_enum(:scan, PIECE).each do
69
97
  match = Regexp.last_match
70
- decoded = decode(match[0])
98
+ decoded = Markdown.decode(match[0])
71
99
  text << decoded
72
100
  decoded.length.times { positions << [first + match.begin(0), first + match.end(0)] }
73
101
  end
@@ -77,14 +105,6 @@ module RuboCop
77
105
  def without_liquid(raw)
78
106
  raw.gsub(LIQUID) { |expression| expression.gsub(/[^\n]/, ' ') }
79
107
  end
80
-
81
- def decode(piece)
82
- return piece.delete_prefix('\\') if piece.start_with?('\\')
83
- return piece unless ENTITY.match?(piece)
84
-
85
- document = Commonmarker.parse(piece, options: { parse: { smart: false } })
86
- document.walk.select { |node| node.type == :text }.map(&:string_content).join
87
- end
88
108
  end
89
109
  end
90
110
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Spellcheck
5
+ # Recognizes spelling directives only in Markdown HTML comment nodes.
6
+ class MarkdownDirectives
7
+ COMMENT = /<!--(.*?)-->/m
8
+ DIRECTIVE = /\A\s*rubocop:(disable|enable|todo)\s+(.+?)\s*\z/m
9
+
10
+ def initialize(buffer, document)
11
+ @buffer = buffer
12
+ @lines = buffer.source.lines
13
+ @disabled = Set.new
14
+ events = document.walk.filter_map { |node| comment_event(node) }.flatten(1).sort_by(&:first)
15
+ collect_lines(events)
16
+ end
17
+
18
+ def mask(raw, first)
19
+ line = @buffer.decompose_position(first).first
20
+ raw.lines.map.with_index do |text, index|
21
+ @disabled.include?(line + index) ? text.gsub(/[^\n]/, ' ') : text
22
+ end.join
23
+ end
24
+
25
+ def disabled?(line)
26
+ @disabled.include?(line)
27
+ end
28
+
29
+ private
30
+
31
+ def comment_event(node)
32
+ return unless %i[html_inline html_block].include?(node.type)
33
+
34
+ raw, first = node_source(node)
35
+ return unless raw.gsub(COMMENT, '').strip.empty?
36
+
37
+ raw.to_enum(:scan, COMMENT).filter_map { event(Regexp.last_match, first) }
38
+ end
39
+
40
+ def node_source(node)
41
+ location = node.source_position
42
+ first = position(location.fetch(:start_line), location.fetch(:start_column) - 1)
43
+ last = position(location.fetch(:end_line), location.fetch(:end_column))
44
+ [@buffer.source[first...last], first]
45
+ end
46
+
47
+ def event(match, first)
48
+ directive = DIRECTIVE.match(match[1])
49
+ return unless directive && relevant?(directive[2])
50
+
51
+ line, column = @buffer.decompose_position(first + match.begin(0))
52
+ prefix = @lines.fetch(line - 1)[0...column]
53
+ [line, directive[1], !prefix.strip.empty?]
54
+ end
55
+
56
+ def position(line, column)
57
+ @buffer.line_range(line).begin_pos + @lines.fetch(line - 1).byteslice(0, column).length
58
+ end
59
+
60
+ def relevant?(names)
61
+ names.split(/\s*--\s*/, 2).first.split(/[,\s]+/).intersect?(%w[all Spellcheck/Spelling])
62
+ end
63
+
64
+ def collect_lines(events)
65
+ events = events.group_by(&:first)
66
+ @lines.each_index do |index|
67
+ line = index + 1
68
+ @disabled << line if @block_disabled
69
+ events.fetch(line, []).each { |event| apply_event(*event) }
70
+ end
71
+ end
72
+
73
+ def apply_event(line, action, inline)
74
+ if inline || action == 'todo'
75
+ @disabled << line unless action == 'enable'
76
+ else
77
+ @block_disabled = action == 'disable'
78
+ @disabled << line if @block_disabled
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Spellcheck
5
+ # Maps explicitly tagged Ruby fences back through Markdown container prefixes.
6
+ class MarkdownRuby
7
+ # Retains Ruby string escaping and report-only identifier corrections.
8
+ class Span < Spellcheck::Span
9
+ def initialize(original, text, positions)
10
+ super(text, positions)
11
+ @original = original
12
+ end
13
+
14
+ def correction(word, suggestions)
15
+ @original.correction(word, suggestions)
16
+ end
17
+ end
18
+
19
+ def initialize(markdown, node, version, config:)
20
+ @buffer = markdown.buffer
21
+ @node = node
22
+ @config = config
23
+ @directives = markdown.directives
24
+ @parser_lines = markdown.parser_lines
25
+ @positions = []
26
+ @source = RuboCop::ProcessedSource.new(mapped_source, version, "#{@buffer.name}.rb")
27
+ @source.config = RuboCop::Config.new
28
+ @source.registry = RuboCop::Cop::Registry.global
29
+ end
30
+
31
+ def each_span
32
+ return enum_for(__method__) unless block_given?
33
+ return unless @source.valid_syntax?
34
+
35
+ RubyCode.new(@source, config: @config).each_code_span do |span|
36
+ yield mapped_span(span)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def mapped_span(span)
43
+ positions = span.positions.map { |first, last| [@positions.fetch(first), @positions.fetch(last - 1) + 1] }
44
+ Span.new(span, enabled_text(span, positions), positions)
45
+ end
46
+
47
+ def enabled_text(span, positions)
48
+ span.text.each_char.with_index.map do |character, index|
49
+ disabled?(span.positions[index].first, positions[index].first) ? ' ' : character
50
+ end.join
51
+ end
52
+
53
+ def mapped_source
54
+ first = @node.source_position.fetch(:start_line) + 1
55
+ @node.string_content.lines.each_with_index.map do |line, index|
56
+ mapped_line(line, first + index)
57
+ end.join
58
+ end
59
+
60
+ def mapped_line(line, number)
61
+ content = line.delete_suffix("\n")
62
+ suffix = mapped_suffix(content, number)
63
+ prefix = @parser_lines.fetch(number - 1).bytesize - suffix.bytesize
64
+ raw = @buffer.source_line(number).byteslice(prefix..)
65
+ indentation = content.delete_suffix(suffix)
66
+ offset = @buffer.line_range(number).end_pos - raw.length
67
+ map_positions(indentation, raw, offset, number)
68
+ "#{indentation}#{raw}\n"
69
+ end
70
+
71
+ def mapped_suffix(content, number)
72
+ parsed = @parser_lines.fetch(number - 1)
73
+ suffix = parsed.end_with?(content) ? content : content.lstrip
74
+ unless parsed.end_with?(suffix)
75
+ raise RuboCop::ValidationError, "Cannot map Ruby fence in #{@buffer.name}:#{number}."
76
+ end
77
+
78
+ suffix
79
+ end
80
+
81
+ def map_positions(indentation, raw, offset, number)
82
+ indentation.length.times { @positions << [offset - 1, @buffer.line_range(number).begin_pos].max }
83
+ (raw.length + 1).times { |column| @positions << (offset + column) }
84
+ end
85
+
86
+ def disabled?(ruby_position, original_position)
87
+ line = @source.buffer.decompose_position(ruby_position).first
88
+ original_line = @buffer.decompose_position(original_position).first
89
+ @directives.disabled?(original_line) ||
90
+ !@source.comment_config.cop_enabled_at_line?('Spellcheck/Spelling', line)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -11,7 +11,7 @@ module RuboCop
11
11
  name: 'rubocop-spellcheck',
12
12
  version: VERSION,
13
13
  homepage: 'https://github.com/dblock/rubocop-spellcheck',
14
- description: 'Spellcheck Ruby comments, Markdown prose, and plain text.'
14
+ description: 'Spellcheck Ruby comments, strings, identifiers, and documentation.'
15
15
  )
16
16
  end
17
17
 
@@ -5,7 +5,7 @@ module RuboCop
5
5
  # Prose files use the non-Ruby callback, with an untouched source buffer.
6
6
  module ProseSource
7
7
  def self.file?(path)
8
- Markdown.file?(path) || text_file?(path)
8
+ Markdown.file?(path) || RDoc.file?(path) || AsciiDoc.file?(path) || text_file?(path)
9
9
  end
10
10
 
11
11
  def self.text_file?(path)