theme-check 0.8.1 → 0.10.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 +3 -0
- data/CHANGELOG.md +39 -0
- data/CONTRIBUTING.md +2 -1
- data/README.md +4 -1
- data/RELEASING.md +5 -3
- data/config/default.yml +46 -1
- data/data/shopify_liquid/tags.yml +3 -0
- 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/nested_snippet.md +1 -1
- data/docs/checks/parser_blocking_script_tag.md +53 -0
- data/docs/checks/space_inside_braces.md +22 -0
- data/exe/theme-check-language-server +1 -2
- data/lib/theme_check.rb +9 -1
- data/lib/theme_check/analyzer.rb +72 -16
- data/lib/theme_check/bug.rb +20 -0
- data/lib/theme_check/check.rb +31 -6
- data/lib/theme_check/checks.rb +49 -4
- 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/nested_snippet.rb +1 -1
- 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/space_inside_braces.rb +5 -5
- 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 +8 -0
- data/lib/theme_check/language_server.rb +2 -0
- data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +1 -0
- data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +10 -8
- data/lib/theme_check/language_server/diagnostics_tracker.rb +64 -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/language_server/variable_lookup_finder.rb +295 -0
- data/lib/theme_check/liquid_check.rb +1 -4
- data/lib/theme_check/offense.rb +18 -0
- data/lib/theme_check/shopify_liquid/tag.rb +13 -0
- data/lib/theme_check/template.rb +8 -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 +17 -3
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "forwardable"
|
3
|
+
|
4
|
+
module ThemeCheck
|
5
|
+
class HtmlNode
|
6
|
+
extend Forwardable
|
7
|
+
attr_reader :template
|
8
|
+
|
9
|
+
def_delegators :@value, :content, :attributes
|
10
|
+
|
11
|
+
def initialize(value, template)
|
12
|
+
@value = value
|
13
|
+
@template = template
|
14
|
+
end
|
15
|
+
|
16
|
+
def literal?
|
17
|
+
@value.name == "text"
|
18
|
+
end
|
19
|
+
|
20
|
+
def element?
|
21
|
+
@value.element?
|
22
|
+
end
|
23
|
+
|
24
|
+
def children
|
25
|
+
@value.children.map { |child| HtmlNode.new(child, template) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def parent
|
29
|
+
HtmlNode.new(@value.parent, template)
|
30
|
+
end
|
31
|
+
|
32
|
+
def name
|
33
|
+
if @value.name == "#document-fragment"
|
34
|
+
"document"
|
35
|
+
else
|
36
|
+
@value.name
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def value
|
41
|
+
if literal?
|
42
|
+
@value.content
|
43
|
+
else
|
44
|
+
@value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def markup
|
49
|
+
@value.to_html
|
50
|
+
end
|
51
|
+
|
52
|
+
def line_number
|
53
|
+
@value.line
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "nokogumbo"
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module ThemeCheck
|
6
|
+
class HtmlVisitor
|
7
|
+
attr_reader :checks
|
8
|
+
|
9
|
+
def initialize(checks)
|
10
|
+
@checks = checks
|
11
|
+
end
|
12
|
+
|
13
|
+
def visit_template(template)
|
14
|
+
doc = parse(template)
|
15
|
+
visit(HtmlNode.new(doc, template))
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def parse(template)
|
21
|
+
Nokogiri::HTML5.fragment(template.source)
|
22
|
+
end
|
23
|
+
|
24
|
+
def visit(node)
|
25
|
+
call_checks(:on_element, node) if node.element?
|
26
|
+
call_checks(:"on_#{node.name}", node)
|
27
|
+
node.children.each { |child| visit(child) }
|
28
|
+
unless node.literal?
|
29
|
+
call_checks(:"after_#{node.name}", node)
|
30
|
+
call_checks(:after_element, node) if node.element?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def call_checks(method, *args)
|
35
|
+
checks.call(method, *args)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -4,10 +4,12 @@ require_relative "language_server/constants"
|
|
4
4
|
require_relative "language_server/handler"
|
5
5
|
require_relative "language_server/server"
|
6
6
|
require_relative "language_server/tokens"
|
7
|
+
require_relative "language_server/variable_lookup_finder"
|
7
8
|
require_relative "language_server/completion_helper"
|
8
9
|
require_relative "language_server/completion_provider"
|
9
10
|
require_relative "language_server/completion_engine"
|
10
11
|
require_relative "language_server/document_link_engine"
|
12
|
+
require_relative "language_server/diagnostics_tracker"
|
11
13
|
|
12
14
|
Dir[__dir__ + "/language_server/completion_providers/*.rb"].each do |file|
|
13
15
|
require file
|
@@ -4,18 +4,20 @@ module ThemeCheck
|
|
4
4
|
module LanguageServer
|
5
5
|
class ObjectCompletionProvider < CompletionProvider
|
6
6
|
def completions(content, cursor)
|
7
|
-
return [] unless
|
8
|
-
|
7
|
+
return [] unless (variable_lookup = variable_lookup_at_cursor(content, cursor))
|
8
|
+
return [] unless variable_lookup.lookups.empty?
|
9
|
+
return [] if content[cursor - 1] == "."
|
9
10
|
ShopifyLiquid::Object.labels
|
10
|
-
.select { |w| w.start_with?(partial) }
|
11
|
+
.select { |w| w.start_with?(partial(variable_lookup)) }
|
11
12
|
.map { |object| object_to_completion(object) }
|
12
13
|
end
|
13
14
|
|
14
|
-
def
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
def variable_lookup_at_cursor(content, cursor)
|
16
|
+
VariableLookupFinder.lookup(content, cursor)
|
17
|
+
end
|
18
|
+
|
19
|
+
def partial(variable_lookup)
|
20
|
+
variable_lookup.name || ''
|
19
21
|
end
|
20
22
|
|
21
23
|
private
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class DiagnosticsTracker
|
6
|
+
def initialize
|
7
|
+
@previously_reported_files = Set.new
|
8
|
+
@single_files_offenses = {}
|
9
|
+
@first_run = true
|
10
|
+
end
|
11
|
+
|
12
|
+
def first_run?
|
13
|
+
@first_run
|
14
|
+
end
|
15
|
+
|
16
|
+
def build_diagnostics(offenses, analyzed_files: nil)
|
17
|
+
reported_files = Set.new
|
18
|
+
new_single_file_offenses = {}
|
19
|
+
|
20
|
+
offenses.group_by(&:template).each do |template, template_offenses|
|
21
|
+
next unless template
|
22
|
+
reported_offenses = template_offenses
|
23
|
+
previous_offenses = @single_files_offenses[template.path]
|
24
|
+
if analyzed_files.nil? || analyzed_files.include?(template.path)
|
25
|
+
# We re-analyzed the file, so we know the template_offenses are update to date.
|
26
|
+
reported_single_file_offenses = reported_offenses.select(&:single_file?)
|
27
|
+
if reported_single_file_offenses.any?
|
28
|
+
new_single_file_offenses[template.path] = reported_single_file_offenses
|
29
|
+
end
|
30
|
+
elsif previous_offenses
|
31
|
+
# Merge in the previous ones, if some
|
32
|
+
reported_offenses |= previous_offenses
|
33
|
+
end
|
34
|
+
yield template.path, reported_offenses
|
35
|
+
reported_files << template.path
|
36
|
+
end
|
37
|
+
|
38
|
+
@single_files_offenses.each do |path, _|
|
39
|
+
# Already reported above, skip
|
40
|
+
next if reported_files.include?(path)
|
41
|
+
|
42
|
+
if analyzed_files.nil? || analyzed_files.include?(path)
|
43
|
+
# We re-analyzed this file, if it was not reported, all offenses in it got fixed
|
44
|
+
yield path, []
|
45
|
+
new_single_file_offenses[path] = nil
|
46
|
+
end
|
47
|
+
# NOTE: No need to re-report previous offenses as LSP should keep them around until
|
48
|
+
# we clear them.
|
49
|
+
reported_files << path
|
50
|
+
end
|
51
|
+
|
52
|
+
# Publish diagnostics with empty array if all issues on a previously reported template
|
53
|
+
# have been fixed.
|
54
|
+
(@previously_reported_files - reported_files).each do |path|
|
55
|
+
yield path, []
|
56
|
+
end
|
57
|
+
|
58
|
+
@previously_reported_files = reported_files
|
59
|
+
@single_files_offenses.merge!(new_single_file_offenses)
|
60
|
+
@first_run = false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require "benchmark"
|
2
3
|
|
3
4
|
module ThemeCheck
|
4
5
|
module LanguageServer
|
@@ -19,7 +20,7 @@ module ThemeCheck
|
|
19
20
|
|
20
21
|
def initialize(server)
|
21
22
|
@server = server
|
22
|
-
@
|
23
|
+
@diagnostics_tracker = DiagnosticsTracker.new
|
23
24
|
end
|
24
25
|
|
25
26
|
def on_initialize(id, params)
|
@@ -52,6 +53,7 @@ module ThemeCheck
|
|
52
53
|
end
|
53
54
|
|
54
55
|
def on_text_document_did_open(_id, params)
|
56
|
+
return unless @diagnostics_tracker.first_run?
|
55
57
|
relative_path = relative_path_from_text_document_uri(params)
|
56
58
|
@storage.write(relative_path, text_document_text(params))
|
57
59
|
analyze_and_send_offenses(text_document_uri(params))
|
@@ -124,17 +126,32 @@ module ThemeCheck
|
|
124
126
|
ignored_patterns: config.ignored_patterns
|
125
127
|
)
|
126
128
|
theme = ThemeCheck::Theme.new(storage)
|
127
|
-
|
128
|
-
offenses = analyze(theme, config)
|
129
|
-
log("Found #{theme.all.size} templates, and #{offenses.size} offenses")
|
130
|
-
send_diagnostics(offenses)
|
131
|
-
end
|
132
|
-
|
133
|
-
def analyze(theme, config)
|
134
129
|
analyzer = ThemeCheck::Analyzer.new(theme, config.enabled_checks)
|
135
|
-
|
136
|
-
|
137
|
-
|
130
|
+
|
131
|
+
if @diagnostics_tracker.first_run?
|
132
|
+
# Analyze the full theme on first run
|
133
|
+
log("Checking #{config.root}")
|
134
|
+
offenses = nil
|
135
|
+
time = Benchmark.measure do
|
136
|
+
offenses = analyzer.analyze_theme
|
137
|
+
end
|
138
|
+
log("Found #{offenses.size} offenses in #{format("%0.2f", time.real)}s")
|
139
|
+
send_diagnostics(offenses)
|
140
|
+
else
|
141
|
+
# Analyze selected files
|
142
|
+
relative_path = Pathname.new(@storage.relative_path(absolute_path))
|
143
|
+
file = theme[relative_path]
|
144
|
+
# Skip if not a theme file
|
145
|
+
if file
|
146
|
+
log("Checking #{relative_path}")
|
147
|
+
offenses = nil
|
148
|
+
time = Benchmark.measure do
|
149
|
+
offenses = analyzer.analyze_files([file])
|
150
|
+
end
|
151
|
+
log("Found #{offenses.size} new offenses in #{format("%0.2f", time.real)}s")
|
152
|
+
send_diagnostics(offenses, [absolute_path])
|
153
|
+
end
|
154
|
+
end
|
138
155
|
end
|
139
156
|
|
140
157
|
def completions(relative_path, line, col)
|
@@ -145,22 +162,10 @@ module ThemeCheck
|
|
145
162
|
@document_link_engine.document_links(relative_path)
|
146
163
|
end
|
147
164
|
|
148
|
-
def send_diagnostics(offenses)
|
149
|
-
|
150
|
-
|
151
|
-
offenses.group_by(&:template).each do |template, template_offenses|
|
152
|
-
next unless template
|
153
|
-
send_diagnostic(template.path, template_offenses)
|
154
|
-
reported_files << template.path
|
165
|
+
def send_diagnostics(offenses, analyzed_files = nil)
|
166
|
+
@diagnostics_tracker.build_diagnostics(offenses, analyzed_files: analyzed_files) do |path, diagnostic_offenses|
|
167
|
+
send_diagnostic(path, diagnostic_offenses)
|
155
168
|
end
|
156
|
-
|
157
|
-
# Publish diagnostics with empty array if all issues on a previously reported template
|
158
|
-
# have been solved.
|
159
|
-
(@previously_reported_files - reported_files).each do |path|
|
160
|
-
send_diagnostic(path, [])
|
161
|
-
end
|
162
|
-
|
163
|
-
@previously_reported_files = reported_files
|
164
169
|
end
|
165
170
|
|
166
171
|
def send_diagnostic(path, offenses)
|
@@ -52,7 +52,7 @@ module ThemeCheck
|
|
52
52
|
response_body = JSON.dump(response)
|
53
53
|
log(JSON.pretty_generate(response)) if $DEBUG
|
54
54
|
|
55
|
-
@out.write("Content-Length: #{response_body.
|
55
|
+
@out.write("Content-Length: #{response_body.bytesize}\r\n")
|
56
56
|
@out.write("\r\n")
|
57
57
|
@out.write(response_body)
|
58
58
|
@out.flush
|
@@ -0,0 +1,295 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
module VariableLookupFinder
|
6
|
+
extend self
|
7
|
+
|
8
|
+
UNCLOSED_SQUARE_BRACKET = /\[[^\]]*\Z/
|
9
|
+
ENDS_IN_BRACKET_POSITION_THAT_CANT_BE_COMPLETED = %r{
|
10
|
+
(
|
11
|
+
# quotes not preceded by a [
|
12
|
+
(?<!\[)['"]|
|
13
|
+
# closing ]
|
14
|
+
\]|
|
15
|
+
# opening [
|
16
|
+
\[
|
17
|
+
)$
|
18
|
+
}x
|
19
|
+
|
20
|
+
VARIABLE_LOOKUP_CHARACTERS = /[a-z0-9_.'"\]\[]/i
|
21
|
+
VARIABLE_LOOKUP = /#{VARIABLE_LOOKUP_CHARACTERS}+/o
|
22
|
+
SYMBOLS_PRECEDING_POTENTIAL_LOOKUPS = %r{
|
23
|
+
(?:
|
24
|
+
\s(?:
|
25
|
+
if|elsif|unless|and|or|#{Liquid::Condition.operators.keys.join("|")}
|
26
|
+
|echo
|
27
|
+
|case|when
|
28
|
+
|cycle
|
29
|
+
|in
|
30
|
+
)
|
31
|
+
|[:,=]
|
32
|
+
)
|
33
|
+
\s+
|
34
|
+
}omix
|
35
|
+
ENDS_WITH_BLANK_POTENTIAL_LOOKUP = /#{SYMBOLS_PRECEDING_POTENTIAL_LOOKUPS}$/oimx
|
36
|
+
ENDS_WITH_POTENTIAL_LOOKUP = /#{SYMBOLS_PRECEDING_POTENTIAL_LOOKUPS}#{VARIABLE_LOOKUP}$/oimx
|
37
|
+
|
38
|
+
def lookup(content, cursor)
|
39
|
+
return if cursor_is_on_bracket_position_that_cant_be_completed(content, cursor)
|
40
|
+
potential_lookup = lookup_liquid_variable(content, cursor) || lookup_liquid_tag(content, cursor)
|
41
|
+
|
42
|
+
# And we only return it if it's parsed by Liquid as VariableLookup
|
43
|
+
return unless potential_lookup.is_a?(Liquid::VariableLookup)
|
44
|
+
potential_lookup
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def cursor_is_on_bracket_position_that_cant_be_completed(content, cursor)
|
50
|
+
content[0..cursor - 1] =~ ENDS_IN_BRACKET_POSITION_THAT_CANT_BE_COMPLETED
|
51
|
+
end
|
52
|
+
|
53
|
+
def cursor_is_on_liquid_variable_lookup_position(content, cursor)
|
54
|
+
previous_char = content[cursor - 1]
|
55
|
+
is_liquid_variable = content =~ Liquid::VariableStart
|
56
|
+
is_in_variable_segment = previous_char =~ VARIABLE_LOOKUP_CHARACTERS
|
57
|
+
is_on_blank_variable_lookup_position = content[0..cursor - 1] =~ /[{:,-]\s+$/
|
58
|
+
(
|
59
|
+
is_liquid_variable && (
|
60
|
+
is_in_variable_segment ||
|
61
|
+
is_on_blank_variable_lookup_position
|
62
|
+
)
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
def lookup_liquid_variable(content, cursor)
|
67
|
+
return unless cursor_is_on_liquid_variable_lookup_position(content, cursor)
|
68
|
+
start_index = content.match(/#{Liquid::VariableStart}-?/o).end(0) + 1
|
69
|
+
end_index = cursor - 1
|
70
|
+
|
71
|
+
# We take the following content
|
72
|
+
# - start after the first two {{
|
73
|
+
# - end at cursor position
|
74
|
+
#
|
75
|
+
# That way, we'll have a partial liquid variable that
|
76
|
+
# can be parsed such that the "last" variable_lookup
|
77
|
+
# will be the one we're trying to complete.
|
78
|
+
markup = content[start_index..end_index]
|
79
|
+
|
80
|
+
# Early return for incomplete variables
|
81
|
+
return empty_lookup if markup =~ /\s+$/
|
82
|
+
|
83
|
+
# Now we go to hack city... The cursor might be in the middle
|
84
|
+
# of a string/square bracket lookup. We need to close those
|
85
|
+
# otherwise the variable parse won't work.
|
86
|
+
markup += "'" if markup.count("'").odd?
|
87
|
+
markup += '"' if markup.count('"').odd?
|
88
|
+
markup += "]" if markup =~ UNCLOSED_SQUARE_BRACKET
|
89
|
+
|
90
|
+
variable = variable_from_markup(markup)
|
91
|
+
|
92
|
+
variable_lookup_for_liquid_variable(variable)
|
93
|
+
end
|
94
|
+
|
95
|
+
def cursor_is_on_liquid_tag_lookup_position(content, cursor)
|
96
|
+
markup = content[0..cursor - 1]
|
97
|
+
is_liquid_tag = content.match?(Liquid::TagStart)
|
98
|
+
is_in_variable_segment = markup =~ ENDS_WITH_POTENTIAL_LOOKUP
|
99
|
+
is_on_blank_variable_lookup_position = markup =~ ENDS_WITH_BLANK_POTENTIAL_LOOKUP
|
100
|
+
(
|
101
|
+
is_liquid_tag && (
|
102
|
+
is_in_variable_segment ||
|
103
|
+
is_on_blank_variable_lookup_position
|
104
|
+
)
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Context:
|
109
|
+
#
|
110
|
+
# We know full well that the code as it is being typed is probably not
|
111
|
+
# something that can be parsed by liquid.
|
112
|
+
#
|
113
|
+
# How this works:
|
114
|
+
#
|
115
|
+
# 1. Attempt to turn the code of the token until the cursor position into
|
116
|
+
# valid liquid code with some hacks.
|
117
|
+
# 2. If the code ends in space at a "potential lookup" spot
|
118
|
+
# a. Then return an empty variable lookup
|
119
|
+
# 3. Parse the valid liquid code
|
120
|
+
# 4. Attempt to extract a VariableLookup from Liquid::Template
|
121
|
+
def lookup_liquid_tag(content, cursor)
|
122
|
+
return unless cursor_is_on_liquid_tag_lookup_position(content, cursor)
|
123
|
+
|
124
|
+
markup = parseable_markup(content, cursor)
|
125
|
+
return empty_lookup if markup == :empty_lookup_markup
|
126
|
+
|
127
|
+
template = Liquid::Template.parse(markup)
|
128
|
+
current_tag = template.root.nodelist[0]
|
129
|
+
|
130
|
+
case current_tag.tag_name
|
131
|
+
when "if", "unless"
|
132
|
+
variable_lookup_for_if_tag(current_tag)
|
133
|
+
when "case"
|
134
|
+
variable_lookup_for_case_tag(current_tag)
|
135
|
+
when "cycle"
|
136
|
+
variable_lookup_for_cycle_tag(current_tag)
|
137
|
+
when "for"
|
138
|
+
variable_lookup_for_for_tag(current_tag)
|
139
|
+
when "tablerow"
|
140
|
+
variable_lookup_for_tablerow_tag(current_tag)
|
141
|
+
when "render"
|
142
|
+
variable_lookup_for_render_tag(current_tag)
|
143
|
+
when "assign"
|
144
|
+
variable_lookup_for_assign_tag(current_tag)
|
145
|
+
when "echo"
|
146
|
+
variable_lookup_for_echo_tag(current_tag)
|
147
|
+
end
|
148
|
+
|
149
|
+
# rubocop:disable Style/RedundantReturn
|
150
|
+
rescue Liquid::SyntaxError
|
151
|
+
# We don't complete variable for liquid syntax errors
|
152
|
+
return
|
153
|
+
end
|
154
|
+
# rubocop:enable Style/RedundantReturn
|
155
|
+
|
156
|
+
def parseable_markup(content, cursor)
|
157
|
+
start_index = 0
|
158
|
+
end_index = cursor - 1
|
159
|
+
markup = content[start_index..end_index]
|
160
|
+
|
161
|
+
# Welcome to Hackcity
|
162
|
+
markup += "'" if markup.count("'").odd?
|
163
|
+
markup += '"' if markup.count('"').odd?
|
164
|
+
markup += "]" if markup =~ UNCLOSED_SQUARE_BRACKET
|
165
|
+
|
166
|
+
# Now check if it's a liquid tag
|
167
|
+
is_liquid_tag = markup =~ tag_regex('liquid')
|
168
|
+
ends_with_blank_potential_lookup = markup =~ ENDS_WITH_BLANK_POTENTIAL_LOOKUP
|
169
|
+
last_line = markup.rstrip.lines.last
|
170
|
+
markup = "{% #{last_line}" if is_liquid_tag
|
171
|
+
|
172
|
+
# Close the tag
|
173
|
+
markup += ' %}'
|
174
|
+
|
175
|
+
# if statements
|
176
|
+
is_if_tag = markup =~ tag_regex('if')
|
177
|
+
return :empty_lookup_markup if is_if_tag && ends_with_blank_potential_lookup
|
178
|
+
markup += '{% endif %}' if is_if_tag
|
179
|
+
|
180
|
+
# unless statements
|
181
|
+
is_unless_tag = markup =~ tag_regex('unless')
|
182
|
+
return :empty_lookup_markup if is_unless_tag && ends_with_blank_potential_lookup
|
183
|
+
markup += '{% endunless %}' if is_unless_tag
|
184
|
+
|
185
|
+
# elsif statements
|
186
|
+
is_elsif_tag = markup =~ tag_regex('elsif')
|
187
|
+
return :empty_lookup_markup if is_elsif_tag && ends_with_blank_potential_lookup
|
188
|
+
markup = '{% if x %}' + markup + '{% endif %}' if is_elsif_tag
|
189
|
+
|
190
|
+
# case statements
|
191
|
+
is_case_tag = markup =~ tag_regex('case')
|
192
|
+
return :empty_lookup_markup if is_case_tag && ends_with_blank_potential_lookup
|
193
|
+
markup += "{% endcase %}" if is_case_tag
|
194
|
+
|
195
|
+
# when
|
196
|
+
is_when_tag = markup =~ tag_regex('when')
|
197
|
+
return :empty_lookup_markup if is_when_tag && ends_with_blank_potential_lookup
|
198
|
+
markup = "{% case x %}" + markup + "{% endcase %}" if is_when_tag
|
199
|
+
|
200
|
+
# for statements
|
201
|
+
is_for_tag = markup =~ tag_regex('for')
|
202
|
+
return :empty_lookup_markup if is_for_tag && ends_with_blank_potential_lookup
|
203
|
+
markup += "{% endfor %}" if is_for_tag
|
204
|
+
|
205
|
+
# tablerow statements
|
206
|
+
is_tablerow_tag = markup =~ tag_regex('tablerow')
|
207
|
+
return :empty_lookup_markup if is_tablerow_tag && ends_with_blank_potential_lookup
|
208
|
+
markup += "{% endtablerow %}" if is_tablerow_tag
|
209
|
+
|
210
|
+
markup
|
211
|
+
end
|
212
|
+
|
213
|
+
def variable_lookup_for_if_tag(if_tag)
|
214
|
+
condition = if_tag.blocks.last
|
215
|
+
variable_lookup_for_condition(condition)
|
216
|
+
end
|
217
|
+
|
218
|
+
def variable_lookup_for_condition(condition)
|
219
|
+
return variable_lookup_for_condition(condition.child_condition) if condition.child_condition
|
220
|
+
return condition.right if condition.right
|
221
|
+
condition.left
|
222
|
+
end
|
223
|
+
|
224
|
+
def variable_lookup_for_case_tag(case_tag)
|
225
|
+
return variable_lookup_for_case_block(case_tag.blocks.last) unless case_tag.blocks.empty?
|
226
|
+
case_tag.left
|
227
|
+
end
|
228
|
+
|
229
|
+
def variable_lookup_for_case_block(condition)
|
230
|
+
condition.right
|
231
|
+
end
|
232
|
+
|
233
|
+
def variable_lookup_for_cycle_tag(cycle_tag)
|
234
|
+
cycle_tag.variables.last
|
235
|
+
end
|
236
|
+
|
237
|
+
def variable_lookup_for_for_tag(for_tag)
|
238
|
+
for_tag.collection_name
|
239
|
+
end
|
240
|
+
|
241
|
+
def variable_lookup_for_tablerow_tag(tablerow_tag)
|
242
|
+
tablerow_tag.collection_name
|
243
|
+
end
|
244
|
+
|
245
|
+
def variable_lookup_for_render_tag(render_tag)
|
246
|
+
return empty_lookup if render_tag.raw =~ /:\s*$/
|
247
|
+
render_tag.attributes.values.last
|
248
|
+
end
|
249
|
+
|
250
|
+
def variable_lookup_for_assign_tag(assign_tag)
|
251
|
+
variable_lookup_for_liquid_variable(assign_tag.from)
|
252
|
+
end
|
253
|
+
|
254
|
+
def variable_lookup_for_echo_tag(echo_tag)
|
255
|
+
variable_lookup_for_liquid_variable(echo_tag.variable)
|
256
|
+
end
|
257
|
+
|
258
|
+
def variable_lookup_for_liquid_variable(variable)
|
259
|
+
has_filters = !variable.filters.empty?
|
260
|
+
|
261
|
+
# Can complete after trailing comma or :
|
262
|
+
if has_filters && variable.raw =~ /[:,]\s*$/
|
263
|
+
empty_lookup
|
264
|
+
elsif has_filters
|
265
|
+
last_filter_argument(variable.filters)
|
266
|
+
elsif variable.name.nil?
|
267
|
+
empty_lookup
|
268
|
+
else
|
269
|
+
variable.name
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def empty_lookup
|
274
|
+
Liquid::VariableLookup.parse('')
|
275
|
+
end
|
276
|
+
|
277
|
+
# We want the last thing in variable.filters which is at most
|
278
|
+
# an array that looks like [name, positional_args, hash_arg]
|
279
|
+
def last_filter_argument(filters)
|
280
|
+
filter = filters.last
|
281
|
+
return filter[2].values.last if filter.size == 3
|
282
|
+
return filter[1].last if filter.size == 2
|
283
|
+
nil
|
284
|
+
end
|
285
|
+
|
286
|
+
def variable_from_markup(markup, parse_context = Liquid::ParseContext.new)
|
287
|
+
Liquid::Variable.new(markup, parse_context)
|
288
|
+
end
|
289
|
+
|
290
|
+
def tag_regex(tag)
|
291
|
+
ShopifyLiquid::Tag.tag_regex(tag)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|