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,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
+ }