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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ec252f7eed899fcb25b28c2bd6de281b0763163b
4
+ data.tar.gz: 19f784180d9754171e1c0efa5c129ea30f5f00de
5
+ SHA512:
6
+ metadata.gz: cfbac115849137341db904e89c6bade3008d95e85a73eda51e4db11aa338384050cb81bb3bf0828528c704dd01469dcd02a071ca8e06e8e0e7138b098ac558ea
7
+ data.tar.gz: 47181e232f471e71de3699fd9a325268d6deafae7a01ae831221d5dcb952f9ab1deea8a04f6a8147b62a566a820762f3abccad4f555f5cf5d982741a1cb1814c
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2016 Francois Chagnon
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,30 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ require "bundler/gem_tasks"
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ require 'rdoc/task'
9
+
10
+ RDoc::Task.new(:rdoc) do |rdoc|
11
+ rdoc.rdoc_dir = 'rdoc'
12
+ rdoc.title = 'BetterHtml'
13
+ rdoc.options << '--line-numbers'
14
+ rdoc.rdoc_files.include('README.rdoc')
15
+ rdoc.rdoc_files.include('lib/**/*.rb')
16
+ end
17
+
18
+ Bundler::GemHelper.install_tasks
19
+
20
+ require 'rake/testtask'
21
+
22
+ Rake::TestTask.new(:test) do |t|
23
+ t.libs << 'lib'
24
+ t.libs << 'test'
25
+ t.pattern = 'test/**/*_test.rb'
26
+ t.verbose = false
27
+ end
28
+
29
+
30
+ task default: :test
@@ -0,0 +1,53 @@
1
+ require 'active_support/core_ext/hash/keys'
2
+
3
+ module BetterHtml
4
+ class Config
5
+ # regex to validate "foo" in "<foo>"
6
+ cattr_accessor :partial_tag_name_pattern
7
+ self.partial_tag_name_pattern = /\A[a-z0-9\-\:]+\z/
8
+
9
+ # regex to validate "bar" in "<foo bar=1>"
10
+ cattr_accessor :partial_attribute_name_pattern
11
+ self.partial_attribute_name_pattern = /\A[a-zA-Z0-9\-\:]+\z/
12
+
13
+ # true if "<foo bar='1'>" is valid syntax
14
+ cattr_accessor :allow_single_quoted_attributes
15
+ self.allow_single_quoted_attributes = false
16
+
17
+ # true if "<foo bar=1>" is valid syntax
18
+ cattr_accessor :allow_unquoted_attributes
19
+ self.allow_unquoted_attributes = false
20
+
21
+ # all methods that return "javascript-safe" strings
22
+ cattr_accessor :javascript_safe_methods
23
+ self.javascript_safe_methods = ['to_json']
24
+
25
+ # name of all html attributes that may contain javascript
26
+ cattr_accessor :javascript_attribute_names
27
+ self.javascript_attribute_names = [/\Aon/i]
28
+
29
+ cattr_accessor :template_exclusion_filter_block
30
+
31
+ def self.template_exclusion_filter(&block)
32
+ self.template_exclusion_filter_block = block
33
+ end
34
+
35
+ cattr_accessor :lodash_safe_javascript_expression
36
+ self.lodash_safe_javascript_expression = [/\AJSON\.stringify\(/]
37
+ end
38
+
39
+ def self.config
40
+ @config ||= Config.new
41
+ yield @config if block_given?
42
+ @config
43
+ end
44
+ end
45
+
46
+ require 'better_html/version'
47
+ require 'better_html/helpers'
48
+ require 'better_html/errors'
49
+ require 'better_html/html_attributes'
50
+ require 'better_html/node_iterator'
51
+ require 'better_html/tree'
52
+
53
+ require 'better_html/railtie' if defined?(Rails)
@@ -0,0 +1,68 @@
1
+ require 'action_view'
2
+ if ActionView.version < Gem::Version.new("5.1")
3
+ require 'better_html/better_erb/erubis_implementation'
4
+ else
5
+ require 'better_html/better_erb/erubi_implementation'
6
+ end
7
+ require 'better_html/better_erb/validated_output_buffer'
8
+
9
+
10
+ class BetterHtml::BetterErb
11
+ cattr_accessor :content_types
12
+ if ActionView.version < Gem::Version.new("5.1")
13
+ self.content_types = {
14
+ 'html.erb' => BetterHtml::BetterErb::ErubisImplementation
15
+ }
16
+ else
17
+ self.content_types = {
18
+ 'html.erb' => BetterHtml::BetterErb::ErubiImplementation
19
+ }
20
+ end
21
+
22
+ def self.prepend!
23
+ ActionView::Template::Handlers::ERB.prepend(ConditionalImplementation)
24
+ end
25
+
26
+ private
27
+
28
+ module ConditionalImplementation
29
+
30
+ def call(template)
31
+ generate(template)
32
+ end
33
+
34
+ private
35
+
36
+ def generate(template)
37
+ # First, convert to BINARY, so in case the encoding is
38
+ # wrong, we can still find an encoding tag
39
+ # (<%# encoding %>) inside the String using a regular
40
+ # expression
41
+
42
+ filename = template.identifier.split("/").last
43
+ exts = filename.split(".")
44
+ exts = exts[1..exts.length].join(".")
45
+ template_source = template.source.dup.force_encoding(Encoding::ASCII_8BIT)
46
+
47
+ erb = template_source.gsub(ActionView::Template::Handlers::ERB::ENCODING_TAG, '')
48
+ encoding = $2
49
+
50
+ erb.force_encoding valid_encoding(template.source.dup, encoding)
51
+
52
+ # Always make sure we return a String in the default_internal
53
+ erb.encode!
54
+
55
+ excluded_template = !!BetterHtml::Config.template_exclusion_filter_block&.call(template.identifier)
56
+ klass = BetterHtml::BetterErb.content_types[exts] unless excluded_template
57
+ klass ||= self.class.erb_implementation
58
+
59
+ generator = klass.new(
60
+ erb,
61
+ :escape => (self.class.escape_whitelist.include? template.type),
62
+ :trim => (self.class.erb_trim_mode == "-")
63
+ )
64
+ generator.validate! if generator.respond_to?(:validate!)
65
+ generator.src
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,50 @@
1
+ require 'action_view'
2
+ require_relative 'runtime_checks'
3
+
4
+ class BetterHtml::BetterErb
5
+ class ErubiImplementation < ActionView::Template::Handlers::ERB::Erubi
6
+ include RuntimeChecks
7
+
8
+ def add_text(text)
9
+ return if text.empty?
10
+
11
+ if text == "\n"
12
+ @parser.parse("\n")
13
+ @newline_pending += 1
14
+ else
15
+ src << "@output_buffer.safe_append='"
16
+ src << "\n" * @newline_pending if @newline_pending > 0
17
+ src << escape_text(text)
18
+ src << "'.freeze;"
19
+
20
+ @parser.parse(text) do |*args|
21
+ check_token(*args)
22
+ end
23
+
24
+ @newline_pending = 0
25
+ end
26
+ end
27
+
28
+ def add_expression(indicator, code)
29
+ if (indicator == "==") || @escape
30
+ add_expr_auto_escaped(src, code, false)
31
+ else
32
+ add_expr_auto_escaped(src, code, true)
33
+ end
34
+ end
35
+
36
+ def add_code(code)
37
+ flush_newline_if_pending(src)
38
+
39
+ block_check(src, "<%#{code}%>")
40
+ @parser.append_placeholder(code)
41
+ super
42
+ end
43
+
44
+ private
45
+
46
+ def escape_text(text)
47
+ text.gsub(/['\\]/, '\\\\\&')
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,44 @@
1
+ require 'action_view'
2
+ require_relative 'runtime_checks'
3
+
4
+ class BetterHtml::BetterErb
5
+ class ErubisImplementation < ActionView::Template::Handlers::Erubis
6
+ include RuntimeChecks
7
+
8
+ def add_text(src, text)
9
+ return if text.empty?
10
+
11
+ if text == "\n"
12
+ @parser.parse("\n")
13
+ @newline_pending += 1
14
+ else
15
+ src << "@output_buffer.safe_append='"
16
+ src << "\n" * @newline_pending if @newline_pending > 0
17
+ src << escape_text(text)
18
+ src << "'.freeze;"
19
+
20
+ @parser.parse(text) do |*args|
21
+ check_token(*args)
22
+ end
23
+
24
+ @newline_pending = 0
25
+ end
26
+ end
27
+
28
+ def add_expr_literal(src, code)
29
+ add_expr_auto_escaped(src, code, true)
30
+ end
31
+
32
+ def add_expr_escaped(src, code)
33
+ add_expr_auto_escaped(src, code, false)
34
+ end
35
+
36
+ def add_stmt(src, code)
37
+ flush_newline_if_pending(src)
38
+
39
+ block_check(src, "<%#{code}%>")
40
+ @parser.append_placeholder(code)
41
+ super
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,161 @@
1
+ require 'html_tokenizer'
2
+ require 'action_view'
3
+
4
+ class BetterHtml::BetterErb
5
+ module RuntimeChecks
6
+ def initialize(*)
7
+ @parser = HtmlTokenizer::Parser.new
8
+ super
9
+ end
10
+
11
+ def validate!
12
+ check_parser_errors
13
+
14
+ unless @parser.context == :none
15
+ raise BetterHtml::HtmlError, 'Detected an open tag at the end of this document.'
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def class_name
22
+ "BetterHtml::BetterErb::ValidatedOutputBuffer"
23
+ end
24
+
25
+ def wrap_method
26
+ "#{class_name}.wrap"
27
+ end
28
+
29
+ def add_expr_auto_escaped(src, code, auto_escape)
30
+ flush_newline_if_pending(src)
31
+
32
+ src << "#{wrap_method}(@output_buffer, (#{parser_context.inspect}), '#{escape_text(code)}'.freeze, #{auto_escape})"
33
+ method_name = "safe_#{@parser.context}_append"
34
+ if code =~ self.class::BLOCK_EXPR
35
+ block_check(src, "<%=#{code}%>")
36
+ src << ".#{method_name}= " << code
37
+ else
38
+ src << ".#{method_name}=(" << code << ");"
39
+ end
40
+ @parser.append_placeholder("<%=#{code}%>")
41
+ end
42
+
43
+ def parser_context
44
+ if [:quoted_value, :unquoted_value, :space_after_attribute].include?(@parser.context)
45
+ {
46
+ tag_name: @parser.tag_name,
47
+ attribute_name: @parser.attribute_name,
48
+ attribute_value: @parser.attribute_value,
49
+ attribute_quoted: @parser.attribute_quoted?,
50
+ quote_character: @parser.quote_character,
51
+ }
52
+ elsif [:attribute_name, :after_attribute_name, :after_equal].include?(@parser.context)
53
+ {
54
+ tag_name: @parser.tag_name,
55
+ attribute_name: @parser.attribute_name,
56
+ }
57
+ elsif [:tag, :tag_name, :tag_end].include?(@parser.context)
58
+ {
59
+ tag_name: @parser.tag_name,
60
+ }
61
+ elsif @parser.context == :rawtext
62
+ {
63
+ tag_name: @parser.tag_name,
64
+ rawtext_text: @parser.rawtext_text,
65
+ }
66
+ elsif @parser.context == :comment
67
+ {
68
+ comment_text: @parser.comment_text,
69
+ }
70
+ elsif [:none, :solidus_or_tag_name].include?(@parser.context)
71
+ {}
72
+ else
73
+ raise RuntimeError, "Tried to interpolate into unknown location #{@parser.context}."
74
+ end
75
+ end
76
+
77
+ def block_check(src, code)
78
+ unless @parser.context == :none || @parser.context == :rawtext
79
+ s = "Ruby statement not allowed.\n"
80
+ s << "In '#{@parser.context}' on line #{@parser.line_number} column #{@parser.column_number}:\n"
81
+ prefix = extract_line(@parser.line_number)
82
+ code = code.lines.first
83
+ s << "#{prefix}#{code}\n"
84
+ s << "#{' ' * prefix.size}#{'^' * code.size}"
85
+ raise BetterHtml::DontInterpolateHere, s
86
+ end
87
+ end
88
+
89
+ def check_parser_errors
90
+ errors = @parser.errors
91
+ return if errors.empty?
92
+
93
+ s = "#{errors.size} error(s) found in HTML document.\n"
94
+ errors.each do |error|
95
+ s = "#{error.message}\n"
96
+ s << "On line #{error.line} column #{error.column}:\n"
97
+ line = extract_line(error.line)
98
+ s << "#{line}\n"
99
+ s << "#{' ' * (error.column)}#{'^' * (line.size - error.column)}"
100
+ end
101
+
102
+ raise BetterHtml::HtmlError, s
103
+ end
104
+
105
+ def check_token(type, *args)
106
+ check_tag_name(type, *args) if type == :tag_name
107
+ check_attribute_name(type, *args) if type == :attribute_name
108
+ check_quoted_value(type, *args) if type == :attribute_quoted_value_start
109
+ check_unquoted_value(type, *args) if type == :attribute_unquoted_value
110
+ end
111
+
112
+ def check_tag_name(type, start, stop, line, column)
113
+ text = @parser.extract(start, stop)
114
+ return if text.upcase == "!DOCTYPE"
115
+ return if BetterHtml.config.partial_tag_name_pattern === text
116
+
117
+ s = "Invalid tag name #{text.inspect} does not match "\
118
+ "regular expression #{BetterHtml.config.partial_tag_name_pattern.inspect}\n"
119
+ s << build_location(line, column, text.size)
120
+ raise BetterHtml::HtmlError, s
121
+ end
122
+
123
+ def check_attribute_name(type, start, stop, line, column)
124
+ text = @parser.extract(start, stop)
125
+ return if BetterHtml.config.partial_attribute_name_pattern === text
126
+
127
+ s = "Invalid attribute name #{text.inspect} does not match "\
128
+ "regular expression #{BetterHtml.config.partial_attribute_name_pattern.inspect}\n"
129
+ s << build_location(line, column, text.size)
130
+ raise BetterHtml::HtmlError, s
131
+ end
132
+
133
+ def check_quoted_value(type, start, stop, line, column)
134
+ return if BetterHtml.config.allow_single_quoted_attributes
135
+ text = @parser.extract(start, stop)
136
+ return if text == '"'
137
+
138
+ s = "Single-quoted attributes are not allowed\n"
139
+ s << build_location(line, column, text.size)
140
+ raise BetterHtml::HtmlError, s
141
+ end
142
+
143
+ def check_unquoted_value(type, start, stop, line, column)
144
+ return if BetterHtml.config.allow_unquoted_attributes
145
+ s = "Unquoted attribute values are not allowed\n"
146
+ s << build_location(line, column, stop-start)
147
+ raise BetterHtml::HtmlError, s
148
+ end
149
+
150
+ def build_location(line, column, length)
151
+ s = "On line #{line} column #{column}:\n"
152
+ s << "#{extract_line(line)}\n"
153
+ s << "#{' ' * column}#{'^' * length}"
154
+ end
155
+
156
+ def extract_line(line)
157
+ line = @parser.document.lines[line-1]
158
+ line.nil? ? "" : line.gsub(/\n$/, '')
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,166 @@
1
+ module BetterHtml
2
+ class BetterErb
3
+ class ValidatedOutputBuffer
4
+ def self.wrap(output, context, code, auto_escape)
5
+ Context.new(output, context, code, auto_escape)
6
+ end
7
+
8
+ class Context
9
+ def initialize(output, context, code, auto_escape)
10
+ @output = output
11
+ @context = context
12
+ @code = code
13
+ @auto_escape = auto_escape
14
+ end
15
+
16
+ def safe_quoted_value_append=(value)
17
+ return if value.nil?
18
+ value = properly_escaped(value)
19
+
20
+ if value.include?(@context[:quote_character])
21
+ raise UnsafeHtmlError, "Detected invalid characters as part of the interpolation "\
22
+ "into a quoted attribute value. The value cannot contain the character #{@context[:quote_character]}."
23
+ end
24
+
25
+ @output.safe_append= value
26
+ end
27
+
28
+ def safe_unquoted_value_append=(value)
29
+ raise DontInterpolateHere, "Do not interpolate without quotes around this "\
30
+ "attribute value. Instead of "\
31
+ "<#{@context[:tag_name]} #{@context[:attribute_name]}=#{@context[:attribute_value]}<%=#{@code}%>> "\
32
+ "try <#{@context[:tag_name]} #{@context[:attribute_name]}=\"#{@context[:attribute_value]}<%=#{@code}%>\">."
33
+ end
34
+
35
+ def safe_space_after_attribute_append=(value)
36
+ raise DontInterpolateHere, "Add a space after this attribute value. Instead of "\
37
+ "<#{@context[:tag_name]} #{@context[:attribute_name]}=\"#{@context[:attribute_value]}\"<%=#{@code}%>> "\
38
+ "try <#{@context[:tag_name]} #{@context[:attribute_name]}=\"#{@context[:attribute_value]}\" <%=#{@code}%>>."
39
+ end
40
+
41
+ def safe_attribute_name_append=(value)
42
+ return if value.nil?
43
+ value = value.to_s
44
+
45
+ unless value =~ /\A[a-z0-9\-]*\z/
46
+ raise UnsafeHtmlError, "Detected invalid characters as part of the interpolation "\
47
+ "into a attribute name around '#{@context[:attribute_name]}<%=#{@code}%>'."
48
+ end
49
+
50
+ @output.safe_append= value
51
+ end
52
+
53
+ def safe_after_attribute_name_append=(value)
54
+ return if value.nil?
55
+
56
+ unless value.is_a?(BetterHtml::HtmlAttributes)
57
+ raise DontInterpolateHere, "Do not interpolate #{value.class} in a tag. "\
58
+ "Instead of <#{@context[:tag_name]} <%=#{@code}%>> please "\
59
+ "try <#{@context[:tag_name]} <%= html_attributes(attr: value) %>>."
60
+ end
61
+
62
+ @output.safe_append= value.to_s
63
+ end
64
+
65
+ def safe_after_equal_append=(value)
66
+ raise DontInterpolateHere, "Do not interpolate without quotes after "\
67
+ "attribute around '#{@context[:attribute_name]}=<%=#{@code}%>'."
68
+ end
69
+
70
+ def safe_tag_append=(value)
71
+ return if value.nil?
72
+
73
+ unless value.is_a?(BetterHtml::HtmlAttributes)
74
+ raise DontInterpolateHere, "Do not interpolate #{value.class} in a tag. "\
75
+ "Instead of <#{@context[:tag_name]} <%=#{@code}%>> please "\
76
+ "try <#{@context[:tag_name]} <%= html_attributes(attr: value) %>>."
77
+ end
78
+
79
+ @output.safe_append= value.to_s
80
+ end
81
+
82
+ def safe_tag_name_append=(value)
83
+ return if value.nil?
84
+ value = value.to_s
85
+
86
+ unless value =~ /\A[a-z0-9\:\-]*\z/
87
+ raise UnsafeHtmlError, "Detected invalid characters as part of the interpolation "\
88
+ "into a tag name around: <#{@context[:tag_name]}<%=#{@code}%>>."
89
+ end
90
+
91
+ @output.safe_append= value
92
+ end
93
+
94
+ def safe_rawtext_append=(value)
95
+ return if value.nil?
96
+
97
+ value = properly_escaped(value)
98
+
99
+ if @context[:tag_name].downcase == 'script' &&
100
+ (value =~ /<script/i || value =~ /<\/script/i)
101
+ # https://www.w3.org/TR/html5/scripting-1.html#restrictions-for-contents-of-script-elements
102
+ raise UnsafeHtmlError, "Detected invalid characters as part of the interpolation "\
103
+ "into a script tag around: <#{@context[:tag_name]}>#{@context[:rawtext_text]}<%=#{@code}%>. "\
104
+ "A script tag cannot contain <script or </script anywhere inside of it."
105
+ elsif value =~ /<#{Regexp.escape(@context[:tag_name].downcase)}/i ||
106
+ value =~ /<\/#{Regexp.escape(@context[:tag_name].downcase)}/i
107
+ raise UnsafeHtmlError, "Detected invalid characters as part of the interpolation "\
108
+ "into a #{@context[:tag_name].downcase} tag around: <#{@context[:tag_name]}>#{@context[:rawtext_text]}<%=#{@code}%>."
109
+ end
110
+
111
+ @output.safe_append= value
112
+ end
113
+
114
+ def safe_comment_append=(value)
115
+ return if value.nil?
116
+ value = properly_escaped(value)
117
+
118
+ # in a <!-- ...here --> we disallow -->
119
+ if value =~ /-->/
120
+ raise UnsafeHtmlError, "Detected invalid characters as part of the interpolation "\
121
+ "into a html comment around: <!--#{@context[:comment_text]}<%=#{@code}%>."
122
+ end
123
+
124
+ @output.safe_append= value
125
+ end
126
+
127
+ def safe_none_append=(value)
128
+ return if value.nil?
129
+ @output.safe_append= properly_escaped(value)
130
+ end
131
+
132
+ private
133
+
134
+ def properly_escaped(value)
135
+ if value.is_a?(ValidatedOutputBuffer)
136
+ # in html context, never escape a ValidatedOutputBuffer
137
+ value.to_s
138
+ else
139
+ # in html context, follow auto_escape rule
140
+ if @auto_escape
141
+ auto_escape_html_safe_value(value.to_s)
142
+ else
143
+ value.to_s
144
+ end
145
+ end
146
+ end
147
+
148
+ def auto_escape_html_safe_value(arg)
149
+ arg.html_safe? ? arg : CGI.escapeHTML(arg).html_safe
150
+ end
151
+ end
152
+
153
+ def html_safe?
154
+ true
155
+ end
156
+
157
+ def html_safe
158
+ self.class.new(@output)
159
+ end
160
+
161
+ def to_s
162
+ @output.html_safe
163
+ end
164
+ end
165
+ end
166
+ end