theme-check 0.8.2 → 0.10.1
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 +3 -0
- data/CHANGELOG.md +45 -0
- data/CONTRIBUTING.md +20 -90
- data/README.md +4 -1
- data/RELEASING.md +5 -3
- data/Rakefile +31 -0
- data/config/default.yml +45 -0
- data/docs/api/check.md +15 -0
- data/docs/api/html_check.md +46 -0
- data/docs/api/json_check.md +19 -0
- data/docs/api/liquid_check.md +99 -0
- data/docs/checks/{CHECK_DOCS_TEMPLATE.md → TEMPLATE.md.erb} +5 -5
- data/docs/checks/asset_url_filters.md +56 -0
- data/docs/checks/content_for_header_modification.md +42 -0
- data/docs/checks/img_lazy_loading.md +61 -0
- data/docs/checks/parser_blocking_script_tag.md +53 -0
- data/exe/theme-check-language-server +1 -2
- data/lib/theme_check.rb +8 -1
- data/lib/theme_check/analyzer.rb +72 -16
- data/lib/theme_check/bug.rb +1 -0
- data/lib/theme_check/check.rb +32 -7
- data/lib/theme_check/checks.rb +9 -1
- data/lib/theme_check/checks/TEMPLATE.rb.erb +11 -0
- data/lib/theme_check/checks/asset_url_filters.rb +46 -0
- data/lib/theme_check/checks/content_for_header_modification.rb +41 -0
- data/lib/theme_check/checks/img_lazy_loading.rb +25 -0
- data/lib/theme_check/checks/img_width_and_height.rb +18 -49
- data/lib/theme_check/checks/missing_template.rb +1 -0
- data/lib/theme_check/checks/parser_blocking_javascript.rb +6 -38
- data/lib/theme_check/checks/parser_blocking_script_tag.rb +20 -0
- data/lib/theme_check/checks/remote_asset.rb +21 -79
- data/lib/theme_check/checks/template_length.rb +3 -0
- data/lib/theme_check/checks/valid_html_translation.rb +1 -0
- data/lib/theme_check/config.rb +2 -0
- data/lib/theme_check/disabled_check.rb +6 -4
- data/lib/theme_check/disabled_checks.rb +25 -9
- data/lib/theme_check/html_check.rb +7 -0
- data/lib/theme_check/html_node.rb +56 -0
- data/lib/theme_check/html_visitor.rb +38 -0
- data/lib/theme_check/json_file.rb +13 -0
- data/lib/theme_check/language_server.rb +1 -0
- data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +1 -0
- data/lib/theme_check/language_server/diagnostics_tracker.rb +66 -0
- data/lib/theme_check/language_server/handler.rb +31 -26
- data/lib/theme_check/language_server/server.rb +1 -1
- data/lib/theme_check/liquid_check.rb +1 -4
- data/lib/theme_check/offense.rb +18 -0
- data/lib/theme_check/template.rb +9 -0
- data/lib/theme_check/theme.rb +7 -2
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check/visitor.rb +2 -11
- metadata +20 -3
data/lib/theme_check/bug.rb
CHANGED
data/lib/theme_check/check.rb
CHANGED
@@ -6,7 +6,7 @@ module ThemeCheck
|
|
6
6
|
include JsonHelpers
|
7
7
|
|
8
8
|
attr_accessor :theme
|
9
|
-
attr_accessor :options
|
9
|
+
attr_accessor :options, :ignored_patterns
|
10
10
|
attr_writer :offenses
|
11
11
|
|
12
12
|
SEVERITIES = [
|
@@ -18,7 +18,7 @@ module ThemeCheck
|
|
18
18
|
CATEGORIES = [
|
19
19
|
:liquid,
|
20
20
|
:translation,
|
21
|
-
:
|
21
|
+
:html,
|
22
22
|
:json,
|
23
23
|
:performance,
|
24
24
|
]
|
@@ -67,12 +67,23 @@ module ThemeCheck
|
|
67
67
|
end
|
68
68
|
defined?(@can_disable) ? @can_disable : true
|
69
69
|
end
|
70
|
+
|
71
|
+
def single_file(single_file = nil)
|
72
|
+
unless single_file.nil?
|
73
|
+
@single_file = single_file
|
74
|
+
end
|
75
|
+
defined?(@single_file) ? @single_file : !method_defined?(:on_end)
|
76
|
+
end
|
70
77
|
end
|
71
78
|
|
72
79
|
def offenses
|
73
80
|
@offenses ||= []
|
74
81
|
end
|
75
82
|
|
83
|
+
def add_offense(message, node: nil, template: node&.template, markup: nil, line_number: nil, &block)
|
84
|
+
offenses << Offense.new(check: self, message: message, template: template, node: node, markup: markup, line_number: line_number, correction: block)
|
85
|
+
end
|
86
|
+
|
76
87
|
def severity
|
77
88
|
self.class.severity
|
78
89
|
end
|
@@ -93,10 +104,6 @@ module ThemeCheck
|
|
93
104
|
@ignored = true
|
94
105
|
end
|
95
106
|
|
96
|
-
def unignore!
|
97
|
-
@ignored = false
|
98
|
-
end
|
99
|
-
|
100
107
|
def ignored?
|
101
108
|
defined?(@ignored) && @ignored
|
102
109
|
end
|
@@ -105,9 +112,27 @@ module ThemeCheck
|
|
105
112
|
self.class.can_disable
|
106
113
|
end
|
107
114
|
|
115
|
+
def single_file?
|
116
|
+
self.class.single_file
|
117
|
+
end
|
118
|
+
|
119
|
+
def whole_theme?
|
120
|
+
!single_file?
|
121
|
+
end
|
122
|
+
|
123
|
+
def ==(other)
|
124
|
+
other.is_a?(Check) && code_name == other.code_name
|
125
|
+
end
|
126
|
+
alias_method :eql?, :==
|
127
|
+
|
108
128
|
def to_s
|
109
129
|
s = +"#{code_name}:\n"
|
110
|
-
properties = {
|
130
|
+
properties = {
|
131
|
+
severity: severity,
|
132
|
+
categories: categories,
|
133
|
+
doc: doc,
|
134
|
+
ignored_patterns: ignored_patterns,
|
135
|
+
}.merge(options)
|
111
136
|
properties.each_pair do |name, value|
|
112
137
|
s << " #{name}: #{value}\n" if value
|
113
138
|
end
|
data/lib/theme_check/checks.rb
CHANGED
@@ -13,7 +13,15 @@ module ThemeCheck
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def disableable
|
16
|
-
self.class.new(select(&:can_disable?))
|
16
|
+
@disableable ||= self.class.new(select(&:can_disable?))
|
17
|
+
end
|
18
|
+
|
19
|
+
def whole_theme
|
20
|
+
@whole_theme ||= self.class.new(select(&:whole_theme?))
|
21
|
+
end
|
22
|
+
|
23
|
+
def single_file
|
24
|
+
@single_file ||= self.class.new(select(&:single_file?))
|
17
25
|
end
|
18
26
|
|
19
27
|
private
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ThemeCheck
|
3
|
+
# TODO: inherit from HtmlCheck or JsonCheck if working on a non-Liquid check
|
4
|
+
class <%= class_name %> < LiquidCheck
|
5
|
+
severity :suggestion
|
6
|
+
category :liquid
|
7
|
+
doc docs_url(__FILE__)
|
8
|
+
|
9
|
+
# TODO: def on_<NODE_TYPE>
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ThemeCheck
|
3
|
+
class AssetUrlFilters < LiquidCheck
|
4
|
+
severity :suggestion
|
5
|
+
categories :liquid, :performance
|
6
|
+
doc docs_url(__FILE__)
|
7
|
+
|
8
|
+
HTML_FILTERS = [
|
9
|
+
'stylesheet_tag',
|
10
|
+
'script_tag',
|
11
|
+
'img_tag',
|
12
|
+
]
|
13
|
+
ASSET_URL_FILTERS = [
|
14
|
+
'asset_url',
|
15
|
+
'asset_img_url',
|
16
|
+
'file_img_url',
|
17
|
+
'file_url',
|
18
|
+
'global_asset_url',
|
19
|
+
'img_url',
|
20
|
+
'payment_type_img_url',
|
21
|
+
'shopify_asset_url',
|
22
|
+
]
|
23
|
+
|
24
|
+
def on_variable(node)
|
25
|
+
record_variable_offense(node)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def record_variable_offense(variable_node)
|
31
|
+
# We flag HTML tags with URLs not hosted by Shopify
|
32
|
+
return if !html_resource_drop?(variable_node) || variable_hosted_by_shopify?(variable_node)
|
33
|
+
add_offense("Use one of the asset_url filters to serve assets", node: variable_node)
|
34
|
+
end
|
35
|
+
|
36
|
+
def html_resource_drop?(variable_node)
|
37
|
+
variable_node.value.filters
|
38
|
+
.any? { |(filter_name, *_filter_args)| HTML_FILTERS.include?(filter_name) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def variable_hosted_by_shopify?(variable_node)
|
42
|
+
variable_node.value.filters
|
43
|
+
.any? { |(filter_name, *_filter_args)| ASSET_URL_FILTERS.include?(filter_name) }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ThemeCheck
|
3
|
+
class ContentForHeaderModification < LiquidCheck
|
4
|
+
severity :error
|
5
|
+
category :liquid
|
6
|
+
doc docs_url(__FILE__)
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@in_assign = false
|
10
|
+
@in_capture = false
|
11
|
+
end
|
12
|
+
|
13
|
+
def on_variable(node)
|
14
|
+
return unless node.value.name.is_a?(Liquid::VariableLookup)
|
15
|
+
return unless node.value.name.name == "content_for_header"
|
16
|
+
|
17
|
+
if @in_assign || @in_capture || node.value.filters.any?
|
18
|
+
add_offense(
|
19
|
+
"Do not rely on the content of `content_for_header`",
|
20
|
+
node: node,
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_assign(_node)
|
26
|
+
@in_assign = true
|
27
|
+
end
|
28
|
+
|
29
|
+
def after_assign(_node)
|
30
|
+
@in_assign = false
|
31
|
+
end
|
32
|
+
|
33
|
+
def on_capture(_node)
|
34
|
+
@in_capture = true
|
35
|
+
end
|
36
|
+
|
37
|
+
def after_capture(_node)
|
38
|
+
@in_capture = false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ThemeCheck
|
3
|
+
class ImgLazyLoading < HtmlCheck
|
4
|
+
severity :suggestion
|
5
|
+
categories :html, :performance
|
6
|
+
doc docs_url(__FILE__)
|
7
|
+
|
8
|
+
ACCEPTED_LOADING_VALUES = %w[lazy eager]
|
9
|
+
|
10
|
+
def on_img(node)
|
11
|
+
loading = node.attributes["loading"]&.value&.downcase
|
12
|
+
return if ACCEPTED_LOADING_VALUES.include?(loading)
|
13
|
+
|
14
|
+
class_list = node.attributes["class"]&.value&.split(" ")
|
15
|
+
|
16
|
+
if class_list&.include?("lazyload")
|
17
|
+
add_offense("Use the native loading=\"lazy\" attribute instead of lazysizes", node: node)
|
18
|
+
elsif loading == "auto"
|
19
|
+
add_offense("Prefer loading=\"lazy\" to defer loading of images", node: node)
|
20
|
+
else
|
21
|
+
add_offense("Add a loading=\"lazy\" attribute to defer loading of images", node: node)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -1,41 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module ThemeCheck
|
3
3
|
# Reports errors when trying to use parser-blocking script tags
|
4
|
-
class ImgWidthAndHeight <
|
5
|
-
include RegexHelpers
|
4
|
+
class ImgWidthAndHeight < HtmlCheck
|
6
5
|
severity :error
|
7
|
-
categories :
|
6
|
+
categories :html, :performance
|
8
7
|
doc docs_url(__FILE__)
|
9
8
|
|
10
|
-
# Not implemented with lookbehinds and lookaheads because performance was shit!
|
11
|
-
IMG_TAG = %r{<img#{HTML_ATTRIBUTES}/?>}oxim
|
12
|
-
SRC_ATTRIBUTE = /\s(src)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
|
13
|
-
WIDTH_ATTRIBUTE = /\s(width)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
|
14
|
-
HEIGHT_ATTRIBUTE = /\s(height)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
|
15
|
-
|
16
|
-
FIELDS = [WIDTH_ATTRIBUTE, HEIGHT_ATTRIBUTE]
|
17
9
|
ENDS_IN_CSS_UNIT = /(cm|mm|in|px|pt|pc|em|ex|ch|rem|vw|vh|vmin|vmax|%)$/i
|
18
10
|
|
19
|
-
def
|
20
|
-
|
21
|
-
|
22
|
-
record_offenses
|
23
|
-
end
|
11
|
+
def on_img(node)
|
12
|
+
width = node.attributes["width"]&.value
|
13
|
+
height = node.attributes["height"]&.value
|
24
14
|
|
25
|
-
|
15
|
+
record_units_in_field_offenses("width", width, node: node)
|
16
|
+
record_units_in_field_offenses("height", height, node: node)
|
26
17
|
|
27
|
-
|
28
|
-
matches(@source, IMG_TAG).each do |img_match|
|
29
|
-
next unless img_match[0] =~ SRC_ATTRIBUTE
|
30
|
-
record_missing_field_offenses(img_match)
|
31
|
-
record_units_in_field_offenses(img_match)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def record_missing_field_offenses(img_match)
|
36
|
-
width = WIDTH_ATTRIBUTE.match(img_match[0])
|
37
|
-
height = HEIGHT_ATTRIBUTE.match(img_match[0])
|
38
|
-
return if width && height
|
18
|
+
return if node.attributes["src"].nil? || (width && height)
|
39
19
|
missing_width = width.nil?
|
40
20
|
missing_height = height.nil?
|
41
21
|
error_message = if missing_width && missing_height
|
@@ -46,29 +26,18 @@ module ThemeCheck
|
|
46
26
|
"Missing height attribute"
|
47
27
|
end
|
48
28
|
|
49
|
-
add_offense(
|
50
|
-
error_message,
|
51
|
-
node: @node,
|
52
|
-
markup: img_match[0],
|
53
|
-
line_number: @source[0...img_match.begin(0)].count("\n") + 1
|
54
|
-
)
|
29
|
+
add_offense(error_message, node: node)
|
55
30
|
end
|
56
31
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
"The #{field_match[1]} attribute does not take units. Replace with \"#{value_without_units}\".",
|
67
|
-
node: @node,
|
68
|
-
markup: value,
|
69
|
-
line_number: @source[0...start].count("\n") + 1
|
70
|
-
)
|
71
|
-
end
|
32
|
+
private
|
33
|
+
|
34
|
+
def record_units_in_field_offenses(attribute, value, node:)
|
35
|
+
return unless value =~ ENDS_IN_CSS_UNIT
|
36
|
+
value_without_units = value.gsub(ENDS_IN_CSS_UNIT, '')
|
37
|
+
add_offense(
|
38
|
+
"The #{attribute} attribute does not take units. Replace with \"#{value_without_units}\".",
|
39
|
+
node: node,
|
40
|
+
)
|
72
41
|
end
|
73
42
|
end
|
74
43
|
end
|
@@ -1,48 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module ThemeCheck
|
3
3
|
# Reports errors when trying to use parser-blocking script tags
|
4
|
-
class ParserBlockingJavaScript <
|
5
|
-
include RegexHelpers
|
4
|
+
class ParserBlockingJavaScript < HtmlCheck
|
6
5
|
severity :error
|
7
|
-
categories :
|
6
|
+
categories :html, :performance
|
8
7
|
doc docs_url(__FILE__)
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
(?:(?!defer|async|type=["']module['"]).)*? # Find tags that don't have defer|async|type="module"
|
14
|
-
/?>
|
15
|
-
}xim
|
16
|
-
SCRIPT_TAG_FILTER = /\{\{[^}]+script_tag\s+\}\}/
|
9
|
+
def on_script(node)
|
10
|
+
return unless node.attributes["src"]
|
11
|
+
return if node.attributes["defer"] || node.attributes["async"] || node.attributes["type"]&.value == "module"
|
17
12
|
|
18
|
-
|
19
|
-
@source = node.template.source
|
20
|
-
@node = node
|
21
|
-
record_offenses
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
|
26
|
-
def record_offenses
|
27
|
-
record_offenses_from_regex(
|
28
|
-
message: "Missing async or defer attribute on script tag",
|
29
|
-
regex: PARSER_BLOCKING_SCRIPT_TAG,
|
30
|
-
)
|
31
|
-
record_offenses_from_regex(
|
32
|
-
message: "The script_tag filter is parser-blocking. Use a script tag with the async or defer attribute for better performance",
|
33
|
-
regex: SCRIPT_TAG_FILTER,
|
34
|
-
)
|
35
|
-
end
|
36
|
-
|
37
|
-
def record_offenses_from_regex(regex: nil, message: nil)
|
38
|
-
matches(@source, regex).each do |match|
|
39
|
-
add_offense(
|
40
|
-
message,
|
41
|
-
node: @node,
|
42
|
-
markup: match[0],
|
43
|
-
line_number: @source[0...match.begin(0)].count("\n") + 1
|
44
|
-
)
|
45
|
-
end
|
13
|
+
add_offense("Missing async or defer attribute on script tag", node: node)
|
46
14
|
end
|
47
15
|
end
|
48
16
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ThemeCheck
|
3
|
+
# Reports errors when trying to use parser-blocking script tags
|
4
|
+
class ParserBlockingScriptTag < LiquidCheck
|
5
|
+
severity :error
|
6
|
+
categories :liquid, :performance
|
7
|
+
doc docs_url(__FILE__)
|
8
|
+
|
9
|
+
def on_variable(node)
|
10
|
+
used_filters = node.value.filters.map { |name, *_rest| name }
|
11
|
+
if used_filters.include?("script_tag")
|
12
|
+
add_offense(
|
13
|
+
"The script_tag filter is parser-blocking. Use a script tag with the async or defer " \
|
14
|
+
"attribute for better performance",
|
15
|
+
node: node
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,99 +1,41 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module ThemeCheck
|
3
|
-
class RemoteAsset <
|
4
|
-
include RegexHelpers
|
3
|
+
class RemoteAsset < HtmlCheck
|
5
4
|
severity :suggestion
|
6
|
-
categories :
|
5
|
+
categories :html, :performance
|
7
6
|
doc docs_url(__FILE__)
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
HTML_FILTERS = [
|
12
|
-
'stylesheet_tag',
|
13
|
-
'script_tag',
|
14
|
-
'img_tag',
|
15
|
-
]
|
16
|
-
ASSET_URL_FILTERS = [
|
17
|
-
'asset_url',
|
18
|
-
'asset_img_url',
|
19
|
-
'file_img_url',
|
20
|
-
'file_url',
|
21
|
-
'global_asset_url',
|
22
|
-
'img_url',
|
23
|
-
'payment_type_img_url',
|
24
|
-
'shopify_asset_url',
|
25
|
-
]
|
26
|
-
|
27
|
-
RESOURCE_TAG = %r{<(?<tag_name>img|script|link|source)#{HTML_ATTRIBUTES}/?>}oim
|
28
|
-
RESOURCE_URL = /\s(?:src|href)=(?<resource_url>#{QUOTED_LIQUID_ATTRIBUTE})/oim
|
29
|
-
ASSET_URL_FILTER = /[\|\s]*(#{ASSET_URL_FILTERS.join('|')})/omi
|
8
|
+
TAGS = %w[img script link source]
|
30
9
|
PROTOCOL = %r{(https?:)?//}
|
31
10
|
ABSOLUTE_PATH = %r{\A/[^/]}im
|
32
11
|
RELATIVE_PATH = %r{\A(?!#{PROTOCOL})[^/\{]}oim
|
33
|
-
REL = /\srel=(?<rel>#{QUOTED_LIQUID_ATTRIBUTE})/oim
|
34
|
-
|
35
|
-
def on_variable(node)
|
36
|
-
record_variable_offense(node)
|
37
|
-
end
|
38
12
|
|
39
|
-
def
|
40
|
-
|
41
|
-
record_html_offenses(node, source)
|
42
|
-
end
|
13
|
+
def on_element(node)
|
14
|
+
return unless TAGS.include?(node.name)
|
43
15
|
|
44
|
-
|
16
|
+
resource_url = node.attributes["src"]&.value || node.attributes["href"]&.value
|
17
|
+
return if resource_url.nil? || resource_url.empty?
|
45
18
|
|
46
|
-
|
47
|
-
|
48
|
-
return if
|
49
|
-
|
50
|
-
end
|
19
|
+
# Ignore if URL is Liquid, taken care of by AssetUrlFilters check
|
20
|
+
return if resource_url =~ ABSOLUTE_PATH
|
21
|
+
return if resource_url =~ RELATIVE_PATH
|
22
|
+
return if url_hosted_by_shopify?(resource_url)
|
51
23
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
end
|
24
|
+
# Ignore non-stylesheet rel tags
|
25
|
+
rel = node.attributes["rel"]
|
26
|
+
return if rel && rel.value != "stylesheet"
|
56
27
|
|
57
|
-
|
58
|
-
|
59
|
-
|
28
|
+
add_offense(
|
29
|
+
"Asset should be served by the Shopify CDN for better performance.",
|
30
|
+
node: node,
|
31
|
+
)
|
60
32
|
end
|
61
33
|
|
62
|
-
|
63
|
-
# HTML AST. We have to resort to looking at the HTML with regexes
|
64
|
-
# to figure out if we have a resource (stylesheet, script, or media)
|
65
|
-
# that points to a remote domain.
|
66
|
-
def record_html_offenses(node, source)
|
67
|
-
matches(source, RESOURCE_TAG).each do |match|
|
68
|
-
tag = match[0]
|
69
|
-
|
70
|
-
# We don't flag stuff without URLs
|
71
|
-
next unless tag =~ RESOURCE_URL
|
72
|
-
resource_match = Regexp.last_match
|
73
|
-
resource_url = resource_match[:resource_url].gsub(START_OR_END_QUOTE, '')
|
74
|
-
|
75
|
-
next if non_stylesheet_link?(tag)
|
76
|
-
next if url_hosted_by_shopify?(resource_url)
|
77
|
-
next if resource_url =~ ABSOLUTE_PATH
|
78
|
-
next if resource_url =~ RELATIVE_PATH
|
79
|
-
next if resource_url.empty?
|
80
|
-
|
81
|
-
start = match.begin(0) + resource_match.begin(:resource_url)
|
82
|
-
add_offense(
|
83
|
-
OFFENSE_MESSAGE,
|
84
|
-
node: node,
|
85
|
-
markup: resource_url,
|
86
|
-
line_number: source[0...start].count("\n") + 1,
|
87
|
-
)
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
def non_stylesheet_link?(tag)
|
92
|
-
tag =~ REL && !(Regexp.last_match[:rel] =~ /\A['"]stylesheet['"]\Z/)
|
93
|
-
end
|
34
|
+
private
|
94
35
|
|
95
36
|
def url_hosted_by_shopify?(url)
|
96
|
-
url
|
37
|
+
url.start_with?(Liquid::VariableStart) &&
|
38
|
+
AssetUrlFilters::ASSET_URL_FILTERS.any? { |filter| url.include?(filter) }
|
97
39
|
end
|
98
40
|
end
|
99
41
|
end
|