tiny_xpath_helper 0.1.8
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.rdoc +116 -0
- data/Rakefile +30 -0
- data/init.rb +1 -0
- data/install.rb +1 -0
- data/lib/tiny_xpath_helper.rb +139 -0
- data/tasks/tiny_xpath_helper_tasks.rake +4 -0
- data/test/test_helper.rb +5 -0
- data/test/tiny_xpath_helper_test.rb +13 -0
- data/test/xml/sample.xml +9 -0
- data/tiny_xpath_helper.gemspec +19 -0
- data/uninstall.rb +1 -0
- metadata +66 -0
data/README.rdoc
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
= TinyXPathHelper
|
2
|
+
|
3
|
+
== Description
|
4
|
+
|
5
|
+
Very light syntax for using XPaths on XML documents or REXML nodes
|
6
|
+
|
7
|
+
== Examples
|
8
|
+
|
9
|
+
TinyXPathHelper.new takes one parameter: an XML document (as a string or IO) or a REXML element
|
10
|
+
>> xpath = TinyXPathHelper.new( File.open( 'test/xml/sample.xml' ) )
|
11
|
+
|
12
|
+
Then, to find the string values of matching nodes, there's a super-light syntax:
|
13
|
+
>> xpath[ 'node/' ]
|
14
|
+
=> ["one", "two", "three\n "]
|
15
|
+
|
16
|
+
It works on attributes, too
|
17
|
+
>> xpath[ 'node/@style' ]
|
18
|
+
=> ["first"]
|
19
|
+
|
20
|
+
If you prefer, you can use #all instead of square brackets
|
21
|
+
>> xpath.all( 'node/@style' )
|
22
|
+
=> ["first"]
|
23
|
+
|
24
|
+
If you just want the first match, use #first
|
25
|
+
>> xpath.first( 'node/' )
|
26
|
+
=> "one"
|
27
|
+
|
28
|
+
A slightly more elaborate API is available as #find_xpath
|
29
|
+
>> xpath.find_xpath( 'node/@style', :format => :rexml, :find => :first )
|
30
|
+
=> style='first'
|
31
|
+
|
32
|
+
Where :format can be :text (returns a string), or :rexml (returns a REXML element),
|
33
|
+
and :find can be :first or :all
|
34
|
+
>> xpath.find_xpath( 'node/@style', :format => :rexml, :find => :all )
|
35
|
+
=> [style='first']
|
36
|
+
|
37
|
+
If you want to filter the output, you can pass something that can be to_proc'd as :format
|
38
|
+
(Note that this acts on the string value of the element, not the REXML node)
|
39
|
+
>> xpath[ 'node/@style', :to_i ]
|
40
|
+
=> [0]
|
41
|
+
|
42
|
+
>> xpath[ 'node/@style', :length ]
|
43
|
+
=> [5]
|
44
|
+
|
45
|
+
>> xpath[ 'node', lambda{|x| x.strip.split(//) } ]
|
46
|
+
=> [["o", "n", "e"], ["t", "w", "o"], ["t", "h", "r", "e", "e"]]
|
47
|
+
|
48
|
+
If you only need to set :format, you may pass just the symbol instead of an options hash
|
49
|
+
This is especially convenient using the [] syntax since a bug in ruby1.8
|
50
|
+
prevents using => inside of []
|
51
|
+
>> xpath[ 'node/@style', :rexml ]
|
52
|
+
=> [style='first']
|
53
|
+
|
54
|
+
Actually, formats :rexml and :text are aliases for the :auto_text parameter, which takes a bool,
|
55
|
+
and defaults to true.
|
56
|
+
>> xpath.all( 'node/@style', :auto_text => false )
|
57
|
+
=> [style='first']
|
58
|
+
|
59
|
+
If you find yourself writing iterative xpaths, you may want to use :format => :tiny_xpath_helper
|
60
|
+
>> node_xpath = xpath.first( 'node', :format => :tiny_xpath_helper)
|
61
|
+
>> node_xpath.first('@style')
|
62
|
+
=> style='first'
|
63
|
+
|
64
|
+
Option defaults can be passed to TinyXPathHelper#new
|
65
|
+
>> xpath_with_options = TinyXPathHelper.new( File.open( 'test/xml/sample.xml' ), :format => :to_i, :find => :all )
|
66
|
+
>> xpath_with_options.find_xpath( 'node' )
|
67
|
+
=> [0,0,0]
|
68
|
+
|
69
|
+
You can access the default options for the TinyXMLHelper Class or any instance with #default_options
|
70
|
+
>> TinyXPathHelper.default_options
|
71
|
+
=> {:format => nil, :auto_text => true, :find => :first}
|
72
|
+
>> xpath_with_options.default_options
|
73
|
+
=> {:format => :to_i, :auto_text => true, :find => :all}
|
74
|
+
|
75
|
+
You can also pass a block, which will get called on the entire return value, but
|
76
|
+
only if at least one element is found
|
77
|
+
>> xpath.all( 'node' ){ |nodes| nodes.count } # nodes is an array of strings
|
78
|
+
=> 3
|
79
|
+
|
80
|
+
>> xpath.first( 'node' ){ |node| node.reverse } # node is a string
|
81
|
+
=> "eno"
|
82
|
+
|
83
|
+
>> xpath.all( 'node', &:count )
|
84
|
+
=> 3
|
85
|
+
|
86
|
+
>> xpath.all( 'nothing' ){ |nodes| raise "this will not run" }
|
87
|
+
=> []
|
88
|
+
|
89
|
+
If you really want shortcut for the case where you want to run something
|
90
|
+
over the array of matching REXML nodes, set :auto_text to false when calling TinyXpathHelper#new
|
91
|
+
>> rexml_xpath = TinyXPathHelper.new( File.open( 'test/xml/sample.xml' ), :auto_text => false )
|
92
|
+
>> rexml_xpath[ 'node', :name ]
|
93
|
+
=> ['node', 'node', 'node']
|
94
|
+
|
95
|
+
The output from :format => :rexml is suitable for passing to another TinyXPathHelper
|
96
|
+
>> node_tree = xpath.find_xpath( 'node/three', :format => :rexml )
|
97
|
+
>> deeper_xpath = TinyXPathHelper.new( node_tree )
|
98
|
+
>> deeper_xpath[ 'b/@beta' ]
|
99
|
+
=> ["true"]
|
100
|
+
|
101
|
+
If you want empty documents to not raise an error
|
102
|
+
>> empty_xpath = TinyXPathHelper.new( String.new, :allow_empty_document => true )
|
103
|
+
>> empty_xpath[ 'b/@beta' ]
|
104
|
+
=> []
|
105
|
+
|
106
|
+
If you want to call to_s on the xpath argument
|
107
|
+
>> xpath_with_indifferent_access = TinyXPathHelper.new( File.open( 'test/xml/sample.xml' ), :with_indifferent_access => true )
|
108
|
+
>> xpath_with_indifferent_access[ :node ]
|
109
|
+
=> ["one", "two", "three\n "]
|
110
|
+
|
111
|
+
|
112
|
+
--
|
113
|
+
doctest_require: 'lib/tiny_xpath_helper.rb'
|
114
|
+
++
|
115
|
+
|
116
|
+
Copyright (c) 2009 raSANTIAGO + Associates LLC
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => :rubydoctest
|
7
|
+
|
8
|
+
desc 'Test the tiny_xpath_helper plugin.'
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
10
|
+
t.libs << 'lib'
|
11
|
+
t.libs << 'test'
|
12
|
+
t.pattern = 'test/**/*_test.rb'
|
13
|
+
t.verbose = true
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'ruby rubydoctest'
|
17
|
+
task :rubydoctest do
|
18
|
+
results = `rubydoctest README.rdoc`
|
19
|
+
raise results if results =~ /FAIL/
|
20
|
+
puts results
|
21
|
+
end
|
22
|
+
|
23
|
+
desc 'Generate documentation for the tiny_xpath_helper plugin.'
|
24
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
25
|
+
rdoc.rdoc_dir = 'rdoc'
|
26
|
+
rdoc.title = 'TinyXPathHelper'
|
27
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
28
|
+
rdoc.rdoc_files.include('README.rdoc')
|
29
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
30
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "lib", "tiny_xpath_helper")
|
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
|
3
|
+
class TinyXPathHelper
|
4
|
+
attr :node
|
5
|
+
|
6
|
+
def initialize(xml, options = {})
|
7
|
+
@xml = xml
|
8
|
+
@node = self.class.xml_node_for_xmlish(xml, options)
|
9
|
+
@options = options.freeze
|
10
|
+
end
|
11
|
+
|
12
|
+
def default_options
|
13
|
+
self.class.default_options.dup.update(@options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def with_options(*options)
|
17
|
+
r = default_options.dup
|
18
|
+
|
19
|
+
options.reverse.each do |option|
|
20
|
+
if not option.is_a?( Hash )
|
21
|
+
option = {:format => option}
|
22
|
+
end
|
23
|
+
r.update(option)
|
24
|
+
end
|
25
|
+
|
26
|
+
return r
|
27
|
+
end
|
28
|
+
|
29
|
+
def find_xpath(xpath_expr, options = {}, &blk)
|
30
|
+
self.class.find_xpath_from( node, xpath_expr, with_options(options), &blk)
|
31
|
+
end
|
32
|
+
|
33
|
+
def first(xpath_expr, options = {}, &blk)
|
34
|
+
self.find_xpath( xpath_expr, with_options(options, :find => :first), &blk)
|
35
|
+
end
|
36
|
+
alias at first
|
37
|
+
|
38
|
+
def all(xpath_expr, options = {}, &blk)
|
39
|
+
self.find_xpath( xpath_expr, with_options(options, :find => :all), &blk)
|
40
|
+
end
|
41
|
+
alias [] all
|
42
|
+
|
43
|
+
def self.io_stream_classes
|
44
|
+
[ IOStream ] rescue [ IO, StringIO ] # thoughtbot-paperclip fixes the ducktype mess in StringIO
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.classes_that_are_xmlish
|
48
|
+
io_stream_classes + [ String, REXML::Document ]
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.xml_node_for_xmlish( xml, options = {} )
|
52
|
+
if io_stream_classes.any?{|k| xml.is_a?(k) }
|
53
|
+
xml = xml.read
|
54
|
+
end
|
55
|
+
if xml.is_a?(String)
|
56
|
+
xml = REXML::Document.new(xml)
|
57
|
+
end
|
58
|
+
if xml.is_a?(REXML::Document)
|
59
|
+
if options[:allow_empty_document] and ! xml.root
|
60
|
+
xml = xml
|
61
|
+
else
|
62
|
+
xml = xml.root
|
63
|
+
end
|
64
|
+
end
|
65
|
+
if not xml.is_a?(REXML::Element)
|
66
|
+
raise TypeError.new("Expected REXML::Element, got #{xml.class}")
|
67
|
+
end
|
68
|
+
return xml
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.xml_node_to_text(node)
|
72
|
+
if(node.respond_to? :text)
|
73
|
+
# XML::Elements don't to_s in the way we want
|
74
|
+
val = node.text
|
75
|
+
else
|
76
|
+
val = node.to_s
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.default_options
|
81
|
+
{:format => nil, :auto_text => true, :find => :first}.freeze
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.find_xpath_from(element, path, options = {}, &blk)
|
85
|
+
if options[:with_indifferent_access]
|
86
|
+
path = path.to_s
|
87
|
+
end
|
88
|
+
|
89
|
+
options = self.default_options.dup.update(options)
|
90
|
+
format = options[:format]
|
91
|
+
auto_text = options[:auto_text]
|
92
|
+
count = options[:find]
|
93
|
+
|
94
|
+
if format == :array
|
95
|
+
count = :all
|
96
|
+
format = nil
|
97
|
+
elsif format == :text
|
98
|
+
auto_text = true
|
99
|
+
format = nil
|
100
|
+
elsif format == :xml or format == :rexml
|
101
|
+
auto_text = false
|
102
|
+
format = nil
|
103
|
+
elsif format == :xpath_helper or format == :tiny_xpath_helper
|
104
|
+
auto_text = false
|
105
|
+
format = self.method(:new)
|
106
|
+
end
|
107
|
+
|
108
|
+
filter1 = auto_text ? self.method(:xml_node_to_text) : nil
|
109
|
+
filter2 = format
|
110
|
+
|
111
|
+
if count == :all
|
112
|
+
elements = REXML::XPath.match(element, path)
|
113
|
+
|
114
|
+
elsif count == :first
|
115
|
+
elements = [ REXML::XPath.first(element, path) ].compact # Haskell hacker wishing for the Maybe monad
|
116
|
+
|
117
|
+
else
|
118
|
+
raise "I don't know how to find #{count.inspect}"
|
119
|
+
end
|
120
|
+
|
121
|
+
elements = elements.map(&filter1).map(&filter2)
|
122
|
+
|
123
|
+
if count == :all
|
124
|
+
r = elements
|
125
|
+
elsif count == :first
|
126
|
+
r = elements.first
|
127
|
+
end
|
128
|
+
|
129
|
+
if(blk and elements.length > 0)
|
130
|
+
return blk.call( r )
|
131
|
+
end
|
132
|
+
|
133
|
+
return r
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
TinyXpathHelper = TinyXPathHelper
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class TinyXPathHelperTest < ActiveSupport::TestCase
|
4
|
+
test "find first returns nil if there is no match" do
|
5
|
+
xpath = TinyXPathHelper.new("<xml/>")
|
6
|
+
assert xpath.first('*').nil?
|
7
|
+
end
|
8
|
+
|
9
|
+
test "find_xpath with :format => :array doesn't crash" do
|
10
|
+
xpath = TinyXPathHelper.new("<xml><a/></xml>")
|
11
|
+
assert xpath.find_xpath('*', :format => :array)
|
12
|
+
end
|
13
|
+
end
|
data/test/xml/sample.xml
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{tiny_xpath_helper}
|
5
|
+
s.version = "0.1.8"
|
6
|
+
|
7
|
+
s.authors = ["Jesse Wolfe"]
|
8
|
+
s.date = %q{2009-06-01}
|
9
|
+
s.description = %q{Very light syntax for using XPaths on XML documents or REXML nodes}
|
10
|
+
s.email = %q{jesse@rasantiago.com}
|
11
|
+
s.files = %w[ uninstall.rb init.rb Rakefile lib/tiny_xpath_helper.rb tiny_xpath_helper.gemspec README.rdoc tasks/tiny_xpath_helper_tasks.rake install.rb test/tiny_xpath_helper_test.rb test/xml test/xml/sample.xml test/test_helper.rb ]
|
12
|
+
s.has_rdoc = false
|
13
|
+
s.homepage = %q{http://github.com/rasantiago/tiny_xpath_helper}
|
14
|
+
s.rdoc_options = ["--inline-source", "--charset=UTF-8"]
|
15
|
+
s.require_paths = ["lib"]
|
16
|
+
s.rubygems_version = %q{1.3.1}
|
17
|
+
s.summary = %q{Very light syntax for using XPaths on XML documents or REXML nodes}
|
18
|
+
end
|
19
|
+
|
data/uninstall.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Uninstall hook code here
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tiny_xpath_helper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.8
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jesse Wolfe
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-06-01 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Very light syntax for using XPaths on XML documents or REXML nodes
|
17
|
+
email: jesse@rasantiago.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- uninstall.rb
|
26
|
+
- init.rb
|
27
|
+
- Rakefile
|
28
|
+
- lib/tiny_xpath_helper.rb
|
29
|
+
- tiny_xpath_helper.gemspec
|
30
|
+
- README.rdoc
|
31
|
+
- tasks/tiny_xpath_helper_tasks.rake
|
32
|
+
- install.rb
|
33
|
+
- test/tiny_xpath_helper_test.rb
|
34
|
+
- test/xml/sample.xml
|
35
|
+
- test/test_helper.rb
|
36
|
+
has_rdoc: true
|
37
|
+
homepage: http://github.com/rasantiago/tiny_xpath_helper
|
38
|
+
licenses: []
|
39
|
+
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options:
|
42
|
+
- --inline-source
|
43
|
+
- --charset=UTF-8
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: "0"
|
51
|
+
version:
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: "0"
|
57
|
+
version:
|
58
|
+
requirements: []
|
59
|
+
|
60
|
+
rubyforge_project:
|
61
|
+
rubygems_version: 1.3.5
|
62
|
+
signing_key:
|
63
|
+
specification_version: 3
|
64
|
+
summary: Very light syntax for using XPaths on XML documents or REXML nodes
|
65
|
+
test_files: []
|
66
|
+
|