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.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Rakefile +30 -0
  4. data/lib/better_html.rb +53 -0
  5. data/lib/better_html/better_erb.rb +68 -0
  6. data/lib/better_html/better_erb/erubi_implementation.rb +50 -0
  7. data/lib/better_html/better_erb/erubis_implementation.rb +44 -0
  8. data/lib/better_html/better_erb/runtime_checks.rb +161 -0
  9. data/lib/better_html/better_erb/validated_output_buffer.rb +166 -0
  10. data/lib/better_html/errors.rb +22 -0
  11. data/lib/better_html/helpers.rb +5 -0
  12. data/lib/better_html/html_attributes.rb +26 -0
  13. data/lib/better_html/node_iterator.rb +144 -0
  14. data/lib/better_html/node_iterator/attribute.rb +34 -0
  15. data/lib/better_html/node_iterator/base.rb +27 -0
  16. data/lib/better_html/node_iterator/cdata.rb +8 -0
  17. data/lib/better_html/node_iterator/comment.rb +8 -0
  18. data/lib/better_html/node_iterator/content_node.rb +13 -0
  19. data/lib/better_html/node_iterator/element.rb +26 -0
  20. data/lib/better_html/node_iterator/html_erb.rb +78 -0
  21. data/lib/better_html/node_iterator/html_lodash.rb +101 -0
  22. data/lib/better_html/node_iterator/javascript_erb.rb +60 -0
  23. data/lib/better_html/node_iterator/location.rb +14 -0
  24. data/lib/better_html/node_iterator/text.rb +8 -0
  25. data/lib/better_html/node_iterator/token.rb +8 -0
  26. data/lib/better_html/railtie.rb +7 -0
  27. data/lib/better_html/test_helper/ruby_expr.rb +89 -0
  28. data/lib/better_html/test_helper/safe_erb_tester.rb +202 -0
  29. data/lib/better_html/test_helper/safe_lodash_tester.rb +121 -0
  30. data/lib/better_html/test_helper/safety_tester_base.rb +34 -0
  31. data/lib/better_html/tree.rb +113 -0
  32. data/lib/better_html/version.rb +3 -0
  33. data/lib/tasks/better_html_tasks.rake +4 -0
  34. data/test/better_html/better_erb/implementation_test.rb +402 -0
  35. data/test/better_html/helpers_test.rb +49 -0
  36. data/test/better_html/node_iterator/html_lodash_test.rb +132 -0
  37. data/test/better_html/node_iterator_test.rb +221 -0
  38. data/test/better_html/test_helper/ruby_expr_test.rb +206 -0
  39. data/test/better_html/test_helper/safe_erb_tester_test.rb +358 -0
  40. data/test/better_html/test_helper/safe_lodash_tester_test.rb +80 -0
  41. data/test/better_html/tree_test.rb +110 -0
  42. data/test/dummy/README.rdoc +28 -0
  43. data/test/dummy/Rakefile +6 -0
  44. data/test/dummy/app/assets/javascripts/application.js +13 -0
  45. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  46. data/test/dummy/app/controllers/application_controller.rb +5 -0
  47. data/test/dummy/app/helpers/application_helper.rb +2 -0
  48. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  49. data/test/dummy/bin/bundle +3 -0
  50. data/test/dummy/bin/rails +4 -0
  51. data/test/dummy/bin/rake +4 -0
  52. data/test/dummy/bin/setup +29 -0
  53. data/test/dummy/config.ru +4 -0
  54. data/test/dummy/config/application.rb +26 -0
  55. data/test/dummy/config/boot.rb +5 -0
  56. data/test/dummy/config/database.yml +25 -0
  57. data/test/dummy/config/environment.rb +5 -0
  58. data/test/dummy/config/environments/development.rb +41 -0
  59. data/test/dummy/config/environments/production.rb +79 -0
  60. data/test/dummy/config/environments/test.rb +42 -0
  61. data/test/dummy/config/initializers/assets.rb +11 -0
  62. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  63. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  64. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  65. data/test/dummy/config/initializers/inflections.rb +16 -0
  66. data/test/dummy/config/initializers/mime_types.rb +4 -0
  67. data/test/dummy/config/initializers/session_store.rb +3 -0
  68. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  69. data/test/dummy/config/locales/en.yml +23 -0
  70. data/test/dummy/config/routes.rb +56 -0
  71. data/test/dummy/config/secrets.yml +22 -0
  72. data/test/dummy/public/404.html +67 -0
  73. data/test/dummy/public/422.html +67 -0
  74. data/test/dummy/public/500.html +66 -0
  75. data/test/dummy/public/favicon.ico +0 -0
  76. data/test/test_helper.rb +19 -0
  77. 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,5 @@
1
+ module BetterHtml::Helpers
2
+ def html_attributes(args)
3
+ BetterHtml::HtmlAttributes.new(args)
4
+ end
5
+ 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,8 @@
1
+ require_relative 'content_node'
2
+
3
+ module BetterHtml
4
+ class NodeIterator
5
+ class CData < ContentNode
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ require_relative 'content_node'
2
+
3
+ module BetterHtml
4
+ class NodeIterator
5
+ class Comment < ContentNode
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ require_relative 'base'
2
+
3
+ module BetterHtml
4
+ class NodeIterator
5
+ class ContentNode < Base
6
+ tokenized_attribute :content
7
+
8
+ def initialize
9
+ @content_parts = []
10
+ end
11
+ end
12
+ end
13
+ 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