om 0.1.10 → 1.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/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