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