haml 4.0.7 → 5.2.2
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 +5 -5
- data/.github/workflows/test.yml +36 -0
- data/.gitignore +19 -0
- data/.gitmodules +3 -0
- data/.yardopts +2 -3
- data/CHANGELOG.md +146 -4
- data/FAQ.md +4 -14
- data/Gemfile +16 -0
- data/MIT-LICENSE +2 -2
- data/README.md +90 -47
- data/REFERENCE.md +160 -74
- data/Rakefile +44 -63
- data/TODO +24 -0
- data/benchmark.rb +70 -0
- data/haml.gemspec +45 -0
- data/lib/haml/.gitattributes +1 -0
- data/lib/haml/attribute_builder.rb +219 -0
- data/lib/haml/attribute_compiler.rb +237 -0
- data/lib/haml/attribute_parser.rb +150 -0
- data/lib/haml/buffer.rb +12 -175
- data/lib/haml/compiler.rb +110 -320
- data/lib/haml/engine.rb +34 -41
- data/lib/haml/error.rb +28 -24
- data/lib/haml/escapable.rb +77 -0
- data/lib/haml/exec.rb +38 -20
- data/lib/haml/filters.rb +22 -27
- data/lib/haml/generator.rb +42 -0
- data/lib/haml/helpers/action_view_extensions.rb +4 -2
- data/lib/haml/helpers/action_view_mods.rb +45 -60
- data/lib/haml/helpers/action_view_xss_mods.rb +2 -0
- data/lib/haml/helpers/safe_erubi_template.rb +20 -0
- data/lib/haml/helpers/safe_erubis_template.rb +5 -1
- data/lib/haml/helpers/xss_mods.rb +23 -13
- data/lib/haml/helpers.rb +134 -89
- data/lib/haml/options.rb +63 -69
- data/lib/haml/parser.rb +319 -227
- data/lib/haml/plugin.rb +54 -0
- data/lib/haml/railtie.rb +43 -12
- data/lib/haml/sass_rails_filter.rb +18 -4
- data/lib/haml/template/options.rb +13 -2
- data/lib/haml/template.rb +13 -6
- data/lib/haml/temple_engine.rb +124 -0
- data/lib/haml/temple_line_counter.rb +30 -0
- data/lib/haml/util.rb +83 -202
- data/lib/haml/version.rb +3 -1
- data/lib/haml.rb +2 -0
- data/yard/default/.gitignore +1 -0
- data/yard/default/fulldoc/html/css/common.sass +15 -0
- data/yard/default/layout/html/footer.erb +12 -0
- metadata +73 -115
- data/lib/haml/template/plugin.rb +0 -41
- data/test/engine_test.rb +0 -2013
- data/test/erb/_av_partial_1.erb +0 -12
- data/test/erb/_av_partial_2.erb +0 -8
- data/test/erb/action_view.erb +0 -62
- data/test/erb/standard.erb +0 -55
- data/test/filters_test.rb +0 -254
- data/test/gemfiles/Gemfile.rails-3.0.x +0 -5
- data/test/gemfiles/Gemfile.rails-3.1.x +0 -6
- data/test/gemfiles/Gemfile.rails-3.2.x +0 -5
- data/test/gemfiles/Gemfile.rails-4.0.x +0 -5
- data/test/haml-spec/LICENSE +0 -14
- data/test/haml-spec/README.md +0 -106
- data/test/haml-spec/lua_haml_spec.lua +0 -38
- data/test/haml-spec/perl_haml_test.pl +0 -81
- data/test/haml-spec/ruby_haml_test.rb +0 -23
- data/test/haml-spec/tests.json +0 -660
- data/test/helper_test.rb +0 -583
- data/test/markaby/standard.mab +0 -52
- data/test/mocks/article.rb +0 -6
- data/test/parser_test.rb +0 -105
- data/test/results/content_for_layout.xhtml +0 -12
- data/test/results/eval_suppressed.xhtml +0 -9
- data/test/results/helpers.xhtml +0 -70
- data/test/results/helpful.xhtml +0 -10
- data/test/results/just_stuff.xhtml +0 -70
- data/test/results/list.xhtml +0 -12
- data/test/results/nuke_inner_whitespace.xhtml +0 -40
- data/test/results/nuke_outer_whitespace.xhtml +0 -148
- data/test/results/original_engine.xhtml +0 -20
- data/test/results/partial_layout.xhtml +0 -5
- data/test/results/partial_layout_erb.xhtml +0 -5
- data/test/results/partials.xhtml +0 -21
- data/test/results/render_layout.xhtml +0 -3
- data/test/results/silent_script.xhtml +0 -74
- data/test/results/standard.xhtml +0 -162
- data/test/results/tag_parsing.xhtml +0 -23
- data/test/results/very_basic.xhtml +0 -5
- data/test/results/whitespace_handling.xhtml +0 -90
- data/test/template_test.rb +0 -354
- data/test/templates/_av_partial_1.haml +0 -9
- data/test/templates/_av_partial_1_ugly.haml +0 -9
- data/test/templates/_av_partial_2.haml +0 -5
- data/test/templates/_av_partial_2_ugly.haml +0 -5
- data/test/templates/_layout.erb +0 -3
- data/test/templates/_layout_for_partial.haml +0 -3
- data/test/templates/_partial.haml +0 -8
- data/test/templates/_text_area.haml +0 -3
- data/test/templates/_text_area_helper.html.haml +0 -4
- data/test/templates/action_view.haml +0 -47
- data/test/templates/action_view_ugly.haml +0 -47
- data/test/templates/breakage.haml +0 -8
- data/test/templates/content_for_layout.haml +0 -8
- data/test/templates/eval_suppressed.haml +0 -11
- data/test/templates/helpers.haml +0 -55
- data/test/templates/helpful.haml +0 -11
- data/test/templates/just_stuff.haml +0 -85
- data/test/templates/list.haml +0 -12
- data/test/templates/nuke_inner_whitespace.haml +0 -32
- data/test/templates/nuke_outer_whitespace.haml +0 -144
- data/test/templates/original_engine.haml +0 -17
- data/test/templates/partial_layout.haml +0 -3
- data/test/templates/partial_layout_erb.erb +0 -4
- data/test/templates/partialize.haml +0 -1
- data/test/templates/partials.haml +0 -12
- data/test/templates/render_layout.haml +0 -2
- data/test/templates/silent_script.haml +0 -45
- data/test/templates/standard.haml +0 -43
- data/test/templates/standard_ugly.haml +0 -43
- data/test/templates/tag_parsing.haml +0 -21
- data/test/templates/very_basic.haml +0 -4
- data/test/templates/whitespace_handling.haml +0 -87
- data/test/test_helper.rb +0 -81
- data/test/util_test.rb +0 -63
data/haml.gemspec
ADDED
|
@@ -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'
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
version.rb merge=ours
|
|
@@ -0,0 +1,219 @@
|
|
|
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(class_id, obj_ref, is_html, attr_wrapper, escape_attrs, hyphenate_data_attrs, *attributes_hashes)
|
|
10
|
+
attributes = class_id
|
|
11
|
+
attributes_hashes.each do |old|
|
|
12
|
+
result = {}
|
|
13
|
+
old.each { |k, v| result[k.to_s] = v }
|
|
14
|
+
merge_attributes!(attributes, result)
|
|
15
|
+
end
|
|
16
|
+
merge_attributes!(attributes, parse_object_ref(obj_ref)) if obj_ref
|
|
17
|
+
build_attributes(is_html, attr_wrapper, escape_attrs, hyphenate_data_attrs, attributes)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def build_attributes(is_html, attr_wrapper, escape_attrs, hyphenate_data_attrs, attributes = {})
|
|
21
|
+
# @TODO this is an absolutely ridiculous amount of arguments. At least
|
|
22
|
+
# some of this needs to be moved into an instance method.
|
|
23
|
+
join_char = hyphenate_data_attrs ? '-' : '_'
|
|
24
|
+
|
|
25
|
+
attributes.each do |key, value|
|
|
26
|
+
if value.is_a?(Hash)
|
|
27
|
+
data_attributes = attributes.delete(key)
|
|
28
|
+
data_attributes = flatten_data_attributes(data_attributes, '', join_char)
|
|
29
|
+
data_attributes = build_data_keys(data_attributes, hyphenate_data_attrs, key)
|
|
30
|
+
verify_attribute_names!(data_attributes.keys)
|
|
31
|
+
attributes = data_attributes.merge(attributes)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
result = attributes.collect do |attr, value|
|
|
36
|
+
next if value.nil?
|
|
37
|
+
|
|
38
|
+
value = filter_and_join(value, ' ') if attr == 'class'
|
|
39
|
+
value = filter_and_join(value, '_') if attr == 'id'
|
|
40
|
+
|
|
41
|
+
if value == true
|
|
42
|
+
next " #{attr}" if is_html
|
|
43
|
+
next " #{attr}=#{attr_wrapper}#{attr}#{attr_wrapper}"
|
|
44
|
+
elsif value == false
|
|
45
|
+
next
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
value =
|
|
49
|
+
if escape_attrs == :once
|
|
50
|
+
Haml::Helpers.escape_once_without_haml_xss(value.to_s)
|
|
51
|
+
elsif escape_attrs
|
|
52
|
+
Haml::Helpers.html_escape_without_haml_xss(value.to_s)
|
|
53
|
+
else
|
|
54
|
+
value.to_s
|
|
55
|
+
end
|
|
56
|
+
" #{attr}=#{attr_wrapper}#{value}#{attr_wrapper}"
|
|
57
|
+
end
|
|
58
|
+
result.compact!
|
|
59
|
+
result.sort!
|
|
60
|
+
result.join
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @return [String, nil]
|
|
64
|
+
def filter_and_join(value, separator)
|
|
65
|
+
return '' if (value.respond_to?(:empty?) && value.empty?)
|
|
66
|
+
|
|
67
|
+
if value.is_a?(Array)
|
|
68
|
+
value = value.flatten
|
|
69
|
+
value.map! {|item| item ? item.to_s : nil}
|
|
70
|
+
value.compact!
|
|
71
|
+
value = value.join(separator)
|
|
72
|
+
else
|
|
73
|
+
value = value ? value.to_s : nil
|
|
74
|
+
end
|
|
75
|
+
!value.nil? && !value.empty? && value
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Merges two attribute hashes.
|
|
79
|
+
# This is the same as `to.merge!(from)`,
|
|
80
|
+
# except that it merges id, class, and data attributes.
|
|
81
|
+
#
|
|
82
|
+
# ids are concatenated with `"_"`,
|
|
83
|
+
# and classes are concatenated with `" "`.
|
|
84
|
+
# data hashes are simply merged.
|
|
85
|
+
#
|
|
86
|
+
# Destructively modifies `to`.
|
|
87
|
+
#
|
|
88
|
+
# @param to [{String => String,Hash}] The attribute hash to merge into
|
|
89
|
+
# @param from [{String => Object}] The attribute hash to merge from
|
|
90
|
+
# @return [{String => String,Hash}] `to`, after being merged
|
|
91
|
+
def merge_attributes!(to, from)
|
|
92
|
+
from.keys.each do |key|
|
|
93
|
+
to[key] = merge_value(key, to[key], from[key])
|
|
94
|
+
end
|
|
95
|
+
to
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Merge multiple values to one attribute value. No destructive operation.
|
|
99
|
+
#
|
|
100
|
+
# @param key [String]
|
|
101
|
+
# @param values [Array<Object>]
|
|
102
|
+
# @return [String,Hash]
|
|
103
|
+
def merge_values(key, *values)
|
|
104
|
+
values.inject(nil) do |to, from|
|
|
105
|
+
merge_value(key, to, from)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def verify_attribute_names!(attribute_names)
|
|
110
|
+
attribute_names.each do |attribute_name|
|
|
111
|
+
if attribute_name =~ INVALID_ATTRIBUTE_NAME_REGEX
|
|
112
|
+
raise InvalidAttributeNameError.new("Invalid attribute name '#{attribute_name}' was rendered")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
# Merge a couple of values to one attribute value. No destructive operation.
|
|
120
|
+
#
|
|
121
|
+
# @param to [String,Hash,nil]
|
|
122
|
+
# @param from [Object]
|
|
123
|
+
# @return [String,Hash]
|
|
124
|
+
def merge_value(key, to, from)
|
|
125
|
+
if from.kind_of?(Hash) || to.kind_of?(Hash)
|
|
126
|
+
from = { nil => from } if !from.is_a?(Hash)
|
|
127
|
+
to = { nil => to } if !to.is_a?(Hash)
|
|
128
|
+
to.merge(from)
|
|
129
|
+
elsif key == 'id'
|
|
130
|
+
merged_id = filter_and_join(from, '_')
|
|
131
|
+
if to && merged_id
|
|
132
|
+
merged_id = "#{to}_#{merged_id}"
|
|
133
|
+
elsif to || merged_id
|
|
134
|
+
merged_id ||= to
|
|
135
|
+
end
|
|
136
|
+
merged_id
|
|
137
|
+
elsif key == 'class'
|
|
138
|
+
merged_class = filter_and_join(from, ' ')
|
|
139
|
+
if to && merged_class
|
|
140
|
+
merged_class = (to.split(' ') | merged_class.split(' ')).join(' ')
|
|
141
|
+
elsif to || merged_class
|
|
142
|
+
merged_class ||= to
|
|
143
|
+
end
|
|
144
|
+
merged_class
|
|
145
|
+
else
|
|
146
|
+
from
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def build_data_keys(data_hash, hyphenate, attr_name="data")
|
|
151
|
+
Hash[data_hash.map do |name, value|
|
|
152
|
+
if name == nil
|
|
153
|
+
[attr_name, value]
|
|
154
|
+
elsif hyphenate
|
|
155
|
+
["#{attr_name}-#{name.to_s.tr('_', '-')}", value]
|
|
156
|
+
else
|
|
157
|
+
["#{attr_name}-#{name}", value]
|
|
158
|
+
end
|
|
159
|
+
end]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def flatten_data_attributes(data, key, join_char, seen = [])
|
|
163
|
+
return {key => data} unless data.is_a?(Hash)
|
|
164
|
+
|
|
165
|
+
return {key => nil} if seen.include? data.object_id
|
|
166
|
+
seen << data.object_id
|
|
167
|
+
|
|
168
|
+
data.sort {|x, y| x[0].to_s <=> y[0].to_s}.inject({}) do |hash, (k, v)|
|
|
169
|
+
joined = key == '' ? k : [key, k].join(join_char)
|
|
170
|
+
hash.merge! flatten_data_attributes(v, joined, join_char, seen)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Takes an array of objects and uses the class and id of the first
|
|
175
|
+
# one to create an attributes hash.
|
|
176
|
+
# The second object, if present, is used as a prefix,
|
|
177
|
+
# just like you can do with `dom_id()` and `dom_class()` in Rails
|
|
178
|
+
def parse_object_ref(ref)
|
|
179
|
+
prefix = ref[1]
|
|
180
|
+
ref = ref[0]
|
|
181
|
+
# Let's make sure the value isn't nil. If it is, return the default Hash.
|
|
182
|
+
return {} if ref.nil?
|
|
183
|
+
class_name =
|
|
184
|
+
if ref.respond_to?(:haml_object_ref)
|
|
185
|
+
ref.haml_object_ref
|
|
186
|
+
else
|
|
187
|
+
underscore(ref.class)
|
|
188
|
+
end
|
|
189
|
+
ref_id =
|
|
190
|
+
if ref.respond_to?(:to_key)
|
|
191
|
+
key = ref.to_key
|
|
192
|
+
key.join('_') unless key.nil?
|
|
193
|
+
else
|
|
194
|
+
ref.id
|
|
195
|
+
end
|
|
196
|
+
id = "#{class_name}_#{ref_id || 'new'}"
|
|
197
|
+
if prefix
|
|
198
|
+
class_name = "#{ prefix }_#{ class_name}"
|
|
199
|
+
id = "#{ prefix }_#{ id }"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
{ 'id'.freeze => id, 'class'.freeze => class_name }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Changes a word from camel case to underscores.
|
|
206
|
+
# Based on the method of the same name in Rails' Inflector,
|
|
207
|
+
# but copied here so it'll run properly without Rails.
|
|
208
|
+
def underscore(camel_cased_word)
|
|
209
|
+
word = camel_cased_word.to_s.dup
|
|
210
|
+
word.gsub!(/::/, '_')
|
|
211
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
212
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
|
213
|
+
word.tr!('-', '_')
|
|
214
|
+
word.downcase!
|
|
215
|
+
word
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
arguments = [@is_html, @attr_wrapper, @escape_attrs, @hyphenate_data_attrs].map(&method(:to_literal)).join(', ')
|
|
55
|
+
"::Haml::AttributeBuilder.build(#{to_literal(attributes)}, #{object_ref}, #{arguments}, #{dynamic_attributes.to_literal})"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Build array of grouped values whose sort order may go back and forth, which is also sorted with key name.
|
|
59
|
+
# This method needs to group values with the same start because it can be changed in `Haml::AttributeBuidler#build_data_keys`.
|
|
60
|
+
# @param values [Array<Haml::AttributeCompiler::AttributeValue>]
|
|
61
|
+
# @return [Array<Array<Haml::AttributeCompiler::AttributeValue>>]
|
|
62
|
+
def group_values_for_sort(values)
|
|
63
|
+
sorted_values = values.sort_by(&:key)
|
|
64
|
+
[].tap do |value_groups|
|
|
65
|
+
until sorted_values.empty?
|
|
66
|
+
key = sorted_values.first.key
|
|
67
|
+
value_group, sorted_values = sorted_values.partition { |v| v.key.start_with?(key) }
|
|
68
|
+
value_groups << value_group
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns array of AttributeValue instances from static attributes and dynamic_attributes. For each key,
|
|
74
|
+
# the values' order in returned value is preserved in the same order as Haml::Buffer#attributes's merge order.
|
|
75
|
+
#
|
|
76
|
+
# @param attributes [{ String => String }]
|
|
77
|
+
# @param parsed_hashes [{ String => String }]
|
|
78
|
+
# @return [Array<AttributeValue>]
|
|
79
|
+
def build_attribute_values(attributes, parsed_hashes)
|
|
80
|
+
[].tap do |attribute_values|
|
|
81
|
+
attributes.each do |key, static_value|
|
|
82
|
+
attribute_values << AttributeValue.new(:static, key, static_value)
|
|
83
|
+
end
|
|
84
|
+
parsed_hashes.each do |parsed_hash|
|
|
85
|
+
parsed_hash.each do |key, dynamic_value|
|
|
86
|
+
attribute_values << AttributeValue.new(:dynamic, key, dynamic_value)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Compiles attribute values with the similar key to Temple expression.
|
|
93
|
+
#
|
|
94
|
+
# @param values [Array<AttributeValue>] whose `key`s are partially or fully the same from left.
|
|
95
|
+
# @return [Array] Temple expression
|
|
96
|
+
def compile_attribute_values(values)
|
|
97
|
+
if values.map(&:key).uniq.size == 1
|
|
98
|
+
compile_attribute(values.first.key, values)
|
|
99
|
+
else
|
|
100
|
+
runtime_build(values)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @param values [Array<AttributeValue>]
|
|
105
|
+
# @return [Array] Temple expression
|
|
106
|
+
def runtime_build(values)
|
|
107
|
+
hash_content = values.group_by(&:key).map do |key, values_for_key|
|
|
108
|
+
"#{frozen_string(key)} => #{merged_value(key, values_for_key)}"
|
|
109
|
+
end.join(', ')
|
|
110
|
+
arguments = [@is_html, @attr_wrapper, @escape_attrs, @hyphenate_data_attrs].map(&method(:to_literal)).join(', ')
|
|
111
|
+
[:dynamic, "::Haml::AttributeBuilder.build({ #{hash_content} }, nil, #{arguments})"]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Renders attribute values statically.
|
|
115
|
+
#
|
|
116
|
+
# @param values [Array<AttributeValue>]
|
|
117
|
+
# @return [Array] Temple expression
|
|
118
|
+
def static_build(values)
|
|
119
|
+
hash_content = values.group_by(&:key).map do |key, values_for_key|
|
|
120
|
+
"#{frozen_string(key)} => #{merged_value(key, values_for_key)}"
|
|
121
|
+
end.join(', ')
|
|
122
|
+
|
|
123
|
+
arguments = [@is_html, @attr_wrapper, @escape_attrs, @hyphenate_data_attrs]
|
|
124
|
+
code = "::Haml::AttributeBuilder.build_attributes"\
|
|
125
|
+
"(#{arguments.map(&method(:to_literal)).join(', ')}, { #{hash_content} })"
|
|
126
|
+
[:static, eval(code).to_s]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @param key [String]
|
|
130
|
+
# @param values [Array<AttributeValue>]
|
|
131
|
+
# @return [String]
|
|
132
|
+
def merged_value(key, values)
|
|
133
|
+
if values.size == 1
|
|
134
|
+
attr_literal(values.first)
|
|
135
|
+
else
|
|
136
|
+
"::Haml::AttributeBuilder.merge_values(#{frozen_string(key)}, #{values.map(&method(:attr_literal)).join(', ')})"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @param str [String]
|
|
141
|
+
# @return [String]
|
|
142
|
+
def frozen_string(str)
|
|
143
|
+
"#{to_literal(str)}.freeze"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Compiles attribute values for one key to Temple expression that generates ` key='value'`.
|
|
147
|
+
#
|
|
148
|
+
# @param key [String]
|
|
149
|
+
# @param values [Array<AttributeValue>]
|
|
150
|
+
# @return [Array] Temple expression
|
|
151
|
+
def compile_attribute(key, values)
|
|
152
|
+
if values.all? { |v| Temple::StaticAnalyzer.static?(attr_literal(v)) }
|
|
153
|
+
return static_build(values)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
case key
|
|
157
|
+
when 'id', 'class'
|
|
158
|
+
compile_id_or_class_attribute(key, values)
|
|
159
|
+
else
|
|
160
|
+
compile_common_attribute(key, values)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# @param id_or_class [String] "id" or "class"
|
|
165
|
+
# @param values [Array<AttributeValue>]
|
|
166
|
+
# @return [Array] Temple expression
|
|
167
|
+
def compile_id_or_class_attribute(id_or_class, values)
|
|
168
|
+
var = unique_name
|
|
169
|
+
[:multi,
|
|
170
|
+
[:code, "#{var} = (#{merged_value(id_or_class, values)})"],
|
|
171
|
+
[:case, var,
|
|
172
|
+
['Hash, Array', runtime_build([AttributeValue.new(:dynamic, id_or_class, var)])],
|
|
173
|
+
['false, nil', [:multi]],
|
|
174
|
+
[:else, [:multi,
|
|
175
|
+
[:static, " #{id_or_class}=#{@attr_wrapper}"],
|
|
176
|
+
[:escape, Escapable::EscapeSafeBuffer.new(@escape_attrs), [:dynamic, var]],
|
|
177
|
+
[:static, @attr_wrapper]],
|
|
178
|
+
]
|
|
179
|
+
],
|
|
180
|
+
]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# @param key [String] Not "id" or "class"
|
|
184
|
+
# @param values [Array<AttributeValue>]
|
|
185
|
+
# @return [Array] Temple expression
|
|
186
|
+
def compile_common_attribute(key, values)
|
|
187
|
+
var = unique_name
|
|
188
|
+
[:multi,
|
|
189
|
+
[:code, "#{var} = (#{merged_value(key, values)})"],
|
|
190
|
+
[:case, var,
|
|
191
|
+
['Hash', runtime_build([AttributeValue.new(:dynamic, key, var)])],
|
|
192
|
+
['true', true_value(key)],
|
|
193
|
+
['false, nil', [:multi]],
|
|
194
|
+
[:else, [:multi,
|
|
195
|
+
[:static, " #{key}=#{@attr_wrapper}"],
|
|
196
|
+
[:escape, Escapable::EscapeSafeBuffer.new(@escape_attrs), [:dynamic, var]],
|
|
197
|
+
[:static, @attr_wrapper]],
|
|
198
|
+
]
|
|
199
|
+
],
|
|
200
|
+
]
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def true_value(key)
|
|
204
|
+
if @is_html
|
|
205
|
+
[:static, " #{key}"]
|
|
206
|
+
else
|
|
207
|
+
[:static, " #{key}=#{@attr_wrapper}#{key}#{@attr_wrapper}"]
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def unique_name
|
|
212
|
+
@unique_name ||= 0
|
|
213
|
+
"_haml_attribute_compiler#{@unique_name += 1}"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# @param [Haml::AttributeCompiler::AttributeValue] attr
|
|
217
|
+
def attr_literal(attr)
|
|
218
|
+
case attr.type
|
|
219
|
+
when :static
|
|
220
|
+
to_literal(attr.value)
|
|
221
|
+
when :dynamic
|
|
222
|
+
attr.value
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# For haml/haml#972
|
|
227
|
+
# @param [Object] value
|
|
228
|
+
def to_literal(value)
|
|
229
|
+
case value
|
|
230
|
+
when true, false
|
|
231
|
+
value.to_s
|
|
232
|
+
else
|
|
233
|
+
Haml::Util.inspect_obj(value)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require 'ripper'
|
|
5
|
+
rescue LoadError
|
|
6
|
+
end
|
|
7
|
+
require 'temple/static_analyzer'
|
|
8
|
+
|
|
9
|
+
module Haml
|
|
10
|
+
# Haml::AttriubuteParser parses Hash literal to { String (key name) => String (value literal) }.
|
|
11
|
+
module AttributeParser
|
|
12
|
+
class UnexpectedTokenError < StandardError; end
|
|
13
|
+
class UnexpectedKeyError < StandardError; end
|
|
14
|
+
|
|
15
|
+
# Indices in Ripper tokens
|
|
16
|
+
TYPE = 1
|
|
17
|
+
TEXT = 2
|
|
18
|
+
|
|
19
|
+
IGNORED_TYPES = %i[on_sp on_ignored_nl].freeze
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
# @return [Boolean] - return true if AttributeParser.parse can be used.
|
|
23
|
+
def available?
|
|
24
|
+
defined?(Ripper) && Temple::StaticAnalyzer.available?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param [String] exp - Old attributes literal or Hash literal generated from new attributes.
|
|
28
|
+
# @return [Hash<String, String>,nil] - Return parsed attribute Hash whose values are Ruby literals, or return nil if argument is not a single Hash literal.
|
|
29
|
+
def parse(exp)
|
|
30
|
+
return nil unless hash_literal?(exp)
|
|
31
|
+
|
|
32
|
+
hash = {}
|
|
33
|
+
each_attribute(exp) do |key, value|
|
|
34
|
+
hash[key] = value
|
|
35
|
+
end
|
|
36
|
+
hash
|
|
37
|
+
rescue UnexpectedTokenError, UnexpectedKeyError
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# @param [String] exp - Ruby expression
|
|
44
|
+
# @return [Boolean] - Return true if exp is a single Hash literal
|
|
45
|
+
def hash_literal?(exp)
|
|
46
|
+
return false if Temple::StaticAnalyzer.syntax_error?(exp)
|
|
47
|
+
sym, body = Ripper.sexp(exp)
|
|
48
|
+
sym == :program && body.is_a?(Array) && body.size == 1 && body[0] && body[0][0] == :hash
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @param [Array] tokens - Ripper tokens. Scanned tokens will be destructively removed from this argument.
|
|
52
|
+
# @return [String] - attribute name in String
|
|
53
|
+
def shift_key!(tokens)
|
|
54
|
+
while !tokens.empty? && IGNORED_TYPES.include?(tokens.first[TYPE])
|
|
55
|
+
tokens.shift # ignore spaces
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
_, type, first_text = tokens.shift
|
|
59
|
+
case type
|
|
60
|
+
when :on_label # `key:`
|
|
61
|
+
first_text.tr(':', '')
|
|
62
|
+
when :on_symbeg # `:key =>`, `:'key' =>` or `:"key" =>`
|
|
63
|
+
key = tokens.shift[TEXT]
|
|
64
|
+
if first_text != ':' # `:'key'` or `:"key"`
|
|
65
|
+
expect_string_end!(tokens.shift)
|
|
66
|
+
end
|
|
67
|
+
shift_hash_rocket!(tokens)
|
|
68
|
+
key
|
|
69
|
+
when :on_tstring_beg # `"key":`, `'key':` or `"key" =>`
|
|
70
|
+
key = tokens.shift[TEXT]
|
|
71
|
+
next_token = tokens.shift
|
|
72
|
+
if next_token[TYPE] != :on_label_end # on_label_end is `":` or `':`, so `"key" =>`
|
|
73
|
+
expect_string_end!(next_token)
|
|
74
|
+
shift_hash_rocket!(tokens)
|
|
75
|
+
end
|
|
76
|
+
key
|
|
77
|
+
else
|
|
78
|
+
raise UnexpectedKeyError.new("unexpected token is given!: #{first_text} (#{type})")
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @param [Array] token - Ripper token
|
|
83
|
+
def expect_string_end!(token)
|
|
84
|
+
if token[TYPE] != :on_tstring_end
|
|
85
|
+
raise UnexpectedTokenError
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @param [Array] tokens - Ripper tokens
|
|
90
|
+
def shift_hash_rocket!(tokens)
|
|
91
|
+
until tokens.empty?
|
|
92
|
+
_, type, str = tokens.shift
|
|
93
|
+
break if type == :on_op && str == '=>'
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @param [String] hash_literal
|
|
98
|
+
# @param [Proc] block - that takes [String, String] as arguments
|
|
99
|
+
def each_attribute(hash_literal, &block)
|
|
100
|
+
all_tokens = Ripper.lex(hash_literal.strip)
|
|
101
|
+
all_tokens = all_tokens[1...-1] || [] # strip tokens for brackets
|
|
102
|
+
|
|
103
|
+
each_balanced_tokens(all_tokens) do |tokens|
|
|
104
|
+
key = shift_key!(tokens)
|
|
105
|
+
value = tokens.map {|t| t[2] }.join.strip
|
|
106
|
+
block.call(key, value)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @param [Array] tokens - Ripper tokens
|
|
111
|
+
# @param [Proc] block - that takes balanced Ripper tokens as arguments
|
|
112
|
+
def each_balanced_tokens(tokens, &block)
|
|
113
|
+
attr_tokens = []
|
|
114
|
+
open_tokens = Hash.new { |h, k| h[k] = 0 }
|
|
115
|
+
|
|
116
|
+
tokens.each do |token|
|
|
117
|
+
case token[TYPE]
|
|
118
|
+
when :on_comma
|
|
119
|
+
if open_tokens.values.all?(&:zero?)
|
|
120
|
+
block.call(attr_tokens)
|
|
121
|
+
attr_tokens = []
|
|
122
|
+
next
|
|
123
|
+
end
|
|
124
|
+
when :on_lbracket
|
|
125
|
+
open_tokens[:array] += 1
|
|
126
|
+
when :on_rbracket
|
|
127
|
+
open_tokens[:array] -= 1
|
|
128
|
+
when :on_lbrace
|
|
129
|
+
open_tokens[:block] += 1
|
|
130
|
+
when :on_rbrace
|
|
131
|
+
open_tokens[:block] -= 1
|
|
132
|
+
when :on_lparen
|
|
133
|
+
open_tokens[:paren] += 1
|
|
134
|
+
when :on_rparen
|
|
135
|
+
open_tokens[:paren] -= 1
|
|
136
|
+
when :on_embexpr_beg
|
|
137
|
+
open_tokens[:embexpr] += 1
|
|
138
|
+
when :on_embexpr_end
|
|
139
|
+
open_tokens[:embexpr] -= 1
|
|
140
|
+
when *IGNORED_TYPES
|
|
141
|
+
next if attr_tokens.empty?
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
attr_tokens << token
|
|
145
|
+
end
|
|
146
|
+
block.call(attr_tokens) unless attr_tokens.empty?
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|