hamlit 2.11.0 → 2.13.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +30 -0
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +43 -1
  5. data/Gemfile +1 -7
  6. data/LICENSE.txt +26 -23
  7. data/README.md +8 -8
  8. data/REFERENCE.md +4 -4
  9. data/benchmark/graph/graph.key +0 -0
  10. data/benchmark/graph/graph.png +0 -0
  11. data/bin/update-haml +125 -0
  12. data/ext/hamlit/hamlit.c +0 -1
  13. data/hamlit.gemspec +1 -1
  14. data/lib/hamlit.rb +6 -4
  15. data/lib/hamlit/attribute_builder.rb +2 -2
  16. data/lib/hamlit/attribute_compiler.rb +3 -3
  17. data/lib/hamlit/compiler/children_compiler.rb +18 -4
  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/temple_line_counter.rb +31 -0
  40. data/lib/hamlit/version.rb +1 -1
  41. metadata +15 -9
  42. data/.travis.yml +0 -47
  43. data/lib/hamlit/parser/MIT-LICENSE +0 -20
  44. 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?