better_html 0.0.3
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +30 -0
- data/lib/better_html.rb +53 -0
- data/lib/better_html/better_erb.rb +68 -0
- data/lib/better_html/better_erb/erubi_implementation.rb +50 -0
- data/lib/better_html/better_erb/erubis_implementation.rb +44 -0
- data/lib/better_html/better_erb/runtime_checks.rb +161 -0
- data/lib/better_html/better_erb/validated_output_buffer.rb +166 -0
- data/lib/better_html/errors.rb +22 -0
- data/lib/better_html/helpers.rb +5 -0
- data/lib/better_html/html_attributes.rb +26 -0
- data/lib/better_html/node_iterator.rb +144 -0
- data/lib/better_html/node_iterator/attribute.rb +34 -0
- data/lib/better_html/node_iterator/base.rb +27 -0
- data/lib/better_html/node_iterator/cdata.rb +8 -0
- data/lib/better_html/node_iterator/comment.rb +8 -0
- data/lib/better_html/node_iterator/content_node.rb +13 -0
- data/lib/better_html/node_iterator/element.rb +26 -0
- data/lib/better_html/node_iterator/html_erb.rb +78 -0
- data/lib/better_html/node_iterator/html_lodash.rb +101 -0
- data/lib/better_html/node_iterator/javascript_erb.rb +60 -0
- data/lib/better_html/node_iterator/location.rb +14 -0
- data/lib/better_html/node_iterator/text.rb +8 -0
- data/lib/better_html/node_iterator/token.rb +8 -0
- data/lib/better_html/railtie.rb +7 -0
- data/lib/better_html/test_helper/ruby_expr.rb +89 -0
- data/lib/better_html/test_helper/safe_erb_tester.rb +202 -0
- data/lib/better_html/test_helper/safe_lodash_tester.rb +121 -0
- data/lib/better_html/test_helper/safety_tester_base.rb +34 -0
- data/lib/better_html/tree.rb +113 -0
- data/lib/better_html/version.rb +3 -0
- data/lib/tasks/better_html_tasks.rake +4 -0
- data/test/better_html/better_erb/implementation_test.rb +402 -0
- data/test/better_html/helpers_test.rb +49 -0
- data/test/better_html/node_iterator/html_lodash_test.rb +132 -0
- data/test/better_html/node_iterator_test.rb +221 -0
- data/test/better_html/test_helper/ruby_expr_test.rb +206 -0
- data/test/better_html/test_helper/safe_erb_tester_test.rb +358 -0
- data/test/better_html/test_helper/safe_lodash_tester_test.rb +80 -0
- data/test/better_html/tree_test.rb +110 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +29 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +26 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +41 -0
- data/test/dummy/config/environments/production.rb +79 -0
- data/test/dummy/config/environments/test.rb +42 -0
- data/test/dummy/config/initializers/assets.rb +11 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +56 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/test_helper.rb +19 -0
- metadata +205 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'erubis/engine/eruby'
|
2
|
+
require_relative 'token'
|
3
|
+
require_relative 'location'
|
4
|
+
|
5
|
+
module BetterHtml
|
6
|
+
class NodeIterator
|
7
|
+
class JavascriptErb < ::Erubis::Eruby
|
8
|
+
attr_reader :tokens
|
9
|
+
|
10
|
+
def initialize(document)
|
11
|
+
@document = ""
|
12
|
+
@tokens = []
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_text(src, text)
|
17
|
+
add_token(:text, text)
|
18
|
+
append(text)
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_stmt(src, code)
|
22
|
+
text = "<%#{code}%>"
|
23
|
+
add_token(:stmt, text, code)
|
24
|
+
append(text)
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_expr_literal(src, code)
|
28
|
+
text = "<%=#{code}%>"
|
29
|
+
add_token(:expr_literal, text, code)
|
30
|
+
append(text)
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_expr_escaped(src, code)
|
34
|
+
text = "<%==#{code}%>"
|
35
|
+
add_token(:expr_escaped, text, code)
|
36
|
+
append(text)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def add_token(type, text, code = nil)
|
42
|
+
start = @document.size
|
43
|
+
stop = start + text.size
|
44
|
+
lines = @document.split("\n", -1)
|
45
|
+
line = lines.empty? ? 1 : lines.size
|
46
|
+
column = lines.empty? ? 0 : lines.last.size
|
47
|
+
@tokens << Token.new(
|
48
|
+
type: type,
|
49
|
+
text: text,
|
50
|
+
code: code,
|
51
|
+
location: Location.new(start, stop, line, column)
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
def append(text)
|
56
|
+
@document << text
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'ripper'
|
2
|
+
require 'pp'
|
3
|
+
|
4
|
+
module BetterHtml
|
5
|
+
module TestHelper
|
6
|
+
class RubyExpr
|
7
|
+
attr_reader :calls
|
8
|
+
|
9
|
+
BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
|
10
|
+
|
11
|
+
class ParseError < RuntimeError; end
|
12
|
+
|
13
|
+
class MethodCall
|
14
|
+
attr_accessor :instance, :method, :arguments
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(code: nil, tree: nil)
|
18
|
+
if code
|
19
|
+
code = code.gsub(BLOCK_EXPR, '')
|
20
|
+
tree = Ripper.sexp(code)
|
21
|
+
raise ParseError, "cannot parse code" unless tree && tree.last.first.is_a?(Array)
|
22
|
+
@tree = tree.last.first
|
23
|
+
else
|
24
|
+
@tree = tree
|
25
|
+
end
|
26
|
+
@calls = []
|
27
|
+
parse!
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def parse!
|
33
|
+
parse_expr(@tree)
|
34
|
+
end
|
35
|
+
|
36
|
+
def parse_expr(expr)
|
37
|
+
case expr.first
|
38
|
+
when :var_ref
|
39
|
+
parse_expr(expr[1])
|
40
|
+
when :paren
|
41
|
+
parse_expr(expr[1].first)
|
42
|
+
when :string_literal
|
43
|
+
parse_expr(expr[1])
|
44
|
+
when :string_content
|
45
|
+
expr[1..-1].each do |subexpr|
|
46
|
+
parse_expr(subexpr)
|
47
|
+
end
|
48
|
+
when :string_embexpr
|
49
|
+
expr[1].each do |subexpr|
|
50
|
+
parse_expr(subexpr)
|
51
|
+
end
|
52
|
+
when :assign, :massign
|
53
|
+
parse_expr(expr[2])
|
54
|
+
when :call
|
55
|
+
@calls << obj = MethodCall.new
|
56
|
+
obj.instance = expr[1]
|
57
|
+
obj.method = parse_expr(expr[3])
|
58
|
+
obj
|
59
|
+
when :fcall, :vcall
|
60
|
+
@calls << obj = MethodCall.new
|
61
|
+
obj.method = parse_expr(expr[1])
|
62
|
+
obj
|
63
|
+
when :method_add_arg
|
64
|
+
# foo(bar) -> foo=expr[1], bar=expr[2]
|
65
|
+
obj = parse_expr(expr[1])
|
66
|
+
obj.arguments = parse_expr(expr[2])
|
67
|
+
obj
|
68
|
+
when :command
|
69
|
+
# foo bar -> foo=expr[1], bar=expr[2]
|
70
|
+
@calls << obj = MethodCall.new
|
71
|
+
obj.method = parse_expr(expr[1])
|
72
|
+
obj.arguments = parse_expr(expr[2])
|
73
|
+
obj
|
74
|
+
when :arg_paren
|
75
|
+
parse_expr(expr[1]) unless expr[1].nil?
|
76
|
+
when :if_mod, :unless_mod
|
77
|
+
# foo if bar -> bar=expr[1], foo=expr[2]
|
78
|
+
parse_expr(expr[2])
|
79
|
+
when :ifop
|
80
|
+
# foo ? bar : baz -> foo=expr[1], bar=expr[2], baz=expr[3]
|
81
|
+
parse_expr(expr[2])
|
82
|
+
parse_expr(expr[3])
|
83
|
+
else
|
84
|
+
expr[1]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'better_html/test_helper/ruby_expr'
|
2
|
+
require_relative 'safety_tester_base'
|
3
|
+
|
4
|
+
module BetterHtml
|
5
|
+
module TestHelper
|
6
|
+
module SafeErbTester
|
7
|
+
include SafetyTesterBase
|
8
|
+
|
9
|
+
SAFETY_TIPS = <<-EOF
|
10
|
+
-----------
|
11
|
+
|
12
|
+
The javascript snippets listed above do not appear to be escaped properly
|
13
|
+
in a javascript context. Here are some tips:
|
14
|
+
|
15
|
+
Never use html_safe inside a html tag, since it is _never_ safe:
|
16
|
+
<a href="<%= value.html_safe %>">
|
17
|
+
^^^^^^^^^^
|
18
|
+
|
19
|
+
Always use .to_json for html attributes which contain javascript, like 'onclick',
|
20
|
+
or twine attributes like 'data-define', 'data-context', 'data-eval', 'data-bind', etc:
|
21
|
+
<div onclick="<%= value.to_json %>">
|
22
|
+
^^^^^^^^
|
23
|
+
|
24
|
+
Always use raw and to_json together within <script> tags:
|
25
|
+
<script type="text/javascript">
|
26
|
+
var yourValue = <%= raw value.to_json %>;
|
27
|
+
</script> ^^^ ^^^^^^^^
|
28
|
+
|
29
|
+
-----------
|
30
|
+
EOF
|
31
|
+
|
32
|
+
def assert_erb_safety(data, **options)
|
33
|
+
tester = Tester.new(data, **options)
|
34
|
+
|
35
|
+
message = ""
|
36
|
+
tester.errors.each do |error|
|
37
|
+
message << format_safety_error(data, error)
|
38
|
+
end
|
39
|
+
|
40
|
+
message << SAFETY_TIPS
|
41
|
+
|
42
|
+
assert_predicate tester.errors, :empty?, message
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
class Tester
|
48
|
+
attr_reader :errors
|
49
|
+
|
50
|
+
VALID_JAVASCRIPT_TAG_TYPES = ['text/javascript', 'text/template', 'text/html']
|
51
|
+
|
52
|
+
def initialize(data, **options)
|
53
|
+
@data = data
|
54
|
+
@errors = Errors.new
|
55
|
+
@options = options.present? ? options.dup : {}
|
56
|
+
@options[:template_language] ||= :html
|
57
|
+
@nodes = BetterHtml::NodeIterator.new(data, @options.slice(:template_language))
|
58
|
+
validate!
|
59
|
+
end
|
60
|
+
|
61
|
+
def add_error(token, message)
|
62
|
+
@errors.add(SafetyTesterBase::SafetyError.new(token, message))
|
63
|
+
end
|
64
|
+
|
65
|
+
def validate!
|
66
|
+
@nodes.each_with_index do |node, index|
|
67
|
+
case node
|
68
|
+
when BetterHtml::NodeIterator::Element
|
69
|
+
validate_element(node)
|
70
|
+
|
71
|
+
if node.name == 'script'
|
72
|
+
next_node = @nodes[index + 1]
|
73
|
+
if next_node.is_a?(BetterHtml::NodeIterator::ContentNode) && !node.closing?
|
74
|
+
if javascript_tag_type?(node, "text/javascript")
|
75
|
+
validate_script_tag_content(next_node)
|
76
|
+
end
|
77
|
+
validate_no_statements(next_node) unless javascript_tag_type?(node, "text/html")
|
78
|
+
end
|
79
|
+
|
80
|
+
validate_javascript_tag_type(node) unless node.closing?
|
81
|
+
end
|
82
|
+
when BetterHtml::NodeIterator::Text
|
83
|
+
if @nodes.template_language == :javascript
|
84
|
+
validate_script_tag_content(node)
|
85
|
+
validate_no_statements(node)
|
86
|
+
else
|
87
|
+
validate_no_javascript_tag(node)
|
88
|
+
end
|
89
|
+
when BetterHtml::NodeIterator::CData, BetterHtml::NodeIterator::Comment
|
90
|
+
validate_no_statements(node)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def javascript_tag_type?(element, which)
|
96
|
+
typeattr = element['type']
|
97
|
+
value = typeattr&.unescaped_value || "text/javascript"
|
98
|
+
value == which
|
99
|
+
end
|
100
|
+
|
101
|
+
def validate_javascript_tag_type(element)
|
102
|
+
typeattr = element['type']
|
103
|
+
return if typeattr.nil?
|
104
|
+
if !VALID_JAVASCRIPT_TAG_TYPES.include?(typeattr.unescaped_value)
|
105
|
+
add_error(typeattr.value_parts.first, "#{typeattr.value} is not a valid type, valid types are #{VALID_JAVASCRIPT_TAG_TYPES.join(', ')}")
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def validate_element(element)
|
110
|
+
element.attributes.each do |attr_token|
|
111
|
+
attr_token.value_parts.each do |value_token|
|
112
|
+
case value_token.type
|
113
|
+
when :expr_literal
|
114
|
+
validate_tag_expression(element, attr_token.name, value_token)
|
115
|
+
when :expr_escaped
|
116
|
+
add_error(value_token, "erb interpolation with '<%==' inside html attribute is never safe")
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def validate_tag_expression(node, attr_name, value_token)
|
123
|
+
expr = RubyExpr.new(code: value_token.code)
|
124
|
+
|
125
|
+
if javascript_attribute_name?(attr_name) && expr.calls.empty?
|
126
|
+
add_error(value_token, "erb interpolation in javascript attribute must call '(...).to_json'")
|
127
|
+
return
|
128
|
+
end
|
129
|
+
|
130
|
+
expr.calls.each do |call|
|
131
|
+
if call.method == 'raw'
|
132
|
+
add_error(value_token, "erb interpolation with '<%= raw(...) %>' inside html attribute is never safe")
|
133
|
+
elsif call.method == 'html_safe'
|
134
|
+
add_error(value_token, "erb interpolation with '<%= (...).html_safe %>' inside html attribute is never safe")
|
135
|
+
elsif javascript_attribute_name?(attr_name) && !javascript_safe_method?(call.method)
|
136
|
+
add_error(value_token, "erb interpolation in javascript attribute must call '(...).to_json'")
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def javascript_attribute_name?(name)
|
142
|
+
BetterHtml.config.javascript_attribute_names.any?{ |other| other === name }
|
143
|
+
end
|
144
|
+
|
145
|
+
def javascript_safe_method?(name)
|
146
|
+
BetterHtml.config.javascript_safe_methods.include?(name)
|
147
|
+
end
|
148
|
+
|
149
|
+
def validate_script_tag_content(node)
|
150
|
+
node.content_parts.each do |token|
|
151
|
+
case token.type
|
152
|
+
when :expr_literal, :expr_escaped
|
153
|
+
expr = RubyExpr.new(code: token.code)
|
154
|
+
if expr.calls.empty?
|
155
|
+
add_error(token, "erb interpolation in javascript tag must call '(...).to_json'")
|
156
|
+
else
|
157
|
+
validate_script_expression(node, token, expr)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def validate_script_expression(node, token, expr)
|
164
|
+
expr.calls.each do |call|
|
165
|
+
if call.method == 'raw'
|
166
|
+
arguments_expr = RubyExpr.new(tree: call.arguments)
|
167
|
+
validate_script_expression(node, token, arguments_expr)
|
168
|
+
elsif call.method == 'html_safe'
|
169
|
+
instance_expr = RubyExpr.new(tree: call.instance)
|
170
|
+
validate_script_expression(node, token, instance_expr)
|
171
|
+
elsif !javascript_safe_method?(call.method)
|
172
|
+
add_error(token, "erb interpolation in javascript tag must call '(...).to_json'")
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def validate_no_statements(node)
|
178
|
+
node.content_parts.each do |token|
|
179
|
+
if token.type == :stmt && !(/\A\s*end/m === token.code)
|
180
|
+
add_error(token, "erb statement not allowed here; did you mean '<%=' ?")
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def validate_no_javascript_tag(node)
|
186
|
+
node.content_parts.each do |token|
|
187
|
+
if [:stmt, :expr_literal, :expr_escaped].include?(token.type)
|
188
|
+
expr = begin
|
189
|
+
RubyExpr.new(code: token.code)
|
190
|
+
rescue RubyExpr::ParseError
|
191
|
+
next
|
192
|
+
end
|
193
|
+
if expr.calls.size == 1 && expr.calls.first.method == 'javascript_tag'
|
194
|
+
add_error(token, "'javascript_tag do' syntax is deprecated; use inline <script> instead")
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require_relative 'safety_tester_base'
|
2
|
+
|
3
|
+
module BetterHtml
|
4
|
+
module TestHelper
|
5
|
+
module SafeLodashTester
|
6
|
+
include SafetyTesterBase
|
7
|
+
|
8
|
+
SAFETY_TIPS = <<-EOF
|
9
|
+
-----------
|
10
|
+
|
11
|
+
The javascript snippets listed above do not appear to be escaped properly
|
12
|
+
in their context. Here are some tips:
|
13
|
+
|
14
|
+
Always use lodash's escape syntax inside a html tag:
|
15
|
+
<a href="[%= value %]">
|
16
|
+
^^^^
|
17
|
+
|
18
|
+
Always use JSON.stringify() for html attributes which contain javascript, like 'onclick',
|
19
|
+
or twine attributes like 'data-define', 'data-context', 'data-eval', 'data-bind', etc:
|
20
|
+
<div onclick="[%= JSON.stringify(value) %]">
|
21
|
+
^^^^^^^^^^^^^^
|
22
|
+
|
23
|
+
Never use <script> tags inside lodash template.
|
24
|
+
<script type="text/javascript">
|
25
|
+
^^^^^^^
|
26
|
+
|
27
|
+
-----------
|
28
|
+
EOF
|
29
|
+
|
30
|
+
def assert_lodash_safety(data)
|
31
|
+
tester = Tester.new(data)
|
32
|
+
|
33
|
+
message = ""
|
34
|
+
tester.errors.each do |error|
|
35
|
+
message << format_safety_error(data, error)
|
36
|
+
end
|
37
|
+
|
38
|
+
message << SAFETY_TIPS
|
39
|
+
|
40
|
+
assert_predicate tester.errors, :empty?, message
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
class Tester
|
46
|
+
attr_reader :errors
|
47
|
+
|
48
|
+
def initialize(data)
|
49
|
+
@data = data
|
50
|
+
@errors = Errors.new
|
51
|
+
@nodes = BetterHtml::NodeIterator.new(data, template_language: :lodash)
|
52
|
+
validate!
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_error(token, message)
|
56
|
+
@errors.add(SafetyTesterBase::SafetyError.new(token, message))
|
57
|
+
end
|
58
|
+
|
59
|
+
def validate!
|
60
|
+
@nodes.each_with_index do |node, index|
|
61
|
+
case node
|
62
|
+
when BetterHtml::NodeIterator::Element
|
63
|
+
validate_element(node)
|
64
|
+
|
65
|
+
if node.name == 'script' && !node.closing?
|
66
|
+
add_error(node.name_parts.first,
|
67
|
+
"No script tags allowed nested in lodash templates")
|
68
|
+
end
|
69
|
+
when BetterHtml::NodeIterator::CData, BetterHtml::NodeIterator::Comment
|
70
|
+
validate_no_statements(node)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def validate_element(element)
|
76
|
+
element.attributes.each do |attribute|
|
77
|
+
attribute.name_parts.each do |token|
|
78
|
+
add_no_statement_error(attribute, token) if token.type == :stmt
|
79
|
+
end
|
80
|
+
|
81
|
+
attribute.value_parts.each do |token|
|
82
|
+
case token.type
|
83
|
+
when :stmt
|
84
|
+
add_no_statement_error(attribute, token)
|
85
|
+
when :expr_literal
|
86
|
+
validate_tag_expression(element, attribute.name, token)
|
87
|
+
when :expr_escaped
|
88
|
+
add_error(token, "lodash interpolation with '[%!' inside html attribute is never safe")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def validate_tag_expression(node, attr_name, value_token)
|
95
|
+
if javascript_attribute_name?(attr_name) && !lodash_safe_javascript_expression?(value_token.code.strip)
|
96
|
+
add_error(value_token, "lodash interpolation in javascript attribute "\
|
97
|
+
"`#{attr_name}` must call `JSON.stringify(#{value_token.code.strip})`")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def javascript_attribute_name?(name)
|
102
|
+
BetterHtml.config.javascript_attribute_names.any?{ |other| other === name }
|
103
|
+
end
|
104
|
+
|
105
|
+
def lodash_safe_javascript_expression?(code)
|
106
|
+
BetterHtml.config.lodash_safe_javascript_expression.any?{ |other| other === code }
|
107
|
+
end
|
108
|
+
|
109
|
+
def validate_no_statements(node)
|
110
|
+
node.content_parts.each do |token|
|
111
|
+
add_no_statement_error(node, token) if token.type == :stmt
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def add_no_statement_error(node, token)
|
116
|
+
add_error(token, "javascript statement not allowed here; did you mean '[%=' ?")
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|