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/compiler.rb CHANGED
@@ -1,330 +1,96 @@
1
- # frozen_string_literal: false
2
- require 'haml/attribute_builder'
3
- require 'haml/attribute_compiler'
4
- require 'haml/temple_line_counter'
1
+ # frozen_string_literal: true
2
+ require 'haml/compiler/children_compiler'
3
+ require 'haml/compiler/comment_compiler'
4
+ require 'haml/compiler/doctype_compiler'
5
+ require 'haml/compiler/script_compiler'
6
+ require 'haml/compiler/silent_script_compiler'
7
+ require 'haml/compiler/tag_compiler'
8
+ require 'haml/filters'
9
+ require 'haml/identity'
5
10
 
6
11
  module Haml
7
12
  class Compiler
8
- include Haml::Util
9
-
10
- attr_accessor :options
11
-
12
- def initialize(options)
13
- @options = Options.wrap(options)
14
- @to_merge = []
15
- @temple = [:multi]
16
- @node = nil
17
- @filters = Filters.defined.merge(options[:filters])
18
- @attribute_compiler = AttributeCompiler.new(@options)
13
+ def initialize(options = {})
14
+ identity = Identity.new
15
+ @children_compiler = ChildrenCompiler.new
16
+ @comment_compiler = CommentCompiler.new
17
+ @doctype_compiler = DoctypeCompiler.new(options)
18
+ @filter_compiler = Filters.new(options)
19
+ @script_compiler = ScriptCompiler.new(identity, options)
20
+ @silent_script_compiler = SilentScriptCompiler.new
21
+ @tag_compiler = TagCompiler.new(identity, options)
19
22
  end
20
23
 
21
- def call(node)
22
- compile(node)
23
- @temple
24
- end
25
-
26
- def compile(node)
27
- parent, @node = @node, node
28
- if node.children.empty?
29
- send(:"compile_#{node.type}")
30
- else
31
- send(:"compile_#{node.type}") {node.children.each {|c| compile c}}
32
- end
33
- ensure
34
- @node = parent
24
+ def call(ast)
25
+ return runtime_error(ast) if ast.is_a?(Error)
26
+ compile(ast)
27
+ rescue Error => e
28
+ runtime_error(e)
35
29
  end
36
30
 
37
31
  private
38
32
 
39
- def compile_root
40
- @output_line = 1
41
- yield if block_given?
42
- flush_merged_text
43
- end
44
-
45
- def compile_plain
46
- push_text("#{@node.value[:text]}\n")
47
- end
48
-
49
- def nuke_inner_whitespace?(node)
50
- if node.value && node.value[:nuke_inner_whitespace]
51
- true
52
- elsif node.parent
53
- nuke_inner_whitespace?(node.parent)
54
- else
55
- false
56
- end
57
- end
58
-
59
- def compile_script(&block)
60
- push_script(@node.value[:text],
61
- :preserve_script => @node.value[:preserve],
62
- :escape_html => @node.value[:escape_html],
63
- :nuke_inner_whitespace => nuke_inner_whitespace?(@node),
64
- &block)
65
- end
66
-
67
- def compile_silent_script
68
- return if @options.suppress_eval
69
- push_silent(@node.value[:text])
70
- keyword = @node.value[:keyword]
71
-
72
- if block_given?
73
- yield
74
- push_silent("end", :can_suppress) unless @node.value[:dont_push_end]
75
- elsif keyword == "end"
76
- if @node.parent.children.last.equal?(@node)
77
- # Since this "end" is ending the block,
78
- # we don't need to generate an additional one
79
- @node.parent.value[:dont_push_end] = true
80
- end
81
- # Don't restore dont_* for end because it isn't a conditional branch.
82
- end
83
- end
84
-
85
- def compile_haml_comment; end
86
-
87
- def compile_tag
88
- t = @node.value
89
-
90
- # Get rid of whitespace outside of the tag if we need to
91
- rstrip_buffer! if t[:nuke_outer_whitespace]
92
-
93
- if @options.suppress_eval
94
- object_ref = :nil
95
- parse = false
96
- value = t[:parse] ? nil : t[:value]
97
- dynamic_attributes = Haml::Parser::DynamicAttributes.new
98
- else
99
- object_ref = t[:object_ref]
100
- parse = t[:parse]
101
- value = t[:value]
102
- dynamic_attributes = t[:dynamic_attributes]
103
- end
104
-
105
- if @options[:trace]
106
- t[:attributes].merge!({"data-trace" => @options.filename.split('/views').last << ":" << @node.line.to_s})
107
- end
108
-
109
- push_text("<#{t[:name]}")
110
- push_temple(@attribute_compiler.compile(t[:attributes], object_ref, dynamic_attributes))
111
- push_text(
112
- if t[:self_closing] && @options.xhtml?
113
- " />#{"\n" unless t[:nuke_outer_whitespace]}"
114
- else
115
- ">#{"\n" unless (t[:self_closing] && @options.html?) ? t[:nuke_outer_whitespace] : (!block_given? || t[:preserve_tag] || t[:nuke_inner_whitespace])}"
116
- end
117
- )
118
-
119
- if value && !parse
120
- push_text("#{value}</#{t[:name]}>#{"\n" unless t[:nuke_outer_whitespace]}")
121
- end
122
-
123
- return if t[:self_closing]
124
-
125
- if value.nil?
126
- yield if block_given?
127
- rstrip_buffer! if t[:nuke_inner_whitespace]
128
- push_text("</#{t[:name]}>#{"\n" unless t[:nuke_outer_whitespace]}")
129
- return
130
- end
131
-
132
- if parse
133
- push_script(value, t.merge(:in_tag => true))
134
- push_text("</#{t[:name]}>#{"\n" unless t[:nuke_outer_whitespace]}")
135
- end
136
- end
137
-
138
- def compile_comment
139
- condition = "#{@node.value[:conditional]}>" if @node.value[:conditional]
140
- revealed = @node.value[:revealed]
141
-
142
- open = "<!--#{condition}#{'<!-->' if revealed}"
143
-
144
- close = "#{'<!--' if revealed}#{'<![endif]' if condition}-->"
145
-
146
- unless block_given?
147
- push_text("#{open} ")
148
-
149
- if @node.value[:parse]
150
- push_script(@node.value[:text], :in_tag => true, :nuke_inner_whitespace => true)
151
- else
152
- push_text(@node.value[:text])
153
- end
154
-
155
- push_text(" #{close}\n")
156
- return
157
- end
158
-
159
- push_text("#{open}\n")
160
- yield if block_given?
161
- push_text("#{close}\n")
162
- end
163
-
164
- def compile_doctype
165
- doctype = text_for_doctype
166
- push_text("#{doctype}\n") if doctype
167
- end
168
-
169
- def compile_filter
170
- unless filter = @filters[@node.value[:name]]
171
- name = @node.value[:name]
172
- if ["maruku", "textile"].include?(name)
173
- raise Error.new(Error.message(:install_haml_contrib, name), @node.line - 1)
174
- else
175
- raise Error.new(Error.message(:filter_not_defined, name), @node.line - 1)
176
- end
177
- end
178
- filter.internal_compile(self, @node.value[:text])
179
- end
180
-
181
- def text_for_doctype
182
- if @node.value[:type] == "xml"
183
- return nil if @options.html?
184
- wrapper = @options.attr_wrapper
185
- return "<?xml version=#{wrapper}1.0#{wrapper} encoding=#{wrapper}#{@node.value[:encoding] || "utf-8"}#{wrapper} ?>"
186
- end
187
-
188
- if @options.html5?
189
- '<!DOCTYPE html>'
33
+ def compile(node)
34
+ case node.type
35
+ when :root
36
+ compile_children(node)
37
+ when :comment
38
+ compile_comment(node)
39
+ when :doctype
40
+ compile_doctype(node)
41
+ when :filter
42
+ compile_filter(node)
43
+ when :plain
44
+ compile_plain(node)
45
+ when :script
46
+ compile_script(node)
47
+ when :silent_script
48
+ compile_silent_script(node)
49
+ when :tag
50
+ compile_tag(node)
51
+ when :haml_comment
52
+ [:multi]
190
53
  else
191
- if @options.xhtml?
192
- if @node.value[:version] == "1.1"
193
- '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
194
- elsif @node.value[:version] == "5"
195
- '<!DOCTYPE html>'
196
- else
197
- case @node.value[:type]
198
- when "strict"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
199
- when "frameset"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
200
- when "mobile"; '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">'
201
- when "rdfa"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">'
202
- when "basic"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">'
203
- else '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
204
- end
205
- end
206
-
207
- elsif @options.html4?
208
- case @node.value[:type]
209
- when "strict"; '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'
210
- when "frameset"; '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">'
211
- else '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'
212
- end
213
- end
54
+ raise InternalError.new("Unexpected node type: #{node.type}")
214
55
  end
215
56
  end
216
57
 
217
- # Evaluates `text` in the context of the scope object, but
218
- # does not output the result.
219
- def push_silent(text, can_suppress = false)
220
- flush_merged_text
221
- return if can_suppress && @options.suppress_eval?
222
- newline = (text == "end") ? ";" : "\n"
223
- @temple << [:code, "#{resolve_newlines}#{text}#{newline}"]
224
- @output_line = @output_line + text.count("\n") + newline.count("\n")
58
+ def compile_children(node)
59
+ @children_compiler.compile(node) { |n| compile(n) }
225
60
  end
226
61
 
227
- # Adds `text` to `@buffer`.
228
- def push_text(text)
229
- @to_merge << [:text, text]
62
+ def compile_comment(node)
63
+ @comment_compiler.compile(node) { |n| compile_children(n) }
230
64
  end
231
65
 
232
- def push_temple(temple)
233
- flush_merged_text
234
- @temple.concat([[:newline]] * resolve_newlines.count("\n"))
235
- @temple << temple
236
- @output_line += TempleLineCounter.count_lines(temple)
66
+ def compile_doctype(node)
67
+ @doctype_compiler.compile(node)
237
68
  end
238
69
 
239
- def flush_merged_text
240
- return if @to_merge.empty?
241
-
242
- @to_merge.each do |type, val|
243
- case type
244
- when :text
245
- @temple << [:static, val]
246
- when :script
247
- @temple << [:dynamic, val]
248
- else
249
- raise SyntaxError.new("[HAML BUG] Undefined entry in Haml::Compiler@to_merge.")
250
- end
251
- end
252
-
253
- @to_merge = []
70
+ def compile_filter(node)
71
+ @filter_compiler.compile(node)
254
72
  end
255
73
 
256
- # Causes `text` to be evaluated in the context of
257
- # the scope object and the result to be added to `@buffer`.
258
- #
259
- # If `opts[:preserve_script]` is true, Haml::Helpers#find_and_preserve is run on
260
- # the result before it is added to `@buffer`
261
- def push_script(text, opts = {})
262
- return if @options.suppress_eval?
263
-
264
- no_format = !(opts[:preserve_script] || opts[:preserve_tag] || opts[:escape_html])
265
-
266
- unless block_given?
267
- push_generated_script(no_format ? "(#{text}\n).to_s" : build_script_formatter("(#{text}\n)", opts))
268
- push_text("\n") unless opts[:in_tag] || opts[:nuke_inner_whitespace]
269
- return
270
- end
271
-
272
- flush_merged_text
273
- push_silent "haml_temp = #{text}"
274
- yield
275
- push_silent('end', :can_suppress) unless @node.value[:dont_push_end]
276
- @temple << [:dynamic, no_format ? 'haml_temp.to_s;' : build_script_formatter('haml_temp', opts)]
74
+ def compile_plain(node)
75
+ [:static, node.value[:text]]
277
76
  end
278
77
 
279
- def build_script_formatter(text, opts)
280
- text = "(#{text}).to_s"
281
- if opts[:escape_html]
282
- text = "::Haml::Helpers.html_escape(#{text})"
283
- end
284
- if opts[:nuke_inner_whitespace]
285
- text = "(#{text}).strip"
286
- end
287
- if opts[:preserve_tag]
288
- text = "_hamlout.fix_textareas!(::Haml::Helpers.preserve(#{text}))"
289
- elsif opts[:preserve_script]
290
- text = "_hamlout.fix_textareas!(::Haml::Helpers.find_and_preserve(#{text}, _hamlout.options[:preserve]))"
291
- end
292
- "#{text};"
78
+ def compile_script(node)
79
+ @script_compiler.compile(node) { |n| compile_children(n) }
293
80
  end
294
81
 
295
- def push_generated_script(text)
296
- @to_merge << [:script, resolve_newlines + text]
297
- @output_line += text.count("\n")
82
+ def compile_silent_script(node)
83
+ @silent_script_compiler.compile(node) { |n| compile_children(n) }
298
84
  end
299
85
 
300
- def resolve_newlines
301
- diff = @node.line - @output_line
302
- return "" if diff <= 0
303
- @output_line = @node.line
304
- "\n" * diff
86
+ def compile_tag(node)
87
+ @tag_compiler.compile(node) { |n| compile_children(n) }
305
88
  end
306
89
 
307
- # Get rid of and whitespace at the end of the buffer
308
- # or the merged text
309
- def rstrip_buffer!(index = -1)
310
- last = @to_merge[index]
311
- if last.nil?
312
- push_silent("_hamlout.rstrip!", false)
313
- return
314
- end
315
-
316
- case last.first
317
- when :text
318
- last[1].rstrip!
319
- if last[1].empty?
320
- @to_merge.slice! index
321
- rstrip_buffer! index
322
- end
323
- when :script
324
- last[1].gsub!(/\(haml_temp, (.*?)\);$/, '(haml_temp.rstrip, \1);')
325
- rstrip_buffer! index - 1
326
- else
327
- raise SyntaxError.new("[HAML BUG] Undefined entry in Haml::Compiler@to_merge.")
90
+ def runtime_error(error)
91
+ [:multi].tap do |temple|
92
+ error.line.times { temple << [:newline] } if error.line
93
+ temple << [:code, %Q[raise #{error.class}.new(%q[#{error.message}], #{error.line.inspect})]]
328
94
  end
329
95
  end
330
96
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+ module Haml
3
+ # Compile [:multi, [:static, 'foo'], [:dynamic, 'bar']] to [:dynamic, '"foo#{bar}"']
4
+ class DynamicMerger < Temple::Filter
5
+ def on_multi(*exps)
6
+ exps = exps.dup
7
+ result = [:multi]
8
+ buffer = []
9
+
10
+ until exps.empty?
11
+ type, arg = exps.first
12
+ if type == :dynamic && arg.count("\n") == 0
13
+ buffer << exps.shift
14
+ elsif type == :static && exps.size > (count = arg.count("\n")) &&
15
+ exps[1, count].all? { |e| e == [:newline] }
16
+ (1 + count).times { buffer << exps.shift }
17
+ elsif type == :newline && exps.size > (count = count_newline(exps)) &&
18
+ exps[count].first == :static && count == exps[count].last.count("\n")
19
+ (count + 1).times { buffer << exps.shift }
20
+ else
21
+ result.concat(merge_dynamic(buffer))
22
+ buffer = []
23
+ result << compile(exps.shift)
24
+ end
25
+ end
26
+ result.concat(merge_dynamic(buffer))
27
+
28
+ result.size == 2 ? result[1] : result
29
+ end
30
+
31
+ private
32
+
33
+ def merge_dynamic(exps)
34
+ # Merge exps only when they have both :static and :dynamic
35
+ unless exps.any? { |type,| type == :static } && exps.any? { |type,| type == :dynamic }
36
+ return exps
37
+ end
38
+
39
+ strlit_body = String.new
40
+ exps.each do |type, arg|
41
+ case type
42
+ when :static
43
+ strlit_body << arg.dump.sub!(/\A"/, '').sub!(/"\z/, '').gsub('\n', "\n")
44
+ when :dynamic
45
+ strlit_body << "\#{#{arg}}"
46
+ when :newline
47
+ # newline is added by `gsub('\n', "\n")`
48
+ else
49
+ raise "unexpected type #{type.inspect} is given to #merge_dynamic"
50
+ end
51
+ end
52
+ [[:dynamic, "%Q\0#{strlit_body}\0"]]
53
+ end
54
+
55
+ def count_newline(exps)
56
+ count = 0
57
+ exps.each do |exp|
58
+ if exp == [:newline]
59
+ count += 1
60
+ else
61
+ return count
62
+ end
63
+ end
64
+ return count
65
+ end
66
+ end
67
+ end