better_html 0.0.12 → 1.0.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/better_html.rb +0 -2
  3. data/lib/better_html/ast/iterator.rb +32 -0
  4. data/lib/better_html/ast/node.rb +14 -0
  5. data/lib/better_html/better_erb/runtime_checks.rb +3 -3
  6. data/lib/better_html/config.rb +12 -0
  7. data/lib/better_html/parser.rb +286 -0
  8. data/lib/better_html/test_helper/ruby_expr.rb +8 -5
  9. data/lib/better_html/test_helper/safe_erb_tester.rb +121 -108
  10. data/lib/better_html/test_helper/safe_lodash_tester.rb +44 -42
  11. data/lib/better_html/tokenizer/base_erb.rb +79 -0
  12. data/lib/better_html/tokenizer/html_erb.rb +31 -0
  13. data/lib/better_html/{node_iterator → tokenizer}/html_lodash.rb +30 -34
  14. data/lib/better_html/tokenizer/javascript_erb.rb +15 -0
  15. data/lib/better_html/{node_iterator → tokenizer}/location.rb +9 -3
  16. data/lib/better_html/tokenizer/token.rb +16 -0
  17. data/lib/better_html/tokenizer/token_array.rb +54 -0
  18. data/lib/better_html/tree/attribute.rb +31 -0
  19. data/lib/better_html/tree/attributes_list.rb +25 -0
  20. data/lib/better_html/tree/tag.rb +39 -0
  21. data/lib/better_html/version.rb +1 -1
  22. data/test/better_html/parser_test.rb +279 -0
  23. data/test/better_html/test_helper/safe_erb_tester_test.rb +11 -0
  24. data/test/better_html/test_helper/safe_lodash_tester_test.rb +11 -1
  25. data/test/better_html/tokenizer/html_erb_test.rb +158 -0
  26. data/test/better_html/tokenizer/html_lodash_test.rb +98 -0
  27. data/test/better_html/tokenizer/location_test.rb +57 -0
  28. data/test/better_html/tokenizer/token_array_test.rb +144 -0
  29. data/test/better_html/tokenizer/token_test.rb +15 -0
  30. metadata +45 -30
  31. data/lib/better_html/node_iterator.rb +0 -144
  32. data/lib/better_html/node_iterator/attribute.rb +0 -34
  33. data/lib/better_html/node_iterator/base.rb +0 -27
  34. data/lib/better_html/node_iterator/cdata.rb +0 -8
  35. data/lib/better_html/node_iterator/comment.rb +0 -8
  36. data/lib/better_html/node_iterator/content_node.rb +0 -13
  37. data/lib/better_html/node_iterator/element.rb +0 -26
  38. data/lib/better_html/node_iterator/html_erb.rb +0 -70
  39. data/lib/better_html/node_iterator/javascript_erb.rb +0 -55
  40. data/lib/better_html/node_iterator/text.rb +0 -8
  41. data/lib/better_html/node_iterator/token.rb +0 -8
  42. data/lib/better_html/tree.rb +0 -113
  43. data/test/better_html/node_iterator/html_erb_test.rb +0 -116
  44. data/test/better_html/node_iterator/html_lodash_test.rb +0 -132
  45. data/test/better_html/node_iterator/location_test.rb +0 -36
  46. data/test/better_html/node_iterator_test.rb +0 -221
  47. data/test/better_html/tree_test.rb +0 -110
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b31eee572de6668e1791c716a07c9d93658aa566
4
- data.tar.gz: 6dd1906d36f68f59fbe4d6386f0d0e7e94ce244b
3
+ metadata.gz: 16bc95c4f55d720a177e9849dea00e7d83c66455
4
+ data.tar.gz: 376b80e51f2379ce6c5640e834de1c62aa990ae5
5
5
  SHA512:
6
- metadata.gz: 1c96cfded69fd94221810490e37b279da7e57323062a06416dd3522d8b58971cd2bb727690d0373af647871170698ef4d4b8830f13e8fa3538699224b9c33dc8
7
- data.tar.gz: 49d55494ae87ce77e2cd45883a8b37b1c999badf3fc8e0a7c06ba4631a4c1d0a698243a4810ec2f6fbd80f88508b3ed4b61e0e54e1d8c08a036c01bea664aa14
6
+ metadata.gz: bf07d75143cfde43414bb3e8a5c2520b527f047d0c796c7ff97e60aa2d23d0bc03ea1f06c72eab5db5f4c10cdb01eccf0c6abe585b0ae394dc662731599e296b
7
+ data.tar.gz: b7dd6bfc56b0a36d773dabbe13a7ad922bc9c5cd322f4a240ba8d9064c8753ec4efca3fb5b1a7c63d36291b60dd48ec142521b3af98811f3a017d5d62f90ab97
@@ -21,7 +21,5 @@ require 'better_html/config'
21
21
  require 'better_html/helpers'
22
22
  require 'better_html/errors'
23
23
  require 'better_html/html_attributes'
24
- require 'better_html/node_iterator'
25
- require 'better_html/tree'
26
24
 
27
25
  require 'better_html/railtie' if defined?(Rails)
@@ -0,0 +1,32 @@
1
+ require 'ast'
2
+ require 'active_support/core_ext/array/wrap'
3
+
4
+ module BetterHtml
5
+ module AST
6
+ class Iterator
7
+ def initialize(types, &block)
8
+ @types = Array.wrap(types)
9
+ @block = block
10
+ end
11
+
12
+ def traverse(node)
13
+ return unless node.is_a?(::AST::Node)
14
+ @block.call(node) if @types.include?(node.type)
15
+ traverse_all(node)
16
+ end
17
+
18
+ def traverse_all(nodes)
19
+ nodes.to_a.each do |node|
20
+ traverse(node) if node.is_a?(::AST::Node)
21
+ end
22
+ end
23
+
24
+ def self.descendants(root_node, type, &block)
25
+ Enumerator.new do |yielder|
26
+ t = new(type) { |node| yielder << node }
27
+ t.traverse(root_node)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,14 @@
1
+ require 'ast'
2
+ require_relative 'iterator'
3
+
4
+ module BetterHtml
5
+ module AST
6
+ class Node < ::AST::Node
7
+ attr_reader :loc
8
+
9
+ def descendants(type, &block)
10
+ AST::Iterator.descendants(self, type, &block)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -111,7 +111,7 @@ class BetterHtml::BetterErb
111
111
  end
112
112
 
113
113
  def check_tag_name(type, start, stop, line, column)
114
- text = @parser.extract(start, stop)
114
+ text = @parser.document[start...stop]
115
115
  return if text.upcase == "!DOCTYPE"
116
116
  return if @config.partial_tag_name_pattern === text
117
117
 
@@ -122,7 +122,7 @@ class BetterHtml::BetterErb
122
122
  end
123
123
 
124
124
  def check_attribute_name(type, start, stop, line, column)
125
- text = @parser.extract(start, stop)
125
+ text = @parser.document[start...stop]
126
126
  return if @config.partial_attribute_name_pattern === text
127
127
 
128
128
  s = "Invalid attribute name #{text.inspect} does not match "\
@@ -133,7 +133,7 @@ class BetterHtml::BetterErb
133
133
 
134
134
  def check_quoted_value(type, start, stop, line, column)
135
135
  return if @config.allow_single_quoted_attributes
136
- text = @parser.extract(start, stop)
136
+ text = @parser.document[start...stop]
137
137
  return if text == '"'
138
138
 
139
139
  s = "Single-quoted attributes are not allowed\n"
@@ -12,5 +12,17 @@ module BetterHtml
12
12
  property :javascript_attribute_names, default: [/\Aon/i]
13
13
  property :template_exclusion_filter
14
14
  property :lodash_safe_javascript_expression, default: [/\AJSON\.stringify\(/]
15
+
16
+ def javascript_attribute_name?(name)
17
+ javascript_attribute_names.any?{ |other| other === name.to_s }
18
+ end
19
+
20
+ def lodash_safe_javascript_expression?(code)
21
+ lodash_safe_javascript_expression.any?{ |other| other === code }
22
+ end
23
+
24
+ def javascript_safe_method?(name)
25
+ javascript_safe_methods.include?(name.to_s)
26
+ end
15
27
  end
16
28
  end
@@ -0,0 +1,286 @@
1
+ require_relative 'tokenizer/javascript_erb'
2
+ require_relative 'tokenizer/html_erb'
3
+ require_relative 'tokenizer/html_lodash'
4
+ require_relative 'tokenizer/location'
5
+ require_relative 'tokenizer/token_array'
6
+ require_relative 'ast/node'
7
+
8
+ module BetterHtml
9
+ class Parser
10
+ attr_reader :template_language
11
+
12
+ def initialize(document, template_language: :html)
13
+ @document = document
14
+ @template_language = template_language
15
+ @erb = case template_language
16
+ when :html
17
+ Tokenizer::HtmlErb.new(@document)
18
+ when :lodash
19
+ Tokenizer::HtmlLodash.new(@document)
20
+ when :javascript
21
+ Tokenizer::JavascriptErb.new(@document)
22
+ else
23
+ raise ArgumentError, "template_language can be :html or :javascript"
24
+ end
25
+ end
26
+
27
+ def nodes_with_type(*type)
28
+ types = Array.wrap(type)
29
+ ast.children.select{ |node| node.is_a?(::AST::Node) && types.include?(node.type) }
30
+ end
31
+
32
+ def ast
33
+ @ast ||= build_document_node
34
+ end
35
+
36
+ def inspect
37
+ "#<#{self.class.name} ast=#{ast.inspect}>"
38
+ end
39
+
40
+ private
41
+
42
+ INTERPOLATION_TYPES = [:erb_begin, :lodash_begin]
43
+
44
+ def build_document_node
45
+ children = []
46
+ tokens = Tokenizer::TokenArray.new(@erb.tokens)
47
+ while tokens.any?
48
+ case tokens.current.type
49
+ when :cdata_start
50
+ children << build_cdata_node(tokens)
51
+ when :comment_start
52
+ children << build_comment_node(tokens)
53
+ when :tag_start
54
+ children << build_tag_node(tokens)
55
+ when :text, *INTERPOLATION_TYPES
56
+ children << build_text_node(tokens)
57
+ else
58
+ raise RuntimeError, "Unhandled token #{tokens.current.type} line #{tokens.current.loc.line} column #{tokens.current.loc.column}, #{children.inspect}"
59
+ end
60
+ end
61
+
62
+ build_node(:document, children.empty? ? nil : children)
63
+ end
64
+
65
+ def build_erb_node(tokens)
66
+ erb_begin = shift_single(tokens, :erb_begin)
67
+ children = [
68
+ shift_single(tokens, :indicator),
69
+ shift_single(tokens, :trim),
70
+ shift_single(tokens, :code),
71
+ shift_single(tokens, :trim),
72
+ ]
73
+ erb_end = shift_single(tokens, :erb_end)
74
+
75
+ build_node(:erb, children, pre: erb_begin, post: erb_end)
76
+ end
77
+
78
+ def build_lodash_node(tokens)
79
+ lodash_begin = shift_single(tokens, :lodash_begin)
80
+ children = [
81
+ shift_single(tokens, :indicator),
82
+ shift_single(tokens, :code),
83
+ ]
84
+ lodash_end = shift_single(tokens, :lodash_end)
85
+
86
+ build_node(:lodash, children, pre: lodash_begin, post: lodash_end)
87
+ end
88
+
89
+ def build_cdata_node(tokens)
90
+ cdata_start, children, cdata_end = shift_between_with_interpolation(tokens, :cdata_start, :cdata_end)
91
+ build_node(:cdata, children, pre: cdata_start, post: cdata_end)
92
+ end
93
+
94
+ def build_comment_node(tokens)
95
+ comment_start, children, comment_end = shift_between_with_interpolation(tokens, :comment_start, :comment_end)
96
+ build_node(:comment, children, pre: comment_start, post: comment_end)
97
+ end
98
+
99
+ def build_tag_node(tokens)
100
+ tag_start, tag_content, tag_end = shift_between(tokens, :tag_start, :tag_end)
101
+ tag_tokens = Tokenizer::TokenArray.new(tag_content)
102
+ tag_tokens.trim(:whitespace)
103
+
104
+ children = [
105
+ shift_single(tag_tokens, :solidus),
106
+ build_tag_name_node(tag_tokens),
107
+ build_tag_attributes_node(tag_tokens),
108
+ shift_single(tag_tokens, :solidus),
109
+ ]
110
+
111
+ build_node(:tag, children, pre: tag_start, post: tag_end)
112
+ end
113
+
114
+ def build_tag_name_node(tokens)
115
+ children = shift_all_with_interpolation(tokens, :tag_name)
116
+ build_node(:tag_name, children) if children.any?
117
+ end
118
+
119
+ def build_tag_attributes_node(tokens)
120
+ attributes_tokens = []
121
+ while tokens.any?
122
+ break if tokens.size == 1 && tokens.last.type == :solidus
123
+ if tokens.current.type == :attribute_name
124
+ attributes_tokens << build_attribute_node(tokens)
125
+ elsif tokens.current.type == :attribute_quoted_value_start
126
+ attributes_tokens << build_nameless_attribute_node(tokens)
127
+ else
128
+ # todo: warn about ignored things
129
+ tokens.shift
130
+ end
131
+ end
132
+
133
+ build_node(:tag_attributes, attributes_tokens) if attributes_tokens.any?
134
+ end
135
+
136
+ def build_nameless_attribute_node(tokens)
137
+ value_node = build_attribute_value_node(tokens)
138
+ build_node(:attribute, [nil, nil, value_node])
139
+ end
140
+
141
+ def build_attribute_node(tokens)
142
+ name_node = build_attribute_name_node(tokens)
143
+ shift_all(tokens, :whitespace)
144
+ equal_token = shift_single(tokens, :equal)
145
+ shift_all(tokens, :whitespace)
146
+ value_node = build_attribute_value_node(tokens) if equal_token.present?
147
+
148
+ build_node(:attribute, [name_node, equal_token, value_node])
149
+ end
150
+
151
+ def build_attribute_name_node(tokens)
152
+ children = shift_all_with_interpolation(tokens, :attribute_name)
153
+ build_node(:attribute_name, children)
154
+ end
155
+
156
+ def build_attribute_value_node(tokens)
157
+ children = shift_all_with_interpolation(tokens,
158
+ :attribute_quoted_value_start, :attribute_quoted_value,
159
+ :attribute_quoted_value_end, :attribute_unquoted_value
160
+ )
161
+
162
+ build_node(:attribute_value, children)
163
+ end
164
+
165
+ def build_text_node(tokens)
166
+ text_tokens = shift_all_with_interpolation(tokens, :text)
167
+ build_node(:text, text_tokens)
168
+ end
169
+
170
+ def build_node(type, tokens, pre: nil, post: nil)
171
+ BetterHtml::AST::Node.new(
172
+ type,
173
+ tokens.present? ? wrap_tokens(tokens) : [],
174
+ loc: tokens.present? ? build_location([pre, *tokens, post]) : empty_location
175
+ )
176
+ end
177
+
178
+ def build_location(enumerable)
179
+ enumerable = enumerable.compact
180
+ raise ArgumentError, "cannot build location for #{enumerable.inspect}" unless enumerable.first && enumerable.last
181
+ Tokenizer::Location.new(@document, enumerable.first.loc.start, enumerable.last.loc.stop)
182
+ end
183
+
184
+ def empty_location
185
+ Tokenizer::Location.new(@document, 0, 0)
186
+ end
187
+
188
+ def shift_all(tokens, *types)
189
+ [].tap do |items|
190
+ while tokens.any?
191
+ if types.include?(tokens.current.type)
192
+ items << tokens.shift
193
+ else
194
+ break
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ def shift_single(tokens, *types)
201
+ tokens.shift if tokens.any? && types.include?(tokens.current.type)
202
+ end
203
+
204
+ def shift_until(tokens, *types)
205
+ [].tap do |items|
206
+ while tokens.any?
207
+ if !types.include?(tokens.current.type)
208
+ items << tokens.shift
209
+ else
210
+ break
211
+ end
212
+ end
213
+ end
214
+ end
215
+
216
+ def build_interpolation_node(tokens)
217
+ if tokens.current.type == :erb_begin
218
+ build_erb_node(tokens)
219
+ elsif tokens.current.type == :lodash_begin
220
+ build_lodash_node(tokens)
221
+ else
222
+ tokens.shift
223
+ end
224
+ end
225
+
226
+ def shift_all_with_interpolation(tokens, *types)
227
+ types = [*INTERPOLATION_TYPES, *types]
228
+ [].tap do |result|
229
+ while tokens.any?
230
+ if types.include?(tokens.current.type)
231
+ result << build_interpolation_node(tokens)
232
+ else
233
+ break
234
+ end
235
+ end
236
+ end
237
+ end
238
+
239
+ def shift_until_with_interpolation(tokens, *types)
240
+ [].tap do |result|
241
+ while tokens.any?
242
+ if !types.include?(tokens.current.type)
243
+ result << build_interpolation_node(tokens)
244
+ else
245
+ break
246
+ end
247
+ end
248
+ end
249
+ end
250
+
251
+ def shift_between(tokens, start_type, end_type)
252
+ start_token = shift_single(tokens, start_type)
253
+ children = shift_until(tokens, end_type)
254
+ end_token = shift_single(tokens, end_type)
255
+
256
+ [start_token, children, end_token]
257
+ end
258
+
259
+ def shift_between_with_interpolation(tokens, start_type, end_type)
260
+ start_token = shift_single(tokens, start_type)
261
+ children = shift_until_with_interpolation(tokens, end_type)
262
+ end_token = shift_single(tokens, end_type)
263
+
264
+ [start_token, children, end_token]
265
+ end
266
+
267
+ def wrap_token(object)
268
+ return unless object
269
+ if object.is_a?(::AST::Node)
270
+ object
271
+ elsif [:text, :tag_name, :attribute_name, :attribute_quoted_value, :attribute_unquoted_value].include?(object.type)
272
+ object.loc.source
273
+ elsif [:attribute_quoted_value_start, :attribute_quoted_value_end].include?(object.type)
274
+ BetterHtml::AST::Node.new(:quote, [object.loc.source], loc: object.loc)
275
+ elsif [:indicator, :code].include?(object.type)
276
+ BetterHtml::AST::Node.new(object.type, [object.loc.source], loc: object.loc)
277
+ else
278
+ BetterHtml::AST::Node.new(object.type, [], loc: object.loc)
279
+ end
280
+ end
281
+
282
+ def wrap_tokens(enumerable)
283
+ enumerable.map { |object| wrap_token(object) }
284
+ end
285
+ end
286
+ end
@@ -22,14 +22,17 @@ module BetterHtml
22
22
  end
23
23
 
24
24
  def initialize(ast)
25
- raise ArgumentError, "expect first argument to be Parser::AST::Node" unless ast.is_a?(Parser::AST::Node)
25
+ raise ArgumentError, "expect first argument to be Parser::AST::Node" unless ast.is_a?(::Parser::AST::Node)
26
26
  @ast = ast
27
27
  end
28
28
 
29
29
  def self.parse(code)
30
- parser = Parser::CurrentRuby.new
31
- parser.diagnostics.consumer = lambda { |diag| }
32
- buf = Parser::Source::Buffer.new('(string)')
30
+ parser = ::Parser::CurrentRuby.new
31
+ parser.diagnostics.ignore_warnings = true
32
+ parser.diagnostics.all_errors_are_fatal = false
33
+ parser.diagnostics.consumer = nil
34
+
35
+ buf = ::Parser::Source::Buffer.new('(string)')
33
36
  buf.source = code.sub(BLOCK_EXPR, '')
34
37
  parsed = parser.parse(buf)
35
38
  raise ParseError, "error parsing code: #{code.inspect}" unless parsed
@@ -53,7 +56,7 @@ module BetterHtml
53
56
 
54
57
  def each_child_node(current=@ast, only: nil, range: (0..-1))
55
58
  current.children[range].each do |child|
56
- if child.is_a?(Parser::AST::Node) && node_match?(child, only)
59
+ if child.is_a?(::Parser::AST::Node) && node_match?(child, only)
57
60
  yield child
58
61
  end
59
62
  end
@@ -1,6 +1,7 @@
1
1
  require 'better_html/test_helper/ruby_expr'
2
2
  require 'better_html/test_helper/safety_error'
3
- require 'parser/current'
3
+ require 'better_html/parser'
4
+ require 'better_html/tree/tag'
4
5
 
5
6
  module BetterHtml
6
7
  module TestHelper
@@ -58,7 +59,7 @@ EOF
58
59
  @errors = Errors.new
59
60
  @options = options.present? ? options.dup : {}
60
61
  @options[:template_language] ||= :html
61
- @nodes = BetterHtml::NodeIterator.new(data, @options.slice(:template_language))
62
+ @parser = BetterHtml::Parser.new(data, @options.slice(:template_language))
62
63
  validate!
63
64
  end
64
65
 
@@ -67,86 +68,98 @@ EOF
67
68
  end
68
69
 
69
70
  def validate!
70
- @nodes.each_with_index do |node, index|
71
- case node
72
- when BetterHtml::NodeIterator::Element
73
- validate_element(node)
74
-
75
- if node.name == 'script'
76
- next_node = @nodes[index + 1]
77
- if next_node.is_a?(BetterHtml::NodeIterator::ContentNode) && !node.closing?
78
- if javascript_tag_type?(node, "text/javascript")
79
- validate_script_tag_content(next_node)
80
- end
81
- validate_no_statements(next_node) unless javascript_tag_type?(node, "text/html")
71
+ @parser.nodes_with_type(:tag).each do |tag_node|
72
+ tag = Tree::Tag.from_node(tag_node)
73
+ next if tag.closing?
74
+
75
+ validate_tag_attributes(tag)
76
+
77
+ if tag.name == 'script'
78
+ index = @parser.ast.to_a.find_index(tag_node)
79
+ next_node = @parser.ast.to_a[index + 1]
80
+ if next_node.type == :text
81
+ if (tag.attributes['type']&.value || "text/javascript") == "text/javascript"
82
+ validate_script_tag_content(next_node)
82
83
  end
83
-
84
- validate_javascript_tag_type(node) unless node.closing?
85
- end
86
- when BetterHtml::NodeIterator::Text
87
- validate_text_content(node)
88
-
89
- if @nodes.template_language == :javascript
90
- validate_script_tag_content(node)
91
- validate_no_statements(node)
92
- else
93
- validate_no_javascript_tag(node)
84
+ validate_no_statements(next_node) unless tag.attributes['type']&.value == "text/html"
94
85
  end
95
- when BetterHtml::NodeIterator::CData, BetterHtml::NodeIterator::Comment
86
+
87
+ validate_javascript_tag_type(tag)
88
+ end
89
+ end
90
+
91
+ @parser.nodes_with_type(:text).each do |node|
92
+ validate_text_node(node)
93
+
94
+ if @parser.template_language == :javascript
95
+ validate_script_tag_content(node)
96
96
  validate_no_statements(node)
97
+ else
98
+ validate_no_javascript_tag(node)
97
99
  end
98
100
  end
99
- end
100
101
 
101
- def javascript_tag_type?(element, which)
102
- typeattr = element['type']
103
- value = typeattr&.unescaped_value || "text/javascript"
104
- value == which
102
+ @parser.nodes_with_type(:cdata, :comment).each do |node|
103
+ validate_no_statements(node)
104
+ end
105
105
  end
106
106
 
107
- def validate_javascript_tag_type(element)
108
- typeattr = element['type']
109
- return if typeattr.nil?
110
- if !VALID_JAVASCRIPT_TAG_TYPES.include?(typeattr.unescaped_value)
111
- add_error(
112
- "#{typeattr.value} is not a valid type, valid types are #{VALID_JAVASCRIPT_TAG_TYPES.join(', ')}",
113
- location: typeattr.value_parts.first.location
114
- )
107
+ def erb_nodes(node)
108
+ Enumerator.new do |yielder|
109
+ next if node.nil?
110
+ node.descendants(:erb).each do |erb_node|
111
+ indicator_node, _, code_node, _ = *erb_node
112
+ yielder.yield(erb_node, indicator_node, code_node)
113
+ end
115
114
  end
116
115
  end
117
116
 
118
- def validate_element(element)
119
- element.attributes.each do |attr_token|
120
- attr_token.value_parts.each do |value_token|
121
- case value_token.type
122
- when :expr_literal
117
+ def validate_javascript_tag_type(tag)
118
+ return unless type_attribute = tag.attributes['type']
119
+ return if VALID_JAVASCRIPT_TAG_TYPES.include?(type_attribute.value)
120
+
121
+ add_error(
122
+ "#{type_attribute.value} is not a valid type, valid types are #{VALID_JAVASCRIPT_TAG_TYPES.join(', ')}",
123
+ location: type_attribute.loc
124
+ )
125
+ end
126
+
127
+ def validate_tag_attributes(tag)
128
+ tag.attributes.each do |attribute|
129
+ erb_nodes(attribute.value_node).each do |erb_node, indicator_node, code_node|
130
+ next if indicator_node.nil?
131
+
132
+ indicator = indicator_node.loc.source
133
+ source = code_node.loc.source
134
+
135
+ if indicator == '='
123
136
  begin
124
- expr = RubyExpr.parse(value_token.code)
125
- validate_tag_expression(value_token, expr, attr_token.name)
137
+ expr = RubyExpr.parse(source)
138
+ validate_tag_expression(code_node, expr, attribute.name)
126
139
  rescue RubyExpr::ParseError
127
140
  nil
128
141
  end
129
- when :expr_escaped
142
+ elsif indicator == '=='
130
143
  add_error(
131
144
  "erb interpolation with '<%==' inside html attribute is never safe",
132
- location: value_token.location
145
+ location: erb_node.loc
133
146
  )
134
147
  end
135
148
  end
136
149
  end
137
150
  end
138
151
 
139
- def validate_text_content(text)
140
- text.content_parts.each do |text_token|
141
- case text_token.type
142
- when :stmt, :expr_literal, :expr_escaped
143
- next if text_token.type == :stmt && text_token.code.start_with?('#')
144
- begin
145
- expr = RubyExpr.parse(text_token.code)
146
- validate_ruby_helper(text_token, expr)
147
- rescue RubyExpr::ParseError
148
- nil
149
- end
152
+ def validate_text_node(text_node)
153
+ erb_nodes(text_node).each do |erb_node, indicator_node, code_node|
154
+ indicator = indicator_node&.loc&.source
155
+ next if indicator == '#'
156
+ source = code_node.loc.source
157
+
158
+ begin
159
+ expr = RubyExpr.parse(source)
160
+ validate_ruby_helper(code_node, expr)
161
+ rescue RubyExpr::ParseError
162
+ nil
150
163
  end
151
164
  end
152
165
  end
@@ -185,13 +198,13 @@ EOF
185
198
  def validate_tag_expression(parent_token, expr, attr_name)
186
199
  return if expr.static_value?
187
200
 
188
- if javascript_attribute_name?(attr_name) && expr.calls.empty?
201
+ if @config.javascript_attribute_name?(attr_name) && expr.calls.empty?
189
202
  add_error(
190
203
  "erb interpolation in javascript attribute must call '(...).to_json'",
191
- location: NodeIterator::Location.new(
204
+ location: Tokenizer::Location.new(
192
205
  @data,
193
- parent_token.code_location.start + expr.start,
194
- parent_token.code_location.start + expr.end
206
+ parent_token.loc.start + expr.start,
207
+ parent_token.loc.start + expr.end - 1
195
208
  )
196
209
  )
197
210
  return
@@ -201,57 +214,54 @@ EOF
201
214
  if call.method == :raw
202
215
  add_error(
203
216
  "erb interpolation with '<%= raw(...) %>' inside html attribute is never safe",
204
- location: NodeIterator::Location.new(
217
+ location: Tokenizer::Location.new(
205
218
  @data,
206
- parent_token.code_location.start + expr.start,
207
- parent_token.code_location.start + expr.end
219
+ parent_token.loc.start + expr.start,
220
+ parent_token.loc.start + expr.end - 1
208
221
  )
209
222
  )
210
223
  elsif call.method == :html_safe
211
224
  add_error(
212
225
  "erb interpolation with '<%= (...).html_safe %>' inside html attribute is never safe",
213
- location: NodeIterator::Location.new(
226
+ location: Tokenizer::Location.new(
214
227
  @data,
215
- parent_token.code_location.start + expr.start,
216
- parent_token.code_location.start + expr.end
228
+ parent_token.loc.start + expr.start,
229
+ parent_token.loc.start + expr.end - 1
217
230
  )
218
231
  )
219
- elsif javascript_attribute_name?(attr_name) && !javascript_safe_method?(call.method)
232
+ elsif @config.javascript_attribute_name?(attr_name) && !@config.javascript_safe_method?(call.method)
220
233
  add_error(
221
234
  "erb interpolation in javascript attribute must call '(...).to_json'",
222
- location: NodeIterator::Location.new(
235
+ location: Tokenizer::Location.new(
223
236
  @data,
224
- parent_token.code_location.start + expr.start,
225
- parent_token.code_location.start + expr.end
237
+ parent_token.loc.start + expr.start,
238
+ parent_token.loc.start + expr.end - 1
226
239
  )
227
240
  )
228
241
  end
229
242
  end
230
243
  end
231
244
 
232
- def javascript_attribute_name?(name)
233
- @config.javascript_attribute_names.any?{ |other| other === name.to_s }
234
- end
235
-
236
- def javascript_safe_method?(name)
237
- @config.javascript_safe_methods.include?(name.to_s)
238
- end
239
-
240
245
  def validate_script_tag_content(node)
241
- node.content_parts.each do |token|
242
- case token.type
243
- when :expr_literal, :expr_escaped
244
- expr = RubyExpr.parse(token.code)
245
- validate_script_expression(node, token, expr)
246
+ erb_nodes(node).each do |erb_node, indicator_node, code_node|
247
+ next unless indicator_node.present?
248
+ indicator = indicator_node.loc.source
249
+ next if indicator == '#'
250
+ source = code_node.loc.source
251
+
252
+ begin
253
+ expr = RubyExpr.parse(source)
254
+ validate_script_expression(erb_node, expr)
255
+ rescue RubyExpr::ParseError
246
256
  end
247
257
  end
248
258
  end
249
259
 
250
- def validate_script_expression(node, token, expr)
260
+ def validate_script_expression(parent_node, expr)
251
261
  if expr.calls.empty?
252
262
  add_error(
253
263
  "erb interpolation in javascript tag must call '(...).to_json'",
254
- location: token.location,
264
+ location: parent_node.loc,
255
265
  )
256
266
  return
257
267
  end
@@ -260,46 +270,49 @@ EOF
260
270
  if call.method == :raw
261
271
  call.arguments.each do |argument_node|
262
272
  arguments_expr = RubyExpr.new(argument_node)
263
- validate_script_expression(node, token, arguments_expr)
273
+ validate_script_expression(parent_node, arguments_expr)
264
274
  end
265
275
  elsif call.method == :html_safe
266
276
  instance_expr = RubyExpr.new(call.instance)
267
- validate_script_expression(node, token, instance_expr)
268
- elsif !javascript_safe_method?(call.method)
277
+ validate_script_expression(parent_node, instance_expr)
278
+ elsif !@config.javascript_safe_method?(call.method)
269
279
  add_error(
270
280
  "erb interpolation in javascript tag must call '(...).to_json'",
271
- location: token.location,
281
+ location: parent_node.loc,
272
282
  )
273
283
  end
274
284
  end
275
285
  end
276
286
 
277
287
  def validate_no_statements(node)
278
- node.content_parts.each do |token|
279
- if token.type == :stmt && !(/\A\s*end/m === token.code) && !token.code.start_with?('#')
280
- add_error(
281
- "erb statement not allowed here; did you mean '<%=' ?",
282
- location: token.location,
283
- )
284
- end
288
+ erb_nodes(node).each do |erb_node, indicator_node, code_node|
289
+ next unless indicator_node.nil?
290
+ source = code_node.loc.source
291
+ next if /\A\s*end/m === source
292
+
293
+ add_error(
294
+ "erb statement not allowed here; did you mean '<%=' ?",
295
+ location: erb_node.loc,
296
+ )
285
297
  end
286
298
  end
287
299
 
288
300
  def validate_no_javascript_tag(node)
289
- node.content_parts.each do |token|
290
- next if token.type == :stmt && token.code.start_with?('#')
291
- if [:stmt, :expr_literal, :expr_escaped].include?(token.type)
292
- expr = begin
293
- RubyExpr.parse(token.code)
294
- rescue RubyExpr::ParseError
295
- next
296
- end
301
+ erb_nodes(node).each do |erb_node, indicator_node, code_node|
302
+ indicator = indicator_node&.loc&.source
303
+ next if indicator == '#'
304
+ source = code_node.loc.source
305
+
306
+ begin
307
+ expr = RubyExpr.parse(source)
308
+
297
309
  if expr.calls.size == 1 && expr.calls.first.method == :javascript_tag
298
310
  add_error(
299
311
  "'javascript_tag do' syntax is deprecated; use inline <script> instead",
300
- location: token.location,
312
+ location: erb_node.loc,
301
313
  )
302
314
  end
315
+ rescue RubyExpr::ParseError
303
316
  end
304
317
  end
305
318
  end