haml 5.2.0 → 6.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/test.yml +40 -0
  4. data/.gitignore +16 -16
  5. data/CHANGELOG.md +26 -1
  6. data/Gemfile +18 -11
  7. data/MIT-LICENSE +1 -1
  8. data/REFERENCE.md +19 -8
  9. data/Rakefile +95 -93
  10. data/bin/bench +66 -0
  11. data/bin/console +11 -0
  12. data/bin/ruby +3 -0
  13. data/bin/setup +7 -0
  14. data/bin/stackprof +27 -0
  15. data/bin/test +24 -0
  16. data/exe/haml +6 -0
  17. data/ext/haml/extconf.rb +10 -0
  18. data/ext/haml/haml.c +537 -0
  19. data/ext/haml/hescape.c +108 -0
  20. data/ext/haml/hescape.h +20 -0
  21. data/haml.gemspec +39 -37
  22. data/lib/haml/ambles.rb +20 -0
  23. data/lib/haml/attribute_builder.rb +140 -129
  24. data/lib/haml/attribute_compiler.rb +85 -192
  25. data/lib/haml/attribute_parser.rb +86 -126
  26. data/lib/haml/cli.rb +154 -0
  27. data/lib/haml/compiler/children_compiler.rb +126 -0
  28. data/lib/haml/compiler/comment_compiler.rb +39 -0
  29. data/lib/haml/compiler/doctype_compiler.rb +46 -0
  30. data/lib/haml/compiler/script_compiler.rb +116 -0
  31. data/lib/haml/compiler/silent_script_compiler.rb +24 -0
  32. data/lib/haml/compiler/tag_compiler.rb +76 -0
  33. data/lib/haml/compiler.rb +63 -296
  34. data/lib/haml/dynamic_merger.rb +67 -0
  35. data/lib/haml/engine.rb +42 -227
  36. data/lib/haml/error.rb +3 -52
  37. data/lib/haml/escapable.rb +6 -70
  38. data/lib/haml/filters/base.rb +12 -0
  39. data/lib/haml/filters/cdata.rb +20 -0
  40. data/lib/haml/filters/coffee.rb +17 -0
  41. data/lib/haml/filters/css.rb +33 -0
  42. data/lib/haml/filters/erb.rb +10 -0
  43. data/lib/haml/filters/escaped.rb +22 -0
  44. data/lib/haml/filters/javascript.rb +33 -0
  45. data/lib/haml/filters/less.rb +20 -0
  46. data/lib/haml/filters/markdown.rb +11 -0
  47. data/lib/haml/filters/plain.rb +29 -0
  48. data/lib/haml/filters/preserve.rb +22 -0
  49. data/lib/haml/filters/ruby.rb +10 -0
  50. data/lib/haml/filters/sass.rb +15 -0
  51. data/lib/haml/filters/scss.rb +15 -0
  52. data/lib/haml/filters/text_base.rb +25 -0
  53. data/lib/haml/filters/tilt_base.rb +49 -0
  54. data/lib/haml/filters.rb +54 -378
  55. data/lib/haml/force_escapable.rb +29 -0
  56. data/lib/haml/haml_error.rb +66 -0
  57. data/lib/haml/helpers.rb +3 -697
  58. data/lib/haml/html.rb +22 -0
  59. data/lib/haml/identity.rb +13 -0
  60. data/lib/haml/object_ref.rb +30 -0
  61. data/lib/haml/parser.rb +208 -50
  62. data/lib/haml/rails_helpers.rb +51 -0
  63. data/lib/haml/rails_template.rb +55 -0
  64. data/lib/haml/railtie.rb +7 -40
  65. data/lib/haml/ruby_expression.rb +32 -0
  66. data/lib/haml/string_splitter.rb +20 -0
  67. data/lib/haml/template.rb +15 -34
  68. data/lib/haml/temple_line_counter.rb +2 -1
  69. data/lib/haml/util.rb +18 -16
  70. data/lib/haml/version.rb +1 -2
  71. data/lib/haml.rb +8 -20
  72. metadata +216 -62
  73. data/.gitmodules +0 -3
  74. data/.travis.yml +0 -72
  75. data/.yardopts +0 -22
  76. data/TODO +0 -24
  77. data/benchmark.rb +0 -70
  78. data/bin/haml +0 -9
  79. data/lib/haml/.gitattributes +0 -1
  80. data/lib/haml/buffer.rb +0 -238
  81. data/lib/haml/exec.rb +0 -347
  82. data/lib/haml/generator.rb +0 -42
  83. data/lib/haml/helpers/action_view_extensions.rb +0 -60
  84. data/lib/haml/helpers/action_view_mods.rb +0 -132
  85. data/lib/haml/helpers/action_view_xss_mods.rb +0 -60
  86. data/lib/haml/helpers/safe_erubi_template.rb +0 -20
  87. data/lib/haml/helpers/safe_erubis_template.rb +0 -33
  88. data/lib/haml/helpers/xss_mods.rb +0 -114
  89. data/lib/haml/options.rb +0 -273
  90. data/lib/haml/plugin.rb +0 -37
  91. data/lib/haml/sass_rails_filter.rb +0 -47
  92. data/lib/haml/template/options.rb +0 -27
  93. data/lib/haml/temple_engine.rb +0 -123
  94. data/yard/default/.gitignore +0 -1
  95. data/yard/default/fulldoc/html/css/common.sass +0 -15
  96. data/yard/default/layout/html/footer.erb +0 -12
data/lib/haml/html.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+ module Haml
3
+ class HTML < Temple::HTML::Fast
4
+ DEPRECATED_FORMATS = %i[html4 html5].freeze
5
+
6
+ def initialize(opts = {})
7
+ if DEPRECATED_FORMATS.include?(opts[:format])
8
+ opts = opts.dup
9
+ opts[:format] = :html
10
+ end
11
+ super(opts)
12
+ end
13
+
14
+ # This dispatcher supports Haml's "revealed" conditional comment.
15
+ def on_html_condcomment(condition, content, revealed = false)
16
+ on_html_comment [:multi,
17
+ [:static, "[#{condition}]>#{'<!-->' if revealed}"],
18
+ content,
19
+ [:static, "#{'<!--' if revealed}<![endif]"]]
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+ module Haml
3
+ class Identity
4
+ def initialize
5
+ @unique_id = 0
6
+ end
7
+
8
+ def generate
9
+ @unique_id += 1
10
+ "_haml_compiler#{@unique_id}"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ module Haml
3
+ module ObjectRef
4
+ class << self
5
+ def parse(args)
6
+ object, prefix = args
7
+ return {} unless object
8
+
9
+ suffix = underscore(object.class)
10
+ {
11
+ 'class' => [prefix, suffix].compact.join('_'),
12
+ 'id' => [prefix, suffix, object.id || 'new'].compact.join('_'),
13
+ }
14
+ end
15
+
16
+ private
17
+
18
+ # Haml::Buffer.underscore
19
+ def underscore(camel_cased_word)
20
+ word = camel_cased_word.to_s.dup
21
+ word.gsub!(/::/, '_')
22
+ word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
23
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
24
+ word.tr!('-', '_')
25
+ word.downcase!
26
+ word
27
+ end
28
+ end
29
+ end
30
+ end
data/lib/haml/parser.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'ripper'
3
4
  require 'strscan'
5
+ require 'haml/haml_error'
6
+ require 'haml/util'
4
7
 
5
8
  module Haml
6
9
  class Parser
@@ -90,8 +93,11 @@ module Haml
90
93
  ID_KEY = 'id'.freeze
91
94
  CLASS_KEY = 'class'.freeze
92
95
 
96
+ # Used for scanning old attributes, substituting the first '{'
97
+ METHOD_CALL_PREFIX = 'a('
98
+
93
99
  def initialize(options)
94
- @options = Options.wrap(options)
100
+ @options = ParserOptions.new(options)
95
101
  # Record the indent levels of "if" statements to validate the subsequent
96
102
  # elsif and else statements are indented at the appropriate level.
97
103
  @script_level_stack = []
@@ -100,6 +106,10 @@ module Haml
100
106
  end
101
107
 
102
108
  def call(template)
109
+ template = Haml::Util.check_haml_encoding(template) do |msg, line|
110
+ raise Haml::Error.new(msg, line)
111
+ end
112
+
103
113
  match = template.rstrip.scan(/(([ \t]+)?(.*?))(?:\Z|\r\n|\r|\n)/m)
104
114
  # discard the last match which is always blank
105
115
  match.pop
@@ -115,7 +125,7 @@ module Haml
115
125
  @indentation = nil
116
126
  @line = next_line
117
127
 
118
- raise SyntaxError.new(Error.message(:indenting_at_start), @line.index) if @line.tabs != 0
128
+ raise HamlSyntaxError.new(HamlError.message(:indenting_at_start), @line.index) if @line.tabs != 0
119
129
 
120
130
  loop do
121
131
  next_line
@@ -138,7 +148,7 @@ module Haml
138
148
  end
139
149
 
140
150
  if !flat? && @next_line.tabs - @line.tabs > 1
141
- raise SyntaxError.new(Error.message(:deeper_indenting, @next_line.tabs - @line.tabs), @next_line.index)
151
+ raise HamlSyntaxError.new(HamlError.message(:deeper_indenting, @next_line.tabs - @line.tabs), @next_line.index)
142
152
  end
143
153
 
144
154
  @line = @next_line
@@ -146,9 +156,9 @@ module Haml
146
156
  # Close all the open tags
147
157
  close until @parent.type == :root
148
158
  @root
149
- rescue Haml::Error => e
159
+ rescue Haml::HamlError => e
150
160
  e.backtrace.unshift "#{@options.filename}:#{(e.line ? e.line + 1 : @line.index + 1) + @options.line - 1}"
151
- raise
161
+ error_with_lineno(e)
152
162
  end
153
163
 
154
164
  def compute_tabs(line)
@@ -158,7 +168,7 @@ module Haml
158
168
  @indentation = line.whitespace
159
169
 
160
170
  if @indentation.include?(?\s) && @indentation.include?(?\t)
161
- raise SyntaxError.new(Error.message(:cant_use_tabs_and_spaces), line.index)
171
+ raise HamlSyntaxError.new(HamlError.message(:cant_use_tabs_and_spaces), line.index)
162
172
  end
163
173
 
164
174
  @flat_spaces = @indentation * (@template_tabs+1) if flat?
@@ -169,15 +179,25 @@ module Haml
169
179
  return tabs if line.whitespace == @indentation * tabs
170
180
  return @template_tabs + 1 if flat? && line.whitespace =~ /^#{@flat_spaces}/
171
181
 
172
- message = Error.message(:inconsistent_indentation,
182
+ message = HamlError.message(:inconsistent_indentation,
173
183
  human_indentation(line.whitespace),
174
184
  human_indentation(@indentation)
175
185
  )
176
- raise SyntaxError.new(message, line.index)
186
+ raise HamlSyntaxError.new(message, line.index)
177
187
  end
178
188
 
179
189
  private
180
190
 
191
+ def error_with_lineno(error)
192
+ return error if error.line
193
+
194
+ trace = error.backtrace.first
195
+ return error unless trace
196
+
197
+ line = trace.match(/\d+\z/).to_s.to_i
198
+ HamlSyntaxError.new(error.message, line)
199
+ end
200
+
181
201
  # @private
182
202
  Line = Struct.new(:whitespace, :text, :full, :index, :parser, :eod) do
183
203
  alias_method :eod?, :eod
@@ -217,7 +237,7 @@ module Haml
217
237
  self[:old] = value
218
238
  end
219
239
 
220
- # This will be a literal for Haml::Buffer#attributes's last argument, `attributes_hashes`.
240
+ # This will be a literal for Haml::HamlBuffer#attributes's last argument, `attributes_hashes`.
221
241
  def to_literal
222
242
  [new, stripped_old].compact.join(', ')
223
243
  end
@@ -300,20 +320,20 @@ module Haml
300
320
 
301
321
  def plain(line, escape_html = nil)
302
322
  if block_opened?
303
- raise SyntaxError.new(Error.message(:illegal_nesting_plain), @next_line.index)
323
+ raise HamlSyntaxError.new(HamlError.message(:illegal_nesting_plain), @next_line.index)
304
324
  end
305
325
 
306
- unless contains_interpolation?(line.text)
326
+ unless Util.contains_interpolation?(line.text)
307
327
  return ParseNode.new(:plain, line.index + 1, :text => line.text)
308
328
  end
309
329
 
310
330
  escape_html = @options.escape_html && @options.mime_type != 'text/plain' if escape_html.nil?
311
- line.text = unescape_interpolation(line.text, escape_html)
312
- script(line, false)
331
+ line.text = Util.unescape_interpolation(line.text)
332
+ script(line, false).tap { |n| n.value[:escape_interpolation] = true if escape_html }
313
333
  end
314
334
 
315
335
  def script(line, escape_html = nil, preserve = false)
316
- raise SyntaxError.new(Error.message(:no_ruby_code, '=')) if line.text.empty?
336
+ raise HamlSyntaxError.new(HamlError.message(:no_ruby_code, '=')) if line.text.empty?
317
337
  line = handle_ruby_multiline(line)
318
338
  escape_html = @options.escape_html if escape_html.nil?
319
339
 
@@ -325,12 +345,12 @@ module Haml
325
345
  end
326
346
 
327
347
  def flat_script(line, escape_html = nil)
328
- raise SyntaxError.new(Error.message(:no_ruby_code, '~')) if line.text.empty?
348
+ raise HamlSyntaxError.new(HamlError.message(:no_ruby_code, '~')) if line.text.empty?
329
349
  script(line, escape_html, :preserve)
330
350
  end
331
351
 
332
352
  def silent_script(line)
333
- raise SyntaxError.new(Error.message(:no_end), line.index) if line.text[1..-1].strip == 'end'
353
+ raise HamlSyntaxError.new(HamlError.message(:no_end), line.index) if line.text[1..-1].strip == 'end'
334
354
 
335
355
  line = handle_ruby_multiline(line)
336
356
  keyword = block_keyword(line.text)
@@ -339,7 +359,7 @@ module Haml
339
359
 
340
360
  if ["else", "elsif", "when"].include?(keyword)
341
361
  if @script_level_stack.empty?
342
- raise Haml::SyntaxError.new(Error.message(:missing_if, keyword), @line.index)
362
+ raise Haml::HamlSyntaxError.new(HamlError.message(:missing_if, keyword), @line.index)
343
363
  end
344
364
 
345
365
  if keyword == 'when' and !@script_level_stack.last[2]
@@ -350,8 +370,8 @@ module Haml
350
370
  end
351
371
 
352
372
  if @script_level_stack.last[1] != @line.tabs
353
- message = Error.message(:bad_script_indent, keyword, @script_level_stack.last[1], @line.tabs)
354
- raise Haml::SyntaxError.new(message, @line.index)
373
+ message = HamlError.message(:bad_script_indent, keyword, @script_level_stack.last[1], @line.tabs)
374
+ raise Haml::HamlSyntaxError.new(message, @line.index)
355
375
  end
356
376
  end
357
377
 
@@ -396,7 +416,8 @@ module Haml
396
416
  when '='
397
417
  parse = true
398
418
  if value[0] == ?=
399
- value = unescape_interpolation(value[1..-1].strip, escape_html)
419
+ value = Util.unescape_interpolation(value[1..-1].strip)
420
+ escape_interpolation = true if escape_html
400
421
  escape_html = false
401
422
  end
402
423
  when '&', '!'
@@ -404,19 +425,21 @@ module Haml
404
425
  parse = true
405
426
  preserve_script = (value[0] == ?~)
406
427
  if value[1] == ?=
407
- value = unescape_interpolation(value[2..-1].strip, escape_html)
428
+ value = Util.unescape_interpolation(value[2..-1].strip)
429
+ escape_interpolation = true if escape_html
408
430
  escape_html = false
409
431
  else
410
432
  value = value[1..-1].strip
411
433
  end
412
- elsif contains_interpolation?(value)
413
- value = unescape_interpolation(value, escape_html)
434
+ elsif Util.contains_interpolation?(value)
435
+ value = Util.unescape_interpolation(value)
436
+ escape_interpolation = true if escape_html
414
437
  parse = true
415
438
  escape_html = false
416
439
  end
417
440
  else
418
- if contains_interpolation?(value)
419
- value = unescape_interpolation(value, escape_html)
441
+ if Util.contains_interpolation?(value)
442
+ value = Util.unescape_interpolation(value, escape_html)
420
443
  parse = true
421
444
  escape_html = false
422
445
  end
@@ -427,22 +450,22 @@ module Haml
427
450
 
428
451
  if attributes_hashes[:new]
429
452
  static_attributes, attributes_hash = attributes_hashes[:new]
430
- AttributeBuilder.merge_attributes!(attributes, static_attributes) if static_attributes
453
+ AttributeMerger.merge_attributes!(attributes, static_attributes) if static_attributes
431
454
  dynamic_attributes.new = attributes_hash
432
455
  end
433
456
 
434
457
  if attributes_hashes[:old]
435
458
  static_attributes = parse_static_hash(attributes_hashes[:old])
436
- AttributeBuilder.merge_attributes!(attributes, static_attributes) if static_attributes
459
+ AttributeMerger.merge_attributes!(attributes, static_attributes) if static_attributes
437
460
  dynamic_attributes.old = attributes_hashes[:old] unless static_attributes || @options.suppress_eval
438
461
  end
439
462
 
440
- raise SyntaxError.new(Error.message(:illegal_nesting_self_closing), @next_line.index) if block_opened? && self_closing
441
- raise SyntaxError.new(Error.message(:no_ruby_code, action), last_line - 1) if parse && value.empty?
442
- raise SyntaxError.new(Error.message(:self_closing_content), last_line - 1) if self_closing && !value.empty?
463
+ raise HamlSyntaxError.new(HamlError.message(:illegal_nesting_self_closing), @next_line.index) if block_opened? && self_closing
464
+ raise HamlSyntaxError.new(HamlError.message(:no_ruby_code, action), last_line - 1) if parse && value.empty?
465
+ raise HamlSyntaxError.new(HamlError.message(:self_closing_content), last_line - 1) if self_closing && !value.empty?
443
466
 
444
467
  if block_opened? && !value.empty? && !is_ruby_multiline?(value)
445
- raise SyntaxError.new(Error.message(:illegal_nesting_line, tag_name), @next_line.index)
468
+ raise HamlSyntaxError.new(HamlError.message(:illegal_nesting_line, tag_name), @next_line.index)
446
469
  end
447
470
 
448
471
  self_closing ||= !!(!block_opened? && value.empty? && @options.autoclose.any? {|t| t === tag_name})
@@ -455,7 +478,8 @@ module Haml
455
478
  :nuke_inner_whitespace => nuke_inner_whitespace,
456
479
  :nuke_outer_whitespace => nuke_outer_whitespace, :object_ref => object_ref,
457
480
  :escape_html => escape_html, :preserve_tag => preserve_tag,
458
- :preserve_script => preserve_script, :parse => parse, :value => line.text)
481
+ :preserve_script => preserve_script, :parse => parse, :value => line.text,
482
+ :escape_interpolation => escape_interpolation)
459
483
  end
460
484
 
461
485
  # Renders a line that creates an XHTML tag and has an implicit div because of
@@ -477,15 +501,15 @@ module Haml
477
501
  conditional, text = balance(text, ?[, ?]) if text[0] == ?[
478
502
  text.strip!
479
503
 
480
- if contains_interpolation?(text)
504
+ if Util.contains_interpolation?(text)
481
505
  parse = true
482
- text = unescape_interpolation(text)
506
+ text = Util.unescape_interpolation(text)
483
507
  else
484
508
  parse = false
485
509
  end
486
510
 
487
511
  if block_opened? && !text.empty?
488
- raise SyntaxError.new(Haml::Error.message(:illegal_nesting_content), @next_line.index)
512
+ raise HamlSyntaxError.new(Haml::HamlError.message(:illegal_nesting_content), @next_line.index)
489
513
  end
490
514
 
491
515
  ParseNode.new(:comment, @line.index + 1, :conditional => conditional, :text => text, :revealed => revealed, :parse => parse)
@@ -493,13 +517,13 @@ module Haml
493
517
 
494
518
  # Renders an XHTML doctype or XML shebang.
495
519
  def doctype(text)
496
- raise SyntaxError.new(Error.message(:illegal_nesting_header), @next_line.index) if block_opened?
520
+ raise HamlSyntaxError.new(HamlError.message(:illegal_nesting_header), @next_line.index) if block_opened?
497
521
  version, type, encoding = text[3..-1].strip.downcase.scan(DOCTYPE_REGEX)[0]
498
522
  ParseNode.new(:doctype, @line.index + 1, :version => version, :type => type, :encoding => encoding)
499
523
  end
500
524
 
501
525
  def filter(name)
502
- raise Error.new(Error.message(:invalid_filter_name, name)) unless name =~ /^\w+$/
526
+ raise HamlError.new(HamlError.message(:invalid_filter_name, name)) unless name =~ /^\w+$/
503
527
 
504
528
  if filter_opened?
505
529
  @flat = true
@@ -548,7 +572,7 @@ module Haml
548
572
 
549
573
  alias :close_script :close_silent_script
550
574
 
551
- # This is a class method so it can be accessed from {Haml::Helpers}.
575
+ # This is a class method so it can be accessed from {Haml::HamlHelpers}.
552
576
  #
553
577
  # Iterates through the classes and ids supplied through `.`
554
578
  # and `#` syntax, and returns a hash with them as attributes,
@@ -572,8 +596,8 @@ module Haml
572
596
  attributes
573
597
  end
574
598
 
575
- # This method doesn't use Haml::AttributeParser because currently it depends on Ripper and Rubinius doesn't provide it.
576
- # Ideally this logic should be placed in Haml::AttributeParser instead of here and this method should use it.
599
+ # This method doesn't use Haml::HamlAttributeParser because currently it depends on Ripper and Rubinius doesn't provide it.
600
+ # Ideally this logic should be placed in Haml::HamlAttributeParser instead of here and this method should use it.
577
601
  #
578
602
  # @param [String] text - Hash literal or text inside old attributes
579
603
  # @return [Hash,nil] - Return nil if text is not static Hash literal
@@ -597,12 +621,12 @@ module Haml
597
621
  # Parses a line into tag_name, attributes, attributes_hash, object_ref, action, value
598
622
  def parse_tag(text)
599
623
  match = text.scan(/%([-:\w]+)([-:\w.#\@]*)(.+)?/)[0]
600
- raise SyntaxError.new(Error.message(:invalid_tag, text)) unless match
624
+ raise HamlSyntaxError.new(HamlError.message(:invalid_tag, text)) unless match
601
625
 
602
626
  tag_name, attributes, rest = match
603
627
 
604
628
  if !attributes.empty? && (attributes =~ /[.#](\.|#|\z)/)
605
- raise SyntaxError.new(Error.message(:illegal_element))
629
+ raise HamlSyntaxError.new(HamlError.message(:illegal_element))
606
630
  end
607
631
 
608
632
  new_attributes_hash = old_attributes_hash = last_line = nil
@@ -651,13 +675,18 @@ module Haml
651
675
  # @return [String] rest
652
676
  # @return [Integer] last_line
653
677
  def parse_old_attributes(text)
654
- text = text.dup
655
678
  last_line = @line.index + 1
656
679
 
657
680
  begin
658
- attributes_hash, rest = balance(text, ?{, ?})
659
- rescue SyntaxError => e
660
- if text.strip[-1] == ?, && e.message == Error.message(:unbalanced_brackets)
681
+ # Old attributes often look like a valid Hash literal, but it sometimes allow code like
682
+ # `{ hash, foo: bar }`, which is compiled to `_hamlout.attributes({}, nil, hash, foo: bar)`.
683
+ #
684
+ # To scan such code correctly, this scans `a( hash, foo: bar }` instead, stops when there is
685
+ # 1 more :on_embexpr_end (the last '}') than :on_embexpr_beg, and resurrects '{' afterwards.
686
+ balanced, rest = balance_tokens(text.sub(?{, METHOD_CALL_PREFIX), :on_embexpr_beg, :on_embexpr_end, count: 1)
687
+ attributes_hash = balanced.sub(METHOD_CALL_PREFIX, ?{)
688
+ rescue HamlSyntaxError => e
689
+ if e.message == HamlError.message(:unbalanced_brackets) && !@template.empty?
661
690
  text << "\n#{@next_line.text}"
662
691
  last_line += 1
663
692
  next_line
@@ -686,7 +715,7 @@ module Haml
686
715
  if name == false
687
716
  scanned = Haml::Util.balance(text, ?(, ?))
688
717
  text = scanned ? scanned.first : text
689
- raise Haml::SyntaxError.new(Error.message(:invalid_attribute_list, text.inspect), last_line - 1)
718
+ raise Haml::HamlSyntaxError.new(HamlError.message(:invalid_attribute_list, text.inspect), last_line - 1)
690
719
  end
691
720
  attributes[name] = value
692
721
  scanner.scan(/\s*/)
@@ -705,7 +734,7 @@ module Haml
705
734
  if type == :static
706
735
  static_attributes[name] = val
707
736
  else
708
- dynamic_attributes << "#{inspect_obj(name)} => #{val},"
737
+ dynamic_attributes << "#{Util.inspect_obj(name)} => #{val},"
709
738
  end
710
739
  end
711
740
  dynamic_attributes << "}"
@@ -740,7 +769,7 @@ module Haml
740
769
 
741
770
  return name, [:static, content.first[1]] if content.size == 1
742
771
  return name, [:dynamic,
743
- %!"#{content.each_with_object(''.dup) {|(t, v), s| s << (t == :str ? inspect_obj(v)[1...-1] : "\#{#{v}}")}}"!]
772
+ %!"#{content.each_with_object(''.dup) {|(t, v), s| s << (t == :str ? Util.inspect_obj(v)[1...-1] : "\#{#{v}}")}}"!]
744
773
  end
745
774
 
746
775
  def next_line
@@ -808,7 +837,26 @@ module Haml
808
837
  end
809
838
 
810
839
  def balance(*args)
811
- Haml::Util.balance(*args) or raise(SyntaxError.new(Error.message(:unbalanced_brackets)))
840
+ Haml::Util.balance(*args) or raise(HamlSyntaxError.new(HamlError.message(:unbalanced_brackets)))
841
+ end
842
+
843
+ # Unlike #balance, this balances Ripper tokens to balance something like `{ a: "}" }` correctly.
844
+ def balance_tokens(buf, start, finish, count: 0)
845
+ text = ''.dup
846
+ Ripper.lex(buf).each do |_, token, str|
847
+ text << str
848
+ case token
849
+ when start
850
+ count += 1
851
+ when finish
852
+ count -= 1
853
+ end
854
+
855
+ if count == 0
856
+ return text, buf.sub(text, '')
857
+ end
858
+ end
859
+ raise HamlSyntaxError.new(HamlError.message(:unbalanced_brackets))
812
860
  end
813
861
 
814
862
  def block_opened?
@@ -824,5 +872,115 @@ module Haml
824
872
  def flat?
825
873
  @flat
826
874
  end
875
+
876
+ class << AttributeMerger = Object.new
877
+ # Merges two attribute hashes.
878
+ # This is the same as `to.merge!(from)`,
879
+ # except that it merges id, class, and data attributes.
880
+ #
881
+ # ids are concatenated with `"_"`,
882
+ # and classes are concatenated with `" "`.
883
+ # data hashes are simply merged.
884
+ #
885
+ # Destructively modifies `to`.
886
+ #
887
+ # @param to [{String => String,Hash}] The attribute hash to merge into
888
+ # @param from [{String => Object}] The attribute hash to merge from
889
+ # @return [{String => String,Hash}] `to`, after being merged
890
+ def merge_attributes!(to, from)
891
+ from.keys.each do |key|
892
+ to[key] = merge_value(key, to[key], from[key])
893
+ end
894
+ to
895
+ end
896
+
897
+ private
898
+
899
+ # @return [String, nil]
900
+ def filter_and_join(value, separator)
901
+ return '' if (value.respond_to?(:empty?) && value.empty?)
902
+
903
+ if value.is_a?(Array)
904
+ value = value.flatten
905
+ value.map! {|item| item ? item.to_s : nil}
906
+ value.compact!
907
+ value = value.join(separator)
908
+ else
909
+ value = value ? value.to_s : nil
910
+ end
911
+ !value.nil? && !value.empty? && value
912
+ end
913
+
914
+ # Merge a couple of values to one attribute value. No destructive operation.
915
+ #
916
+ # @param to [String,Hash,nil]
917
+ # @param from [Object]
918
+ # @return [String,Hash]
919
+ def merge_value(key, to, from)
920
+ if from.kind_of?(Hash) || to.kind_of?(Hash)
921
+ from = { nil => from } if !from.is_a?(Hash)
922
+ to = { nil => to } if !to.is_a?(Hash)
923
+ to.merge(from)
924
+ elsif key == 'id'
925
+ merged_id = filter_and_join(from, '_')
926
+ if to && merged_id
927
+ merged_id = "#{to}_#{merged_id}"
928
+ elsif to || merged_id
929
+ merged_id ||= to
930
+ end
931
+ merged_id
932
+ elsif key == 'class'
933
+ merged_class = filter_and_join(from, ' ')
934
+ if to && merged_class
935
+ merged_class = (to.split(' ') | merged_class.split(' ')).join(' ')
936
+ elsif to || merged_class
937
+ merged_class ||= to
938
+ end
939
+ merged_class
940
+ else
941
+ from
942
+ end
943
+ end
944
+ end
945
+ private_constant :AttributeMerger
946
+
947
+ class ParserOptions
948
+ # A list of options that are actually used in the parser
949
+ AVAILABLE_OPTIONS = %i[
950
+ autoclose
951
+ escape_html
952
+ filename
953
+ line
954
+ mime_type
955
+ preserve
956
+ remove_whitespace
957
+ suppress_eval
958
+ ].each do |option|
959
+ attr_reader option
960
+ end
961
+
962
+ DEFAULTS = {
963
+ autoclose: %w(area base basefont br col command embed frame
964
+ hr img input isindex keygen link menuitem meta
965
+ param source track wbr),
966
+ escape_html: false,
967
+ filename: '(haml)',
968
+ line: 1,
969
+ mime_type: 'text/html',
970
+ preserve: %w(textarea pre code),
971
+ remove_whitespace: false,
972
+ suppress_eval: false,
973
+ }
974
+
975
+ def initialize(values = {})
976
+ DEFAULTS.each {|k, v| instance_variable_set :"@#{k}", v}
977
+ AVAILABLE_OPTIONS.each do |key|
978
+ if values.key?(key)
979
+ instance_variable_set :"@#{key}", values[key]
980
+ end
981
+ end
982
+ end
983
+ end
984
+ private_constant :ParserOptions
827
985
  end
828
986
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: false
2
+ require 'haml/helpers'
3
+
4
+ # Currently this Haml::Helpers depends on
5
+ # ActionView internal implementation. (not desired)
6
+ module Haml
7
+ module RailsHelpers
8
+ include Helpers
9
+ extend self
10
+
11
+ DEFAULT_PRESERVE_TAGS = %w[textarea pre code].freeze
12
+
13
+ def find_and_preserve(input = nil, tags = DEFAULT_PRESERVE_TAGS, &block)
14
+ return find_and_preserve(capture_haml(&block), input || tags) if block
15
+
16
+ tags = tags.each_with_object('') do |t, s|
17
+ s << '|' unless s.empty?
18
+ s << Regexp.escape(t)
19
+ end
20
+
21
+ re = /<(#{tags})([^>]*)>(.*?)(<\/\1>)/im
22
+ input.to_s.gsub(re) do |s|
23
+ s =~ re # Can't rely on $1, etc. existing since Rails' SafeBuffer#gsub is incompatible
24
+ "<#{$1}#{$2}>#{preserve($3)}</#{$1}>"
25
+ end
26
+ end
27
+
28
+ def preserve(input = nil, &block)
29
+ return preserve(capture_haml(&block)) if block
30
+ super.html_safe
31
+ end
32
+
33
+ def surround(front, back = front, &block)
34
+ output = capture_haml(&block)
35
+
36
+ "#{escape_once(front)}#{output.chomp}#{escape_once(back)}\n".html_safe
37
+ end
38
+
39
+ def precede(str, &block)
40
+ "#{escape_once(str)}#{capture_haml(&block).chomp}\n".html_safe
41
+ end
42
+
43
+ def succeed(str, &block)
44
+ "#{capture_haml(&block).chomp}#{escape_once(str)}\n".html_safe
45
+ end
46
+
47
+ def capture_haml(*args, &block)
48
+ capture(*args, &block)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+ require 'temple'
3
+ require 'haml/engine'
4
+ require 'haml/rails_helpers'
5
+ require 'haml/util'
6
+
7
+ module Haml
8
+ class RailsTemplate
9
+ # Compatible with: https://github.com/judofyr/temple/blob/v0.7.7/lib/temple/mixins/options.rb#L15-L24
10
+ class << self
11
+ def options
12
+ @options ||= {
13
+ generator: Temple::Generators::RailsOutputBuffer,
14
+ use_html_safe: true,
15
+ streaming: true,
16
+ buffer_class: 'ActionView::OutputBuffer',
17
+ disable_capture: true,
18
+ }
19
+ end
20
+
21
+ def set_options(opts)
22
+ options.update(opts)
23
+ end
24
+ end
25
+
26
+ def call(template, source = nil)
27
+ source ||= template.source
28
+ options = RailsTemplate.options
29
+
30
+ # https://github.com/haml/haml/blob/4.0.7/lib/haml/template/plugin.rb#L19-L20
31
+ # https://github.com/haml/haml/blob/4.0.7/lib/haml/options.rb#L228
32
+ if template.respond_to?(:type) && template.type == 'text/xml'
33
+ options = options.merge(format: :xhtml)
34
+ end
35
+
36
+ if ActionView::Base.try(:annotate_rendered_view_with_filenames) && template.format == :html
37
+ options = options.merge(
38
+ preamble: "<!-- BEGIN #{template.short_identifier} -->\n",
39
+ postamble: "<!-- END #{template.short_identifier} -->\n",
40
+ )
41
+ end
42
+
43
+ Engine.new(options).call(source)
44
+ end
45
+
46
+ def supports_streaming?
47
+ RailsTemplate.options[:streaming]
48
+ end
49
+ end
50
+ ActionView::Template.register_template_handler(:haml, RailsTemplate.new)
51
+ end
52
+
53
+ # Haml extends Haml::Helpers in ActionView each time.
54
+ # It costs much, so Haml includes a compatible module at first.
55
+ ActionView::Base.send :include, Haml::RailsHelpers