haml 4.0.6 → 5.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +19 -0
  3. data/.gitmodules +3 -0
  4. data/.travis.yml +72 -0
  5. data/.yardopts +2 -3
  6. data/CHANGELOG.md +138 -4
  7. data/FAQ.md +4 -14
  8. data/Gemfile +16 -0
  9. data/MIT-LICENSE +2 -2
  10. data/README.md +79 -42
  11. data/REFERENCE.md +142 -67
  12. data/Rakefile +44 -63
  13. data/TODO +24 -0
  14. data/benchmark.rb +70 -0
  15. data/haml.gemspec +45 -0
  16. data/lib/haml.rb +2 -0
  17. data/lib/haml/.gitattributes +1 -0
  18. data/lib/haml/attribute_builder.rb +164 -0
  19. data/lib/haml/attribute_compiler.rb +235 -0
  20. data/lib/haml/attribute_parser.rb +150 -0
  21. data/lib/haml/buffer.rb +29 -136
  22. data/lib/haml/compiler.rb +110 -320
  23. data/lib/haml/engine.rb +34 -41
  24. data/lib/haml/error.rb +28 -24
  25. data/lib/haml/escapable.rb +77 -0
  26. data/lib/haml/exec.rb +38 -20
  27. data/lib/haml/filters.rb +22 -27
  28. data/lib/haml/generator.rb +42 -0
  29. data/lib/haml/helpers.rb +134 -89
  30. data/lib/haml/helpers/action_view_extensions.rb +4 -2
  31. data/lib/haml/helpers/action_view_mods.rb +45 -60
  32. data/lib/haml/helpers/action_view_xss_mods.rb +2 -0
  33. data/lib/haml/helpers/safe_erubi_template.rb +20 -0
  34. data/lib/haml/helpers/safe_erubis_template.rb +5 -1
  35. data/lib/haml/helpers/xss_mods.rb +23 -13
  36. data/lib/haml/options.rb +63 -69
  37. data/lib/haml/parser.rb +292 -228
  38. data/lib/haml/plugin.rb +37 -0
  39. data/lib/haml/railtie.rb +38 -12
  40. data/lib/haml/sass_rails_filter.rb +18 -4
  41. data/lib/haml/template.rb +13 -6
  42. data/lib/haml/template/options.rb +13 -2
  43. data/lib/haml/temple_engine.rb +123 -0
  44. data/lib/haml/temple_line_counter.rb +30 -0
  45. data/lib/haml/util.rb +83 -202
  46. data/lib/haml/version.rb +3 -1
  47. data/yard/default/.gitignore +1 -0
  48. data/yard/default/fulldoc/html/css/common.sass +15 -0
  49. data/yard/default/layout/html/footer.erb +12 -0
  50. metadata +73 -108
  51. data/lib/haml/template/plugin.rb +0 -41
  52. data/test/engine_test.rb +0 -2013
  53. data/test/erb/_av_partial_1.erb +0 -12
  54. data/test/erb/_av_partial_2.erb +0 -8
  55. data/test/erb/action_view.erb +0 -62
  56. data/test/erb/standard.erb +0 -55
  57. data/test/filters_test.rb +0 -254
  58. data/test/gemfiles/Gemfile.rails-3.0.x +0 -5
  59. data/test/gemfiles/Gemfile.rails-3.1.x +0 -6
  60. data/test/gemfiles/Gemfile.rails-3.2.x +0 -5
  61. data/test/gemfiles/Gemfile.rails-4.0.x +0 -5
  62. data/test/helper_test.rb +0 -583
  63. data/test/markaby/standard.mab +0 -52
  64. data/test/mocks/article.rb +0 -6
  65. data/test/parser_test.rb +0 -105
  66. data/test/results/content_for_layout.xhtml +0 -12
  67. data/test/results/eval_suppressed.xhtml +0 -9
  68. data/test/results/helpers.xhtml +0 -70
  69. data/test/results/helpful.xhtml +0 -10
  70. data/test/results/just_stuff.xhtml +0 -70
  71. data/test/results/list.xhtml +0 -12
  72. data/test/results/nuke_inner_whitespace.xhtml +0 -40
  73. data/test/results/nuke_outer_whitespace.xhtml +0 -148
  74. data/test/results/original_engine.xhtml +0 -20
  75. data/test/results/partial_layout.xhtml +0 -5
  76. data/test/results/partial_layout_erb.xhtml +0 -5
  77. data/test/results/partials.xhtml +0 -21
  78. data/test/results/render_layout.xhtml +0 -3
  79. data/test/results/silent_script.xhtml +0 -74
  80. data/test/results/standard.xhtml +0 -162
  81. data/test/results/tag_parsing.xhtml +0 -23
  82. data/test/results/very_basic.xhtml +0 -5
  83. data/test/results/whitespace_handling.xhtml +0 -90
  84. data/test/template_test.rb +0 -354
  85. data/test/templates/_av_partial_1.haml +0 -9
  86. data/test/templates/_av_partial_1_ugly.haml +0 -9
  87. data/test/templates/_av_partial_2.haml +0 -5
  88. data/test/templates/_av_partial_2_ugly.haml +0 -5
  89. data/test/templates/_layout.erb +0 -3
  90. data/test/templates/_layout_for_partial.haml +0 -3
  91. data/test/templates/_partial.haml +0 -8
  92. data/test/templates/_text_area.haml +0 -3
  93. data/test/templates/_text_area_helper.html.haml +0 -4
  94. data/test/templates/action_view.haml +0 -47
  95. data/test/templates/action_view_ugly.haml +0 -47
  96. data/test/templates/breakage.haml +0 -8
  97. data/test/templates/content_for_layout.haml +0 -8
  98. data/test/templates/eval_suppressed.haml +0 -11
  99. data/test/templates/helpers.haml +0 -55
  100. data/test/templates/helpful.haml +0 -11
  101. data/test/templates/just_stuff.haml +0 -85
  102. data/test/templates/list.haml +0 -12
  103. data/test/templates/nuke_inner_whitespace.haml +0 -32
  104. data/test/templates/nuke_outer_whitespace.haml +0 -144
  105. data/test/templates/original_engine.haml +0 -17
  106. data/test/templates/partial_layout.haml +0 -3
  107. data/test/templates/partial_layout_erb.erb +0 -4
  108. data/test/templates/partialize.haml +0 -1
  109. data/test/templates/partials.haml +0 -12
  110. data/test/templates/render_layout.haml +0 -2
  111. data/test/templates/silent_script.haml +0 -45
  112. data/test/templates/standard.haml +0 -43
  113. data/test/templates/standard_ugly.haml +0 -43
  114. data/test/templates/tag_parsing.haml +0 -21
  115. data/test/templates/very_basic.haml +0 -4
  116. data/test/templates/whitespace_handling.haml +0 -87
  117. data/test/test_helper.rb +0 -81
  118. data/test/util_test.rb +0 -63
data/TODO ADDED
@@ -0,0 +1,24 @@
1
+ # -*- mode: org -*-
2
+ #+STARTUP: nofold
3
+
4
+ * Documentation
5
+ Redo tutorial?
6
+ Using helpers
7
+ haml_concat and haml_tag in particular
8
+ Syntax highlighting?
9
+
10
+ * Code
11
+ Keep track of error offsets everywhere
12
+ Use this to show error location in messages
13
+ ** Haml
14
+ Support finer-grained HTML-escaping in filters
15
+ Speed
16
+ Make tags with dynamic attributes pre-render as much as possible
17
+ Including the attribute name where doable
18
+ :html improvements
19
+ Ignore closing tags where we can
20
+ http://code.google.com/speed/articles/optimizing-html.html
21
+ Requires Haml parsing refactor
22
+ Don't quote attributes that don't require it
23
+ http://www.w3.org/TR/REC-html40/intro/sgmltut.html#h-3.2.2
24
+ http://www.w3.org/TR/html5/syntax.html#attributes
@@ -0,0 +1,70 @@
1
+ require "bundler/setup"
2
+ require "haml"
3
+ require "rbench"
4
+
5
+ times = (ARGV.first || 1000).to_i
6
+
7
+ if times == 0 # Invalid parameter
8
+ puts <<END
9
+ ruby #$0 [times=1000]
10
+ Benchmark Haml against various other templating languages.
11
+ END
12
+ exit 1
13
+ end
14
+
15
+ %w[erb erubi rails active_support action_controller
16
+ action_view action_pack haml/template rbench].each {|dep| require(dep)}
17
+
18
+ def view
19
+ base = ActionView::Base.new
20
+ base.view_paths << File.join(File.dirname(__FILE__), '/test')
21
+ base
22
+ end
23
+
24
+ def render(view, file)
25
+ view.render :file => file
26
+ end
27
+
28
+ RBench.run(times) do
29
+ column :haml, :title => "Haml"
30
+ column :erb, :title => "ERB"
31
+ column :erubi, :title => "Erubi"
32
+
33
+ template_name = 'standard'
34
+ haml_template = File.read("#{File.dirname(__FILE__)}/test/templates/#{template_name}.haml")
35
+ erb_template = File.read("#{File.dirname(__FILE__)}/test/erb/#{template_name}.erb")
36
+
37
+ report "Cached" do
38
+ obj = Object.new
39
+
40
+ Haml::Engine.new(haml_template).def_method(obj, :haml)
41
+ if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+
42
+ obj.instance_eval("def erb; #{ERB.new(erb_template, trim_mode: '-').src}; end")
43
+ else
44
+ obj.instance_eval("def erb; #{ERB.new(erb_template, nil, '-').src}; end")
45
+ end
46
+ obj.instance_eval("def erubi; #{Erubi::Engine.new(erb_template).src}; end")
47
+
48
+ haml { obj.haml }
49
+ erb { obj.erb }
50
+ erubi { obj.erubi }
51
+ end
52
+
53
+ report "ActionView" do
54
+ # To cache the template
55
+ render view, 'templates/standard'
56
+ render view, 'erb/standard'
57
+
58
+ haml { render view, 'templates/standard' }
59
+ erubi { render view, 'erb/standard' }
60
+ end
61
+
62
+ report "ActionView with deep partials" do
63
+ # To cache the template
64
+ render view, 'templates/action_view'
65
+ render view, 'erb/action_view'
66
+
67
+ haml { render view, 'templates/action_view' }
68
+ erubi { render view, 'erb/action_view' }
69
+ end
70
+ end
@@ -0,0 +1,45 @@
1
+ ($LOAD_PATH << File.expand_path("../lib", __FILE__)).uniq!
2
+ require "haml/version"
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = 'haml'
6
+ spec.summary = "An elegant, structured (X)HTML/XML templating engine."
7
+ spec.version = Haml::VERSION
8
+ spec.authors = ['Natalie Weizenbaum', 'Hampton Catlin', 'Norman Clarke', 'Akira Matsuda']
9
+ spec.email = ['haml@googlegroups.com', 'ronnie@dio.jp']
10
+
11
+ spec.executables = ['haml']
12
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
13
+ f.match(%r{\Atest/})
14
+ end
15
+ spec.homepage = 'http://haml.info/'
16
+ spec.license = "MIT"
17
+ spec.metadata = {
18
+ "bug_tracker_uri" => "https://github.com/haml/haml/issues",
19
+ "changelog_uri" => "https://github.com/haml/haml/blob/main/CHANGELOG.md",
20
+ "documentation_uri" => "http://haml.info/docs.html",
21
+ "homepage_uri" => "http://haml.info",
22
+ "mailing_list_uri" => "https://groups.google.com/forum/?fromgroups#!forum/haml",
23
+ "source_code_uri" => "https://github.com/haml/haml"
24
+ }
25
+
26
+ spec.required_ruby_version = '>= 2.0.0'
27
+
28
+ spec.add_dependency 'temple', '>= 0.8.0'
29
+ spec.add_dependency 'tilt'
30
+
31
+ spec.add_development_dependency 'rails', '>= 4.0.0'
32
+ spec.add_development_dependency 'rbench'
33
+ spec.add_development_dependency 'minitest', '>= 4.0'
34
+ spec.add_development_dependency 'nokogiri'
35
+ spec.add_development_dependency 'simplecov', '0.17.1' # Locked to this version due to https://github.com/codeclimate/test-reporter/issues/418
36
+
37
+ spec.description = <<-END
38
+ Haml (HTML Abstraction Markup Language) is a layer on top of HTML or XML that's
39
+ designed to express the structure of documents in a non-repetitive, elegant, and
40
+ easy way by using indentation rather than closing tags and allowing Ruby to be
41
+ embedded with ease. It was originally envisioned as a plugin for Ruby on Rails,
42
+ but it can function as a stand-alone templating engine.
43
+ END
44
+
45
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'haml/version'
2
4
 
3
5
  # The module that contains everything Haml-related:
@@ -0,0 +1 @@
1
+ version.rb merge=ours
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Haml
4
+ module AttributeBuilder
5
+ # https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
6
+ INVALID_ATTRIBUTE_NAME_REGEX = /[ \0"'>\/=]/
7
+
8
+ class << self
9
+ def build_attributes(is_html, attr_wrapper, escape_attrs, hyphenate_data_attrs, attributes = {})
10
+ # @TODO this is an absolutely ridiculous amount of arguments. At least
11
+ # some of this needs to be moved into an instance method.
12
+ join_char = hyphenate_data_attrs ? '-' : '_'
13
+
14
+ attributes.each do |key, value|
15
+ if value.is_a?(Hash)
16
+ data_attributes = attributes.delete(key)
17
+ data_attributes = flatten_data_attributes(data_attributes, '', join_char)
18
+ data_attributes = build_data_keys(data_attributes, hyphenate_data_attrs, key)
19
+ verify_attribute_names!(data_attributes.keys)
20
+ attributes = data_attributes.merge(attributes)
21
+ end
22
+ end
23
+
24
+ result = attributes.collect do |attr, value|
25
+ next if value.nil?
26
+
27
+ value = filter_and_join(value, ' ') if attr == 'class'
28
+ value = filter_and_join(value, '_') if attr == 'id'
29
+
30
+ if value == true
31
+ next " #{attr}" if is_html
32
+ next " #{attr}=#{attr_wrapper}#{attr}#{attr_wrapper}"
33
+ elsif value == false
34
+ next
35
+ end
36
+
37
+ value =
38
+ if escape_attrs == :once
39
+ Haml::Helpers.escape_once_without_haml_xss(value.to_s)
40
+ elsif escape_attrs
41
+ Haml::Helpers.html_escape_without_haml_xss(value.to_s)
42
+ else
43
+ value.to_s
44
+ end
45
+ " #{attr}=#{attr_wrapper}#{value}#{attr_wrapper}"
46
+ end
47
+ result.compact!
48
+ result.sort!
49
+ result.join
50
+ end
51
+
52
+ # @return [String, nil]
53
+ def filter_and_join(value, separator)
54
+ return '' if (value.respond_to?(:empty?) && value.empty?)
55
+
56
+ if value.is_a?(Array)
57
+ value = value.flatten
58
+ value.map! {|item| item ? item.to_s : nil}
59
+ value.compact!
60
+ value = value.join(separator)
61
+ else
62
+ value = value ? value.to_s : nil
63
+ end
64
+ !value.nil? && !value.empty? && value
65
+ end
66
+
67
+ # Merges two attribute hashes.
68
+ # This is the same as `to.merge!(from)`,
69
+ # except that it merges id, class, and data attributes.
70
+ #
71
+ # ids are concatenated with `"_"`,
72
+ # and classes are concatenated with `" "`.
73
+ # data hashes are simply merged.
74
+ #
75
+ # Destructively modifies `to`.
76
+ #
77
+ # @param to [{String => String,Hash}] The attribute hash to merge into
78
+ # @param from [{String => Object}] The attribute hash to merge from
79
+ # @return [{String => String,Hash}] `to`, after being merged
80
+ def merge_attributes!(to, from)
81
+ from.keys.each do |key|
82
+ to[key] = merge_value(key, to[key], from[key])
83
+ end
84
+ to
85
+ end
86
+
87
+ # Merge multiple values to one attribute value. No destructive operation.
88
+ #
89
+ # @param key [String]
90
+ # @param values [Array<Object>]
91
+ # @return [String,Hash]
92
+ def merge_values(key, *values)
93
+ values.inject(nil) do |to, from|
94
+ merge_value(key, to, from)
95
+ end
96
+ end
97
+
98
+ def verify_attribute_names!(attribute_names)
99
+ attribute_names.each do |attribute_name|
100
+ if attribute_name =~ INVALID_ATTRIBUTE_NAME_REGEX
101
+ raise InvalidAttributeNameError.new("Invalid attribute name '#{attribute_name}' was rendered")
102
+ end
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ # Merge a couple of values to one attribute value. No destructive operation.
109
+ #
110
+ # @param to [String,Hash,nil]
111
+ # @param from [Object]
112
+ # @return [String,Hash]
113
+ def merge_value(key, to, from)
114
+ if from.kind_of?(Hash) || to.kind_of?(Hash)
115
+ from = { nil => from } if !from.is_a?(Hash)
116
+ to = { nil => to } if !to.is_a?(Hash)
117
+ to.merge(from)
118
+ elsif key == 'id'
119
+ merged_id = filter_and_join(from, '_')
120
+ if to && merged_id
121
+ merged_id = "#{to}_#{merged_id}"
122
+ elsif to || merged_id
123
+ merged_id ||= to
124
+ end
125
+ merged_id
126
+ elsif key == 'class'
127
+ merged_class = filter_and_join(from, ' ')
128
+ if to && merged_class
129
+ merged_class = (to.split(' ') | merged_class.split(' ')).join(' ')
130
+ elsif to || merged_class
131
+ merged_class ||= to
132
+ end
133
+ merged_class
134
+ else
135
+ from
136
+ end
137
+ end
138
+
139
+ def build_data_keys(data_hash, hyphenate, attr_name="data")
140
+ Hash[data_hash.map do |name, value|
141
+ if name == nil
142
+ [attr_name, value]
143
+ elsif hyphenate
144
+ ["#{attr_name}-#{name.to_s.tr('_', '-')}", value]
145
+ else
146
+ ["#{attr_name}-#{name}", value]
147
+ end
148
+ end]
149
+ end
150
+
151
+ def flatten_data_attributes(data, key, join_char, seen = [])
152
+ return {key => data} unless data.is_a?(Hash)
153
+
154
+ return {key => nil} if seen.include? data.object_id
155
+ seen << data.object_id
156
+
157
+ data.sort {|x, y| x[0].to_s <=> y[0].to_s}.inject({}) do |hash, (k, v)|
158
+ joined = key == '' ? k : [key, k].join(join_char)
159
+ hash.merge! flatten_data_attributes(v, joined, join_char, seen)
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'haml/attribute_parser'
4
+
5
+ module Haml
6
+ class AttributeCompiler
7
+ # @param type [Symbol] :static or :dynamic
8
+ # @param key [String]
9
+ # @param value [String] Actual string value for :static type, value's Ruby literal for :dynamic type.
10
+ AttributeValue = Struct.new(:type, :key, :value)
11
+
12
+ # @param options [Haml::Options]
13
+ def initialize(options)
14
+ @is_html = [:html4, :html5].include?(options[:format])
15
+ @attr_wrapper = options[:attr_wrapper]
16
+ @escape_attrs = options[:escape_attrs]
17
+ @hyphenate_data_attrs = options[:hyphenate_data_attrs]
18
+ end
19
+
20
+ # Returns Temple expression to render attributes.
21
+ #
22
+ # @param attributes [Hash]
23
+ # @param object_ref [String,:nil]
24
+ # @param dynamic_attributes [Haml::Parser::DynamicAttributes]
25
+ # @return [Array] Temple expression
26
+ def compile(attributes, object_ref, dynamic_attributes)
27
+ if object_ref != :nil || !AttributeParser.available?
28
+ return [:dynamic, compile_runtime_build(attributes, object_ref, dynamic_attributes)]
29
+ end
30
+
31
+ parsed_hashes = [dynamic_attributes.new, dynamic_attributes.old].compact.map do |attribute_hash|
32
+ unless (hash = AttributeParser.parse(attribute_hash))
33
+ return [:dynamic, compile_runtime_build(attributes, object_ref, dynamic_attributes)]
34
+ end
35
+ hash
36
+ end
37
+ attribute_values = build_attribute_values(attributes, parsed_hashes)
38
+ AttributeBuilder.verify_attribute_names!(attribute_values.map(&:key))
39
+
40
+ [:multi, *group_values_for_sort(attribute_values).map { |value_group|
41
+ compile_attribute_values(value_group)
42
+ }]
43
+ end
44
+
45
+ private
46
+
47
+ # Returns a script to render attributes on runtime.
48
+ #
49
+ # @param attributes [Hash]
50
+ # @param object_ref [String,:nil]
51
+ # @param dynamic_attributes [Haml::Parser::DynamicAttributes]
52
+ # @return [String] Attributes rendering code
53
+ def compile_runtime_build(attributes, object_ref, dynamic_attributes)
54
+ "_hamlout.attributes(#{to_literal(attributes)}, #{object_ref}, #{dynamic_attributes.to_literal})"
55
+ end
56
+
57
+ # Build array of grouped values whose sort order may go back and forth, which is also sorted with key name.
58
+ # This method needs to group values with the same start because it can be changed in `Haml::AttributeBuidler#build_data_keys`.
59
+ # @param values [Array<Haml::AttributeCompiler::AttributeValue>]
60
+ # @return [Array<Array<Haml::AttributeCompiler::AttributeValue>>]
61
+ def group_values_for_sort(values)
62
+ sorted_values = values.sort_by(&:key)
63
+ [].tap do |value_groups|
64
+ until sorted_values.empty?
65
+ key = sorted_values.first.key
66
+ value_group, sorted_values = sorted_values.partition { |v| v.key.start_with?(key) }
67
+ value_groups << value_group
68
+ end
69
+ end
70
+ end
71
+
72
+ # Returns array of AttributeValue instances from static attributes and dynamic_attributes. 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 similar key to Temple expression.
92
+ #
93
+ # @param values [Array<AttributeValue>] whose `key`s are partially or fully the same from left.
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(&method(:to_literal)).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
+ attr_literal(values.first)
133
+ else
134
+ "::Haml::AttributeBuilder.merge_values(#{frozen_string(key)}, #{values.map(&method(:attr_literal)).join(', ')})"
135
+ end
136
+ end
137
+
138
+ # @param str [String]
139
+ # @return [String]
140
+ def frozen_string(str)
141
+ "#{to_literal(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?(attr_literal(v)) }
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, Escapable::EscapeSafeBuffer.new(@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, Escapable::EscapeSafeBuffer.new(@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
+
214
+ # @param [Haml::AttributeCompiler::AttributeValue] attr
215
+ def attr_literal(attr)
216
+ case attr.type
217
+ when :static
218
+ to_literal(attr.value)
219
+ when :dynamic
220
+ attr.value
221
+ end
222
+ end
223
+
224
+ # For haml/haml#972
225
+ # @param [Object] value
226
+ def to_literal(value)
227
+ case value
228
+ when true, false
229
+ value.to_s
230
+ else
231
+ Haml::Util.inspect_obj(value)
232
+ end
233
+ end
234
+ end
235
+ end