dcparker-shopify 0.1.9 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,2 +1,5 @@
1
- v0.1.0 First release, a rewrite of Shopify's own Rails plugin, turned into a gem with similar workings.
1
+ v0.2.0 Second release, using HTTParty, capable of multiple shop connections in one app, and thread-safe.
2
+
2
3
  v0.1.9 2.0 Beta, another rewrite using the light HTTParty instead of ActiveResource.
4
+
5
+ v0.1.0 First release, a rewrite of Shopify's own Rails plugin, turned into a gem with similar workings.
data/Manifest CHANGED
@@ -1,7 +1,19 @@
1
1
  CHANGELOG
2
+ lib/shopify/extlib/assertions.rb
3
+ lib/shopify/extlib/class.rb
4
+ lib/shopify/extlib/hash.rb
5
+ lib/shopify/extlib/hook.rb
6
+ lib/shopify/extlib/inflection.rb
7
+ lib/shopify/extlib/logger.rb
8
+ lib/shopify/extlib/object.rb
9
+ lib/shopify/extlib/pathname.rb
10
+ lib/shopify/extlib/rubygems.rb
11
+ lib/shopify/extlib/string.rb
12
+ lib/shopify/extlib/time.rb
13
+ lib/shopify/extlib.rb
2
14
  lib/shopify/support.rb
3
15
  lib/shopify.rb
4
16
  LICENSE
5
17
  Manifest
6
- README
18
+ README.textile
7
19
  shopify.gemspec
data/README.textile ADDED
@@ -0,0 +1,19 @@
1
+ h1. Shopify Rubygem
2
+
3
+ * Read any kind of data from Shopify, but no support built-in yet to save data back to Shopify.
4
+ * Connect to multiple shops in the same app.
5
+ * Thread-safe.
6
+
7
+ Example Usage:
8
+
9
+ <pre>
10
+ shop = Shopify.new('store_name', 'api-key', 'api-secret', 'auth-token')
11
+ order = shop.orders(:limit => 1)[0] # => gets first order
12
+ order.line_items # => the line items within that order
13
+ order.fulfillments # => gets all fulfillments related to this order
14
+ blogs = shop.blogs # => gets all blogs for this shop
15
+ articles = blogs[0].articles # => gets all the articles in this blog
16
+ articles[0].comments # => gets the comments for that article
17
+ shop.products # => get all products in this shop
18
+ ... and much more ... :)
19
+ </pre>
data/lib/shopify.rb CHANGED
@@ -2,24 +2,52 @@ include_path = File.expand_path(File.dirname(__FILE__))
2
2
  $:.unshift(include_path) unless $:.include?(include_path)
3
3
  require 'shopify/support'
4
4
 
5
- module Shopify
6
- include HTTParty
5
+ # Class: Shopify
6
+ # Usage:
7
+ # shop = Shopify.new(host, [key, [secret, [token]]])
8
+ # shop.orders
9
+ # TODO: Make the object remember the results of queries such as shop.orders when called without parameters,
10
+ # and reload only when you call shop.orders(true)
11
+ class Shopify
12
+ attr_reader :host
13
+
14
+ def initialize(host, key=nil, secret=nil, token=nil)
15
+ @default_options = {}
16
+ extend HTTParty::ClassMethods
7
17
 
8
- def self.setup(host, key=nil, secret=nil, token=nil)
9
18
  host.gsub!(/https?:\/\//, '') # remove http(s)://
10
19
  @host = host.include?('.') ? host : "#{host}.myshopify.com" # extend url to myshopify.com if no host is given
11
20
  @key = key
12
21
  @secret = secret
13
22
  @token = token
14
- if [host, key, secret, token].all?
23
+ setup
24
+ end
25
+
26
+ def needs_authorization?
27
+ ![@host, @key, @secret, @token].all?
28
+ end
29
+
30
+ def authorize!(token)
31
+ @token = token
32
+ setup
33
+ end
34
+
35
+ def authorization_url(mode='w')
36
+ "http://#{@host}/admin/api/auth?api_key=#{@key}&mode=#{mode}"
37
+ end
38
+
39
+ def setup
40
+ unless needs_authorization?
15
41
  base_uri "http://#{@host}/admin"
16
42
  basic_auth @key, Digest::MD5.hexdigest("#{@secret.chomp}#{@token.chomp}")
17
43
  format :xml
18
- return false
19
- else
20
- "http://#{@host}/admin/api/auth?api_key=#{@key}&mode=#{mode}"
21
44
  end
22
45
  end
46
+ private :setup
47
+
48
+ ##############################
49
+ ## Shopify Object Classes ##
50
+ ##############################
23
51
 
24
52
  # /admin/blogs.xml
25
53
  class Blog < ShopifyModel
@@ -33,7 +61,7 @@ module Shopify
33
61
 
34
62
  # /admin/blogs/[blog_id]/articles.xml
35
63
  class Article < ShopifyModel
36
- child_of Blog
64
+ children_of Blog
37
65
  attr_accessor :author, :blog_id, :body, :body_html, :created_at, :id, :published_at, :title, :updated_at
38
66
  def comments(query_params={})
39
67
  Shopify.comments(query_params.merge(:article_id => id, :blog_id => blog_id))
@@ -85,9 +113,14 @@ module Shopify
85
113
  end
86
114
  end
87
115
 
116
+ class LineItem < ShopifyModel
117
+ children_of Order
118
+ attr_accessor :fulfillment_service, :grams, :id, :price, :quantity, :sku, :title, :variant_id, :vendor, :name, :product_title
119
+ end
120
+
88
121
  # /admin/orders/[order_id]/fulfillments.xml
89
122
  class Fulfillment < ShopifyModel
90
- child_of Order
123
+ children_of Order
91
124
  attr_accessor :id, :order_id, :status, :tracking_number, :line_items, :receipt
92
125
  end
93
126
 
@@ -113,19 +146,19 @@ module Shopify
113
146
 
114
147
  # /admin/products/[product_id]/images.xml
115
148
  class Image < ShopifyModel
116
- child_of Product
149
+ children_of Product
117
150
  attr_accessor :id, :position, :product_id, :src
118
151
  end
119
152
 
120
153
  # /admin/products/[product_id]/variants.xml
121
154
  class Variant < ShopifyModel
122
- child_of Product
155
+ children_of Product
123
156
  attr_accessor :compare_at_price, :fulfillment_service, :grams, :id, :inventory_management, :inventory_policy, :inventory_quantity, :position, :price, :product_id, :sku, :title
124
157
  end
125
158
 
126
159
  # /admin/countries/[country_id]/provinces.xml
127
160
  class Province < ShopifyModel
128
- child_of Country
161
+ children_of Country
129
162
  attr_accessor :code, :id, :name, :tax
130
163
  end
131
164
 
@@ -143,7 +176,7 @@ module Shopify
143
176
 
144
177
  # /admin/orders/[order_id]/transactions.xml
145
178
  class Transaction < ShopifyModel
146
- child_of Order
179
+ children_of Order
147
180
  attr_accessor :amount, :authorization, :created_at, :kind, :order_id, :status, :receipt
148
181
  end
149
182
  end
@@ -0,0 +1,9 @@
1
+ # :nodoc:all
2
+
3
+ require 'quickbooks/extlib/class'
4
+ require 'quickbooks/extlib/object'
5
+ require 'quickbooks/extlib/string'
6
+ require 'quickbooks/extlib/hash'
7
+ require 'quickbooks/extlib/time'
8
+ require 'quickbooks/extlib/assertions'
9
+ require 'quickbooks/extlib/inflection'
@@ -0,0 +1,8 @@
1
+ module Extlib # :nodoc:all
2
+ module Assertions
3
+ def assert_kind_of(name, value, *klasses)
4
+ klasses.each { |k| return if value.kind_of?(k) }
5
+ raise ArgumentError, "+#{name}+ should be #{klasses.map { |k| k.name } * ' or '}, but was #{value.class.name}", caller(2)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,98 @@
1
+ # Copyright (c) 2004-2008 David Heinemeier Hansson
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ # Allows attributes to be shared within an inheritance hierarchy, but where
23
+ # each descendant gets a copy of their parents' attributes, instead of just a
24
+ # pointer to the same. This means that the child can add elements to, for
25
+ # example, an array without those additions being shared with either their
26
+ # parent, siblings, or children, which is unlike the regular class-level
27
+ # attributes that are shared across the entire hierarchy.
28
+ class Class # :nodoc:all
29
+ # Defines class-level and instance-level attribute reader.
30
+ #
31
+ # @param *syms<Array> Array of attributes to define reader for.
32
+ # @return <Array[#to_s]> List of attributes that were made into cattr_readers
33
+ #
34
+ # @api public
35
+ #
36
+ # @todo Is this inconsistent in that it does not allow you to prevent
37
+ # an instance_reader via :instance_reader => false
38
+ def cattr_reader(*syms)
39
+ syms.flatten.each do |sym|
40
+ next if sym.is_a?(Hash)
41
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
42
+ unless defined? @@#{sym}
43
+ @@#{sym} = nil
44
+ end
45
+
46
+ def self.#{sym}
47
+ @@#{sym}
48
+ end
49
+
50
+ def #{sym}
51
+ @@#{sym}
52
+ end
53
+ RUBY
54
+ end
55
+ end
56
+
57
+ # Defines class-level (and optionally instance-level) attribute writer.
58
+ #
59
+ # @param <Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to define writer for.
60
+ # @option syms :instance_writer<Boolean> if true, instance-level attribute writer is defined.
61
+ # @return <Array[#to_s]> List of attributes that were made into cattr_writers
62
+ #
63
+ # @api public
64
+ def cattr_writer(*syms)
65
+ options = syms.last.is_a?(Hash) ? syms.pop : {}
66
+ syms.flatten.each do |sym|
67
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
68
+ unless defined? @@#{sym}
69
+ @@#{sym} = nil
70
+ end
71
+
72
+ def self.#{sym}=(obj)
73
+ @@#{sym} = obj
74
+ end
75
+ RUBY
76
+
77
+ unless options[:instance_writer] == false
78
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
79
+ def #{sym}=(obj)
80
+ @@#{sym} = obj
81
+ end
82
+ RUBY
83
+ end
84
+ end
85
+ end
86
+
87
+ # Defines class-level (and optionally instance-level) attribute accessor.
88
+ #
89
+ # @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to define accessor for.
90
+ # @option syms :instance_writer<Boolean> if true, instance-level attribute writer is defined.
91
+ # @return <Array[#to_s]> List of attributes that were made into accessors
92
+ #
93
+ # @api public
94
+ def cattr_accessor(*syms)
95
+ cattr_reader(*syms)
96
+ cattr_writer(*syms)
97
+ end
98
+ end
@@ -0,0 +1,327 @@
1
+ class Hash # :nodoc:all
2
+ ##
3
+ # Convert to URL query param string
4
+ #
5
+ # { :name => "Bob",
6
+ # :address => {
7
+ # :street => '111 Ruby Ave.',
8
+ # :city => 'Ruby Central',
9
+ # :phones => ['111-111-1111', '222-222-2222']
10
+ # }
11
+ # }.to_params
12
+ # #=> "name=Bob&address[city]=Ruby Central&address[phones][]=111-111-1111&address[phones][]=222-222-2222&address[street]=111 Ruby Ave."
13
+ #
14
+ # @return [String] This hash as a query string
15
+ #
16
+ # @api public
17
+ def to_params
18
+ params = self.map { |k,v| normalize_param(k,v) }.join
19
+ params.chop! # trailing &
20
+ params
21
+ end
22
+
23
+ ##
24
+ # Convert a key, value pair into a URL query param string
25
+ #
26
+ # normalize_param(:name, "Bob") #=> "name=Bob&"
27
+ #
28
+ # @param [Object] key The key for the param.
29
+ # @param [Object] value The value for the param.
30
+ #
31
+ # @return <String> This key value pair as a param
32
+ #
33
+ # @api public
34
+ def normalize_param(key, value)
35
+ param = ''
36
+ stack = []
37
+
38
+ if value.is_a?(Array)
39
+ param << value.map { |element| normalize_param("#{key}[]", element) }.join
40
+ elsif value.is_a?(Hash)
41
+ stack << [key,value]
42
+ else
43
+ param << "#{key}=#{value}&"
44
+ end
45
+
46
+ stack.each do |parent, hash|
47
+ hash.each do |key, value|
48
+ if value.is_a?(Hash)
49
+ stack << ["#{parent}[#{key}]", value]
50
+ else
51
+ param << normalize_param("#{parent}[#{key}]", value)
52
+ end
53
+ end
54
+ end
55
+
56
+ param
57
+ end
58
+
59
+ ##
60
+ # Create a hash with *only* key/value pairs in receiver and +allowed+
61
+ #
62
+ # { :one => 1, :two => 2, :three => 3 }.only(:one) #=> { :one => 1 }
63
+ #
64
+ # @param [Array[String, Symbol]] *allowed The hash keys to include.
65
+ #
66
+ # @return [Hash] A new hash with only the selected keys.
67
+ #
68
+ # @api public
69
+ def only(*allowed)
70
+ hash = {}
71
+ allowed.each {|k| hash[k] = self[k] if self.has_key?(k) }
72
+ hash
73
+ end
74
+
75
+ ##
76
+ # Create a hash with all key/value pairs in receiver *except* +rejected+
77
+ #
78
+ # { :one => 1, :two => 2, :three => 3 }.except(:one)
79
+ # #=> { :two => 2, :three => 3 }
80
+ #
81
+ # @param [Array[String, Symbol]] *rejected The hash keys to exclude.
82
+ #
83
+ # @return [Hash] A new hash without the selected keys.
84
+ #
85
+ # @api public
86
+ def except(*rejected)
87
+ hash = self.dup
88
+ rejected.each {|k| hash.delete(k) }
89
+ hash
90
+ end
91
+
92
+ # @return <String> The hash as attributes for an XML tag.
93
+ #
94
+ # @example
95
+ # { :one => 1, "two"=>"TWO" }.to_xml_attributes
96
+ # #=> 'one="1" two="TWO"'
97
+ def to_xml_attributes
98
+ map do |k,v|
99
+ %{#{k.to_s.snake_case.sub(/^(.{1,1})/) { |m| m.downcase }}="#{v}"}
100
+ end.join(' ')
101
+ end
102
+
103
+ alias_method :to_html_attributes, :to_xml_attributes
104
+
105
+ # @param html_class<#to_s>
106
+ # The HTML class to add to the :class key. The html_class will be
107
+ # concatenated to any existing classes.
108
+ #
109
+ # @example hash[:class] #=> nil
110
+ # @example hash.add_html_class!(:selected)
111
+ # @example hash[:class] #=> "selected"
112
+ # @example hash.add_html_class!("class1 class2")
113
+ # @example hash[:class] #=> "selected class1 class2"
114
+ def add_html_class!(html_class)
115
+ if self[:class]
116
+ self[:class] = "#{self[:class]} #{html_class}"
117
+ else
118
+ self[:class] = html_class.to_s
119
+ end
120
+ end
121
+
122
+ # Converts all keys into string values. This is used during reloading to
123
+ # prevent problems when classes are no longer declared.
124
+ #
125
+ # @return <Array> An array of they hash's keys
126
+ #
127
+ # @example
128
+ # hash = { One => 1, Two => 2 }.proctect_keys!
129
+ # hash # => { "One" => 1, "Two" => 2 }
130
+ def protect_keys!
131
+ keys.each {|key| self[key.to_s] = delete(key) }
132
+ end
133
+
134
+ # Attempts to convert all string keys into Class keys. We run this after
135
+ # reloading to convert protected hashes back into usable hashes.
136
+ #
137
+ # @example
138
+ # # Provided that classes One and Two are declared in this scope:
139
+ # hash = { "One" => 1, "Two" => 2 }.unproctect_keys!
140
+ # hash # => { One => 1, Two => 2 }
141
+ def unprotect_keys!
142
+ keys.each do |key|
143
+ (self[Object.full_const_get(key)] = delete(key)) rescue nil
144
+ end
145
+ end
146
+
147
+ # Destructively and non-recursively convert each key to an uppercase string,
148
+ # deleting nil values along the way.
149
+ #
150
+ # @return <Hash> The newly environmentized hash.
151
+ #
152
+ # @example
153
+ # { :name => "Bob", :contact => { :email => "bob@bob.com" } }.environmentize_keys!
154
+ # #=> { "NAME" => "Bob", "CONTACT" => { :email => "bob@bob.com" } }
155
+ def environmentize_keys!
156
+ keys.each do |key|
157
+ val = delete(key)
158
+ next if val.nil?
159
+ self[key.to_s.upcase] = val
160
+ end
161
+ self
162
+ end
163
+ end
164
+
165
+ require 'rexml/parsers/streamparser'
166
+ require 'rexml/parsers/baseparser'
167
+ require 'rexml/light/node'
168
+
169
+ # This is a slighly modified version of the XMLUtilityNode from
170
+ # http://merb.devjavu.com/projects/merb/ticket/95 (has.sox@gmail.com)
171
+ # It's mainly just adding vowels, as I ht cd wth n vwls :)
172
+ # This represents the hard part of the work, all I did was change the
173
+ # underlying parser.
174
+ class REXMLUtilityNode # :nodoc:all
175
+ attr_accessor :name, :attributes, :children, :type
176
+ cattr_accessor :typecasts, :available_typecasts
177
+
178
+ self.typecasts = {}
179
+ self.typecasts["integer"] = lambda{|v| v.nil? ? nil : v.to_i}
180
+ self.typecasts["boolean"] = lambda{|v| v.nil? ? nil : (v.strip != "false")}
181
+ self.typecasts["datetime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
182
+ self.typecasts["date"] = lambda{|v| v.nil? ? nil : Date.parse(v)}
183
+ self.typecasts["dateTime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
184
+ self.typecasts["decimal"] = lambda{|v| BigDecimal(v)}
185
+ self.typecasts["double"] = lambda{|v| v.nil? ? nil : v.to_f}
186
+ self.typecasts["float"] = lambda{|v| v.nil? ? nil : v.to_f}
187
+ self.typecasts["symbol"] = lambda{|v| v.to_sym}
188
+ self.typecasts["string"] = lambda{|v| v.to_s}
189
+ self.typecasts["yaml"] = lambda{|v| v.nil? ? nil : YAML.load(v)}
190
+ self.typecasts["base64Binary"] = lambda{|v| v.unpack('m').first }
191
+
192
+ self.available_typecasts = self.typecasts.keys
193
+
194
+ def initialize(name, attributes = {})
195
+ @name = name.tr("-", "_")
196
+ # leave the type alone if we don't know what it is
197
+ @type = self.class.available_typecasts.include?(attributes["type"]) ? attributes.delete("type") : attributes["type"]
198
+
199
+ @nil_element = attributes.delete("nil") == "true"
200
+ @attributes = undasherize_keys(attributes)
201
+ @children = []
202
+ @text = false
203
+ end
204
+
205
+ def add_node(node)
206
+ @text = true if node.is_a? String
207
+ @children << node
208
+ end
209
+
210
+ def to_hash
211
+ if @type == "file"
212
+ f = StringIO.new((@children.first || '').unpack('m').first)
213
+ class << f
214
+ attr_accessor :original_filename, :content_type
215
+ end
216
+ f.original_filename = attributes['name'] || 'untitled'
217
+ f.content_type = attributes['content_type'] || 'application/octet-stream'
218
+ return {name => f}
219
+ end
220
+
221
+ if @text
222
+ return { name => typecast_value( translate_xml_entities( inner_html ) ) }
223
+ else
224
+ #change repeating groups into an array
225
+ groups = @children.inject({}) { |s,e| (s[e.name] ||= []) << e; s }
226
+
227
+ out = nil
228
+ if @type == "array"
229
+ out = []
230
+ groups.each do |k, v|
231
+ if v.size == 1
232
+ out << v.first.to_hash.entries.first.last
233
+ else
234
+ out << v.map{|e| e.to_hash[k]}
235
+ end
236
+ end
237
+ out = out.flatten
238
+
239
+ else # If Hash
240
+ out = {}
241
+ groups.each do |k,v|
242
+ if v.size == 1
243
+ out.merge!(v.first)
244
+ else
245
+ out.merge!( k => v.map{|e| e.to_hash[k]})
246
+ end
247
+ end
248
+ out.merge! attributes unless attributes.empty?
249
+ out = out.empty? ? nil : out
250
+ end
251
+
252
+ if @type && out.nil?
253
+ { name => typecast_value(out) }
254
+ else
255
+ { name => out }
256
+ end
257
+ end
258
+ end
259
+
260
+ # Typecasts a value based upon its type. For instance, if
261
+ # +node+ has #type == "integer",
262
+ # {{[node.typecast_value("12") #=> 12]}}
263
+ #
264
+ # @param value<String> The value that is being typecast.
265
+ #
266
+ # @details [:type options]
267
+ # "integer"::
268
+ # converts +value+ to an integer with #to_i
269
+ # "boolean"::
270
+ # checks whether +value+, after removing spaces, is the literal
271
+ # "true"
272
+ # "datetime"::
273
+ # Parses +value+ using Time.parse, and returns a UTC Time
274
+ # "date"::
275
+ # Parses +value+ using Date.parse
276
+ #
277
+ # @return <Integer, TrueClass, FalseClass, Time, Date, Object>
278
+ # The result of typecasting +value+.
279
+ #
280
+ # @note
281
+ # If +self+ does not have a "type" key, or if it's not one of the
282
+ # options specified above, the raw +value+ will be returned.
283
+ def typecast_value(value)
284
+ return value unless @type
285
+ proc = self.class.typecasts[@type]
286
+ proc.nil? ? value : proc.call(value)
287
+ end
288
+
289
+ # Convert basic XML entities into their literal values.
290
+ #
291
+ # @param value<#gsub> An XML fragment.
292
+ #
293
+ # @return <#gsub> The XML fragment after converting entities.
294
+ def translate_xml_entities(value)
295
+ value.gsub(/&lt;/, "<").
296
+ gsub(/&gt;/, ">").
297
+ gsub(/&quot;/, '"').
298
+ gsub(/&apos;/, "'").
299
+ gsub(/&amp;/, "&")
300
+ end
301
+
302
+ # Take keys of the form foo-bar and convert them to foo_bar
303
+ def undasherize_keys(params)
304
+ params.keys.each do |key, value|
305
+ params[key.tr("-", "_")] = params.delete(key)
306
+ end
307
+ params
308
+ end
309
+
310
+ # Get the inner_html of the REXML node.
311
+ def inner_html
312
+ @children.join
313
+ end
314
+
315
+ # Converts the node into a readable HTML node.
316
+ #
317
+ # @return <String> The HTML node in text form.
318
+ def to_html
319
+ attributes.merge!(:type => @type ) if @type
320
+ "<#{name}#{attributes.to_xml_attributes}>#{@nil_element ? '' : inner_html}</#{name}>"
321
+ end
322
+
323
+ # @alias #to_html #to_s
324
+ def to_s
325
+ to_html
326
+ end
327
+ end