theme-check 1.6.1 → 1.6.2

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/docs/api/html_check.md +7 -7
  4. data/docs/api/liquid_check.md +10 -10
  5. data/docs/checks/convert_include_to_render.md +1 -1
  6. data/docs/checks/missing_enable_comment.md +1 -1
  7. data/lib/theme_check/analyzer.rb +15 -15
  8. data/lib/theme_check/asset_file.rb +1 -1
  9. data/lib/theme_check/check.rb +2 -2
  10. data/lib/theme_check/checks/html_parsing_error.rb +2 -2
  11. data/lib/theme_check/checks/matching_translations.rb +1 -1
  12. data/lib/theme_check/checks/missing_template.rb +6 -6
  13. data/lib/theme_check/checks/nested_snippet.rb +2 -2
  14. data/lib/theme_check/checks/required_layout_theme_object.rb +2 -2
  15. data/lib/theme_check/checks/syntax_error.rb +5 -5
  16. data/lib/theme_check/checks/template_length.rb +2 -2
  17. data/lib/theme_check/checks/undefined_object.rb +7 -7
  18. data/lib/theme_check/checks/unused_assign.rb +4 -4
  19. data/lib/theme_check/checks/unused_snippet.rb +7 -7
  20. data/lib/theme_check/checks/valid_json.rb +1 -1
  21. data/lib/theme_check/checks.rb +2 -2
  22. data/lib/theme_check/cli.rb +1 -1
  23. data/lib/theme_check/corrector.rb +6 -6
  24. data/lib/theme_check/disabled_check.rb +3 -3
  25. data/lib/theme_check/disabled_checks.rb +9 -9
  26. data/lib/theme_check/html_node.rb +36 -28
  27. data/lib/theme_check/html_visitor.rb +6 -6
  28. data/lib/theme_check/in_memory_storage.rb +1 -1
  29. data/lib/theme_check/json_check.rb +2 -2
  30. data/lib/theme_check/language_server/diagnostics_tracker.rb +8 -8
  31. data/lib/theme_check/{template.rb → liquid_file.rb} +2 -2
  32. data/lib/theme_check/liquid_node.rb +291 -0
  33. data/lib/theme_check/{visitor.rb → liquid_visitor.rb} +4 -4
  34. data/lib/theme_check/locale_diff.rb +5 -5
  35. data/lib/theme_check/node.rb +12 -225
  36. data/lib/theme_check/offense.rb +15 -15
  37. data/lib/theme_check/position.rb +1 -1
  38. data/lib/theme_check/theme.rb +1 -1
  39. data/lib/theme_check/{template_rewriter.rb → theme_file_rewriter.rb} +1 -1
  40. data/lib/theme_check/version.rb +1 -1
  41. data/lib/theme_check.rb +11 -10
  42. data/theme-check.gemspec +1 -1
  43. metadata +8 -7
@@ -47,7 +47,7 @@ module ThemeCheck
47
47
  raise
48
48
  rescue => e
49
49
  node = args.first
50
- template = node.respond_to?(:template) ? node.template.relative_path : "?"
50
+ theme_file = node.respond_to?(:theme_file) ? node.theme_file.relative_path : "?"
51
51
  markup = node.respond_to?(:markup) ? node.markup : ""
52
52
  node_class = node.respond_to?(:value) ? node.value.class : "?"
53
53
  line_number = node.respond_to?(:line_number) ? node.line_number : "?"
@@ -59,7 +59,7 @@ module ThemeCheck
59
59
  #{e.backtrace.join("\n ")}
60
60
  ```
61
61
 
62
- Template: `#{template}`
62
+ Theme File: `#{theme_file}`
63
63
  Node: `#{node_class}`
64
64
  Markup:
65
65
  ```
@@ -186,7 +186,7 @@ module ThemeCheck
186
186
  storage = ThemeCheck::FileSystemStorage.new(@config.root, ignored_patterns: @config.ignored_patterns)
187
187
  theme = ThemeCheck::Theme.new(storage)
188
188
  if theme.all.empty?
189
- raise Abort, "No templates found."
189
+ raise Abort, "No theme files found."
190
190
  end
191
191
  analyzer = ThemeCheck::Analyzer.new(theme, @config.enabled_checks, @config.auto_correct)
192
192
  analyzer.analyze_theme
@@ -2,25 +2,25 @@
2
2
 
3
3
  module ThemeCheck
4
4
  class Corrector
5
- def initialize(template:)
6
- @template = template
5
+ def initialize(theme_file:)
6
+ @theme_file = theme_file
7
7
  end
8
8
 
9
9
  def insert_after(node, content)
10
- @template.rewriter.insert_after(node, content)
10
+ @theme_file.rewriter.insert_after(node, content)
11
11
  end
12
12
 
13
13
  def insert_before(node, content)
14
- @template.rewriter.insert_before(node, content)
14
+ @theme_file.rewriter.insert_before(node, content)
15
15
  end
16
16
 
17
17
  def replace(node, content)
18
- @template.rewriter.replace(node, content)
18
+ @theme_file.rewriter.replace(node, content)
19
19
  node.markup = content
20
20
  end
21
21
 
22
22
  def wrap(node, insert_before, insert_after)
23
- @template.rewriter.wrap(node, insert_before, insert_after)
23
+ @theme_file.rewriter.wrap(node, insert_before, insert_after)
24
24
  end
25
25
 
26
26
  def create(theme, relative_path, content)
@@ -4,11 +4,11 @@
4
4
  # We'll use the node position to figure out if the test is disabled or not.
5
5
  module ThemeCheck
6
6
  class DisabledCheck
7
- attr_reader :name, :template, :ranges
7
+ attr_reader :name, :theme_file, :ranges
8
8
  attr_accessor :first_line
9
9
 
10
- def initialize(template, name)
11
- @template = template
10
+ def initialize(theme_file, name)
11
+ @theme_file = theme_file
12
12
  @name = name
13
13
  @ranges = []
14
14
  @first_line = false
@@ -11,8 +11,8 @@ module ThemeCheck
11
11
 
12
12
  def initialize
13
13
  @disabled_checks = Hash.new do |hash, key|
14
- template, check_name = key
15
- hash[key] = DisabledCheck.new(template, check_name)
14
+ theme_file, check_name = key
15
+ hash[key] = DisabledCheck.new(theme_file, check_name)
16
16
  end
17
17
  end
18
18
 
@@ -20,26 +20,26 @@ module ThemeCheck
20
20
  text = comment_text(node)
21
21
  if start_disabling?(text)
22
22
  checks_from_text(text).each do |check_name|
23
- disabled = @disabled_checks[[node.template, check_name]]
23
+ disabled = @disabled_checks[[node.theme_file, check_name]]
24
24
  disabled.start_index = node.start_index
25
25
  disabled.first_line = true if node.line_number == 1
26
26
  end
27
27
  elsif stop_disabling?(text)
28
28
  checks_from_text(text).each do |check_name|
29
- disabled = @disabled_checks[[node.template, check_name]]
29
+ disabled = @disabled_checks[[node.theme_file, check_name]]
30
30
  next unless disabled
31
31
  disabled.end_index = node.end_index
32
32
  end
33
33
  end
34
34
  end
35
35
 
36
- def disabled?(check, template, check_name, index)
36
+ def disabled?(check, theme_file, check_name, index)
37
37
  return true if check.ignored_patterns&.any? do |pattern|
38
- template.relative_path.fnmatch?(pattern)
38
+ theme_file.relative_path.fnmatch?(pattern)
39
39
  end
40
40
 
41
- @disabled_checks[[template, :all]]&.disabled?(index) ||
42
- @disabled_checks[[template, check_name]]&.disabled?(index)
41
+ @disabled_checks[[theme_file, :all]]&.disabled?(index) ||
42
+ @disabled_checks[[theme_file, check_name]]&.disabled?(index)
43
43
  end
44
44
 
45
45
  def checks_missing_end_index
@@ -51,7 +51,7 @@ module ThemeCheck
51
51
  def remove_disabled_offenses(checks)
52
52
  checks.disableable.each do |check|
53
53
  check.offenses.reject! do |offense|
54
- disabled?(check, offense.template, offense.code_name, offense.start_index)
54
+ disabled?(check, offense.theme_file, offense.code_name, offense.start_index)
55
55
  end
56
56
  end
57
57
  end
@@ -2,18 +2,50 @@
2
2
  require "forwardable"
3
3
 
4
4
  module ThemeCheck
5
- class HtmlNode
5
+ class HtmlNode < Node
6
6
  extend Forwardable
7
7
  include RegexHelpers
8
- attr_reader :template, :parent
8
+ attr_reader :theme_file, :parent
9
9
 
10
- def initialize(value, template, placeholder_values = [], parent = nil)
10
+ def initialize(value, theme_file, placeholder_values = [], parent = nil)
11
11
  @value = value
12
- @template = template
12
+ @theme_file = theme_file
13
13
  @placeholder_values = placeholder_values
14
14
  @parent = parent
15
15
  end
16
16
 
17
+ # @value is not forwarded because we _need_ to replace the
18
+ # placeholders for the HtmlNode to make sense.
19
+ def value
20
+ if literal?
21
+ content
22
+ else
23
+ markup
24
+ end
25
+ end
26
+
27
+ def children
28
+ @children ||= @value
29
+ .children
30
+ .map { |child| HtmlNode.new(child, theme_file, @placeholder_values, self) }
31
+ end
32
+
33
+ def markup
34
+ @markup ||= replace_placeholders(@value.to_html)
35
+ end
36
+
37
+ def line_number
38
+ @value.line
39
+ end
40
+
41
+ def start_index
42
+ raise NotImplementedError
43
+ end
44
+
45
+ def end_index
46
+ raise NotImplementedError
47
+ end
48
+
17
49
  def literal?
18
50
  @value.name == "text"
19
51
  end
@@ -22,12 +54,6 @@ module ThemeCheck
22
54
  @value.element?
23
55
  end
24
56
 
25
- def children
26
- @children ||= @value
27
- .children
28
- .map { |child| HtmlNode.new(child, template, @placeholder_values, self) }
29
- end
30
-
31
57
  def attributes
32
58
  @attributes ||= @value.attributes
33
59
  .map { |k, v| [replace_placeholders(k), replace_placeholders(v.value)] }
@@ -38,16 +64,6 @@ module ThemeCheck
38
64
  @content ||= replace_placeholders(@value.content)
39
65
  end
40
66
 
41
- # @value is not forwarded because we _need_ to replace the
42
- # placeholders for the HtmlNode to make sense.
43
- def value
44
- if literal?
45
- content
46
- else
47
- markup
48
- end
49
- end
50
-
51
67
  def name
52
68
  if @value.name == "#document-fragment"
53
69
  "document"
@@ -56,14 +72,6 @@ module ThemeCheck
56
72
  end
57
73
  end
58
74
 
59
- def markup
60
- @markup ||= replace_placeholders(@value.to_html)
61
- end
62
-
63
- def line_number
64
- @value.line
65
- end
66
-
67
75
  private
68
76
 
69
77
  def replace_placeholders(string)
@@ -11,18 +11,18 @@ module ThemeCheck
11
11
  @checks = checks
12
12
  end
13
13
 
14
- def visit_template(template)
15
- doc, placeholder_values = parse(template)
16
- visit(HtmlNode.new(doc, template, placeholder_values))
14
+ def visit_liquid_file(liquid_file)
15
+ doc, placeholder_values = parse(liquid_file)
16
+ visit(HtmlNode.new(doc, liquid_file, placeholder_values))
17
17
  rescue ArgumentError => e
18
- call_checks(:on_parse_error, e, template)
18
+ call_checks(:on_parse_error, e, liquid_file)
19
19
  end
20
20
 
21
21
  private
22
22
 
23
- def parse(template)
23
+ def parse(liquid_file)
24
24
  placeholder_values = []
25
- parseable_source = +template.source.clone
25
+ parseable_source = +liquid_file.source.clone
26
26
 
27
27
  # Replace all non-empty liquid tags with ≬{i}######≬ to prevent the HTML
28
28
  # parser from freaking out. We transparently replace those placeholders in
@@ -2,7 +2,7 @@
2
2
 
3
3
  # An in-memory storage is not written to disk. The reasons why you'd
4
4
  # want to do that are your own. The idea is to not write to disk
5
- # something that doesn't need to be there. If you have your template
5
+ # something that doesn't need to be there. If you have your theme
6
6
  # as a big hash already, leave it like that and save yourself some IO.
7
7
  module ThemeCheck
8
8
  class InMemoryStorage < Storage
@@ -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, &block)
8
- offenses << Offense.new(check: self, message: message, markup: markup, line_number: line_number, template: template, correction: block)
7
+ def add_offense(message, markup: nil, line_number: nil, theme_file: nil, &block)
8
+ offenses << Offense.new(check: self, message: message, markup: markup, line_number: line_number, theme_file: theme_file, correction: block)
9
9
  end
10
10
  end
11
11
  end
@@ -19,22 +19,22 @@ module ThemeCheck
19
19
  new_single_file_offenses = {}
20
20
  analyzed_files = analyzed_files.map { |path| Pathname.new(path) } if analyzed_files
21
21
 
22
- offenses.group_by(&:template).each do |template, template_offenses|
23
- next unless template
22
+ offenses.group_by(&:theme_file).each do |theme_file, template_offenses|
23
+ next unless theme_file
24
24
  reported_offenses = template_offenses
25
- previous_offenses = @single_files_offenses[template.path]
26
- if analyzed_files.nil? || analyzed_files.include?(template.path)
25
+ previous_offenses = @single_files_offenses[theme_file.path]
26
+ if analyzed_files.nil? || analyzed_files.include?(theme_file.path)
27
27
  # We re-analyzed the file, so we know the template_offenses are update to date.
28
28
  reported_single_file_offenses = reported_offenses.select(&:single_file?)
29
29
  if reported_single_file_offenses.any?
30
- new_single_file_offenses[template.path] = reported_single_file_offenses
30
+ new_single_file_offenses[theme_file.path] = reported_single_file_offenses
31
31
  end
32
32
  elsif previous_offenses
33
33
  # Merge in the previous ones, if some
34
34
  reported_offenses |= previous_offenses
35
35
  end
36
- yield template.path, reported_offenses
37
- reported_files << template.path
36
+ yield theme_file.path, reported_offenses
37
+ reported_files << theme_file.path
38
38
  end
39
39
 
40
40
  @single_files_offenses.each do |path, _|
@@ -51,7 +51,7 @@ module ThemeCheck
51
51
  reported_files << path
52
52
  end
53
53
 
54
- # Publish diagnostics with empty array if all issues on a previously reported template
54
+ # Publish diagnostics with empty array if all issues on a previously reported theme_file
55
55
  # have been fixed.
56
56
  (@previously_reported_files - reported_files).each do |path|
57
57
  yield path, []
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ThemeCheck
4
- class Template < ThemeFile
4
+ class LiquidFile < ThemeFile
5
5
  def write
6
6
  content = rewriter.to_s
7
7
  if source != content
@@ -28,7 +28,7 @@ module ThemeCheck
28
28
  end
29
29
 
30
30
  def rewriter
31
- @rewriter ||= TemplateRewriter.new(@relative_path, source)
31
+ @rewriter ||= ThemeFileRewriter.new(@relative_path, source)
32
32
  end
33
33
 
34
34
  def source_excerpt(line)
@@ -0,0 +1,291 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ # A node from the Liquid AST, the result of parsing a liquid file.
5
+ class LiquidNode < Node
6
+ attr_reader :value, :parent, :theme_file
7
+
8
+ def initialize(value, parent, theme_file)
9
+ raise ArgumentError, "Expected a Liquid AST Node" if value.is_a?(LiquidNode)
10
+ @value = value
11
+ @parent = parent
12
+ @theme_file = theme_file
13
+ @tag_markup = nil
14
+ @line_number_offset = 0
15
+ end
16
+
17
+ # Array of children nodes.
18
+ def children
19
+ @children ||= begin
20
+ nodes =
21
+ if comment?
22
+ []
23
+ elsif defined?(@value.class::ParseTreeVisitor)
24
+ @value.class::ParseTreeVisitor.new(@value, {}).children
25
+ elsif @value.respond_to?(:nodelist)
26
+ Array(@value.nodelist)
27
+ else
28
+ []
29
+ end
30
+ # Work around a bug in Liquid::Variable::ParseTreeVisitor that doesn't return
31
+ # the args in a hash as children nodes.
32
+ nodes = nodes.flat_map do |node|
33
+ case node
34
+ when Hash
35
+ node.values
36
+ else
37
+ node
38
+ end
39
+ end
40
+ nodes.map { |node| LiquidNode.new(node, self, @theme_file) }
41
+ end
42
+ end
43
+
44
+ # The original source code of the node. Doesn't contain wrapping braces.
45
+ def markup
46
+ if tag?
47
+ tag_markup
48
+ elsif @value.instance_variable_defined?(:@markup)
49
+ @value.instance_variable_get(:@markup)
50
+ end
51
+ end
52
+
53
+ def markup=(markup)
54
+ if @value.instance_variable_defined?(:@markup)
55
+ @value.instance_variable_set(:@markup, markup)
56
+ end
57
+ end
58
+
59
+ # Most nodes have a line number, but it's not guaranteed.
60
+ def line_number
61
+ if tag? && @value.respond_to?(:line_number)
62
+ markup # initialize the line_number_offset
63
+ @value.line_number - @line_number_offset
64
+ elsif @value.respond_to?(:line_number)
65
+ @value.line_number
66
+ end
67
+ end
68
+
69
+ def start_index
70
+ position.start_index
71
+ end
72
+
73
+ def end_index
74
+ position.end_index
75
+ end
76
+
77
+ # Literals are hard-coded values in the liquid file.
78
+ def literal?
79
+ @value.is_a?(String) || @value.is_a?(Integer)
80
+ end
81
+
82
+ # A {% tag %} node?
83
+ def tag?
84
+ @value.is_a?(Liquid::Tag)
85
+ end
86
+
87
+ def variable?
88
+ @value.is_a?(Liquid::Variable)
89
+ end
90
+
91
+ # A {% comment %} block node?
92
+ def comment?
93
+ @value.is_a?(Liquid::Comment)
94
+ end
95
+
96
+ # Top level node of every liquid_file.
97
+ def document?
98
+ @value.is_a?(Liquid::Document)
99
+ end
100
+ alias_method :root?, :document?
101
+
102
+ # A {% tag %}...{% endtag %} node?
103
+ def block_tag?
104
+ @value.is_a?(Liquid::Block)
105
+ end
106
+
107
+ # The body of blocks
108
+ def block_body?
109
+ @value.is_a?(Liquid::BlockBody)
110
+ end
111
+
112
+ # A block of type of node?
113
+ def block?
114
+ block_tag? || block_body? || document?
115
+ end
116
+
117
+ # The `:under_score_name` of this type of node. Used to dispatch to the `on_<type_name>`
118
+ # and `after_<type_name>` check methods.
119
+ def type_name
120
+ @type_name ||= StringHelpers.underscore(StringHelpers.demodulize(@value.class.name)).to_sym
121
+ end
122
+
123
+ def source
124
+ theme_file&.source
125
+ end
126
+
127
+ WHITESPACE = /\s/
128
+
129
+ # Is this node inside a `{% liquid ... %}` block?
130
+ def inside_liquid_tag?
131
+ # What we're doing here is starting at the start of the tag and
132
+ # backtrack on all the whitespace until we land on something. If
133
+ # that something is {% or %-, then we can safely assume that
134
+ # we're inside a full tag and not a liquid tag.
135
+ @inside_liquid_tag ||= if tag? && start_index && source
136
+ i = 1
137
+ i += 1 while source[start_index - i] =~ WHITESPACE && i < start_index
138
+ first_two_backtracked_characters = source[(start_index - i - 1)..(start_index - i)]
139
+ first_two_backtracked_characters != "{%" && first_two_backtracked_characters != "%-"
140
+ else
141
+ false
142
+ end
143
+ end
144
+
145
+ # Is this node inside a tag or variable that starts by removing whitespace. i.e. {%- or {{-
146
+ def whitespace_trimmed_start?
147
+ @whitespace_trimmed_start ||= if start_index && source && !inside_liquid_tag?
148
+ i = 1
149
+ i += 1 while source[start_index - i] =~ WHITESPACE && i < start_index
150
+ source[start_index - i] == "-"
151
+ else
152
+ false
153
+ end
154
+ end
155
+
156
+ # Is this node inside a tag or variable ends starts by removing whitespace. i.e. -%} or -}}
157
+ def whitespace_trimmed_end?
158
+ @whitespace_trimmed_end ||= if end_index && source && !inside_liquid_tag?
159
+ i = 0
160
+ i += 1 while source[end_index + i] =~ WHITESPACE && i < source.size
161
+ source[end_index + i] == "-"
162
+ else
163
+ false
164
+ end
165
+ end
166
+
167
+ def start_token
168
+ return "" if inside_liquid_tag?
169
+ output = ""
170
+ output += "{{" if variable?
171
+ output += "{%" if tag?
172
+ output += "-" if whitespace_trimmed_start?
173
+ output
174
+ end
175
+
176
+ def end_token
177
+ return "" if inside_liquid_tag?
178
+ output = ""
179
+ output += "-" if whitespace_trimmed_end?
180
+ output += "}}" if variable?
181
+ output += "%}" if tag?
182
+ output
183
+ end
184
+
185
+ private
186
+
187
+ def position
188
+ @position ||= Position.new(
189
+ markup,
190
+ theme_file&.source,
191
+ line_number_1_indexed: line_number
192
+ )
193
+ end
194
+
195
+ # Here we're hacking around a glorious bug in Liquid that makes it so the
196
+ # line_number and markup of a tag is wrong if there's whitespace
197
+ # between the tag_name and the markup of the tag.
198
+ #
199
+ # {%
200
+ # render
201
+ # 'foo'
202
+ # %}
203
+ #
204
+ # Returns a raw value of "render 'foo'\n".
205
+ # The "\n " between render and 'foo' got replaced by a single space.
206
+ #
207
+ # And the line number is the one of 'foo'\n%}. Yay!
208
+ #
209
+ # This breaks any kind of position logic we have since that string
210
+ # does not exist in the theme_file.
211
+ def tag_markup
212
+ return @tag_markup if @tag_markup
213
+
214
+ l = 1
215
+ scanner = StringScanner.new(source)
216
+ scanner.scan_until(/\n/) while l < @value.line_number && (l += 1)
217
+ start = scanner.charpos
218
+
219
+ tag_name = @value.tag_name
220
+ tag_markup = @value.instance_variable_get('@markup')
221
+
222
+ # This is tricky, if the tag_markup is empty, then the tag could
223
+ # either start on a previous line, or the tag could start on the
224
+ # same line.
225
+ #
226
+ # Consider this:
227
+ # 1 {%
228
+ # 2 comment
229
+ # 3 %}{% endcomment %}{%comment%}
230
+ #
231
+ # Both comments would markup == "" AND line_number == 3
232
+ #
233
+ # There's no way to determine which one is the correct one, but
234
+ # we'll try our best to at least give you one.
235
+ #
236
+ # To screw with you even more, the name of the tag could be
237
+ # outside of a tag on the same line :) But I won't do anything
238
+ # about that (yet?).
239
+ #
240
+ # {% comment
241
+ # %}comment{% endcomment %}
242
+ if tag_markup.empty?
243
+ eol = source.index("\n", start) || source.size
244
+
245
+ # OK here I'm trying one of two things. Either tag_start is on
246
+ # the same line OR tag_start is on a previous line. The line
247
+ # number would be at the end of the whitespace after tag_name.
248
+ unless (tag_start = source.index(tag_name, start)) && tag_start < eol
249
+ tag_start = start
250
+ tag_start -= 1 while source[tag_start - 1] =~ WHITESPACE
251
+ tag_start -= @value.tag_name.size
252
+
253
+ # keep track of the error in line_number
254
+ @line_number_offset = source[tag_start...start].count("\n")
255
+ end
256
+ tag_end = tag_start + tag_name.size
257
+ tag_end += 1 while source[tag_end] =~ WHITESPACE
258
+
259
+ # return the real raw content
260
+ @tag_markup = source[tag_start...tag_end]
261
+ return @tag_markup
262
+
263
+ # See https://github.com/Shopify/theme-check/pull/423/files#r701936559 for a detailed explanation
264
+ # of why we're doing the check below.
265
+ #
266
+ # TL;DR it's because line_numbers are not enough to accurately
267
+ # determine the position of the raw markup and because that
268
+ # markup could be present on the same line outside of a Tag. e.g.
269
+ #
270
+ # uhoh {% if uhoh %}
271
+ elsif (match = /#{tag_name} +#{Regexp.escape(tag_markup)}/.match(source, start))
272
+ return @tag_markup = match[0]
273
+ end
274
+
275
+ # find the markup
276
+ markup_start = source.index(tag_markup, start)
277
+ markup_end = markup_start + tag_markup.size
278
+
279
+ # go back until you find the tag_name
280
+ tag_start = markup_start
281
+ tag_start -= 1 while source[tag_start - 1] =~ WHITESPACE
282
+ tag_start -= tag_name.size
283
+
284
+ # keep track of the error in line_number
285
+ @line_number_offset = source[tag_start...markup_start].count("\n")
286
+
287
+ # return the real raw content
288
+ @tag_markup = source[tag_start...markup_end]
289
+ end
290
+ end
291
+ end