better_html 0.0.12 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/better_html.rb +0 -2
- data/lib/better_html/ast/iterator.rb +32 -0
- data/lib/better_html/ast/node.rb +14 -0
- data/lib/better_html/better_erb/runtime_checks.rb +3 -3
- data/lib/better_html/config.rb +12 -0
- data/lib/better_html/parser.rb +286 -0
- data/lib/better_html/test_helper/ruby_expr.rb +8 -5
- data/lib/better_html/test_helper/safe_erb_tester.rb +121 -108
- data/lib/better_html/test_helper/safe_lodash_tester.rb +44 -42
- data/lib/better_html/tokenizer/base_erb.rb +79 -0
- data/lib/better_html/tokenizer/html_erb.rb +31 -0
- data/lib/better_html/{node_iterator → tokenizer}/html_lodash.rb +30 -34
- data/lib/better_html/tokenizer/javascript_erb.rb +15 -0
- data/lib/better_html/{node_iterator → tokenizer}/location.rb +9 -3
- data/lib/better_html/tokenizer/token.rb +16 -0
- data/lib/better_html/tokenizer/token_array.rb +54 -0
- data/lib/better_html/tree/attribute.rb +31 -0
- data/lib/better_html/tree/attributes_list.rb +25 -0
- data/lib/better_html/tree/tag.rb +39 -0
- data/lib/better_html/version.rb +1 -1
- data/test/better_html/parser_test.rb +279 -0
- data/test/better_html/test_helper/safe_erb_tester_test.rb +11 -0
- data/test/better_html/test_helper/safe_lodash_tester_test.rb +11 -1
- data/test/better_html/tokenizer/html_erb_test.rb +158 -0
- data/test/better_html/tokenizer/html_lodash_test.rb +98 -0
- data/test/better_html/tokenizer/location_test.rb +57 -0
- data/test/better_html/tokenizer/token_array_test.rb +144 -0
- data/test/better_html/tokenizer/token_test.rb +15 -0
- metadata +45 -30
- data/lib/better_html/node_iterator.rb +0 -144
- data/lib/better_html/node_iterator/attribute.rb +0 -34
- data/lib/better_html/node_iterator/base.rb +0 -27
- data/lib/better_html/node_iterator/cdata.rb +0 -8
- data/lib/better_html/node_iterator/comment.rb +0 -8
- data/lib/better_html/node_iterator/content_node.rb +0 -13
- data/lib/better_html/node_iterator/element.rb +0 -26
- data/lib/better_html/node_iterator/html_erb.rb +0 -70
- data/lib/better_html/node_iterator/javascript_erb.rb +0 -55
- data/lib/better_html/node_iterator/text.rb +0 -8
- data/lib/better_html/node_iterator/token.rb +0 -8
- data/lib/better_html/tree.rb +0 -113
- data/test/better_html/node_iterator/html_erb_test.rb +0 -116
- data/test/better_html/node_iterator/html_lodash_test.rb +0 -132
- data/test/better_html/node_iterator/location_test.rb +0 -36
- data/test/better_html/node_iterator_test.rb +0 -221
- data/test/better_html/tree_test.rb +0 -110
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 16bc95c4f55d720a177e9849dea00e7d83c66455
|
4
|
+
data.tar.gz: 376b80e51f2379ce6c5640e834de1c62aa990ae5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bf07d75143cfde43414bb3e8a5c2520b527f047d0c796c7ff97e60aa2d23d0bc03ea1f06c72eab5db5f4c10cdb01eccf0c6abe585b0ae394dc662731599e296b
|
7
|
+
data.tar.gz: b7dd6bfc56b0a36d773dabbe13a7ad922bc9c5cd322f4a240ba8d9064c8753ec4efca3fb5b1a7c63d36291b60dd48ec142521b3af98811f3a017d5d62f90ab97
|
data/lib/better_html.rb
CHANGED
@@ -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
|
@@ -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.
|
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.
|
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.
|
136
|
+
text = @parser.document[start...stop]
|
137
137
|
return if text == '"'
|
138
138
|
|
139
139
|
s = "Single-quoted attributes are not allowed\n"
|
data/lib/better_html/config.rb
CHANGED
@@ -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.
|
32
|
-
|
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
|
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
|
-
@
|
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
|
-
@
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
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
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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(
|
125
|
-
validate_tag_expression(
|
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
|
-
|
142
|
+
elsif indicator == '=='
|
130
143
|
add_error(
|
131
144
|
"erb interpolation with '<%==' inside html attribute is never safe",
|
132
|
-
location:
|
145
|
+
location: erb_node.loc
|
133
146
|
)
|
134
147
|
end
|
135
148
|
end
|
136
149
|
end
|
137
150
|
end
|
138
151
|
|
139
|
-
def
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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:
|
204
|
+
location: Tokenizer::Location.new(
|
192
205
|
@data,
|
193
|
-
parent_token.
|
194
|
-
parent_token.
|
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:
|
217
|
+
location: Tokenizer::Location.new(
|
205
218
|
@data,
|
206
|
-
parent_token.
|
207
|
-
parent_token.
|
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:
|
226
|
+
location: Tokenizer::Location.new(
|
214
227
|
@data,
|
215
|
-
parent_token.
|
216
|
-
parent_token.
|
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) &&
|
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:
|
235
|
+
location: Tokenizer::Location.new(
|
223
236
|
@data,
|
224
|
-
parent_token.
|
225
|
-
parent_token.
|
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.
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
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(
|
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:
|
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(
|
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(
|
268
|
-
elsif
|
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:
|
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.
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
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.
|
290
|
-
|
291
|
-
if
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
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:
|
312
|
+
location: erb_node.loc,
|
301
313
|
)
|
302
314
|
end
|
315
|
+
rescue RubyExpr::ParseError
|
303
316
|
end
|
304
317
|
end
|
305
318
|
end
|