ROXML 3.0.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.
- data/.gitignore +6 -0
- data/.gitmodules +3 -0
- data/History.txt +299 -0
- data/MIT-LICENSE +18 -0
- data/README.rdoc +161 -0
- data/Rakefile +95 -0
- data/TODO +39 -0
- data/VERSION +1 -0
- data/config/website.yml +2 -0
- data/examples/amazon.rb +35 -0
- data/examples/current_weather.rb +27 -0
- data/examples/dashed_elements.rb +20 -0
- data/examples/library.rb +40 -0
- data/examples/posts.rb +27 -0
- data/examples/rails.rb +70 -0
- data/examples/twitter.rb +37 -0
- data/examples/xml/active_record.xml +70 -0
- data/examples/xml/amazon.xml +133 -0
- data/examples/xml/current_weather.xml +89 -0
- data/examples/xml/dashed_elements.xml +52 -0
- data/examples/xml/posts.xml +23 -0
- data/examples/xml/twitter.xml +422 -0
- data/lib/roxml.rb +547 -0
- data/lib/roxml/definition.rb +236 -0
- data/lib/roxml/hash_definition.rb +25 -0
- data/lib/roxml/xml.rb +43 -0
- data/lib/roxml/xml/parsers/libxml.rb +91 -0
- data/lib/roxml/xml/parsers/nokogiri.rb +77 -0
- data/lib/roxml/xml/references.rb +297 -0
- data/roxml.gemspec +201 -0
- data/spec/definition_spec.rb +486 -0
- data/spec/examples/active_record_spec.rb +40 -0
- data/spec/examples/amazon_spec.rb +54 -0
- data/spec/examples/current_weather_spec.rb +37 -0
- data/spec/examples/dashed_elements_spec.rb +20 -0
- data/spec/examples/library_spec.rb +46 -0
- data/spec/examples/post_spec.rb +24 -0
- data/spec/examples/twitter_spec.rb +32 -0
- data/spec/roxml_spec.rb +372 -0
- data/spec/shared_specs.rb +15 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/libxml.rb +3 -0
- data/spec/support/nokogiri.rb +3 -0
- data/spec/xml/attributes_spec.rb +36 -0
- data/spec/xml/namespace_spec.rb +240 -0
- data/spec/xml/namespaces_spec.rb +32 -0
- data/spec/xml/parser_spec.rb +26 -0
- data/tasks/rdoc.rake +13 -0
- data/tasks/rspec.rake +25 -0
- data/tasks/test.rake +35 -0
- data/test/fixtures/book_malformed.xml +5 -0
- data/test/fixtures/book_pair.xml +8 -0
- data/test/fixtures/book_text_with_attribute.xml +5 -0
- data/test/fixtures/book_valid.xml +5 -0
- data/test/fixtures/book_with_authors.xml +7 -0
- data/test/fixtures/book_with_contributions.xml +9 -0
- data/test/fixtures/book_with_contributors.xml +7 -0
- data/test/fixtures/book_with_contributors_attrs.xml +7 -0
- data/test/fixtures/book_with_default_namespace.xml +9 -0
- data/test/fixtures/book_with_depth.xml +6 -0
- data/test/fixtures/book_with_octal_pages.xml +4 -0
- data/test/fixtures/book_with_publisher.xml +7 -0
- data/test/fixtures/book_with_wrapped_attr.xml +3 -0
- data/test/fixtures/dictionary_of_attr_name_clashes.xml +8 -0
- data/test/fixtures/dictionary_of_attrs.xml +6 -0
- data/test/fixtures/dictionary_of_guarded_names.xml +6 -0
- data/test/fixtures/dictionary_of_mixeds.xml +4 -0
- data/test/fixtures/dictionary_of_name_clashes.xml +10 -0
- data/test/fixtures/dictionary_of_names.xml +4 -0
- data/test/fixtures/dictionary_of_texts.xml +10 -0
- data/test/fixtures/library.xml +30 -0
- data/test/fixtures/library_uppercase.xml +30 -0
- data/test/fixtures/muffins.xml +3 -0
- data/test/fixtures/nameless_ageless_youth.xml +2 -0
- data/test/fixtures/node_with_attr_name_conflicts.xml +1 -0
- data/test/fixtures/node_with_name_conflicts.xml +4 -0
- data/test/fixtures/numerology.xml +4 -0
- data/test/fixtures/person.xml +1 -0
- data/test/fixtures/person_with_guarded_mothers.xml +13 -0
- data/test/fixtures/person_with_mothers.xml +10 -0
- data/test/mocks/dictionaries.rb +57 -0
- data/test/mocks/mocks.rb +279 -0
- data/test/support/fixtures.rb +11 -0
- data/test/test_helper.rb +34 -0
- data/test/unit/definition_test.rb +235 -0
- data/test/unit/deprecations_test.rb +24 -0
- data/test/unit/to_xml_test.rb +81 -0
- data/test/unit/xml_attribute_test.rb +39 -0
- data/test/unit/xml_block_test.rb +81 -0
- data/test/unit/xml_bool_test.rb +122 -0
- data/test/unit/xml_convention_test.rb +150 -0
- data/test/unit/xml_hash_test.rb +115 -0
- data/test/unit/xml_initialize_test.rb +49 -0
- data/test/unit/xml_name_test.rb +141 -0
- data/test/unit/xml_namespace_test.rb +31 -0
- data/test/unit/xml_object_test.rb +207 -0
- data/test/unit/xml_required_test.rb +94 -0
- data/test/unit/xml_text_test.rb +71 -0
- data/website/index.html +98 -0
- metadata +254 -0
@@ -0,0 +1,236 @@
|
|
1
|
+
require 'lib/roxml/hash_definition'
|
2
|
+
|
3
|
+
class Module
|
4
|
+
def bool_attr_reader(*attrs)
|
5
|
+
attrs.each do |attr|
|
6
|
+
define_method :"#{attr}?" do
|
7
|
+
instance_variable_get(:"@#{attr}") || false
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ROXML
|
14
|
+
class ContradictoryNamespaces < StandardError
|
15
|
+
end
|
16
|
+
|
17
|
+
class Definition # :nodoc:
|
18
|
+
attr_reader :name, :type, :wrapper, :hash, :blocks, :accessor, :to_xml, :attr_name, :namespace
|
19
|
+
bool_attr_reader :name_explicit, :array, :cdata, :required, :frozen
|
20
|
+
|
21
|
+
def initialize(sym, opts = {}, &block)
|
22
|
+
opts.assert_valid_keys(:from, :in, :as, :namespace,
|
23
|
+
:else, :required, :frozen, :cdata, :to_xml)
|
24
|
+
@default = opts.delete(:else)
|
25
|
+
@to_xml = opts.delete(:to_xml)
|
26
|
+
@name_explicit = opts.has_key?(:from) && opts[:from].is_a?(String)
|
27
|
+
@cdata = opts.delete(:cdata)
|
28
|
+
@required = opts.delete(:required)
|
29
|
+
@frozen = opts.delete(:frozen)
|
30
|
+
@wrapper = opts.delete(:in)
|
31
|
+
@namespace = opts.delete(:namespace)
|
32
|
+
|
33
|
+
@accessor = sym.to_s
|
34
|
+
opts[:as] ||=
|
35
|
+
if @accessor.ends_with?('?')
|
36
|
+
:bool
|
37
|
+
elsif @accessor.ends_with?('_on')
|
38
|
+
Date
|
39
|
+
elsif @accessor.ends_with?('_at')
|
40
|
+
DateTime
|
41
|
+
end
|
42
|
+
|
43
|
+
@array = opts[:as].is_a?(Array)
|
44
|
+
@blocks = collect_blocks(block, opts[:as])
|
45
|
+
|
46
|
+
@type = extract_type(opts[:as])
|
47
|
+
if @type.respond_to?(:roxml_tag_name)
|
48
|
+
opts[:from] ||= @type.roxml_tag_name
|
49
|
+
end
|
50
|
+
|
51
|
+
if opts[:from] == :content
|
52
|
+
opts[:from] = '.'
|
53
|
+
elsif opts[:from] == :name
|
54
|
+
opts[:from] = '*'
|
55
|
+
elsif opts[:from] == :attr
|
56
|
+
@type = :attr
|
57
|
+
opts[:from] = nil
|
58
|
+
elsif opts[:from] == :name
|
59
|
+
opts[:from] = '*'
|
60
|
+
elsif opts[:from].to_s.starts_with?('@')
|
61
|
+
@type = :attr
|
62
|
+
opts[:from].sub!('@', '')
|
63
|
+
end
|
64
|
+
|
65
|
+
@attr_name = accessor.to_s.chomp('?')
|
66
|
+
@name = (opts[:from] || @attr_name).to_s
|
67
|
+
@name = @name.singularize if hash? || array?
|
68
|
+
if hash? && (hash.key.name? || hash.value.name?)
|
69
|
+
@name = '*'
|
70
|
+
end
|
71
|
+
raise ContradictoryNamespaces if @name.include?(':') && (@namespace.present? || @namespace == false)
|
72
|
+
|
73
|
+
raise ArgumentError, "Can't specify both :else default and :required" if required? && @default
|
74
|
+
end
|
75
|
+
|
76
|
+
def instance_variable_name
|
77
|
+
:"@#{attr_name}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def setter
|
81
|
+
:"#{attr_name}="
|
82
|
+
end
|
83
|
+
|
84
|
+
def hash
|
85
|
+
if hash?
|
86
|
+
@type.wrapper ||= name
|
87
|
+
@type
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def hash?
|
92
|
+
@type.is_a?(HashDefinition)
|
93
|
+
end
|
94
|
+
|
95
|
+
def name?
|
96
|
+
@name == '*'
|
97
|
+
end
|
98
|
+
|
99
|
+
def content?
|
100
|
+
@name == '.'
|
101
|
+
end
|
102
|
+
|
103
|
+
def default
|
104
|
+
if @default.nil?
|
105
|
+
@default = [] if array?
|
106
|
+
@default = {} if hash?
|
107
|
+
end
|
108
|
+
@default.duplicable? ? @default.dup : @default
|
109
|
+
end
|
110
|
+
|
111
|
+
def to_ref(inst)
|
112
|
+
case type
|
113
|
+
when :attr then XMLAttributeRef
|
114
|
+
when :text then XMLTextRef
|
115
|
+
when HashDefinition then XMLHashRef
|
116
|
+
when Symbol then raise ArgumentError, "Invalid type argument #{type}"
|
117
|
+
else XMLObjectRef
|
118
|
+
end.new(self, inst)
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
def self.all(items, &block)
|
123
|
+
array = items.is_a?(Array)
|
124
|
+
results = (array ? items : [items]).map do |item|
|
125
|
+
yield item
|
126
|
+
end
|
127
|
+
|
128
|
+
array ? results : results.first
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.fetch_bool(value, default)
|
132
|
+
value = value.to_s.downcase
|
133
|
+
if %w{true yes 1 t}.include? value
|
134
|
+
true
|
135
|
+
elsif %w{false no 0 f}.include? value
|
136
|
+
false
|
137
|
+
else
|
138
|
+
default
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
CORE_BLOCK_SHORTHANDS = {
|
143
|
+
# Core Shorthands
|
144
|
+
Integer => lambda do |val|
|
145
|
+
all(val) do |v|
|
146
|
+
Integer(v) unless v.blank?
|
147
|
+
end
|
148
|
+
end,
|
149
|
+
Float => lambda do |val|
|
150
|
+
all(val) do |v|
|
151
|
+
Float(v) unless v.blank?
|
152
|
+
end
|
153
|
+
end,
|
154
|
+
Fixnum => lambda do |val|
|
155
|
+
all(val) do |v|
|
156
|
+
v.to_i unless v.blank?
|
157
|
+
end
|
158
|
+
end,
|
159
|
+
Time => lambda do |val|
|
160
|
+
all(val) {|v| Time.parse(v) unless v.blank? }
|
161
|
+
end,
|
162
|
+
|
163
|
+
:bool => nil,
|
164
|
+
:bool_standalone => lambda do |val|
|
165
|
+
all(val) do |v|
|
166
|
+
fetch_bool(v, nil)
|
167
|
+
end
|
168
|
+
end,
|
169
|
+
:bool_combined => lambda do |val|
|
170
|
+
all(val) do |v|
|
171
|
+
fetch_bool(v, v)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
}
|
175
|
+
|
176
|
+
def self.block_shorthands
|
177
|
+
# dynamically load these shorthands at class definition time, but
|
178
|
+
# only if they're already availbable
|
179
|
+
CORE_BLOCK_SHORTHANDS.tap do |blocks|
|
180
|
+
blocks.reverse_merge!(BigDecimal => lambda do |val|
|
181
|
+
all(val) do |v|
|
182
|
+
BigDecimal.new(v) unless v.blank?
|
183
|
+
end
|
184
|
+
end) if defined?(BigDecimal)
|
185
|
+
|
186
|
+
blocks.reverse_merge!(DateTime => lambda do |val|
|
187
|
+
if defined?(DateTime)
|
188
|
+
all(val) {|v| DateTime.parse(v) unless v.blank? }
|
189
|
+
end
|
190
|
+
end) if defined?(DateTime)
|
191
|
+
|
192
|
+
blocks.reverse_merge!(Date => lambda do |val|
|
193
|
+
if defined?(Date)
|
194
|
+
all(val) {|v| Date.parse(v) unless v.blank? }
|
195
|
+
end
|
196
|
+
end) if defined?(Date)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def collect_blocks(block, as)
|
201
|
+
if as.is_a?(Array)
|
202
|
+
if as.size > 1
|
203
|
+
raise ArgumentError, "multiple :as types (#{as.map(&:inspect).join(', ')}) is not supported. Use a block if you want more complicated behavior."
|
204
|
+
end
|
205
|
+
|
206
|
+
as = as.first
|
207
|
+
end
|
208
|
+
|
209
|
+
if as == :bool
|
210
|
+
# if a second block is present, and we can't coerce the xml value
|
211
|
+
# to bool, we need to be able to pass it to the user-provided block
|
212
|
+
as = (block ? :bool_combined : :bool_standalone)
|
213
|
+
end
|
214
|
+
as = self.class.block_shorthands.fetch(as) do
|
215
|
+
unless as.respond_to?(:from_xml) || (as.respond_to?(:first) && as.first.respond_to?(:from_xml)) || (as.is_a?(Hash) && !(as.keys & [:key, :value]).empty?)
|
216
|
+
raise ArgumentError, "Invalid :as argument #{as}" unless as.nil?
|
217
|
+
end
|
218
|
+
nil
|
219
|
+
end
|
220
|
+
[as, block].compact
|
221
|
+
end
|
222
|
+
|
223
|
+
def extract_type(as)
|
224
|
+
if as.is_a?(Hash)
|
225
|
+
return HashDefinition.new(as)
|
226
|
+
elsif as.respond_to?(:from_xml)
|
227
|
+
return as
|
228
|
+
elsif as.is_a?(Array) && as.first.respond_to?(:from_xml)
|
229
|
+
@array = true
|
230
|
+
return as.first
|
231
|
+
else
|
232
|
+
:text
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ROXML
|
2
|
+
class HashDefinition # :nodoc:
|
3
|
+
attr_reader :key, :value
|
4
|
+
attr_accessor :wrapper
|
5
|
+
|
6
|
+
def initialize(opts)
|
7
|
+
opts.assert_valid_keys(:key, :value)
|
8
|
+
|
9
|
+
@key = Definition.new(nil, to_definition_options(opts, :key))
|
10
|
+
@value = Definition.new(nil, to_definition_options(opts, :value))
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
def to_definition_options(opts, what)
|
15
|
+
case opts[what]
|
16
|
+
when Hash
|
17
|
+
opts[what]
|
18
|
+
when String, Symbol
|
19
|
+
{:from => opts[what]}
|
20
|
+
else
|
21
|
+
raise ArgumentError, "unrecognized hash parameter: #{what} => #{opts[what]}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/roxml/xml.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module ROXML
|
2
|
+
unless const_defined? 'XML_PARSER'
|
3
|
+
PREFERRED_PARSERS = %w[nokogiri libxml].freeze
|
4
|
+
parsers = PREFERRED_PARSERS.dup
|
5
|
+
begin
|
6
|
+
require parsers.first
|
7
|
+
XML_PARSER = parsers.first # :nodoc:
|
8
|
+
rescue LoadError
|
9
|
+
if parsers.size > 1
|
10
|
+
parsers.shift
|
11
|
+
retry
|
12
|
+
else
|
13
|
+
raise "Could not load either nokogiri or libxml"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
require File.join('lib/roxml/xml/parsers', XML_PARSER)
|
19
|
+
|
20
|
+
module XML
|
21
|
+
class Node
|
22
|
+
def self.from(data)
|
23
|
+
case data
|
24
|
+
when XML::Node
|
25
|
+
data
|
26
|
+
when XML::Document
|
27
|
+
data.root
|
28
|
+
when File, IO
|
29
|
+
Parser.parse_io(data).root
|
30
|
+
else
|
31
|
+
if (defined?(URI) && data.is_a?(URI::Generic)) ||
|
32
|
+
(defined?(Pathname) && data.is_a?(Pathname))
|
33
|
+
Parser.parse_file(data.to_s).root
|
34
|
+
else
|
35
|
+
Parser.parse(data).root
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
require 'lib/roxml/xml/references'
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'libxml'
|
2
|
+
require 'cgi'
|
3
|
+
|
4
|
+
module ROXML
|
5
|
+
module XML # :nodoc:all
|
6
|
+
Document = LibXML::XML::Document
|
7
|
+
Node = LibXML::XML::Node
|
8
|
+
Parser = LibXML::XML::Parser
|
9
|
+
Error = LibXML::XML::Error
|
10
|
+
|
11
|
+
module NamespacedSearch
|
12
|
+
def search(xpath, roxml_namespaces = {})
|
13
|
+
if namespaces.default
|
14
|
+
roxml_namespaces = {:xmlns => namespaces.default.href}.merge(roxml_namespaces)
|
15
|
+
end
|
16
|
+
if roxml_namespaces.present?
|
17
|
+
find(xpath, roxml_namespaces.map {|prefix, href| [prefix, href].join(':') })
|
18
|
+
else
|
19
|
+
find(xpath)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def namespaced(xpath)
|
25
|
+
xpath.split('/').map do |component|
|
26
|
+
if component =~ /\w+/ && !component.include?(':') && !component.starts_with?('@')
|
27
|
+
"xmlns:#{component}"
|
28
|
+
else
|
29
|
+
component
|
30
|
+
end
|
31
|
+
end.join('/')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Document
|
36
|
+
include NamespacedSearch
|
37
|
+
|
38
|
+
def default_namespace
|
39
|
+
default = namespaces.default
|
40
|
+
default.prefix || 'xmlns' if default
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
delegate :namespaces, :to => :root
|
45
|
+
end
|
46
|
+
|
47
|
+
class Node
|
48
|
+
include NamespacedSearch
|
49
|
+
|
50
|
+
class << self
|
51
|
+
def new_with_entity_escaping(name, content = nil, namespace = nil)
|
52
|
+
new_without_entity_escaping(name, content && CGI.escapeHTML(content), namespace)
|
53
|
+
end
|
54
|
+
alias_method_chain :new, :entity_escaping
|
55
|
+
|
56
|
+
alias :create :new
|
57
|
+
end
|
58
|
+
|
59
|
+
def default_namespace
|
60
|
+
doc.default_namespace
|
61
|
+
end
|
62
|
+
|
63
|
+
def add_child(child)
|
64
|
+
# libxml 1.1.3 changed child_add from returning child to returning self
|
65
|
+
self << child
|
66
|
+
child
|
67
|
+
end
|
68
|
+
|
69
|
+
alias_method :set_libxml_content, :content=
|
70
|
+
def content=(string)
|
71
|
+
set_libxml_content(string.gsub('&', '&'))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class Parser
|
76
|
+
class << self
|
77
|
+
def parse(str_data)
|
78
|
+
string(str_data).parse
|
79
|
+
end
|
80
|
+
|
81
|
+
def parse_file(path)
|
82
|
+
file(path).parse
|
83
|
+
end
|
84
|
+
|
85
|
+
def parse_io(stream)
|
86
|
+
io(stream).parse
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module ROXML
|
4
|
+
module XML # :nodoc:all
|
5
|
+
Document = Nokogiri::XML::Document
|
6
|
+
Element = Nokogiri::XML::Element
|
7
|
+
Node = Nokogiri::XML::Node
|
8
|
+
|
9
|
+
module Error; end
|
10
|
+
|
11
|
+
class Parser
|
12
|
+
class << self
|
13
|
+
def parse(string)
|
14
|
+
Nokogiri::XML(string)
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse_file(path) #:nodoc:
|
18
|
+
path = path.sub('file:', '') if path.starts_with?('file:')
|
19
|
+
parse(open(path))
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_io(stream) #:nodoc:
|
23
|
+
parse(stream)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Document
|
29
|
+
def save(path)
|
30
|
+
open(path, 'w') do |file|
|
31
|
+
file << serialize
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def default_namespace
|
36
|
+
'xmlns' if root.namespaces['xmlns']
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module NodeExtensions
|
41
|
+
def search(xpath, roxml_namespaces = {})
|
42
|
+
xpath = "./#{xpath}"
|
43
|
+
(roxml_namespaces.present? ? super(xpath, roxml_namespaces) : super(xpath)).map {|i| i }
|
44
|
+
end
|
45
|
+
|
46
|
+
def attributes
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def default_namespace
|
51
|
+
document.default_namespace
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class Element
|
56
|
+
include NodeExtensions
|
57
|
+
|
58
|
+
def empty?
|
59
|
+
children.empty?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class Node
|
64
|
+
class << self
|
65
|
+
def new_cdata(content)
|
66
|
+
Nokogiri::XML::CDATA.new(Document.new, content)
|
67
|
+
end
|
68
|
+
|
69
|
+
def create(name)
|
70
|
+
new(name, Document.new)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
include NodeExtensions
|
74
|
+
alias :remove! :remove
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|