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.
- 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
|