haml 5.0.4 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 -15
  5. data/CHANGELOG.md +62 -1
  6. data/Gemfile +18 -14
  7. data/MIT-LICENSE +2 -2
  8. data/README.md +4 -5
  9. data/REFERENCE.md +46 -12
  10. data/Rakefile +93 -103
  11. data/bin/bench +66 -0
  12. data/bin/console +11 -0
  13. data/bin/ruby +3 -0
  14. data/bin/setup +7 -0
  15. data/bin/stackprof +27 -0
  16. data/bin/test +24 -0
  17. data/exe/haml +6 -0
  18. data/ext/haml/extconf.rb +10 -0
  19. data/ext/haml/haml.c +537 -0
  20. data/ext/haml/hescape.c +108 -0
  21. data/ext/haml/hescape.h +20 -0
  22. data/haml.gemspec +39 -30
  23. data/lib/haml/ambles.rb +20 -0
  24. data/lib/haml/attribute_builder.rb +140 -128
  25. data/lib/haml/attribute_compiler.rb +86 -181
  26. data/lib/haml/attribute_parser.rb +86 -124
  27. data/lib/haml/cli.rb +154 -0
  28. data/lib/haml/compiler/children_compiler.rb +126 -0
  29. data/lib/haml/compiler/comment_compiler.rb +39 -0
  30. data/lib/haml/compiler/doctype_compiler.rb +46 -0
  31. data/lib/haml/compiler/script_compiler.rb +116 -0
  32. data/lib/haml/compiler/silent_script_compiler.rb +24 -0
  33. data/lib/haml/compiler/tag_compiler.rb +76 -0
  34. data/lib/haml/compiler.rb +64 -298
  35. data/lib/haml/dynamic_merger.rb +67 -0
  36. data/lib/haml/engine.rb +43 -219
  37. data/lib/haml/error.rb +29 -27
  38. data/lib/haml/escapable.rb +6 -42
  39. data/lib/haml/filters/base.rb +12 -0
  40. data/lib/haml/filters/cdata.rb +20 -0
  41. data/lib/haml/filters/coffee.rb +17 -0
  42. data/lib/haml/filters/css.rb +33 -0
  43. data/lib/haml/filters/erb.rb +10 -0
  44. data/lib/haml/filters/escaped.rb +22 -0
  45. data/lib/haml/filters/javascript.rb +33 -0
  46. data/lib/haml/filters/less.rb +20 -0
  47. data/lib/haml/filters/markdown.rb +11 -0
  48. data/lib/haml/filters/plain.rb +29 -0
  49. data/lib/haml/filters/preserve.rb +22 -0
  50. data/lib/haml/filters/ruby.rb +10 -0
  51. data/lib/haml/filters/sass.rb +15 -0
  52. data/lib/haml/filters/scss.rb +15 -0
  53. data/lib/haml/filters/text_base.rb +25 -0
  54. data/lib/haml/filters/tilt_base.rb +49 -0
  55. data/lib/haml/filters.rb +55 -378
  56. data/lib/haml/force_escapable.rb +29 -0
  57. data/lib/haml/helpers.rb +4 -696
  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 -43
  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 -33
  68. data/lib/haml/temple_line_counter.rb +2 -0
  69. data/lib/haml/util.rb +23 -21
  70. data/lib/haml/version.rb +1 -1
  71. data/lib/haml.rb +8 -19
  72. metadata +222 -50
  73. data/.gitmodules +0 -3
  74. data/.travis.yml +0 -54
  75. data/.yardopts +0 -23
  76. data/TODO +0 -24
  77. data/benchmark.rb +0 -66
  78. data/bin/haml +0 -9
  79. data/lib/haml/.gitattributes +0 -1
  80. data/lib/haml/buffer.rb +0 -235
  81. data/lib/haml/exec.rb +0 -348
  82. data/lib/haml/generator.rb +0 -41
  83. data/lib/haml/helpers/action_view_extensions.rb +0 -59
  84. data/lib/haml/helpers/action_view_mods.rb +0 -129
  85. data/lib/haml/helpers/action_view_xss_mods.rb +0 -59
  86. data/lib/haml/helpers/safe_erubi_template.rb +0 -19
  87. data/lib/haml/helpers/safe_erubis_template.rb +0 -32
  88. data/lib/haml/helpers/xss_mods.rb +0 -110
  89. data/lib/haml/options.rb +0 -273
  90. data/lib/haml/plugin.rb +0 -34
  91. data/lib/haml/sass_rails_filter.rb +0 -46
  92. data/lib/haml/template/options.rb +0 -26
  93. data/lib/haml/temple_engine.rb +0 -121
  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,5 +1,9 @@
1
- # frozen_string_literal: false
1
+ # frozen_string_literal: true
2
+
3
+ require 'ripper'
2
4
  require 'strscan'
5
+ require 'haml/error'
6
+ require 'haml/util'
3
7
 
4
8
  module Haml
5
9
  class Parser
@@ -60,7 +64,7 @@ module Haml
60
64
  SILENT_SCRIPT,
61
65
  ESCAPE,
62
66
  FILTER
63
- ]
67
+ ].freeze
64
68
 
65
69
  # The value of the character that designates that a line is part
66
70
  # of a multiline string.
@@ -74,8 +78,8 @@ module Haml
74
78
  #
75
79
  BLOCK_WITH_SPACES = /do\s*\|\s*[^\|]*\s+\|\z/
76
80
 
77
- MID_BLOCK_KEYWORDS = %w[else elsif rescue ensure end when]
78
- START_BLOCK_KEYWORDS = %w[if begin case unless]
81
+ MID_BLOCK_KEYWORDS = %w[else elsif rescue ensure end when].freeze
82
+ START_BLOCK_KEYWORDS = %w[if begin case unless].freeze
79
83
  # Try to parse assignments to block starters as best as possible
80
84
  START_BLOCK_KEYWORD_REGEX = /(?:\w+(?:,\s*\w+)*\s*=\s*)?(#{START_BLOCK_KEYWORDS.join('|')})/
81
85
  BLOCK_KEYWORD_REGEX = /^-?\s*(?:(#{MID_BLOCK_KEYWORDS.join('|')})|#{START_BLOCK_KEYWORD_REGEX.source})\b/
@@ -89,16 +93,27 @@ module Haml
89
93
  ID_KEY = 'id'.freeze
90
94
  CLASS_KEY = 'class'.freeze
91
95
 
96
+ # Used for scanning old attributes, substituting the first '{'
97
+ METHOD_CALL_PREFIX = 'a('
98
+
92
99
  def initialize(options)
93
- @options = Options.wrap(options)
100
+ @options = ParserOptions.new(options)
94
101
  # Record the indent levels of "if" statements to validate the subsequent
95
102
  # elsif and else statements are indented at the appropriate level.
96
103
  @script_level_stack = []
97
104
  @template_index = 0
98
105
  @template_tabs = 0
106
+ # When used in Haml::Engine, which gives options[:generator] to every filter
107
+ # in the engine, including Haml::Parser, we don't want to throw exceptions.
108
+ # However, when Haml::Parser is used as a library, we want to throw exceptions.
109
+ @raise_error = !options.key?(:generator)
99
110
  end
100
111
 
101
112
  def call(template)
113
+ template = Haml::Util.check_haml_encoding(template) do |msg, line|
114
+ raise Haml::Error.new(msg, line)
115
+ end
116
+
102
117
  match = template.rstrip.scan(/(([ \t]+)?(.*?))(?:\Z|\r\n|\r|\n)/m)
103
118
  # discard the last match which is always blank
104
119
  match.pop
@@ -147,7 +162,8 @@ module Haml
147
162
  @root
148
163
  rescue Haml::Error => e
149
164
  e.backtrace.unshift "#{@options.filename}:#{(e.line ? e.line + 1 : @line.index + 1) + @options.line - 1}"
150
- raise
165
+ raise if @raise_error
166
+ error_with_lineno(e)
151
167
  end
152
168
 
153
169
  def compute_tabs(line)
@@ -177,8 +193,18 @@ module Haml
177
193
 
178
194
  private
179
195
 
196
+ def error_with_lineno(error)
197
+ return error if error.line
198
+
199
+ trace = error.backtrace.first
200
+ return error unless trace
201
+
202
+ line = trace.match(/\d+\z/).to_s.to_i
203
+ SyntaxError.new(error.message, line)
204
+ end
205
+
180
206
  # @private
181
- class Line < Struct.new(:whitespace, :text, :full, :index, :parser, :eod)
207
+ Line = Struct.new(:whitespace, :text, :full, :index, :parser, :eod) do
182
208
  alias_method :eod?, :eod
183
209
 
184
210
  # @private
@@ -194,28 +220,29 @@ module Haml
194
220
  end
195
221
 
196
222
  # @private
197
- class ParseNode < Struct.new(:type, :line, :value, :parent, :children)
223
+ ParseNode = Struct.new(:type, :line, :value, :parent, :children) do
198
224
  def initialize(*args)
199
225
  super
200
226
  self.children ||= []
201
227
  end
202
228
 
203
229
  def inspect
204
- %Q[(#{type} #{value.inspect}#{children.each_with_object('') {|c, s| s << "\n#{c.inspect.gsub!(/^/, ' ')}"}})]
230
+ %Q[(#{type} #{value.inspect}#{children.each_with_object(''.dup) {|c, s| s << "\n#{c.inspect.gsub!(/^/, ' ')}"}})].dup
205
231
  end
206
232
  end
207
233
 
208
234
  # @param [String] new - Hash literal including dynamic values.
209
235
  # @param [String] old - Hash literal including dynamic values or Ruby literal of multiple Hashes which MUST be interpreted as method's last arguments.
210
- class DynamicAttributes < Struct.new(:new, :old)
236
+ DynamicAttributes = Struct.new(:new, :old) do
237
+ undef :old=
211
238
  def old=(value)
212
239
  unless value =~ /\A{.*}\z/m
213
240
  raise ArgumentError.new('Old attributes must start with "{" and end with "}"')
214
241
  end
215
- super
242
+ self[:old] = value
216
243
  end
217
244
 
218
- # This will be a literal for Haml::Buffer#attributes's last argument, `attributes_hashes`.
245
+ # This will be a literal for Haml::HamlBuffer#attributes's last argument, `attributes_hashes`.
219
246
  def to_literal
220
247
  [new, stripped_old].compact.join(', ')
221
248
  end
@@ -287,7 +314,7 @@ module Haml
287
314
  end
288
315
 
289
316
  def block_keyword(text)
290
- return unless keyword = text.scan(BLOCK_KEYWORD_REGEX)[0]
317
+ return unless (keyword = text.scan(BLOCK_KEYWORD_REGEX)[0])
291
318
  keyword[0] || keyword[1]
292
319
  end
293
320
 
@@ -301,13 +328,13 @@ module Haml
301
328
  raise SyntaxError.new(Error.message(:illegal_nesting_plain), @next_line.index)
302
329
  end
303
330
 
304
- unless contains_interpolation?(line.text)
331
+ unless Util.contains_interpolation?(line.text)
305
332
  return ParseNode.new(:plain, line.index + 1, :text => line.text)
306
333
  end
307
334
 
308
- escape_html = @options.escape_html if escape_html.nil?
309
- line.text = unescape_interpolation(line.text, escape_html)
310
- script(line, false)
335
+ escape_html = @options.escape_html && @options.mime_type != 'text/plain' if escape_html.nil?
336
+ line.text = Util.unescape_interpolation(line.text)
337
+ script(line, false).tap { |n| n.value[:escape_interpolation] = true if escape_html }
311
338
  end
312
339
 
313
340
  def script(line, escape_html = nil, preserve = false)
@@ -394,7 +421,8 @@ module Haml
394
421
  when '='
395
422
  parse = true
396
423
  if value[0] == ?=
397
- value = unescape_interpolation(value[1..-1].strip, escape_html)
424
+ value = Util.unescape_interpolation(value[1..-1].strip)
425
+ escape_interpolation = true if escape_html
398
426
  escape_html = false
399
427
  end
400
428
  when '&', '!'
@@ -402,19 +430,21 @@ module Haml
402
430
  parse = true
403
431
  preserve_script = (value[0] == ?~)
404
432
  if value[1] == ?=
405
- value = unescape_interpolation(value[2..-1].strip, escape_html)
433
+ value = Util.unescape_interpolation(value[2..-1].strip)
434
+ escape_interpolation = true if escape_html
406
435
  escape_html = false
407
436
  else
408
437
  value = value[1..-1].strip
409
438
  end
410
- elsif contains_interpolation?(value)
411
- value = unescape_interpolation(value, escape_html)
439
+ elsif Util.contains_interpolation?(value)
440
+ value = Util.unescape_interpolation(value)
441
+ escape_interpolation = true if escape_html
412
442
  parse = true
413
443
  escape_html = false
414
444
  end
415
445
  else
416
- if contains_interpolation?(value)
417
- value = unescape_interpolation(value, escape_html)
446
+ if Util.contains_interpolation?(value)
447
+ value = Util.unescape_interpolation(value, escape_html)
418
448
  parse = true
419
449
  escape_html = false
420
450
  end
@@ -425,13 +455,13 @@ module Haml
425
455
 
426
456
  if attributes_hashes[:new]
427
457
  static_attributes, attributes_hash = attributes_hashes[:new]
428
- AttributeBuilder.merge_attributes!(attributes, static_attributes) if static_attributes
458
+ AttributeMerger.merge_attributes!(attributes, static_attributes) if static_attributes
429
459
  dynamic_attributes.new = attributes_hash
430
460
  end
431
461
 
432
462
  if attributes_hashes[:old]
433
463
  static_attributes = parse_static_hash(attributes_hashes[:old])
434
- AttributeBuilder.merge_attributes!(attributes, static_attributes) if static_attributes
464
+ AttributeMerger.merge_attributes!(attributes, static_attributes) if static_attributes
435
465
  dynamic_attributes.old = attributes_hashes[:old] unless static_attributes || @options.suppress_eval
436
466
  end
437
467
 
@@ -453,7 +483,8 @@ module Haml
453
483
  :nuke_inner_whitespace => nuke_inner_whitespace,
454
484
  :nuke_outer_whitespace => nuke_outer_whitespace, :object_ref => object_ref,
455
485
  :escape_html => escape_html, :preserve_tag => preserve_tag,
456
- :preserve_script => preserve_script, :parse => parse, :value => line.text)
486
+ :preserve_script => preserve_script, :parse => parse, :value => line.text,
487
+ :escape_interpolation => escape_interpolation)
457
488
  end
458
489
 
459
490
  # Renders a line that creates an XHTML tag and has an implicit div because of
@@ -475,9 +506,9 @@ module Haml
475
506
  conditional, text = balance(text, ?[, ?]) if text[0] == ?[
476
507
  text.strip!
477
508
 
478
- if contains_interpolation?(text)
509
+ if Util.contains_interpolation?(text)
479
510
  parse = true
480
- text = unescape_interpolation(text)
511
+ text = Util.unescape_interpolation(text)
481
512
  else
482
513
  parse = false
483
514
  end
@@ -534,7 +565,7 @@ module Haml
534
565
 
535
566
  # Post-process case statements to normalize the nesting of "when" clauses
536
567
  return unless node.value[:keyword] == "case"
537
- return unless first = node.children.first
568
+ return unless (first = node.children.first)
538
569
  return unless first.type == :silent_script && first.value[:keyword] == "when"
539
570
  return if first.children.empty?
540
571
  # If the case node has a "when" child with children, it's the
@@ -546,7 +577,7 @@ module Haml
546
577
 
547
578
  alias :close_script :close_silent_script
548
579
 
549
- # This is a class method so it can be accessed from {Haml::Helpers}.
580
+ # This is a class method so it can be accessed from {Haml::HamlHelpers}.
550
581
  #
551
582
  # Iterates through the classes and ids supplied through `.`
552
583
  # and `#` syntax, and returns a hash with them as attributes,
@@ -570,8 +601,8 @@ module Haml
570
601
  attributes
571
602
  end
572
603
 
573
- # This method doesn't use Haml::AttributeParser because currently it depends on Ripper and Rubinius doesn't provide it.
574
- # Ideally this logic should be placed in Haml::AttributeParser instead of here and this method should use it.
604
+ # This method doesn't use Haml::HamlAttributeParser because currently it depends on Ripper and Rubinius doesn't provide it.
605
+ # Ideally this logic should be placed in Haml::HamlAttributeParser instead of here and this method should use it.
575
606
  #
576
607
  # @param [String] text - Hash literal or text inside old attributes
577
608
  # @return [Hash,nil] - Return nil if text is not static Hash literal
@@ -583,9 +614,9 @@ module Haml
583
614
  scanner = StringScanner.new(text)
584
615
  scanner.scan(/\s+/)
585
616
  until scanner.eos?
586
- return unless key = scanner.scan(LITERAL_VALUE_REGEX)
617
+ return unless (key = scanner.scan(LITERAL_VALUE_REGEX))
587
618
  return unless scanner.scan(/\s*=>\s*/)
588
- return unless value = scanner.scan(LITERAL_VALUE_REGEX)
619
+ return unless (value = scanner.scan(LITERAL_VALUE_REGEX))
589
620
  return unless scanner.scan(/\s*(?:,|$)\s*/)
590
621
  attributes[eval(key).to_s] = eval(value).to_s
591
622
  end
@@ -649,13 +680,18 @@ module Haml
649
680
  # @return [String] rest
650
681
  # @return [Integer] last_line
651
682
  def parse_old_attributes(text)
652
- text = text.dup
653
683
  last_line = @line.index + 1
654
684
 
655
685
  begin
656
- attributes_hash, rest = balance(text, ?{, ?})
686
+ # Old attributes often look like a valid Hash literal, but it sometimes allow code like
687
+ # `{ hash, foo: bar }`, which is compiled to `_hamlout.attributes({}, nil, hash, foo: bar)`.
688
+ #
689
+ # To scan such code correctly, this scans `a( hash, foo: bar }` instead, stops when there is
690
+ # 1 more :on_embexpr_end (the last '}') than :on_embexpr_beg, and resurrects '{' afterwards.
691
+ balanced, rest = balance_tokens(text.sub(?{, METHOD_CALL_PREFIX), :on_embexpr_beg, :on_embexpr_end, count: 1)
692
+ attributes_hash = balanced.sub(METHOD_CALL_PREFIX, ?{)
657
693
  rescue SyntaxError => e
658
- if text.strip[-1] == ?, && e.message == Error.message(:unbalanced_brackets)
694
+ if e.message == Error.message(:unbalanced_brackets) && !@template.empty?
659
695
  text << "\n#{@next_line.text}"
660
696
  last_line += 1
661
697
  next_line
@@ -698,12 +734,12 @@ module Haml
698
734
  end
699
735
 
700
736
  static_attributes = {}
701
- dynamic_attributes = "{"
737
+ dynamic_attributes = "{".dup
702
738
  attributes.each do |name, (type, val)|
703
739
  if type == :static
704
740
  static_attributes[name] = val
705
741
  else
706
- dynamic_attributes << "#{inspect_obj(name)} => #{val},"
742
+ dynamic_attributes << "#{Util.inspect_obj(name)} => #{val},"
707
743
  end
708
744
  end
709
745
  dynamic_attributes << "}"
@@ -713,7 +749,7 @@ module Haml
713
749
  end
714
750
 
715
751
  def parse_new_attribute(scanner)
716
- unless name = scanner.scan(/[-:\w]+/)
752
+ unless (name = scanner.scan(/[-:\w]+/))
717
753
  return if scanner.scan(/\)/)
718
754
  return false
719
755
  end
@@ -722,8 +758,8 @@ module Haml
722
758
  return name, [:static, true] unless scanner.scan(/=/) #/end
723
759
 
724
760
  scanner.scan(/\s*/)
725
- unless quote = scanner.scan(/["']/)
726
- return false unless var = scanner.scan(/(@@?|\$)?\w+/)
761
+ unless (quote = scanner.scan(/["']/))
762
+ return false unless (var = scanner.scan(/(@@?|\$)?\w+/))
727
763
  return name, [:dynamic, var]
728
764
  end
729
765
 
@@ -738,7 +774,7 @@ module Haml
738
774
 
739
775
  return name, [:static, content.first[1]] if content.size == 1
740
776
  return name, [:dynamic,
741
- %!"#{content.each_with_object('') {|(t, v), s| s << (t == :str ? inspect_obj(v)[1...-1] : "\#{#{v}}")}}"!]
777
+ %!"#{content.each_with_object(''.dup) {|(t, v), s| s << (t == :str ? Util.inspect_obj(v)[1...-1] : "\#{#{v}}")}}"!]
742
778
  end
743
779
 
744
780
  def next_line
@@ -809,6 +845,25 @@ module Haml
809
845
  Haml::Util.balance(*args) or raise(SyntaxError.new(Error.message(:unbalanced_brackets)))
810
846
  end
811
847
 
848
+ # Unlike #balance, this balances Ripper tokens to balance something like `{ a: "}" }` correctly.
849
+ def balance_tokens(buf, start, finish, count: 0)
850
+ text = ''.dup
851
+ Ripper.lex(buf).each do |_, token, str|
852
+ text << str
853
+ case token
854
+ when start
855
+ count += 1
856
+ when finish
857
+ count -= 1
858
+ end
859
+
860
+ if count == 0
861
+ return text, buf.sub(text, '')
862
+ end
863
+ end
864
+ raise SyntaxError.new(Error.message(:unbalanced_brackets))
865
+ end
866
+
812
867
  def block_opened?
813
868
  @next_line.tabs > @line.tabs
814
869
  end
@@ -822,5 +877,115 @@ module Haml
822
877
  def flat?
823
878
  @flat
824
879
  end
880
+
881
+ class << AttributeMerger = Object.new
882
+ # Merges two attribute hashes.
883
+ # This is the same as `to.merge!(from)`,
884
+ # except that it merges id, class, and data attributes.
885
+ #
886
+ # ids are concatenated with `"_"`,
887
+ # and classes are concatenated with `" "`.
888
+ # data hashes are simply merged.
889
+ #
890
+ # Destructively modifies `to`.
891
+ #
892
+ # @param to [{String => String,Hash}] The attribute hash to merge into
893
+ # @param from [{String => Object}] The attribute hash to merge from
894
+ # @return [{String => String,Hash}] `to`, after being merged
895
+ def merge_attributes!(to, from)
896
+ from.keys.each do |key|
897
+ to[key] = merge_value(key, to[key], from[key])
898
+ end
899
+ to
900
+ end
901
+
902
+ private
903
+
904
+ # @return [String, nil]
905
+ def filter_and_join(value, separator)
906
+ return '' if (value.respond_to?(:empty?) && value.empty?)
907
+
908
+ if value.is_a?(Array)
909
+ value = value.flatten
910
+ value.map! {|item| item ? item.to_s : nil}
911
+ value.compact!
912
+ value = value.join(separator)
913
+ else
914
+ value = value ? value.to_s : nil
915
+ end
916
+ !value.nil? && !value.empty? && value
917
+ end
918
+
919
+ # Merge a couple of values to one attribute value. No destructive operation.
920
+ #
921
+ # @param to [String,Hash,nil]
922
+ # @param from [Object]
923
+ # @return [String,Hash]
924
+ def merge_value(key, to, from)
925
+ if from.kind_of?(Hash) || to.kind_of?(Hash)
926
+ from = { nil => from } if !from.is_a?(Hash)
927
+ to = { nil => to } if !to.is_a?(Hash)
928
+ to.merge(from)
929
+ elsif key == 'id'
930
+ merged_id = filter_and_join(from, '_')
931
+ if to && merged_id
932
+ merged_id = "#{to}_#{merged_id}"
933
+ elsif to || merged_id
934
+ merged_id ||= to
935
+ end
936
+ merged_id
937
+ elsif key == 'class'
938
+ merged_class = filter_and_join(from, ' ')
939
+ if to && merged_class
940
+ merged_class = (to.split(' ') | merged_class.split(' ')).join(' ')
941
+ elsif to || merged_class
942
+ merged_class ||= to
943
+ end
944
+ merged_class
945
+ else
946
+ from
947
+ end
948
+ end
949
+ end
950
+ private_constant :AttributeMerger
951
+
952
+ class ParserOptions
953
+ # A list of options that are actually used in the parser
954
+ AVAILABLE_OPTIONS = %i[
955
+ autoclose
956
+ escape_html
957
+ filename
958
+ line
959
+ mime_type
960
+ preserve
961
+ remove_whitespace
962
+ suppress_eval
963
+ ].each do |option|
964
+ attr_reader option
965
+ end
966
+
967
+ DEFAULTS = {
968
+ autoclose: %w(area base basefont br col command embed frame
969
+ hr img input isindex keygen link menuitem meta
970
+ param source track wbr),
971
+ escape_html: false,
972
+ filename: '(haml)',
973
+ line: 1,
974
+ mime_type: 'text/html',
975
+ preserve: %w(textarea pre code),
976
+ remove_whitespace: false,
977
+ suppress_eval: false,
978
+ }
979
+
980
+ def initialize(values = {})
981
+ DEFAULTS.each {|k, v| instance_variable_set :"@#{k}", v}
982
+ AVAILABLE_OPTIONS.each do |key|
983
+ if values.key?(key)
984
+ instance_variable_set :"@#{key}", values[key]
985
+ end
986
+ end
987
+ end
988
+ end
989
+ private_constant :ParserOptions
825
990
  end
826
991
  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