hamlit 2.11.0 → 2.13.0

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +17 -11
  4. data/CHANGELOG.md +29 -1
  5. data/Gemfile +1 -7
  6. data/LICENSE.txt +26 -23
  7. data/REFERENCE.md +4 -4
  8. data/bin/update-haml +125 -0
  9. data/ext/hamlit/hamlit.c +0 -1
  10. data/lib/hamlit/attribute_builder.rb +2 -2
  11. data/lib/hamlit/attribute_compiler.rb +3 -3
  12. data/lib/hamlit/compiler/children_compiler.rb +18 -4
  13. data/lib/hamlit/compiler/comment_compiler.rb +1 -0
  14. data/lib/hamlit/filters/escaped.rb +1 -1
  15. data/lib/hamlit/filters/markdown.rb +1 -0
  16. data/lib/hamlit/filters/preserve.rb +1 -1
  17. data/lib/hamlit/filters/text_base.rb +1 -1
  18. data/lib/hamlit/filters/tilt_base.rb +1 -1
  19. data/lib/hamlit/parser.rb +6 -2
  20. data/lib/hamlit/parser/haml_attribute_builder.rb +164 -0
  21. data/lib/hamlit/parser/haml_buffer.rb +20 -130
  22. data/lib/hamlit/parser/haml_compiler.rb +1 -553
  23. data/lib/hamlit/parser/haml_error.rb +29 -25
  24. data/lib/hamlit/parser/haml_escapable.rb +1 -0
  25. data/lib/hamlit/parser/haml_generator.rb +1 -0
  26. data/lib/hamlit/parser/haml_helpers.rb +41 -59
  27. data/lib/hamlit/parser/{haml_xss_mods.rb → haml_helpers/xss_mods.rb} +20 -15
  28. data/lib/hamlit/parser/haml_options.rb +53 -66
  29. data/lib/hamlit/parser/haml_parser.rb +103 -71
  30. data/lib/hamlit/parser/haml_temple_engine.rb +123 -0
  31. data/lib/hamlit/parser/haml_util.rb +10 -40
  32. data/lib/hamlit/rails_template.rb +1 -1
  33. data/lib/hamlit/string_splitter.rb +1 -0
  34. data/lib/hamlit/temple_line_counter.rb +31 -0
  35. data/lib/hamlit/version.rb +1 -1
  36. metadata +10 -6
  37. data/lib/hamlit/parser/MIT-LICENSE +0 -20
  38. data/lib/hamlit/parser/README.md +0 -30
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Hamlit
2
3
  class Filters
3
4
  class Markdown < TiltBase
@@ -12,7 +12,7 @@ module Hamlit
12
12
 
13
13
  def compile_text(text)
14
14
  if ::Hamlit::HamlUtil.contains_interpolation?(text)
15
- [:dynamic, ::Hamlit::HamlUtil.slow_unescape_interpolation(text)]
15
+ [:dynamic, ::Hamlit::HamlUtil.unescape_interpolation(text)]
16
16
  else
17
17
  [:static, text]
18
18
  end
@@ -6,7 +6,7 @@ module Hamlit
6
6
  text = node.value[:text].rstrip.gsub(/^/, prefix)
7
7
  if ::Hamlit::HamlUtil.contains_interpolation?(node.value[:text])
8
8
  # original: Haml::Filters#compile
9
- text = ::Hamlit::HamlUtil.slow_unescape_interpolation(text).gsub(/(\\+)n/) do |s|
9
+ text = ::Hamlit::HamlUtil.unescape_interpolation(text).gsub(/(\\+)n/) do |s|
10
10
  escapes = $1.size
11
11
  next s if escapes % 2 == 0
12
12
  "#{'\\' * (escapes - 1)}\n"
@@ -35,7 +35,7 @@ module Hamlit
35
35
 
36
36
  def dynamic_compile(node, name, indent_width: 0)
37
37
  # original: Haml::Filters#compile
38
- text = ::Hamlit::HamlUtil.slow_unescape_interpolation(node.value[:text]).gsub(/(\\+)n/) do |s|
38
+ text = ::Hamlit::HamlUtil.unescape_interpolation(node.value[:text]).gsub(/(\\+)n/) do |s|
39
39
  escapes = $1.size
40
40
  next s if escapes % 2 == 0
41
41
  "#{'\\' * (escapes - 1)}\n"
@@ -2,13 +2,17 @@
2
2
  # Hamlit::Parser uses original Haml::Parser to generate Haml AST.
3
3
  # hamlit/parser/haml_* are modules originally in haml gem.
4
4
 
5
+ require 'hamlit/parser/haml_attribute_builder'
5
6
  require 'hamlit/parser/haml_error'
6
7
  require 'hamlit/parser/haml_util'
8
+ require 'hamlit/parser/haml_helpers'
7
9
  require 'hamlit/parser/haml_buffer'
8
10
  require 'hamlit/parser/haml_compiler'
9
11
  require 'hamlit/parser/haml_parser'
10
- require 'hamlit/parser/haml_helpers'
11
12
  require 'hamlit/parser/haml_options'
13
+ require 'hamlit/parser/haml_escapable'
14
+ require 'hamlit/parser/haml_generator'
15
+ require 'hamlit/parser/haml_temple_engine'
12
16
 
13
17
  module Hamlit
14
18
  class Parser
@@ -29,7 +33,7 @@ module Hamlit
29
33
  template = Hamlit::HamlUtil.check_haml_encoding(template) do |msg, line|
30
34
  raise Hamlit::Error.new(msg, line)
31
35
  end
32
- HamlParser.new(template, HamlOptions.new(@options)).parse
36
+ HamlParser.new(HamlOptions.new(@options)).call(template)
33
37
  rescue ::Hamlit::HamlError => e
34
38
  error_with_lineno(e)
35
39
  end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hamlit
4
+ module HamlAttributeBuilder
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
+ Hamlit::HamlHelpers.escape_once_without_haml_xss(value.to_s)
40
+ elsif escape_attrs
41
+ Hamlit::HamlHelpers.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 HamlInvalidAttributeNameError.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
@@ -1,6 +1,4 @@
1
- require 'hamlit/parser/haml_helpers'
2
- require 'hamlit/parser/haml_util'
3
- require 'hamlit/parser/haml_compiler'
1
+ # frozen_string_literal: true
4
2
 
5
3
  module Hamlit
6
4
  # This class is used only internally. It holds the buffer of HTML that
@@ -8,12 +6,8 @@ module Hamlit
8
6
  # It's called from within the precompiled code,
9
7
  # and helps reduce the amount of processing done within `instance_eval`ed code.
10
8
  class HamlBuffer
11
- ID_KEY = 'id'.freeze
12
- CLASS_KEY = 'class'.freeze
13
- DATA_KEY = 'data'.freeze
14
-
15
- include ::Hamlit::HamlHelpers
16
- include ::Hamlit::HamlUtil
9
+ include Hamlit::HamlHelpers
10
+ include Hamlit::HamlUtil
17
11
 
18
12
  # The string that holds the compiled HTML. This is aliased as
19
13
  # `_erbout` for compatibility with ERB-specific code.
@@ -21,10 +15,10 @@ module Hamlit
21
15
  # @return [String]
22
16
  attr_accessor :buffer
23
17
 
24
- # The options hash passed in from {Haml::Engine}.
18
+ # The options hash passed in from {Hamlit::HamlEngine}.
25
19
  #
26
20
  # @return [{String => Object}]
27
- # @see Haml::Options#for_buffer
21
+ # @see Hamlit::HamlOptions#for_buffer
28
22
  attr_accessor :options
29
23
 
30
24
  # The {Buffer} for the enclosing Haml document.
@@ -94,11 +88,12 @@ module Hamlit
94
88
 
95
89
  # @param upper [Buffer] The parent buffer
96
90
  # @param options [{Symbol => Object}] An options hash.
97
- # See {Haml::Engine#options\_for\_buffer}
91
+ # See {Hamlit::HamlEngine#options\_for\_buffer}
98
92
  def initialize(upper = nil, options = {})
99
93
  @active = true
100
94
  @upper = upper
101
- @options = options
95
+ @options = HamlOptions.buffer_defaults
96
+ @options = @options.merge(options) unless options.empty?
102
97
  @buffer = new_encoded_string
103
98
  @tabulation = 0
104
99
 
@@ -135,70 +130,15 @@ module Hamlit
135
130
  @real_tabs += tab_change
136
131
  end
137
132
 
138
- # the number of arguments here is insane, but passing in an options hash instead of named arguments
139
- # causes a significant performance regression
140
- def format_script(result, preserve_script, in_tag, preserve_tag, escape_html, nuke_inner_whitespace, interpolated, ugly)
141
- result_name = escape_html ? html_escape(result.to_s) : result.to_s
142
-
143
- if ugly
144
- result = nuke_inner_whitespace ? result_name.strip : result_name
145
- result = preserve(result, preserve_script, preserve_tag)
146
- fix_textareas!(result) if toplevel? && result.include?('<textarea')
147
- return result
148
- end
149
-
150
- # If we're interpolated,
151
- # then the custom tabulation is handled in #push_text.
152
- # The easiest way to avoid it here is to reset @tabulation.
153
- if interpolated
154
- old_tabulation = @tabulation
155
- @tabulation = 0
156
- end
157
-
158
- in_tag_no_nuke = in_tag && !nuke_inner_whitespace
159
- preserved_no_nuke = in_tag_no_nuke && preserve_tag
160
- tabulation = !preserved_no_nuke && @real_tabs
161
-
162
- result = nuke_inner_whitespace ? result_name.strip : result_name.rstrip
163
- result = preserve(result, preserve_script, preserve_tag)
164
-
165
- has_newline = !preserved_no_nuke && result.include?("\n")
166
-
167
- if in_tag_no_nuke && (preserve_tag || !has_newline)
168
- @real_tabs -= 1
169
- @tabulation = old_tabulation if interpolated
170
- return result
171
- end
172
-
173
- unless preserved_no_nuke
174
- # Precompiled tabulation may be wrong
175
- result = "#{tabs}#{result}" if !interpolated && !in_tag && @tabulation > 0
176
-
177
- if has_newline
178
- result.gsub! "\n", "\n#{tabs(tabulation)}"
179
-
180
- # Add tabulation if it wasn't precompiled
181
- result = "#{tabs(tabulation)}#{result}" if in_tag_no_nuke
182
- end
183
-
184
- fix_textareas!(result) if toplevel? && result.include?('<textarea')
185
-
186
- if in_tag_no_nuke
187
- result = "\n#{result}\n#{tabs(tabulation-1)}"
188
- @real_tabs -= 1
189
- end
190
- @tabulation = old_tabulation if interpolated
191
- result
192
- end
193
- end
194
-
195
133
  def attributes(class_id, obj_ref, *attributes_hashes)
196
134
  attributes = class_id
197
135
  attributes_hashes.each do |old|
198
- self.class.merge_attrs(attributes, Hash[old.map {|k, v| [k.to_s, v]}])
136
+ result = {}
137
+ old.each { |k, v| result[k.to_s] = v }
138
+ HamlAttributeBuilder.merge_attributes!(attributes, result)
199
139
  end
200
- self.class.merge_attrs(attributes, parse_object_ref(obj_ref)) if obj_ref
201
- ::Hamlit::HamlCompiler.build_attributes(
140
+ HamlAttributeBuilder.merge_attributes!(attributes, parse_object_ref(obj_ref)) if obj_ref
141
+ HamlAttributeBuilder.build_attributes(
202
142
  html?, @options[:attr_wrapper], @options[:escape_attrs], @options[:hyphenate_data_attrs], attributes)
203
143
  end
204
144
 
@@ -213,61 +153,6 @@ module Hamlit
213
153
  buffer << buffer.slice!(capture_position..-1).rstrip
214
154
  end
215
155
 
216
- # Merges two attribute hashes.
217
- # This is the same as `to.merge!(from)`,
218
- # except that it merges id, class, and data attributes.
219
- #
220
- # ids are concatenated with `"_"`,
221
- # and classes are concatenated with `" "`.
222
- # data hashes are simply merged.
223
- #
224
- # Destructively modifies both `to` and `from`.
225
- #
226
- # @param to [{String => String}] The attribute hash to merge into
227
- # @param from [{String => #to_s}] The attribute hash to merge from
228
- # @return [{String => String}] `to`, after being merged
229
- def self.merge_attrs(to, from)
230
- from[ID_KEY] = ::Hamlit::HamlCompiler.filter_and_join(from[ID_KEY], '_') if from[ID_KEY]
231
- if to[ID_KEY] && from[ID_KEY]
232
- to[ID_KEY] << "_#{from.delete(ID_KEY)}"
233
- elsif to[ID_KEY] || from[ID_KEY]
234
- from[ID_KEY] ||= to[ID_KEY]
235
- end
236
-
237
- from[CLASS_KEY] = ::Hamlit::HamlCompiler.filter_and_join(from[CLASS_KEY], ' ') if from[CLASS_KEY]
238
- if to[CLASS_KEY] && from[CLASS_KEY]
239
- # Make sure we don't duplicate class names
240
- from[CLASS_KEY] = (from[CLASS_KEY].to_s.split(' ') | to[CLASS_KEY].split(' ')).sort.join(' ')
241
- elsif to[CLASS_KEY] || from[CLASS_KEY]
242
- from[CLASS_KEY] ||= to[CLASS_KEY]
243
- end
244
-
245
- from.keys.each do |key|
246
- next unless from[key].kind_of?(Hash) || to[key].kind_of?(Hash)
247
-
248
- from_data = from.delete(key)
249
- # forces to_data & from_data into a hash
250
- from_data = { nil => from_data } if from_data && !from_data.is_a?(Hash)
251
- to[key] = { nil => to[key] } if to[key] && !to[key].is_a?(Hash)
252
-
253
- if from_data && !to[key]
254
- to[key] = from_data
255
- elsif from_data && to[key]
256
- to[key].merge! from_data
257
- end
258
- end
259
-
260
- to.merge!(from)
261
- end
262
-
263
- private
264
-
265
- def preserve(result, preserve_script, preserve_tag)
266
- return ::Hamlit::HamlHelpers.preserve(result) if preserve_tag
267
- return ::Hamlit::HamlHelpers.find_and_preserve(result, options[:preserve]) if preserve_script
268
- result
269
- end
270
-
271
156
  # Works like #{find_and_preserve}, but allows the first newline after a
272
157
  # preserved opening tag to remain unencoded, and then outdents the content.
273
158
  # This change was motivated primarily by the change in Rails 3.2.3 to emit
@@ -277,6 +162,8 @@ module Hamlit
277
162
  # @since Haml 4.0.1
278
163
  # @private
279
164
  def fix_textareas!(input)
165
+ return input unless input.include?('<textarea'.freeze)
166
+
280
167
  pattern = /<(textarea)([^>]*)>(\n|&#x000A;)(.*?)<\/textarea>/im
281
168
  input.gsub!(pattern) do |s|
282
169
  match = pattern.match(s)
@@ -288,10 +175,13 @@ module Hamlit
288
175
  end
289
176
  "<#{match[1]}#{match[2]}>\n#{content}</#{match[1]}>"
290
177
  end
178
+ input
291
179
  end
292
180
 
181
+ private
182
+
293
183
  def new_encoded_string
294
- "".encode(Encoding.find(options[:encoding]))
184
+ "".encode(options[:encoding])
295
185
  end
296
186
 
297
187
  @@tab_cache = {}
@@ -329,7 +219,7 @@ module Hamlit
329
219
  id = "#{ prefix }_#{ id }"
330
220
  end
331
221
 
332
- {ID_KEY => id, CLASS_KEY => class_name}
222
+ { 'id'.freeze => id, 'class'.freeze => class_name }
333
223
  end
334
224
 
335
225
  # Changes a word from camel case to underscores.
@@ -1,553 +1 @@
1
- require 'hamlit/parser/haml_util'
2
- require 'hamlit/parser/haml_parser'
3
-
4
- module Hamlit
5
- class HamlCompiler
6
- include ::Hamlit::HamlUtil
7
-
8
- attr_accessor :options
9
-
10
- def initialize(options)
11
- @options = options
12
- @output_tabs = 0
13
- @to_merge = []
14
- @precompiled = ''
15
- @node = nil
16
- end
17
-
18
- def compile(node)
19
- parent, @node = @node, node
20
- if node.children.empty?
21
- send(:"compile_#{node.type}")
22
- else
23
- send(:"compile_#{node.type}") {node.children.each {|c| compile c}}
24
- end
25
- ensure
26
- @node = parent
27
- end
28
-
29
- # The source code that is evaluated to produce the Haml document.
30
- #
31
- # This is automatically converted to the correct encoding
32
- # (see {file:REFERENCE.md#encodings the `:encoding` option}).
33
- #
34
- # @return [String]
35
- def precompiled
36
- encoding = Encoding.find(@options.encoding)
37
- return @precompiled.force_encoding(encoding) if encoding == Encoding::ASCII_8BIT
38
- return @precompiled.encode(encoding)
39
- end
40
-
41
- def precompiled_with_return_value
42
- "#{precompiled};#{precompiled_method_return_value}"
43
- end
44
-
45
- # Returns the precompiled string with the preamble and postamble.
46
- #
47
- # Initializes to ActionView::OutputBuffer when available; this is necessary
48
- # to avoid ordering issues with partial layouts in Rails. If not available,
49
- # initializes to nil.
50
- def precompiled_with_ambles(local_names)
51
- preamble = <<END.tr!("\n", ';')
52
- begin
53
- extend ::Hamlit::HamlHelpers
54
- _hamlout = @haml_buffer = ::Hamlit::HamlBuffer.new(haml_buffer, #{options.for_buffer.inspect})
55
- _erbout = _hamlout.buffer
56
- @output_buffer = output_buffer ||= ActionView::OutputBuffer.new rescue nil
57
- END
58
- postamble = <<END.tr!("\n", ';')
59
- #{precompiled_method_return_value}
60
- ensure
61
- @haml_buffer = @haml_buffer.upper if @haml_buffer
62
- end
63
- END
64
- "#{preamble}#{locals_code(local_names)}#{precompiled}#{postamble}"
65
- end
66
-
67
- private
68
-
69
- # Returns the string used as the return value of the precompiled method.
70
- # This method exists so it can be monkeypatched to return modified values.
71
- def precompiled_method_return_value
72
- "_erbout"
73
- end
74
-
75
- def locals_code(names)
76
- names = names.keys if Hash === names
77
-
78
- names.each_with_object('') do |name, code|
79
- # Can't use || because someone might explicitly pass in false with a symbol
80
- sym_local = "_haml_locals[#{inspect_obj(name.to_sym)}]"
81
- str_local = "_haml_locals[#{inspect_obj(name.to_s)}]"
82
- code << "#{name} = #{sym_local}.nil? ? #{str_local} : #{sym_local};"
83
- end
84
- end
85
-
86
- def compile_root
87
- @dont_indent_next_line = @dont_tab_up_next_text = false
88
- @output_line = 1
89
- yield if block_given?
90
- flush_merged_text
91
- end
92
-
93
- def compile_plain
94
- push_text @node.value[:text]
95
- end
96
-
97
- def nuke_inner_whitespace?(node)
98
- if node.value && node.value[:nuke_inner_whitespace]
99
- true
100
- elsif node.parent
101
- nuke_inner_whitespace?(node.parent)
102
- else
103
- false
104
- end
105
- end
106
-
107
- def compile_script(&block)
108
- push_script(@node.value[:text],
109
- :preserve_script => @node.value[:preserve],
110
- :escape_html => @node.value[:escape_html],
111
- :nuke_inner_whitespace => nuke_inner_whitespace?(@node),
112
- &block)
113
- end
114
-
115
- def compile_silent_script
116
- return if @options.suppress_eval
117
- push_silent(@node.value[:text])
118
- keyword = @node.value[:keyword]
119
-
120
- if block_given?
121
- # Store these values because for conditional statements,
122
- # we want to restore them for each branch
123
- @node.value[:dont_indent_next_line] = @dont_indent_next_line
124
- @node.value[:dont_tab_up_next_text] = @dont_tab_up_next_text
125
- yield
126
- push_silent("end", :can_suppress) unless @node.value[:dont_push_end]
127
- elsif keyword == "end"
128
- if @node.parent.children.last.equal?(@node)
129
- # Since this "end" is ending the block,
130
- # we don't need to generate an additional one
131
- @node.parent.value[:dont_push_end] = true
132
- end
133
- # Don't restore dont_* for end because it isn't a conditional branch.
134
- elsif ::Hamlit::HamlParser::MID_BLOCK_KEYWORDS.include?(keyword)
135
- # Restore dont_* for this conditional branch
136
- @dont_indent_next_line = @node.parent.value[:dont_indent_next_line]
137
- @dont_tab_up_next_text = @node.parent.value[:dont_tab_up_next_text]
138
- end
139
- end
140
-
141
- def compile_haml_comment; end
142
-
143
- def compile_tag
144
- t = @node.value
145
-
146
- # Get rid of whitespace outside of the tag if we need to
147
- rstrip_buffer! if t[:nuke_outer_whitespace]
148
-
149
- dont_indent_next_line =
150
- (t[:nuke_outer_whitespace] && !block_given?) ||
151
- (t[:nuke_inner_whitespace] && block_given?)
152
-
153
- if @options.suppress_eval
154
- object_ref = :nil
155
- parse = false
156
- value = t[:parse] ? nil : t[:value]
157
- attributes_hashes = {}
158
- preserve_script = false
159
- else
160
- object_ref = t[:object_ref]
161
- parse = t[:parse]
162
- value = t[:value]
163
- attributes_hashes = t[:attributes_hashes]
164
- preserve_script = t[:preserve_script]
165
- end
166
-
167
- if @options[:trace]
168
- t[:attributes].merge!({"data-trace" => @options.filename.split('/views').last << ":" << @node.line.to_s})
169
- end
170
-
171
- # Check if we can render the tag directly to text and not process it in the buffer
172
- if (object_ref == :nil) && attributes_hashes.empty? && !preserve_script
173
- tag_closed = !block_given? && !t[:self_closing] && !parse
174
-
175
- open_tag = prerender_tag(t[:name], t[:self_closing], t[:attributes])
176
- if tag_closed
177
- open_tag << "#{value}</#{t[:name]}>"
178
- open_tag << "\n" unless t[:nuke_outer_whitespace]
179
- elsif !(parse || t[:nuke_inner_whitespace] ||
180
- (t[:self_closing] && t[:nuke_outer_whitespace]))
181
- open_tag << "\n"
182
- end
183
-
184
- push_merged_text(open_tag,
185
- tag_closed || t[:self_closing] || t[:nuke_inner_whitespace] ? 0 : 1,
186
- !t[:nuke_outer_whitespace])
187
-
188
- @dont_indent_next_line = dont_indent_next_line
189
- return if tag_closed
190
- else
191
- if attributes_hashes.empty?
192
- attributes_hashes = ''
193
- elsif attributes_hashes.size == 1
194
- attributes_hashes = ", #{attributes_hashes.first}"
195
- else
196
- attributes_hashes = ", #{attributes_hashes.join(", ")}"
197
- end
198
-
199
- push_merged_text "<#{t[:name]}", 0, !t[:nuke_outer_whitespace]
200
- push_generated_script(
201
- "_hamlout.attributes(#{inspect_obj(t[:attributes])}, #{object_ref}#{attributes_hashes})")
202
- concat_merged_text(
203
- if t[:self_closing] && @options.xhtml?
204
- " />#{"\n" unless t[:nuke_outer_whitespace]}"
205
- else
206
- ">#{"\n" unless (t[:self_closing] && @options.html?) ? t[:nuke_outer_whitespace] : (!block_given? || t[:preserve_tag] || t[:nuke_inner_whitespace])}"
207
- end)
208
-
209
- if value && !parse
210
- concat_merged_text("#{value}</#{t[:name]}>#{"\n" unless t[:nuke_outer_whitespace]}")
211
- elsif !t[:nuke_inner_whitespace] && !t[:self_closing]
212
- @to_merge << [:text, '', 1]
213
- end
214
-
215
- @dont_indent_next_line = dont_indent_next_line
216
- end
217
-
218
- return if t[:self_closing]
219
-
220
- if value.nil?
221
- @output_tabs += 1 unless t[:nuke_inner_whitespace]
222
- yield if block_given?
223
- @output_tabs -= 1 unless t[:nuke_inner_whitespace]
224
- rstrip_buffer! if t[:nuke_inner_whitespace]
225
- push_merged_text("</#{t[:name]}>#{"\n" unless t[:nuke_outer_whitespace]}",
226
- t[:nuke_inner_whitespace] ? 0 : -1, !t[:nuke_inner_whitespace])
227
- @dont_indent_next_line = t[:nuke_outer_whitespace]
228
- return
229
- end
230
-
231
- if parse
232
- push_script(value, t.merge(:in_tag => true))
233
- concat_merged_text("</#{t[:name]}>#{"\n" unless t[:nuke_outer_whitespace]}")
234
- end
235
- end
236
-
237
- def compile_comment
238
- condition = "#{@node.value[:conditional]}>" if @node.value[:conditional]
239
- revealed = @node.value[:revealed]
240
-
241
- open = "<!--#{condition}#{'<!-->' if revealed}"
242
-
243
- close = "#{'<!--' if revealed}#{'<![endif]' if condition}-->"
244
-
245
- unless block_given?
246
- push_merged_text("#{open} ")
247
-
248
- if @node.value[:parse]
249
- push_script(@node.value[:text], :in_tag => true, :nuke_inner_whitespace => true)
250
- else
251
- push_merged_text(@node.value[:text], 0, false)
252
- end
253
-
254
- push_merged_text(" #{close}\n", 0, false)
255
- return
256
- end
257
-
258
- push_text(open, 1)
259
- @output_tabs += 1
260
- yield if block_given?
261
- @output_tabs -= 1
262
- push_text(close, -1)
263
- end
264
-
265
- def compile_doctype
266
- doctype = text_for_doctype
267
- push_text doctype if doctype
268
- end
269
-
270
- def compile_filter
271
- unless filter = Filters.defined[@node.value[:name]]
272
- name = @node.value[:name]
273
- if ["maruku", "textile"].include?(name)
274
- raise ::Hamlit::HamlError.new(::Hamlit::HamlError.message(:install_haml_contrib, name), @node.line - 1)
275
- else
276
- raise ::Hamlit::HamlError.new(::Hamlit::HamlError.message(:filter_not_defined, name), @node.line - 1)
277
- end
278
- end
279
- filter.internal_compile(self, @node.value[:text])
280
- end
281
-
282
- def text_for_doctype
283
- if @node.value[:type] == "xml"
284
- return nil if @options.html?
285
- wrapper = @options.attr_wrapper
286
- return "<?xml version=#{wrapper}1.0#{wrapper} encoding=#{wrapper}#{@node.value[:encoding] || "utf-8"}#{wrapper} ?>"
287
- end
288
-
289
- if @options.html5?
290
- '<!DOCTYPE html>'
291
- else
292
- if @options.xhtml?
293
- if @node.value[:version] == "1.1"
294
- '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
295
- elsif @node.value[:version] == "5"
296
- '<!DOCTYPE html>'
297
- else
298
- case @node.value[:type]
299
- when "strict"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
300
- when "frameset"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
301
- when "mobile"; '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">'
302
- when "rdfa"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">'
303
- when "basic"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">'
304
- else '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
305
- end
306
- end
307
-
308
- elsif @options.html4?
309
- case @node.value[:type]
310
- when "strict"; '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'
311
- when "frameset"; '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">'
312
- else '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'
313
- end
314
- end
315
- end
316
- end
317
-
318
- # Evaluates `text` in the context of the scope object, but
319
- # does not output the result.
320
- def push_silent(text, can_suppress = false)
321
- flush_merged_text
322
- return if can_suppress && @options.suppress_eval?
323
- newline = (text == "end") ? ";" : "\n"
324
- @precompiled << "#{resolve_newlines}#{text}#{newline}"
325
- @output_line = @output_line + text.count("\n") + newline.count("\n")
326
- end
327
-
328
- # Adds `text` to `@buffer` with appropriate tabulation
329
- # without parsing it.
330
- def push_merged_text(text, tab_change = 0, indent = true)
331
- text = !indent || @dont_indent_next_line || @options.ugly ? text : "#{' ' * @output_tabs}#{text}"
332
- @to_merge << [:text, text, tab_change]
333
- @dont_indent_next_line = false
334
- end
335
-
336
- # Concatenate `text` to `@buffer` without tabulation.
337
- def concat_merged_text(text)
338
- @to_merge << [:text, text, 0]
339
- end
340
-
341
- def push_text(text, tab_change = 0)
342
- push_merged_text("#{text}\n", tab_change)
343
- end
344
-
345
- def flush_merged_text
346
- return if @to_merge.empty?
347
-
348
- mtabs = 0
349
- @to_merge.map! do |type, val, tabs|
350
- case type
351
- when :text
352
- mtabs += tabs
353
- inspect_obj(val)[1...-1]
354
- when :script
355
- if mtabs != 0 && !@options.ugly
356
- val = "_hamlout.adjust_tabs(#{mtabs}); " + val
357
- end
358
- mtabs = 0
359
- "\#{#{val}}"
360
- else
361
- raise ::Hamlit::HamlSyntaxError.new("[HAML BUG] Undefined entry in ::Hamlit::HamlCompiler@to_merge.")
362
- end
363
- end
364
- str = @to_merge.join
365
-
366
- unless str.empty?
367
- @precompiled <<
368
- if @options.ugly
369
- "_hamlout.buffer << \"#{str}\";"
370
- else
371
- "_hamlout.push_text(\"#{str}\", #{mtabs}, #{@dont_tab_up_next_text.inspect});"
372
- end
373
- end
374
- @to_merge = []
375
- @dont_tab_up_next_text = false
376
- end
377
-
378
- # Causes `text` to be evaluated in the context of
379
- # the scope object and the result to be added to `@buffer`.
380
- #
381
- # If `opts[:preserve_script]` is true, Haml::Helpers#find_and_preserve is run on
382
- # the result before it is added to `@buffer`
383
- def push_script(text, opts = {})
384
- return if @options.suppress_eval?
385
-
386
- args = [:preserve_script, :in_tag, :preserve_tag, :escape_html, :nuke_inner_whitespace]
387
- args.map! {|name| !!opts[name]}
388
- args << !block_given? << @options.ugly
389
-
390
- no_format = @options.ugly &&
391
- !(opts[:preserve_script] || opts[:preserve_tag] || opts[:escape_html])
392
-
393
- # Prerender tabulation unless we're in a tag
394
- push_merged_text '' unless opts[:in_tag]
395
-
396
- unless block_given?
397
- format_script_method = "_hamlout.format_script((#{text}\n),#{args.join(',')});"
398
- push_generated_script(no_format ? "#{text}\n" : format_script_method)
399
- concat_merged_text("\n") unless opts[:in_tag] || opts[:nuke_inner_whitespace]
400
- return
401
- end
402
-
403
- flush_merged_text
404
- push_silent "haml_temp = #{text}"
405
- yield
406
- push_silent('end', :can_suppress) unless @node.value[:dont_push_end]
407
- format_script_method = "_hamlout.format_script(haml_temp,#{args.join(',')});"
408
- @precompiled << "_hamlout.buffer << #{no_format ? "haml_temp.to_s;" : format_script_method}"
409
- concat_merged_text("\n") unless opts[:in_tag] || opts[:nuke_inner_whitespace] || @options.ugly
410
- end
411
-
412
- def push_generated_script(text)
413
- @to_merge << [:script, resolve_newlines + text]
414
- @output_line += text.count("\n")
415
- end
416
-
417
- # This is a class method so it can be accessed from Buffer.
418
- def self.build_attributes(is_html, attr_wrapper, escape_attrs, hyphenate_data_attrs, attributes = {})
419
- # @TODO this is an absolutely ridiculous amount of arguments. At least
420
- # some of this needs to be moved into an instance method.
421
- quote_escape = attr_wrapper == '"' ? "&#x0022;" : "&#x0027;"
422
- other_quote_char = attr_wrapper == '"' ? "'" : '"'
423
- join_char = hyphenate_data_attrs ? '-' : '_'
424
-
425
- attributes.each do |key, value|
426
- if value.is_a?(Hash)
427
- data_attributes = attributes.delete(key)
428
- data_attributes = flatten_data_attributes(data_attributes, '', join_char)
429
- data_attributes = build_data_keys(data_attributes, hyphenate_data_attrs, key)
430
- attributes = data_attributes.merge(attributes)
431
- end
432
- end
433
-
434
- result = attributes.collect do |attr, value|
435
- next if value.nil?
436
-
437
- value = filter_and_join(value, ' ') if attr == 'class'
438
- value = filter_and_join(value, '_') if attr == 'id'
439
-
440
- if value == true
441
- next " #{attr}" if is_html
442
- next " #{attr}=#{attr_wrapper}#{attr}#{attr_wrapper}"
443
- elsif value == false
444
- next
445
- end
446
-
447
- escaped =
448
- if escape_attrs == :once
449
- ::Hamlit::HamlHelpers.escape_once(value.to_s)
450
- elsif escape_attrs
451
- ::Hamlit::HamlHelpers.html_escape(value.to_s)
452
- else
453
- value.to_s
454
- end
455
- value = ::Hamlit::HamlHelpers.preserve(escaped)
456
- if escape_attrs
457
- # We want to decide whether or not to escape quotes
458
- value.gsub!(/&quot;|&#x0022;/, '"')
459
- this_attr_wrapper = attr_wrapper
460
- if value.include? attr_wrapper
461
- if value.include? other_quote_char
462
- value.gsub!(attr_wrapper, quote_escape)
463
- else
464
- this_attr_wrapper = other_quote_char
465
- end
466
- end
467
- else
468
- this_attr_wrapper = attr_wrapper
469
- end
470
- " #{attr}=#{this_attr_wrapper}#{value}#{this_attr_wrapper}"
471
- end
472
- result.compact!
473
- result.sort!
474
- result.join
475
- end
476
-
477
- def self.filter_and_join(value, separator)
478
- return '' if (value.respond_to?(:empty?) && value.empty?)
479
-
480
- if value.is_a?(Array)
481
- value.flatten!
482
- value.map! {|item| item ? item.to_s : nil}
483
- value.compact!
484
- value = value.join(separator)
485
- else
486
- value = value ? value.to_s : nil
487
- end
488
- !value.nil? && !value.empty? && value
489
- end
490
-
491
- def self.build_data_keys(data_hash, hyphenate, attr_name="data")
492
- Hash[data_hash.map do |name, value|
493
- if name == nil
494
- [attr_name, value]
495
- elsif hyphenate
496
- ["#{attr_name}-#{name.to_s.tr('_', '-')}", value]
497
- else
498
- ["#{attr_name}-#{name}", value]
499
- end
500
- end]
501
- end
502
-
503
- def self.flatten_data_attributes(data, key, join_char, seen = [])
504
- return {key => data} unless data.is_a?(Hash)
505
-
506
- return {key => nil} if seen.include? data.object_id
507
- seen << data.object_id
508
-
509
- data.sort {|x, y| x[0].to_s <=> y[0].to_s}.inject({}) do |hash, (k, v)|
510
- joined = key == '' ? k : [key, k].join(join_char)
511
- hash.merge! flatten_data_attributes(v, joined, join_char, seen)
512
- end
513
- end
514
-
515
- def prerender_tag(name, self_close, attributes)
516
- attributes_string = ::Hamlit::HamlCompiler.build_attributes(
517
- @options.html?, @options.attr_wrapper, @options.escape_attrs, @options.hyphenate_data_attrs, attributes)
518
- "<#{name}#{attributes_string}#{self_close && @options.xhtml? ? ' /' : ''}>"
519
- end
520
-
521
- def resolve_newlines
522
- diff = @node.line - @output_line
523
- return "" if diff <= 0
524
- @output_line = @node.line
525
- "\n" * diff
526
- end
527
-
528
- # Get rid of and whitespace at the end of the buffer
529
- # or the merged text
530
- def rstrip_buffer!(index = -1)
531
- last = @to_merge[index]
532
- if last.nil?
533
- push_silent("_hamlout.rstrip!", false)
534
- @dont_tab_up_next_text = true
535
- return
536
- end
537
-
538
- case last.first
539
- when :text
540
- last[1].rstrip!
541
- if last[1].empty?
542
- @to_merge.slice! index
543
- rstrip_buffer! index
544
- end
545
- when :script
546
- last[1].gsub!(/\(haml_temp, (.*?)\);$/, '(haml_temp.rstrip, \1);')
547
- rstrip_buffer! index - 1
548
- else
549
- raise ::Hamlit::HamlSyntaxError.new("[HAML BUG] Undefined entry in ::Hamlit::HamlCompiler@to_merge.")
550
- end
551
- end
552
- end
553
- end
1
+ class Hamlit::HamlCompiler; end