theme-check 1.6.1 → 1.6.2

Sign up to get free protection for your applications and to get access to all the features.
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