rsxml 0.1.4 → 0.2.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.
@@ -0,0 +1,25 @@
1
+ module Rsxml
2
+ module Util
3
+ module_function
4
+
5
+ # simple option checking, with value constraints and sub-hash checking
6
+ def check_opts(constraints, opts)
7
+ opts ||= {}
8
+ opts.each{|k,v| raise "opt not permitted: #{k.inspect}" if !constraints.has_key?(k)}
9
+ Hash[constraints.map do |k,constraint|
10
+ if opts.has_key?(k)
11
+ v = opts[k]
12
+ if constraint.is_a?(Array)
13
+ raise "unknown value for opt #{k.inspect}: #{v.inspect}. permitted values are: #{constraint.inspect}" if !constraint.include?(v)
14
+ [k,v]
15
+ elsif constraint.is_a?(Hash)
16
+ raise "opt #{k.inspect} must be a Hash" if !v.is_a?(Hash)
17
+ [k,check_opts(constraint, v || {})]
18
+ else
19
+ [k,v]
20
+ end
21
+ end
22
+ end]
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,132 @@
1
+ module Rsxml
2
+ module Visitor
3
+ class << self
4
+ include Util
5
+ end
6
+
7
+ class Context
8
+ attr_reader :ns_stack
9
+ attr_reader :node_stack
10
+ attr_reader :prev_siblings
11
+ def initialize(initial_ns_bindings = nil)
12
+ @ns_stack=[]
13
+ @ns_stack << initial_ns_bindings if initial_ns_bindings
14
+ @node_stack=[]
15
+ @prev_siblings=[]
16
+ @sibling_stack=[]
17
+ end
18
+
19
+ def push_node(node)
20
+ node_stack.push(node)
21
+ @sibling_stack.push(@prev_siblings)
22
+ @prev_siblings=[]
23
+ end
24
+
25
+ def pop_node
26
+ n = node_stack.pop
27
+ @prev_siblings = @sibling_stack.pop
28
+ @prev_siblings << n
29
+ end
30
+
31
+ def processed_node(node)
32
+ @prev_siblings << node
33
+ end
34
+ end
35
+
36
+ class WriteXmlVisitor
37
+ attr_reader :xml
38
+
39
+ def initialize(xml_builder=nil)
40
+ @xml = xml_builder || Builder::XmlMarkup.new
41
+ end
42
+
43
+ def tag(context, name, attrs)
44
+ qname = Namespace::compact_qname(context.ns_stack, name)
45
+ qattrs = Namespace::compact_attr_qnames(context.ns_stack, attrs)
46
+
47
+ xml.__send__(qname, qattrs) do
48
+ yield
49
+ end
50
+ end
51
+
52
+ def text(context, text)
53
+ xml << text
54
+ end
55
+
56
+ def to_s
57
+ xml.target!
58
+ end
59
+ end
60
+
61
+ class BuildRsxmlVisitor
62
+ attr_reader :sexp
63
+ attr_reader :cursor_stack
64
+ attr_reader :opts
65
+
66
+ # The <tt>:style</tt> option specifies how the Rsxml is to be produced
67
+ # :xml style is with compact <tt>"prefix:local_name"</tt> Strings for QNames, and namespace declaration attributes
68
+ # :exploded style is with <tt>[local_name, prefix, uri]</tt> triples for QNames, and no namespace declaration attributes
69
+ OPTS = {:style=>[:xml, :exploded]}
70
+
71
+ def initialize(opts=nil)
72
+ @opts = opts || {}
73
+ opts = Util.check_opts(OPTS, opts)
74
+ @cursor_stack = []
75
+ @sexp
76
+ end
77
+
78
+ def compact_qname(qname)
79
+ local_name, prefix, uri = qname
80
+
81
+ [prefix, local_name].map{|s| (!s || s.empty?) ? nil : s}.compact.join(":")
82
+ end
83
+
84
+ def compact_attr_names(attrs)
85
+ Hash[attrs.map{|qname,value| [compact_qname(qname), value]}]
86
+ end
87
+
88
+ # strip namespace decls from exploded attributes
89
+ def strip_namespace_decls(attrs)
90
+ Hash[attrs.map do |qname,value|
91
+ local_name, prefix, uri = qname
92
+ if !(prefix=="xmlns" || (prefix==nil && local_name=="xmlns"))
93
+ [qname,value]
94
+ end
95
+ end.compact]
96
+ end
97
+
98
+ def tag(context, tag, attrs)
99
+
100
+ opts[:style] ||= :exploded
101
+ if opts[:style] == :xml
102
+ tag = compact_qname(tag)
103
+ attrs = compact_attr_names(attrs)
104
+ elsif opts[:style] == :exploded
105
+ attrs = strip_namespace_decls(attrs)
106
+ end
107
+
108
+ el = [tag, (attrs if attrs.size>0)].compact
109
+
110
+ if !cursor_stack.last
111
+ @sexp = el
112
+ else
113
+ cursor_stack.last << el
114
+ end
115
+ cursor_stack.push(el)
116
+
117
+ begin
118
+ yield
119
+ ensure
120
+ cursor_stack.pop
121
+ end
122
+ end
123
+
124
+ def text(context, text)
125
+ cursor_stack.last << text
126
+ end
127
+ end
128
+
129
+
130
+
131
+ end
132
+ end
@@ -0,0 +1,99 @@
1
+ module Rsxml
2
+ module Xml
3
+
4
+ module_function
5
+
6
+ WRAP_ELEMENT = "RsxmlXmlWrapper"
7
+
8
+ def wrap_fragment(fragment, ns_prefixes)
9
+ return fragment if !ns_prefixes
10
+
11
+ ns_attrs = Hash[ns_prefixes.map do |prefix,href|
12
+ prefix = nil if prefix.to_s.length == 0
13
+ [["xmlns", prefix].compact.join(":"), href]
14
+ end]
15
+ xml = Builder::XmlMarkup.new
16
+ xml.__send__(WRAP_ELEMENT, ns_attrs) do
17
+ xml << fragment
18
+ end
19
+ xml.target!
20
+ end
21
+
22
+ def unwrap_fragment(node)
23
+ if node.name==WRAP_ELEMENT
24
+ node.children.first
25
+ else
26
+ node
27
+ end
28
+ end
29
+
30
+ def explode_node(node)
31
+ node_name = node.name
32
+ if node.namespace
33
+ node_prefix = node.namespace.prefix || "" # nokogiri has nil prefix for default namespace
34
+ node_uri = node.namespace.href
35
+ end
36
+ node = node_uri ? [node_name, node_prefix, node_uri] : node_name
37
+ end
38
+
39
+ def explode_ns_definitions(element)
40
+ Hash[element.namespace_definitions.map do |ns|
41
+ if ns.prefix
42
+ [[ns.prefix, "xmlns"], ns.href]
43
+ else
44
+ ["xmlns", ns.href]
45
+ end
46
+ end]
47
+ end
48
+
49
+ # given a <tt>Nokogiri::XML::Element</tt> in +element+ , produce
50
+ # a <tt>[[local_name, prefix, namespace], {[local_name, prefix, namespace]=>value}</tt>
51
+ # pair of exploded element name and attributes with exploded names
52
+ def explode_element(element)
53
+ eelement = explode_node(element)
54
+
55
+ eattrs = Hash[element.attributes.map do |name,attr|
56
+ [explode_node(attr), attr.value]
57
+ end]
58
+
59
+ ns_decl_attrs = explode_ns_definitions(element)
60
+ eattrs = eattrs.merge(ns_decl_attrs)
61
+
62
+ [eelement, eattrs]
63
+ end
64
+
65
+ # pre-order traversal of the Nokogiri Nodes, calling methods on
66
+ # the visitor with each Node
67
+ def traverse(element, visitor, context = Visitor::Context.new)
68
+ eelement, eattrs = explode_element(element)
69
+
70
+ ns_bindings = Rsxml::Namespace.extract_declared_namespace_bindings(element.namespaces)
71
+ context.ns_stack.push(ns_bindings)
72
+
73
+ begin
74
+ visitor.tag(context, eelement, eattrs) do
75
+ context.push_node([eelement, eattrs])
76
+ begin
77
+ element.children.each do |child|
78
+ if child.element?
79
+ traverse(child, visitor, context)
80
+ elsif child.text?
81
+ visitor.text(context, child.content)
82
+ context.processed_node(child.content)
83
+ else
84
+ Rsxml.log{|logger| logger.warn("unknown Nokogiri Node type: #{child.inspect}")}
85
+ end
86
+ end
87
+ ensure
88
+ context.pop_node
89
+ end
90
+ end
91
+ ensure
92
+ context.ns_stack.pop
93
+ end
94
+
95
+ visitor
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,250 @@
1
+ require File.expand_path("../../spec_helper", __FILE__)
2
+
3
+ module Rsxml
4
+ describe "find_namespace_uri" do
5
+ it "should find a namespace uri in a stack of prefix bindings" do
6
+ Namespace.find_namespace_uri([{"foo"=>"http://foo.com/foo"},
7
+ {"bar"=>"http://bar.com/bar"}],
8
+ "bar").should == "http://bar.com/bar"
9
+ Namespace.find_namespace_uri([{"foo"=>"http://foo.com/foo"},
10
+ {"bar"=>"http://bar.com/bar"}],
11
+ "foo").should == "http://foo.com/foo"
12
+ end
13
+
14
+ it "should return nil if there is no matching binding" do
15
+ Namespace.find_namespace_uri([{"foo"=>"http://foo.com/foo"},
16
+ {"bar"=>"http://bar.com/bar"}],
17
+ "blah").should == nil
18
+ Namespace.find_namespace_uri([],
19
+ "blah").should == nil
20
+ end
21
+
22
+ end
23
+
24
+ describe "explode_qname" do
25
+ it "should do nothing to an array with more than one element" do
26
+ Namespace.explode_qname([], ["bar", "foo", "http://foo.com/foo"]).should ==
27
+ ["bar", "foo", "http://foo.com/foo"]
28
+ Namespace.explode_qname([], ["bar", "foo"]).should ==
29
+ ["bar", "foo"]
30
+ end
31
+
32
+ it "should return the first element of an array with only the first non-nil element" do
33
+ Namespace.explode_qname([], ["bar"]).should ==
34
+ "bar"
35
+ end
36
+
37
+ it "should raise an error if prefix is nil but uri is not" do
38
+ lambda {
39
+ Namespace.explode_qname([], ["bar", nil, "http://foo.com/foo"])
40
+ }.should raise_error(/invalid name/)
41
+ end
42
+
43
+ it "should unqualify an unprefixed name with no default namespace" do
44
+ Namespace.explode_qname([], "bar").should == "bar"
45
+ end
46
+
47
+ it "should unqualify an unprefixed name in the default namespace" do
48
+ Namespace.explode_qname([{""=>"http://foo.com/foo"}], "bar").should ==
49
+ ["bar", "", "http://foo.com/foo"]
50
+ end
51
+
52
+ it "should unqualify a prefixed name with a bound namespace" do
53
+ Namespace.explode_qname([{"foo"=>"http://foo.com/foo"}], "foo:bar").should ==
54
+ ["bar", "foo", "http://foo.com/foo"]
55
+ end
56
+
57
+ it "should raise an error with a prefixed name with no bound namespace" do
58
+ lambda {
59
+ Namespace.explode_qname([], "foo:bar")
60
+ }.should raise_error(/not bound/)
61
+ end
62
+ end
63
+
64
+ describe "split_qname" do
65
+ it "should split a name with prefix" do
66
+ Namespace.split_qname("foo:bar").should == ["bar", "foo"]
67
+ end
68
+
69
+ it "should leave a name without prefix unchanged" do
70
+ Namespace.split_qname("bar").should == "bar"
71
+ end
72
+
73
+ it "should leave an array unchanged" do
74
+ Namespace.split_qname(["bar", "foo"]).should == ["bar", "foo"]
75
+ end
76
+ end
77
+
78
+ describe "compact_attr_qnames" do
79
+ it "should qualify attribute names when there is a default namespaces" do
80
+ Namespace.compact_attr_qnames([{""=>"http://default.com/default","foo"=>"http://foo.com/foo"}], {["bar", "foo"] => "barbar", ["boo", ""] => "booboo", "baz" =>"bazbaz"}).should ==
81
+ {"foo:bar"=>"barbar", "boo"=>"booboo", "baz"=>"bazbaz"}
82
+ end
83
+
84
+ it "should raise an exception if a namespace is referenced but not bound" do
85
+ lambda {
86
+ Namespace.compact_attr_qnames([], {["bar", "foo"] => "barbar", "baz" =>"bazbaz"})
87
+ }.should raise_error(/not bound/)
88
+ end
89
+
90
+ it "should raise an exception if default namespace referenced but not bound" do
91
+ lambda {
92
+ Namespace.compact_attr_qnames([{"foo"=>"http://foo.com/foo"}], {["bar", "foo"] => "barbar", ["boo", ""] => "booboo", "baz" =>"bazbaz"})
93
+ }.should raise_error(/not bound/)
94
+ end
95
+ end
96
+
97
+ describe "explode_attr_qnames" do
98
+ it "should unqualify attributes when there is no default namespace" do
99
+ Namespace.explode_attr_qnames([{"foo"=>"http://foo.com/foo"}], {"foo:bar"=>"barbar", "baz"=>"bazbaz"}).should ==
100
+ {["bar", "foo", "http://foo.com/foo"]=>"barbar", "baz"=>"bazbaz"}
101
+ end
102
+
103
+ it "should unqualify attributes when there is a default namespace" do
104
+ Namespace.explode_attr_qnames([{""=>"http://default.com/default", "foo"=>"http://foo.com/foo"}], {"foo:bar"=>"barbar", "baz"=>"bazbaz"}).should ==
105
+ {["bar", "foo", "http://foo.com/foo"]=>"barbar", "baz"=>"bazbaz"}
106
+ end
107
+ end
108
+
109
+ describe "compact_qname" do
110
+ it "should produce a qname from a pair or triple" do
111
+ Namespace.compact_qname([{"foo"=>"http://foo.com/foo"}],
112
+ ["bar", "foo"]).should ==
113
+ "foo:bar"
114
+ Namespace.compact_qname([{"foo"=>"http://foo.com/foo"}],
115
+ ["bar", "foo", "http://foo.com/foo"]).should ==
116
+ "foo:bar"
117
+ end
118
+
119
+ it "should produce a qname with default namespace" do
120
+ Namespace.compact_qname([{""=>"http://foo.com/foo"}],
121
+ ["bar", ""]).should ==
122
+ "bar"
123
+
124
+ Namespace.compact_qname([{""=>"http://foo.com/foo"}],
125
+ ["bar", "", "http://foo.com/foo"]).should ==
126
+ "bar"
127
+ end
128
+
129
+ it "should do nothing to a String" do
130
+ Namespace.compact_qname([], "foo:bar").should == "foo:bar"
131
+ end
132
+
133
+ it "should raise an error if prefix is nil but uri is not" do
134
+ lambda {
135
+ Namespace.compact_qname([], ["bar", nil, "http://foo.com/foo"])
136
+ }.should raise_error(/invalid name/)
137
+ end
138
+
139
+ it "should raise an error if a prefix is not bound" do
140
+ lambda {
141
+ Namespace.compact_qname([],
142
+ ["bar", "foo", "http://foo.com/foo"])
143
+ }.should raise_error(/not bound/)
144
+ end
145
+
146
+ it "should raise an error if a prefix binding clashes" do
147
+ lambda {
148
+ Namespace.compact_qname([{"foo"=>"http://foo.com/foo"}],
149
+ ["bar", "foo", "http://bar.com/bar"])
150
+ }.should raise_error(/'foo' is bound/)
151
+ end
152
+
153
+ it "should raise an error if default namespace binding clashes" do
154
+ lambda {
155
+ Namespace.compact_qname([{""=>"http://foo.com/foo"}],
156
+ ["bar", "", "http://bar.com/bar"])
157
+ }.should raise_error(/'' is bound/)
158
+ end
159
+
160
+ end
161
+
162
+ describe "extract_declared_namespace_bindings" do
163
+ it "should extract a hash of declared namespace bindings from a Hash of attributes" do
164
+ Namespace.extract_declared_namespace_bindings({"xmlns"=>"http://default.com/default", "xmlns:foo"=>"http://foo.com/foo", "foo"=>"bar"}).should ==
165
+ {""=>"http://default.com/default", "foo"=>"http://foo.com/foo"}
166
+ end
167
+ end
168
+
169
+ describe "extract_explicit_namespace_bindings" do
170
+ it "should extract a hash of explicit namespace bindings from expanded tags" do
171
+ Namespace.extract_explicit_namespace_bindings(["bar", "foo", "http://foo.com/foo"],
172
+ {["baz", "bar", "http://bar.com/bar"]=>"baz",
173
+ ["boo", "", "http://default.com/default"]=>"boo"}).should ==
174
+ {"foo"=>"http://foo.com/foo", "bar"=>"http://bar.com/bar", ""=>"http://default.com/default"}
175
+ end
176
+
177
+ it "should raise an error if extracted namespaces clash" do
178
+ lambda{
179
+ Namespace.extract_explicit_namespace_bindings(["bar", "foo", "http://foo.com/foo"],
180
+ {["baz", "foo", "http://bar.com/bar"]=>"baz",
181
+ ["boo", "", "http://default.com/default"]=>"boo"})
182
+ }.should raise_error(/bindings clash/)
183
+ end
184
+ end
185
+
186
+ describe "undeclared_namespace_bindings" do
187
+ it "should determine which explicit namespaces are not declared in context" do
188
+ Namespace.undeclared_namespace_bindings([{""=>"http://default.com/default", "foo"=>"http://foo.com/foo"}], {""=>"http://default.com/default", "foo"=>"http://foo.com/foo", "bar"=>"http://bar.com/bar"}).should ==
189
+ {"bar"=>"http://bar.com/bar"}
190
+ end
191
+
192
+ it "should raise an exception if an explicit prefix clashes with a declard prefix" do
193
+ lambda {
194
+ Namespace.undeclared_namespace_bindings([{""=>"http://default.com/default", "foo"=>"http://foo.com/foo"}], {""=>"http://foo.com/foo", "foo"=>"http://foo.com/foo", "bar"=>"http://bar.com/bar"})
195
+ }.should raise_error(/is bound to uri/)
196
+ end
197
+ end
198
+
199
+ describe "merge_namespace_bindings" do
200
+ it "should merge two sets of namespace bindings" do
201
+ Namespace.merge_namespace_bindings({""=>"http://default.com/default",
202
+ "boo"=>"http://boo.com/boo"},
203
+ {"foo"=>"http://foo.com/foo",
204
+ "bar"=>"http://bar.com/bar"}).should ==
205
+ {""=>"http://default.com/default",
206
+ "boo"=>"http://boo.com/boo",
207
+ "foo"=>"http://foo.com/foo",
208
+ "bar"=>"http://bar.com/bar"}
209
+ end
210
+
211
+ it "should raise an error if there are binding clashes" do
212
+ lambda {
213
+ Namespace.merge_namespace_bindings({""=>"http://default.com/default",
214
+ "boo"=>"http://boo.com/boo"},
215
+ {""=>"http://foo.com/foo",
216
+ "bar"=>"http://bar.com/bar"})
217
+ }.should raise_error(/bindings clash/)
218
+ end
219
+ end
220
+
221
+ describe "exploded_namespace_declarations" do
222
+ it "should produce unqalified namespaces declarations" do
223
+ Namespace.exploded_namespace_declarations({""=>"http://default.com/default", "foo"=>"http://foo.com/foo"}).should ==
224
+ {"xmlns"=>"http://default.com/default", ["foo", "xmlns"]=>"http://foo.com/foo"}
225
+ end
226
+ end
227
+
228
+ describe "namespace_bindings_declarations" do
229
+ it "should extract declared and explicit bindings and additional declarations" do
230
+ Namespace.namespace_bindings_declarations([{"foo"=>"http://foo.com/foo"}],
231
+ [:bar, "foo", "http://foo.com/foo"],
232
+ { [:b, "bar", "http://bar.com/bar"]=>"barbar",
233
+ [:c, "baz", "http://baz.com/baz"]=>"bazbaz",
234
+ [:d, "foo"]=>"booboo"}).should ==
235
+ [{ "baz"=>"http://baz.com/baz",
236
+ "bar"=>"http://bar.com/bar"},
237
+ { ["baz", "xmlns"]=>"http://baz.com/baz",
238
+ ["bar", "xmlns"]=>"http://bar.com/bar"}]
239
+ end
240
+
241
+ it "should extract explicit default namespace bindings" do
242
+ Namespace.namespace_bindings_declarations([{"foo"=>"http://foo.com/foo"}],
243
+ [:bar, "", "http://default.com/default"],
244
+ { [:d, "foo"]=>"booboo"}).should ==
245
+ [{ ""=>"http://default.com/default"},
246
+ { "xmlns"=>"http://default.com/default"}]
247
+ end
248
+ end
249
+
250
+ end