fast_haml 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.gitmodules +3 -0
  4. data/.rspec +2 -0
  5. data/.travis.yml +25 -0
  6. data/Appraisals +26 -0
  7. data/CHANGELOG.md +2 -0
  8. data/Gemfile +8 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +96 -0
  11. data/Rakefile +11 -0
  12. data/benchmark/rendering.rb +29 -0
  13. data/bin/fast_haml +4 -0
  14. data/ext/attribute_builder/attribute_builder.c +259 -0
  15. data/ext/attribute_builder/extconf.rb +3 -0
  16. data/fast_haml.gemspec +35 -0
  17. data/gemfiles/rails_4.0.gemfile +9 -0
  18. data/gemfiles/rails_4.1.gemfile +9 -0
  19. data/gemfiles/rails_4.2.gemfile +9 -0
  20. data/gemfiles/rails_edge.gemfile +10 -0
  21. data/haml_spec_test.rb +22 -0
  22. data/lib/fast_haml/ast.rb +112 -0
  23. data/lib/fast_haml/cli.rb +38 -0
  24. data/lib/fast_haml/compiler.rb +325 -0
  25. data/lib/fast_haml/element_parser.rb +288 -0
  26. data/lib/fast_haml/engine.rb +32 -0
  27. data/lib/fast_haml/filter_compilers/base.rb +29 -0
  28. data/lib/fast_haml/filter_compilers/cdata.rb +15 -0
  29. data/lib/fast_haml/filter_compilers/css.rb +15 -0
  30. data/lib/fast_haml/filter_compilers/escaped.rb +22 -0
  31. data/lib/fast_haml/filter_compilers/javascript.rb +15 -0
  32. data/lib/fast_haml/filter_compilers/plain.rb +17 -0
  33. data/lib/fast_haml/filter_compilers/preserve.rb +30 -0
  34. data/lib/fast_haml/filter_compilers/ruby.rb +13 -0
  35. data/lib/fast_haml/filter_compilers.rb +37 -0
  36. data/lib/fast_haml/filter_parser.rb +54 -0
  37. data/lib/fast_haml/html.rb +44 -0
  38. data/lib/fast_haml/indent_tracker.rb +84 -0
  39. data/lib/fast_haml/line_parser.rb +66 -0
  40. data/lib/fast_haml/parser.rb +251 -0
  41. data/lib/fast_haml/parser_utils.rb +17 -0
  42. data/lib/fast_haml/rails_handler.rb +10 -0
  43. data/lib/fast_haml/railtie.rb +8 -0
  44. data/lib/fast_haml/ruby_multiline.rb +23 -0
  45. data/lib/fast_haml/static_hash_parser.rb +113 -0
  46. data/lib/fast_haml/syntax_error.rb +10 -0
  47. data/lib/fast_haml/text_compiler.rb +69 -0
  48. data/lib/fast_haml/tilt.rb +16 -0
  49. data/lib/fast_haml/version.rb +3 -0
  50. data/lib/fast_haml.rb +10 -0
  51. data/spec/rails/Rakefile +6 -0
  52. data/spec/rails/app/assets/images/.keep +0 -0
  53. data/spec/rails/app/assets/javascripts/application.js +13 -0
  54. data/spec/rails/app/assets/stylesheets/application.css +15 -0
  55. data/spec/rails/app/controllers/application_controller.rb +5 -0
  56. data/spec/rails/app/controllers/books_controller.rb +8 -0
  57. data/spec/rails/app/controllers/concerns/.keep +0 -0
  58. data/spec/rails/app/helpers/application_helper.rb +2 -0
  59. data/spec/rails/app/mailers/.keep +0 -0
  60. data/spec/rails/app/models/.keep +0 -0
  61. data/spec/rails/app/models/book.rb +9 -0
  62. data/spec/rails/app/models/concerns/.keep +0 -0
  63. data/spec/rails/app/views/books/hello.html.haml +2 -0
  64. data/spec/rails/app/views/books/with_capture.html.haml +4 -0
  65. data/spec/rails/app/views/books/with_variables.html.haml +4 -0
  66. data/spec/rails/app/views/layouts/application.html.haml +9 -0
  67. data/spec/rails/bin/bundle +3 -0
  68. data/spec/rails/bin/rails +4 -0
  69. data/spec/rails/bin/rake +4 -0
  70. data/spec/rails/bin/setup +29 -0
  71. data/spec/rails/config/application.rb +12 -0
  72. data/spec/rails/config/boot.rb +3 -0
  73. data/spec/rails/config/database.yml +25 -0
  74. data/spec/rails/config/environment.rb +5 -0
  75. data/spec/rails/config/environments/development.rb +41 -0
  76. data/spec/rails/config/environments/production.rb +79 -0
  77. data/spec/rails/config/environments/test.rb +42 -0
  78. data/spec/rails/config/initializers/assets.rb +11 -0
  79. data/spec/rails/config/initializers/backtrace_silencers.rb +7 -0
  80. data/spec/rails/config/initializers/cookies_serializer.rb +3 -0
  81. data/spec/rails/config/initializers/filter_parameter_logging.rb +4 -0
  82. data/spec/rails/config/initializers/inflections.rb +16 -0
  83. data/spec/rails/config/initializers/mime_types.rb +4 -0
  84. data/spec/rails/config/initializers/secret_key_base.rb +6 -0
  85. data/spec/rails/config/initializers/session_store.rb +3 -0
  86. data/spec/rails/config/initializers/wrap_parameters.rb +14 -0
  87. data/spec/rails/config/locales/en.yml +23 -0
  88. data/spec/rails/config/routes.rb +7 -0
  89. data/spec/rails/config/secrets.yml +22 -0
  90. data/spec/rails/config.ru +4 -0
  91. data/spec/rails/db/seeds.rb +7 -0
  92. data/spec/rails/lib/assets/.keep +0 -0
  93. data/spec/rails/lib/tasks/.keep +0 -0
  94. data/spec/rails/log/.keep +0 -0
  95. data/spec/rails/public/404.html +67 -0
  96. data/spec/rails/public/422.html +67 -0
  97. data/spec/rails/public/500.html +66 -0
  98. data/spec/rails/public/favicon.ico +0 -0
  99. data/spec/rails/public/robots.txt +5 -0
  100. data/spec/rails/spec/requests/fast_haml_spec.rb +41 -0
  101. data/spec/rails/vendor/assets/javascripts/.keep +0 -0
  102. data/spec/rails/vendor/assets/stylesheets/.keep +0 -0
  103. data/spec/rails_helper.rb +4 -0
  104. data/spec/render/attribute_spec.rb +209 -0
  105. data/spec/render/comment_spec.rb +61 -0
  106. data/spec/render/doctype_spec.rb +62 -0
  107. data/spec/render/element_spec.rb +165 -0
  108. data/spec/render/filters/cdata_spec.rb +12 -0
  109. data/spec/render/filters/css_spec.rb +45 -0
  110. data/spec/render/filters/escaped_spec.rb +14 -0
  111. data/spec/render/filters/javascript_spec.rb +44 -0
  112. data/spec/render/filters/plain_spec.rb +24 -0
  113. data/spec/render/filters/preserve_spec.rb +25 -0
  114. data/spec/render/filters/ruby_spec.rb +13 -0
  115. data/spec/render/filters_spec.rb +11 -0
  116. data/spec/render/haml_comment_spec.rb +24 -0
  117. data/spec/render/multiline_spec.rb +39 -0
  118. data/spec/render/plain_spec.rb +20 -0
  119. data/spec/render/preserve_spec.rb +8 -0
  120. data/spec/render/sanitize_spec.rb +36 -0
  121. data/spec/render/script_spec.rb +74 -0
  122. data/spec/render/silent_script_spec.rb +97 -0
  123. data/spec/render/unescape_spec.rb +40 -0
  124. data/spec/spec_helper.rb +49 -0
  125. data/spec/tilt_spec.rb +33 -0
  126. metadata +427 -0
@@ -0,0 +1,112 @@
1
+ module FastHaml
2
+ module Ast
3
+ module Construct
4
+ end
5
+
6
+ module HasChildren
7
+ def initialize(*)
8
+ super
9
+ self.children ||= []
10
+ end
11
+
12
+ def <<(ast)
13
+ self.children << ast
14
+ end
15
+ end
16
+
17
+ class Root < Struct.new(:children)
18
+ include HasChildren
19
+ end
20
+
21
+ class Doctype < Struct.new(:doctype)
22
+ end
23
+
24
+ class Element < Struct.new(
25
+ :children,
26
+ :tag_name,
27
+ :static_class,
28
+ :static_id,
29
+ :attributes,
30
+ :oneline_child,
31
+ :self_closing,
32
+ :nuke_inner_whitespace,
33
+ :nuke_outer_whitespace,
34
+ )
35
+ include HasChildren
36
+
37
+ def initialize(*)
38
+ super
39
+ self.static_class ||= ''
40
+ self.static_id ||= ''
41
+ self.attributes ||= ''
42
+ self.self_closing ||= false
43
+ self.nuke_inner_whitespace ||= false
44
+ self.nuke_outer_whitespace ||= false
45
+ end
46
+ end
47
+
48
+ class Script < Struct.new(
49
+ :children,
50
+ :script,
51
+ :escape_html,
52
+ :preserve,
53
+ :mid_block_keyword,
54
+ )
55
+ include HasChildren
56
+
57
+ def initialize(*)
58
+ super
59
+ if self.escape_html.nil?
60
+ self.escape_html = true
61
+ end
62
+ if self.preserve.nil?
63
+ self.preserve = false
64
+ end
65
+ if self.mid_block_keyword.nil?
66
+ self.mid_block_keyword = false
67
+ end
68
+ end
69
+ end
70
+
71
+ class SilentScript < Struct.new(:children, :script, :mid_block_keyword)
72
+ include HasChildren
73
+
74
+ def initialize(*)
75
+ super
76
+ if self.mid_block_keyword.nil?
77
+ self.mid_block_keyword = false
78
+ end
79
+ end
80
+ end
81
+
82
+ class HtmlComment < Struct.new(:children, :comment, :conditional)
83
+ include HasChildren
84
+
85
+ def initialize(*)
86
+ super
87
+ self.comment ||= ''
88
+ self.conditional ||= ''
89
+ end
90
+ end
91
+
92
+ class HamlComment < Struct.new(:children)
93
+ include HasChildren
94
+ end
95
+
96
+ class Text < Struct.new(:text, :escape_html)
97
+ def initialize(*)
98
+ super
99
+ if self.escape_html.nil?
100
+ self.escape_html = true
101
+ end
102
+ end
103
+ end
104
+
105
+ class Filter < Struct.new(:name, :texts)
106
+ def initialize(*)
107
+ super
108
+ self.texts ||= []
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,38 @@
1
+ require 'fast_haml'
2
+ require 'thor'
3
+
4
+ module FastHaml
5
+ class CLI < Thor
6
+ desc 'render FILE', 'Render haml template'
7
+ def render(file)
8
+ puts eval(compile_file(file))
9
+ end
10
+
11
+ desc 'compile FILE', 'Compile haml template'
12
+ def compile(file)
13
+ puts compile_file(file)
14
+ end
15
+
16
+ desc 'parse FILE', 'Render fast_haml AST'
17
+ def parse(file)
18
+ require 'pp'
19
+ pp parse_file(file)
20
+ end
21
+
22
+ desc 'temple FILE', 'Render temple AST'
23
+ def temple(file)
24
+ require 'pp'
25
+ pp FastHaml::Compiler.new.call(parse_file(file))
26
+ end
27
+
28
+ private
29
+
30
+ def compile_file(file)
31
+ FastHaml::Engine.new.call(File.read(file))
32
+ end
33
+
34
+ def parse_file(file)
35
+ FastHaml::Parser.new.call(File.read(file))
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,325 @@
1
+ require 'temple'
2
+ require 'fast_haml/ast'
3
+ require 'fast_haml/filter_compilers'
4
+ require 'fast_haml/static_hash_parser'
5
+ require 'fast_haml/text_compiler'
6
+
7
+ module FastHaml
8
+ class Compiler < Temple::Parser
9
+ DEFAULT_AUTO_CLOSE_TAGS = %w[
10
+ area base basefont br col command embed frame hr img input isindex keygen
11
+ link menuitem meta param source track wbr
12
+ ]
13
+ DEFAULT_PRESERVE_TAGS = %w[pre textarea code]
14
+
15
+ define_options(
16
+ autoclose: DEFAULT_AUTO_CLOSE_TAGS,
17
+ format: :html,
18
+ preserve: DEFAULT_PRESERVE_TAGS,
19
+ )
20
+
21
+ def initialize(*)
22
+ super
23
+ @text_compiler = TextCompiler.new
24
+ end
25
+
26
+ def call(ast)
27
+ compile(ast)
28
+ end
29
+
30
+ def self.find_and_preserve(input)
31
+ # Taken from the original haml code
32
+ re = /<(#{options[:preserve].map(&Regexp.method(:escape)).join('|')})([^>]*)>(.*?)(<\/\1>)/im
33
+ input.to_s.gsub(re) do |s|
34
+ s =~ re # Can't rely on $1, etc. existing since Rails' SafeBuffer#gsub is incompatible
35
+ "<#{$1}#{$2}>#{preserve($3)}</#{$1}>"
36
+ end
37
+ end
38
+
39
+ def self.preserve(input)
40
+ # Taken from the original haml code
41
+ input.to_s.chomp("\n").gsub(/\n/, '&#x000A;').gsub(/\r/, '')
42
+ end
43
+
44
+ private
45
+
46
+ def compile(ast)
47
+ case ast
48
+ when Ast::Root
49
+ compile_root(ast)
50
+ when Ast::Doctype
51
+ compile_doctype(ast)
52
+ when Ast::HtmlComment
53
+ compile_html_comment(ast)
54
+ when Ast::HamlComment
55
+ [:multi]
56
+ when Ast::Element
57
+ compile_element(ast)
58
+ when Ast::Script
59
+ compile_script(ast)
60
+ when Ast::SilentScript
61
+ compile_silent_script(ast)
62
+ when Ast::Text
63
+ compile_text(ast)
64
+ when Ast::Filter
65
+ compile_filter(ast)
66
+ else
67
+ raise "InternalError: Unknown AST node #{ast.class}: #{ast.inspect}"
68
+ end
69
+ end
70
+
71
+ def compile_root(ast)
72
+ [:multi, [:newline]].tap do |temple|
73
+ compile_children(ast, temple)
74
+ end
75
+ end
76
+
77
+ def compile_children(ast, temple)
78
+ was_newline = false
79
+ ast.children.each do |c|
80
+ if c.is_a?(Ast::Element) && c.nuke_outer_whitespace && was_newline
81
+ # pop newline
82
+ x = temple.pop
83
+ if x != [:newline]
84
+ raise "InternalError: Unexpected pop (expected [:newline]): #{x}"
85
+ end
86
+ x = temple.pop
87
+ if x != [:static, "\n"]
88
+ raise "InternalError: Unexpected pop (expected [:static, newline]): #{x}"
89
+ end
90
+ end
91
+ temple << compile(c)
92
+ if was_newline = need_newline?(ast, c)
93
+ temple << [:static, "\n"]
94
+ temple << [:newline]
95
+ end
96
+ end
97
+ end
98
+
99
+ def need_newline?(parent, child)
100
+ if parent.is_a?(Ast::Element) && nuke_inner_whitespace?(parent) && parent.children.last.equal?(child)
101
+ return false
102
+ end
103
+ case child
104
+ when Ast::Script
105
+ child.children.empty?
106
+ when Ast::SilentScript, Ast::HamlComment
107
+ false
108
+ when Ast::Element
109
+ !child.nuke_outer_whitespace
110
+ else
111
+ true
112
+ end
113
+ end
114
+
115
+ def compile_text(ast)
116
+ @text_compiler.compile(ast.text, escape_html: ast.escape_html)
117
+ end
118
+
119
+ # html5 and html4 is deprecated in temple.
120
+ DEFAULT_DOCTYPE = {
121
+ html: 'html',
122
+ html5: 'html',
123
+ html4: 'transitional',
124
+ xhtml: 'transitional',
125
+ }.freeze
126
+
127
+ def compile_doctype(ast)
128
+ doctype = ast.doctype.downcase
129
+ if doctype.empty?
130
+ doctype = DEFAULT_DOCTYPE[options[:format]]
131
+ end
132
+ [:haml, :doctype, doctype]
133
+ end
134
+
135
+ def compile_html_comment(ast)
136
+ if ast.children.empty?
137
+ if ast.conditional.empty?
138
+ [:html, :comment, [:static, " #{ast.comment} "]]
139
+ else
140
+ [:html, :comment, [:static, "[#{ast.conditional}]> #{ast.comment} <![endif]"]]
141
+ end
142
+ else
143
+ temple = [:multi]
144
+ if ast.conditional.empty?
145
+ temple << [:static, "\n"]
146
+ else
147
+ temple << [:static, "[#{ast.conditional}]>\n"]
148
+ end
149
+ compile_children(ast, temple)
150
+ unless ast.conditional.empty?
151
+ temple << [:static, "<![endif]"]
152
+ end
153
+ [:multi, [:html, :comment, temple]]
154
+ end
155
+ end
156
+
157
+ def compile_element(ast)
158
+ temple = [
159
+ :haml, :tag,
160
+ ast.tag_name,
161
+ self_closing?(ast),
162
+ compile_attributes(ast.attributes, ast.static_id, ast.static_class),
163
+ ]
164
+
165
+ if ast.oneline_child
166
+ temple << compile(ast.oneline_child)
167
+ elsif !ast.children.empty?
168
+ children = [:multi]
169
+ unless nuke_inner_whitespace?(ast)
170
+ children << [:static, "\n"]
171
+ end
172
+ children << [:newline]
173
+ compile_children(ast, children)
174
+ temple << children
175
+ end
176
+
177
+ temple
178
+ end
179
+
180
+ def self_closing?(ast)
181
+ ast.self_closing || options[:autoclose].include?(ast.tag_name)
182
+ end
183
+
184
+ def nuke_inner_whitespace?(ast)
185
+ ast.nuke_inner_whitespace || options[:preserve].include?(ast.tag_name)
186
+ end
187
+
188
+ def compile_attributes(text, static_id, static_class)
189
+ if text.empty?
190
+ return compile_static_id_and_class(static_id, static_class)
191
+ end
192
+
193
+ if attrs = try_optimize_attributes(text, static_id, static_class)
194
+ return [:html, :attrs, *attrs]
195
+ end
196
+
197
+ # Slow version
198
+
199
+ h = {}
200
+ unless static_class.empty?
201
+ h[:class] = static_class.split(/ +/)
202
+ end
203
+ unless static_id.empty?
204
+ h[:id] = static_id
205
+ end
206
+
207
+ t =
208
+ if h.empty?
209
+ text
210
+ else
211
+ "#{h.inspect}, #{text}"
212
+ end
213
+ [:haml, :attrs, t]
214
+ end
215
+
216
+ def compile_static_id_and_class(static_id, static_class)
217
+ [:html, :attrs].tap do |html_attrs|
218
+ unless static_class.empty?
219
+ html_attrs << [:haml, :attr, 'class', [:static, static_class]]
220
+ end
221
+ unless static_id.empty?
222
+ html_attrs << [:haml, :attr, 'id', [:static, static_id]]
223
+ end
224
+ end
225
+ end
226
+
227
+ def try_optimize_attributes(text, static_id, static_class)
228
+ parser = StaticHashParser.new
229
+ unless parser.parse("{#{text}}")
230
+ return nil
231
+ end
232
+
233
+ static_attributes, dynamic_attributes = build_optimized_attributes(parser, static_id, static_class)
234
+ if static_attributes.nil?
235
+ return nil
236
+ end
237
+
238
+ if dynamic_attributes.has_key?('data')
239
+ # XXX: Quit optimization...
240
+ return nil
241
+ end
242
+
243
+ (static_attributes.keys + dynamic_attributes.keys).sort.flat_map do |k|
244
+ if static_attributes.has_key?(k)
245
+ compile_static_attribute(k, static_attributes[k])
246
+ else
247
+ compile_dynamic_attribute(k, dynamic_attributes[k])
248
+ end
249
+ end
250
+ end
251
+
252
+ def build_optimized_attributes(parser, static_id, static_class)
253
+ static_attributes = {}
254
+ parser.static_attributes.each do |k, v|
255
+ static_attributes[k.to_s] = v;
256
+ end
257
+ unless static_class.empty?
258
+ static_attributes['class'] = [static_class.split(/ +/), static_attributes['class']].compact.flatten.sort.join(' ')
259
+ end
260
+ unless static_id.empty?
261
+ static_attributes['id'] = [static_id, static_attributes['id']].compact.join('_')
262
+ end
263
+
264
+ dynamic_attributes = {}
265
+ parser.dynamic_attributes.each do |k, v|
266
+ k = k.to_s
267
+ if static_attributes.has_key?(k)
268
+ if StaticHashParser::SPECIAL_ATTRIBUTES.include?(k)
269
+ # XXX: Quit optimization
270
+ return [nil, nil]
271
+ end
272
+ end
273
+ dynamic_attributes[k] = v
274
+ end
275
+
276
+ [static_attributes, dynamic_attributes]
277
+ end
278
+
279
+ def compile_static_attribute(key, value)
280
+ case
281
+ when value == true
282
+ [[:haml, :attr, key, [:multi]]]
283
+ when value.is_a?(Hash) && key == 'data'
284
+ data = AttributeBuilder.normalize_data(value)
285
+ data.keys.sort.map do |k|
286
+ [:haml, :attr, "data-#{k}", [:static, Temple::Utils.escape_html(data[k])]]
287
+ end
288
+ else
289
+ [[:haml, :attr, key, [:static, Temple::Utils.escape_html(value)]]]
290
+ end
291
+ end
292
+
293
+ def compile_dynamic_attribute(key, value)
294
+ [[:haml, :attr, key, [:escape, true, [:dynamic, value]]]]
295
+ end
296
+
297
+ def compile_script(ast)
298
+ sym = unique_name
299
+ temple = [:multi, [:code, "#{sym} = #{ast.script}"], [:newline]]
300
+ compile_children(ast, temple)
301
+ if !ast.children.empty? && !ast.mid_block_keyword
302
+ temple << [:code, 'end']
303
+ end
304
+ if !ast.escape_html && ast.preserve
305
+ temple << [:haml, :preserve, sym]
306
+ else
307
+ temple << [:escape, ast.escape_html, [:dynamic, "#{sym}.to_s"]]
308
+ end
309
+ temple
310
+ end
311
+
312
+ def compile_silent_script(ast)
313
+ temple = [:multi, [:code, ast.script], [:newline]]
314
+ compile_children(ast, temple)
315
+ if !ast.children.empty? && !ast.mid_block_keyword
316
+ temple << [:code, 'end']
317
+ end
318
+ temple
319
+ end
320
+
321
+ def compile_filter(ast)
322
+ FilterCompilers.find(ast.name).compile(ast.texts)
323
+ end
324
+ end
325
+ end