haml 5.1.2 → 6.3.0

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 +3 -0
  3. data/.github/workflows/test.yml +36 -0
  4. data/.gitignore +16 -15
  5. data/.yardopts +0 -3
  6. data/CHANGELOG.md +189 -1
  7. data/FAQ.md +1 -1
  8. data/Gemfile +20 -12
  9. data/MIT-LICENSE +1 -1
  10. data/README.md +10 -17
  11. data/REFERENCE.md +129 -164
  12. data/Rakefile +15 -89
  13. data/bin/bench +66 -0
  14. data/bin/console +11 -0
  15. data/bin/ruby +3 -0
  16. data/bin/setup +7 -0
  17. data/bin/stackprof +27 -0
  18. data/bin/test +24 -0
  19. data/exe/haml +6 -0
  20. data/haml.gemspec +34 -35
  21. data/lib/haml/ambles.rb +20 -0
  22. data/lib/haml/attribute_builder.rb +131 -133
  23. data/lib/haml/attribute_compiler.rb +91 -182
  24. data/lib/haml/attribute_parser.rb +92 -126
  25. data/lib/haml/cli.rb +154 -0
  26. data/lib/haml/compiler/children_compiler.rb +155 -0
  27. data/lib/haml/compiler/comment_compiler.rb +51 -0
  28. data/lib/haml/compiler/doctype_compiler.rb +52 -0
  29. data/lib/haml/compiler/script_compiler.rb +114 -0
  30. data/lib/haml/compiler/silent_script_compiler.rb +24 -0
  31. data/lib/haml/compiler/tag_compiler.rb +76 -0
  32. data/lib/haml/compiler.rb +63 -296
  33. data/lib/haml/dynamic_merger.rb +67 -0
  34. data/lib/haml/engine.rb +48 -227
  35. data/lib/haml/error.rb +5 -4
  36. data/lib/haml/escape.rb +13 -0
  37. data/lib/haml/escape_any.rb +21 -0
  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 +59 -0
  54. data/lib/haml/filters.rb +54 -378
  55. data/lib/haml/force_escape.rb +29 -0
  56. data/lib/haml/helpers.rb +3 -691
  57. data/lib/haml/html.rb +22 -0
  58. data/lib/haml/identity.rb +13 -0
  59. data/lib/haml/object_ref.rb +35 -0
  60. data/lib/haml/parser.rb +190 -27
  61. data/lib/haml/rails_helpers.rb +53 -0
  62. data/lib/haml/rails_template.rb +62 -0
  63. data/lib/haml/railtie.rb +3 -41
  64. data/lib/haml/ruby_expression.rb +32 -0
  65. data/lib/haml/string_splitter.rb +140 -0
  66. data/lib/haml/template.rb +15 -34
  67. data/lib/haml/temple_line_counter.rb +2 -1
  68. data/lib/haml/util.rb +20 -16
  69. data/lib/haml/version.rb +1 -2
  70. data/lib/haml/whitespace.rb +8 -0
  71. data/lib/haml.rb +8 -20
  72. metadata +205 -53
  73. data/.gitmodules +0 -3
  74. data/.travis.yml +0 -97
  75. data/TODO +0 -24
  76. data/benchmark.rb +0 -70
  77. data/bin/haml +0 -9
  78. data/lib/haml/.gitattributes +0 -1
  79. data/lib/haml/buffer.rb +0 -238
  80. data/lib/haml/escapable.rb +0 -50
  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 -111
  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/compiler.rb CHANGED
@@ -1,329 +1,96 @@
1
1
  # frozen_string_literal: true
2
-
3
- require 'haml/attribute_builder'
4
- require 'haml/attribute_compiler'
5
- require 'haml/temple_line_counter'
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'
6
10
 
7
11
  module Haml
8
12
  class Compiler
9
- include Haml::Util
10
-
11
- attr_accessor :options
12
-
13
- def initialize(options)
14
- @options = Options.wrap(options)
15
- @to_merge = []
16
- @temple = [:multi]
17
- @node = nil
18
- @filters = Filters.defined.merge(options[:filters])
19
- @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)
20
22
  end
21
23
 
22
- def call(node)
23
- compile(node)
24
- @temple
25
- end
26
-
27
- def compile(node)
28
- parent, @node = @node, node
29
- if node.children.empty?
30
- send(:"compile_#{node.type}")
31
- else
32
- send(:"compile_#{node.type}") {node.children.each {|c| compile c}}
33
- end
34
- ensure
35
- @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)
36
29
  end
37
30
 
38
31
  private
39
32
 
40
- def compile_root
41
- @output_line = 1
42
- yield if block_given?
43
- flush_merged_text
44
- end
45
-
46
- def compile_plain
47
- push_text("#{@node.value[:text]}\n")
48
- end
49
-
50
- def nuke_inner_whitespace?(node)
51
- if node.value && node.value[:nuke_inner_whitespace]
52
- true
53
- elsif node.parent
54
- nuke_inner_whitespace?(node.parent)
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]
55
53
  else
56
- false
54
+ raise InternalError.new("Unexpected node type: #{node.type}")
57
55
  end
58
56
  end
59
57
 
60
- def compile_script(&block)
61
- push_script(@node.value[:text],
62
- :preserve_script => @node.value[:preserve],
63
- :escape_html => @node.value[:escape_html],
64
- :nuke_inner_whitespace => nuke_inner_whitespace?(@node),
65
- &block)
58
+ def compile_children(node)
59
+ @children_compiler.compile(node) { |n| compile(n) }
66
60
  end
67
61
 
68
- def compile_silent_script
69
- return if @options.suppress_eval
70
- push_silent(@node.value[:text])
71
- keyword = @node.value[:keyword]
72
-
73
- if block_given?
74
- yield
75
- push_silent("end", :can_suppress) unless @node.value[:dont_push_end]
76
- elsif keyword == "end"
77
- if @node.parent.children.last.equal?(@node)
78
- # Since this "end" is ending the block,
79
- # we don't need to generate an additional one
80
- @node.parent.value[:dont_push_end] = true
81
- end
82
- # Don't restore dont_* for end because it isn't a conditional branch.
83
- end
62
+ def compile_comment(node)
63
+ @comment_compiler.compile(node) { |n| compile_children(n) }
84
64
  end
85
65
 
86
- def compile_haml_comment; end
87
-
88
- def compile_tag
89
- t = @node.value
90
-
91
- # Get rid of whitespace outside of the tag if we need to
92
- rstrip_buffer! if t[:nuke_outer_whitespace]
93
-
94
- if @options.suppress_eval
95
- object_ref = :nil
96
- parse = false
97
- value = t[:parse] ? nil : t[:value]
98
- dynamic_attributes = Haml::Parser::DynamicAttributes.new
99
- else
100
- object_ref = t[:object_ref]
101
- parse = t[:parse]
102
- value = t[:value]
103
- dynamic_attributes = t[:dynamic_attributes]
104
- end
105
-
106
- if @options[:trace]
107
- t[:attributes].merge!({"data-trace" => @options.filename.split('/views').last << ":" << @node.line.to_s})
108
- end
109
-
110
- push_text("<#{t[:name]}")
111
- push_temple(@attribute_compiler.compile(t[:attributes], object_ref, dynamic_attributes))
112
- push_text(
113
- if t[:self_closing] && @options.xhtml?
114
- " />#{"\n" unless t[:nuke_outer_whitespace]}"
115
- else
116
- ">#{"\n" unless (t[:self_closing] && @options.html?) ? t[:nuke_outer_whitespace] : (!block_given? || t[:preserve_tag] || t[:nuke_inner_whitespace])}"
117
- end
118
- )
119
-
120
- if value && !parse
121
- push_text("#{value}</#{t[:name]}>#{"\n" unless t[:nuke_outer_whitespace]}")
122
- end
123
-
124
- return if t[:self_closing]
125
-
126
- if value.nil?
127
- yield if block_given?
128
- rstrip_buffer! if t[:nuke_inner_whitespace]
129
- push_text("</#{t[:name]}>#{"\n" unless t[:nuke_outer_whitespace]}")
130
- return
131
- end
132
-
133
- if parse
134
- push_script(value, t.merge(:in_tag => true))
135
- push_text("</#{t[:name]}>#{"\n" unless t[:nuke_outer_whitespace]}")
136
- end
66
+ def compile_doctype(node)
67
+ @doctype_compiler.compile(node)
137
68
  end
138
69
 
139
- def compile_comment
140
- condition = "#{@node.value[:conditional]}>" if @node.value[:conditional]
141
- revealed = @node.value[:revealed]
142
-
143
- open = "<!--#{condition}#{'<!-->' if revealed}"
144
-
145
- close = "#{'<!--' if revealed}#{'<![endif]' if condition}-->"
146
-
147
- unless block_given?
148
- push_text("#{open} ")
149
-
150
- if @node.value[:parse]
151
- push_script(@node.value[:text], :in_tag => true, :nuke_inner_whitespace => true)
152
- else
153
- push_text(@node.value[:text])
154
- end
155
-
156
- push_text(" #{close}\n")
157
- return
158
- end
159
-
160
- push_text("#{open}\n")
161
- yield if block_given?
162
- push_text("#{close}\n")
163
- end
164
-
165
- def compile_doctype
166
- doctype = text_for_doctype
167
- push_text("#{doctype}\n") if doctype
168
- end
169
-
170
- def compile_filter
171
- unless (filter = @filters[@node.value[:name]])
172
- name = @node.value[:name]
173
- if ["maruku", "textile"].include?(name)
174
- raise Error.new(Error.message(:install_haml_contrib, name), @node.line - 1)
175
- else
176
- raise Error.new(Error.message(:filter_not_defined, name), @node.line - 1)
177
- end
178
- end
179
- filter.internal_compile(self, @node.value[:text])
180
- end
181
-
182
- def text_for_doctype
183
- if @node.value[:type] == "xml"
184
- return nil if @options.html?
185
- wrapper = @options.attr_wrapper
186
- return "<?xml version=#{wrapper}1.0#{wrapper} encoding=#{wrapper}#{@node.value[:encoding] || "utf-8"}#{wrapper} ?>"
187
- end
188
-
189
- if @options.html5?
190
- '<!DOCTYPE html>'
191
- elsif @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
70
+ def compile_filter(node)
71
+ @filter_compiler.compile(node)
214
72
  end
215
73
 
216
- # Evaluates `text` in the context of the scope object, but
217
- # does not output the result.
218
- def push_silent(text, can_suppress = false)
219
- flush_merged_text
220
- return if can_suppress && @options.suppress_eval?
221
- newline = (text == "end") ? ";" : "\n"
222
- @temple << [:code, "#{resolve_newlines}#{text}#{newline}"]
223
- @output_line = @output_line + text.count("\n") + newline.count("\n")
74
+ def compile_plain(node)
75
+ [:static, node.value[:text]]
224
76
  end
225
77
 
226
- # Adds `text` to `@buffer`.
227
- def push_text(text)
228
- @to_merge << [:text, text]
78
+ def compile_script(node)
79
+ @script_compiler.compile(node) { |n| compile_children(n) }
229
80
  end
230
81
 
231
- def push_temple(temple)
232
- flush_merged_text
233
- @temple.concat([[:newline]] * resolve_newlines.count("\n"))
234
- @temple << temple
235
- @output_line += TempleLineCounter.count_lines(temple)
82
+ def compile_silent_script(node)
83
+ @silent_script_compiler.compile(node) { |n| compile_children(n) }
236
84
  end
237
85
 
238
- def flush_merged_text
239
- return if @to_merge.empty?
240
-
241
- @to_merge.each do |type, val|
242
- case type
243
- when :text
244
- @temple << [:static, val]
245
- when :script
246
- @temple << [:dynamic, val]
247
- else
248
- raise SyntaxError.new("[HAML BUG] Undefined entry in Haml::Compiler@to_merge.")
249
- end
250
- end
251
-
252
- @to_merge = []
86
+ def compile_tag(node)
87
+ @tag_compiler.compile(node) { |n| compile_children(n) }
253
88
  end
254
89
 
255
- # Causes `text` to be evaluated in the context of
256
- # the scope object and the result to be added to `@buffer`.
257
- #
258
- # If `opts[:preserve_script]` is true, Haml::Helpers#find_and_preserve is run on
259
- # the result before it is added to `@buffer`
260
- def push_script(text, opts = {})
261
- return if @options.suppress_eval?
262
-
263
- no_format = !(opts[:preserve_script] || opts[:preserve_tag] || opts[:escape_html])
264
-
265
- unless block_given?
266
- push_generated_script(no_format ? "(#{text}\n).to_s" : build_script_formatter("(#{text}\n)", opts))
267
- push_text("\n") unless opts[:in_tag] || opts[:nuke_inner_whitespace]
268
- return
269
- end
270
-
271
- flush_merged_text
272
- push_silent "haml_temp = #{text}"
273
- yield
274
- push_silent('end', :can_suppress) unless @node.value[:dont_push_end]
275
- @temple << [:dynamic, no_format ? 'haml_temp.to_s;' : build_script_formatter('haml_temp', opts)]
276
- end
277
-
278
- def build_script_formatter(text, opts)
279
- text = "(#{text}).to_s"
280
- if opts[:escape_html]
281
- text = "::Haml::Helpers.html_escape(#{text})"
282
- end
283
- if opts[:nuke_inner_whitespace]
284
- text = "(#{text}).strip"
285
- end
286
- if opts[:preserve_tag]
287
- text = "_hamlout.fix_textareas!(::Haml::Helpers.preserve(#{text}))"
288
- elsif opts[:preserve_script]
289
- text = "_hamlout.fix_textareas!(::Haml::Helpers.find_and_preserve(#{text}, _hamlout.options[:preserve]))"
290
- end
291
- "#{text};"
292
- end
293
-
294
- def push_generated_script(text)
295
- @to_merge << [:script, resolve_newlines + text]
296
- @output_line += text.count("\n")
297
- end
298
-
299
- def resolve_newlines
300
- diff = @node.line - @output_line
301
- return "" if diff <= 0
302
- @output_line = @node.line
303
- "\n" * diff
304
- end
305
-
306
- # Get rid of and whitespace at the end of the buffer
307
- # or the merged text
308
- def rstrip_buffer!(index = -1)
309
- last = @to_merge[index]
310
- if last.nil?
311
- push_silent("_hamlout.rstrip!", false)
312
- return
313
- end
314
-
315
- case last.first
316
- when :text
317
- last[1] = last[1].rstrip
318
- if last[1].empty?
319
- @to_merge.slice! index
320
- rstrip_buffer! index
321
- end
322
- when :script
323
- last[1].gsub!(/\(haml_temp, (.*?)\);$/, '(haml_temp.rstrip, \1);')
324
- rstrip_buffer! index - 1
325
- else
326
- 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})]]
327
94
  end
328
95
  end
329
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