better_html 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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,22 @@
|
|
1
|
+
require 'active_support/core_ext/string/output_safety'
|
2
|
+
require 'action_view'
|
3
|
+
|
4
|
+
module BetterHtml
|
5
|
+
class InterpolatorError < RuntimeError; end
|
6
|
+
class DontInterpolateHere < InterpolatorError; end
|
7
|
+
class UnsafeHtmlError < InterpolatorError; end
|
8
|
+
class HtmlError < RuntimeError; end
|
9
|
+
|
10
|
+
class Errors
|
11
|
+
delegate :[], :each, :size, :first,
|
12
|
+
:empty?, :any?, :present?, to: :@errors
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@errors = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def add(error)
|
19
|
+
@errors << error
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module BetterHtml
|
2
|
+
class HtmlAttributes
|
3
|
+
def initialize(data)
|
4
|
+
@data = data.stringify_keys
|
5
|
+
end
|
6
|
+
|
7
|
+
def to_s
|
8
|
+
@data.map do |key, value|
|
9
|
+
unless key =~ BetterHtml.config.partial_attribute_name_pattern
|
10
|
+
raise ArgumentError, "Attribute names must match the pattern #{BetterHtml.config.partial_attribute_name_pattern.inspect}"
|
11
|
+
end
|
12
|
+
if value.nil?
|
13
|
+
"#{key}"
|
14
|
+
else
|
15
|
+
value = value.to_s
|
16
|
+
escaped_value = value.html_safe? ? value : CGI.escapeHTML(value)
|
17
|
+
if escaped_value.include?('"')
|
18
|
+
raise ArgumentError, "The value provided for attribute '#{key}' contains a `\"` "\
|
19
|
+
"character which is not allowed. Did you call .html_safe without properly escaping this data?"
|
20
|
+
end
|
21
|
+
"#{key}=\"#{escaped_value}\""
|
22
|
+
end
|
23
|
+
end.join(" ")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require_relative 'node_iterator/javascript_erb'
|
2
|
+
require_relative 'node_iterator/html_erb'
|
3
|
+
require_relative 'node_iterator/html_lodash'
|
4
|
+
require_relative 'node_iterator/cdata'
|
5
|
+
require_relative 'node_iterator/comment'
|
6
|
+
require_relative 'node_iterator/element'
|
7
|
+
require_relative 'node_iterator/attribute'
|
8
|
+
require_relative 'node_iterator/text'
|
9
|
+
|
10
|
+
module BetterHtml
|
11
|
+
class NodeIterator
|
12
|
+
attr_reader :nodes, :template_language
|
13
|
+
|
14
|
+
delegate :each, :each_with_index, :[], to: :nodes
|
15
|
+
delegate :parser, to: :@erb, allow_nil: true
|
16
|
+
delegate :errors, to: :parser, allow_nil: true, prefix: true
|
17
|
+
|
18
|
+
def initialize(document, template_language: :html)
|
19
|
+
@document = document
|
20
|
+
@template_language = template_language
|
21
|
+
@erb = case template_language
|
22
|
+
when :html
|
23
|
+
HtmlErb.new(@document)
|
24
|
+
when :lodash
|
25
|
+
HtmlLodash.new(@document)
|
26
|
+
when :javascript
|
27
|
+
JavascriptErb.new(@document)
|
28
|
+
else
|
29
|
+
raise ArgumentError, "template_language can be :html or :javascript"
|
30
|
+
end
|
31
|
+
@nodes = parse!
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def parse!
|
37
|
+
nodes = []
|
38
|
+
tokens = @erb.tokens.dup
|
39
|
+
while token = tokens[0]
|
40
|
+
case token.type
|
41
|
+
when :cdata_start
|
42
|
+
tokens.shift
|
43
|
+
nodes << consume_cdata(tokens)
|
44
|
+
when :comment_start
|
45
|
+
tokens.shift
|
46
|
+
nodes << consume_comment(tokens)
|
47
|
+
when :tag_start
|
48
|
+
tokens.shift
|
49
|
+
nodes << consume_element(tokens)
|
50
|
+
when :text, :stmt, :expr_literal, :expr_escaped
|
51
|
+
nodes << consume_text(tokens)
|
52
|
+
else
|
53
|
+
raise RuntimeError, "Unhandled token #{token.type} line #{token.location.line} column #{token.location.column}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
nodes
|
57
|
+
end
|
58
|
+
|
59
|
+
def consume_cdata(tokens)
|
60
|
+
node = CData.new
|
61
|
+
while tokens.any? && tokens[0].type != :cdata_end
|
62
|
+
node.content_parts << tokens.shift
|
63
|
+
end
|
64
|
+
tokens.shift if tokens.any? && tokens[0].type == :cdata_end
|
65
|
+
node
|
66
|
+
end
|
67
|
+
|
68
|
+
def consume_comment(tokens)
|
69
|
+
node = Comment.new
|
70
|
+
while tokens.any? && tokens[0].type != :comment_end
|
71
|
+
node.content_parts << tokens.shift
|
72
|
+
end
|
73
|
+
tokens.shift if tokens.any? && tokens[0].type == :comment_end
|
74
|
+
node
|
75
|
+
end
|
76
|
+
|
77
|
+
def consume_element(tokens)
|
78
|
+
node = Element.new
|
79
|
+
if tokens.any? && tokens[0].type == :solidus
|
80
|
+
tokens.shift
|
81
|
+
node.closing = true
|
82
|
+
end
|
83
|
+
while tokens.any? && [:tag_name, :stmt, :expr_literal, :expr_escaped].include?(tokens[0].type)
|
84
|
+
node.name_parts << tokens.shift
|
85
|
+
end
|
86
|
+
while tokens.any?
|
87
|
+
token = tokens[0]
|
88
|
+
if token.type == :attribute_name
|
89
|
+
node.attributes << consume_attribute(tokens)
|
90
|
+
elsif token.type == :attribute_quoted_value_start
|
91
|
+
node.attributes << consume_attribute_value(tokens)
|
92
|
+
elsif token.type == :tag_end
|
93
|
+
tokens.shift
|
94
|
+
node.self_closing = token.self_closing
|
95
|
+
break
|
96
|
+
else
|
97
|
+
tokens.shift
|
98
|
+
end
|
99
|
+
end
|
100
|
+
node
|
101
|
+
end
|
102
|
+
|
103
|
+
def consume_attribute(tokens)
|
104
|
+
node = Attribute.new
|
105
|
+
while tokens.any? && [:attribute_name, :stmt, :expr_literal, :expr_escaped].include?(tokens[0].type)
|
106
|
+
node.name_parts << tokens.shift
|
107
|
+
end
|
108
|
+
return node unless consume_equal?(tokens)
|
109
|
+
while tokens.any? && [
|
110
|
+
:attribute_quoted_value_start, :attribute_quoted_value,
|
111
|
+
:attribute_quoted_value_end, :attribute_unquoted_value,
|
112
|
+
:stmt, :expr_literal, :expr_escaped].include?(tokens[0].type)
|
113
|
+
node.value_parts << tokens.shift
|
114
|
+
end
|
115
|
+
node
|
116
|
+
end
|
117
|
+
|
118
|
+
def consume_attribute_value(tokens)
|
119
|
+
node = Attribute.new
|
120
|
+
while tokens.any? && [
|
121
|
+
:attribute_quoted_value_start, :attribute_quoted_value,
|
122
|
+
:attribute_quoted_value_end, :attribute_unquoted_value,
|
123
|
+
:stmt, :expr_literal, :expr_escaped].include?(tokens[0].type)
|
124
|
+
node.value_parts << tokens.shift
|
125
|
+
end
|
126
|
+
node
|
127
|
+
end
|
128
|
+
|
129
|
+
def consume_equal?(tokens)
|
130
|
+
while tokens.any? && [:whitespace, :equal].include?(tokens[0].type)
|
131
|
+
return true if tokens.shift.type == :equal
|
132
|
+
end
|
133
|
+
false
|
134
|
+
end
|
135
|
+
|
136
|
+
def consume_text(tokens)
|
137
|
+
node = Text.new
|
138
|
+
while tokens.any? && [:text, :stmt, :expr_literal, :expr_escaped].include?(tokens[0].type)
|
139
|
+
node.content_parts << tokens.shift
|
140
|
+
end
|
141
|
+
node
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module BetterHtml
|
4
|
+
class NodeIterator
|
5
|
+
class Attribute < Base
|
6
|
+
tokenized_attribute :name
|
7
|
+
tokenized_attribute :value
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@name_parts = []
|
11
|
+
@value_parts = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def unescaped_value_parts
|
15
|
+
value_parts.map do |part|
|
16
|
+
next if ["'", '"'].include?(part.text)
|
17
|
+
if [:attribute_quoted_value, :attribute_unquoted_value].include?(part.type)
|
18
|
+
CGI.unescapeHTML(part.text)
|
19
|
+
else
|
20
|
+
part.text
|
21
|
+
end
|
22
|
+
end.compact
|
23
|
+
end
|
24
|
+
|
25
|
+
def unescaped_value
|
26
|
+
unescaped_value_parts.join
|
27
|
+
end
|
28
|
+
|
29
|
+
def value_without_quotes
|
30
|
+
value_parts.map{ |s| ["'", '"'].include?(s.text) ? '' : s.text }.join
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module BetterHtml
|
2
|
+
class NodeIterator
|
3
|
+
class Base
|
4
|
+
def self.tokenized_attribute(name)
|
5
|
+
class_eval <<~RUBY
|
6
|
+
attr_reader :#{name}_parts
|
7
|
+
|
8
|
+
def #{name}
|
9
|
+
#{name}_parts.map(&:text).join
|
10
|
+
end
|
11
|
+
RUBY
|
12
|
+
end
|
13
|
+
|
14
|
+
def node_type
|
15
|
+
self.class.name.split('::').last.downcase.to_sym
|
16
|
+
end
|
17
|
+
|
18
|
+
%w(text cdata comment element).each do |name|
|
19
|
+
class_eval <<~RUBY
|
20
|
+
def #{name}?
|
21
|
+
node_type == :#{name}
|
22
|
+
end
|
23
|
+
RUBY
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module BetterHtml
|
4
|
+
class NodeIterator
|
5
|
+
class Element < Base
|
6
|
+
tokenized_attribute :name
|
7
|
+
attr_reader :attributes
|
8
|
+
attr_accessor :closing, :self_closing
|
9
|
+
alias_method :closing?, :closing
|
10
|
+
alias_method :self_closing?, :self_closing
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@name_parts = []
|
14
|
+
@attributes = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def find_attr(wanted)
|
18
|
+
@attributes.each do |attribute|
|
19
|
+
return attribute if attribute.name == wanted
|
20
|
+
end
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
alias_method :[], :find_attr
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'erubis/engine/eruby'
|
2
|
+
require 'html_tokenizer'
|
3
|
+
require_relative 'token'
|
4
|
+
require_relative 'location'
|
5
|
+
|
6
|
+
module BetterHtml
|
7
|
+
class NodeIterator
|
8
|
+
class HtmlErb < ::Erubis::Eruby
|
9
|
+
attr_reader :tokens
|
10
|
+
attr_reader :parser
|
11
|
+
|
12
|
+
def initialize(document)
|
13
|
+
@parser = HtmlTokenizer::Parser.new
|
14
|
+
@tokens = []
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_text(src, text)
|
19
|
+
@parser.parse(text) { |*args| add_tokens(*args) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_stmt(src, code)
|
23
|
+
text = "<%#{code}%>"
|
24
|
+
start = @parser.document_length
|
25
|
+
stop = start + text.size
|
26
|
+
@tokens << Token.new(
|
27
|
+
type: :stmt,
|
28
|
+
code: code,
|
29
|
+
text: text,
|
30
|
+
location: Location.new(start, stop, @parser.line_number, @parser.column_number)
|
31
|
+
)
|
32
|
+
@parser.append_placeholder(text)
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_expr_literal(src, code)
|
36
|
+
text = "<%=#{code}%>"
|
37
|
+
start = @parser.document_length
|
38
|
+
stop = start + text.size
|
39
|
+
@tokens << Token.new(
|
40
|
+
type: :expr_literal,
|
41
|
+
code: code,
|
42
|
+
text: text,
|
43
|
+
location: Location.new(start, stop, @parser.line_number, @parser.column_number)
|
44
|
+
)
|
45
|
+
@parser.append_placeholder(text)
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_expr_escaped(src, code)
|
49
|
+
text = "<%==#{code}%>"
|
50
|
+
start = @parser.document_length
|
51
|
+
stop = start + text.size
|
52
|
+
@tokens << Token.new(
|
53
|
+
type: :expr_escaped,
|
54
|
+
code: code,
|
55
|
+
text: text,
|
56
|
+
location: Location.new(start, stop, @parser.line_number, @parser.column_number)
|
57
|
+
)
|
58
|
+
@parser.append_placeholder(text)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def add_tokens(type, start, stop, line, column)
|
64
|
+
extra_attributes = if type == :tag_end
|
65
|
+
{
|
66
|
+
self_closing: @parser.self_closing_tag?
|
67
|
+
}
|
68
|
+
end
|
69
|
+
@tokens << Token.new(
|
70
|
+
type: type,
|
71
|
+
text: @parser.extract(start, stop),
|
72
|
+
location: Location.new(start, stop, line, column),
|
73
|
+
**(extra_attributes || {})
|
74
|
+
)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require_relative 'token'
|
2
|
+
require_relative 'location'
|
3
|
+
|
4
|
+
module BetterHtml
|
5
|
+
class NodeIterator
|
6
|
+
class HtmlLodash
|
7
|
+
attr_reader :tokens
|
8
|
+
attr_reader :parser
|
9
|
+
|
10
|
+
cattr_accessor :lodash_escape, :lodash_evaluate, :lodash_interpolate
|
11
|
+
self.lodash_escape = %r{(?:\[\%)=(.+?)(?:\%\])}m
|
12
|
+
self.lodash_evaluate = %r{(?:\[\%)(.+?)(?:\%\])}m
|
13
|
+
self.lodash_interpolate = %r{(?:\[\%)!(.+?)(?:\%\])}m
|
14
|
+
|
15
|
+
def initialize(source)
|
16
|
+
@source = source
|
17
|
+
@scanner = StringScanner.new(source)
|
18
|
+
@parser = HtmlTokenizer::Parser.new
|
19
|
+
@tokens = []
|
20
|
+
scan!
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def scan!
|
26
|
+
while @scanner.rest?
|
27
|
+
scanned = @scanner.scan_until(scan_pattern)
|
28
|
+
if scanned.present?
|
29
|
+
captures = scan_pattern.match(scanned).captures
|
30
|
+
if pre_match = captures[0]
|
31
|
+
add_text(pre_match) unless pre_match.blank?
|
32
|
+
end
|
33
|
+
match = captures[1]
|
34
|
+
if code = lodash_escape.match(match)
|
35
|
+
add_expr_escape(match, code.captures[0])
|
36
|
+
elsif code = lodash_interpolate.match(match)
|
37
|
+
add_expr_interpolate(match, code.captures[0])
|
38
|
+
elsif code = lodash_evaluate.match(match)
|
39
|
+
add_stmt(match, code.captures[0])
|
40
|
+
else
|
41
|
+
raise RuntimeError, 'unexpected match'
|
42
|
+
end
|
43
|
+
else
|
44
|
+
text = @source[(@scanner.pos)..(@source.size)]
|
45
|
+
add_text(text) unless text.blank?
|
46
|
+
break
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def scan_pattern
|
52
|
+
@scan_pattern ||= begin
|
53
|
+
patterns = [
|
54
|
+
lodash_escape,
|
55
|
+
lodash_interpolate,
|
56
|
+
lodash_evaluate
|
57
|
+
].map(&:source).join("|")
|
58
|
+
Regexp.new("(?<pre_patch>.*?)(?<match>" + patterns + ")", Regexp::MULTILINE)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def add_text(text)
|
63
|
+
@parser.parse(text) do |type, start, stop, line, column|
|
64
|
+
add_token(type, @parser.extract(start, stop), start: start, stop: stop, line: line, column: column)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def add_stmt(text, code)
|
69
|
+
add_token(:stmt, text, code: code)
|
70
|
+
@parser.append_placeholder(text)
|
71
|
+
end
|
72
|
+
|
73
|
+
def add_expr_interpolate(text, code)
|
74
|
+
add_token(:expr_escaped, text, code: code)
|
75
|
+
@parser.append_placeholder(text)
|
76
|
+
end
|
77
|
+
|
78
|
+
def add_expr_escape(text, code)
|
79
|
+
add_token(:expr_literal, text, code: code)
|
80
|
+
@parser.append_placeholder(text)
|
81
|
+
end
|
82
|
+
|
83
|
+
def add_token(type, text, code: nil, start: nil, stop: nil, line: nil, column: nil)
|
84
|
+
start ||= @parser.document_length
|
85
|
+
stop ||= start + text.size
|
86
|
+
extra_attributes = if type == :tag_end
|
87
|
+
{
|
88
|
+
self_closing: @parser.self_closing_tag?
|
89
|
+
}
|
90
|
+
end
|
91
|
+
@tokens << Token.new(
|
92
|
+
type: type,
|
93
|
+
text: text,
|
94
|
+
code: code,
|
95
|
+
location: Location.new(start, stop, line || @parser.line_number, column || @parser.column_number),
|
96
|
+
**(extra_attributes || {})
|
97
|
+
)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|