fast_haml 0.1.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 (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