mdub-crack 0.1.4

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.
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ *.gem
data/History ADDED
@@ -0,0 +1,15 @@
1
+ == 0.1.3 2009-06-22
2
+ * 1 minor patch
3
+ * Parsing a text node with attributes stores them in the attributes method (tamalw)
4
+
5
+ == 0.1.2 2009-04-21
6
+ * 2 minor patches
7
+ * Correct unnormalization of attribute values (der-flo)
8
+ * Fix error in parsing YAML in the case where a hash value ends with backslashes, and there are subsequent values in the hash (deadprogrammer)
9
+
10
+ == 0.1.1 2009-03-31
11
+ * 1 minor patch
12
+ * Parsing empty or blank xml now returns empty hash instead of raising error.
13
+
14
+ == 0.1.0 2009-03-28
15
+ * Initial release.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 John Nunemaker
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.
data/README.rdoc ADDED
@@ -0,0 +1,42 @@
1
+ = crack
2
+
3
+ Really simple JSON and XML parsing, ripped from Merb and Rails. The XML parser is ripped from Merb and the JSON parser is ripped from Rails. I take no credit, just packaged them for all to enjoy and easily use.
4
+
5
+ == note on releases
6
+
7
+ Releases are tagged on github and also released as gems on github and rubyforge. Master is pushed to whenever I add a patch or a new feature. To build from master, you can clone the code, generate the updated gemspec, build the gem and install.
8
+
9
+ * rake gemspec
10
+ * gem build httparty.gemspec
11
+ * gem install the gem that was built
12
+
13
+ == note on patches/pull requests
14
+
15
+ * Fork the project.
16
+ * Make your feature addition or bug fix.
17
+ * Add tests for it. This is important so I don't break it in a future version unintentionally.
18
+ * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself in another branch so I can ignore when I pull)
19
+ * Send me a pull request. Bonus points for topic branches.
20
+
21
+ == usage
22
+
23
+ gem 'crack'
24
+ require 'crack' # for xml and json
25
+ require 'crack/json' # for just json
26
+ require 'crack/xml' # for just xml
27
+
28
+ == examples
29
+
30
+ Crack::XML.parse("<tag>This is the contents</tag>")
31
+ # => {'tag' => 'This is the contents'}
32
+
33
+ Crack::JSON.parse('{"tag":"This is the contents"}')
34
+ # => {'tag' => 'This is the contents'}
35
+
36
+ == Copyright
37
+
38
+ Copyright (c) 2009 John Nunemaker. See LICENSE for details.
39
+
40
+ == Docs
41
+
42
+ http://rdoc.info/projects/jnunemaker/crack
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "crack"
8
+ gem.summary = %Q{Really simple JSON and XML parsing, ripped from Merb and Rails.}
9
+ gem.email = "nunemaker@gmail.com"
10
+ gem.homepage = "http://github.com/jnunemaker/crack"
11
+ gem.authors = ["John Nunemaker"]
12
+ gem.rubyforge_project = 'crack'
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ rescue LoadError
16
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
17
+ end
18
+
19
+ require 'rake/rdoctask'
20
+ Rake::RDocTask.new do |rdoc|
21
+ rdoc.rdoc_dir = 'rdoc'
22
+ rdoc.title = 'crack'
23
+ rdoc.options << '--line-numbers' << '--inline-source'
24
+ rdoc.rdoc_files.include('README*')
25
+ rdoc.rdoc_files.include('lib/**/*.rb')
26
+ end
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new(:test) do |test|
30
+ test.libs << 'lib' << 'test'
31
+ test.pattern = 'test/**/*_test.rb'
32
+ test.verbose = false
33
+ end
34
+
35
+ begin
36
+ require 'rcov/rcovtask'
37
+ Rcov::RcovTask.new do |test|
38
+ test.libs << 'test'
39
+ test.pattern = 'test/**/*_test.rb'
40
+ test.verbose = true
41
+ end
42
+ rescue LoadError
43
+ task :rcov do
44
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
45
+ end
46
+ end
47
+
48
+
49
+ task :default => :test
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 4
data/crack.gemspec ADDED
@@ -0,0 +1,61 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{crack}
5
+ s.version = "0.1.4"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["John Nunemaker"]
9
+ s.date = %q{2009-07-19}
10
+ s.email = %q{nunemaker@gmail.com}
11
+ s.extra_rdoc_files = [
12
+ "LICENSE",
13
+ "README.rdoc"
14
+ ]
15
+ s.files = [
16
+ ".gitignore",
17
+ "History",
18
+ "LICENSE",
19
+ "README.rdoc",
20
+ "Rakefile",
21
+ "VERSION.yml",
22
+ "crack.gemspec",
23
+ "lib/crack.rb",
24
+ "lib/crack/core_extensions.rb",
25
+ "lib/crack/json.rb",
26
+ "lib/crack/xml.rb",
27
+ "test/crack_test.rb",
28
+ "test/data/twittersearch-firefox.json",
29
+ "test/data/twittersearch-ie.json",
30
+ "test/hash_test.rb",
31
+ "test/json_test.rb",
32
+ "test/string_test.rb",
33
+ "test/test_helper.rb",
34
+ "test/xml_test.rb"
35
+ ]
36
+ s.has_rdoc = true
37
+ s.homepage = %q{http://github.com/jnunemaker/crack}
38
+ s.rdoc_options = ["--charset=UTF-8"]
39
+ s.require_paths = ["lib"]
40
+ s.rubyforge_project = %q{crack}
41
+ s.rubygems_version = %q{1.3.1}
42
+ s.summary = %q{Really simple JSON and XML parsing, ripped from Merb and Rails.}
43
+ s.test_files = [
44
+ "test/crack_test.rb",
45
+ "test/hash_test.rb",
46
+ "test/json_test.rb",
47
+ "test/string_test.rb",
48
+ "test/test_helper.rb",
49
+ "test/xml_test.rb"
50
+ ]
51
+
52
+ if s.respond_to? :specification_version then
53
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
54
+ s.specification_version = 2
55
+
56
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
57
+ else
58
+ end
59
+ else
60
+ end
61
+ end
data/lib/crack.rb ADDED
@@ -0,0 +1,7 @@
1
+ module Crack
2
+ class ParseError < StandardError; end
3
+ end
4
+
5
+ require 'crack/core_extensions'
6
+ require 'crack/json'
7
+ require 'crack/xml'
@@ -0,0 +1,128 @@
1
+ require 'uri'
2
+
3
+ class Object #:nodoc:
4
+ # @return <TrueClass, FalseClass>
5
+ #
6
+ # @example [].blank? #=> true
7
+ # @example [1].blank? #=> false
8
+ # @example [nil].blank? #=> false
9
+ #
10
+ # Returns true if the object is nil or empty (if applicable)
11
+ def blank?
12
+ nil? || (respond_to?(:empty?) && empty?)
13
+ end unless method_defined?(:blank?)
14
+ end # class Object
15
+
16
+ class Numeric #:nodoc:
17
+ # @return <TrueClass, FalseClass>
18
+ #
19
+ # Numerics can't be blank
20
+ def blank?
21
+ false
22
+ end unless method_defined?(:blank?)
23
+ end # class Numeric
24
+
25
+ class NilClass #:nodoc:
26
+ # @return <TrueClass, FalseClass>
27
+ #
28
+ # Nils are always blank
29
+ def blank?
30
+ true
31
+ end unless method_defined?(:blank?)
32
+ end # class NilClass
33
+
34
+ class TrueClass #:nodoc:
35
+ # @return <TrueClass, FalseClass>
36
+ #
37
+ # True is not blank.
38
+ def blank?
39
+ false
40
+ end unless method_defined?(:blank?)
41
+ end # class TrueClass
42
+
43
+ class FalseClass #:nodoc:
44
+ # False is always blank.
45
+ def blank?
46
+ true
47
+ end unless method_defined?(:blank?)
48
+ end # class FalseClass
49
+
50
+ class String #:nodoc:
51
+ # @example "".blank? #=> true
52
+ # @example " ".blank? #=> true
53
+ # @example " hey ho ".blank? #=> false
54
+ #
55
+ # @return <TrueClass, FalseClass>
56
+ #
57
+ # Strips out whitespace then tests if the string is empty.
58
+ def blank?
59
+ strip.empty?
60
+ end unless method_defined?(:blank?)
61
+
62
+ def snake_case
63
+ return self.downcase if self =~ /^[A-Z]+$/
64
+ self.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/, '_\&') =~ /_*(.*)/
65
+ return $+.downcase
66
+ end unless method_defined?(:snake_case)
67
+ end # class String
68
+
69
+ class Hash #:nodoc:
70
+ # @return <String> This hash as a query string
71
+ #
72
+ # @example
73
+ # { :name => "Bob",
74
+ # :address => {
75
+ # :street => '111 Ruby Ave.',
76
+ # :city => 'Ruby Central',
77
+ # :phones => ['111-111-1111', '222-222-2222']
78
+ # }
79
+ # }.to_params
80
+ # #=> "name=Bob&address[city]=Ruby Central&address[phones][]=111-111-1111&address[phones][]=222-222-2222&address[street]=111 Ruby Ave."
81
+ def to_params
82
+ params = self.map { |k,v| normalize_param(k,v) }.join
83
+ params.chop! # trailing &
84
+ params
85
+ end
86
+
87
+ # @param key<Object> The key for the param.
88
+ # @param value<Object> The value for the param.
89
+ #
90
+ # @return <String> This key value pair as a param
91
+ #
92
+ # @example normalize_param(:name, "Bob Jones") #=> "name=Bob%20Jones&"
93
+ def normalize_param(key, value)
94
+ param = ''
95
+ stack = []
96
+
97
+ if value.is_a?(Array)
98
+ param << value.map { |element| normalize_param("#{key}[]", element) }.join
99
+ elsif value.is_a?(Hash)
100
+ stack << [key,value]
101
+ else
102
+ param << "#{key}=#{URI.encode(value.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))}&"
103
+ end
104
+
105
+ stack.each do |parent, hash|
106
+ hash.each do |key, value|
107
+ if value.is_a?(Hash)
108
+ stack << ["#{parent}[#{key}]", value]
109
+ else
110
+ param << normalize_param("#{parent}[#{key}]", value)
111
+ end
112
+ end
113
+ end
114
+
115
+ param
116
+ end
117
+
118
+ # @return <String> The hash as attributes for an XML tag.
119
+ #
120
+ # @example
121
+ # { :one => 1, "two"=>"TWO" }.to_xml_attributes
122
+ # #=> 'one="1" two="TWO"'
123
+ def to_xml_attributes
124
+ map do |k,v|
125
+ %{#{k.to_s.snake_case.sub(/^(.{1,1})/) { |m| m.downcase }}="#{v}"}
126
+ end.join(' ')
127
+ end
128
+ end
data/lib/crack/json.rb ADDED
@@ -0,0 +1,68 @@
1
+ # Copyright (c) 2004-2008 David Heinemeier Hansson
2
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
3
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
4
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
5
+
6
+ require 'yaml'
7
+ require 'strscan'
8
+
9
+ module Crack
10
+ class JSON
11
+ def self.parse(json)
12
+ YAML.load(unescape(convert_json_to_yaml(json)))
13
+ rescue ArgumentError => e
14
+ raise ParseError, "Invalid JSON string"
15
+ end
16
+
17
+ protected
18
+ def self.unescape(str)
19
+ str.gsub(/\\u([0-9a-f]{4})/) { [$1.hex].pack("U") }
20
+ end
21
+
22
+ # matches YAML-formatted dates
23
+ DATE_REGEX = /^\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[ \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?)?$/
24
+
25
+ # Ensure that ":" and "," are always followed by a space
26
+ def self.convert_json_to_yaml(json) #:nodoc:
27
+ scanner, quoting, marks, pos, times = StringScanner.new(json), false, [], nil, []
28
+ while scanner.scan_until(/(\\['"]|['":,\\]|\\.)/)
29
+ case char = scanner[1]
30
+ when '"', "'"
31
+ if !quoting
32
+ quoting = char
33
+ pos = scanner.pos
34
+ elsif quoting == char
35
+ if json[pos..scanner.pos-2] =~ DATE_REGEX
36
+ # found a date, track the exact positions of the quotes so we can remove them later.
37
+ # oh, and increment them for each current mark, each one is an extra padded space that bumps
38
+ # the position in the final YAML output
39
+ total_marks = marks.size
40
+ times << pos+total_marks << scanner.pos+total_marks
41
+ end
42
+ quoting = false
43
+ end
44
+ when ":",","
45
+ marks << scanner.pos - 1 unless quoting
46
+ when "\\"
47
+ scanner.skip(/\\/)
48
+ end
49
+ end
50
+
51
+ if marks.empty?
52
+ json.gsub(/\\\//, '/')
53
+ else
54
+ left_pos = [-1].push(*marks)
55
+ right_pos = marks << json.length
56
+ output = []
57
+ left_pos.each_with_index do |left, i|
58
+ output << json[left.succ..right_pos[i]]
59
+ end
60
+ output = output * " "
61
+
62
+ times.each { |i| output[i-1] = ' ' }
63
+ output.gsub!(/\\\//, '/')
64
+ output
65
+ end
66
+ end
67
+ end
68
+ end
data/lib/crack/xml.rb ADDED
@@ -0,0 +1,229 @@
1
+ require 'rexml/parsers/streamparser'
2
+ require 'rexml/parsers/baseparser'
3
+ require 'rexml/light/node'
4
+ require 'rexml/text'
5
+ require 'date'
6
+ require 'time'
7
+ require 'yaml'
8
+ require 'bigdecimal'
9
+
10
+ # This is a slighly modified version of the XMLUtilityNode from
11
+ # http://merb.devjavu.com/projects/merb/ticket/95 (has.sox@gmail.com)
12
+ # It's mainly just adding vowels, as I ht cd wth n vwls :)
13
+ # This represents the hard part of the work, all I did was change the
14
+ # underlying parser.
15
+ class REXMLUtilityNode #:nodoc:
16
+ attr_accessor :name, :attributes, :children, :type
17
+
18
+ def self.typecasts
19
+ @@typecasts
20
+ end
21
+
22
+ def self.typecasts=(obj)
23
+ @@typecasts = obj
24
+ end
25
+
26
+ def self.available_typecasts
27
+ @@available_typecasts
28
+ end
29
+
30
+ def self.available_typecasts=(obj)
31
+ @@available_typecasts = obj
32
+ end
33
+
34
+ self.typecasts = {}
35
+ self.typecasts["integer"] = lambda{|v| v.nil? ? nil : v.to_i}
36
+ self.typecasts["boolean"] = lambda{|v| v.nil? ? nil : (v.strip != "false")}
37
+ self.typecasts["datetime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
38
+ self.typecasts["date"] = lambda{|v| v.nil? ? nil : Date.parse(v)}
39
+ self.typecasts["dateTime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
40
+ self.typecasts["decimal"] = lambda{|v| v.nil? ? nil : BigDecimal(v.to_s)}
41
+ self.typecasts["double"] = lambda{|v| v.nil? ? nil : v.to_f}
42
+ self.typecasts["float"] = lambda{|v| v.nil? ? nil : v.to_f}
43
+ self.typecasts["symbol"] = lambda{|v| v.nil? ? nil : v.to_sym}
44
+ self.typecasts["string"] = lambda{|v| v.to_s}
45
+ self.typecasts["yaml"] = lambda{|v| v.nil? ? nil : YAML.load(v)}
46
+ self.typecasts["base64Binary"] = lambda{|v| v.unpack('m').first }
47
+
48
+ self.available_typecasts = self.typecasts.keys
49
+
50
+ def initialize(name, normalized_attributes = {})
51
+
52
+ # unnormalize attribute values
53
+ attributes = Hash[* normalized_attributes.map { |key, value|
54
+ [ key, unnormalize_xml_entities(value) ]
55
+ }.flatten]
56
+
57
+ @name = name.tr("-", "_")
58
+ # leave the type alone if we don't know what it is
59
+ @type = self.class.available_typecasts.include?(attributes["type"]) ? attributes.delete("type") : attributes["type"]
60
+
61
+ @nil_element = attributes.delete("nil") == "true"
62
+ @attributes = undasherize_keys(attributes)
63
+ @children = []
64
+ @text = false
65
+ end
66
+
67
+ def add_node(node)
68
+ @text = true if node.is_a? String
69
+ @children << node
70
+ end
71
+
72
+ def to_hash
73
+ if @type == "file"
74
+ f = StringIO.new((@children.first || '').unpack('m').first)
75
+ class << f
76
+ attr_accessor :original_filename, :content_type
77
+ end
78
+ f.original_filename = attributes['name'] || 'untitled'
79
+ f.content_type = attributes['content_type'] || 'application/octet-stream'
80
+ return {name => f}
81
+ end
82
+
83
+ if @text
84
+ t = typecast_value( unnormalize_xml_entities( inner_html ) )
85
+ begin
86
+ t.extend(Crack::HasAttributes)
87
+ t.attributes = attributes
88
+ rescue TypeError => e
89
+ end
90
+ return { name => t }
91
+ else
92
+ #change repeating groups into an array
93
+ groups = @children.inject({}) { |s,e| (s[e.name] ||= []) << e; s }
94
+
95
+ out = nil
96
+ if @type == "array"
97
+ out = Crack::Array.new
98
+ groups.each do |k, v|
99
+ if v.size == 1
100
+ out << v.first.to_hash.entries.first.last
101
+ else
102
+ out << v.map{|e| e.to_hash[k]}
103
+ end
104
+ end
105
+ out.attributes = attributes.reject { |k,v| k == "type" }
106
+
107
+ out = out.flatten
108
+
109
+ else # If Hash
110
+ out = {}
111
+ groups.each do |k,v|
112
+ if v.size == 1
113
+ out.merge!(v.first)
114
+ else
115
+ out.merge!( k => v.map{|e| e.to_hash[k]})
116
+ end
117
+ end
118
+ out.merge! attributes unless attributes.empty?
119
+ out = out.empty? ? nil : out
120
+ end
121
+
122
+ if @type && out.nil?
123
+ { name => typecast_value(out) }
124
+ else
125
+ { name => out }
126
+ end
127
+ end
128
+ end
129
+
130
+ # Typecasts a value based upon its type. For instance, if
131
+ # +node+ has #type == "integer",
132
+ # {{[node.typecast_value("12") #=> 12]}}
133
+ #
134
+ # @param value<String> The value that is being typecast.
135
+ #
136
+ # @details [:type options]
137
+ # "integer"::
138
+ # converts +value+ to an integer with #to_i
139
+ # "boolean"::
140
+ # checks whether +value+, after removing spaces, is the literal
141
+ # "true"
142
+ # "datetime"::
143
+ # Parses +value+ using Time.parse, and returns a UTC Time
144
+ # "date"::
145
+ # Parses +value+ using Date.parse
146
+ #
147
+ # @return <Integer, TrueClass, FalseClass, Time, Date, Object>
148
+ # The result of typecasting +value+.
149
+ #
150
+ # @note
151
+ # If +self+ does not have a "type" key, or if it's not one of the
152
+ # options specified above, the raw +value+ will be returned.
153
+ def typecast_value(value)
154
+ return value unless @type
155
+ proc = self.class.typecasts[@type]
156
+ proc.nil? ? value : proc.call(value)
157
+ end
158
+
159
+ # Take keys of the form foo-bar and convert them to foo_bar
160
+ def undasherize_keys(params)
161
+ params.keys.each do |key, value|
162
+ params[key.tr("-", "_")] = params.delete(key)
163
+ end
164
+ params
165
+ end
166
+
167
+ # Get the inner_html of the REXML node.
168
+ def inner_html
169
+ @children.join
170
+ end
171
+
172
+ # Converts the node into a readable HTML node.
173
+ #
174
+ # @return <String> The HTML node in text form.
175
+ def to_html
176
+ attributes.merge!(:type => @type ) if @type
177
+ "<#{name}#{attributes.to_xml_attributes}>#{@nil_element ? '' : inner_html}</#{name}>"
178
+ end
179
+
180
+ # @alias #to_html #to_s
181
+ def to_s
182
+ to_html
183
+ end
184
+
185
+ private
186
+
187
+ def unnormalize_xml_entities value
188
+ REXML::Text.unnormalize(value)
189
+ end
190
+ end
191
+
192
+ module Crack
193
+
194
+ class XML
195
+ def self.parse(xml)
196
+ stack = []
197
+ parser = REXML::Parsers::BaseParser.new(xml)
198
+
199
+ while true
200
+ event = parser.pull
201
+ case event[0]
202
+ when :end_document
203
+ break
204
+ when :end_doctype, :start_doctype
205
+ # do nothing
206
+ when :start_element
207
+ stack.push REXMLUtilityNode.new(event[1], event[2])
208
+ when :end_element
209
+ if stack.size > 1
210
+ temp = stack.pop
211
+ stack.last.add_node(temp)
212
+ end
213
+ when :text, :cdata
214
+ stack.last.add_node(event[1]) unless event[1].strip.length == 0 || stack.empty?
215
+ end
216
+ end
217
+ stack.length > 0 ? stack.pop.to_hash : {}
218
+ end
219
+ end
220
+
221
+ module HasAttributes
222
+ attr_accessor :attributes
223
+ end
224
+
225
+ class Array < ::Array
226
+ include HasAttributes
227
+ end
228
+
229
+ end