om 0.1.10 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile ADDED
@@ -0,0 +1,27 @@
1
+ h1. opinionated-xml
2
+
3
+ A library to help you tame sprawling XML schemas like MODS.
4
+
5
+ h2. Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
13
+ * Send me a pull request. Bonus points for topic branches.
14
+
15
+ h2. Acknowledgements
16
+
17
+ Creator: Matt Zumwalt ("MediaShelf":http://yourmediashelf.com)
18
+
19
+ Thanks to
20
+
21
+ Bess Sadler, who enabled us to take knowledge gleaned from developing Blacklight and apply it to OM metadata indexing
22
+ Ross Singer
23
+ Those who participated in the Opinionated MODS breakout session at Code4Lib 2010
24
+
25
+ h2. Copyright
26
+
27
+ Copyright (c) 2010 Matt Zumwalt. See LICENSE for details.
data/Rakefile CHANGED
@@ -11,7 +11,7 @@ begin
11
11
  gem.homepage = "http://github.com/mediashelf/om"
12
12
  gem.authors = ["Matt Zumwalt"]
13
13
 
14
- gem.add_dependency('nokogiri')
14
+ gem.add_dependency('nokogiri', ">= 1.4.2")
15
15
  gem.add_dependency('facets')
16
16
 
17
17
  gem.add_development_dependency "rspec", ">= 1.2.9"
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.10
1
+ 1.0.0
@@ -0,0 +1,64 @@
1
+ class OM::Samples::ModsArticle
2
+
3
+ include OM::XML::Document
4
+
5
+ set_terminology do |t|
6
+ t.root(:path=>"mods", :xmlns=>"http://www.loc.gov/mods/v3", :schema=>"http://www.loc.gov/standards/mods/v3/mods-3-2.xsd")
7
+
8
+ t.title_info(:path=>"titleInfo") {
9
+ t.main_title(:path=>"title", :label=>"title")
10
+ t.language(:path=>{:attribute=>"lang"})
11
+ }
12
+ t.abstract
13
+ t.topic_tag(:path=>"subject", :default_content_path=>"topic")
14
+ # This is a mods:name. The underscore is purely to avoid namespace conflicts.
15
+ t.name_ {
16
+ # this is a namepart
17
+ t.namePart(:index_as=>[:searchable, :displayable, :facetable, :sortable], :required=>:true, :type=>:string, :label=>"generic name")
18
+ # affiliations are great
19
+ t.affiliation
20
+ t.displayForm
21
+ t.role(:ref=>[:role])
22
+ t.description
23
+ t.date(:path=>"namePart", :attributes=>{:type=>"date"})
24
+ t.last_name(:path=>"namePart", :attributes=>{:type=>"family"})
25
+ t.first_name(:path=>"namePart", :attributes=>{:type=>"given"}, :label=>"first name")
26
+ t.terms_of_address(:path=>"namePart", :attributes=>{:type=>"termsOfAddress"})
27
+ }
28
+ # lookup :person, :first_name
29
+ t.person(:ref=>:name, :attributes=>{:type=>"personal"})
30
+ t.organizaton(:ref=>:name, :attributes=>{:type=>"institutional"})
31
+ t.conference(:ref=>:name, :attributes=>{:type=>"conference"})
32
+
33
+ t.role {
34
+ t.text(:path=>"roleTerm",:attributes=>{:type=>"text"})
35
+ t.code(:path=>"roleTerm",:attributes=>{:type=>"code"})
36
+ }
37
+ t.journal(:path=>'relatedItem', :attributes=>{:type=>"host"}) {
38
+ t.title_info
39
+ t.origin_info(:path=>"originInfo") {
40
+ t.publisher
41
+ t.date_issued(:path=>"dateIssued")
42
+ }
43
+ t.issn(:path=>"identifier", :attributes=>{:type=>"issn"})
44
+ t.issue(:path=>"part") {
45
+ t.volume(:path=>"detail", :attributes=>{:type=>"volume"}, :default_content_path=>"number")
46
+ t.level(:path=>"detail", :attributes=>{:type=>"number"}, :default_content_path=>"number")
47
+ t.extent
48
+ t.pages(:path=>"extent", :attributes=>{:unit=>"pages"}) {
49
+ t.start
50
+ t.end
51
+ }
52
+ t.publication_date(:path=>"date")
53
+ t.start_page(:proxy=>[:pages, :start])
54
+ t.end_page(:proxy=>[:pages, :end])
55
+ }
56
+ }
57
+
58
+ end
59
+
60
+ # Changes from OM::Properties implementation
61
+ # renamed family_name => last_name
62
+ # start_page & end_page now accessible as [:journal, :issue, :pages, :start] (etc.)
63
+
64
+ end
data/lib/om/samples.rb ADDED
@@ -0,0 +1,2 @@
1
+ module OM::Samples;end
2
+ require "om/samples/mods_article"
@@ -0,0 +1,24 @@
1
+ module OM::TreeNode
2
+
3
+ attr_accessor :ancestors
4
+
5
+ # insert the mapper into the given parent
6
+ def set_parent(parent_mapper)
7
+ parent_mapper.children[@name] = self
8
+ @ancestors << parent_mapper
9
+ end
10
+
11
+ # insert the given mapper into the current mappers children
12
+ def add_child(child_mapper)
13
+ child_mapper.ancestors << self
14
+ @children[child_mapper.name.to_sym] = child_mapper
15
+ end
16
+
17
+ def retrieve_child(child_name)
18
+ child = @children.fetch(child_name, nil)
19
+ end
20
+
21
+ def parent
22
+ ancestors.last
23
+ end
24
+ end
@@ -0,0 +1,68 @@
1
+ module OM::XML::Document
2
+
3
+
4
+ # Class Methods -- These methods will be available on classes that include this Module
5
+
6
+ module ClassMethods
7
+
8
+ attr_accessor :terminology
9
+
10
+ # Sets the OM::XML::Terminology for the Document
11
+ # Expects +&block+ that will be passed into OM::XML::Terminology::Builder.new
12
+ def set_terminology &block
13
+ @terminology = OM::XML::Terminology::Builder.new( &block ).build
14
+ end
15
+
16
+ # Returns any namespaces defined by the Class' Terminology
17
+ def ox_namespaces
18
+ if @terminology.nil?
19
+ return {}
20
+ else
21
+ return @terminology.namespaces
22
+ end
23
+ end
24
+
25
+ end
26
+
27
+ # Instance Methods -- These methods will be available on instances of classes that include this module
28
+
29
+ attr_accessor :ox_namespaces
30
+
31
+ def self.included(klass)
32
+ klass.extend(ClassMethods)
33
+
34
+ klass.send(:include, OM::XML::Container)
35
+ klass.send(:include, OM::XML::TermValueOperators)
36
+ end
37
+
38
+ # Applies the property's corresponding xpath query, returning the result Nokogiri::XML::NodeSet
39
+ def find_by_terms_and_value(*term_pointer)
40
+ xpath = self.class.terminology.xpath_for(*term_pointer)
41
+ if xpath.nil?
42
+ return nil
43
+ else
44
+ return ng_xml.xpath(xpath, ox_namespaces)
45
+ end
46
+ end
47
+
48
+
49
+ # +term_pointer+ Variable length array of values in format [:accessor_name, :accessor_name ...] or [{:accessor_name=>index}, :accessor_name ...]
50
+ # example: {:person => 1}, :first_name
51
+ # example: [:person, 1, :first_name]
52
+ # Currently, indexes must be integers.
53
+ def find_by_terms(*term_pointer)
54
+ xpath = self.class.terminology.xpath_with_indexes(*term_pointer)
55
+ if xpath.nil?
56
+ return nil
57
+ else
58
+ return ng_xml.xpath(xpath, ox_namespaces)
59
+ end
60
+ end
61
+
62
+ # Returns a hash combining the current documents namespaces (provided by nokogiri) and any namespaces that have been set up by your Terminology.
63
+ # Most importantly, this matches the 'oxns' namespace to the namespace you provided in your Terminology's root term config
64
+ def ox_namespaces
65
+ @ox_namespaces ||= ng_xml.namespaces.merge(self.class.ox_namespaces)
66
+ end
67
+
68
+ end
@@ -0,0 +1,37 @@
1
+ class OM::XML::NamedTermProxy
2
+
3
+ attr_accessor :proxy_pointer, :name
4
+
5
+ include OM::TreeNode
6
+
7
+ def initialize(name, proxy_pointer, opts={})
8
+ opts = {:namespace_prefix=>"oxns", :ancestors=>[], :children=>{}}.merge(opts)
9
+ [:children, :ancestors].each do |accessor_name|
10
+ instance_variable_set("@#{accessor_name}", opts.fetch(accessor_name, nil) )
11
+ end
12
+ @name = name
13
+ @proxy_pointer = proxy_pointer
14
+ end
15
+
16
+ def proxied_term
17
+ self.parent.retrieve_term(*self.proxy_pointer)
18
+ end
19
+
20
+ # do nothing -- this is to prevent errors when the parent term calls generate_xpath_queries! on its children
21
+ def generate_xpath_queries!
22
+ # do nothing
23
+ end
24
+
25
+ def method_missing
26
+ end
27
+
28
+ # Any unknown method calls will be proxied to the proxied term
29
+ def method_missing method, *args, &block
30
+ if args.empty?
31
+ return self.proxied_term.send(method)
32
+ else
33
+ return self.proxied_term.send(method, args)
34
+ end
35
+ end
36
+
37
+ end
@@ -0,0 +1,14 @@
1
+ module OM::XML::NodeGenerator
2
+
3
+ # Module Methods -- These methods can be called directly on the Module itself
4
+ def self.generate(term, builder_new_value, opts={})
5
+ template = term.xml_builder_template(opts)
6
+ builder_call_body = eval('"' + template + '"')
7
+ builder = Nokogiri::XML::Builder.new do |xml|
8
+ eval( builder_call_body )
9
+ end
10
+
11
+ return builder.doc
12
+ end
13
+
14
+ end
@@ -0,0 +1,223 @@
1
+ class OM::XML::Term
2
+
3
+ # Term::Builder Class Definition
4
+
5
+ class Builder
6
+ attr_accessor :name, :settings, :children, :terminology_builder
7
+
8
+ def initialize(name, terminology_builder=nil)
9
+ @name = name.to_sym
10
+ @terminology_builder = terminology_builder
11
+ @settings = {:required=>false, :data_type=>:string}
12
+ @children = {}
13
+ end
14
+
15
+ def add_child(child)
16
+ @children[child.name] = child
17
+ end
18
+
19
+ def retrieve_child(child_name)
20
+ child = @children.fetch(child_name, nil)
21
+ end
22
+
23
+ def lookup_refs(nodes_visited=[])
24
+ result = []
25
+ if @settings[:ref]
26
+ # Fail if we do not have terminology builder
27
+ if self.terminology_builder.nil?
28
+ raise "Cannot perform lookup_ref for the #{self.name} builder. It doesn't have a reference to any terminology builder"
29
+ end
30
+ begin
31
+ target = self.terminology_builder.retrieve_term_builder(*@settings[:ref])
32
+ rescue OM::XML::Terminology::BadPointerError
33
+ # Clarify message on BadPointerErrors
34
+ raise OM::XML::Terminology::BadPointerError, "#{self.name} refers to a Term Builder that doesn't exist. The bad pointer is #{@settings[:ref].inspect}"
35
+ end
36
+
37
+ # Fail on circular references and return an intelligible error message
38
+ if nodes_visited.contains?(target)
39
+ nodes_visited << self
40
+ nodes_visited << target
41
+ trail = ""
42
+ nodes_visited.each_with_index do |node, z|
43
+ trail << node.name.inspect
44
+ unless z == nodes_visited.length-1
45
+ trail << " => "
46
+ end
47
+ end
48
+ raise OM::XML::Terminology::CircularReferenceError, "Circular reference in Terminology: #{trail}"
49
+ end
50
+ result << target
51
+ result.concat( target.lookup_refs(nodes_visited << self) )
52
+ end
53
+ return result
54
+ end
55
+
56
+ # If a :ref value has been set, looks up the target of that ref and merges the target's settings & children with the current builder's settings & children
57
+ # operates recursively, so it is possible to apply refs that in turn refer to other nodes.
58
+ def resolve_refs!
59
+ name_of_last_ref = nil
60
+ lookup_refs.each_with_index do |ref,z|
61
+ @settings = two_layer_merge(@settings, ref.settings)
62
+ @children.merge!(ref.children)
63
+ name_of_last_ref = ref.name
64
+ end
65
+ if @settings[:path].nil? && !name_of_last_ref.nil?
66
+ @settings[:path] = name_of_last_ref.to_s
67
+ end
68
+ @settings.delete :ref
69
+ return self
70
+ end
71
+
72
+ # Returns a new Hash that merges +downstream_hash+ with +upstream_hash+
73
+ # similar to calling +upstream_hash+.merge(+downstream_hash+) only it also merges
74
+ # any internal values that are themselves Hashes.
75
+ def two_layer_merge(downstream_hash, upstream_hash)
76
+ up = upstream_hash.dup
77
+ dn = downstream_hash.dup
78
+ up.each_pair do |setting_name, value|
79
+ if value.kind_of?(Hash) && downstream_hash.has_key?(setting_name)
80
+ dn[setting_name] = value.merge(downstream_hash[setting_name])
81
+ up.delete(setting_name)
82
+ end
83
+ end
84
+ return up.merge(dn)
85
+ end
86
+
87
+ # Builds a new OM::XML::Term based on the Builder object's current settings
88
+ # If no path has been provided, uses the Builder object's name as the term's path
89
+ # Recursively builds any children, appending the results as children of the Term that's being built.
90
+ def build
91
+ self.resolve_refs!
92
+ if term.self.settings.has_key?(:proxy)
93
+ term = OM::XML::NamedTermProxy.new(self.name, self.settings[:proxy])
94
+ else
95
+ term = OM::XML::Term.new(self.name)
96
+
97
+ self.settings.each do |name, values|
98
+ if term.respond_to?(name.to_s+"=")
99
+ term.instance_variable_set("@#{name}", values)
100
+ end
101
+ end
102
+ @children.each_value do |child|
103
+ term.add_child child.build
104
+ end
105
+ term.generate_xpath_queries!
106
+ end
107
+
108
+ return term
109
+ end
110
+
111
+ # Any unknown method calls will add an entry to the settings hash and return the current object
112
+ def method_missing method, *args, &block
113
+ if args.length == 1
114
+ args = args.first
115
+ end
116
+ @settings[method] = args
117
+ return self
118
+ end
119
+ end
120
+
121
+ # Term Class Definition
122
+
123
+ attr_accessor :name, :xpath, :xpath_constrained, :xpath_relative, :path, :index_as, :required, :data_type, :variant_of, :path, :attributes, :default_content_path, :namespace_prefix, :is_root_term
124
+ attr_accessor :children, :internal_xml, :terminology
125
+
126
+ include OM::TreeNode
127
+
128
+ def initialize(name, opts={})
129
+ opts = {:namespace_prefix=>"oxns", :ancestors=>[], :children=>{}}.merge(opts)
130
+ [:children, :ancestors,:path, :index_as, :required, :type, :variant_of, :path, :attributes, :default_content_path, :namespace_prefix].each do |accessor_name|
131
+ instance_variable_set("@#{accessor_name}", opts.fetch(accessor_name, nil) )
132
+ end
133
+ @name = name
134
+ if @path.nil? || @path.empty?
135
+ @path = name.to_s
136
+ end
137
+ end
138
+
139
+ def self.from_node(mapper_xml)
140
+ name = mapper_xml.attribute("name").text.to_sym
141
+ attributes = {}
142
+ mapper_xml.xpath("./attribute").each do |a|
143
+ attributes[a.attribute("name").text.to_sym] = a.attribute("value").text
144
+ end
145
+ new_mapper = self.new(name, :attributes=>attributes)
146
+ [:index_as, :required, :type, :variant_of, :path, :default_content_path, :namespace_prefix].each do |accessor_name|
147
+ attribute = mapper_xml.attribute(accessor_name.to_s)
148
+ unless attribute.nil?
149
+ new_mapper.instance_variable_set("@#{accessor_name}", attribute.text )
150
+ end
151
+ end
152
+ new_mapper.internal_xml = mapper_xml
153
+
154
+ mapper_xml.xpath("./mapper").each do |child_node|
155
+ child = self.from_node(child_node)
156
+ new_mapper.add_child(child)
157
+ end
158
+
159
+ return new_mapper
160
+ end
161
+
162
+ # crawl down into mapper's children hash to find the desired mapper
163
+ # ie. @test_mapper.retrieve_mapper(:conference, :role, :text)
164
+ def retrieve_term(*pointers)
165
+ children_hash = self.children
166
+ pointers.each do |p|
167
+ if children_hash.has_key?(p)
168
+ target = children_hash[p]
169
+ if pointers.index(p) == pointers.length-1
170
+ return target
171
+ else
172
+ children_hash = target.children
173
+ end
174
+ else
175
+ return nil
176
+ end
177
+ end
178
+ return target
179
+ end
180
+
181
+ def is_root_term?
182
+ @is_root_term == true
183
+ end
184
+
185
+ def xpath_absolute
186
+ @xpath
187
+ end
188
+
189
+ # +term_pointers+ reference to the property you want to generate a builder template for
190
+ # @opts
191
+ def xml_builder_template(extra_opts = {})
192
+ extra_attributes = extra_opts.fetch(:attributes, {})
193
+
194
+ node_options = []
195
+ node_child_template = ""
196
+ if !self.default_content_path.nil?
197
+ node_child_options = ["\':::builder_new_value:::\'"]
198
+ node_child_template = " { xml.#{self.default_content_path}( #{OM::XML.delimited_list(node_child_options)} ) }"
199
+ else
200
+ node_options = ["\':::builder_new_value:::\'"]
201
+ end
202
+ if !self.attributes.nil?
203
+ self.attributes.merge(extra_attributes).each_pair do |k,v|
204
+ node_options << ":#{k}=>\'#{v}\'"
205
+ end
206
+ end
207
+ template = "xml.#{self.path}( #{OM::XML.delimited_list(node_options)} )" + node_child_template
208
+ return template.gsub( /:::(.*?):::/ ) { '#{'+$1+'}' }
209
+ end
210
+
211
+ # Generates absolute, relative, and constrained xpaths for the term, setting xpath, xpath_relative, and xpath_constrained accordingly.
212
+ # Also triggers update_xpath_values! on all child nodes, as their absolute paths rely on those of their parent nodes.
213
+ def generate_xpath_queries!
214
+ self.xpath = OM::XML::TermXpathGenerator.generate_absolute_xpath(self)
215
+ self.xpath_constrained = OM::XML::TermXpathGenerator.generate_constrained_xpath(self)
216
+ self.xpath_relative = OM::XML::TermXpathGenerator.generate_relative_xpath(self)
217
+ self.children.each_value {|child| child.generate_xpath_queries! }
218
+ return self
219
+ end
220
+
221
+ # private :update_xpath_values
222
+
223
+ end
@@ -0,0 +1,182 @@
1
+ require "open-uri"
2
+ require "logger"
3
+
4
+ class OM::XML::ParentNodeNotFoundError < RuntimeError; end
5
+ module OM::XML::TermValueOperators
6
+
7
+ # Retrieves all of the nodes from the current document that match +term_pointer+ and returns an array of their values
8
+ def term_values(*term_pointer)
9
+ result = []
10
+ find_by_terms(*term_pointer).each {|node| result << node.text }
11
+ # find_by_terms(*OM.destringify(term_pointer)).each {|node| result << node.text }
12
+ return result
13
+ end
14
+
15
+ # alias for term_values
16
+ def property_values(*lookup_args)
17
+ term_values(*lookup_args)
18
+ end
19
+
20
+ #
21
+ # example term values hash: {[{":person"=>"0"}, "role", "text"]=>{"0"=>"role1", "1"=>"role2", "2"=>"role3"}, [{:person=>1}, :family_name]=>"Andronicus", [{"person"=>"1"},:given_name]=>["Titus"],[{:person=>1},:role,:text]=>["otherrole1","otherrole2"] }
22
+ def update_values(params={})
23
+ # remove any terms from params that this datastream doesn't recognize
24
+
25
+ params.delete_if do |term_pointer,new_values|
26
+ if term_pointer.kind_of?(String)
27
+ true
28
+ else
29
+ !self.class.terminology.has_term?(*OM.destringify(term_pointer))
30
+ end
31
+ end
32
+
33
+ result = params.dup
34
+
35
+ params.each_pair do |term_pointer,new_values|
36
+ pointer = OM.destringify(term_pointer)
37
+ template = OM.pointers_to_flat_array(pointer,false)
38
+ hn = OM::XML::Terminology.term_hierarchical_name(*pointer)
39
+
40
+ # Sanitize new_values to always be a hash with indexes
41
+ case new_values
42
+ when Hash
43
+ when Array
44
+ nv = new_values.dup
45
+ new_values = {}
46
+ nv.each {|v| new_values[nv.index(v).to_s] = v}
47
+ else
48
+ new_values = {"0"=>new_values}
49
+ end
50
+
51
+ # Populate the response hash appropriately, using hierarchical names for terms as keys rather than the given pointers.
52
+ result.delete(term_pointer)
53
+ result[hn] = new_values.dup
54
+
55
+ # Skip any submitted values if the new value matches the current values
56
+ current_values = term_values(*pointer)
57
+ new_values.delete_if do |y,z|
58
+ if current_values[y.to_i]==z and y.to_i > -1
59
+ true
60
+ else
61
+ false
62
+ end
63
+ end
64
+
65
+ # Fill out the pointer completely if the final term is a NamedTermProxy
66
+ term = self.class.terminology.retrieve_term( *OM.pointers_to_flat_array(pointer,false) )
67
+ if term.kind_of? OM::XML::NamedTermProxy
68
+ pointer.pop
69
+ pointer = pointer.concat(term.proxy_pointer)
70
+ end
71
+
72
+ xpath = self.class.terminology.xpath_with_indexes(*pointer)
73
+ parent_pointer = pointer.dup
74
+ parent_pointer.pop
75
+ parent_xpath = self.class.terminology.xpath_with_indexes(*parent_pointer)
76
+
77
+ # If the value doesn't exist yet, append it. Otherwise, update the existing value.
78
+ new_values.each do |y,z|
79
+ if find_by_terms(*pointer)[y.to_i].nil? || y.to_i == -1
80
+ result[hn].delete(y)
81
+ term_values_append(:parent_select=>parent_xpath,:child_index=>0,:template=>template,:values=>z)
82
+ new_array_index = find_by_terms(*pointer).length - 1
83
+ result[hn][new_array_index.to_s] = z
84
+ else
85
+ term_value_update(xpath, y.to_i, z)
86
+ end
87
+ end
88
+ end
89
+ return result
90
+ end
91
+
92
+ def term_values_append(opts={})
93
+ parent_select = Array( opts[:parent_select] )
94
+ child_index = opts[:child_index]
95
+ template = opts[:template]
96
+ new_values = Array( opts[:values] )
97
+
98
+ # If template is a string, use it as the template, otherwise use it as arguments to xml_builder_template
99
+ unless template.instance_of?(String)
100
+ template_args = Array(template)
101
+ if template_args.last.kind_of?(Hash)
102
+ template_opts = template_args.delete_at(template_args.length - 1)
103
+ template_args << template_opts
104
+ end
105
+ template = self.class.terminology.xml_builder_template( *template_args )
106
+ end
107
+
108
+ parent_nodeset = find_by_terms(*parent_select)
109
+ parent_node = node_from_set(parent_nodeset, child_index)
110
+
111
+ if parent_node.nil?
112
+ raise OM::XML::ParentNodeNotFoundError, "Failed to find a parent node to insert values into based on :parent_select #{parent_select.inspect} with :child_index #{child_index.inspect}"
113
+ end
114
+
115
+ builder = Nokogiri::XML::Builder.with(parent_node) do |xml|
116
+ new_values.each do |builder_new_value|
117
+ builder_arg = eval('"'+ template + '"') # this inserts builder_new_value into the builder template
118
+ eval(builder_arg)
119
+ end
120
+ end
121
+
122
+ # Nokogiri::XML::Node.new(builder.to_xml, foo)
123
+
124
+ return parent_node
125
+
126
+ end
127
+
128
+ def term_value_update(node_select,child_index,new_value,opts={})
129
+ # template = opts.fetch(:template,nil)
130
+
131
+ node = find_by_terms_and_value(*node_select)[child_index]
132
+ if new_value == "" || new_value == :delete
133
+ node.remove
134
+ else
135
+ node.content = new_value
136
+ end
137
+ end
138
+
139
+ # def term_value_set(term_ref, query_opts, node_index, new_value)
140
+ # end
141
+
142
+ def term_value_delete(opts={})
143
+ parent_select = Array( opts[:parent_select] )
144
+ parent_index = opts[:parent_index]
145
+ child_index = opts[:child_index]
146
+ xpath_select = opts[:select]
147
+
148
+ if !xpath_select.nil?
149
+ node = find_by_terms_and_value(xpath_select).first
150
+ else
151
+ # parent_nodeset = find_by_terms_and_value(parent_select, parent_select)
152
+ parent_nodeset = find_by_terms_and_value(*parent_select)
153
+
154
+ if parent_index.nil?
155
+ node = node_from_set(parent_nodeset, child_index)
156
+ else
157
+ parent = node_from_set(parent_nodeset, parent_index)
158
+ # this next line is a hack around the fact that element_children() sometimes doesn't work.
159
+ node = node_from_set(parent.xpath("*"), child_index)
160
+ end
161
+ end
162
+
163
+ node.remove
164
+ end
165
+
166
+
167
+ # Allows you to provide an array index _or_ a symbol representing the function to call on the nodeset in order to retrieve the node.
168
+ def node_from_set(nodeset, index)
169
+ if index.kind_of?(Integer)
170
+ node = nodeset[index]
171
+ elsif index.kind_of?(Symbol) && nodeset.respond_to?(index)
172
+ node = nodeset.send(index)
173
+ else
174
+ raise "Could not retrieve node using index #{index}."
175
+ end
176
+
177
+ return node
178
+ end
179
+
180
+ private :node_from_set
181
+
182
+ end