theme-check 0.10.2 → 1.3.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.
- checksums.yaml +4 -4
- data/.github/workflows/theme-check.yml +2 -6
- data/CHANGELOG.md +51 -0
- data/CONTRIBUTING.md +1 -1
- data/README.md +39 -0
- data/RELEASING.md +34 -2
- data/bin/theme-check +29 -0
- data/bin/theme-check-language-server +29 -0
- data/config/default.yml +46 -3
- data/config/nothing.yml +11 -0
- data/config/theme_app_extension.yml +168 -0
- data/data/shopify_liquid/objects.yml +2 -0
- data/docs/checks/app_block_valid_tags.md +40 -0
- data/docs/checks/asset_size_app_block_css.md +52 -0
- data/docs/checks/asset_size_app_block_javascript.md +57 -0
- data/docs/checks/asset_size_css_stylesheet_tag.md +50 -0
- data/docs/checks/deprecate_bgsizes.md +66 -0
- data/docs/checks/deprecate_lazysizes.md +61 -0
- data/docs/checks/liquid_tag.md +2 -2
- data/docs/checks/missing_template.md +25 -0
- data/docs/checks/pagination_size.md +44 -0
- data/docs/checks/template_length.md +12 -2
- data/docs/checks/undefined_object.md +5 -0
- data/lib/theme_check/analyzer.rb +25 -21
- data/lib/theme_check/asset_file.rb +3 -15
- data/lib/theme_check/bug.rb +3 -1
- data/lib/theme_check/check.rb +26 -4
- data/lib/theme_check/checks/app_block_valid_tags.rb +36 -0
- data/lib/theme_check/checks/asset_size_app_block_css.rb +44 -0
- data/lib/theme_check/checks/asset_size_app_block_javascript.rb +44 -0
- data/lib/theme_check/checks/asset_size_css.rb +11 -74
- data/lib/theme_check/checks/asset_size_css_stylesheet_tag.rb +24 -0
- data/lib/theme_check/checks/asset_size_javascript.rb +11 -37
- data/lib/theme_check/checks/convert_include_to_render.rb +3 -1
- data/lib/theme_check/checks/deprecate_bgsizes.rb +14 -0
- data/lib/theme_check/checks/deprecate_lazysizes.rb +16 -0
- data/lib/theme_check/checks/img_lazy_loading.rb +2 -7
- data/lib/theme_check/checks/img_width_and_height.rb +3 -3
- data/lib/theme_check/checks/liquid_tag.rb +2 -2
- data/lib/theme_check/checks/missing_template.rb +21 -5
- data/lib/theme_check/checks/pagination_size.rb +65 -0
- data/lib/theme_check/checks/parser_blocking_javascript.rb +1 -1
- data/lib/theme_check/checks/remote_asset.rb +4 -2
- data/lib/theme_check/checks/space_inside_braces.rb +27 -7
- data/lib/theme_check/checks/template_length.rb +18 -4
- data/lib/theme_check/checks/undefined_object.rb +1 -1
- data/lib/theme_check/checks/valid_html_translation.rb +1 -1
- data/lib/theme_check/checks.rb +11 -1
- data/lib/theme_check/cli.rb +52 -15
- data/lib/theme_check/config.rb +56 -10
- data/lib/theme_check/corrector.rb +4 -0
- data/lib/theme_check/exceptions.rb +29 -27
- data/lib/theme_check/file_system_storage.rb +12 -0
- data/lib/theme_check/html_check.rb +1 -0
- data/lib/theme_check/html_node.rb +37 -16
- data/lib/theme_check/html_visitor.rb +17 -3
- data/lib/theme_check/json_check.rb +2 -2
- data/lib/theme_check/json_file.rb +2 -29
- data/lib/theme_check/json_printer.rb +26 -0
- data/lib/theme_check/language_server/constants.rb +8 -0
- data/lib/theme_check/language_server/document_link_engine.rb +40 -4
- data/lib/theme_check/language_server/handler.rb +6 -2
- data/lib/theme_check/language_server/server.rb +13 -2
- data/lib/theme_check/liquid_check.rb +0 -12
- data/lib/theme_check/node.rb +6 -4
- data/lib/theme_check/offense.rb +56 -3
- data/lib/theme_check/parsing_helpers.rb +7 -4
- data/lib/theme_check/position.rb +98 -14
- data/lib/theme_check/regex_helpers.rb +20 -0
- data/lib/theme_check/tags.rb +62 -8
- data/lib/theme_check/template.rb +3 -32
- data/lib/theme_check/theme.rb +2 -0
- data/lib/theme_check/theme_file.rb +40 -0
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check.rb +16 -0
- data/theme-check.gemspec +1 -1
- metadata +26 -7
- data/bin/liquid-server +0 -4
@@ -1,18 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
require "
|
2
|
+
require "nokogiri"
|
3
3
|
require "forwardable"
|
4
4
|
|
5
5
|
module ThemeCheck
|
6
6
|
class HtmlVisitor
|
7
|
+
include RegexHelpers
|
7
8
|
attr_reader :checks
|
8
9
|
|
9
10
|
def initialize(checks)
|
10
11
|
@checks = checks
|
12
|
+
@placeholder_values = []
|
11
13
|
end
|
12
14
|
|
13
15
|
def visit_template(template)
|
14
16
|
doc = parse(template)
|
15
|
-
visit(HtmlNode.new(doc, template))
|
17
|
+
visit(HtmlNode.new(doc, template, @placeholder_values))
|
16
18
|
rescue ArgumentError => e
|
17
19
|
call_checks(:on_parse_error, e, template)
|
18
20
|
end
|
@@ -20,7 +22,19 @@ module ThemeCheck
|
|
20
22
|
private
|
21
23
|
|
22
24
|
def parse(template)
|
23
|
-
|
25
|
+
parseable_source = +template.source.clone
|
26
|
+
|
27
|
+
# Replace all liquid tags with {%#{i}######%} to prevent the HTML
|
28
|
+
# parser from freaking out. We transparently replace those placeholders in
|
29
|
+
# HtmlNode.
|
30
|
+
matches(parseable_source, LIQUID_TAG_OR_VARIABLE).each do |m|
|
31
|
+
value = m[0]
|
32
|
+
@placeholder_values.push(value)
|
33
|
+
key = (@placeholder_values.size - 1).to_s
|
34
|
+
parseable_source[m.begin(0)...m.end(0)] = "{%#{key.ljust(m.end(0) - m.begin(0) - 4, '#')}%}"
|
35
|
+
end
|
36
|
+
|
37
|
+
Nokogiri::HTML5.fragment(parseable_source, max_tree_depth: 400, max_attributes: 400)
|
24
38
|
end
|
25
39
|
|
26
40
|
def visit(node)
|
@@ -4,8 +4,8 @@ module ThemeCheck
|
|
4
4
|
class JsonCheck < Check
|
5
5
|
extend ChecksTracking
|
6
6
|
|
7
|
-
def add_offense(message, markup: nil, line_number: nil, template: nil)
|
8
|
-
offenses << Offense.new(check: self, message: message, markup: markup, line_number: line_number, template: template)
|
7
|
+
def add_offense(message, markup: nil, line_number: nil, template: nil, &block)
|
8
|
+
offenses << Offense.new(check: self, message: message, markup: markup, line_number: line_number, template: template, correction: block)
|
9
9
|
end
|
10
10
|
end
|
11
11
|
end
|
@@ -1,29 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require "json"
|
3
|
-
require "pathname"
|
4
3
|
|
5
4
|
module ThemeCheck
|
6
|
-
class JsonFile
|
5
|
+
class JsonFile < ThemeFile
|
7
6
|
def initialize(relative_path, storage)
|
8
|
-
|
9
|
-
@storage = storage
|
7
|
+
super
|
10
8
|
@loaded = false
|
11
9
|
@content = nil
|
12
10
|
@parser_error = nil
|
13
11
|
end
|
14
12
|
|
15
|
-
def path
|
16
|
-
@storage.path(@relative_path)
|
17
|
-
end
|
18
|
-
|
19
|
-
def relative_path
|
20
|
-
@relative_pathname ||= Pathname.new(@relative_path)
|
21
|
-
end
|
22
|
-
|
23
|
-
def source
|
24
|
-
@source ||= @storage.read(@relative_path)
|
25
|
-
end
|
26
|
-
|
27
13
|
def content
|
28
14
|
load!
|
29
15
|
@content
|
@@ -34,23 +20,10 @@ module ThemeCheck
|
|
34
20
|
@parser_error
|
35
21
|
end
|
36
22
|
|
37
|
-
def name
|
38
|
-
relative_path.sub_ext('').to_s
|
39
|
-
end
|
40
|
-
|
41
23
|
def json?
|
42
24
|
true
|
43
25
|
end
|
44
26
|
|
45
|
-
def liquid?
|
46
|
-
false
|
47
|
-
end
|
48
|
-
|
49
|
-
def ==(other)
|
50
|
-
other.is_a?(JsonFile) && relative_path == other.relative_path
|
51
|
-
end
|
52
|
-
alias_method :eql?, :==
|
53
|
-
|
54
27
|
private
|
55
28
|
|
56
29
|
def load!
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module ThemeCheck
|
5
|
+
class JsonPrinter
|
6
|
+
def print(offenses)
|
7
|
+
json = offenses_by_path(offenses)
|
8
|
+
puts JSON.dump(json)
|
9
|
+
end
|
10
|
+
|
11
|
+
def offenses_by_path(offenses)
|
12
|
+
offenses
|
13
|
+
.map(&:to_h)
|
14
|
+
.group_by { |offense| offense[:path] }
|
15
|
+
.map do |(path, path_offenses)|
|
16
|
+
{
|
17
|
+
path: path,
|
18
|
+
offenses: path_offenses.map { |offense| offense.filter { |k, _v| k != :path } },
|
19
|
+
errorCount: path_offenses.count { |offense| offense[:severity] == Check::SEVERITY_VALUES[:error] },
|
20
|
+
suggestionCount: path_offenses.count { |offense| offense[:severity] == Check::SEVERITY_VALUES[:suggestion] },
|
21
|
+
styleCount: path_offenses.count { |offense| offense[:severity] == Check::SEVERITY_VALUES[:style] },
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -10,5 +10,13 @@ module ThemeCheck
|
|
10
10
|
^\s*render\s+'(?<partial>[^']*)'|
|
11
11
|
^\s*render\s+"(?<partial>[^"]*)"
|
12
12
|
}mix
|
13
|
+
ASSET_INCLUDE = %r{
|
14
|
+
\{\%-?\s*'(?<partial>[^']*)'\s*\|\s*asset_url|
|
15
|
+
\{\%-?\s*"(?<partial>[^"]*)"\s*\|\s*asset_url|
|
16
|
+
|
17
|
+
# in liquid tags the whole line is white space until the asset partial
|
18
|
+
^\s*'(?<partial>[^']*)'\s*\|\s*asset_url|
|
19
|
+
^\s*"(?<partial>[^"]*)"\s*\|\s*asset_url
|
20
|
+
}mix
|
13
21
|
end
|
14
22
|
end
|
@@ -13,7 +13,7 @@ module ThemeCheck
|
|
13
13
|
def document_links(relative_path)
|
14
14
|
buffer = @storage.read(relative_path)
|
15
15
|
return [] unless buffer
|
16
|
-
matches(buffer, PARTIAL_RENDER).map do |match|
|
16
|
+
snippet_matches = matches(buffer, PARTIAL_RENDER).map do |match|
|
17
17
|
start_line, start_character = from_index_to_row_column(
|
18
18
|
buffer,
|
19
19
|
match.begin(:partial),
|
@@ -25,7 +25,7 @@ module ThemeCheck
|
|
25
25
|
)
|
26
26
|
|
27
27
|
{
|
28
|
-
target:
|
28
|
+
target: snippet_link(match[:partial]),
|
29
29
|
range: {
|
30
30
|
start: {
|
31
31
|
line: start_line,
|
@@ -38,10 +38,46 @@ module ThemeCheck
|
|
38
38
|
},
|
39
39
|
}
|
40
40
|
end
|
41
|
+
asset_matches = matches(buffer, ASSET_INCLUDE).map do |match|
|
42
|
+
start_line, start_character = from_index_to_row_column(
|
43
|
+
buffer,
|
44
|
+
match.begin(:partial),
|
45
|
+
)
|
46
|
+
|
47
|
+
end_line, end_character = from_index_to_row_column(
|
48
|
+
buffer,
|
49
|
+
match.end(:partial)
|
50
|
+
)
|
51
|
+
|
52
|
+
{
|
53
|
+
target: asset_link(match[:partial]),
|
54
|
+
range: {
|
55
|
+
start: {
|
56
|
+
line: start_line,
|
57
|
+
character: start_character,
|
58
|
+
},
|
59
|
+
end: {
|
60
|
+
line: end_line,
|
61
|
+
character: end_character,
|
62
|
+
},
|
63
|
+
},
|
64
|
+
}
|
65
|
+
end
|
66
|
+
snippet_matches + asset_matches
|
67
|
+
end
|
68
|
+
|
69
|
+
def snippet_link(partial)
|
70
|
+
file_link('snippets', partial, '.liquid')
|
41
71
|
end
|
42
72
|
|
43
|
-
def
|
44
|
-
|
73
|
+
def asset_link(partial)
|
74
|
+
file_link('assets', partial, '')
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def file_link(directory, partial, extension)
|
80
|
+
"file://#{@storage.path(directory + '/' + partial + extension)}"
|
45
81
|
end
|
46
82
|
end
|
47
83
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require "benchmark"
|
3
|
+
require "uri"
|
4
|
+
require "cgi"
|
3
5
|
|
4
6
|
module ThemeCheck
|
5
7
|
module LanguageServer
|
@@ -98,8 +100,10 @@ module ThemeCheck
|
|
98
100
|
path_from_uri(params.dig('textDocument', 'uri'))
|
99
101
|
end
|
100
102
|
|
101
|
-
def path_from_uri(
|
102
|
-
|
103
|
+
def path_from_uri(uri_string)
|
104
|
+
return if uri_string.nil?
|
105
|
+
uri = URI(uri_string)
|
106
|
+
CGI.unescape(uri.path)
|
103
107
|
end
|
104
108
|
|
105
109
|
def relative_path_from_text_document_uri(params)
|
@@ -52,8 +52,19 @@ module ThemeCheck
|
|
52
52
|
response_body = JSON.dump(response)
|
53
53
|
log(JSON.pretty_generate(response)) if $DEBUG
|
54
54
|
|
55
|
-
|
56
|
-
|
55
|
+
# Because programming is fun,
|
56
|
+
#
|
57
|
+
# Ruby on Windows turns \n into \r\n. Which means that \r\n
|
58
|
+
# gets turned into \r\r\n. Which means that the protocol
|
59
|
+
# breaks on windows unless we turn STDOUT into binary mode and
|
60
|
+
# set the encoding manually (yuk!) or we do this little hack
|
61
|
+
# here and put \n which gets transformed into \r\n on windows
|
62
|
+
# only...
|
63
|
+
#
|
64
|
+
# Hours wasted: 8.
|
65
|
+
eol = Gem.win_platform? ? "\n" : "\r\n"
|
66
|
+
@out.write("Content-Length: #{response_body.bytesize}#{eol}")
|
67
|
+
@out.write(eol)
|
57
68
|
@out.write(response_body)
|
58
69
|
@out.flush
|
59
70
|
end
|
@@ -5,17 +5,5 @@ module ThemeCheck
|
|
5
5
|
class LiquidCheck < Check
|
6
6
|
extend ChecksTracking
|
7
7
|
include ParsingHelpers
|
8
|
-
|
9
|
-
# TODO: remove this once all regex checks are migrate to HtmlCheck# TODO: remove this once all regex checks are migrate to HtmlCheck
|
10
|
-
TAG = /#{Liquid::TagStart}.*?#{Liquid::TagEnd}/om
|
11
|
-
VARIABLE = /#{Liquid::VariableStart}.*?#{Liquid::VariableEnd}/om
|
12
|
-
START_OR_END_QUOTE = /(^['"])|(['"]$)/
|
13
|
-
QUOTED_LIQUID_ATTRIBUTE = %r{
|
14
|
-
'(?:#{TAG}|#{VARIABLE}|[^'])*'| # any combination of tag/variable or non straight quote inside straight quotes
|
15
|
-
"(?:#{TAG}|#{VARIABLE}|[^"])*" # any combination of tag/variable or non double quotes inside double quotes
|
16
|
-
}omix
|
17
|
-
ATTR = /[a-z0-9-]+/i
|
18
|
-
HTML_ATTRIBUTE = /#{ATTR}(?:=#{QUOTED_LIQUID_ATTRIBUTE})?/omix
|
19
|
-
HTML_ATTRIBUTES = /(?:#{HTML_ATTRIBUTE}|\s)*/omix
|
20
8
|
end
|
21
9
|
end
|
data/lib/theme_check/node.rb
CHANGED
@@ -22,9 +22,7 @@ module ThemeCheck
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def markup=(markup)
|
25
|
-
if
|
26
|
-
@value.raw = markup
|
27
|
-
elsif @value.instance_variable_defined?(:@markup)
|
25
|
+
if @value.instance_variable_defined?(:@markup)
|
28
26
|
@value.instance_variable_set(:@markup, markup)
|
29
27
|
end
|
30
28
|
end
|
@@ -127,7 +125,11 @@ module ThemeCheck
|
|
127
125
|
end
|
128
126
|
|
129
127
|
def position
|
130
|
-
@position ||= Position.new(
|
128
|
+
@position ||= Position.new(
|
129
|
+
markup,
|
130
|
+
template&.source,
|
131
|
+
line_number_1_indexed: line_number
|
132
|
+
)
|
131
133
|
end
|
132
134
|
|
133
135
|
def start_index
|
data/lib/theme_check/offense.rb
CHANGED
@@ -1,11 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module ThemeCheck
|
3
3
|
class Offense
|
4
|
+
include PositionHelper
|
5
|
+
|
4
6
|
MAX_SOURCE_EXCERPT_SIZE = 120
|
5
7
|
|
6
8
|
attr_reader :check, :message, :template, :node, :markup, :line_number, :correction
|
7
9
|
|
8
|
-
def initialize(
|
10
|
+
def initialize(
|
11
|
+
check:, # instance of a ThemeCheck::Check
|
12
|
+
message: nil, # error message for the offense
|
13
|
+
template: nil, # Template
|
14
|
+
node: nil, # Node or HtmlNode
|
15
|
+
markup: nil, # string
|
16
|
+
line_number: nil, # line number of the error (1-indexed)
|
17
|
+
# node_markup_offset is the index inside node.markup to start
|
18
|
+
# looking for markup :mindblow:.
|
19
|
+
# This is so we can accurately highlight node substrings.
|
20
|
+
# e.g. if we have the following scenario in which we
|
21
|
+
# want to highlight the middle comma
|
22
|
+
# * node.markup == "replace ',',', '"
|
23
|
+
# * markup == ","
|
24
|
+
# Then we need some way of telling our Position class to start
|
25
|
+
# looking for the second comma. This is done with node_markup_offset.
|
26
|
+
# More context can be found in #376.
|
27
|
+
node_markup_offset: 0,
|
28
|
+
correction: nil # block
|
29
|
+
)
|
9
30
|
@check = check
|
10
31
|
@correction = correction
|
11
32
|
|
@@ -39,7 +60,13 @@ module ThemeCheck
|
|
39
60
|
@node.line_number
|
40
61
|
end
|
41
62
|
|
42
|
-
@position = Position.new(
|
63
|
+
@position = Position.new(
|
64
|
+
@markup,
|
65
|
+
@template&.source,
|
66
|
+
line_number_1_indexed: @line_number,
|
67
|
+
node_markup_offset: node_markup_offset,
|
68
|
+
node_markup: node&.markup
|
69
|
+
)
|
43
70
|
end
|
44
71
|
|
45
72
|
def source_excerpt
|
@@ -103,8 +130,13 @@ module ThemeCheck
|
|
103
130
|
tokens.join(":") if tokens.any?
|
104
131
|
end
|
105
132
|
|
133
|
+
def location_range
|
134
|
+
tokens = [template&.relative_path, start_index, end_index].compact
|
135
|
+
tokens.join(":") if tokens.any?
|
136
|
+
end
|
137
|
+
|
106
138
|
def correctable?
|
107
|
-
|
139
|
+
!!correction
|
108
140
|
end
|
109
141
|
|
110
142
|
def correct
|
@@ -139,5 +171,26 @@ module ThemeCheck
|
|
139
171
|
message
|
140
172
|
end
|
141
173
|
end
|
174
|
+
|
175
|
+
def to_s_range
|
176
|
+
if template
|
177
|
+
"#{message} at #{location_range}"
|
178
|
+
else
|
179
|
+
message
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def to_h
|
184
|
+
{
|
185
|
+
check: check.code_name,
|
186
|
+
path: template&.relative_path,
|
187
|
+
severity: check.severity_value,
|
188
|
+
start_line: start_line,
|
189
|
+
start_column: start_column,
|
190
|
+
end_line: end_line,
|
191
|
+
end_column: end_column,
|
192
|
+
message: message,
|
193
|
+
}
|
194
|
+
end
|
142
195
|
end
|
143
196
|
end
|
@@ -5,13 +5,16 @@ module ThemeCheck
|
|
5
5
|
def outside_of_strings(markup)
|
6
6
|
scanner = StringScanner.new(markup)
|
7
7
|
|
8
|
-
while scanner.scan(/.*?("|')/)
|
9
|
-
|
8
|
+
while scanner.scan(/.*?("|')/m)
|
9
|
+
chunk_start = scanner.pre_match.size
|
10
|
+
yield scanner.matched[0..-2], chunk_start
|
11
|
+
quote = scanner.matched[-1] == "'" ? "'" : "\""
|
10
12
|
# Skip to the end of the string
|
11
|
-
|
13
|
+
# Check for empty string first, since follow regexp uses lookahead
|
14
|
+
scanner.skip(/#{quote}/) || scanner.skip_until(/[^\\]#{quote}/)
|
12
15
|
end
|
13
16
|
|
14
|
-
yield scanner.rest if scanner.rest?
|
17
|
+
yield scanner.rest, scanner.charpos if scanner.rest?
|
15
18
|
end
|
16
19
|
end
|
17
20
|
end
|
data/lib/theme_check/position.rb
CHANGED
@@ -4,42 +4,64 @@ module ThemeCheck
|
|
4
4
|
class Position
|
5
5
|
include PositionHelper
|
6
6
|
|
7
|
-
def initialize(
|
8
|
-
|
9
|
-
|
7
|
+
def initialize(
|
8
|
+
needle_arg,
|
9
|
+
contents_arg,
|
10
|
+
line_number_1_indexed: nil,
|
11
|
+
node_markup: nil,
|
12
|
+
node_markup_offset: 0 # the index of markup inside the node_markup
|
13
|
+
)
|
14
|
+
@needle = needle_arg
|
15
|
+
@contents = contents_arg
|
10
16
|
@line_number_1_indexed = line_number_1_indexed
|
11
|
-
@
|
12
|
-
@
|
17
|
+
@node_markup_offset = node_markup_offset
|
18
|
+
@node_markup = node_markup
|
19
|
+
@strict_position = StrictPosition.new(
|
20
|
+
needle,
|
21
|
+
contents,
|
22
|
+
start_index,
|
23
|
+
)
|
13
24
|
end
|
14
25
|
|
15
|
-
def
|
26
|
+
def start_line_offset
|
16
27
|
from_row_column_to_index(contents, line_number, 0)
|
17
28
|
end
|
18
29
|
|
30
|
+
def start_offset
|
31
|
+
return start_line_offset if @node_markup.nil?
|
32
|
+
node_markup_start = contents.index(@node_markup, start_line_offset)
|
33
|
+
return start_line_offset if node_markup_start.nil?
|
34
|
+
node_markup_start + @node_markup_offset
|
35
|
+
end
|
36
|
+
|
19
37
|
# 0-indexed, inclusive
|
20
38
|
def start_index
|
21
|
-
contents.index(needle,
|
39
|
+
contents.index(needle, start_offset)
|
22
40
|
end
|
23
41
|
|
24
42
|
# 0-indexed, exclusive
|
25
43
|
def end_index
|
26
|
-
|
44
|
+
@strict_position.end_index
|
27
45
|
end
|
28
46
|
|
47
|
+
# 0-indexed, inclusive
|
29
48
|
def start_row
|
30
|
-
|
49
|
+
@strict_position.start_row
|
31
50
|
end
|
32
51
|
|
52
|
+
# 0-indexed, inclusive
|
33
53
|
def start_column
|
34
|
-
|
54
|
+
@strict_position.start_column
|
35
55
|
end
|
36
56
|
|
57
|
+
# 0-indexed, exclusive (both taken together are) therefore you
|
58
|
+
# might end up on a newline character or the next line
|
37
59
|
def end_row
|
38
|
-
|
60
|
+
@strict_position.end_row
|
39
61
|
end
|
40
62
|
|
41
63
|
def end_column
|
42
|
-
|
64
|
+
@strict_position.end_column
|
43
65
|
end
|
44
66
|
|
45
67
|
private
|
@@ -55,15 +77,77 @@ module ThemeCheck
|
|
55
77
|
end
|
56
78
|
|
57
79
|
def needle
|
58
|
-
if
|
59
|
-
|
80
|
+
if has_content_and_line_number_but_no_needle?
|
81
|
+
entire_line_needle
|
60
82
|
elsif contents.empty? || @needle.nil?
|
61
83
|
''
|
84
|
+
elsif !can_find_needle?
|
85
|
+
entire_line_needle
|
62
86
|
else
|
63
87
|
@needle
|
64
88
|
end
|
65
89
|
end
|
66
90
|
|
91
|
+
def has_content_and_line_number_but_no_needle?
|
92
|
+
@needle.nil? && !contents.empty? && @line_number_1_indexed.is_a?(Integer)
|
93
|
+
end
|
94
|
+
|
95
|
+
def can_find_needle?
|
96
|
+
!!contents.index(@needle)
|
97
|
+
end
|
98
|
+
|
99
|
+
def entire_line_needle
|
100
|
+
contents.lines(chomp: true)[line_number] || ''
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# This method is stricter than Position in the sense that it doesn't
|
105
|
+
# accept invalid inputs. Makes for code that is easier to understand.
|
106
|
+
class StrictPosition
|
107
|
+
include PositionHelper
|
108
|
+
|
109
|
+
attr_reader :needle, :contents
|
110
|
+
|
111
|
+
def initialize(needle, contents, start_index)
|
112
|
+
raise ArgumentError, 'Bad start_index' unless start_index.is_a?(Integer)
|
113
|
+
raise ArgumentError, 'Bad contents' unless contents.is_a?(String)
|
114
|
+
raise ArgumentError, 'Bad needle' unless needle.is_a?(String) || !contents.index(needle, start_index)
|
115
|
+
|
116
|
+
@needle = needle
|
117
|
+
@contents = contents
|
118
|
+
@start_index = start_index
|
119
|
+
@start_row_column = nil
|
120
|
+
@end_row_column = nil
|
121
|
+
end
|
122
|
+
|
123
|
+
# 0-indexed, inclusive
|
124
|
+
def start_index
|
125
|
+
@contents.index(needle, @start_index)
|
126
|
+
end
|
127
|
+
|
128
|
+
# 0-indexed, exclusive
|
129
|
+
def end_index
|
130
|
+
start_index + needle.size
|
131
|
+
end
|
132
|
+
|
133
|
+
def start_row
|
134
|
+
start_row_column[0]
|
135
|
+
end
|
136
|
+
|
137
|
+
def start_column
|
138
|
+
start_row_column[1]
|
139
|
+
end
|
140
|
+
|
141
|
+
def end_row
|
142
|
+
end_row_column[0]
|
143
|
+
end
|
144
|
+
|
145
|
+
def end_column
|
146
|
+
end_row_column[1]
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
67
151
|
def start_row_column
|
68
152
|
return @start_row_column unless @start_row_column.nil?
|
69
153
|
@start_row_column = from_index_to_row_column(contents, start_index)
|