rsxml 0.1.4 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +47 -6
- data/VERSION +1 -1
- data/lib/rsxml.rb +23 -149
- data/lib/rsxml/namespace.rb +175 -0
- data/lib/rsxml/sexp.rb +90 -0
- data/lib/rsxml/util.rb +25 -0
- data/lib/rsxml/visitor.rb +132 -0
- data/lib/rsxml/xml.rb +99 -0
- data/spec/rsxml/namespace_spec.rb +250 -0
- data/spec/rsxml/sexp_spec.rb +15 -0
- data/spec/rsxml/util_spec.rb +41 -0
- data/spec/rsxml/visitor_spec.rb +82 -0
- data/spec/rsxml/xml_spec.rb +68 -0
- data/spec/rsxml_spec.rb +88 -32
- metadata +20 -5
data/README.rdoc
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
= rsxml
|
2
2
|
|
3
|
-
A Ruby library to translate XML documents into an s-expression representation, and back again
|
3
|
+
A Ruby library to translate XML documents into an s-expression representation, and back again, in the style of SXML : http://en.wikipedia.org/wiki/SXML
|
4
|
+
|
5
|
+
Why would you want to do this ? Well, s-expressions can be == compared natively in Ruby, are easy to read
|
6
|
+
and editors indent them nicely when embedded in code. These features make them very suitable for writing readable
|
7
|
+
XML generation code and readable tests for XML generating code
|
4
8
|
|
5
9
|
Rsxml represents XML documents as s-expressions thus :
|
6
10
|
|
@@ -17,14 +21,51 @@ It is easy to convert XML docuemnts to Rsxml representation and back again :
|
|
17
21
|
Rsxml.to_rsxml(xml)
|
18
22
|
=> ["Foo", {"foofoo"=>"10"}, ["Bar", "barbar"]]
|
19
23
|
|
20
|
-
|
24
|
+
=== Namespaces
|
21
25
|
|
22
|
-
|
23
|
-
|
26
|
+
XML namespaces are dealt with straightforwardly. When an XML document is converted to Rsxml, namespaces are preserved, and you can specify namespaces in an Rsxml structure in two ways
|
27
|
+
* using QName prefixes and declarative attributes, exactly as with XML
|
28
|
+
* using exploded QNames consisting of <tt>[local_name, prefix, uri]</tt> triples and <tt>[local_name, prefix]</tt> pairs
|
24
29
|
|
30
|
+
=== Converting to Rsxml
|
25
31
|
|
26
|
-
Rsxml
|
27
|
-
|
32
|
+
When you convert an XML document to Rsxml you can choose either <tt>:xml</tt> or <tt>:exploded</tt> style
|
33
|
+
|
34
|
+
==== <tt>:xml</tt> style
|
35
|
+
|
36
|
+
In <tt>:xml</tt> style namespaces are declared using attributes, and namespaces are referenced using
|
37
|
+
prefixed QNames, just as in XML
|
38
|
+
|
39
|
+
Rsxml.to_rsxml('<foo:foofoo xmlns:foo="http://foo.com/foo" foo:bar="barbar"/>', :style=>:xml)
|
40
|
+
=> ["foo:foofoo", {"foo:bar"=>"barbar", "xmlns:foo"=>"http://foo.com/foo"}]
|
41
|
+
|
42
|
+
==== <tt>:exploded</tt> style
|
43
|
+
|
44
|
+
In <tt>:exploded</tt> style namespaces are not declared using attributes, and QNames are specified
|
45
|
+
using <tt>[local_name, prefix, uri]</tt> triples
|
46
|
+
|
47
|
+
Rsxml.to_rsxml('<foo:foofoo xmlns:foo="http://foo.com/foo" foo:bar="barbar"/>', :style=>:exploded)
|
48
|
+
=> [["foofoo", "foo", "http://foo.com/foo"], {["bar", "foo", "http://foo.com/foo"]=>"barbar"}]
|
49
|
+
|
50
|
+
=== Converting to XML
|
51
|
+
|
52
|
+
Rsxml styles can be mixed, and unnecessary namespace references can be skipped for readability
|
53
|
+
|
54
|
+
Rsxml.to_xml([["foofoo", "foo", "http://foo.com/foo"], {"foo:bar"=>"1", ["baz", "foo"]=>"2"}])
|
55
|
+
=> '<foo:foofoo foo:baz="2" foo:bar="1" xmlns:foo="http://foo.com/foo"></foo:foofoo>'
|
56
|
+
|
57
|
+
=== Fragments
|
58
|
+
|
59
|
+
XML Fragments, without proper namespace declarations, can be parsed by passing a Hash of namespace
|
60
|
+
prefix bindings
|
61
|
+
|
62
|
+
Rsxml.to_rsxml('<foo:foofoo foo:bar="barbar"/>', :ns=>{"foo"=>"http://foo.com/foo"}, :style=>:xml)
|
63
|
+
=> ["foo:foofoo", {"foo:bar"=>"barbar"}]
|
64
|
+
|
65
|
+
Fragments can be generated similarly :
|
66
|
+
|
67
|
+
Rsxml.to_xml(["foo:foofoo", {"foo:bar"=>"barbar"}], :ns=>{"foo"=>"http://foo.com/foo"})
|
68
|
+
=> '<foo:foofoo foo:bar="barbar"></foo:foofoo>'
|
28
69
|
|
29
70
|
== Install
|
30
71
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
data/lib/rsxml.rb
CHANGED
@@ -1,26 +1,36 @@
|
|
1
|
+
$: << File.expand_path('../../lib', __FILE__)
|
2
|
+
|
1
3
|
require 'nokogiri'
|
2
4
|
require 'builder'
|
5
|
+
require 'rsxml/util'
|
6
|
+
require 'rsxml/namespace'
|
7
|
+
require 'rsxml/visitor'
|
8
|
+
require 'rsxml/sexp'
|
9
|
+
require 'rsxml/xml'
|
3
10
|
|
4
11
|
module Rsxml
|
12
|
+
class << self
|
13
|
+
include Util
|
14
|
+
attr_accessor :logger
|
15
|
+
end
|
16
|
+
|
5
17
|
module_function
|
6
18
|
|
7
|
-
def
|
8
|
-
(
|
9
|
-
raise "opt not permitted: #{k}" if !constraints.has_key?(k)
|
10
|
-
constraint = constraints[k]
|
11
|
-
end
|
19
|
+
def log
|
20
|
+
yield(logger) if logger
|
12
21
|
end
|
13
22
|
|
23
|
+
TO_XML_OPTS = {:ns=>nil}
|
24
|
+
|
14
25
|
# convert an Rsxml s-expression representation of an XML document to XML
|
15
26
|
# Rsxml.to_xml(["Foo", {"foofoo"=>"10"}, ["Bar", "barbar"] ["Baz"]])
|
16
27
|
# => '<Foo foofoo="10"><Bar>barbar</Bar><Baz></Baz></Foo>'
|
17
|
-
def to_xml(rsxml,
|
18
|
-
|
19
|
-
Sexp.
|
20
|
-
xml.target!
|
28
|
+
def to_xml(rsxml, opts={})
|
29
|
+
opts = check_opts(TO_XML_OPTS, opts)
|
30
|
+
Sexp.traverse(rsxml, Visitor::WriteXmlVisitor.new, Visitor::Context.new(opts[:ns])).to_s
|
21
31
|
end
|
22
32
|
|
23
|
-
TO_RSXML_OPTS = {:ns=>nil}
|
33
|
+
TO_RSXML_OPTS = {:ns=>nil}.merge(Visitor::BuildRsxmlVisitor::OPTS)
|
24
34
|
|
25
35
|
# convert an XML string to an Rsxml s-expression representation
|
26
36
|
# Rsxml.to_rsxml('<Foo foofoo="10"><Bar>barbar</Bar><Baz></Baz></Foo>')
|
@@ -33,10 +43,10 @@ module Rsxml
|
|
33
43
|
# Rsxml.to_rsxml(fragment, {"foo"=>"http://foo.com/foo", ""=>"http://baz.com/baz"})
|
34
44
|
# => ["foo:Foo", {"foo:foofoo"=>"10", "xmlns:foo"=>"http://foo.com/foo", "xmlns"=>"http://baz.com/baz"}, ["Bar", "barbar"], ["Baz"]]
|
35
45
|
def to_rsxml(doc, opts={})
|
36
|
-
check_opts(TO_RSXML_OPTS, opts)
|
37
|
-
doc = Xml.wrap_fragment(doc, opts
|
46
|
+
opts = check_opts(TO_RSXML_OPTS, opts)
|
47
|
+
doc = Xml.wrap_fragment(doc, opts.delete(:ns))
|
38
48
|
root = Xml.unwrap_fragment(Nokogiri::XML(doc).children.first)
|
39
|
-
Xml.
|
49
|
+
Xml.traverse(root, Visitor::BuildRsxmlVisitor.new(opts)).sexp
|
40
50
|
end
|
41
51
|
|
42
52
|
# compare two documents in XML or Rsxml. returns +true+ if they are identical, and
|
@@ -47,140 +57,4 @@ module Rsxml
|
|
47
57
|
Sexp.compare(sexp_a, sexp_b)
|
48
58
|
end
|
49
59
|
|
50
|
-
module Sexp
|
51
|
-
module_function
|
52
|
-
|
53
|
-
def write_xml(xml, sexp, path="", &transformer)
|
54
|
-
tag, attrs, children = decompose_sexp(sexp)
|
55
|
-
|
56
|
-
if transformer
|
57
|
-
txtag, txattrs = transformer.call(tag, attrs, path)
|
58
|
-
else
|
59
|
-
txtag, txattrs = [tag, attrs]
|
60
|
-
end
|
61
|
-
|
62
|
-
cp = [path, tag].join("/")
|
63
|
-
xml.__send__(txtag, txattrs) do
|
64
|
-
children.each_with_index do |child, i|
|
65
|
-
if child.is_a?(Array)
|
66
|
-
write_xml(xml, child, "#{cp}[#{i}]", &transformer)
|
67
|
-
else
|
68
|
-
xml << child
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def decompose_sexp(sexp)
|
75
|
-
raise "invalid rsxml: #{rsxml.inspect}" if sexp.length<1
|
76
|
-
tag = sexp[0].to_s
|
77
|
-
if sexp[1].is_a?(Hash)
|
78
|
-
attrs = sexp[1]
|
79
|
-
children = sexp[2..-1]
|
80
|
-
else
|
81
|
-
attrs = {}
|
82
|
-
children = sexp[1..-1]
|
83
|
-
end
|
84
|
-
[tag, attrs, children]
|
85
|
-
end
|
86
|
-
|
87
|
-
class ComparisonError < RuntimeError
|
88
|
-
attr_reader :path
|
89
|
-
def initialize(msg, path)
|
90
|
-
super("[#{path}]: #{msg}")
|
91
|
-
@path = path
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
def compare(sexpa, sexpb, path=nil)
|
96
|
-
taga, attrsa, childrena = decompose_sexp(sexpa)
|
97
|
-
tagb, attrsb, childrenb = decompose_sexp(sexpb)
|
98
|
-
|
99
|
-
raise ComparisonError.new("element names differ: '#{taga}', '#{tagb}'", path) if taga != tagb
|
100
|
-
raise ComparisonError.new("attributes differ", path) if attrsa != attrsb
|
101
|
-
raise ComparisonError.new("child cound differes", path) if childrena.length != childrenb.length
|
102
|
-
|
103
|
-
path = [path, taga].compact.join("/")
|
104
|
-
(0...childrena.length).each do |i|
|
105
|
-
if childrena[i].is_a?(Array) && childrenb[i].is_a?(Array)
|
106
|
-
compare(childrena[i], childrenb[i], path)
|
107
|
-
else
|
108
|
-
raise ComparisonError.new("content differs: '#{childrena[i]}', '#{childrenb[i]}'", path) if childrena[i] != childrenb[i]
|
109
|
-
end
|
110
|
-
end
|
111
|
-
true
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
module Xml
|
116
|
-
module_function
|
117
|
-
|
118
|
-
WRAP_ELEMENT = "RsxmlXmlWrapper"
|
119
|
-
|
120
|
-
def wrap_fragment(fragment, ns_prefixes)
|
121
|
-
return fragment if !ns_prefixes
|
122
|
-
|
123
|
-
ns_attrs = Hash[*ns_prefixes.map do |prefix,href|
|
124
|
-
prefix = nil if prefix.to_s.length == 0
|
125
|
-
[["xmlns", prefix].compact.join(":"), href]
|
126
|
-
end.flatten]
|
127
|
-
xml = Builder::XmlMarkup.new
|
128
|
-
xml.__send__(WRAP_ELEMENT, ns_attrs) do
|
129
|
-
xml << fragment
|
130
|
-
end
|
131
|
-
xml.target!
|
132
|
-
end
|
133
|
-
|
134
|
-
def unwrap_fragment(node)
|
135
|
-
if node.name==WRAP_ELEMENT
|
136
|
-
node.children.first
|
137
|
-
else
|
138
|
-
node
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
def read_xml(node, ns_stack)
|
143
|
-
prefix = node.namespace.prefix if node.namespace
|
144
|
-
tag = node.name
|
145
|
-
ns_tag = [prefix,tag].compact.join(":")
|
146
|
-
|
147
|
-
attrs = read_attributes(node.attributes)
|
148
|
-
attrs = attrs.merge(namespace_attributes(node.namespaces, ns_stack))
|
149
|
-
attrs = nil if attrs.empty?
|
150
|
-
|
151
|
-
children = node.children.map do |child|
|
152
|
-
if child.text?
|
153
|
-
child.text
|
154
|
-
else
|
155
|
-
begin
|
156
|
-
ns_stack.push(node.namespaces)
|
157
|
-
read_xml(child, ns_stack)
|
158
|
-
ensure
|
159
|
-
ns_stack.pop
|
160
|
-
end
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
[ns_tag, attrs, *children].compact
|
165
|
-
end
|
166
|
-
|
167
|
-
def read_attributes(attrs)
|
168
|
-
Hash[*attrs.map do |n, attr|
|
169
|
-
prefix = attr.namespace.prefix if attr.namespace
|
170
|
-
name = attr.name
|
171
|
-
ns_name = [prefix,name].compact.join(":")
|
172
|
-
[ns_name, attr.value]
|
173
|
-
end.flatten]
|
174
|
-
end
|
175
|
-
|
176
|
-
def namespace_attributes(namespaces, ns_stack)
|
177
|
-
Hash[*namespaces.map do |prefix,href|
|
178
|
-
[prefix, href] if !find_namespace(prefix, ns_stack)
|
179
|
-
end.compact.flatten]
|
180
|
-
end
|
181
|
-
|
182
|
-
def find_namespace(prefix, ns_stack)
|
183
|
-
ns_stack.reverse.find{ |nsh| nsh.has_key?(prefix)}
|
184
|
-
end
|
185
|
-
end
|
186
60
|
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
module Rsxml
|
2
|
+
module Namespace
|
3
|
+
module_function
|
4
|
+
|
5
|
+
# compact all attribute QNames to Strings
|
6
|
+
def compact_attr_qnames(ns_stack, attrs)
|
7
|
+
Hash[attrs.map do |name,value|
|
8
|
+
[compact_qname(ns_stack, name), value]
|
9
|
+
end]
|
10
|
+
end
|
11
|
+
|
12
|
+
# explode attribute QNames to [LocalPart, prefix, URI] triples,
|
13
|
+
def explode_attr_qnames(ns_stack, attrs)
|
14
|
+
Hash[attrs.map do |name, value|
|
15
|
+
uq_name = explode_qname(ns_stack, name, true)
|
16
|
+
local_name, prefix, uri = uq_name
|
17
|
+
if !prefix || prefix==""
|
18
|
+
[local_name, value]
|
19
|
+
else
|
20
|
+
[uq_name, value]
|
21
|
+
end
|
22
|
+
end]
|
23
|
+
end
|
24
|
+
|
25
|
+
# produce a QName String from a [LocalPart, prefix, URI] triple
|
26
|
+
def compact_qname(ns_stack, name)
|
27
|
+
return name if name.is_a?(String)
|
28
|
+
|
29
|
+
local_part, prefix, uri = name
|
30
|
+
raise "invalid name: #{name}" if !prefix && uri
|
31
|
+
if prefix
|
32
|
+
if prefix!="xmlns"
|
33
|
+
ns = find_namespace_uri(ns_stack, prefix, uri)
|
34
|
+
raise "namespace prefix not bound to a namespace: '#{prefix}'" if ! ns
|
35
|
+
end
|
36
|
+
[prefix, local_part].map{|s| s.to_s unless s.to_s.empty?}.compact.join(':')
|
37
|
+
else
|
38
|
+
local_part
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# split a QName into [LocalPart, prefix, URI] triple
|
43
|
+
def explode_qname(ns_stack, qname, attr=false)
|
44
|
+
if qname.is_a?(Array)
|
45
|
+
if qname.length>1 && !qname[1].nil?
|
46
|
+
return qname
|
47
|
+
elsif qname.length>1 && qname[1].nil? && !qname[2].nil?
|
48
|
+
raise "invalid name: #{qname.inspect}"
|
49
|
+
else
|
50
|
+
return qname[0]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
local_part, prefix = split_qname(qname)
|
55
|
+
if prefix
|
56
|
+
if prefix=="xmlns" && attr
|
57
|
+
[local_part, prefix]
|
58
|
+
else
|
59
|
+
uri = find_namespace_uri(ns_stack, prefix)
|
60
|
+
raise "namespace prefix not bound: '#{prefix}'" if ! uri
|
61
|
+
[local_part, prefix, uri]
|
62
|
+
end
|
63
|
+
else
|
64
|
+
if attr
|
65
|
+
local_part
|
66
|
+
else
|
67
|
+
default_uri = find_namespace_uri(ns_stack, "")
|
68
|
+
if default_uri
|
69
|
+
[local_part, "", default_uri]
|
70
|
+
else
|
71
|
+
local_part
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# split a qname String into a [local_part, prefix] pair
|
78
|
+
def split_qname(qname)
|
79
|
+
return qname if qname.is_a?(Array)
|
80
|
+
|
81
|
+
if qname =~ /^[^:]+:[^:]+$/
|
82
|
+
[*qname.split(':')].reverse
|
83
|
+
else
|
84
|
+
qname
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# returns the namespace uri for a prefix, if declared in the stack
|
89
|
+
def find_namespace_uri(ns_stack, prefix, uri_check=nil)
|
90
|
+
tns = ns_stack.reverse.find{|ns| ns.has_key?(prefix)}
|
91
|
+
uri = tns[prefix] if tns
|
92
|
+
raise "prefix: '#{prefix}' is bound to uri: '#{uri}', but should be '#{uri_check}'" if uri_check && uri && uri!=uri_check
|
93
|
+
uri
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
# extract a Hash of {prefix=>uri} mappings declared in attributes
|
98
|
+
def extract_declared_namespace_bindings(attrs)
|
99
|
+
Hash[attrs.map do |name,value|
|
100
|
+
local_part, prefix, uri = split_qname(name)
|
101
|
+
if (prefix && prefix == "xmlns")
|
102
|
+
[local_part, value]
|
103
|
+
elsif (!prefix && local_part == "xmlns")
|
104
|
+
["", value]
|
105
|
+
end
|
106
|
+
end.compact]
|
107
|
+
end
|
108
|
+
|
109
|
+
# extract a Hash of {prefix=>uri} mappings from exploded QName tag and attrs
|
110
|
+
def extract_explicit_namespace_bindings(tag, attrs)
|
111
|
+
tag_local_part, tag_prefix, tag_uri = tag
|
112
|
+
ns = {}
|
113
|
+
ns[tag_prefix] = tag_uri if tag_prefix && tag_uri
|
114
|
+
|
115
|
+
attrs.each do |name, value|
|
116
|
+
attr_local_part, attr_prefix, attr_uri = name
|
117
|
+
if attr_prefix && attr_uri
|
118
|
+
raise "bindings clash: '#{attr_prefix}'=>'#{ns[attr_prefix]}' , '#{attr_prefix}'=>'#{attr_uri}'" if ns.has_key?(attr_prefix) && ns[attr_prefix]!=attr_uri
|
119
|
+
ns[attr_prefix] = attr_uri
|
120
|
+
end
|
121
|
+
end
|
122
|
+
ns
|
123
|
+
end
|
124
|
+
|
125
|
+
# figure out which explicit namespaces need declaring
|
126
|
+
#
|
127
|
+
# +ns_stack+ is the stack of namespace bindings
|
128
|
+
# +ns_explicit+ is the explicit refs for a tag
|
129
|
+
def undeclared_namespace_bindings(ns_stack, ns_explicit)
|
130
|
+
Hash[ns_explicit.map do |prefix,uri|
|
131
|
+
[prefix, uri] if !find_namespace_uri(ns_stack, prefix, uri)
|
132
|
+
end.compact]
|
133
|
+
end
|
134
|
+
|
135
|
+
# produce a Hash of namespace declaration attributes with exploded
|
136
|
+
# QNames, from
|
137
|
+
# a Hash of namespace prefix bindings
|
138
|
+
def exploded_namespace_declarations(ns)
|
139
|
+
Hash[ns.map do |prefix, uri|
|
140
|
+
if prefix==""
|
141
|
+
["xmlns", uri]
|
142
|
+
else
|
143
|
+
[[prefix, "xmlns"], uri]
|
144
|
+
end
|
145
|
+
end]
|
146
|
+
end
|
147
|
+
|
148
|
+
# merges two sets of namespace bindings, raising error on clash
|
149
|
+
def merge_namespace_bindings(ns1, ns2)
|
150
|
+
m = ns1.clone
|
151
|
+
ns2.each do |k,v|
|
152
|
+
raise "bindings clash: '#{k}'=>'#{m[k]}' , '#{k}'=>'#{v}'" if m.has_key?(k) && m[k]!=v
|
153
|
+
m[k]=v
|
154
|
+
end
|
155
|
+
m
|
156
|
+
end
|
157
|
+
|
158
|
+
# given the existing +ns_stack+ of ns bindings, a +tag+ and it's +attributes+,
|
159
|
+
# return a pair <tt>[ns_bindings, ns_additional_decls]</tt> containing
|
160
|
+
# ns bindings for the stack, and additional required (exploded) namespace
|
161
|
+
# declarations to be added to the attributes
|
162
|
+
def namespace_bindings_declarations(ns_stack, tag, attrs)
|
163
|
+
ns_declared = extract_declared_namespace_bindings(attrs)
|
164
|
+
ns_explicit = extract_explicit_namespace_bindings(tag, attrs)
|
165
|
+
ns_undeclared = undeclared_namespace_bindings(ns_stack + [ns_declared], ns_explicit)
|
166
|
+
ns_bindings = merge_namespace_bindings(ns_declared, ns_undeclared)
|
167
|
+
|
168
|
+
# and declarations for undeclared namespaces
|
169
|
+
ns_additional_decls = exploded_namespace_declarations(ns_undeclared)
|
170
|
+
|
171
|
+
[ns_bindings, ns_additional_decls]
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
end
|
data/lib/rsxml/sexp.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
module Rsxml
|
2
|
+
module Sexp
|
3
|
+
|
4
|
+
module_function
|
5
|
+
|
6
|
+
# pre-order traversal of the sexp, calling methods on
|
7
|
+
# the visitor with each node
|
8
|
+
def traverse(sexp, visitor, context=Visitor::Context.new)
|
9
|
+
tag, attrs, children = decompose_sexp(sexp)
|
10
|
+
|
11
|
+
ns_bindings, ns_additional_decls = Namespace::namespace_bindings_declarations(context.ns_stack, tag, attrs)
|
12
|
+
|
13
|
+
context.ns_stack.push(ns_bindings)
|
14
|
+
|
15
|
+
etag = Namespace::explode_qname(context.ns_stack, tag)
|
16
|
+
eattrs = Namespace::explode_attr_qnames(context.ns_stack, attrs)
|
17
|
+
|
18
|
+
eattrs = eattrs.merge(ns_additional_decls)
|
19
|
+
|
20
|
+
begin
|
21
|
+
visitor.tag(context, etag, eattrs) do
|
22
|
+
context.push_node([etag, eattrs])
|
23
|
+
begin
|
24
|
+
children.each_with_index do |child, i|
|
25
|
+
if child.is_a?(Array)
|
26
|
+
traverse(child, visitor, context)
|
27
|
+
else
|
28
|
+
visitor.text(context, child)
|
29
|
+
context.processed_node(child)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
ensure
|
33
|
+
context.pop_node
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
ensure
|
38
|
+
context.ns_stack.pop
|
39
|
+
end
|
40
|
+
|
41
|
+
visitor
|
42
|
+
end
|
43
|
+
|
44
|
+
# decompose a sexp to a [tag, attrs, children] list
|
45
|
+
def decompose_sexp(sexp)
|
46
|
+
raise "invalid rsxml: #{rsxml.inspect}" if sexp.length<1
|
47
|
+
if sexp[0].is_a?(Array)
|
48
|
+
tag = sexp[0]
|
49
|
+
else
|
50
|
+
tag = sexp[0].to_s
|
51
|
+
end
|
52
|
+
if sexp[1].is_a?(Hash)
|
53
|
+
attrs = sexp[1]
|
54
|
+
children = sexp[2..-1]
|
55
|
+
else
|
56
|
+
attrs = {}
|
57
|
+
children = sexp[1..-1]
|
58
|
+
end
|
59
|
+
[tag, attrs, children]
|
60
|
+
end
|
61
|
+
|
62
|
+
class ComparisonError < RuntimeError
|
63
|
+
attr_reader :path
|
64
|
+
def initialize(msg, path)
|
65
|
+
super("[#{path}]: #{msg}")
|
66
|
+
@path = path
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def compare(sexpa, sexpb, path=nil)
|
71
|
+
taga, attrsa, childrena = decompose_sexp(sexpa)
|
72
|
+
tagb, attrsb, childrenb = decompose_sexp(sexpb)
|
73
|
+
|
74
|
+
raise ComparisonError.new("element names differ: '#{taga}', '#{tagb}'", path) if taga != tagb
|
75
|
+
raise ComparisonError.new("attributes differ", path) if attrsa != attrsb
|
76
|
+
raise ComparisonError.new("child count differs", path) if childrena.length != childrenb.length
|
77
|
+
|
78
|
+
path = [path, taga].compact.join("/")
|
79
|
+
(0...childrena.length).each do |i|
|
80
|
+
if childrena[i].is_a?(Array) && childrenb[i].is_a?(Array)
|
81
|
+
compare(childrena[i], childrenb[i], path)
|
82
|
+
else
|
83
|
+
raise ComparisonError.new("content differs: '#{childrena[i]}', '#{childrenb[i]}'", path) if childrena[i] != childrenb[i]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|