faml 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f3b0f1b792e063159d89d05f763e294c63d45348
4
+ data.tar.gz: 7aa644aea3462479bd94e0def8c174c7e560e8bc
5
+ SHA512:
6
+ metadata.gz: 71db1678c99dd7e46e7d64ea14e8336879a69da2beb115524715f265e730402c617c63fc2bde58061b07190cadffd20730bacc9ad17ecefca2352a94f881e8c8
7
+ data.tar.gz: 63150b32c393d4b75a10681f944f98c0fa86a08f2eedc1a90151e6feb74bd09e454d02616d268e0a00918872be127a1acfc9c460dc2bf1a7858ad785ff71f2e4
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ *.log
15
+
16
+ spec/rails/tmp
@@ -0,0 +1,3 @@
1
+ [submodule "haml-spec"]
2
+ path = haml-spec
3
+ url = https://github.com/haml/haml-spec
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,27 @@
1
+ language: ruby
2
+ sudo: false
3
+ rvm:
4
+ - 2.0.0
5
+ - 2.1
6
+ - 2.2
7
+ - ruby-head
8
+ gemfile:
9
+ - gemfiles/rails_4.0.gemfile
10
+ - gemfiles/rails_4.1.gemfile
11
+ - gemfiles/rails_4.2.gemfile
12
+ - gemfiles/rails_edge.gemfile
13
+ after_script:
14
+ - bundle exec rake benchmark:rendering
15
+ matrix:
16
+ allow_failures:
17
+ - rvm: ruby-head
18
+ - gemfile: gemfiles/rails_edge.gemfile
19
+ # https://github.com/rspec/rspec-rails/pull/1264
20
+ - rvm: 2.2
21
+ gemfile: gemfiles/rails_4.0.gemfile
22
+ exclude:
23
+ # Rails 5 requires to run on Ruby 2.2.0 or newer.
24
+ - rvm: 2.0.0
25
+ gemfile: gemfiles/rails_edge.gemfile
26
+ - rvm: 2.1
27
+ gemfile: gemfiles/rails_edge.gemfile
@@ -0,0 +1,26 @@
1
+ appraise 'rails-4.0' do
2
+ gem 'rails', '~> 4.0.0'
3
+ gem 'rspec-rails'
4
+ gem 'sqlite3'
5
+ end
6
+
7
+ appraise 'rails-4.1' do
8
+ gem 'rails', '~> 4.1.0'
9
+ gem 'rspec-rails'
10
+ gem 'sqlite3'
11
+ end
12
+
13
+ appraise 'rails-4.2' do
14
+ gem 'rails', '~> 4.2.0'
15
+ gem 'rspec-rails'
16
+ gem 'sqlite3'
17
+ end
18
+
19
+ appraise 'rails-edge' do
20
+ gem 'rails', git: 'https://github.com/rails/rails'
21
+ gem 'arel', git: 'https://github.com/rails/arel'
22
+ gem 'rspec-rails'
23
+ gem 'sqlite3'
24
+ end
25
+
26
+ # vim: set ft=ruby:
@@ -0,0 +1,47 @@
1
+ ## 0.2.0 (2015-03-19)
2
+ - Rename from fast_haml to faml
3
+ - Allow .faml extension
4
+
5
+ ## 0.1.10 (2015-03-19)
6
+ - Fix ruby filter to not generate newlines
7
+ - Support markdown filter
8
+
9
+ ## 0.1.9 (2015-03-18)
10
+ - Refactor script parser (internal)
11
+ - Fix newline generation with filters
12
+ - Support sass, scss and coffee filter
13
+
14
+ ## 0.1.8 (2015-03-17)
15
+ - Fix whitespace removal (`<` and `>`) behavior
16
+ - Internally, new instructions `mknl` and `rmnl` are added.
17
+
18
+ ## 0.1.7 (2015-03-16)
19
+ - Fix attribute rendering with falsey values
20
+ - https://github.com/eagletmt/faml/pull/11
21
+
22
+ ## 0.1.6 (2015-03-11)
23
+ - Fix render error with comment-only script
24
+ - https://github.com/eagletmt/faml/issues/6
25
+ - Fix parsing error at multiline attributes
26
+ - https://github.com/eagletmt/faml/issues/7
27
+
28
+ ## 0.1.5 (2015-03-01)
29
+ - Fix minor newline generation bug with Haml comment
30
+
31
+ ## 0.1.4 (2015-02-28)
32
+ - Fix newline generation around empty lines
33
+ - Internal: introduce Ast::Empty and remove LineCounter
34
+
35
+ ## 0.1.3 (2015-02-27)
36
+ - Fix internal compiler error when `>` is used
37
+ - Fix newline generation at Ast::Element case
38
+
39
+ ## 0.1.2 (2015-02-24)
40
+ - Keep newlines for better backtrace (#4)
41
+
42
+ ## 0.1.1 (2015-02-23)
43
+ - Fix attribute parsing with `%span {foo}` or `%span (foo)` cases.
44
+ - Fix comparison with statically-compilable class attributes like `%span.foo{class: bar}` .
45
+
46
+ ## 0.1.0 (2015-02-23)
47
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in faml.gemspec
4
+ gemspec
5
+
6
+ gem 'rails'
7
+ gem 'rspec-rails'
8
+ gem 'sqlite3'
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Kohei Suzuki
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,115 @@
1
+ # Faml
2
+ [![Gem Version](https://badge.fury.io/rb/faml.svg)](http://badge.fury.io/rb/faml)
3
+ [![Build Status](https://travis-ci.org/eagletmt/faml.svg)](https://travis-ci.org/eagletmt/faml)
4
+ [![Coverage Status](https://coveralls.io/repos/eagletmt/faml/badge.svg)](https://coveralls.io/r/eagletmt/faml)
5
+ [![Code Climate](https://codeclimate.com/github/eagletmt/faml/badges/gpa.svg)](https://codeclimate.com/github/eagletmt/faml)
6
+
7
+ Faster implementation of Haml template language.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'faml'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install faml
24
+
25
+ ## Usage
26
+
27
+ Just replace your `gem 'haml'` with `gem 'faml'` .
28
+
29
+ ## Incompatibilities
30
+ There are several incompatibilities.
31
+
32
+ ### Hash attributes
33
+ Hash attributes are only supported to "data" attributes.
34
+
35
+ With original haml, `%span{foo: {bar: 'baz'}}` is rendered as `<span foo-bar='baz'></span>` .
36
+ With faml, it's rendered as `<span foo='{:bar=&gt;&quot;baz&quot;}'></span>` .
37
+
38
+ Only "data" attributes are converted to hyphenated attributes.
39
+
40
+ ### HTML-escape by default
41
+ Even with non-Rails project, all string are HTML-escaped.
42
+
43
+ ### "ugly" mode only
44
+ Only "ugly" mode in original haml is supported.
45
+
46
+ ```haml
47
+ %div
48
+ %div hello
49
+ ```
50
+
51
+ is always rendered as
52
+
53
+ ```html
54
+ <div>
55
+ <div>hello</div>
56
+ </div>
57
+ ```
58
+
59
+ It's equivalent to haml's "ugly" mode.
60
+
61
+ ### Others
62
+ If you find other incompatibility, please report it to me :-p.
63
+
64
+ ## Why faml is faster?
65
+ ### Temple backend
66
+ I use [temple](https://github.com/judofyr/temple) to achieve faster template rendering.
67
+ It's used by [slim](https://github.com/slim-template/slim) template language & engine which is known as fast.
68
+
69
+ 1. Faml::Parser converts source language (Haml template) to own AST (Faml::Ast) .
70
+ - You can see the Faml::Ast by running `faml parse template.haml` .
71
+ 2. Faml::Compiler compiles Faml::Ast into Temple AST.
72
+ - You can see the Temple AST by running `faml temple template.haml` .
73
+ 3. Temple compiles its AST into Ruby code.
74
+ - You can see the Ruby code by running `faml compile template.haml` .
75
+ - During this process, several optimizations are performed such as Temple::Filters::MultiFlattener and Temple::Filters::StaticMerger.
76
+
77
+ ### Attribute optimization
78
+ Although Haml allows arbitrary Ruby hash in the attribute syntax, most attributes are written in hash literal form.
79
+ All keys are string or symbol literals (i.e., not dynamic values) in typical case.
80
+
81
+ ```haml
82
+ %div{class: some_helper_method(current_user)}
83
+ %a{href: foo_path(@item)}= @item.name
84
+ ```
85
+
86
+ There is an optimization chance if we could know the value is String.
87
+ I introduced incompatibility to expand the chance: all attribute values are converted to String by `#to_s` except for `id`, `class` and `data` .
88
+ This will enable us to avoid runtime expensive hash merging and rendering.
89
+ The runtime hash merging is implemented by C extension in faml.
90
+
91
+ Internally, attributes are categolized into three types.
92
+
93
+ 1. Static attributes
94
+ - Both the key and the value are literal.
95
+ - Compiled into string literals.
96
+ - Fastest.
97
+ - e.g. `%input{checked: false}`
98
+ 2. Dynamic attributes
99
+ - The key is literal but the value isn't.
100
+ - The key is compiled into string literal. The value is interpolated at run-time.
101
+ - Relatively fast.
102
+ - e.g. `%input{checked: helper_method(@record)}`
103
+ 3. Ruby attributes
104
+ - Both the key and the value are non-literal expression.
105
+ - The attributes are stringified at run-time.
106
+ - Slow.
107
+ - e.g. `%input{helper_method(@record)}`
108
+
109
+ ## Contributing
110
+
111
+ 1. Fork it ( https://github.com/eagletmt/faml/fork )
112
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
113
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
114
+ 4. Push to the branch (`git push origin my-new-feature`)
115
+ 5. Create a new Pull Request
@@ -0,0 +1,28 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ task :default => [:compile, :spec]
4
+
5
+ require 'rake/extensiontask'
6
+ Rake::ExtensionTask.new('attribute_builder') do |ext|
7
+ ext.lib_dir = 'lib/faml'
8
+ end
9
+
10
+ require 'rspec/core/rake_task'
11
+ RSpec::Core::RakeTask.new(:spec)
12
+
13
+ namespace :benchmark do
14
+ task :rendering => ['benchmark:rendering:haml', 'benchmark:rendering:attributes']
15
+ namespace :rendering do
16
+ desc "Run benchmark with Haml's standard template"
17
+ task :haml do
18
+ haml_gem = Gem::Specification.find_by_name('haml')
19
+ standard_haml_path = File.join(haml_gem.gem_dir, 'test', 'templates', 'standard.haml')
20
+ sh 'ruby', 'benchmark/rendering.rb', standard_haml_path
21
+ end
22
+
23
+ desc "Run benchmark for attribute builder"
24
+ task :attributes do
25
+ sh 'ruby', 'benchmark/rendering.rb', File.join('benchmark/attribute_builder.haml')
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ - h = { 'user' => { id: 1234, name: 'eagletmt' }, book_id: 5432 }
2
+ - c = %w[content active]
3
+
4
+ %span.book{data: h, class: c}
5
+ Book
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env ruby
2
+ require 'benchmark/ips'
3
+ require 'haml'
4
+ require 'faml'
5
+ require 'escape_utils/html/haml'
6
+
7
+ template = ARGV[0]
8
+ unless template
9
+ $stderr.puts "Usage: #{$0} template.haml"
10
+ exit 1
11
+ end
12
+
13
+ Benchmark.ips do |x|
14
+ obj = Object.new
15
+
16
+ Haml::Engine.new(File.read(template), ugly: true, escape_html: true).def_method(obj, :haml)
17
+ code_array = Faml::Engine.new.call(File.read(template))
18
+ obj.instance_eval("def faml_array; #{code_array}; end")
19
+ code_string = Faml::Engine.new(generator: Temple::Generators::RailsOutputBuffer).call(File.read(template))
20
+ obj.instance_eval("def faml_string; #{code_string}; end")
21
+
22
+ x.report('Haml') do
23
+ obj.haml
24
+ end
25
+
26
+ x.report('Faml (Array)') do
27
+ obj.faml_array
28
+ end
29
+
30
+ x.report('Faml (String)') do
31
+ obj.faml_string
32
+ end
33
+
34
+ x.compare!
35
+ end
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'faml/cli'
3
+
4
+ Faml::CLI.start(ARGV)
@@ -0,0 +1,261 @@
1
+ #include <ruby.h>
2
+ #include <ruby/version.h>
3
+
4
+ #if (RUBY_API_VERSION_MAJOR > 2) || (RUBY_API_VERSION_MAJOR == 2 && RUBY_API_VERSION_MINOR >= 1)
5
+ /* define nothing */
6
+ #else
7
+ # define RARRAY_AREF(a, i) RARRAY_PTR(a)[i]
8
+ # define rb_ary_new_capa rb_ary_new2
9
+ #endif
10
+
11
+ VALUE rb_mAttributeBuilder;
12
+ static ID id_keys, id_sort_bang, id_merge_bang, id_temple, id_utils, id_escape_html, id_gsub, id_to_s;
13
+
14
+ static void
15
+ concat_array_attribute(VALUE attributes, VALUE hash, VALUE key)
16
+ {
17
+ VALUE v;
18
+
19
+ Check_Type(hash, T_HASH);
20
+ v = rb_hash_delete(hash, key);
21
+ if (!NIL_P(v)) {
22
+ VALUE ary;
23
+
24
+ v = rb_Array(v);
25
+ ary = rb_hash_lookup(attributes, key);
26
+ Check_Type(ary, T_ARRAY);
27
+ rb_ary_concat(ary, v);
28
+ }
29
+ }
30
+
31
+ static int
32
+ stringify_keys_i(VALUE key, VALUE value, VALUE arg)
33
+ {
34
+ key = rb_funcall(key, id_to_s, 0);
35
+ rb_hash_aset(arg, key, value);
36
+ return ST_CONTINUE;
37
+ }
38
+
39
+ static VALUE
40
+ stringify_keys(VALUE hash)
41
+ {
42
+ VALUE h = rb_hash_new();
43
+ rb_hash_foreach(hash, stringify_keys_i, h);
44
+ return h;
45
+ }
46
+
47
+ struct normalize_data_i2_arg {
48
+ VALUE key, normalized;
49
+ };
50
+
51
+ static int
52
+ normalize_data_i2(VALUE key, VALUE value, VALUE ptr)
53
+ {
54
+ struct normalize_data_i2_arg *arg = (struct normalize_data_i2_arg *)ptr;
55
+ VALUE k = rb_funcall(arg->key, id_to_s, 0);
56
+
57
+ k = rb_funcall(k, id_gsub, 2, rb_str_new_cstr("_"), rb_str_new_cstr("-"));
58
+ rb_str_cat(k, "-", 1);
59
+ rb_str_append(k, key);
60
+ rb_hash_aset(arg->normalized, k, value);
61
+ return ST_CONTINUE;
62
+ }
63
+
64
+ static VALUE normalize_data(VALUE data);
65
+
66
+ static int
67
+ normalize_data_i(VALUE key, VALUE value, VALUE normalized)
68
+ {
69
+ if (RB_TYPE_P(value, T_HASH)) {
70
+ struct normalize_data_i2_arg arg;
71
+ arg.key = key;
72
+ arg.normalized = normalized;
73
+ rb_hash_foreach(normalize_data(value), normalize_data_i2, (VALUE)(&arg));
74
+ } else {
75
+ key = rb_funcall(key, id_to_s, 0);
76
+ key = rb_funcall(key, id_gsub, 2, rb_str_new_cstr("_"), rb_str_new_cstr("-"));
77
+ rb_hash_aset(normalized, key, value);
78
+ }
79
+ return ST_CONTINUE;
80
+ }
81
+
82
+ static VALUE
83
+ normalize_data(VALUE data)
84
+ {
85
+ VALUE normalized;
86
+
87
+ Check_Type(data, T_HASH);
88
+ normalized = rb_hash_new();
89
+ rb_hash_foreach(data, normalize_data_i, normalized);
90
+ return normalized;
91
+ }
92
+
93
+ static void
94
+ normalize(VALUE hash)
95
+ {
96
+ VALUE keys = rb_funcall(hash, id_keys, 0);
97
+ const long len = RARRAY_LEN(keys);
98
+ long i;
99
+ for (i = 0; i < len; i++) {
100
+ VALUE key = RARRAY_AREF(keys, i);
101
+ const char *key_cstr = StringValueCStr(key);
102
+ VALUE value = rb_hash_lookup(hash, key);
103
+ if (RB_TYPE_P(value, T_HASH) && strcmp(key_cstr, "data") == 0) {
104
+ VALUE data, data_keys;
105
+ long data_len, j;
106
+
107
+ rb_hash_delete(hash, key);
108
+ data = normalize_data(value);
109
+ data_keys = rb_funcall(data, id_keys, 0);
110
+ rb_funcall(data_keys, id_sort_bang, 0);
111
+ data_len = RARRAY_LEN(data_keys);
112
+ for (j = 0; j < data_len; j++) {
113
+ VALUE data_key = RARRAY_AREF(data_keys, j);
114
+ VALUE k = rb_str_buf_new(5 + RSTRING_LEN(data_key));
115
+ rb_str_buf_cat(k, "data-", 5);
116
+ rb_str_buf_append(k, data_key);
117
+ rb_hash_aset(hash, k, rb_hash_lookup(data, data_key));
118
+ }
119
+ } else if (!(RB_TYPE_P(value, T_TRUE) || RB_TYPE_P(value, T_FALSE) || NIL_P(value))) {
120
+ rb_hash_aset(hash, key, rb_funcall(value, id_to_s, 0));
121
+ }
122
+ }
123
+ }
124
+
125
+ static void
126
+ merge(VALUE attributes, int argc, VALUE *argv)
127
+ {
128
+ int i;
129
+
130
+ for (i = 0; i < argc; i++) {
131
+ VALUE h;
132
+
133
+ Check_Type(argv[i], T_HASH);
134
+ h = stringify_keys(argv[i]);
135
+ concat_array_attribute(attributes, h, rb_str_new_cstr("class"));
136
+ concat_array_attribute(attributes, h, rb_str_new_cstr("id"));
137
+ normalize(h);
138
+ rb_funcall(attributes, id_merge_bang, 1, h);
139
+ }
140
+ }
141
+
142
+ static VALUE
143
+ put_attribute(VALUE attr_quote, VALUE key, VALUE value)
144
+ {
145
+ VALUE utils_class, str;
146
+ long len;
147
+
148
+ value = rb_funcall(value, id_to_s, 0);
149
+ utils_class = rb_const_get(rb_const_get(rb_cObject, id_temple), id_utils);
150
+ value = rb_funcall(utils_class, id_escape_html, 1, value);
151
+
152
+ len = 2 + 2*RSTRING_LEN(attr_quote) + RSTRING_LEN(key) + RSTRING_LEN(value);
153
+ str = rb_str_buf_new(len);
154
+ rb_str_buf_cat(str, " ", 1);
155
+ rb_str_buf_append(str, key);
156
+ rb_str_buf_cat(str, "=", 1);
157
+ rb_str_buf_append(str, attr_quote);
158
+ rb_str_buf_append(str, value);
159
+ rb_str_buf_append(str, attr_quote);
160
+ return str;
161
+ }
162
+
163
+ static VALUE
164
+ build_attribute(VALUE attr_quote, VALUE key, VALUE value)
165
+ {
166
+ const char *key_cstr = StringValueCStr(key);
167
+ if (strcmp(key_cstr, "class") == 0) {
168
+ long len;
169
+
170
+ Check_Type(value, T_ARRAY);
171
+ len = RARRAY_LEN(value);
172
+ if (len == 0) {
173
+ return rb_str_new_cstr("");
174
+ } else {
175
+ long i;
176
+ VALUE ary = rb_ary_new_capa(len);
177
+ for (i = 0; i < len; i++) {
178
+ VALUE v = RARRAY_AREF(value, i);
179
+ rb_ary_push(ary, rb_funcall(v, id_to_s, 0));
180
+ }
181
+ rb_funcall(ary, id_sort_bang, 0);
182
+ return put_attribute(attr_quote, key, rb_ary_join(ary, rb_str_new_cstr(" ")));
183
+ }
184
+ } else if (strcmp(key_cstr, "id") == 0) {
185
+ long len = RARRAY_LEN(value);
186
+
187
+ Check_Type(value, T_ARRAY);
188
+ len = RARRAY_LEN(value);
189
+ if (len == 0) {
190
+ return rb_str_new_cstr("");
191
+ } else {
192
+ long i;
193
+ VALUE ary = rb_ary_new_capa(len);
194
+ for (i = 0; i < len; i++) {
195
+ VALUE v = RARRAY_AREF(value, i);
196
+ rb_ary_push(ary, rb_funcall(v, id_to_s, 0));
197
+ }
198
+ return put_attribute(attr_quote, key, rb_ary_join(ary, rb_str_new_cstr("_")));
199
+ }
200
+ } else if (RB_TYPE_P(value, T_TRUE)) {
201
+ VALUE attr = rb_str_buf_new(1 + RSTRING_LEN(key));
202
+ rb_str_buf_cat(attr, " ", 1);
203
+ rb_str_buf_append(attr, key);
204
+ return attr;
205
+ } else if (RB_TYPE_P(value, T_FALSE) || NIL_P(value)) {
206
+ return Qnil;
207
+ } else {
208
+ return put_attribute(attr_quote, key, value);
209
+ }
210
+ return value;
211
+ }
212
+
213
+ static VALUE
214
+ m_build(int argc, VALUE *argv, VALUE self)
215
+ {
216
+ VALUE attr_quote, attributes, keys, buf;
217
+ long len, i;
218
+
219
+ rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS);
220
+ attr_quote = argv[0];
221
+ attributes = rb_hash_new();
222
+ rb_hash_aset(attributes, rb_str_new_cstr("id"), rb_ary_new());
223
+ rb_hash_aset(attributes, rb_str_new_cstr("class"), rb_ary_new());
224
+ merge(attributes, argc-1, argv+1);
225
+
226
+ keys = rb_funcall(attributes, id_keys, 0);
227
+ rb_funcall(keys, id_sort_bang, 0);
228
+ len = RARRAY_LEN(keys);
229
+ buf = rb_ary_new_capa(len);
230
+ for (i = 0; i < len; i++) {
231
+ VALUE k = RARRAY_AREF(keys, i);
232
+ rb_ary_push(buf, build_attribute(attr_quote, k, rb_hash_lookup(attributes, k)));
233
+ }
234
+
235
+ return rb_ary_join(buf, Qnil);
236
+ }
237
+
238
+ static VALUE
239
+ m_normalize_data(VALUE self, VALUE data)
240
+ {
241
+ return normalize_data(data);
242
+ }
243
+
244
+ void
245
+ Init_attribute_builder(void)
246
+ {
247
+ VALUE mFaml = rb_define_module("Faml");
248
+ rb_mAttributeBuilder = rb_define_module_under(mFaml, "AttributeBuilder");
249
+ rb_define_singleton_method(rb_mAttributeBuilder, "build", RUBY_METHOD_FUNC(m_build), -1);
250
+ rb_define_singleton_method(rb_mAttributeBuilder, "normalize_data", RUBY_METHOD_FUNC(m_normalize_data), 1);
251
+
252
+ id_keys = rb_intern("keys");
253
+ id_sort_bang = rb_intern("sort!");
254
+ id_merge_bang = rb_intern("merge!");
255
+ id_temple = rb_intern("Temple");
256
+ id_utils = rb_intern("Utils");
257
+ id_escape_html = rb_intern("escape_html");
258
+ id_gsub = rb_intern("gsub");
259
+ id_to_s = rb_intern("to_s");
260
+ rb_require("temple");
261
+ }