haml 4.1.0.beta.1 → 5.0.0.beta.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of haml might be problematic. Click here for more details.

Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +36 -6
  4. data/FAQ.md +4 -14
  5. data/MIT-LICENSE +1 -1
  6. data/README.md +81 -48
  7. data/REFERENCE.md +86 -50
  8. data/Rakefile +28 -41
  9. data/lib/haml/attribute_builder.rb +163 -0
  10. data/lib/haml/attribute_compiler.rb +214 -0
  11. data/lib/haml/attribute_parser.rb +112 -0
  12. data/lib/haml/buffer.rb +24 -126
  13. data/lib/haml/compiler.rb +62 -281
  14. data/lib/haml/engine.rb +16 -23
  15. data/lib/haml/error.rb +2 -0
  16. data/lib/haml/escapable.rb +48 -0
  17. data/lib/haml/exec.rb +23 -12
  18. data/lib/haml/filters.rb +3 -4
  19. data/lib/haml/generator.rb +36 -0
  20. data/lib/haml/helpers.rb +61 -48
  21. data/lib/haml/helpers/action_view_extensions.rb +1 -1
  22. data/lib/haml/helpers/action_view_mods.rb +32 -50
  23. data/lib/haml/helpers/safe_erubi_template.rb +26 -0
  24. data/lib/haml/helpers/safe_erubis_template.rb +2 -0
  25. data/lib/haml/helpers/xss_mods.rb +17 -12
  26. data/lib/haml/options.rb +32 -36
  27. data/lib/haml/parser.rb +61 -38
  28. data/lib/haml/{template/plugin.rb → plugin.rb} +5 -2
  29. data/lib/haml/railtie.rb +14 -6
  30. data/lib/haml/template.rb +11 -6
  31. data/lib/haml/temple_engine.rb +119 -0
  32. data/lib/haml/temple_line_counter.rb +28 -0
  33. data/lib/haml/util.rb +17 -112
  34. data/lib/haml/version.rb +1 -1
  35. data/test/attribute_parser_test.rb +105 -0
  36. data/test/engine_test.rb +202 -106
  37. data/test/filters_test.rb +32 -19
  38. data/test/gemfiles/Gemfile.rails-4.0.x +7 -1
  39. data/test/gemfiles/Gemfile.rails-4.0.x.lock +57 -71
  40. data/test/gemfiles/Gemfile.rails-4.1.x +5 -0
  41. data/test/gemfiles/Gemfile.rails-4.2.x +5 -0
  42. data/test/gemfiles/Gemfile.rails-5.0.x +4 -0
  43. data/test/helper_test.rb +156 -109
  44. data/test/options_test.rb +21 -0
  45. data/test/parser_test.rb +49 -4
  46. data/test/results/eval_suppressed.xhtml +4 -4
  47. data/test/results/helpers.xhtml +43 -41
  48. data/test/results/helpful.xhtml +6 -3
  49. data/test/results/just_stuff.xhtml +21 -20
  50. data/test/results/list.xhtml +9 -9
  51. data/test/results/nuke_inner_whitespace.xhtml +22 -22
  52. data/test/results/nuke_outer_whitespace.xhtml +84 -92
  53. data/test/results/original_engine.xhtml +17 -17
  54. data/test/results/partial_layout.xhtml +4 -3
  55. data/test/results/partial_layout_erb.xhtml +4 -3
  56. data/test/results/partials.xhtml +11 -10
  57. data/test/results/silent_script.xhtml +63 -63
  58. data/test/results/standard.xhtml +156 -159
  59. data/test/results/tag_parsing.xhtml +19 -19
  60. data/test/results/very_basic.xhtml +2 -2
  61. data/test/results/whitespace_handling.xhtml +77 -76
  62. data/test/template_test.rb +21 -48
  63. data/test/template_test_helper.rb +38 -0
  64. data/test/templates/just_stuff.haml +1 -0
  65. data/test/templates/standard_ugly.haml +1 -0
  66. data/test/temple_line_counter_test.rb +40 -0
  67. data/test/test_helper.rb +10 -10
  68. data/test/util_test.rb +1 -48
  69. metadata +49 -35
  70. data/lib/haml/temple.rb +0 -85
  71. data/test/gemfiles/Gemfile.rails-3.2.x +0 -4
  72. data/test/templates/_av_partial_1_ugly.haml +0 -9
  73. data/test/templates/_av_partial_2_ugly.haml +0 -5
  74. data/test/templates/action_view_ugly.haml +0 -47
  75. data/test/templates/standard_ugly.haml +0 -43
data/Rakefile CHANGED
@@ -1,19 +1,12 @@
1
1
  require "rake/clean"
2
2
  require "rake/testtask"
3
- require "rubygems/package_task"
3
+ require "bundler/gem_tasks"
4
4
 
5
5
  task :default => :test
6
6
 
7
7
  CLEAN.replace %w(pkg doc coverage .yardoc test/haml vendor)
8
8
 
9
- def silence_warnings
10
- the_real_stderr, $stderr = $stderr, StringIO.new
11
- yield
12
- ensure
13
- $stderr = the_real_stderr
14
- end
15
-
16
- desc "Benchmark Haml against ERb. TIMES=n sets the number of runs, default is 1000."
9
+ desc "Benchmark Haml against ERB. TIMES=n sets the number of runs, default is 1000."
17
10
  task :benchmark do
18
11
  sh "ruby benchmark.rb #{ENV['TIMES']}"
19
12
  end
@@ -36,11 +29,6 @@ end
36
29
  desc "Run Simplecov"
37
30
  task :coverage => [:set_coverage_env, :test]
38
31
 
39
- gemspec = File.expand_path("../haml.gemspec", __FILE__)
40
- if File.exist? gemspec
41
- Gem::PackageTask.new(eval(File.read(gemspec))) { |pkg| }
42
- end
43
-
44
32
  task :submodules do
45
33
  if File.exist?(File.dirname(__FILE__) + "/.git")
46
34
  sh %{git submodule sync}
@@ -48,31 +36,32 @@ task :submodules do
48
36
  end
49
37
  end
50
38
 
51
- begin
52
- silence_warnings do
53
- require 'yard'
54
- end
55
-
56
- namespace :doc do
57
- desc "List all undocumented methods and classes."
58
- task :undocumented do
59
- command = 'yard --list --query '
60
- command << '"object.docstring.blank? && '
61
- command << '!(object.type == :method && object.is_alias?)"'
62
- sh command
39
+ namespace :doc do
40
+ task :sass do
41
+ require 'sass'
42
+ Dir["yard/default/**/*.sass"].each do |sass|
43
+ File.open(sass.gsub(/sass$/, 'css'), 'w') do |f|
44
+ f.write(Sass::Engine.new(File.read(sass)).render)
45
+ end
63
46
  end
64
47
  end
65
48
 
66
- desc "Generate documentation"
67
- task(:doc) {sh "yard"}
49
+ desc "List all undocumented methods and classes."
50
+ task :undocumented do
51
+ command = 'yard --list --query '
52
+ command << '"object.docstring.blank? && '
53
+ command << '!(object.type == :method && object.is_alias?)"'
54
+ sh command
55
+ end
56
+ end
68
57
 
69
- desc "Generate documentation incrementally"
70
- task(:redoc) {sh "yard -c"}
58
+ desc "Generate documentation"
59
+ task(:doc => 'doc:sass') {sh "yard"}
71
60
 
72
- rescue LoadError
73
- end
61
+ desc "Generate documentation incrementally"
62
+ task(:redoc) {sh "yard -c"}
74
63
 
75
- desc <<END
64
+ desc <<END
76
65
  Profile Haml.
77
66
  TIMES=n sets the number of runs. Defaults to 1000.
78
67
  FILE=str sets the file to profile. Defaults to 'standard'
@@ -86,10 +75,9 @@ task :profile do
86
75
  require 'bundler/setup'
87
76
  require 'ruby-prof'
88
77
  require 'haml'
89
- default =
90
78
  file = File.read(File.expand_path("../#{file}", __FILE__))
91
79
  obj = Object.new
92
- Haml::Engine.new(file, :ugly => true).def_method(obj, :render)
80
+ Haml::Engine.new(file).def_method(obj, :render)
93
81
  result = RubyProf.profile { times.times { obj.render } }
94
82
 
95
83
  RubyProf.const_get("#{(ENV['OUTPUT'] || 'Flat').capitalize}Printer").new(result).print
@@ -104,14 +92,13 @@ def gemfiles
104
92
  end
105
93
 
106
94
  def with_each_gemfile
107
- old_env = ENV['BUNDLE_GEMFILE']
108
95
  gemfiles.each do |gemfile|
109
- puts "Using gemfile: #{gemfile}"
110
- ENV['BUNDLE_GEMFILE'] = gemfile
111
- yield
96
+ Bundler.with_clean_env do
97
+ puts "Using gemfile: #{gemfile}"
98
+ ENV['BUNDLE_GEMFILE'] = gemfile
99
+ yield
100
+ end
112
101
  end
113
- ensure
114
- ENV['BUNDLE_GEMFILE'] = old_env
115
102
  end
116
103
 
117
104
  namespace :test do
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+ module Haml
3
+ module AttributeBuilder
4
+ # https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
5
+ INVALID_ATTRIBUTE_NAME_REGEX = /[ \0"'>\/=]/
6
+
7
+ class << self
8
+ def build_attributes(is_html, attr_wrapper, escape_attrs, hyphenate_data_attrs, attributes = {})
9
+ # @TODO this is an absolutely ridiculous amount of arguments. At least
10
+ # some of this needs to be moved into an instance method.
11
+ join_char = hyphenate_data_attrs ? '-' : '_'
12
+
13
+ attributes.each do |key, value|
14
+ if value.is_a?(Hash)
15
+ data_attributes = attributes.delete(key)
16
+ data_attributes = flatten_data_attributes(data_attributes, '', join_char)
17
+ data_attributes = build_data_keys(data_attributes, hyphenate_data_attrs, key)
18
+ verify_attribute_names!(data_attributes.keys)
19
+ attributes = data_attributes.merge(attributes)
20
+ end
21
+ end
22
+
23
+ result = attributes.collect do |attr, value|
24
+ next if value.nil?
25
+
26
+ value = filter_and_join(value, ' ') if attr == 'class'
27
+ value = filter_and_join(value, '_') if attr == 'id'
28
+
29
+ if value == true
30
+ next " #{attr}" if is_html
31
+ next " #{attr}=#{attr_wrapper}#{attr}#{attr_wrapper}"
32
+ elsif value == false
33
+ next
34
+ end
35
+
36
+ value =
37
+ if escape_attrs == :once
38
+ Haml::Helpers.escape_once(value.to_s)
39
+ elsif escape_attrs
40
+ Haml::Helpers.html_escape(value.to_s)
41
+ else
42
+ value.to_s
43
+ end
44
+ " #{attr}=#{attr_wrapper}#{value}#{attr_wrapper}"
45
+ end
46
+ result.compact!
47
+ result.sort!
48
+ result.join
49
+ end
50
+
51
+ # @return [String, nil]
52
+ def filter_and_join(value, separator)
53
+ return '' if (value.respond_to?(:empty?) && value.empty?)
54
+
55
+ if value.is_a?(Array)
56
+ value = value.flatten
57
+ value.map! {|item| item ? item.to_s : nil}
58
+ value.compact!
59
+ value = value.join(separator)
60
+ else
61
+ value = value ? value.to_s : nil
62
+ end
63
+ !value.nil? && !value.empty? && value
64
+ end
65
+
66
+ # Merges two attribute hashes.
67
+ # This is the same as `to.merge!(from)`,
68
+ # except that it merges id, class, and data attributes.
69
+ #
70
+ # ids are concatenated with `"_"`,
71
+ # and classes are concatenated with `" "`.
72
+ # data hashes are simply merged.
73
+ #
74
+ # Destructively modifies `to`.
75
+ #
76
+ # @param to [{String => String,Hash}] The attribute hash to merge into
77
+ # @param from [{String => Object}] The attribute hash to merge from
78
+ # @return [{String => String,Hash}] `to`, after being merged
79
+ def merge_attributes!(to, from)
80
+ from.keys.each do |key|
81
+ to[key] = merge_value(key, to[key], from[key])
82
+ end
83
+ to
84
+ end
85
+
86
+ # Merge multiple values to one attribute value. No destructive operation.
87
+ #
88
+ # @param key [String]
89
+ # @param values [Array<Object>]
90
+ # @return [String,Hash]
91
+ def merge_values(key, *values)
92
+ values.inject(nil) do |to, from|
93
+ merge_value(key, to, from)
94
+ end
95
+ end
96
+
97
+ def verify_attribute_names!(attribute_names)
98
+ attribute_names.each do |attribute_name|
99
+ if attribute_name =~ INVALID_ATTRIBUTE_NAME_REGEX
100
+ raise InvalidAttributeNameError.new("Invalid attribute name '#{attribute_name}' was rendered")
101
+ end
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ # Merge a couple of values to one attribute value. No destructive operation.
108
+ #
109
+ # @param to [String,Hash,nil]
110
+ # @param from [Object]
111
+ # @return [String,Hash]
112
+ def merge_value(key, to, from)
113
+ if from.kind_of?(Hash) || to.kind_of?(Hash)
114
+ from = { nil => from } if !from.is_a?(Hash)
115
+ to = { nil => to } if !to.is_a?(Hash)
116
+ to.merge(from)
117
+ elsif key == 'id'
118
+ merged_id = filter_and_join(from, '_')
119
+ if to && merged_id
120
+ merged_id = "#{to}_#{merged_id}"
121
+ elsif to || merged_id
122
+ merged_id ||= to
123
+ end
124
+ merged_id
125
+ elsif key == 'class'
126
+ merged_class = filter_and_join(from, ' ')
127
+ if to && merged_class
128
+ merged_class = (merged_class.split(' ') | to.split(' ')).sort.join(' ')
129
+ elsif to || merged_class
130
+ merged_class ||= to
131
+ end
132
+ merged_class
133
+ else
134
+ from
135
+ end
136
+ end
137
+
138
+ def build_data_keys(data_hash, hyphenate, attr_name="data")
139
+ Hash[data_hash.map do |name, value|
140
+ if name == nil
141
+ [attr_name, value]
142
+ elsif hyphenate
143
+ ["#{attr_name}-#{name.to_s.tr('_', '-')}", value]
144
+ else
145
+ ["#{attr_name}-#{name}", value]
146
+ end
147
+ end]
148
+ end
149
+
150
+ def flatten_data_attributes(data, key, join_char, seen = [])
151
+ return {key => data} unless data.is_a?(Hash)
152
+
153
+ return {key => nil} if seen.include? data.object_id
154
+ seen << data.object_id
155
+
156
+ data.sort {|x, y| x[0].to_s <=> y[0].to_s}.inject({}) do |hash, (k, v)|
157
+ joined = key == '' ? k : [key, k].join(join_char)
158
+ hash.merge! flatten_data_attributes(v, joined, join_char, seen)
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,214 @@
1
+ require 'haml/attribute_parser'
2
+
3
+ module Haml
4
+ class AttributeCompiler
5
+ # @param type [Symbol] :static or :dynamic
6
+ # @param key [String]
7
+ # @param value [String] Actual string value for :static type, value's Ruby literal for :dynamic type.
8
+ class AttributeValue < Struct.new(:type, :key, :value)
9
+ # @return [String] A Ruby literal of value.
10
+ def to_literal
11
+ case type
12
+ when :static
13
+ Haml::Util.inspect_obj(value)
14
+ when :dynamic
15
+ value
16
+ end
17
+ end
18
+
19
+ # Key's substring before a hyphen. This is necessary because values with the same
20
+ # base_key can conflict by Haml::AttributeBuidler#build_data_keys.
21
+ def base_key
22
+ key.split('-', 2).first
23
+ end
24
+ end
25
+
26
+ # Returns a script to render attributes on runtime.
27
+ #
28
+ # @param attributes [Hash]
29
+ # @param object_ref [String,:nil]
30
+ # @param attributes_hashes [Array<String>]
31
+ # @return [String] Attributes rendering code
32
+ def self.runtime_build(attributes, object_ref, attributes_hashes)
33
+ "_hamlout.attributes(#{Haml::Util.inspect_obj(attributes)}, #{object_ref},#{attributes_hashes.join(', ')})"
34
+ end
35
+
36
+ # @param options [Haml::Options]
37
+ def initialize(options)
38
+ @is_html = [:html4, :html5].include?(options[:format])
39
+ @attr_wrapper = options[:attr_wrapper]
40
+ @escape_attrs = options[:escape_attrs]
41
+ @hyphenate_data_attrs = options[:hyphenate_data_attrs]
42
+ end
43
+
44
+ # Returns Temple expression to render attributes.
45
+ #
46
+ # @param attributes [Hash]
47
+ # @param object_ref [String,:nil]
48
+ # @param attributes_hashes [Array<String>]
49
+ # @return [Array] Temple expression
50
+ def compile(attributes, object_ref, attributes_hashes)
51
+ if object_ref != :nil || !AttributeParser.available?
52
+ return [:dynamic, AttributeCompiler.runtime_build(attributes, object_ref, attributes_hashes)]
53
+ end
54
+
55
+ parsed_hashes = attributes_hashes.map do |attribute_hash|
56
+ unless (hash = AttributeParser.parse(attribute_hash))
57
+ return [:dynamic, AttributeCompiler.runtime_build(attributes, object_ref, attributes_hashes)]
58
+ end
59
+ hash
60
+ end
61
+ attribute_values = build_attribute_values(attributes, parsed_hashes)
62
+ AttributeBuilder.verify_attribute_names!(attribute_values.map(&:key))
63
+
64
+ values_by_base_key = attribute_values.group_by(&:base_key)
65
+ [:multi, *values_by_base_key.keys.sort.map { |base_key|
66
+ compile_attribute_values(values_by_base_key[base_key])
67
+ }]
68
+ end
69
+
70
+ private
71
+
72
+ # Returns array of AttributeValue instnces from static attributes and dynamic attributes_hashes. For each key,
73
+ # the values' order in returned value is preserved in the same order as Haml::Buffer#attributes's merge order.
74
+ #
75
+ # @param attributes [{ String => String }]
76
+ # @param parsed_hashes [{ String => String }]
77
+ # @return [Array<AttributeValue>]
78
+ def build_attribute_values(attributes, parsed_hashes)
79
+ [].tap do |attribute_values|
80
+ attributes.each do |key, static_value|
81
+ attribute_values << AttributeValue.new(:static, key, static_value)
82
+ end
83
+ parsed_hashes.each do |parsed_hash|
84
+ parsed_hash.each do |key, dynamic_value|
85
+ attribute_values << AttributeValue.new(:dynamic, key, dynamic_value)
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ # Compiles attribute values with the same base_key to Temple expression.
92
+ #
93
+ # @param values [Array<AttributeValue>] `base_key`'s results are the same. `key`'s result may differ.
94
+ # @return [Array] Temple expression
95
+ def compile_attribute_values(values)
96
+ if values.map(&:key).uniq.size == 1
97
+ compile_attribute(values.first.key, values)
98
+ else
99
+ runtime_build(values)
100
+ end
101
+ end
102
+
103
+ # @param values [Array<AttributeValue>]
104
+ # @return [Array] Temple expression
105
+ def runtime_build(values)
106
+ hash_content = values.group_by(&:key).map do |key, values_for_key|
107
+ "#{frozen_string(key)} => #{merged_value(key, values_for_key)}"
108
+ end.join(', ')
109
+ [:dynamic, "_hamlout.attributes({ #{hash_content} }, nil)"]
110
+ end
111
+
112
+ # Renders attribute values statically.
113
+ #
114
+ # @param values [Array<AttributeValue>]
115
+ # @return [Array] Temple expression
116
+ def static_build(values)
117
+ hash_content = values.group_by(&:key).map do |key, values_for_key|
118
+ "#{frozen_string(key)} => #{merged_value(key, values_for_key)}"
119
+ end.join(', ')
120
+
121
+ arguments = [@is_html, @attr_wrapper, @escape_attrs, @hyphenate_data_attrs]
122
+ code = "::Haml::AttributeBuilder.build_attributes"\
123
+ "(#{arguments.map { |a| Haml::Util.inspect_obj(a) }.join(', ')}, { #{hash_content} })"
124
+ [:static, eval(code).to_s]
125
+ end
126
+
127
+ # @param key [String]
128
+ # @param values [Array<AttributeValue>]
129
+ # @return [String]
130
+ def merged_value(key, values)
131
+ if values.size == 1
132
+ values.first.to_literal
133
+ else
134
+ "::Haml::AttributeBuilder.merge_values(#{frozen_string(key)}, #{values.map(&:to_literal).join(', ')})"
135
+ end
136
+ end
137
+
138
+ # @param str [String]
139
+ # @return [String]
140
+ def frozen_string(str)
141
+ "#{Haml::Util.inspect_obj(str)}.freeze"
142
+ end
143
+
144
+ # Compiles attribute values for one key to Temple expression that generates ` key='value'`.
145
+ #
146
+ # @param key [String]
147
+ # @param values [Array<AttributeValue>]
148
+ # @return [Array] Temple expression
149
+ def compile_attribute(key, values)
150
+ if values.all? { |v| Temple::StaticAnalyzer.static?(v.to_literal) }
151
+ return static_build(values)
152
+ end
153
+
154
+ case key
155
+ when 'id', 'class'
156
+ compile_id_or_class_attribute(key, values)
157
+ else
158
+ compile_common_attribute(key, values)
159
+ end
160
+ end
161
+
162
+ # @param id_or_class [String] "id" or "class"
163
+ # @param values [Array<AttributeValue>]
164
+ # @return [Array] Temple expression
165
+ def compile_id_or_class_attribute(id_or_class, values)
166
+ var = unique_name
167
+ [:multi,
168
+ [:code, "#{var} = (#{merged_value(id_or_class, values)})"],
169
+ [:case, var,
170
+ ['Hash, Array', runtime_build([AttributeValue.new(:dynamic, id_or_class, var)])],
171
+ ['false, nil', [:multi]],
172
+ [:else, [:multi,
173
+ [:static, " #{id_or_class}=#{@attr_wrapper}"],
174
+ [:escape, @escape_attrs, [:dynamic, var]],
175
+ [:static, @attr_wrapper]],
176
+ ]
177
+ ],
178
+ ]
179
+ end
180
+
181
+ # @param key [String] Not "id" or "class"
182
+ # @param values [Array<AttributeValue>]
183
+ # @return [Array] Temple expression
184
+ def compile_common_attribute(key, values)
185
+ var = unique_name
186
+ [:multi,
187
+ [:code, "#{var} = (#{merged_value(key, values)})"],
188
+ [:case, var,
189
+ ['Hash', runtime_build([AttributeValue.new(:dynamic, key, var)])],
190
+ ['true', true_value(key)],
191
+ ['false, nil', [:multi]],
192
+ [:else, [:multi,
193
+ [:static, " #{key}=#{@attr_wrapper}"],
194
+ [:escape, @escape_attrs, [:dynamic, var]],
195
+ [:static, @attr_wrapper]],
196
+ ]
197
+ ],
198
+ ]
199
+ end
200
+
201
+ def true_value(key)
202
+ if @is_html
203
+ [:static, " #{key}"]
204
+ else
205
+ [:static, " #{key}=#{@attr_wrapper}#{key}#{@attr_wrapper}"]
206
+ end
207
+ end
208
+
209
+ def unique_name
210
+ @unique_name ||= 0
211
+ "_haml_attribute_compiler#{@unique_name += 1}"
212
+ end
213
+ end
214
+ end