erector 0.8.3 → 0.9.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. data/Gemfile +21 -0
  2. data/Rakefile +171 -0
  3. data/VERSION.yml +3 -2
  4. data/lib/erector.rb +3 -5
  5. data/lib/erector/abstract_widget.rb +76 -31
  6. data/lib/erector/attributes.rb +34 -0
  7. data/lib/erector/caching.rb +1 -0
  8. data/lib/erector/convenience.rb +12 -4
  9. data/lib/erector/dependency.rb +2 -1
  10. data/lib/erector/element.rb +113 -0
  11. data/lib/erector/erect/erect.rb +10 -7
  12. data/lib/erector/externals.rb +2 -1
  13. data/lib/erector/html.rb +6 -340
  14. data/lib/erector/html_widget.rb +300 -0
  15. data/lib/erector/inline.rb +1 -1
  16. data/lib/erector/output.rb +39 -12
  17. data/lib/erector/promise.rb +137 -0
  18. data/lib/erector/rails2/extensions/rails_widget.rb +1 -1
  19. data/lib/erector/rails3.rb +1 -1
  20. data/lib/erector/sass.rb +14 -19
  21. data/lib/erector/tag.rb +65 -0
  22. data/lib/erector/text.rb +123 -0
  23. data/lib/erector/version.rb +1 -1
  24. data/lib/erector/widget.rb +52 -12
  25. data/lib/erector/widgets/page.rb +12 -10
  26. data/lib/erector/xml_widget.rb +131 -0
  27. data/spec/erector/caching_spec.rb +1 -0
  28. data/spec/erector/externals_spec.rb +0 -1
  29. data/spec/erector/html_spec.rb +9 -25
  30. data/spec/erector/output_spec.rb +102 -9
  31. data/spec/erector/promise_spec.rb +173 -0
  32. data/spec/erector/sass_spec.rb +1 -1
  33. data/spec/erector/tag_spec.rb +67 -0
  34. data/spec/erector/widget_spec.rb +53 -2
  35. data/spec/erector/xml_widget_spec.rb +74 -0
  36. data/spec/rails2/rails_app/Gemfile +1 -0
  37. data/spec/rails2/rails_app/spec/rails_spec_helper.rb +2 -0
  38. data/spec/rails_root/Gemfile +11 -0
  39. data/spec/rails_root/README +256 -0
  40. data/spec/rails_root/Rakefile +7 -0
  41. data/spec/rails_root/app/controllers/application.rb +6 -0
  42. data/spec/rails_root/app/controllers/application_controller.rb +3 -0
  43. data/spec/rails_root/app/helpers/application_helper.rb +2 -0
  44. data/spec/rails_root/app/views/layouts/application.html.erb +14 -0
  45. data/spec/rails_root/app/views/test/_erb.erb +1 -0
  46. data/spec/rails_root/app/views/test/_erector.rb +5 -0
  47. data/spec/rails_root/app/views/test/_partial_with_locals.rb +7 -0
  48. data/spec/rails_root/app/views/test/bare.rb +5 -0
  49. data/spec/rails_root/app/views/test/erb_from_erector.html.rb +5 -0
  50. data/spec/rails_root/app/views/test/erector_from_erb.html.erb +1 -0
  51. data/spec/rails_root/app/views/test/erector_with_locals_from_erb.html.erb +6 -0
  52. data/spec/rails_root/app/views/test/implicit_assigns.html.rb +5 -0
  53. data/spec/rails_root/app/views/test/needs.html.rb +7 -0
  54. data/spec/rails_root/app/views/test/needs_subclass.html.rb +5 -0
  55. data/spec/rails_root/app/views/test/protected_instance_variable.html.rb +5 -0
  56. data/spec/rails_root/app/views/test/render_default.html.rb +5 -0
  57. data/spec/rails_root/app/views/test/render_partial.html.rb +5 -0
  58. data/spec/rails_root/config.ru +4 -0
  59. data/spec/rails_root/config/application.rb +42 -0
  60. data/spec/rails_root/config/boot.rb +13 -0
  61. data/spec/rails_root/config/database.yml +22 -0
  62. data/spec/rails_root/config/environment.rb +5 -0
  63. data/spec/rails_root/config/environments/development.rb +22 -0
  64. data/spec/rails_root/config/environments/production.rb +49 -0
  65. data/spec/rails_root/config/environments/test.rb +35 -0
  66. data/spec/rails_root/config/initializers/backtrace_silencers.rb +7 -0
  67. data/spec/rails_root/config/initializers/inflections.rb +10 -0
  68. data/spec/rails_root/config/initializers/mime_types.rb +5 -0
  69. data/spec/rails_root/config/initializers/secret_token.rb +7 -0
  70. data/spec/rails_root/config/initializers/session_store.rb +8 -0
  71. data/spec/rails_root/config/locales/en.yml +5 -0
  72. data/spec/rails_root/config/routes.rb +58 -0
  73. data/spec/rails_root/db/seeds.rb +7 -0
  74. data/spec/rails_root/doc/README_FOR_APP +2 -0
  75. data/spec/rails_root/log/development.log +17 -0
  76. data/spec/rails_root/log/test.log +3750 -0
  77. data/spec/rails_root/public/404.html +26 -0
  78. data/spec/rails_root/public/422.html +26 -0
  79. data/spec/rails_root/public/500.html +26 -0
  80. data/spec/rails_root/public/dispatch.cgi +10 -0
  81. data/spec/rails_root/public/dispatch.fcgi +24 -0
  82. data/spec/rails_root/public/dispatch.rb +10 -0
  83. data/spec/rails_root/public/favicon.ico +0 -0
  84. data/spec/rails_root/public/images/rails.png +0 -0
  85. data/spec/rails_root/public/index.html +262 -0
  86. data/spec/rails_root/public/javascripts/application.js +2 -0
  87. data/spec/rails_root/public/javascripts/controls.js +965 -0
  88. data/spec/rails_root/public/javascripts/dragdrop.js +974 -0
  89. data/spec/rails_root/public/javascripts/effects.js +1123 -0
  90. data/spec/rails_root/public/javascripts/prototype.js +6001 -0
  91. data/spec/rails_root/public/javascripts/rails.js +175 -0
  92. data/spec/rails_root/public/robots.txt +5 -0
  93. data/spec/rails_root/script/about +3 -0
  94. data/spec/rails_root/script/console +3 -0
  95. data/spec/rails_root/script/destroy +3 -0
  96. data/spec/rails_root/script/generate +3 -0
  97. data/spec/rails_root/script/performance/benchmarker +3 -0
  98. data/spec/rails_root/script/performance/profiler +3 -0
  99. data/spec/rails_root/script/performance/request +3 -0
  100. data/spec/rails_root/script/plugin +3 -0
  101. data/spec/rails_root/script/process/inspector +3 -0
  102. data/spec/rails_root/script/process/reaper +3 -0
  103. data/spec/rails_root/script/process/spawner +3 -0
  104. data/spec/rails_root/script/rails +6 -0
  105. data/spec/rails_root/script/runner +3 -0
  106. data/spec/rails_root/script/server +3 -0
  107. data/spec/rails_root/spec/form_builder_spec.rb +21 -0
  108. data/spec/rails_root/spec/rails_helpers_spec.rb +220 -0
  109. data/spec/rails_root/spec/rails_spec_helper.rb +10 -0
  110. data/spec/rails_root/spec/rails_widget_spec.rb +83 -0
  111. data/spec/rails_root/spec/render_spec.rb +298 -0
  112. data/spec/rails_root/test/performance/browsing_test.rb +9 -0
  113. data/spec/rails_root/test/test_helper.rb +13 -0
  114. data/spec/spec_helper.rb +3 -1
  115. metadata +202 -66
data/Gemfile ADDED
@@ -0,0 +1,21 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "treetop", ">= 1.2.3"
4
+
5
+ group :development do
6
+ gem "activesupport", "~>3"
7
+ gem "rspec", "~>2"
8
+ gem "rubyforge"
9
+ gem "rr"
10
+ gem "nokogiri"
11
+ gem "jeweler"
12
+ gem "haml"
13
+ gem "sass"
14
+ gem "erubis"
15
+ gem "rdoc", "~>2.3"
16
+ gem "wrong", ">=0.5.4"
17
+ end
18
+
19
+ group :rails do
20
+ gem "rails", "~> 3.0.0"
21
+ end
@@ -0,0 +1,171 @@
1
+ puts "RUBY_VERSION=#{RUBY_VERSION}"
2
+
3
+ begin
4
+ # fix http://stackoverflow.com/questions/4932881/gemcutter-rake-build-now-throws-undefined-method-write-for-syckemitter
5
+ require 'psych' unless RUBY_VERSION =~ /^1\.8/
6
+ rescue LoadError
7
+ warn "Couldn't find psych; continuing."
8
+ end
9
+
10
+ require 'rake'
11
+ require 'rake/testtask'
12
+
13
+ require "rspec/core/rake_task"
14
+
15
+ require 'rdoc'
16
+ here = File.expand_path(File.dirname(__FILE__))
17
+ $LOAD_PATH.unshift("#{here}/lib")
18
+
19
+ require "erector/version"
20
+
21
+ begin
22
+ require 'jeweler'
23
+ Jeweler::Tasks.new do |gemspec|
24
+ gemspec.version = Erector::VERSION
25
+ gemspec.name = "erector"
26
+ gemspec.summary = "HTML/XML Builder library"
27
+ gemspec.email = "erector@googlegroups.com"
28
+ gemspec.description = "Erector is a Builder-like view framework, inspired by Markaby but overcoming some of its flaws. In Erector all views are objects, not template files, which allows the full power of object-oriented programming (inheritance, modular decomposition, encapsulation) in views."
29
+ gemspec.files = FileList[
30
+ "README.txt",
31
+ "VERSION.yml",
32
+ "lib/**/*",
33
+ "bin/erector",
34
+ ]
35
+ gemspec.executables = ["erector"]
36
+ specs = Dir.glob("spec/**/*") #.reject { |file| file =~ %r{spec/rails2/} }
37
+ gemspec.test_files = ([
38
+ "Rakefile",
39
+ "Gemfile",
40
+ ] + specs).flatten
41
+ gemspec.homepage = "http://erector.rubyforge.org/"
42
+ gemspec.authors = [
43
+ "Alex Chaffee",
44
+ "Brian Takita",
45
+ "Jeff Dean",
46
+ "Jim Kingdon",
47
+ "John Firebaugh",
48
+ ]
49
+ # gemspec.add_dependency 'treetop', ">= 1.2.3" # Jeweler now reads Gemfile, I think
50
+ end
51
+
52
+ Jeweler::RubyforgeTasks.new do |rubyforge|
53
+ rubyforge.doc_task = "rdoc"
54
+ rubyforge.remote_doc_path = "rdoc"
55
+ end
56
+
57
+ rescue LoadError
58
+ puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install jeweler"
59
+ end
60
+
61
+ desc "Default: run most tests"
62
+ task :default => :spec
63
+ task :cruise => [:install_gems, :print_environment, :test]
64
+ task :test => :spec
65
+
66
+ task :install_gems do
67
+ sh "bundle check"
68
+ end
69
+
70
+ desc "Build the web site from the .rb files in web/"
71
+ task :web do
72
+ files = Dir["web/*.rb"] - ["web/page.rb", "web/sidebar.rb", "web/clickable_li.rb"]
73
+ require 'erector'
74
+ require 'erector/erect/erect'
75
+ $: << "."
76
+ Erector::Widget.prettyprint_default = true
77
+ Erector::Erect.new(["--to-html", * files]).run
78
+ end
79
+
80
+ desc "Generate rdoc"
81
+ task :docs => :rdoc
82
+
83
+ task :rdoc => :clean_rdoc
84
+ task :clean_rdoc do
85
+ FileUtils.rm_rf("rdoc")
86
+ end
87
+
88
+ # push the docs to Rubyforge
89
+ task :publish_docs => :"rubyforge:release:docs"
90
+
91
+ desc "Publish web site to RubyForge"
92
+ task :publish_web do
93
+ config = YAML.load(File.read(File.expand_path("~/.rubyforge/user-config.yml")))
94
+ host = "#{config["username"]}@rubyforge.org"
95
+ rubyforge_name = "erector"
96
+ remote_dir = "/var/www/gforge-projects/#{rubyforge_name}"
97
+ local_dir = "web"
98
+ rdoc_dir = "rdoc"
99
+ rsync_args = '--archive --verbose --delete'
100
+
101
+ sh %{rsync #{rsync_args} --exclude=#{rdoc_dir} #{local_dir}/ #{host}:#{remote_dir}}
102
+ end
103
+
104
+ require 'rdoc/task'
105
+ RDoc::Task.new(:rdoc) do |rdoc|
106
+ rdoc.rdoc_dir = 'rdoc'
107
+ rdoc.title = "Erector #{Erector::VERSION}"
108
+ rdoc.options << '--inline-source' << "--promiscuous"
109
+ rdoc.options << "--main=README.txt"
110
+ # rdoc.options << '--diagram' if RUBY_PLATFORM !~ /win32/ and `which dot` =~ /\/dot/ and not ENV['NODOT']
111
+ rdoc.rdoc_files.include('README.txt')
112
+ rdoc.rdoc_files.include('lib/**/*.rb')
113
+ rdoc.rdoc_files.include('bin/**/*')
114
+ end
115
+
116
+ desc "Regenerate unicode.rb from UnicodeData.txt from unicode.org. Only needs to be run when there is a new version of the Unicode specification"
117
+ task(:build_unicode) do
118
+ require 'lib/erector/unicode_builder'
119
+ builder = Erector::UnicodeBuilder.new(
120
+ File.open("/usr/lib/perl5/5.8.8/unicore/UnicodeData.txt"),
121
+ File.open("lib/erector/unicode.rb", "w")
122
+ )
123
+ builder.generate
124
+ end
125
+
126
+ task :print_environment do
127
+ puts <<-ENVIRONMENT
128
+ Build environment:
129
+ #{`uname -a`.chomp}
130
+ #{`ruby -v`.chomp}
131
+ SQLite3: #{`sqlite3 -version`}
132
+ #{`gem env`}
133
+ Local gems:
134
+ #{`gem list`.gsub(/^/, ' ')}
135
+ ENVIRONMENT
136
+ end
137
+
138
+ namespace :spec do
139
+ desc "Run core specs."
140
+ RSpec::Core::RakeTask.new(:core) do |spec|
141
+ spec.pattern = 'spec/erector/*_spec.rb'
142
+ end
143
+
144
+ desc "Run specs for the 'erector' command line tool."
145
+ RSpec::Core::RakeTask.new(:erect) do |spec|
146
+ spec.pattern = 'spec/erect/*_spec.rb'
147
+ end
148
+
149
+ desc "Run specs for erector's Rails integration."
150
+ RSpec::Core::RakeTask.new(:rails) do |spec|
151
+ spec.pattern = 'spec/rails_root/spec/*_spec.rb'
152
+ end
153
+
154
+ desc "Run specs for erector's Rails integration under Rails 2."
155
+ task :rails2 do
156
+ Dir.chdir("spec/rails2/rails_app") do
157
+ # Bundler.with_clean_env do
158
+ sh "BUNDLE_GEMFILE='./Gemfile' bundle exec rake rails2"
159
+ # end
160
+ end
161
+ end
162
+
163
+ desc "Run all specs under Rails 3.1 - prepare with 'bundle install --gemfile Gemfile-rails31'"
164
+ task :rails31 do
165
+ gemfile = "#{here}/Gemfile-rails31"
166
+ sh "BUNDLE_GEMFILE='#{gemfile}' bundle exec rake spec:core spec:erect spec:rails"
167
+ end
168
+ end
169
+
170
+ desc "Run most specs"
171
+ task :spec => ['spec:core', 'spec:erect', 'spec:rails', 'spec:rails2']
@@ -1,4 +1,5 @@
1
1
  ---
2
2
  :major: 0
3
- :minor: 8
4
- :patch: 3
3
+ :minor: 9
4
+ :patch: 0
5
+ :build: pre1
@@ -3,11 +3,6 @@ end
3
3
 
4
4
  require "cgi"
5
5
  require "yaml"
6
- begin
7
- require "sass"
8
- rescue LoadError => e
9
- # oh well, no Sass
10
- end
11
6
 
12
7
  require "erector/raw_string"
13
8
  require "erector/dependencies"
@@ -21,7 +16,10 @@ require "erector/html"
21
16
  require "erector/convenience"
22
17
  require "erector/jquery"
23
18
  require "erector/sass"
19
+
24
20
  require "erector/abstract_widget"
21
+ require "erector/xml_widget"
22
+ require "erector/html_widget"
25
23
  require "erector/widget"
26
24
 
27
25
  require "erector/inline"
@@ -1,8 +1,25 @@
1
+ require 'erector/element'
2
+ require 'erector/attributes'
3
+ require 'erector/text'
4
+ require 'erector/convenience'
5
+ require 'erector/after_initialize'
6
+ require 'erector/output'
7
+
1
8
  module Erector
2
9
 
3
- # Abstract base class for Widget. This pattern allows Widget to include lots of nicely organized modules and still
4
- # have proper semantics for "super" in subclasses. See the rdoc for Widget for the list of all the included modules.
10
+ # Abstract base class for Widget. This pattern allows Widget to include lots
11
+ # of nicely organized modules and still have proper semantics for "super" in
12
+ # subclasses. See the rdoc for Widget for the list of all the included
13
+ # modules.
5
14
  class AbstractWidget
15
+
16
+ include Erector::Element
17
+ include Erector::Attributes
18
+ include Erector::Text
19
+ include Erector::AfterInitialize
20
+
21
+ include Erector::Convenience
22
+
6
23
  @@prettyprint_default = false
7
24
  def prettyprint_default
8
25
  @@prettyprint_default
@@ -50,30 +67,30 @@ module Erector
50
67
  # method and returns the string.
51
68
  #
52
69
  # Options:
53
- # output:: the string to output to. Default: a new empty string
70
+ # output:: the string (or array, or Erector::Output) to output to.
71
+ # Default: a new empty string
54
72
  # prettyprint:: whether Erector should add newlines and indentation.
55
- # Default: the value of prettyprint_default (which is false
56
- # by default).
73
+ # Default: the value of prettyprint_default (which, in turn,
74
+ # is false by default).
57
75
  # indentation:: the amount of spaces to indent. Ignored unless prettyprint
58
76
  # is true.
59
77
  # max_length:: preferred maximum length of a line. Line wraps will only
60
- # occur at space characters, so a long word may end up creating
61
- # a line longer than this. If nil (default), then there is no
62
- # arbitrary limit to line lengths, and only internal newline
63
- # characters and prettyprinting will determine newlines in the
64
- # output.
78
+ # occur at space characters, so a long word may end up
79
+ # creating a line longer than this. If nil (default), then
80
+ # there is no arbitrary limit to line lengths, and only
81
+ # internal newline characters and prettyprinting will
82
+ # determine newlines in the output.
65
83
  # helpers:: a helpers object containing utility methods. Usually this is a
66
84
  # Rails view object.
67
85
  # content_method_name:: in case you want to call a method other than
68
86
  # #content, pass its name in here.
69
87
  #
70
- def to_html(options = {})
71
- raise "Erector::Widget#to_html takes an options hash, not a symbol. Try calling \"to_html(:content_method_name=> :#{options})\"" if options.is_a? Symbol
88
+ def render(options = {})
72
89
  _render(options).to_s
73
90
  end
74
91
 
75
- # alias for #to_html
76
- # @deprecated Please use {#to_html} instead
92
+ # alias for #render
93
+ # @deprecated Please use {#render} instead
77
94
  def to_s(*args)
78
95
  unless defined? @@already_warned_to_s
79
96
  $stderr.puts "Erector::Widget#to_s is deprecated. Please use #to_html instead. Called from #{caller.first}"
@@ -82,36 +99,39 @@ module Erector
82
99
  to_html(*args)
83
100
  end
84
101
 
85
- # Entry point for rendering a widget (and all its children). Same as #to_html
86
- # only it returns an array, for theoretical performance improvements when using a
87
- # Rack server (like Sinatra or Rails Metal).
102
+ # Entry point for rendering a widget (and all its children). Same as
103
+ # #render / #to_html only it returns an array, for theoretical performance
104
+ # improvements when using a Rack server (like Sinatra or Rails Metal).
88
105
  #
89
- # # Options: see #to_html
106
+ # # Options: see #render
90
107
  def to_a(options = {})
91
108
  _render(options).to_a
92
109
  end
93
110
 
94
111
  # Template method which must be overridden by all widget subclasses.
95
112
  # Inside this method you call the magic #element methods which emit HTML
96
- # and text to the output string. If you call "super" (or don't override
113
+ # and text to the output string.
114
+ #
115
+ # If you call "super" (or don't override
97
116
  # +content+, or explicitly call "call_block") then your widget will
98
117
  # execute the block that was passed into its constructor. The semantics of
99
- # this block are confusing; make sure to read the rdoc for Erector#call_block
118
+ # this block are confusing; make sure to read the rdoc for
119
+ # Erector#call_block
100
120
  def content
101
121
  call_block
102
122
  end
103
-
104
- # When this method is executed, the default block that was passed in to
105
- # the widget's constructor will be executed. The semantics of this
123
+
124
+ # When this method is executed, the default block that was passed in to
125
+ # the widget's constructor will be executed. The semantics of this
106
126
  # block -- that is, what "self" is, and whether it has access to
107
127
  # Erector methods like "div" and "text", and the widget's instance
108
128
  # variables -- can be quite confusing. The rule is, most of the time the
109
129
  # block is evaluated using "call" or "yield", which means that its scope
110
130
  # is that of the caller. So if that caller is not an Erector widget, it
111
- # will *not* have access to the Erector methods, but it *will* have access
131
+ # will *not* have access to the Erector methods, but it *will* have access
112
132
  # to instance variables and methods of the calling object.
113
- #
114
- # If you want this block to have access to Erector methods then use
133
+ #
134
+ # If you want this block to have access to Erector methods then use
115
135
  # Erector::Inline#content or Erector#inline.
116
136
  def call_block
117
137
  @_block.call(self) if @_block
@@ -122,7 +142,8 @@ module Erector
122
142
  # the second argument is a hash used to populate its instance variables.
123
143
  # If the first argument is an instance then the hash must be unspecified
124
144
  # (or empty). If a block is passed to this method, then it gets set as the
125
- # rendered widget's block.
145
+ # rendered widget's block, and will be executed when that widget calls
146
+ # +call_block+ or calls +super+ from inside its +content+ method.
126
147
  #
127
148
  # This is the preferred way to call one widget from inside another. This
128
149
  # method assures that the same output string is used, which gives better
@@ -140,8 +161,8 @@ module Erector
140
161
 
141
162
  # Creates a whole new output string, executes the block, then converts the
142
163
  # output string to a string and returns it as raw text. If at all possible
143
- # you should avoid this method since it hurts performance, and use +widget+
144
- # instead.
164
+ # you should avoid this method since it hurts performance, and use
165
+ # +widget+ instead.
145
166
  def capture
146
167
  original, @_output = output, Output.new
147
168
  yield
@@ -152,22 +173,46 @@ module Erector
152
173
  end
153
174
 
154
175
  protected
176
+ # executes this widget's #content method, which emits stuff onto the
177
+ # output stream
155
178
  def _render(options = {}, &block)
156
179
  @_block = block if block
157
180
  @_parent = options[:parent] || parent
158
181
  @_helpers = options[:helpers] || parent
159
- @_output = options[:output]
160
- @_output = Output.new(options) unless output.is_a?(Output)
182
+ if options[:output]
183
+ # todo: document that either :buffer or :output can be used to specify an output buffer, and deprecate :output
184
+ if options[:output].is_a? Output
185
+ @_output = options[:output]
186
+ else
187
+ @_output = Output.new({:buffer => options[:output]}.merge(options))
188
+ end
189
+ else
190
+ @_output = Output.new(options)
191
+ end
161
192
 
162
193
  output.widgets << self.class
163
194
  send(options[:content_method_name] || :content)
164
195
  output
165
196
  end
166
197
 
198
+ # same as _render, but using a parent widget's output stream and helpers
167
199
  def _render_via(parent, options = {}, &block)
168
200
  _render(options.merge(:parent => parent,
169
201
  :output => parent.output,
170
202
  :helpers => parent.helpers), &block)
171
203
  end
204
+
205
+ protected
206
+
207
+ def sort_for_xml_declaration(attributes)
208
+ # correct order is "version, encoding, standalone" (XML 1.0 section 2.8).
209
+ # But we only try to put version before encoding for now.
210
+ stringized = []
211
+ attributes.each do |key, value|
212
+ stringized << [key.to_s, value]
213
+ end
214
+ stringized.sort{|a, b| b <=> a}
215
+ end
216
+
172
217
  end
173
218
  end
@@ -0,0 +1,34 @@
1
+ module Erector
2
+ module Attributes
3
+ def format_attributes(attributes)
4
+ if !attributes || attributes.empty?
5
+ ""
6
+ else
7
+ format_sorted(sort_attributes(attributes))
8
+ end
9
+ end
10
+
11
+ def format_sorted(sorted)
12
+ results = ['']
13
+ sorted.each do |key, value|
14
+ if value
15
+ if value.is_a?(Array)
16
+ value = value.flatten
17
+ next if value.empty?
18
+ value = value.join(' ')
19
+ end
20
+ results << "#{key}=\"#{h(value)}\""
21
+ end
22
+ end
23
+ results.join(' ')
24
+ end
25
+
26
+ def sort_attributes(attributes)
27
+ stringized = []
28
+ attributes.each do |key, value|
29
+ stringized << [key.to_s, value]
30
+ end
31
+ stringized.sort
32
+ end
33
+ end
34
+ end