moxml 0.5.101 → 0.5.102

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9109c62e1dc81916376846aa35756d02e7caa0caedbf96937c6c89fb99687389
4
- data.tar.gz: cab100fc1e408a0c511d330c81bc5f2b71e8dc0f6fa30e2c1b7a7e094d1c81a1
3
+ metadata.gz: d182614a13bd3934c6d6dc1bb9b137015dc1daa124aeac24df40246ea3109103
4
+ data.tar.gz: 51d96b5f964b185772d4378913c5e48e4775c41cf5fd1a3e5cf942576fa33183
5
5
  SHA512:
6
- metadata.gz: d041b4af64d6fe7d0d87d129ce35053da5405ba23b866e7dd3ef8c7237a21b73ff60d9baefafacf39781d5691e44b95ec295fcfe9eb54fa5e2a091ed01ea6ab7
7
- data.tar.gz: 2f21edaf882c65e3229c617eb5df04e49b0198de3133d968e898fa9ec9218d34b52c5f165791804aafb328bae55db20fddc80c0976d596c686e66630bbd0293d
6
+ metadata.gz: f4e9ae7331917e4fb295aa541028cb233d889127e4e32982d24ad1d696df668f52d223b70a1c6f2e96360f72138abf36a2eb055f15b9f48c069cb7b421191cfe
7
+ data.tar.gz: '0362854ca321c56e434a0f030232088d200d948dfe231c2d2cf4d2017c3af7316610e3f18ece3ca85a9a9a2c0d98f431c9f3bb465f186fe39ae23b63ea2e9a7b'
@@ -13,6 +13,13 @@ on:
13
13
  repository_dispatch:
14
14
  types: [ release-passed ]
15
15
 
16
+ # One live run per workflow+event+ref: a superseded push to the
17
+ # same PR/branch cancels its predecessors instead of queueing
18
+ # behind them (the runner queue otherwise stacks generations).
19
+ concurrency:
20
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
21
+ cancel-in-progress: true
22
+
16
23
  jobs:
17
24
  rake:
18
25
  uses: metanorma/ci/.github/workflows/dependent-rake.yml@main
@@ -6,6 +6,13 @@ on:
6
6
  tags: [ v* ]
7
7
  pull_request:
8
8
 
9
+ # One live run per workflow+event+ref: a superseded push to the
10
+ # same PR/branch cancels its predecessors instead of queueing
11
+ # behind them (the runner queue otherwise stacks generations).
12
+ concurrency:
13
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
9
16
  permissions:
10
17
  contents: read
11
18
 
@@ -6,6 +6,13 @@ on:
6
6
  tags: [ v* ]
7
7
  pull_request:
8
8
 
9
+ # One live run per workflow+event+ref: a superseded push to the
10
+ # same PR/branch cancels its predecessors instead of queueing
11
+ # behind them (the runner queue otherwise stacks generations).
12
+ concurrency:
13
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
9
16
  permissions:
10
17
  contents: read
11
18
 
@@ -5,6 +5,13 @@ on:
5
5
  branches: [ main ]
6
6
  pull_request:
7
7
 
8
+ # One live run per workflow+event+ref: a superseded push to the
9
+ # same PR/branch cancels its predecessors instead of queueing
10
+ # behind them (the runner queue otherwise stacks generations).
11
+ concurrency:
12
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
8
15
  permissions:
9
16
  contents: read
10
17
 
@@ -6,6 +6,13 @@ on:
6
6
  pull_request:
7
7
  workflow_dispatch:
8
8
 
9
+ # One live run per workflow+event+ref: a superseded push to the
10
+ # same PR/branch cancels its predecessors instead of queueing
11
+ # behind them (the runner queue otherwise stacks generations).
12
+ concurrency:
13
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
9
16
  permissions:
10
17
  contents: read
11
18
 
@@ -47,8 +47,11 @@ module Moxml
47
47
 
48
48
  def parse_fragment(xml, _context = nil)
49
49
  doc = parse("<m>#{xml}</m>")
50
- root = root(doc)
51
- root ? children(root) : []
50
+ # doc.root is a wrapper (adapter root overrides answer
51
+ # patched/unpatched natives indifferently); children() and
52
+ # every adapter below expect natives.
53
+ root_node = doc.root
54
+ root_node ? children(root_node.native) : []
52
55
  end
53
56
 
54
57
  # Streaming incremental parse (leptris engine): yields each
@@ -154,6 +157,14 @@ module Moxml
154
157
  node
155
158
  end
156
159
 
160
+ # Element-only children: adapters whose engine carries the
161
+ # face override (leptris C filter, nokogiri native); the
162
+ # default type-filters the native child list so non-element
163
+ # wrappers never mint on element-only traversals.
164
+ def element_children(node)
165
+ children(node).select { |child| node_type(child) == :element }
166
+ end
167
+
157
168
  # Prefixed-attribute value fast path: adapters whose engine
158
169
  # resolves expanded-name (uri, local) lookups natively answer
159
170
  # the value here; others fall back to the resolver's
@@ -394,6 +405,13 @@ namespace_validation_mode: :strict)
394
405
  true
395
406
  end
396
407
 
408
+ # Serialization of the node's children (Element#inner_xml).
409
+ # Built over the children/serialize contract; adapters with a
410
+ # native inner-serialization face may override.
411
+ def inner_xml(node)
412
+ children(node).map { |child| serialize(child) }.join
413
+ end
414
+
397
415
  # Whether the engine offers a bulk materialization path for
398
416
  # Materializer (issue #132). When true, the adapter gets
399
417
  # #materialize_fields(native, buffers, &block) — fill the
@@ -100,6 +100,10 @@ module Moxml
100
100
  NN_NEXT_SIBLING = NN.instance_method(:next_sibling)
101
101
  NN_CONTENT = NN.instance_method(:content)
102
102
  NN_CHILDREN = NN.instance_method(:children)
103
+ # The C element filter — captured because ElementBehavior's
104
+ # element_children alias shadows the face on Identity natives
105
+ # (a plain dispatch would recurse into the wrapper method).
106
+ NN_ELEMENT_CHILDREN = NN.instance_method(:element_children)
103
107
  NN_NODE_TYPE = NN.instance_method(:node_type)
104
108
  NN_DOCUMENT = NN.instance_method(:document)
105
109
  NN_ADD_CHILD = NN.instance_method(:add_child)
@@ -1397,6 +1401,19 @@ module Moxml
1397
1401
  end
1398
1402
  end
1399
1403
 
1404
+ # Element-only children through the engine-level filter: the
1405
+ # C face mints element-kind natives directly (klass-children),
1406
+ # so interleaved text/comment nodes cost nothing. Entity
1407
+ # markers are text-kind — the element filter excludes them by
1408
+ # construction, no marker split needed.
1409
+ def element_children(node)
1410
+ if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
1411
+ return NN_ELEMENT_CHILDREN.bind_call(node).to_a
1412
+ end
1413
+
1414
+ to_binding(node).element_children
1415
+ end
1416
+
1400
1417
  def children(node, entity_bearing: false)
1401
1418
  # NATIVE_READ_LAYER gates the whole keep-path: it consults
1402
1419
  # NN_DOCUMENT (defined only with the native layer), and
@@ -1596,10 +1613,25 @@ module Moxml
1596
1613
  [copied, pointers]
1597
1614
  end
1598
1615
 
1599
- # The binding reports the root element as parentless; the
1600
- # moxml contract roots at the document.
1616
+ # The binding reports the root element and the document-level
1617
+ # parts as parentless; the moxml contract roots them at the
1618
+ # document. DocType carries no #parent at all (plain
1619
+ # c_ptr+document value object) and a parentless PI is a
1620
+ # document-level one by construction — in-tree parts have
1621
+ # element parents.
1601
1622
  def root_parent(node)
1602
- node.parent || (node.document&.root == node ? node.document : nil)
1623
+ return node.document if node.is_a?(::Leptris::XML::DocType)
1624
+
1625
+ parent = node.parent
1626
+ return parent if parent
1627
+
1628
+ doc = node.document
1629
+ return nil unless doc
1630
+
1631
+ return doc if doc.root == node ||
1632
+ node.is_a?(::Leptris::XML::ProcessingInstruction)
1633
+
1634
+ nil
1603
1635
  end
1604
1636
 
1605
1637
  def next_sibling(node)
@@ -1949,7 +1981,19 @@ module Moxml
1949
1981
  node = to_binding(node)
1950
1982
  new_node = to_binding(new_node)
1951
1983
  end
1952
- return node.replace(new_node) if node.is_a?(::Leptris::XML::Element)
1984
+ if node.is_a?(::Leptris::XML::Element)
1985
+ # NOT Element#replace: it detaches via
1986
+ # leptris_element_remove_child, which leaves the replaced
1987
+ # node's sibling links pointing into the live tree. A later
1988
+ # append of that node splices the stale run under the new
1989
+ # parent, and when the run contains the append target the
1990
+ # tree cycles (standoc term cleanup does
1991
+ # t << n.replace(t2)). Insert + unlink instead: the unlink
1992
+ # path clears the sibling links.
1993
+ node.add_next_sibling(new_node)
1994
+ node.unlink
1995
+ return new_node
1996
+ end
1953
1997
 
1954
1998
  # libleptris only offers element-anchored insertion, so a
1955
1999
  # content node (text/comment/CDATA/PI) is replaced by
@@ -2175,7 +2219,22 @@ module Moxml
2175
2219
  node = to_binding(node)
2176
2220
  end
2177
2221
  return nil unless native_context_node?(node)
2178
- return nil unless native_expression?(expression)
2222
+ # Hash-bound xmlns rides native on probing bindings
2223
+ # (leptris resolves the reserved prefix from the hash);
2224
+ # empty mappings (xmlns="") select no-namespace elements
2225
+ # and stay on the Ruby engine — the C pairs-set rejects
2226
+ # empty URIs. Everything else keeps the expression-keyed
2227
+ # gate.
2228
+ return nil unless (namespaces.is_a?(Hash) &&
2229
+ namespaces["xmlns"].to_s.length.positive? &&
2230
+ xmlns_native?) ||
2231
+ native_expression?(expression)
2232
+ # The leptris engine mishandles namespace-prefixed node
2233
+ # tests: "m:*" matches elements in ANY namespace and
2234
+ # "m:name" matches none, regardless of the binding. Route
2235
+ # namespace-bound queries to the Ruby engine until the
2236
+ # engine grows correct prefix resolution.
2237
+ return nil if namespaces && !namespaces.empty?
2179
2238
  # The binding reports the root element parentless while moxml
2180
2239
  # roots it at the document, so parent-axis queries from the
2181
2240
  # root element keep the Ruby engine.
@@ -2311,6 +2370,28 @@ module Moxml
2311
2370
  # xmlns:name is a nokogiri-compat convention addressing
2312
2371
  # elements in the default namespace; only the Ruby engine
2313
2372
  # implements it.
2373
+ # One-time capability probe: does the binding's native engine
2374
+ # resolve the reserved xmlns prefix from the namespace hash?
2375
+ # Memoized class-level — the answer never changes for an
2376
+ # install. Only hash-bound xmlns queries may ride native
2377
+ # (the gate below is expression-keyed and cannot see the
2378
+ # call's namespaces); unbound xmlns stays on the Ruby engine,
2379
+ # whose reserved-prefix branch matches default-namespace
2380
+ # elements.
2381
+ def xmlns_native?
2382
+ return @xmlns_native unless @xmlns_native.nil?
2383
+
2384
+ @xmlns_native = begin
2385
+ probe = parse('<r xmlns="urn:p"><a>1</a></r>', {}, nil)
2386
+ raw = to_binding(probe.root.native)
2387
+ res = raw.xpath("./xmlns:a", { "xmlns" => "urn:p" })
2388
+ list = res.respond_to?(:to_a) ? res.to_a : [res].compact
2389
+ list.size == 1 && list.first.text.to_s == "1"
2390
+ rescue StandardError
2391
+ false
2392
+ end
2393
+ end
2394
+
2314
2395
  def uses_xmlns_prefix?(ast)
2315
2396
  return true if ast.type == :test && ast.value[:namespace] == "xmlns"
2316
2397
 
@@ -1099,8 +1099,9 @@ module Moxml
1099
1099
  target_doc = doc || (element.is_a?(::LibXML::XML::Node) ? element.doc : nil)
1100
1100
 
1101
1101
  if target_doc
1102
- # Use deep import to ensure all descendants are included
1103
- imported = target_doc.import(child, true)
1102
+ # libxml-ruby 6.0 dropped import's deep flag; the import
1103
+ # walks the subtree either way.
1104
+ imported = target_doc.import(child)
1104
1105
  element << imported
1105
1106
  else
1106
1107
  # No target document - create a deep copy of the node instead
@@ -318,6 +318,10 @@ module Moxml
318
318
  node.children
319
319
  end
320
320
 
321
+ def element_children(node)
322
+ node.element_children
323
+ end
324
+
321
325
  def adjacent_to_entity_reference?(node)
322
326
  node.previous_sibling.is_a?(::Nokogiri::XML::EntityReference) ||
323
327
  node.next_sibling.is_a?(::Nokogiri::XML::EntityReference)
@@ -16,6 +16,38 @@ module Moxml
16
16
  end
17
17
 
18
18
  def root=(element)
19
+ owner = element.is_a?(Node) && element.document
20
+ # Only a genuinely ATTACHED foreign element adopts: the document
21
+ # face answers arbitrary Document wrappers for FLOATING elements
22
+ # (leptris: a stale mint-time document; ox: an Element), and
23
+ # adopting a floating element splices its (empty) copy as root —
24
+ # the caller keeps appending to the detached original and the
25
+ # tree serializes empty (lutaml-model's builder lost every
26
+ # child this way). Attached means parented, or another
27
+ # document's current root.
28
+ attached = !element.parent.nil? ||
29
+ (owner.is_a?(Document) && owner.root.equal?(element))
30
+ if owner.is_a?(Document) && attached &&
31
+ !owner.equal?(self) && element.context.equal?(context)
32
+ # libleptris refuses a root owned by another document (the
33
+ # engine's set_root ownership check), while Nokogiri adopts.
34
+ # Cross-document CHILD attaches are supported, so build the
35
+ # root here, mirror its declarations and attributes, and
36
+ # splice the foreign subtree child-by-child — each attach
37
+ # rides adapter add_child, which pins the owner document
38
+ # against GC (issue #304).
39
+ new_root = create_element(element.name)
40
+ element.attribute_pairs.each { |k, v| new_root[k] = v }
41
+ element.declared_namespaces.each do |prefix, uri|
42
+ new_root.add_namespace(prefix, uri)
43
+ end
44
+ adapter.set_root(@native, new_root.native)
45
+ new_root.parent_node = self
46
+ element.children.to_a.each { |child| new_root.add_child(child) }
47
+ context.bump_children_generation
48
+ invalidate_children_cache!
49
+ return
50
+ end
19
51
  adapter.set_root(@native, element.native)
20
52
  element.parent_node = self
21
53
  invalidate_children_cache!
data/lib/moxml/element.rb CHANGED
@@ -159,6 +159,40 @@ module Moxml
159
159
  end
160
160
  end
161
161
 
162
+ # Nokogiri-compatible: delete(name) removes the attribute
163
+ def delete(name)
164
+ remove_attribute(name)
165
+ end
166
+
167
+ # Nokogiri-compatible: element-only children. The adapter seam
168
+ # filters at the engine level where the binding carries the face
169
+ # (leptris bulk element children, nokogiri native), so interleaved
170
+ # text/comment nodes never mint wrappers on hot traversals.
171
+ # Memo rides the children generation (#310's mutation clock).
172
+ def elements
173
+ @elements = nil if @elements_gen != context.children_generation
174
+ @elements_gen = context.children_generation
175
+ @elements ||= NodeSet.new(adapter.element_children(@native), context, self)
176
+ end
177
+ alias element_children elements
178
+
179
+ # Nokogiri-compatible: children= replaces the entire child list
180
+ def children=(node_or_nodes)
181
+ natives = case node_or_nodes
182
+ when String
183
+ wrapper = "_moxml_children_#{Process.pid}_#{object_id}"
184
+ context.parse("<#{wrapper}>#{node_or_nodes}</#{wrapper}>")
185
+ .root.children.map(&:native)
186
+ when Moxml::Node then [node_or_nodes.native]
187
+ when Array, NodeSet then node_or_nodes.map(&:native)
188
+ else
189
+ raise ArgumentError, "children= accepts String, Node, or Array"
190
+ end
191
+ adapter.replace_children(@native, natives)
192
+ context.bump_children_generation
193
+ invalidate_children_cache!
194
+ end
195
+
162
196
  def remove_attribute(name)
163
197
  # Both remove paths end in invalidate_attribute_cache!, which
164
198
  # bumps the generation.
@@ -233,6 +267,19 @@ module Moxml
233
267
  invalidate_namespace_cache!
234
268
  end
235
269
 
270
+ # Nokogiri-compatible: bind the element to +uri+ as its DEFAULT
271
+ # namespace — the name drops any prefix and the declaration lands
272
+ # on this element. Children keep their own bindings. Skip when an
273
+ # identical default namespace is already in scope: declaring again
274
+ # serializes a duplicate xmlns attribute. (add_namespace is the
275
+ # primitive: namespace=({nil => uri}) declares twice on leptris.)
276
+ def default_namespace=(uri)
277
+ self.name = name.split(":", 2)[-1] if name.to_s.include?(":")
278
+ return if namespace_prefix.to_s.empty? && namespace_uri.to_s == uri.to_s
279
+
280
+ add_namespace(nil, uri.to_s)
281
+ end
282
+
236
283
  # All namespaces IN SCOPE for this element — its own
237
284
  # declarations plus everything inherited from ancestors —
238
285
  # matching the Nokogiri #namespaces contract consumers port
@@ -296,6 +343,8 @@ module Moxml
296
343
  invalidate_children_cache!
297
344
  end
298
345
 
346
+ alias content= text=
347
+
299
348
  def inner_text
300
349
  text = raw_inner_text
301
350
  entity_bearing? ? adapter.restore_entities(text) : text
data/lib/moxml/node.rb CHANGED
@@ -104,7 +104,104 @@ module Moxml
104
104
  Moxml::Node.wrap(adapter.previous_sibling(@native), context)
105
105
  end
106
106
 
107
+ # Nokogiri-compatible reader spellings.
108
+ alias next next_sibling
109
+ alias previous previous_sibling
110
+
111
+ # Nokogiri-compatible: nearest sibling elements, skipping text and
112
+ # comment nodes.
113
+ def next_element
114
+ s = next_sibling
115
+ s = s.next_sibling while s && !s.is_a?(Moxml::Element)
116
+ s
117
+ end
118
+
119
+ def previous_element
120
+ s = previous_sibling
121
+ s = s.previous_sibling while s && !s.is_a?(Moxml::Element)
122
+ s
123
+ end
124
+
125
+ # Nokogiri-compatible append sugar: node << appends. Strings
126
+ # parse as fragments (markup, not literal text).
127
+ def <<(node)
128
+ add_child(node)
129
+ self
130
+ end
131
+
132
+ # Nokogiri-compatible: reparent — node.parent = new_parent moves
133
+ # this node (removing it from its current tree) under the target.
134
+ def parent=(new_parent)
135
+ remove
136
+ new_parent.add_child(self)
137
+ end
138
+
139
+ # Insert ahead of the current first child (nokogiri-monkeypatch
140
+ # convention used across metanorma). Strings parse as fragments
141
+ # (the sibling_operand convention).
142
+ def add_first_child(node)
143
+ if first_child
144
+ first_child.add_previous_sibling(node)
145
+ else
146
+ add_child(node)
147
+ end
148
+ self
149
+ end
150
+
107
151
  def add_child(node)
152
+ # Nokogiri semantics: a String operand is MARKUP, parsed and
153
+ # appended (cleanup code inserts "<bibliography/>" style
154
+ # strings); the adapter path would mint a literal text node.
155
+ # Metanorma's Nokogiri returns the new children as a NodeSet
156
+ # for markup (misc.add_child("<UnitsML/>").first idiom), self
157
+ # for nodes.
158
+ if node.is_a?(String)
159
+ nodes = context.parse_fragment(node).to_a
160
+ nodes.each { |child| attach_child(child) }
161
+ return NodeSet.new(nodes.map(&:native), context)
162
+ end
163
+ attach_child(node)
164
+ self
165
+ end
166
+
167
+ def add_previous_sibling(node)
168
+ # String operands insert every fragment root; returns the new
169
+ # nodes as a NodeSet (metanorma idiom: sect.add_next_sibling(
170
+ # "<bibliography/>").first).
171
+ if node.is_a?(String)
172
+ nodes = context.parse_fragment(node).to_a
173
+ nodes.each { |n| attach_previous_sibling(n) }
174
+ return NodeSet.new(nodes.map(&:native), context)
175
+ end
176
+ attach_previous_sibling(node)
177
+ self
178
+ end
179
+
180
+ # Nokogiri-compatible assignment forms: node.next = / node.previous =.
181
+ # String operands are parsed as XML fragments, like Nokogiri.
182
+ def next=(node)
183
+ add_next_sibling(node)
184
+ end
185
+
186
+ def previous=(node)
187
+ add_previous_sibling(node)
188
+ end
189
+
190
+ def add_next_sibling(node)
191
+ if node.is_a?(String)
192
+ nodes = context.parse_fragment(node).to_a
193
+ anchor = self
194
+ nodes.each do |n|
195
+ attach_next_sibling_to(anchor, n)
196
+ anchor = n
197
+ end
198
+ return NodeSet.new(nodes.map(&:native), context)
199
+ end
200
+ attach_next_sibling_to(self, node)
201
+ self
202
+ end
203
+
204
+ def attach_child(node)
108
205
  context.bump_children_generation
109
206
  node = prepare_node(node)
110
207
  adapter.add_child(@native, node.native)
@@ -121,7 +218,7 @@ module Moxml
121
218
  self
122
219
  end
123
220
 
124
- def add_previous_sibling(node)
221
+ def attach_previous_sibling(node)
125
222
  context.bump_children_generation
126
223
  node = prepare_node(node)
127
224
  adapter.add_previous_sibling(@native, node.native)
@@ -134,32 +231,26 @@ module Moxml
134
231
  self
135
232
  end
136
233
 
137
- # Nokogiri-compatible assignment forms: node.next = / node.previous =.
138
- # String operands are parsed as XML fragments, like Nokogiri.
139
- def next=(node)
140
- add_next_sibling(sibling_operand(node))
141
- end
142
-
143
- def previous=(node)
144
- add_previous_sibling(sibling_operand(node))
145
- end
146
-
147
- def sibling_operand(node)
148
- return node unless node.is_a?(String)
149
-
150
- context.parse_fragment(node).first
151
- end
152
-
153
- def add_next_sibling(node)
234
+ def attach_next_sibling_to(anchor, node)
154
235
  context.bump_children_generation
155
236
  node = prepare_node(node)
156
- adapter.add_next_sibling(@native, node.native)
157
- parent&.invalidate_children_cache!
158
- invalidate_parent_children_cache!
237
+ adapter.add_next_sibling(anchor.native, node.native)
238
+ anchor.parent&.invalidate_children_cache!
239
+ anchor.invalidate_parent_children_cache!
159
240
  self
160
241
  end
161
242
 
162
243
  def remove
244
+ # Nokogiri parity: unlinking an already-detached node is a
245
+ # no-op returning self. Re-attach idioms like
246
+ # `t << n.replace(x).remove` (standoc term cleanup) detach with
247
+ # #replace and rely on this; leptris would raise "Not found".
248
+ # The wrapper-side @parent_node link decides when present
249
+ # (add_child sets it); the adapter resolution covers
250
+ # link-less wrappers (at()-resolved nodes), whose parents are
251
+ # canonical since #313.
252
+ return self if @parent_node.nil? && parent.nil?
253
+
163
254
  context.bump_children_generation
164
255
  invalidate_parent_children_cache!
165
256
  adapter.remove(@native)
@@ -203,6 +294,18 @@ module Moxml
203
294
  result
204
295
  end
205
296
 
297
+ # Nokogiri-compatible: string interpolation of a node serializes
298
+ # it ("#{node}" in cleanup code), not Object#to_s.
299
+ def to_s
300
+ to_xml
301
+ end
302
+
303
+ # Nokogiri-compatible: to_str yields the text content (their C
304
+ # node_to_str; standoc does x.content = x.to_str on <script>).
305
+ def to_str
306
+ text
307
+ end
308
+
206
309
  # Memoized against the adapter's serialize generation — the
207
310
  # entity-marker flag flips at parse and entity-reference mint,
208
311
  # both adapter-level, and the generation bump is the invalidation
@@ -375,7 +478,15 @@ module Moxml
375
478
  # the document node.
376
479
  #
377
480
  # @return [NodeSet] ancestors ordered nearest-first
378
- def ancestors
481
+ # Nokogiri-compatible: an element-name argument filters the set.
482
+ def ancestors(selector = nil)
483
+ all = _ancestors_all
484
+ return all unless selector
485
+
486
+ all.select { |a| a.name == selector.to_s }
487
+ end
488
+
489
+ def _ancestors_all
379
490
  return NodeSet.new([], context) if document?
380
491
 
381
492
  natives = []
@@ -439,18 +550,9 @@ module Moxml
439
550
  # elements share the same qualified name, keeping paths minimal.
440
551
  #
441
552
  # @return [String] XPath expression
442
- # @raise [Moxml::NotImplementedError] for node types other than
443
- # element and document
444
553
  def path
445
554
  return "/" if document?
446
555
 
447
- unless element?
448
- raise Moxml::NotImplementedError.new(
449
- "path is only supported for element and document nodes",
450
- feature: "path",
451
- )
452
- end
453
-
454
556
  segments = []
455
557
  current = self
456
558
  while current && !current.document?
@@ -460,6 +562,19 @@ module Moxml
460
562
  "/#{segments.join('/')}"
461
563
  end
462
564
 
565
+ # The XPath node-test spelling for this non-element node.
566
+ def path_type_test
567
+ if text? || cdata?
568
+ "text()"
569
+ elsif comment?
570
+ "comment()"
571
+ elsif processing_instruction?
572
+ "processing-instruction()"
573
+ else
574
+ name.to_s
575
+ end
576
+ end
577
+
463
578
  # Returns the 1-based line number where this node appears in the
464
579
  # source XML, or nil when the underlying adapter does not track
465
580
  # source positions.
@@ -564,19 +679,25 @@ module Moxml
564
679
 
565
680
  private
566
681
 
567
- # XPath segment for an element: the qualified name, plus a positional
568
- # predicate only when same-named element siblings make it ambiguous.
569
- def path_segment_for(element)
570
- name = element.name
571
- parent = element.parent
682
+ # XPath segment for a node: elements use their qualified name,
683
+ # other node kinds their XPath type test (text(), comment(), ...).
684
+ # A positional predicate is emitted only when same-kind siblings
685
+ # make the segment ambiguous.
686
+ def path_segment_for(node)
687
+ name = node.element? ? node.name : node.path_type_test
688
+ parent = node.parent
572
689
  return name unless parent
573
690
 
574
- same_name = parent.children.select do |child|
575
- child.element? && child.name == name
691
+ same_kind = parent.children.select do |child|
692
+ if node.element?
693
+ child.element? && child.name == name
694
+ else
695
+ !child.element? && child.path_type_test == name
696
+ end
576
697
  end
577
- return name if same_name.size == 1
698
+ return name if same_kind.size == 1
578
699
 
579
- "#{name}[#{same_name.find_index(element) + 1}]"
700
+ "#{name}[#{same_kind.find_index(node) + 1}]"
580
701
  end
581
702
 
582
703
  def prepare_node(node)
@@ -6,6 +6,15 @@ module Moxml
6
6
 
7
7
  attr_reader :context
8
8
 
9
+ # Nokogiri-compatible set difference: nodes in this set absent
10
+ # from the other, identity-compared on natives.
11
+ def -(other)
12
+ excluded = {}.compare_by_identity
13
+ other.native_nodes.each { |n| excluded[n] = true }
14
+ remaining = native_nodes.reject { |n| excluded.key?(n) }
15
+ self.class.new(remaining, @context, @parent_node)
16
+ end
17
+
9
18
  # nodes: Array of natives, or an adapter's LazyNodeSet — the
10
19
  # native set is held unmaterialized until an operation needs an
11
20
  # Array (#+, #<<, #delete, Range slices).
@@ -76,6 +85,23 @@ module Moxml
76
85
  end
77
86
  alias length size
78
87
 
88
+ # Nokogiri-compatible: serialize the set's members in order.
89
+ def to_xml(options = {})
90
+ map { |n| n.to_xml(options) }.join
91
+ end
92
+
93
+ # Nokogiri-compatible: string interpolation of a NodeSet
94
+ # serializes it ("#{nodes}" in cleanup code), not Object#to_s.
95
+ def to_s
96
+ to_xml
97
+ end
98
+
99
+ # Nokogiri-compatible: Array#flatten and multiple-assignment
100
+ # treat a NodeSet as its wrapped-element array via to_ary.
101
+ def to_ary
102
+ to_a
103
+ end
104
+
79
105
  def to_a
80
106
  i = 0
81
107
  wrapped = wrapped_buffer
data/lib/moxml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.101"
4
+ VERSION = "0.5.102"
5
5
  end
@@ -892,9 +892,21 @@ module Moxml
892
892
  end
893
893
  end
894
894
 
895
- # Union: | (pipe)
896
- def on_pipe(ast, input)
897
- left, right = ast.children
895
+ # Union: | (pipe). Unions are N-ARY — the parser flattens
896
+ # a|b|c into one union node — and may nest (parenthesized
897
+ # groups become union children). Every member's nodes flow
898
+ # through the block when one is given (nested unions), else
899
+ # into one result NodeSet (#315: the old two-children
900
+ # destructure silently dropped every branch after the second).
901
+ def on_pipe(ast, input, &block)
902
+ if block
903
+ code = nil
904
+ ast.children.each do |child|
905
+ fragment = process(child, input, &block)
906
+ code = code ? code.followed_by(fragment) : fragment
907
+ end
908
+ return code
909
+ end
898
910
 
899
911
  union = unique_literal(:union)
900
912
  context_var = context_literal
@@ -903,22 +915,13 @@ module Moxml
903
915
  nodeset_class = const_ref("Moxml", "NodeSet")
904
916
  empty_array = Ruby::Node.new(:array, [])
905
917
 
906
- # Expressions such as "a | b | c"
907
- if left.type == :pipe
908
- union.assign(process(left, input))
909
- .followed_by(process(right, input) { |node| union << node })
910
- .followed_by(union)
911
- # Expressions such as "a | b"
912
- else
913
- nodeset_new = Ruby::Node.new(:send,
914
- [nodeset_class, "new", empty_array,
915
- context_var])
916
-
917
- union.assign(nodeset_new)
918
- .followed_by(process(left, input) { |node| union << node })
919
- .followed_by(process(right, input) { |node| union << node })
920
- .followed_by(union)
918
+ code = union.assign(Ruby::Node.new(:send,
919
+ [nodeset_class, "new", empty_array,
920
+ context_var]))
921
+ ast.children.each do |child|
922
+ code = code.followed_by(process(child, input) { |node| union << node })
921
923
  end
924
+ code.followed_by(union)
922
925
  end
923
926
 
924
927
  # Variable: $variable
@@ -82,4 +82,305 @@ RSpec.describe "Nokogiri-compatible node sugar" do
82
82
  expect(doc.root.children.map(&:name)).to eq(%w[a x text b])
83
83
  end
84
84
  end
85
+
86
+ describe "Node#<< append sugar" do
87
+ it "appends parsed fragments from strings" do
88
+ doc = Moxml.parse("<r/>")
89
+ doc.root << "<a/><b/>"
90
+ expect(doc.root.children.map(&:name)).to eq(%w[a b])
91
+ end
92
+
93
+ it "appends plain text as a text node" do
94
+ text_parent = Moxml.parse("<p/>").root
95
+ text_parent << "hello"
96
+ expect(text_parent.text).to eq("hello")
97
+ end
98
+
99
+ it "appends nodes" do
100
+ doc = Moxml.parse("<r/>")
101
+ doc.root << Moxml.parse("<b/>").root
102
+ expect(doc.root.children.map(&:name)).to eq(%w[b])
103
+ end
104
+
105
+ it "returns self for chaining" do
106
+ doc = Moxml.parse("<r/>")
107
+ expect(doc.root << "<a/>").to be(doc.root)
108
+ end
109
+ end
110
+
111
+ describe "Element#delete" do
112
+ it "removes an attribute by name" do
113
+ doc = Moxml.parse("<r annex='yes' id='x'/>")
114
+ doc.root.delete("annex")
115
+ expect(doc.root["annex"]).to be_nil
116
+ expect(doc.root["id"]).to eq("x")
117
+ end
118
+ end
119
+
120
+ describe "Element#children=" do
121
+ it "replaces all children with a string" do
122
+ doc = Moxml.parse("<r><old/>text</r>")
123
+ doc.root.children = "<a/><b/>"
124
+ expect(doc.root.children.map(&:name)).to eq(%w[a b])
125
+ end
126
+
127
+ it "replaces with a node" do
128
+ doc = Moxml.parse("<r><old/></r>")
129
+ node = Moxml.parse("<x/>").root
130
+ doc.root.children = node
131
+ expect(doc.root.children.map(&:name)).to eq(%w[x])
132
+ end
133
+
134
+ it "replaces with an array of nodes" do
135
+ doc = Moxml.parse("<r><old/></r>")
136
+ frag = Moxml.parse("<w><x/><y/></w>").root.children
137
+ doc.root.children = frag
138
+ expect(doc.root.children.map(&:name)).to eq(%w[x y])
139
+ end
140
+
141
+ it "invalidates memoized children holders" do
142
+ doc = Moxml.parse("<r><old/></r>")
143
+ root = doc.root
144
+ root.children
145
+ root.children = "<new/>"
146
+ expect(root.children.map(&:name)).to eq(%w[new])
147
+ end
148
+ end
149
+
150
+ describe "Element#elements" do
151
+ it "returns only element children" do
152
+ doc = Moxml.parse("<r>text<a/><!-- c --><b/></r>")
153
+ expect(doc.root.elements.map(&:name)).to eq(%w[a b])
154
+ expect(doc.root.element_children.map(&:name)).to eq(%w[a b])
155
+ end
156
+
157
+ it "stays fresh across mutations" do
158
+ doc = Moxml.parse("<r><a/></r>")
159
+ root = doc.root
160
+ expect(root.elements.map(&:name)).to eq(%w[a])
161
+ root.add_child(doc.create_element("b"))
162
+ expect(root.elements.map(&:name)).to eq(%w[a b])
163
+ end
164
+ end
165
+
166
+ describe "NodeSet#- difference" do
167
+ it "removes nodes present in the other set" do
168
+ doc = Moxml.parse("<r><a/><b/><c/></r>")
169
+ diff = doc.root.children - doc.search("//a")
170
+ expect(diff.map(&:name).sort).to eq(%w[b c])
171
+ end
172
+ end
173
+
174
+ describe "NodeSet#to_ary" do
175
+ it "flattens inside arrays" do
176
+ doc = Moxml.parse("<r><a/><b/></r>")
177
+ flat = [doc.root, doc.root.elements].flatten
178
+ expect(flat.map(&:name)).to include("r", "a", "b")
179
+ end
180
+ end
181
+
182
+ describe "Node#ancestors(selector)" do
183
+ it "filters ancestors by element name" do
184
+ doc = Moxml.parse("<r><table><tr><td><p/></td></tr></table></r>")
185
+ p_node = doc.at("//p")
186
+ expect(p_node.ancestors("table").map(&:name)).to eq(%w[table])
187
+ expect(p_node.ancestors.map(&:name)).to include("td", "tr", "table")
188
+ end
189
+ end
190
+
191
+ describe "Node#next_element/previous_element" do
192
+ it "skips interleaved text nodes" do
193
+ doc = Moxml.parse("<r><a/>text<b/></r>")
194
+ a = doc.at("//a")
195
+ expect(a.next_element.name).to eq("b")
196
+ expect(doc.at("//b").previous_element.name).to eq("a")
197
+ end
198
+ end
199
+
200
+ describe "Node#parent= reparenting" do
201
+ it "moves a node under a new parent" do
202
+ doc = Moxml.parse("<r><from><p/></from><to/></r>")
203
+ p_node = doc.at("//p")
204
+ p_node.parent = doc.at("//to")
205
+ expect(doc.at("//to/p")).not_to be_nil
206
+ expect(doc.at("//from/p")).to be_nil
207
+ end
208
+ end
209
+
210
+ describe "NodeSet#to_xml" do
211
+ it "serializes the set's members in order" do
212
+ doc = Moxml.parse("<r><a>x</a>tail</r>")
213
+ expect(doc.root.children.to_xml).to eq("<a>x</a>tail")
214
+ end
215
+ end
216
+
217
+ describe "Node#add_first_child" do
218
+ it "inserts before existing children and handles empty parents" do
219
+ doc = Moxml.parse("<r><b/></r>")
220
+ doc.root.add_first_child("<a/>")
221
+ expect(doc.root.children.map(&:name)).to eq(%w[a b])
222
+ Moxml.parse("<r/>").root.add_first_child("<x/>")
223
+ end
224
+ end
225
+
226
+ describe "Node#remove on a detached node" do
227
+ it "is a no-op returning self (Nokogiri parity)" do
228
+ doc = Moxml.parse("<r><p><c/></p><t/></r>")
229
+ p_node = doc.at("//p")
230
+ t = doc.create_element("d")
231
+ p_node.replace(t)
232
+ expect(p_node.parent).to be_nil
233
+ expect(p_node.remove).to equal(p_node)
234
+ t << p_node
235
+ expect(t.children.map(&:name)).to eq(%w[p])
236
+ expect(doc.at("//d/p/c")).not_to be_nil
237
+ end
238
+ end
239
+
240
+ describe "Element#replace + re-attach under the replacement" do
241
+ it "does not cycle the tree (leptris stale-run splice)" do
242
+ doc = Moxml.parse("<r><a>x</a><b/></r>")
243
+ a = doc.at("//a")
244
+ t = doc.create_element("t")
245
+ a.replace(t)
246
+ t << a
247
+ expect(doc.root.children.map(&:name)).to eq(%w[t b])
248
+ expect(t.children.map(&:name)).to eq(%w[a])
249
+ expect(doc.at("//t/a/text()").text).to eq("x")
250
+ end
251
+ end
252
+
253
+ describe "Node/NodeSet#to_s interpolation" do
254
+ it "serializes instead of Object#to_s" do
255
+ doc = Moxml.parse("<r><a>x</a><b/></r>")
256
+ # rubocop:disable Style/RedundantInterpolation -- the interpolation IS the contract under test
257
+ node = doc.at("//a")
258
+ expect("#{node}").to eq("<a>x</a>")
259
+ children = doc.root.children
260
+ expect("#{children}").to eq("<a>x</a><b></b>")
261
+ # rubocop:enable Style/RedundantInterpolation
262
+ end
263
+ end
264
+
265
+ describe "Element#default_namespace=" do
266
+ it "binds the element to the default namespace" do
267
+ doc = Moxml.parse("<r><math><mi>x</mi></math></r>")
268
+ math = doc.at("//math")
269
+ math.default_namespace = "http://www.w3.org/1998/Math/MathML"
270
+ expect(math.namespace_uri.to_s).to eq("http://www.w3.org/1998/Math/MathML")
271
+ expect(math.to_xml).to include('xmlns="http://www.w3.org/1998/Math/MathML"')
272
+ end
273
+ end
274
+
275
+ describe "Node#to_str" do
276
+ it "yields the text content" do
277
+ doc = Moxml.parse("<r><s>a<b>c</b>d</s></r>")
278
+ expect(doc.at("//s").to_str).to eq("acd")
279
+ expect(doc.at("//b").to_str).to eq("c")
280
+ end
281
+ end
282
+
283
+ describe "Element#content=" do
284
+ it "replaces children with the given text" do
285
+ doc = Moxml.parse("<r><s><b>x</b>y</s></r>")
286
+ s = doc.at("//s")
287
+ s.content = "1 < 2"
288
+ expect(s.children.size).to eq(1)
289
+ expect(s.content).to eq("1 < 2")
290
+ expect(s.to_xml).to eq("<s>1 &lt; 2</s>")
291
+ end
292
+ end
293
+
294
+ describe "Node#next/previous readers" do
295
+ it "return adjacent siblings" do
296
+ doc = Moxml.parse("<r><a/>mid<b/></r>")
297
+ a = doc.at("//a")
298
+ expect(a.next.name).to eq("text")
299
+ expect(a.next.next.name).to eq("b")
300
+ expect(doc.at("//b").previous.name).to eq("text")
301
+ expect(doc.at("//b").previous.previous.name).to eq("a")
302
+ end
303
+ end
304
+
305
+ describe "Node#path for non-element nodes" do
306
+ it "builds type-test segments" do
307
+ doc = Moxml.parse("<r><p>a<!--c--></p><p>b</p></r>")
308
+ expect(doc.at("//p").path).to eq("/r/p[1]")
309
+ texts = doc.xpath("//p/text()")
310
+ expect(texts[0].path).to eq("/r/p[1]/text()")
311
+ expect(doc.xpath("//p/comment()").first.path).to eq("/r/p[1]/comment()")
312
+ end
313
+ end
314
+
315
+ describe "Document#root= with a foreign root" do
316
+ it "adopts the subtree from another document" do
317
+ doc = Moxml.parse("<old><x/></old>")
318
+ other = Moxml.parse(%(<r xmlns="urn:x" a="1"><p>hi<b>t</b></p></r>))
319
+ doc.root = other.root
320
+ expect(doc.to_xml(indent: 0).strip)
321
+ .to eq(%(<r xmlns="urn:x" a="1"><p>hi<b>t</b></p></r>))
322
+ expect(doc.root.namespace_uri.to_s).to eq("urn:x")
323
+ end
324
+ end
325
+
326
+ describe "String operands are markup, not literal text" do
327
+ it "add_child / add_next_sibling / add_previous_sibling parse strings" do
328
+ doc = Moxml.parse("<r><a/></r>")
329
+ a = doc.at("//a")
330
+ a.add_child("<c/>")
331
+ a.add_next_sibling("<d/>")
332
+ a.add_previous_sibling("<b/>")
333
+ expect(a.children.map(&:name)).to eq(%w[c])
334
+ expect(doc.root.children.map(&:name)).to eq(%w[b a d])
335
+ expect(doc.at("//r/a/c")).not_to be_nil
336
+ end
337
+ end
338
+
339
+ describe "Markup-string insertion returns a NodeSet of new nodes" do
340
+ it "add_child / add_next_sibling return the new nodes; nodes return self" do
341
+ doc = Moxml.parse("<r><a/></r>")
342
+ a = doc.at("//a")
343
+ kids = a.add_child("<c1/><c2/>")
344
+ expect(kids).to be_a(Moxml::NodeSet)
345
+ expect(kids.map(&:name)).to eq(%w[c1 c2])
346
+ sibs = a.add_next_sibling("<n1/><n2/>")
347
+ expect(sibs.map(&:name)).to eq(%w[n1 n2])
348
+ expect(doc.root.children.map(&:name)).to eq(%w[a n1 n2])
349
+ prev = a.add_previous_sibling("<p1/><p2/>")
350
+ expect(prev.map(&:name)).to eq(%w[p1 p2])
351
+ expect(doc.root.children.map(&:name)).to eq(%w[p1 p2 a n1 n2])
352
+ expect(a.add_child(doc.at("//p1"))).to equal(a)
353
+ end
354
+ end
355
+
356
+ describe "default_namespace= idempotence" do
357
+ it "does not duplicate an identical in-scope default namespace" do
358
+ doc = Moxml.parse(%(<r xmlns="urn:x"><math xmlns="urn:m"/></r>))
359
+ math = doc.at("//m:math", "m" => "urn:m")
360
+ math.default_namespace = "urn:m"
361
+ expect(math.to_xml).to eq(%(<math xmlns="urn:m"></math>))
362
+ end
363
+ end
364
+
365
+ describe "xpath with namespace bindings" do
366
+ it "prefixed star and name tests honor the namespace URI" do
367
+ # m must be DECLARED on the element — a bare m:title with only
368
+ # a default xmlns is a no-namespace element whose literal name
369
+ # carries the colon, and no prefix binding can select it.
370
+ doc = Moxml.parse(%(<r><bibdata/><m:title xmlns:m="urn:x"/></r>))
371
+ hits = doc.root.xpath(".//m:*", "m" => "urn:unitsml")
372
+ expect(hits.map(&:name)).to eq([])
373
+ named = doc.root.at_xpath(".//m:title", "m" => "urn:x")
374
+ expect(named&.name).to eq("title")
375
+ none = doc.root.at_xpath(".//m:title", "m" => "urn:unitsml")
376
+ expect(none).to be_nil
377
+ end
378
+ end
379
+
380
+ describe "Element#inner_xml" do
381
+ it "serializes the children" do
382
+ doc = Moxml.parse("<r><a>x</a><b/></r>")
383
+ expect(doc.root.inner_xml).to eq("<a>x</a><b/>")
384
+ end
385
+ end
85
386
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # moxml#315: unions are N-ARY in the parser (a|b|c parses to one
6
+ # union node); the compiler's two-children destructure silently
7
+ # dropped every branch after the second. The reserved xmlns prefix
8
+ # hid the drop for namespaced consumers because those queries rode
9
+ # the native seam; when they fell back to the Ruby engine (or on
10
+ # adapters whose engine IS this one, e.g. ox), branches vanished.
11
+ RSpec.describe "XPath union compilation" do
12
+ let(:ctx) { Moxml.new(:leptris) }
13
+ let(:doc) { ctx.parse('<r xmlns="urn:x"><a>1</a><b>2</b><c>3</c></r>') }
14
+
15
+ it "keeps every branch of a flat n-ary union (moxml#315)" do
16
+ r = doc.at_xpath("/xmlns:r", { "xmlns" => "urn:x" })
17
+ names = r.xpath("./xmlns:a|./xmlns:b|./xmlns:c", { "xmlns" => "urn:x" })
18
+ .map(&:text)
19
+ expect(names).to eq(%w[1 2 3])
20
+ end
21
+
22
+ it "keeps branches under the reserved xmlns prefix through the engine" do
23
+ r = doc.at_xpath("/xmlns:r", { "xmlns" => "urn:x" })
24
+ ast = Moxml::XPath::Parser.parse("./xmlns:a|./xmlns:b|./xmlns:c")
25
+ proc_c = Moxml::XPath::Compiler.compile_with_cache(ast, namespaces: { "xmlns" => "urn:x" })
26
+ expect(proc_c.call(r).to_a.map(&:text)).to eq(%w[1 2 3])
27
+ end
28
+
29
+ it "keeps parenthesized nested unions" do
30
+ r = doc.at_xpath("/xmlns:r", { "xmlns" => "urn:x" })
31
+ ast = Moxml::XPath::Parser.parse("(./xmlns:a|./xmlns:b)|./xmlns:c")
32
+ proc_c = Moxml::XPath::Compiler.compile_with_cache(ast, namespaces: { "xmlns" => "urn:x" })
33
+ expect(proc_c.call(r).to_a.map(&:text)).to eq(%w[1 2 3])
34
+ end
35
+
36
+ it "unions still work on adapters whose engine is the Ruby engine" do
37
+ ctx_ox = Moxml.new(:ox)
38
+ doc_ox = ctx_ox.parse("<r><a>1</a><b>2</b><c>3</c></r>")
39
+ r = doc_ox.root
40
+ expect(r.xpath("./a|./b|./c").map(&:text)).to eq(%w[1 2 3])
41
+ end
42
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.101
4
+ version: 0.5.102
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -560,6 +560,7 @@ files:
560
560
  - spec/moxml/xpath/parser_spec.rb
561
561
  - spec/moxml/xpath/ruby/generator_spec.rb
562
562
  - spec/moxml/xpath/ruby/node_spec.rb
563
+ - spec/moxml/xpath/union_spec.rb
563
564
  - spec/moxml/xpath_capabilities_spec.rb
564
565
  - spec/moxml/xpath_spec.rb
565
566
  - spec/moxml/xslt/stylesheet_spec.rb