better_html 1.0.14 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/MIT-LICENSE +1 -0
- data/Rakefile +19 -14
- data/ext/better_html_ext/better_html.h +1 -0
- data/ext/better_html_ext/extconf.rb +16 -0
- data/ext/better_html_ext/html_tokenizer.c +12 -0
- data/ext/better_html_ext/html_tokenizer.h +7 -0
- data/ext/better_html_ext/parser.c +793 -0
- data/ext/better_html_ext/parser.h +93 -0
- data/ext/better_html_ext/tokenizer.c +717 -0
- data/ext/better_html_ext/tokenizer.h +80 -0
- data/lib/better_html/ast/iterator.rb +14 -9
- data/lib/better_html/ast/node.rb +4 -2
- data/lib/better_html/better_erb/erubi_implementation.rb +43 -39
- data/lib/better_html/better_erb/runtime_checks.rb +140 -133
- data/lib/better_html/better_erb/validated_output_buffer.rb +30 -22
- data/lib/better_html/better_erb.rb +58 -54
- data/lib/better_html/config.rb +7 -4
- data/lib/better_html/errors.rb +4 -2
- data/lib/better_html/helpers.rb +7 -3
- data/lib/better_html/html_attributes.rb +6 -2
- data/lib/better_html/parser.rb +21 -14
- data/lib/better_html/railtie.rb +8 -4
- data/lib/better_html/test_helper/ruby_node.rb +15 -9
- data/lib/better_html/test_helper/safe_erb/allowed_script_type.rb +8 -4
- data/lib/better_html/test_helper/safe_erb/base.rb +12 -9
- data/lib/better_html/test_helper/safe_erb/no_javascript_tag_helper.rb +7 -3
- data/lib/better_html/test_helper/safe_erb/no_statements.rb +7 -3
- data/lib/better_html/test_helper/safe_erb/script_interpolation.rb +9 -4
- data/lib/better_html/test_helper/safe_erb/tag_interpolation.rb +23 -20
- data/lib/better_html/test_helper/safe_erb_tester.rb +35 -33
- data/lib/better_html/test_helper/safe_lodash_tester.rb +36 -34
- data/lib/better_html/test_helper/safety_error.rb +2 -0
- data/lib/better_html/tokenizer/base_erb.rb +19 -15
- data/lib/better_html/tokenizer/html_erb.rb +3 -2
- data/lib/better_html/tokenizer/html_lodash.rb +22 -13
- data/lib/better_html/tokenizer/javascript_erb.rb +3 -1
- data/lib/better_html/tokenizer/location.rb +17 -6
- data/lib/better_html/tokenizer/token.rb +2 -0
- data/lib/better_html/tokenizer/token_array.rb +8 -8
- data/lib/better_html/tree/attribute.rb +10 -6
- data/lib/better_html/tree/attributes_list.rb +9 -5
- data/lib/better_html/tree/tag.rb +10 -6
- data/lib/better_html/version.rb +3 -1
- data/lib/better_html.rb +19 -17
- data/lib/tasks/better_html_tasks.rake +1 -0
- metadata +43 -148
- data/lib/better_html/better_erb/erubis_implementation.rb +0 -44
- data/test/better_html/better_erb/implementation_test.rb +0 -406
- data/test/better_html/errors_test.rb +0 -13
- data/test/better_html/helpers_test.rb +0 -49
- data/test/better_html/parser_test.rb +0 -314
- data/test/better_html/test_helper/ruby_node_test.rb +0 -288
- data/test/better_html/test_helper/safe_erb/allowed_script_type_test.rb +0 -45
- data/test/better_html/test_helper/safe_erb/no_javascript_tag_helper_test.rb +0 -37
- data/test/better_html/test_helper/safe_erb/no_statements_test.rb +0 -128
- data/test/better_html/test_helper/safe_erb/script_interpolation_test.rb +0 -149
- data/test/better_html/test_helper/safe_erb/tag_interpolation_test.rb +0 -303
- data/test/better_html/test_helper/safe_lodash_tester_test.rb +0 -90
- data/test/better_html/tokenizer/html_erb_test.rb +0 -180
- data/test/better_html/tokenizer/html_lodash_test.rb +0 -98
- data/test/better_html/tokenizer/location_test.rb +0 -75
- data/test/better_html/tokenizer/token_array_test.rb +0 -145
- data/test/better_html/tokenizer/token_test.rb +0 -15
- data/test/dummy/README.rdoc +0 -28
- data/test/dummy/Rakefile +0 -6
- data/test/dummy/app/assets/javascripts/application.js +0 -13
- data/test/dummy/app/assets/stylesheets/application.css +0 -15
- data/test/dummy/app/controllers/application_controller.rb +0 -5
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/views/layouts/application.html.erb +0 -14
- data/test/dummy/bin/bundle +0 -3
- data/test/dummy/bin/rails +0 -4
- data/test/dummy/bin/rake +0 -4
- data/test/dummy/bin/setup +0 -29
- data/test/dummy/config/application.rb +0 -26
- data/test/dummy/config/boot.rb +0 -5
- data/test/dummy/config/database.yml +0 -25
- data/test/dummy/config/environment.rb +0 -5
- data/test/dummy/config/environments/development.rb +0 -41
- data/test/dummy/config/environments/production.rb +0 -79
- data/test/dummy/config/environments/test.rb +0 -42
- data/test/dummy/config/initializers/assets.rb +0 -11
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/test/dummy/config/initializers/cookies_serializer.rb +0 -3
- data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -4
- data/test/dummy/config/initializers/inflections.rb +0 -16
- data/test/dummy/config/initializers/mime_types.rb +0 -4
- data/test/dummy/config/initializers/session_store.rb +0 -3
- data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
- data/test/dummy/config/locales/en.yml +0 -23
- data/test/dummy/config/routes.rb +0 -56
- data/test/dummy/config/secrets.yml +0 -22
- data/test/dummy/config.ru +0 -4
- data/test/dummy/public/404.html +0 -67
- data/test/dummy/public/422.html +0 -67
- data/test/dummy/public/500.html +0 -66
- data/test/dummy/public/favicon.ico +0 -0
- data/test/test_helper.rb +0 -30
@@ -1,5 +1,7 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
require "better_html/test_helper/ruby_node"
|
3
5
|
|
4
6
|
module BetterHtml
|
5
7
|
module TestHelper
|
@@ -7,7 +9,7 @@ module BetterHtml
|
|
7
9
|
class ScriptInterpolation < Base
|
8
10
|
def validate
|
9
11
|
script_tags.each do |tag, content_node|
|
10
|
-
if content_node.present? && (tag.attributes[
|
12
|
+
if content_node.present? && (tag.attributes["type"]&.value || "text/javascript") == "text/javascript"
|
11
13
|
validate_script(content_node)
|
12
14
|
end
|
13
15
|
end
|
@@ -24,8 +26,10 @@ module BetterHtml
|
|
24
26
|
def validate_script(node)
|
25
27
|
erb_nodes(node).each do |erb_node, indicator_node, code_node|
|
26
28
|
next unless indicator_node.present?
|
29
|
+
|
27
30
|
indicator = indicator_node.loc.source
|
28
|
-
next if indicator ==
|
31
|
+
next if indicator == "#" || indicator == "%"
|
32
|
+
|
29
33
|
source = code_node.loc.source
|
30
34
|
|
31
35
|
ruby_node = begin
|
@@ -34,6 +38,7 @@ module BetterHtml
|
|
34
38
|
nil
|
35
39
|
end
|
36
40
|
next unless ruby_node
|
41
|
+
|
37
42
|
validate_script_interpolation(erb_node, ruby_node)
|
38
43
|
end
|
39
44
|
end
|
@@ -1,15 +1,14 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
require "better_html/test_helper/ruby_node"
|
3
5
|
|
4
6
|
module BetterHtml
|
5
7
|
module TestHelper
|
6
8
|
module SafeErb
|
7
9
|
class TagInterpolation < Base
|
8
|
-
|
9
|
-
|
10
|
-
title textarea script
|
11
|
-
style xmp iframe noembed noframes listing plaintext
|
12
|
-
)
|
10
|
+
NO_HTML_TAGS = ["title", "textarea", "script", "style", "xmp", "iframe", "noembed", "noframes", "listing",
|
11
|
+
"plaintext",]
|
13
12
|
|
14
13
|
def validate
|
15
14
|
@parser.nodes_with_type(:tag).each do |tag_node|
|
@@ -43,7 +42,7 @@ module BetterHtml
|
|
43
42
|
indicator = indicator_node.loc.source
|
44
43
|
source = code_node.loc.source
|
45
44
|
|
46
|
-
if indicator ==
|
45
|
+
if indicator == "="
|
47
46
|
ruby_node = begin
|
48
47
|
RubyNode.parse(source)
|
49
48
|
rescue ::Parser::SyntaxError
|
@@ -55,7 +54,7 @@ module BetterHtml
|
|
55
54
|
handle_missing_safe_wrapper(code_node, ruby_node, attribute.name)
|
56
55
|
end
|
57
56
|
end
|
58
|
-
elsif indicator ==
|
57
|
+
elsif indicator == "=="
|
59
58
|
add_error(
|
60
59
|
"erb interpolation with '<%==' inside html attribute is never safe",
|
61
60
|
location: erb_node.loc
|
@@ -65,9 +64,10 @@ module BetterHtml
|
|
65
64
|
end
|
66
65
|
|
67
66
|
def validate_text_node(text_node)
|
68
|
-
erb_nodes(text_node).each do |
|
67
|
+
erb_nodes(text_node).each do |_erb_node, indicator_node, code_node|
|
69
68
|
indicator = indicator_node&.loc&.source
|
70
|
-
next if indicator ==
|
69
|
+
next if indicator == "#" || indicator == "%"
|
70
|
+
|
71
71
|
source = code_node.loc.source
|
72
72
|
|
73
73
|
ruby_node = begin
|
@@ -76,6 +76,7 @@ module BetterHtml
|
|
76
76
|
nil
|
77
77
|
end
|
78
78
|
next unless ruby_node
|
79
|
+
|
79
80
|
no_unsafe_calls(code_node, ruby_node)
|
80
81
|
validate_ruby_helper(code_node, ruby_node)
|
81
82
|
end
|
@@ -93,12 +94,13 @@ module BetterHtml
|
|
93
94
|
|
94
95
|
def validate_ruby_helper_hash_entry(parent_node, ruby_node, key_prefix, key_node, value_node)
|
95
96
|
return unless [:sym, :str].include?(key_node.type)
|
96
|
-
|
97
|
+
|
98
|
+
key = [key_prefix, key_node.children.first.to_s].compact.join("-").dasherize
|
97
99
|
case value_node.type
|
98
100
|
when :dstr
|
99
101
|
validate_ruby_helper_hash_value(parent_node, ruby_node, key, value_node)
|
100
102
|
when :hash
|
101
|
-
if key ==
|
103
|
+
if key == "data"
|
102
104
|
value_node.child_nodes.select(&:pair?).each do |pair_node|
|
103
105
|
validate_ruby_helper_hash_entry(parent_node, ruby_node, key, *pair_node.children)
|
104
106
|
end
|
@@ -114,6 +116,7 @@ module BetterHtml
|
|
114
116
|
|
115
117
|
def handle_missing_safe_wrapper(parent_node, ruby_node, attr_name)
|
116
118
|
return unless @config.javascript_attribute_name?(attr_name)
|
119
|
+
|
117
120
|
method_calls = ruby_node.return_values.select(&:method_call?)
|
118
121
|
unsafe_calls = method_calls.select { |node| !@config.javascript_safe_method?(node.method_name) }
|
119
122
|
if method_calls.empty?
|
@@ -140,13 +143,13 @@ module BetterHtml
|
|
140
143
|
ruby_node.return_values.each do |call_node|
|
141
144
|
next if call_node.static_return_value?
|
142
145
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
)
|
149
|
-
|
146
|
+
next unless @config.javascript_attribute_name?(attr_name) &&
|
147
|
+
!@config.javascript_safe_method?(call_node.method_name)
|
148
|
+
|
149
|
+
add_error(
|
150
|
+
"erb interpolation in javascript attribute must be wrapped in safe helper such as '(...).to_json'",
|
151
|
+
location: nested_location(parent_node, ruby_node)
|
152
|
+
)
|
150
153
|
end
|
151
154
|
end
|
152
155
|
|
@@ -1,45 +1,47 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "better_html/parser"
|
4
|
+
require "better_html/test_helper/safety_error"
|
5
|
+
require "better_html/test_helper/safe_erb/base"
|
6
|
+
require "better_html/test_helper/safe_erb/no_statements"
|
7
|
+
require "better_html/test_helper/safe_erb/allowed_script_type"
|
8
|
+
require "better_html/test_helper/safe_erb/no_javascript_tag_helper"
|
9
|
+
require "better_html/test_helper/safe_erb/tag_interpolation"
|
10
|
+
require "better_html/test_helper/safe_erb/script_interpolation"
|
11
|
+
require "better_html/tree/tag"
|
10
12
|
|
11
13
|
module BetterHtml
|
12
14
|
module TestHelper
|
13
15
|
module SafeErbTester
|
14
|
-
SAFETY_TIPS =
|
15
|
-
-----------
|
16
|
+
SAFETY_TIPS = <<~EOF
|
17
|
+
-----------
|
16
18
|
|
17
|
-
The javascript snippets listed above do not appear to be escaped properly
|
18
|
-
in a javascript context. Here are some tips:
|
19
|
+
The javascript snippets listed above do not appear to be escaped properly
|
20
|
+
in a javascript context. Here are some tips:
|
19
21
|
|
20
|
-
Never use html_safe inside a html tag, since it is _never_ safe:
|
21
|
-
|
22
|
-
|
22
|
+
Never use html_safe inside a html tag, since it is _never_ safe:
|
23
|
+
<a href="<%= value.html_safe %>">
|
24
|
+
^^^^^^^^^^
|
23
25
|
|
24
|
-
Always use .to_json for html attributes which contain javascript, like 'onclick',
|
25
|
-
or twine attributes like 'data-define', 'data-context', 'data-eval', 'data-bind', etc:
|
26
|
-
|
27
|
-
|
26
|
+
Always use .to_json for html attributes which contain javascript, like 'onclick',
|
27
|
+
or twine attributes like 'data-define', 'data-context', 'data-eval', 'data-bind', etc:
|
28
|
+
<div onclick="<%= value.to_json %>">
|
29
|
+
^^^^^^^^
|
28
30
|
|
29
|
-
Always use raw and to_json together within <script> tags:
|
30
|
-
|
31
|
-
|
32
|
-
|
31
|
+
Always use raw and to_json together within <script> tags:
|
32
|
+
<script type="text/javascript">
|
33
|
+
var yourValue = <%= raw value.to_json %>;
|
34
|
+
</script> ^^^ ^^^^^^^^
|
33
35
|
|
34
|
-
-----------
|
35
|
-
EOF
|
36
|
+
-----------
|
37
|
+
EOF
|
36
38
|
|
37
39
|
def assert_erb_safety(data, **options)
|
38
40
|
options = options.present? ? options.dup : {}
|
39
41
|
options[:template_language] ||= :html
|
40
|
-
buffer = ::Parser::Source::Buffer.new(options[:filename] ||
|
42
|
+
buffer = ::Parser::Source::Buffer.new(options[:filename] || "(buffer)")
|
41
43
|
buffer.source = data
|
42
|
-
parser = BetterHtml::Parser.new(buffer, options)
|
44
|
+
parser = BetterHtml::Parser.new(buffer, **options)
|
43
45
|
|
44
46
|
tester_classes = [
|
45
47
|
SafeErb::NoStatements,
|
@@ -52,21 +54,21 @@ EOF
|
|
52
54
|
end
|
53
55
|
|
54
56
|
testers = tester_classes.map do |tester_klass|
|
55
|
-
|
57
|
+
tester_klass.new(parser)
|
56
58
|
end
|
57
59
|
testers.each(&:validate)
|
58
60
|
errors = testers.map(&:errors).flatten
|
59
61
|
|
60
62
|
messages = errors.map do |error|
|
61
63
|
<<~EOL
|
62
|
-
|
63
|
-
|
64
|
-
|
64
|
+
In #{buffer.name}:#{error.location.line}
|
65
|
+
#{error.message}
|
66
|
+
#{error.location.line_source_with_underline}\n
|
65
67
|
EOL
|
66
68
|
end
|
67
69
|
messages << SAFETY_TIPS
|
68
70
|
|
69
|
-
assert_predicate
|
71
|
+
assert_predicate(errors, :empty?, messages.join)
|
70
72
|
end
|
71
73
|
end
|
72
74
|
end
|
@@ -1,34 +1,37 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "better_html/test_helper/safety_error"
|
4
|
+
require "better_html/ast/iterator"
|
5
|
+
require "better_html/tree/tag"
|
6
|
+
require "better_html/parser"
|
4
7
|
|
5
8
|
module BetterHtml
|
6
9
|
module TestHelper
|
7
10
|
module SafeLodashTester
|
8
|
-
SAFETY_TIPS =
|
9
|
-
-----------
|
11
|
+
SAFETY_TIPS = <<~EOF
|
12
|
+
-----------
|
10
13
|
|
11
|
-
The javascript snippets listed above do not appear to be escaped properly
|
12
|
-
in their context. Here are some tips:
|
14
|
+
The javascript snippets listed above do not appear to be escaped properly
|
15
|
+
in their context. Here are some tips:
|
13
16
|
|
14
|
-
Always use lodash's escape syntax inside a html tag:
|
15
|
-
|
16
|
-
|
17
|
+
Always use lodash's escape syntax inside a html tag:
|
18
|
+
<a href="[%= value %]">
|
19
|
+
^^^^
|
17
20
|
|
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
|
-
|
21
|
-
|
21
|
+
Always use JSON.stringify() for html attributes which contain javascript, like 'onclick',
|
22
|
+
or twine attributes like 'data-define', 'data-context', 'data-eval', 'data-bind', etc:
|
23
|
+
<div onclick="[%= JSON.stringify(value) %]">
|
24
|
+
^^^^^^^^^^^^^^
|
22
25
|
|
23
|
-
Never use <script> tags inside lodash template.
|
24
|
-
|
25
|
-
|
26
|
+
Never use <script> tags inside lodash template.
|
27
|
+
<script type="text/javascript">
|
28
|
+
^^^^^^^
|
26
29
|
|
27
|
-
-----------
|
28
|
-
EOF
|
30
|
+
-----------
|
31
|
+
EOF
|
29
32
|
|
30
33
|
def assert_lodash_safety(data, **options)
|
31
|
-
buffer = ::Parser::Source::Buffer.new(options[:filename] ||
|
34
|
+
buffer = ::Parser::Source::Buffer.new(options[:filename] || "(buffer)")
|
32
35
|
buffer.source = data
|
33
36
|
tester = Tester.new(buffer, **options)
|
34
37
|
|
@@ -43,11 +46,9 @@ EOF
|
|
43
46
|
|
44
47
|
message << SAFETY_TIPS
|
45
48
|
|
46
|
-
assert_predicate
|
49
|
+
assert_predicate(tester.errors, :empty?, message)
|
47
50
|
end
|
48
51
|
|
49
|
-
private
|
50
|
-
|
51
52
|
class Tester
|
52
53
|
attr_reader :errors
|
53
54
|
|
@@ -69,12 +70,12 @@ EOF
|
|
69
70
|
validate_tag_attributes(tag)
|
70
71
|
validate_no_statements(tag_node)
|
71
72
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
73
|
+
next unless tag.name == "script" && !tag.closing?
|
74
|
+
|
75
|
+
add_error(
|
76
|
+
"No script tags allowed nested in lodash templates",
|
77
|
+
location: tag_node.loc
|
78
|
+
)
|
78
79
|
end
|
79
80
|
|
80
81
|
@parser.nodes_with_type(:cdata, :comment).each do |node|
|
@@ -85,6 +86,7 @@ EOF
|
|
85
86
|
def lodash_nodes(node)
|
86
87
|
Enumerator.new do |yielder|
|
87
88
|
next if node.nil?
|
89
|
+
|
88
90
|
node.descendants(:lodash).each do |lodash_node|
|
89
91
|
indicator_node, code_node = *lodash_node
|
90
92
|
yielder.yield(lodash_node, indicator_node, code_node)
|
@@ -94,12 +96,12 @@ EOF
|
|
94
96
|
|
95
97
|
def validate_tag_attributes(tag)
|
96
98
|
tag.attributes.each do |attribute|
|
97
|
-
lodash_nodes(attribute.value_node).each do |lodash_node, indicator_node,
|
99
|
+
lodash_nodes(attribute.value_node).each do |lodash_node, indicator_node, _code_node|
|
98
100
|
next if indicator_node.nil?
|
99
101
|
|
100
|
-
if indicator_node.loc.source ==
|
102
|
+
if indicator_node.loc.source == "="
|
101
103
|
validate_tag_expression(attribute, lodash_node)
|
102
|
-
elsif indicator_node.loc.source ==
|
104
|
+
elsif indicator_node.loc.source == "!"
|
103
105
|
add_error(
|
104
106
|
"lodash interpolation with '[%!' inside html attribute is never safe",
|
105
107
|
location: lodash_node.loc
|
@@ -115,14 +117,14 @@ EOF
|
|
115
117
|
if @config.javascript_attribute_name?(attribute.name) && !@config.lodash_safe_javascript_expression?(source)
|
116
118
|
add_error(
|
117
119
|
"lodash interpolation in javascript attribute "\
|
118
|
-
|
120
|
+
"`#{attribute.name}` must call `JSON.stringify(#{source})`",
|
119
121
|
location: lodash_node.loc
|
120
122
|
)
|
121
123
|
end
|
122
124
|
end
|
123
125
|
|
124
126
|
def validate_no_statements(node)
|
125
|
-
lodash_nodes(node).each do |lodash_node, indicator_node,
|
127
|
+
lodash_nodes(node).each do |lodash_node, indicator_node, _code_node|
|
126
128
|
add_no_statement_error(lodash_node.loc) if indicator_node.nil?
|
127
129
|
end
|
128
130
|
end
|
@@ -1,7 +1,9 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "erubi"
|
4
|
+
require_relative "token"
|
5
|
+
require_relative "location"
|
6
|
+
require "parser/source/buffer"
|
5
7
|
|
6
8
|
module BetterHtml
|
7
9
|
module Tokenizer
|
@@ -14,7 +16,9 @@ module BetterHtml
|
|
14
16
|
attr_reader :current_position
|
15
17
|
|
16
18
|
def initialize(buffer)
|
17
|
-
raise ArgumentError,
|
19
|
+
raise ArgumentError,
|
20
|
+
"first argument must be Parser::Source::Buffer" unless buffer.is_a?(::Parser::Source::Buffer)
|
21
|
+
|
18
22
|
@buffer = buffer
|
19
23
|
@tokens = []
|
20
24
|
@current_position = 0
|
@@ -28,13 +32,13 @@ module BetterHtml
|
|
28
32
|
end
|
29
33
|
|
30
34
|
def add_code(code)
|
31
|
-
if code[0] ==
|
32
|
-
add_erb_tokens(nil,
|
35
|
+
if code[0] == "%"
|
36
|
+
add_erb_tokens(nil, "%", code[1..-1], nil)
|
33
37
|
append("<%#{code}%>")
|
34
38
|
else
|
35
39
|
_, ltrim_or_comment, code, rtrim = *STMT_TRIM_MATCHER.match(code)
|
36
|
-
ltrim = ltrim_or_comment if ltrim_or_comment ==
|
37
|
-
indicator = ltrim_or_comment if ltrim_or_comment ==
|
40
|
+
ltrim = ltrim_or_comment if ltrim_or_comment == "-"
|
41
|
+
indicator = ltrim_or_comment if ltrim_or_comment == "#"
|
38
42
|
add_erb_tokens(ltrim, indicator, code, rtrim)
|
39
43
|
append("<%#{ltrim}#{indicator}#{code}#{rtrim}%>")
|
40
44
|
end
|
@@ -49,28 +53,28 @@ module BetterHtml
|
|
49
53
|
def add_erb_tokens(ltrim, indicator, code, rtrim)
|
50
54
|
pos = current_position
|
51
55
|
|
52
|
-
|
56
|
+
add_token(:erb_begin, pos, pos + 2)
|
53
57
|
pos += 2
|
54
58
|
|
55
59
|
if ltrim
|
56
|
-
|
60
|
+
add_token(:trim, pos, pos + ltrim.length)
|
57
61
|
pos += ltrim.length
|
58
62
|
end
|
59
63
|
|
60
64
|
if indicator
|
61
|
-
|
65
|
+
add_token(:indicator, pos, pos + indicator.length)
|
62
66
|
pos += indicator.length
|
63
67
|
end
|
64
68
|
|
65
|
-
|
69
|
+
add_token(:code, pos, pos + code.length)
|
66
70
|
pos += code.length
|
67
71
|
|
68
72
|
if rtrim
|
69
|
-
|
73
|
+
add_token(:trim, pos, pos + rtrim.length)
|
70
74
|
pos += rtrim.length
|
71
75
|
end
|
72
76
|
|
73
|
-
|
77
|
+
add_token(:erb_end, pos, pos + 2)
|
74
78
|
end
|
75
79
|
|
76
80
|
def add_token(type, begin_pos, end_pos)
|
@@ -1,6 +1,8 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support"
|
4
|
+
require_relative "token"
|
5
|
+
require_relative "location"
|
4
6
|
|
5
7
|
module BetterHtml
|
6
8
|
module Tokenizer
|
@@ -9,9 +11,9 @@ module BetterHtml
|
|
9
11
|
attr_reader :parser
|
10
12
|
|
11
13
|
cattr_accessor :lodash_escape, :lodash_evaluate, :lodash_interpolate
|
12
|
-
self.lodash_escape =
|
13
|
-
self.lodash_evaluate =
|
14
|
-
self.lodash_interpolate =
|
14
|
+
self.lodash_escape = /(?:\[\%)=(.+?)(?:\%\])/m
|
15
|
+
self.lodash_evaluate = /(?:\[\%)(.+?)(?:\%\])/m
|
16
|
+
self.lodash_interpolate = /(?:\[\%)!(.+?)(?:\%\])/m
|
15
17
|
|
16
18
|
def initialize(buffer)
|
17
19
|
@buffer = buffer
|
@@ -26,25 +28,32 @@ module BetterHtml
|
|
26
28
|
def scan!
|
27
29
|
while @scanner.rest?
|
28
30
|
scanned = @scanner.scan_until(scan_pattern)
|
31
|
+
|
29
32
|
if scanned.present?
|
30
33
|
captures = scan_pattern.match(scanned).captures
|
31
|
-
|
32
|
-
|
34
|
+
|
35
|
+
if (pre_match = captures[0])
|
36
|
+
add_text(pre_match) if pre_match.present? # rubocop:disable Metrics/BlockNesting
|
33
37
|
end
|
38
|
+
|
34
39
|
match = captures[1]
|
35
|
-
|
40
|
+
|
41
|
+
if (code = lodash_escape.match(match))
|
36
42
|
add_lodash_tokens("=", code.captures[0])
|
37
|
-
elsif code = lodash_interpolate.match(match)
|
43
|
+
elsif (code = lodash_interpolate.match(match))
|
38
44
|
add_lodash_tokens("!", code.captures[0])
|
39
|
-
elsif code = lodash_evaluate.match(match)
|
45
|
+
elsif (code = lodash_evaluate.match(match))
|
40
46
|
add_lodash_tokens(nil, code.captures[0])
|
41
47
|
else
|
42
|
-
raise
|
48
|
+
raise "unexpected match"
|
43
49
|
end
|
50
|
+
|
44
51
|
@parser.append_placeholder(match)
|
45
52
|
else
|
46
53
|
text = @buffer.source[(@scanner.pos)..(@buffer.source.size)]
|
54
|
+
|
47
55
|
add_text(text) unless text.blank?
|
56
|
+
|
48
57
|
break
|
49
58
|
end
|
50
59
|
end
|
@@ -55,7 +64,7 @@ module BetterHtml
|
|
55
64
|
patterns = [
|
56
65
|
lodash_escape,
|
57
66
|
lodash_interpolate,
|
58
|
-
lodash_evaluate
|
67
|
+
lodash_evaluate,
|
59
68
|
].map(&:source).join("|")
|
60
69
|
Regexp.new("(?<pre_patch>.*?)(?<match>" + patterns + ")", Regexp::MULTILINE)
|
61
70
|
end
|
@@ -1,13 +1,24 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "parser/source/buffer"
|
4
|
+
require "parser/source/range"
|
3
5
|
|
4
6
|
module BetterHtml
|
5
7
|
module Tokenizer
|
6
8
|
class Location < ::Parser::Source::Range
|
7
9
|
def initialize(buffer, begin_pos, end_pos)
|
8
|
-
raise ArgumentError,
|
9
|
-
|
10
|
-
|
10
|
+
raise ArgumentError,
|
11
|
+
"first argument must be Parser::Source::Buffer" unless buffer.is_a?(::Parser::Source::Buffer)
|
12
|
+
|
13
|
+
if begin_pos > buffer.source.size
|
14
|
+
raise ArgumentError,
|
15
|
+
"begin_pos location #{begin_pos} is out of range for document of size #{buffer.source.size}"
|
16
|
+
end
|
17
|
+
|
18
|
+
if (end_pos - 1) > buffer.source.size
|
19
|
+
raise ArgumentError,
|
20
|
+
"end_pos location #{end_pos} is out of range for document of size #{buffer.source.size}"
|
21
|
+
end
|
11
22
|
|
12
23
|
super(buffer, begin_pos, end_pos)
|
13
24
|
end
|
@@ -29,7 +40,7 @@ module BetterHtml
|
|
29
40
|
spaces = source_line.scan(/\A\s*/).first
|
30
41
|
column_without_spaces = [column - spaces.length, 0].max
|
31
42
|
underscore_length = [[end_pos - begin_pos, source_line.length - column_without_spaces].min, 1].max
|
32
|
-
"#{source_line.gsub(/\A\s*/,
|
43
|
+
"#{source_line.gsub(/\A\s*/, "")}\n#{" " * column_without_spaces}#{"^" * underscore_length}"
|
33
44
|
end
|
34
45
|
|
35
46
|
def with(begin_pos: @begin_pos, end_pos: @end_pos)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module BetterHtml
|
2
4
|
module Tokenizer
|
3
5
|
class TokenArray
|
@@ -8,26 +10,24 @@ module BetterHtml
|
|
8
10
|
end
|
9
11
|
|
10
12
|
def shift
|
11
|
-
raise
|
13
|
+
raise "no tokens left to shift" if empty?
|
14
|
+
|
12
15
|
item = @list[@current]
|
13
16
|
@current += 1
|
14
17
|
item
|
15
18
|
end
|
16
19
|
|
17
20
|
def pop
|
18
|
-
raise
|
21
|
+
raise "no tokens left to pop" if empty?
|
22
|
+
|
19
23
|
item = @list[@last - 1]
|
20
24
|
@last -= 1
|
21
25
|
item
|
22
26
|
end
|
23
27
|
|
24
28
|
def trim(type)
|
25
|
-
while current&.type == type
|
26
|
-
|
27
|
-
end
|
28
|
-
while last&.type == type
|
29
|
-
pop
|
30
|
-
end
|
29
|
+
shift while current&.type == type
|
30
|
+
pop while last&.type == type
|
31
31
|
end
|
32
32
|
|
33
33
|
def empty?
|