faml 0.2.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 (140) 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 +27 -0
  6. data/Appraisals +26 -0
  7. data/CHANGELOG.md +47 -0
  8. data/Gemfile +8 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +115 -0
  11. data/Rakefile +28 -0
  12. data/benchmark/attribute_builder.haml +5 -0
  13. data/benchmark/rendering.rb +35 -0
  14. data/bin/faml +4 -0
  15. data/ext/attribute_builder/attribute_builder.c +261 -0
  16. data/ext/attribute_builder/extconf.rb +3 -0
  17. data/faml.gemspec +38 -0
  18. data/gemfiles/rails_4.0.gemfile +9 -0
  19. data/gemfiles/rails_4.1.gemfile +9 -0
  20. data/gemfiles/rails_4.2.gemfile +9 -0
  21. data/gemfiles/rails_edge.gemfile +10 -0
  22. data/haml_spec_test.rb +22 -0
  23. data/lib/faml.rb +10 -0
  24. data/lib/faml/ast.rb +112 -0
  25. data/lib/faml/cli.rb +38 -0
  26. data/lib/faml/compiler.rb +374 -0
  27. data/lib/faml/element_parser.rb +235 -0
  28. data/lib/faml/engine.rb +34 -0
  29. data/lib/faml/filter_compilers.rb +41 -0
  30. data/lib/faml/filter_compilers/base.rb +43 -0
  31. data/lib/faml/filter_compilers/cdata.rb +15 -0
  32. data/lib/faml/filter_compilers/coffee.rb +16 -0
  33. data/lib/faml/filter_compilers/css.rb +16 -0
  34. data/lib/faml/filter_compilers/escaped.rb +23 -0
  35. data/lib/faml/filter_compilers/javascript.rb +16 -0
  36. data/lib/faml/filter_compilers/markdown.rb +18 -0
  37. data/lib/faml/filter_compilers/plain.rb +15 -0
  38. data/lib/faml/filter_compilers/preserve.rb +26 -0
  39. data/lib/faml/filter_compilers/ruby.rb +17 -0
  40. data/lib/faml/filter_compilers/sass.rb +15 -0
  41. data/lib/faml/filter_compilers/scss.rb +16 -0
  42. data/lib/faml/filter_compilers/tilt_base.rb +34 -0
  43. data/lib/faml/filter_parser.rb +54 -0
  44. data/lib/faml/html.rb +58 -0
  45. data/lib/faml/indent_tracker.rb +84 -0
  46. data/lib/faml/line_parser.rb +66 -0
  47. data/lib/faml/newline.rb +30 -0
  48. data/lib/faml/parser.rb +211 -0
  49. data/lib/faml/parser_utils.rb +17 -0
  50. data/lib/faml/rails_handler.rb +10 -0
  51. data/lib/faml/railtie.rb +9 -0
  52. data/lib/faml/ruby_multiline.rb +23 -0
  53. data/lib/faml/script_parser.rb +84 -0
  54. data/lib/faml/static_hash_parser.rb +113 -0
  55. data/lib/faml/syntax_error.rb +10 -0
  56. data/lib/faml/text_compiler.rb +69 -0
  57. data/lib/faml/tilt.rb +17 -0
  58. data/lib/faml/version.rb +3 -0
  59. data/spec/compiler_newline_spec.rb +162 -0
  60. data/spec/rails/Rakefile +6 -0
  61. data/spec/rails/app/assets/images/.keep +0 -0
  62. data/spec/rails/app/assets/javascripts/application.js +13 -0
  63. data/spec/rails/app/assets/stylesheets/application.css +15 -0
  64. data/spec/rails/app/controllers/application_controller.rb +5 -0
  65. data/spec/rails/app/controllers/books_controller.rb +8 -0
  66. data/spec/rails/app/controllers/concerns/.keep +0 -0
  67. data/spec/rails/app/helpers/application_helper.rb +2 -0
  68. data/spec/rails/app/mailers/.keep +0 -0
  69. data/spec/rails/app/models/.keep +0 -0
  70. data/spec/rails/app/models/book.rb +9 -0
  71. data/spec/rails/app/models/concerns/.keep +0 -0
  72. data/spec/rails/app/views/books/hello.html.haml +2 -0
  73. data/spec/rails/app/views/books/with_capture.html.haml +4 -0
  74. data/spec/rails/app/views/books/with_variables.html.haml +4 -0
  75. data/spec/rails/app/views/layouts/application.html.haml +9 -0
  76. data/spec/rails/bin/bundle +3 -0
  77. data/spec/rails/bin/rails +4 -0
  78. data/spec/rails/bin/rake +4 -0
  79. data/spec/rails/bin/setup +29 -0
  80. data/spec/rails/config.ru +4 -0
  81. data/spec/rails/config/application.rb +12 -0
  82. data/spec/rails/config/boot.rb +3 -0
  83. data/spec/rails/config/database.yml +25 -0
  84. data/spec/rails/config/environment.rb +5 -0
  85. data/spec/rails/config/environments/development.rb +41 -0
  86. data/spec/rails/config/environments/production.rb +79 -0
  87. data/spec/rails/config/environments/test.rb +42 -0
  88. data/spec/rails/config/initializers/assets.rb +11 -0
  89. data/spec/rails/config/initializers/backtrace_silencers.rb +7 -0
  90. data/spec/rails/config/initializers/cookies_serializer.rb +3 -0
  91. data/spec/rails/config/initializers/filter_parameter_logging.rb +4 -0
  92. data/spec/rails/config/initializers/inflections.rb +16 -0
  93. data/spec/rails/config/initializers/mime_types.rb +4 -0
  94. data/spec/rails/config/initializers/secret_key_base.rb +6 -0
  95. data/spec/rails/config/initializers/session_store.rb +3 -0
  96. data/spec/rails/config/initializers/wrap_parameters.rb +14 -0
  97. data/spec/rails/config/locales/en.yml +23 -0
  98. data/spec/rails/config/routes.rb +7 -0
  99. data/spec/rails/config/secrets.yml +22 -0
  100. data/spec/rails/db/seeds.rb +7 -0
  101. data/spec/rails/lib/assets/.keep +0 -0
  102. data/spec/rails/lib/tasks/.keep +0 -0
  103. data/spec/rails/log/.keep +0 -0
  104. data/spec/rails/public/404.html +67 -0
  105. data/spec/rails/public/422.html +67 -0
  106. data/spec/rails/public/500.html +66 -0
  107. data/spec/rails/public/favicon.ico +0 -0
  108. data/spec/rails/public/robots.txt +5 -0
  109. data/spec/rails/spec/requests/faml_spec.rb +41 -0
  110. data/spec/rails/vendor/assets/javascripts/.keep +0 -0
  111. data/spec/rails/vendor/assets/stylesheets/.keep +0 -0
  112. data/spec/rails_helper.rb +4 -0
  113. data/spec/render/attribute_spec.rb +241 -0
  114. data/spec/render/comment_spec.rb +61 -0
  115. data/spec/render/doctype_spec.rb +57 -0
  116. data/spec/render/element_spec.rb +136 -0
  117. data/spec/render/filters/cdata_spec.rb +12 -0
  118. data/spec/render/filters/coffee_spec.rb +25 -0
  119. data/spec/render/filters/css_spec.rb +45 -0
  120. data/spec/render/filters/escaped_spec.rb +14 -0
  121. data/spec/render/filters/javascript_spec.rb +44 -0
  122. data/spec/render/filters/markdown_spec.rb +19 -0
  123. data/spec/render/filters/plain_spec.rb +24 -0
  124. data/spec/render/filters/preserve_spec.rb +24 -0
  125. data/spec/render/filters/ruby_spec.rb +13 -0
  126. data/spec/render/filters/sass_spec.rb +28 -0
  127. data/spec/render/filters/scss_spec.rb +32 -0
  128. data/spec/render/filters_spec.rb +11 -0
  129. data/spec/render/haml_comment_spec.rb +24 -0
  130. data/spec/render/multiline_spec.rb +39 -0
  131. data/spec/render/newline_spec.rb +83 -0
  132. data/spec/render/plain_spec.rb +20 -0
  133. data/spec/render/preserve_spec.rb +8 -0
  134. data/spec/render/sanitize_spec.rb +36 -0
  135. data/spec/render/script_spec.rb +81 -0
  136. data/spec/render/silent_script_spec.rb +97 -0
  137. data/spec/render/unescape_spec.rb +45 -0
  138. data/spec/spec_helper.rb +47 -0
  139. data/spec/tilt_spec.rb +33 -0
  140. metadata +489 -0
@@ -0,0 +1,3 @@
1
+ require 'mkmf'
2
+
3
+ create_makefile('faml/attribute_builder')
@@ -0,0 +1,38 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'faml/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "faml"
8
+ spec.version = Faml::VERSION
9
+ spec.authors = ["Kohei Suzuki"]
10
+ spec.email = ["eagletmt@gmail.com"]
11
+ spec.summary = %q{Faster implementation of Haml template language.}
12
+ spec.description = %q{Faster implementation of Haml template language.}
13
+ spec.homepage = "https://github.com/eagletmt/faml"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.extensions = ['ext/attribute_builder/extconf.rb']
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "escape_utils"
23
+ spec.add_dependency "parser"
24
+ spec.add_dependency "temple"
25
+ spec.add_dependency "tilt"
26
+ spec.add_development_dependency "appraisal"
27
+ spec.add_development_dependency "benchmark-ips"
28
+ spec.add_development_dependency "bundler"
29
+ spec.add_development_dependency "coffee-script"
30
+ spec.add_development_dependency "coveralls"
31
+ spec.add_development_dependency "haml"
32
+ spec.add_development_dependency "rake"
33
+ spec.add_development_dependency "rake-compiler"
34
+ spec.add_development_dependency "redcarpet"
35
+ spec.add_development_dependency "rspec", ">= 3"
36
+ spec.add_development_dependency "sass"
37
+ spec.add_development_dependency "simplecov"
38
+ end
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 4.0.0"
6
+ gem "rspec-rails"
7
+ gem "sqlite3"
8
+
9
+ gemspec :path => "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 4.1.0"
6
+ gem "rspec-rails"
7
+ gem "sqlite3"
8
+
9
+ gemspec :path => "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 4.2.0"
6
+ gem "rspec-rails"
7
+ gem "sqlite3"
8
+
9
+ gemspec :path => "../"
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", :git => "https://github.com/rails/rails"
6
+ gem "rspec-rails"
7
+ gem "sqlite3"
8
+ gem "arel", :git => "https://github.com/rails/arel"
9
+
10
+ gemspec :path => "../"
@@ -0,0 +1,22 @@
1
+ require 'minitest/autorun'
2
+ require 'json'
3
+ require 'faml'
4
+
5
+ class HamlTest < Minitest::Test
6
+ contexts = JSON.parse(File.read(File.join(__dir__, 'haml-spec', 'tests.json')))
7
+ contexts.each do |context|
8
+ context[1].each do |name, test|
9
+ define_method("test_spec: #{name} (#{context[0]})") do
10
+ html = test["html"]
11
+ haml = test["haml"]
12
+ locals = Hash[(test["locals"] || {}).map {|x, y| [x.to_sym, y]}]
13
+ options = Hash[(test["config"] || {}).map {|x, y| [x.to_sym, y]}]
14
+ options[:format] = options[:format].to_sym if options.key?(:format)
15
+ tilt = Tilt.new("#{name}.haml", nil, options) { haml }
16
+ result = tilt.render(Object.new, locals)
17
+
18
+ assert_equal html, result.strip
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,10 @@
1
+ require 'faml/engine'
2
+ require 'faml/tilt'
3
+ require 'faml/version'
4
+
5
+ begin
6
+ gem 'rails'
7
+ require 'rails'
8
+ require 'faml/railtie'
9
+ rescue LoadError
10
+ end
@@ -0,0 +1,112 @@
1
+ module Faml
2
+ module Ast
3
+ module HasChildren
4
+ def initialize(*)
5
+ super
6
+ self.children ||= []
7
+ end
8
+
9
+ def <<(ast)
10
+ self.children << ast
11
+ end
12
+ end
13
+
14
+ class Root < Struct.new(:children)
15
+ include HasChildren
16
+ end
17
+
18
+ class Doctype < Struct.new(:doctype)
19
+ end
20
+
21
+ class Element < Struct.new(
22
+ :children,
23
+ :tag_name,
24
+ :static_class,
25
+ :static_id,
26
+ :attributes,
27
+ :oneline_child,
28
+ :self_closing,
29
+ :nuke_inner_whitespace,
30
+ :nuke_outer_whitespace,
31
+ )
32
+ include HasChildren
33
+
34
+ def initialize(*)
35
+ super
36
+ self.static_class ||= ''
37
+ self.static_id ||= ''
38
+ self.attributes ||= ''
39
+ self.self_closing ||= false
40
+ self.nuke_inner_whitespace ||= false
41
+ self.nuke_outer_whitespace ||= false
42
+ end
43
+ end
44
+
45
+ class Script < Struct.new(
46
+ :children,
47
+ :script,
48
+ :escape_html,
49
+ :preserve,
50
+ :mid_block_keyword,
51
+ )
52
+ include HasChildren
53
+
54
+ def initialize(*)
55
+ super
56
+ if self.escape_html.nil?
57
+ self.escape_html = true
58
+ end
59
+ if self.preserve.nil?
60
+ self.preserve = false
61
+ end
62
+ if self.mid_block_keyword.nil?
63
+ self.mid_block_keyword = false
64
+ end
65
+ end
66
+ end
67
+
68
+ class SilentScript < Struct.new(:children, :script, :mid_block_keyword)
69
+ include HasChildren
70
+
71
+ def initialize(*)
72
+ super
73
+ if self.mid_block_keyword.nil?
74
+ self.mid_block_keyword = false
75
+ end
76
+ end
77
+ end
78
+
79
+ class HtmlComment < Struct.new(:children, :comment, :conditional)
80
+ include HasChildren
81
+
82
+ def initialize(*)
83
+ super
84
+ self.comment ||= ''
85
+ self.conditional ||= ''
86
+ end
87
+ end
88
+
89
+ class HamlComment < Struct.new(:children)
90
+ include HasChildren
91
+ end
92
+
93
+ class Text < Struct.new(:text, :escape_html)
94
+ def initialize(*)
95
+ super
96
+ if self.escape_html.nil?
97
+ self.escape_html = true
98
+ end
99
+ end
100
+ end
101
+
102
+ class Filter < Struct.new(:name, :texts)
103
+ def initialize(*)
104
+ super
105
+ self.texts ||= []
106
+ end
107
+ end
108
+
109
+ class Empty
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,38 @@
1
+ require 'faml'
2
+ require 'thor'
3
+
4
+ module Faml
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 faml 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 Faml::Compiler.new.call(parse_file(file))
26
+ end
27
+
28
+ private
29
+
30
+ def compile_file(file)
31
+ Faml::Engine.new.call(File.read(file))
32
+ end
33
+
34
+ def parse_file(file)
35
+ Faml::Parser.new.call(File.read(file))
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,374 @@
1
+ require 'parser/current'
2
+ require 'temple'
3
+ require 'faml/ast'
4
+ require 'faml/filter_compilers'
5
+ require 'faml/static_hash_parser'
6
+ require 'faml/text_compiler'
7
+
8
+ module Faml
9
+ class Compiler < Temple::Parser
10
+ class UnparsableRubyCode < StandardError
11
+ end
12
+
13
+ DEFAULT_AUTO_CLOSE_TAGS = %w[
14
+ area base basefont br col command embed frame hr img input isindex keygen
15
+ link menuitem meta param source track wbr
16
+ ]
17
+ DEFAULT_PRESERVE_TAGS = %w[pre textarea code]
18
+
19
+ define_options(
20
+ autoclose: DEFAULT_AUTO_CLOSE_TAGS,
21
+ format: :html,
22
+ preserve: DEFAULT_PRESERVE_TAGS,
23
+ )
24
+
25
+ def initialize(*)
26
+ super
27
+ @text_compiler = TextCompiler.new
28
+ end
29
+
30
+ def call(ast)
31
+ compile(ast)
32
+ end
33
+
34
+ def self.find_and_preserve(input)
35
+ # Taken from the original haml code
36
+ re = /<(#{options[:preserve].map(&Regexp.method(:escape)).join('|')})([^>]*)>(.*?)(<\/\1>)/im
37
+ input.to_s.gsub(re) do |s|
38
+ s =~ re # Can't rely on $1, etc. existing since Rails' SafeBuffer#gsub is incompatible
39
+ "<#{$1}#{$2}>#{preserve($3)}</#{$1}>"
40
+ end
41
+ end
42
+
43
+ def self.preserve(input)
44
+ # Taken from the original haml code
45
+ input.to_s.chomp("\n").gsub(/\n/, '&#x000A;').gsub(/\r/, '')
46
+ end
47
+
48
+ private
49
+
50
+ def compile(ast)
51
+ case ast
52
+ when Ast::Root
53
+ compile_root(ast)
54
+ when Ast::Doctype
55
+ compile_doctype(ast)
56
+ when Ast::HtmlComment
57
+ compile_html_comment(ast)
58
+ when Ast::HamlComment
59
+ compile_haml_comment(ast)
60
+ when Ast::Empty
61
+ [:multi]
62
+ when Ast::Element
63
+ compile_element(ast)
64
+ when Ast::Script
65
+ compile_script(ast)
66
+ when Ast::SilentScript
67
+ compile_silent_script(ast)
68
+ when Ast::Text
69
+ compile_text(ast)
70
+ when Ast::Filter
71
+ compile_filter(ast)
72
+ else
73
+ raise "InternalError: Unknown AST node #{ast.class}: #{ast.inspect}"
74
+ end
75
+ end
76
+
77
+ def compile_root(ast)
78
+ [:multi].tap do |temple|
79
+ compile_children(ast, temple)
80
+ end
81
+ end
82
+
83
+ def compile_children(ast, temple)
84
+ ast.children.each do |c|
85
+ temple << compile(c)
86
+ if need_newline?(c)
87
+ temple << [:mknl]
88
+ end
89
+ unless suppress_code_newline?(c)
90
+ temple << [:newline]
91
+ end
92
+ end
93
+ end
94
+
95
+ def need_newline?(child)
96
+ case child
97
+ when Ast::Script
98
+ child.children.empty?
99
+ when Ast::SilentScript, Ast::HamlComment, Ast::Empty
100
+ false
101
+ when Ast::Element
102
+ !child.nuke_outer_whitespace
103
+ when Ast::Filter
104
+ FilterCompilers.find(child.name).need_newline?
105
+ else
106
+ true
107
+ end
108
+ end
109
+
110
+ def suppress_code_newline?(ast)
111
+ ast.is_a?(Ast::Script) ||
112
+ ast.is_a?(Ast::SilentScript) ||
113
+ (ast.is_a?(Ast::Element) && suppress_code_newline?(ast.oneline_child)) ||
114
+ (ast.is_a?(Ast::Element) && !ast.children.empty?) ||
115
+ (ast.is_a?(Ast::HtmlComment) && !ast.conditional.empty?)
116
+ end
117
+
118
+ def compile_text(ast)
119
+ @text_compiler.compile(ast.text, escape_html: ast.escape_html)
120
+ end
121
+
122
+ # html5 and html4 is deprecated in temple.
123
+ DEFAULT_DOCTYPE = {
124
+ html: 'html',
125
+ html5: 'html',
126
+ html4: 'transitional',
127
+ xhtml: 'transitional',
128
+ }.freeze
129
+
130
+ def compile_doctype(ast)
131
+ doctype = ast.doctype.downcase
132
+ if doctype.empty?
133
+ doctype = DEFAULT_DOCTYPE[options[:format]]
134
+ end
135
+ [:haml, :doctype, doctype]
136
+ end
137
+
138
+ def compile_html_comment(ast)
139
+ if ast.children.empty?
140
+ if ast.conditional.empty?
141
+ [:html, :comment, [:static, " #{ast.comment} "]]
142
+ else
143
+ [:html, :comment, [:static, "[#{ast.conditional}]> #{ast.comment} <![endif]"]]
144
+ end
145
+ else
146
+ temple = [:multi]
147
+ if ast.conditional.empty?
148
+ temple << [:mknl]
149
+ else
150
+ temple << [:static, "[#{ast.conditional}]>"] << [:mknl] << [:newline]
151
+ end
152
+ compile_children(ast, temple)
153
+ unless ast.conditional.empty?
154
+ temple << [:static, "<![endif]"]
155
+ end
156
+ [:multi, [:html, :comment, temple]]
157
+ end
158
+ end
159
+
160
+ def compile_haml_comment(ast)
161
+ [:multi].concat([[:newline]] * ast.children.size)
162
+ end
163
+
164
+ def compile_element(ast)
165
+ temple = [
166
+ :haml, :tag,
167
+ ast.tag_name,
168
+ self_closing?(ast),
169
+ compile_attributes(ast.attributes, ast.static_id, ast.static_class),
170
+ ]
171
+
172
+ if ast.oneline_child
173
+ temple << compile(ast.oneline_child)
174
+ elsif !ast.children.empty?
175
+ temple << compile_element_children(ast)
176
+ end
177
+
178
+ if ast.nuke_outer_whitespace
179
+ [:multi, [:rmnl], temple]
180
+ else
181
+ temple
182
+ end
183
+ end
184
+
185
+ def self_closing?(ast)
186
+ ast.self_closing || options[:autoclose].include?(ast.tag_name)
187
+ end
188
+
189
+ def compile_element_children(ast)
190
+ children = [:multi]
191
+ unless nuke_inner_whitespace?(ast)
192
+ children << [:mknl]
193
+ end
194
+ children << [:newline]
195
+ compile_children(ast, children)
196
+ if nuke_inner_whitespace?(ast)
197
+ children << [:rmnl]
198
+ end
199
+ children
200
+ end
201
+
202
+ def nuke_inner_whitespace?(ast)
203
+ ast.nuke_inner_whitespace || options[:preserve].include?(ast.tag_name)
204
+ end
205
+
206
+ def compile_attributes(text, static_id, static_class)
207
+ if text.empty?
208
+ return compile_static_id_and_class(static_id, static_class)
209
+ end
210
+
211
+ if attrs = try_optimize_attributes(text, static_id, static_class)
212
+ return [:html, :attrs, *attrs]
213
+ end
214
+
215
+ # Slow version
216
+
217
+ h = {}
218
+ unless static_class.empty?
219
+ h[:class] = static_class.split(/ +/)
220
+ end
221
+ unless static_id.empty?
222
+ h[:id] = static_id
223
+ end
224
+
225
+ t =
226
+ if h.empty?
227
+ text
228
+ else
229
+ "#{h.inspect}, #{text}"
230
+ end
231
+ [:haml, :attrs, t]
232
+ end
233
+
234
+ def compile_static_id_and_class(static_id, static_class)
235
+ [:html, :attrs].tap do |html_attrs|
236
+ unless static_class.empty?
237
+ html_attrs << [:haml, :attr, 'class', [:static, static_class]]
238
+ end
239
+ unless static_id.empty?
240
+ html_attrs << [:haml, :attr, 'id', [:static, static_id]]
241
+ end
242
+ end
243
+ end
244
+
245
+ def try_optimize_attributes(text, static_id, static_class)
246
+ parser = StaticHashParser.new
247
+ unless parser.parse("{#{text}}")
248
+ assert_valid_ruby_code!(text)
249
+ return nil
250
+ end
251
+
252
+ static_attributes, dynamic_attributes = build_optimized_attributes(parser, static_id, static_class)
253
+ if static_attributes.nil?
254
+ return nil
255
+ end
256
+
257
+ if dynamic_attributes.has_key?('data')
258
+ # XXX: Quit optimization...
259
+ return nil
260
+ end
261
+
262
+ (static_attributes.keys + dynamic_attributes.keys).sort.flat_map do |k|
263
+ if static_attributes.has_key?(k)
264
+ compile_static_attribute(k, static_attributes[k])
265
+ else
266
+ compile_dynamic_attribute(k, dynamic_attributes[k])
267
+ end
268
+ end
269
+ end
270
+
271
+ def assert_valid_ruby_code!(text)
272
+ parser = ::Parser::CurrentRuby.new
273
+ parser.diagnostics.consumer = nil
274
+ buffer = ::Parser::Source::Buffer.new('(faml)')
275
+ buffer.source = "call(#{text})"
276
+ parser.parse(buffer)
277
+ true
278
+ rescue ::Parser::SyntaxError
279
+ raise UnparsableRubyCode.new("Unparsable Ruby code is given to attributes: #{text}")
280
+ end
281
+
282
+ def build_optimized_attributes(parser, static_id, static_class)
283
+ static_attributes = build_optimized_static_attributes(parser, static_id, static_class)
284
+ dynamic_attributes = build_optimized_dynamic_attributes(parser, static_attributes)
285
+ if dynamic_attributes
286
+ [static_attributes, dynamic_attributes]
287
+ else
288
+ [nil, nil]
289
+ end
290
+ end
291
+
292
+ def build_optimized_static_attributes(parser, static_id, static_class)
293
+ static_attributes = {}
294
+ parser.static_attributes.each do |k, v|
295
+ static_attributes[k.to_s] = v
296
+ end
297
+ unless static_class.empty?
298
+ static_attributes['class'] = [static_class.split(/ +/), static_attributes['class']].compact.flatten.map(&:to_s).sort.join(' ')
299
+ end
300
+ unless static_id.empty?
301
+ static_attributes['id'] = [static_id, static_attributes['id']].compact.join('_')
302
+ end
303
+ static_attributes
304
+ end
305
+
306
+ def build_optimized_dynamic_attributes(parser, static_attributes)
307
+ dynamic_attributes = {}
308
+ parser.dynamic_attributes.each do |k, v|
309
+ k = k.to_s
310
+ if static_attributes.has_key?(k)
311
+ if StaticHashParser::SPECIAL_ATTRIBUTES.include?(k)
312
+ # XXX: Quit optimization
313
+ return nil
314
+ end
315
+ end
316
+ dynamic_attributes[k] = v
317
+ end
318
+ dynamic_attributes
319
+ end
320
+
321
+ def compile_static_attribute(key, value)
322
+ case
323
+ when value == true
324
+ [[:haml, :attr, key, [:multi]]]
325
+ when value == false || value == nil
326
+ [[:multi]]
327
+ when value.is_a?(Hash) && key == 'data'
328
+ data = AttributeBuilder.normalize_data(value)
329
+ data.keys.sort.map do |k|
330
+ [:haml, :attr, "data-#{k}", [:static, Temple::Utils.escape_html(data[k])]]
331
+ end
332
+ else
333
+ [[:haml, :attr, key, [:static, Temple::Utils.escape_html(value)]]]
334
+ end
335
+ end
336
+
337
+ def compile_dynamic_attribute(key, value)
338
+ [[:haml, :attr, key, [:dvalue, value]]]
339
+ end
340
+
341
+ def compile_script(ast)
342
+ sym = unique_name
343
+ temple = [:multi]
344
+ if ast.children.empty?
345
+ temple << [:code, "#{sym} = (#{ast.script}"] << [:newline] << [:code, ')']
346
+ else
347
+ temple << [:code, "#{sym} = #{ast.script}"] << [:newline]
348
+ compile_children(ast, temple)
349
+ if !ast.mid_block_keyword
350
+ temple << [:code, 'end']
351
+ end
352
+ end
353
+ if !ast.escape_html && ast.preserve
354
+ temple << [:haml, :preserve, sym]
355
+ else
356
+ temple << [:escape, ast.escape_html, [:dynamic, "#{sym}.to_s"]]
357
+ end
358
+ temple
359
+ end
360
+
361
+ def compile_silent_script(ast)
362
+ temple = [:multi, [:code, ast.script], [:newline]]
363
+ compile_children(ast, temple)
364
+ if !ast.children.empty? && !ast.mid_block_keyword
365
+ temple << [:code, 'end']
366
+ end
367
+ temple
368
+ end
369
+
370
+ def compile_filter(ast)
371
+ FilterCompilers.find(ast.name).compile(ast.texts)
372
+ end
373
+ end
374
+ end