hamlit 2.11.1 → 2.14.1

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 (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
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ripper'
1
4
  require 'strscan'
2
- require 'hamlit/parser/haml_util'
3
- require 'hamlit/parser/haml_error'
4
5
 
5
6
  module Hamlit
6
7
  class HamlParser
7
- include ::Hamlit::HamlUtil
8
+ include Hamlit::HamlUtil
8
9
 
9
10
  attr_reader :root
10
11
 
@@ -61,7 +62,7 @@ module Hamlit
61
62
  SILENT_SCRIPT,
62
63
  ESCAPE,
63
64
  FILTER
64
- ]
65
+ ].freeze
65
66
 
66
67
  # The value of the character that designates that a line is part
67
68
  # of a multiline string.
@@ -75,8 +76,8 @@ module Hamlit
75
76
  #
76
77
  BLOCK_WITH_SPACES = /do\s*\|\s*[^\|]*\s+\|\z/
77
78
 
78
- MID_BLOCK_KEYWORDS = %w[else elsif rescue ensure end when]
79
- START_BLOCK_KEYWORDS = %w[if begin case unless]
79
+ MID_BLOCK_KEYWORDS = %w[else elsif rescue ensure end when].freeze
80
+ START_BLOCK_KEYWORDS = %w[if begin case unless].freeze
80
81
  # Try to parse assignments to block starters as best as possible
81
82
  START_BLOCK_KEYWORD_REGEX = /(?:\w+(?:,\s*\w+)*\s*=\s*)?(#{START_BLOCK_KEYWORDS.join('|')})/
82
83
  BLOCK_KEYWORD_REGEX = /^-?\s*(?:(#{MID_BLOCK_KEYWORDS.join('|')})|#{START_BLOCK_KEYWORD_REGEX.source})\b/
@@ -90,14 +91,19 @@ module Hamlit
90
91
  ID_KEY = 'id'.freeze
91
92
  CLASS_KEY = 'class'.freeze
92
93
 
93
- def initialize(template, options)
94
- @options = options
94
+ # Used for scanning old attributes, substituting the first '{'
95
+ METHOD_CALL_PREFIX = 'a('
96
+
97
+ def initialize(options)
98
+ @options = HamlOptions.wrap(options)
95
99
  # Record the indent levels of "if" statements to validate the subsequent
96
100
  # elsif and else statements are indented at the appropriate level.
97
101
  @script_level_stack = []
98
102
  @template_index = 0
99
103
  @template_tabs = 0
104
+ end
100
105
 
106
+ def call(template)
101
107
  match = template.rstrip.scan(/(([ \t]+)?(.*?))(?:\Z|\r\n|\r|\n)/m)
102
108
  # discard the last match which is always blank
103
109
  match.pop
@@ -106,16 +112,14 @@ module Hamlit
106
112
  end
107
113
  # Append special end-of-document marker
108
114
  @template << Line.new(nil, '-#', '-#', @template.size, self, true)
109
- end
110
115
 
111
- def parse
112
116
  @root = @parent = ParseNode.new(:root)
113
117
  @flat = false
114
118
  @filter_buffer = nil
115
119
  @indentation = nil
116
120
  @line = next_line
117
121
 
118
- raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:indenting_at_start), @line.index) if @line.tabs != 0
122
+ raise HamlSyntaxError.new(HamlError.message(:indenting_at_start), @line.index) if @line.tabs != 0
119
123
 
120
124
  loop do
121
125
  next_line
@@ -138,7 +142,7 @@ module Hamlit
138
142
  end
139
143
 
140
144
  if !flat? && @next_line.tabs - @line.tabs > 1
141
- raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:deeper_indenting, @next_line.tabs - @line.tabs), @next_line.index)
145
+ raise HamlSyntaxError.new(HamlError.message(:deeper_indenting, @next_line.tabs - @line.tabs), @next_line.index)
142
146
  end
143
147
 
144
148
  @line = @next_line
@@ -146,7 +150,7 @@ module Hamlit
146
150
  # Close all the open tags
147
151
  close until @parent.type == :root
148
152
  @root
149
- rescue ::Hamlit::HamlError => e
153
+ rescue Hamlit::HamlError => e
150
154
  e.backtrace.unshift "#{@options.filename}:#{(e.line ? e.line + 1 : @line.index + 1) + @options.line - 1}"
151
155
  raise
152
156
  end
@@ -158,7 +162,7 @@ module Hamlit
158
162
  @indentation = line.whitespace
159
163
 
160
164
  if @indentation.include?(?\s) && @indentation.include?(?\t)
161
- raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:cant_use_tabs_and_spaces), line.index)
165
+ raise HamlSyntaxError.new(HamlError.message(:cant_use_tabs_and_spaces), line.index)
162
166
  end
163
167
 
164
168
  @flat_spaces = @indentation * (@template_tabs+1) if flat?
@@ -169,17 +173,17 @@ module Hamlit
169
173
  return tabs if line.whitespace == @indentation * tabs
170
174
  return @template_tabs + 1 if flat? && line.whitespace =~ /^#{@flat_spaces}/
171
175
 
172
- message = ::Hamlit::HamlError.message(:inconsistent_indentation,
176
+ message = HamlError.message(:inconsistent_indentation,
173
177
  human_indentation(line.whitespace),
174
178
  human_indentation(@indentation)
175
179
  )
176
- raise ::Hamlit::HamlSyntaxError.new(message, line.index)
180
+ raise HamlSyntaxError.new(message, line.index)
177
181
  end
178
182
 
179
183
  private
180
184
 
181
185
  # @private
182
- class Line < Struct.new(:whitespace, :text, :full, :index, :parser, :eod)
186
+ Line = Struct.new(:whitespace, :text, :full, :index, :parser, :eod) do
183
187
  alias_method :eod?, :eod
184
188
 
185
189
  # @private
@@ -195,14 +199,39 @@ module Hamlit
195
199
  end
196
200
 
197
201
  # @private
198
- class ParseNode < Struct.new(:type, :line, :value, :parent, :children)
202
+ ParseNode = Struct.new(:type, :line, :value, :parent, :children) do
199
203
  def initialize(*args)
200
204
  super
201
205
  self.children ||= []
202
206
  end
203
207
 
204
208
  def inspect
205
- %Q[(#{type} #{value.inspect}#{children.each_with_object('') {|c, s| s << "\n#{c.inspect.gsub!(/^/, ' ')}"}})]
209
+ %Q[(#{type} #{value.inspect}#{children.each_with_object(''.dup) {|c, s| s << "\n#{c.inspect.gsub!(/^/, ' ')}"}})].dup
210
+ end
211
+ end
212
+
213
+ # @param [String] new - Hash literal including dynamic values.
214
+ # @param [String] old - Hash literal including dynamic values or Ruby literal of multiple Hashes which MUST be interpreted as method's last arguments.
215
+ DynamicAttributes = Struct.new(:new, :old) do
216
+ undef :old=
217
+ def old=(value)
218
+ unless value =~ /\A{.*}\z/m
219
+ raise ArgumentError.new('Old attributes must start with "{" and end with "}"')
220
+ end
221
+ self[:old] = value
222
+ end
223
+
224
+ # This will be a literal for Hamlit::HamlBuffer#attributes's last argument, `attributes_hashes`.
225
+ def to_literal
226
+ [new, stripped_old].compact.join(', ')
227
+ end
228
+
229
+ private
230
+
231
+ # For `%foo{ { foo: 1 }, bar: 2 }`, :old is "{ { foo: 1 }, bar: 2 }" and this method returns " { foo: 1 }, bar: 2 " for last argument.
232
+ def stripped_old
233
+ return nil if old.nil?
234
+ old.sub!(/\A{/, '').sub!(/}\z/m, '')
206
235
  end
207
236
  end
208
237
 
@@ -264,7 +293,7 @@ module Hamlit
264
293
  end
265
294
 
266
295
  def block_keyword(text)
267
- return unless keyword = text.scan(BLOCK_KEYWORD_REGEX)[0]
296
+ return unless (keyword = text.scan(BLOCK_KEYWORD_REGEX)[0])
268
297
  keyword[0] || keyword[1]
269
298
  end
270
299
 
@@ -275,20 +304,20 @@ module Hamlit
275
304
 
276
305
  def plain(line, escape_html = nil)
277
306
  if block_opened?
278
- raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_nesting_plain), @next_line.index)
307
+ raise HamlSyntaxError.new(HamlError.message(:illegal_nesting_plain), @next_line.index)
279
308
  end
280
309
 
281
310
  unless contains_interpolation?(line.text)
282
311
  return ParseNode.new(:plain, line.index + 1, :text => line.text)
283
312
  end
284
313
 
285
- escape_html = @options.escape_html if escape_html.nil?
286
- line.text = ::Hamlit::HamlUtil.unescape_interpolation(line.text)
314
+ escape_html = @options.escape_html && @options.mime_type != 'text/plain' if escape_html.nil?
315
+ line.text = unescape_interpolation(line.text)
287
316
  script(line, false).tap { |n| n.value[:escape_interpolation] = true if escape_html }
288
317
  end
289
318
 
290
319
  def script(line, escape_html = nil, preserve = false)
291
- raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:no_ruby_code, '=')) if line.text.empty?
320
+ raise HamlSyntaxError.new(HamlError.message(:no_ruby_code, '=')) if line.text.empty?
292
321
  line = handle_ruby_multiline(line)
293
322
  escape_html = @options.escape_html if escape_html.nil?
294
323
 
@@ -300,12 +329,12 @@ module Hamlit
300
329
  end
301
330
 
302
331
  def flat_script(line, escape_html = nil)
303
- raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:no_ruby_code, '~')) if line.text.empty?
332
+ raise HamlSyntaxError.new(HamlError.message(:no_ruby_code, '~')) if line.text.empty?
304
333
  script(line, escape_html, :preserve)
305
334
  end
306
335
 
307
336
  def silent_script(line)
308
- raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:no_end), line.index) if line.text[1..-1].strip == 'end'
337
+ raise HamlSyntaxError.new(HamlError.message(:no_end), line.index) if line.text[1..-1].strip == 'end'
309
338
 
310
339
  line = handle_ruby_multiline(line)
311
340
  keyword = block_keyword(line.text)
@@ -314,7 +343,7 @@ module Hamlit
314
343
 
315
344
  if ["else", "elsif", "when"].include?(keyword)
316
345
  if @script_level_stack.empty?
317
- raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:missing_if, keyword), @line.index)
346
+ raise Hamlit::HamlSyntaxError.new(HamlError.message(:missing_if, keyword), @line.index)
318
347
  end
319
348
 
320
349
  if keyword == 'when' and !@script_level_stack.last[2]
@@ -325,8 +354,8 @@ module Hamlit
325
354
  end
326
355
 
327
356
  if @script_level_stack.last[1] != @line.tabs
328
- message = ::Hamlit::HamlError.message(:bad_script_indent, keyword, @script_level_stack.last[1], @line.tabs)
329
- raise ::Hamlit::HamlSyntaxError.new(message, @line.index)
357
+ message = HamlError.message(:bad_script_indent, keyword, @script_level_stack.last[1], @line.tabs)
358
+ raise Hamlit::HamlSyntaxError.new(message, @line.index)
330
359
  end
331
360
  end
332
361
 
@@ -363,7 +392,6 @@ module Hamlit
363
392
 
364
393
  preserve_tag = @options.preserve.include?(tag_name)
365
394
  nuke_inner_whitespace ||= preserve_tag
366
- preserve_tag = false if @options.ugly
367
395
  escape_html = (action == '&' || (action != '!' && @options.escape_html))
368
396
 
369
397
  case action
@@ -372,7 +400,7 @@ module Hamlit
372
400
  when '='
373
401
  parse = true
374
402
  if value[0] == ?=
375
- value = ::Hamlit::HamlUtil.unescape_interpolation(value[1..-1].strip)
403
+ value = unescape_interpolation(value[1..-1].strip)
376
404
  escape_interpolation = true if escape_html
377
405
  escape_html = false
378
406
  end
@@ -381,50 +409,47 @@ module Hamlit
381
409
  parse = true
382
410
  preserve_script = (value[0] == ?~)
383
411
  if value[1] == ?=
384
- value = ::Hamlit::HamlUtil.unescape_interpolation(value[2..-1].strip)
412
+ value = unescape_interpolation(value[2..-1].strip)
385
413
  escape_interpolation = true if escape_html
386
414
  escape_html = false
387
415
  else
388
416
  value = value[1..-1].strip
389
417
  end
390
418
  elsif contains_interpolation?(value)
391
- value = ::Hamlit::HamlUtil.unescape_interpolation(value)
419
+ value = unescape_interpolation(value)
392
420
  escape_interpolation = true if escape_html
393
421
  parse = true
394
422
  escape_html = false
395
423
  end
396
424
  else
397
425
  if contains_interpolation?(value)
398
- value = ::Hamlit::HamlUtil.unescape_interpolation(value)
399
- escape_interpolation = true if escape_html
426
+ value = unescape_interpolation(value, escape_html)
400
427
  parse = true
401
428
  escape_html = false
402
429
  end
403
430
  end
404
431
 
405
- attributes = ::Hamlit::HamlParser.parse_class_and_id(attributes)
406
- attributes_list = []
432
+ attributes = HamlParser.parse_class_and_id(attributes)
433
+ dynamic_attributes = DynamicAttributes.new
407
434
 
408
435
  if attributes_hashes[:new]
409
436
  static_attributes, attributes_hash = attributes_hashes[:new]
410
- ::Hamlit::HamlBuffer.merge_attrs(attributes, static_attributes) if static_attributes
411
- attributes_list << attributes_hash
437
+ HamlAttributeBuilder.merge_attributes!(attributes, static_attributes) if static_attributes
438
+ dynamic_attributes.new = attributes_hash
412
439
  end
413
440
 
414
441
  if attributes_hashes[:old]
415
442
  static_attributes = parse_static_hash(attributes_hashes[:old])
416
- ::Hamlit::HamlBuffer.merge_attrs(attributes, static_attributes) if static_attributes
417
- attributes_list << attributes_hashes[:old] unless static_attributes || @options.suppress_eval
443
+ HamlAttributeBuilder.merge_attributes!(attributes, static_attributes) if static_attributes
444
+ dynamic_attributes.old = attributes_hashes[:old] unless static_attributes || @options.suppress_eval
418
445
  end
419
446
 
420
- attributes_list.compact!
421
-
422
- raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_nesting_self_closing), @next_line.index) if block_opened? && self_closing
423
- raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:no_ruby_code, action), last_line - 1) if parse && value.empty?
424
- raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:self_closing_content), last_line - 1) if self_closing && !value.empty?
447
+ raise HamlSyntaxError.new(HamlError.message(:illegal_nesting_self_closing), @next_line.index) if block_opened? && self_closing
448
+ raise HamlSyntaxError.new(HamlError.message(:no_ruby_code, action), last_line - 1) if parse && value.empty?
449
+ raise HamlSyntaxError.new(HamlError.message(:self_closing_content), last_line - 1) if self_closing && !value.empty?
425
450
 
426
451
  if block_opened? && !value.empty? && !is_ruby_multiline?(value)
427
- raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_nesting_line, tag_name), @next_line.index)
452
+ raise HamlSyntaxError.new(HamlError.message(:illegal_nesting_line, tag_name), @next_line.index)
428
453
  end
429
454
 
430
455
  self_closing ||= !!(!block_opened? && value.empty? && @options.autoclose.any? {|t| t === tag_name})
@@ -433,7 +458,7 @@ module Hamlit
433
458
  line = handle_ruby_multiline(line) if parse
434
459
 
435
460
  ParseNode.new(:tag, line.index + 1, :name => tag_name, :attributes => attributes,
436
- :attributes_hashes => attributes_list, :self_closing => self_closing,
461
+ :dynamic_attributes => dynamic_attributes, :self_closing => self_closing,
437
462
  :nuke_inner_whitespace => nuke_inner_whitespace,
438
463
  :nuke_outer_whitespace => nuke_outer_whitespace, :object_ref => object_ref,
439
464
  :escape_html => escape_html, :preserve_tag => preserve_tag,
@@ -462,13 +487,13 @@ module Hamlit
462
487
 
463
488
  if contains_interpolation?(text)
464
489
  parse = true
465
- text = slow_unescape_interpolation(text)
490
+ text = unescape_interpolation(text)
466
491
  else
467
492
  parse = false
468
493
  end
469
494
 
470
495
  if block_opened? && !text.empty?
471
- raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_nesting_content), @next_line.index)
496
+ raise HamlSyntaxError.new(Hamlit::HamlError.message(:illegal_nesting_content), @next_line.index)
472
497
  end
473
498
 
474
499
  ParseNode.new(:comment, @line.index + 1, :conditional => conditional, :text => text, :revealed => revealed, :parse => parse)
@@ -476,13 +501,13 @@ module Hamlit
476
501
 
477
502
  # Renders an XHTML doctype or XML shebang.
478
503
  def doctype(text)
479
- raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_nesting_header), @next_line.index) if block_opened?
504
+ raise HamlSyntaxError.new(HamlError.message(:illegal_nesting_header), @next_line.index) if block_opened?
480
505
  version, type, encoding = text[3..-1].strip.downcase.scan(DOCTYPE_REGEX)[0]
481
506
  ParseNode.new(:doctype, @line.index + 1, :version => version, :type => type, :encoding => encoding)
482
507
  end
483
508
 
484
509
  def filter(name)
485
- raise ::Hamlit::HamlError.new(::Hamlit::HamlError.message(:invalid_filter_name, name)) unless name =~ /^\w+$/
510
+ raise HamlError.new(HamlError.message(:invalid_filter_name, name)) unless name =~ /^\w+$/
486
511
 
487
512
  if filter_opened?
488
513
  @flat = true
@@ -519,7 +544,7 @@ module Hamlit
519
544
 
520
545
  # Post-process case statements to normalize the nesting of "when" clauses
521
546
  return unless node.value[:keyword] == "case"
522
- return unless first = node.children.first
547
+ return unless (first = node.children.first)
523
548
  return unless first.type == :silent_script && first.value[:keyword] == "when"
524
549
  return if first.children.empty?
525
550
  # If the case node has a "when" child with children, it's the
@@ -531,7 +556,7 @@ module Hamlit
531
556
 
532
557
  alias :close_script :close_silent_script
533
558
 
534
- # This is a class method so it can be accessed from {Haml::Helpers}.
559
+ # This is a class method so it can be accessed from {Hamlit::HamlHelpers}.
535
560
  #
536
561
  # Iterates through the classes and ids supplied through `.`
537
562
  # and `#` syntax, and returns a hash with them as attributes,
@@ -540,7 +565,7 @@ module Hamlit
540
565
  attributes = {}
541
566
  return attributes if list.empty?
542
567
 
543
- list.scan(/([#.])([-:_a-zA-Z0-9]+)/) do |type, property|
568
+ list.scan(/([#.])([-:_a-zA-Z0-9\@]+)/) do |type, property|
544
569
  case type
545
570
  when '.'
546
571
  if attributes[CLASS_KEY]
@@ -555,16 +580,22 @@ module Hamlit
555
580
  attributes
556
581
  end
557
582
 
583
+ # This method doesn't use Hamlit::HamlAttributeParser because currently it depends on Ripper and Rubinius doesn't provide it.
584
+ # Ideally this logic should be placed in Hamlit::HamlAttributeParser instead of here and this method should use it.
585
+ #
586
+ # @param [String] text - Hash literal or text inside old attributes
587
+ # @return [Hash,nil] - Return nil if text is not static Hash literal
558
588
  def parse_static_hash(text)
559
589
  attributes = {}
560
590
  return attributes if text.empty?
561
591
 
592
+ text = text[1...-1] # strip brackets
562
593
  scanner = StringScanner.new(text)
563
594
  scanner.scan(/\s+/)
564
595
  until scanner.eos?
565
- return unless key = scanner.scan(LITERAL_VALUE_REGEX)
596
+ return unless (key = scanner.scan(LITERAL_VALUE_REGEX))
566
597
  return unless scanner.scan(/\s*=>\s*/)
567
- return unless value = scanner.scan(LITERAL_VALUE_REGEX)
598
+ return unless (value = scanner.scan(LITERAL_VALUE_REGEX))
568
599
  return unless scanner.scan(/\s*(?:,|$)\s*/)
569
600
  attributes[eval(key).to_s] = eval(value).to_s
570
601
  end
@@ -573,13 +604,13 @@ module Hamlit
573
604
 
574
605
  # Parses a line into tag_name, attributes, attributes_hash, object_ref, action, value
575
606
  def parse_tag(text)
576
- match = text.scan(/%([-:\w]+)([-:\w.#]*)(.+)?/)[0]
577
- raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:invalid_tag, text)) unless match
607
+ match = text.scan(/%([-:\w]+)([-:\w.#\@]*)(.+)?/)[0]
608
+ raise HamlSyntaxError.new(HamlError.message(:invalid_tag, text)) unless match
578
609
 
579
610
  tag_name, attributes, rest = match
580
611
 
581
612
  if !attributes.empty? && (attributes =~ /[.#](\.|#|\z)/)
582
- raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_element))
613
+ raise HamlSyntaxError.new(HamlError.message(:illegal_element))
583
614
  end
584
615
 
585
616
  new_attributes_hash = old_attributes_hash = last_line = nil
@@ -624,14 +655,22 @@ module Hamlit
624
655
  nuke_inner_whitespace, action, value, last_line || @line.index + 1]
625
656
  end
626
657
 
658
+ # @return [String] attributes_hash - Hash literal starting with `{` and ending with `}`
659
+ # @return [String] rest
660
+ # @return [Integer] last_line
627
661
  def parse_old_attributes(text)
628
- text = text.dup
629
662
  last_line = @line.index + 1
630
663
 
631
664
  begin
632
- attributes_hash, rest = balance(text, ?{, ?})
633
- rescue ::Hamlit::HamlSyntaxError => e
634
- if text.strip[-1] == ?, && e.message == ::Hamlit::HamlError.message(:unbalanced_brackets)
665
+ # Old attributes often look like a valid Hash literal, but it sometimes allow code like
666
+ # `{ hash, foo: bar }`, which is compiled to `_hamlout.attributes({}, nil, hash, foo: bar)`.
667
+ #
668
+ # To scan such code correctly, this scans `a( hash, foo: bar }` instead, stops when there is
669
+ # 1 more :on_embexpr_end (the last '}') than :on_embexpr_beg, and resurrects '{' afterwards.
670
+ balanced, rest = balance_tokens(text.sub(?{, METHOD_CALL_PREFIX), :on_embexpr_beg, :on_embexpr_end, count: 1)
671
+ attributes_hash = balanced.sub(METHOD_CALL_PREFIX, ?{)
672
+ rescue HamlSyntaxError => e
673
+ if e.message == HamlError.message(:unbalanced_brackets) && !@template.empty?
635
674
  text << "\n#{@next_line.text}"
636
675
  last_line += 1
637
676
  next_line
@@ -641,10 +680,12 @@ module Hamlit
641
680
  raise e
642
681
  end
643
682
 
644
- attributes_hash = attributes_hash[1...-1] if attributes_hash
645
683
  return attributes_hash, rest, last_line
646
684
  end
647
685
 
686
+ # @return [Array<Hash,String,nil>] - [static_attributes (Hash), dynamic_attributes (nil or String starting with `{` and ending with `}`)]
687
+ # @return [String] rest
688
+ # @return [Integer] last_line
648
689
  def parse_new_attributes(text)
649
690
  scanner = StringScanner.new(text)
650
691
  last_line = @line.index + 1
@@ -656,9 +697,9 @@ module Hamlit
656
697
  break if name.nil?
657
698
 
658
699
  if name == false
659
- scanned = ::Hamlit::HamlUtil.balance(text, ?(, ?))
700
+ scanned = Hamlit::HamlUtil.balance(text, ?(, ?))
660
701
  text = scanned ? scanned.first : text
661
- raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:invalid_attribute_list, text.inspect), last_line - 1)
702
+ raise Hamlit::HamlSyntaxError.new(HamlError.message(:invalid_attribute_list, text.inspect), last_line - 1)
662
703
  end
663
704
  attributes[name] = value
664
705
  scanner.scan(/\s*/)
@@ -672,7 +713,7 @@ module Hamlit
672
713
  end
673
714
 
674
715
  static_attributes = {}
675
- dynamic_attributes = "{"
716
+ dynamic_attributes = "{".dup
676
717
  attributes.each do |name, (type, val)|
677
718
  if type == :static
678
719
  static_attributes[name] = val
@@ -687,7 +728,7 @@ module Hamlit
687
728
  end
688
729
 
689
730
  def parse_new_attribute(scanner)
690
- unless name = scanner.scan(/[-:\w]+/)
731
+ unless (name = scanner.scan(/[-:\w]+/))
691
732
  return if scanner.scan(/\)/)
692
733
  return false
693
734
  end
@@ -696,8 +737,8 @@ module Hamlit
696
737
  return name, [:static, true] unless scanner.scan(/=/) #/end
697
738
 
698
739
  scanner.scan(/\s*/)
699
- unless quote = scanner.scan(/["']/)
700
- return false unless var = scanner.scan(/(@@?|\$)?\w+/)
740
+ unless (quote = scanner.scan(/["']/))
741
+ return false unless (var = scanner.scan(/(@@?|\$)?\w+/))
701
742
  return name, [:dynamic, var]
702
743
  end
703
744
 
@@ -712,7 +753,7 @@ module Hamlit
712
753
 
713
754
  return name, [:static, content.first[1]] if content.size == 1
714
755
  return name, [:dynamic,
715
- %!"#{content.each_with_object('') {|(t, v), s| s << (t == :str ? inspect_obj(v)[1...-1] : "\#{#{v}}")}}"!]
756
+ %!"#{content.each_with_object(''.dup) {|(t, v), s| s << (t == :str ? inspect_obj(v)[1...-1] : "\#{#{v}}")}}"!]
716
757
  end
717
758
 
718
759
  def next_line
@@ -780,7 +821,26 @@ module Hamlit
780
821
  end
781
822
 
782
823
  def balance(*args)
783
- ::Hamlit::HamlUtil.balance(*args) or raise(::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:unbalanced_brackets)))
824
+ Hamlit::HamlUtil.balance(*args) or raise(HamlSyntaxError.new(HamlError.message(:unbalanced_brackets)))
825
+ end
826
+
827
+ # Unlike #balance, this balances Ripper tokens to balance something like `{ a: "}" }` correctly.
828
+ def balance_tokens(buf, start, finish, count: 0)
829
+ text = ''.dup
830
+ Ripper.lex(buf).each do |_, token, str|
831
+ text << str
832
+ case token
833
+ when start
834
+ count += 1
835
+ when finish
836
+ count -= 1
837
+ end
838
+
839
+ if count == 0
840
+ return text, buf.sub(text, '')
841
+ end
842
+ end
843
+ raise HamlSyntaxError.new(HamlError.message(:unbalanced_brackets))
784
844
  end
785
845
 
786
846
  def block_opened?