haml 5.2.2 → 6.0.0.beta.1

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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/test.yml +13 -9
  4. data/.gitignore +16 -16
  5. data/CHANGELOG.md +13 -3
  6. data/Gemfile +18 -11
  7. data/MIT-LICENSE +1 -1
  8. data/README.md +13 -19
  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 +135 -179
  24. data/lib/haml/attribute_compiler.rb +85 -194
  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 +179 -49
  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 -45
  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 +17 -15
  70. data/lib/haml/version.rb +1 -2
  71. data/lib/haml.rb +8 -20
  72. metadata +211 -57
  73. data/.gitmodules +0 -3
  74. data/.yardopts +0 -22
  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 -182
  80. data/lib/haml/exec.rb +0 -347
  81. data/lib/haml/generator.rb +0 -42
  82. data/lib/haml/helpers/action_view_extensions.rb +0 -60
  83. data/lib/haml/helpers/action_view_mods.rb +0 -132
  84. data/lib/haml/helpers/action_view_xss_mods.rb +0 -60
  85. data/lib/haml/helpers/safe_erubi_template.rb +0 -20
  86. data/lib/haml/helpers/safe_erubis_template.rb +0 -33
  87. data/lib/haml/helpers/xss_mods.rb +0 -114
  88. data/lib/haml/options.rb +0 -273
  89. data/lib/haml/plugin.rb +0 -54
  90. data/lib/haml/sass_rails_filter.rb +0 -47
  91. data/lib/haml/template/options.rb +0 -27
  92. data/lib/haml/temple_engine.rb +0 -124
  93. data/yard/default/.gitignore +0 -1
  94. data/yard/default/fulldoc/html/css/common.sass +0 -15
  95. 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?(HamlError)
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