hamlit 2.11.1 → 2.14.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +36 -0
  3. data/.gitignore +2 -1
  4. data/CHANGELOG.md +53 -0
  5. data/Gemfile +0 -4
  6. data/LICENSE.txt +26 -23
  7. data/README.md +9 -8
  8. data/benchmark/graph/graph.key +0 -0
  9. data/benchmark/graph/graph.png +0 -0
  10. data/bin/update-haml +125 -0
  11. data/ext/hamlit/hamlit.c +0 -1
  12. data/hamlit.gemspec +1 -1
  13. data/lib/hamlit.rb +6 -4
  14. data/lib/hamlit/attribute_builder.rb +2 -2
  15. data/lib/hamlit/attribute_compiler.rb +3 -3
  16. data/lib/hamlit/cli.rb +34 -10
  17. data/lib/hamlit/compiler/children_compiler.rb +1 -1
  18. data/lib/hamlit/compiler/comment_compiler.rb +1 -0
  19. data/lib/hamlit/filters/escaped.rb +1 -1
  20. data/lib/hamlit/filters/markdown.rb +1 -0
  21. data/lib/hamlit/filters/preserve.rb +1 -1
  22. data/lib/hamlit/filters/text_base.rb +1 -1
  23. data/lib/hamlit/filters/tilt_base.rb +1 -1
  24. data/lib/hamlit/parser.rb +6 -2
  25. data/lib/hamlit/parser/haml_attribute_builder.rb +164 -0
  26. data/lib/hamlit/parser/haml_buffer.rb +20 -130
  27. data/lib/hamlit/parser/haml_compiler.rb +1 -553
  28. data/lib/hamlit/parser/haml_error.rb +29 -25
  29. data/lib/hamlit/parser/haml_escapable.rb +1 -0
  30. data/lib/hamlit/parser/haml_generator.rb +1 -0
  31. data/lib/hamlit/parser/haml_helpers.rb +41 -59
  32. data/lib/hamlit/parser/{haml_xss_mods.rb → haml_helpers/xss_mods.rb} +20 -15
  33. data/lib/hamlit/parser/haml_options.rb +53 -66
  34. data/lib/hamlit/parser/haml_parser.rb +133 -73
  35. data/lib/hamlit/parser/haml_temple_engine.rb +123 -0
  36. data/lib/hamlit/parser/haml_util.rb +10 -40
  37. data/lib/hamlit/rails_template.rb +1 -1
  38. data/lib/hamlit/string_splitter.rb +1 -0
  39. data/lib/hamlit/version.rb +1 -1
  40. metadata +17 -12
  41. data/.travis.yml +0 -49
  42. data/lib/hamlit/parser/MIT-LICENSE +0 -20
  43. data/lib/hamlit/parser/README.md +0 -30
@@ -39,7 +39,7 @@ module Hamlit
39
39
  when :script, :silent_script
40
40
  @lineno += 1
41
41
  when :tag
42
- node.value[:attributes_hashes].each do |attribute_hash|
42
+ [node.value[:dynamic_attributes].new, node.value[:dynamic_attributes].old].compact.each do |attribute_hash|
43
43
  @lineno += attribute_hash.count("\n")
44
44
  end
45
45
  @lineno += 1 if node.children.empty? && node.value[:parse]
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Hamlit
2
3
  class Compiler
3
4
  class CommentCompiler
@@ -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
@@ -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.