moxml 0.5.101 → 0.5.103
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 +4 -4
- data/.github/workflows/dependent-tests.yml +7 -0
- data/.github/workflows/opal.yml +7 -0
- data/.github/workflows/rake.yml +7 -0
- data/.github/workflows/round-trip.yml +7 -0
- data/.github/workflows/windows.yml +7 -0
- data/lib/moxml/adapter/base.rb +20 -2
- data/lib/moxml/adapter/leptris.rb +136 -7
- data/lib/moxml/adapter/libxml.rb +3 -2
- data/lib/moxml/adapter/nokogiri.rb +4 -0
- data/lib/moxml/document.rb +32 -0
- data/lib/moxml/element.rb +49 -0
- data/lib/moxml/node.rb +161 -40
- data/lib/moxml/node_set.rb +26 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xpath/compiler.rb +21 -18
- data/spec/moxml/at_search_spec.rb +301 -0
- data/spec/moxml/xpath/union_spec.rb +42 -0
- data/spec/moxml/xslt/stylesheet_spec.rb +19 -6
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 566f2842eac4e01753965cb5f56eada53d6683b3ef6b576015eb4b0da572ae67
|
|
4
|
+
data.tar.gz: ccc15ef083358a018e1536983017c6681c70c11ccbb4678ac123e5b0b2c9aada
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2c29296b60bedabbc512c9f5c454b0195b3632dbf9952b0c397fc3bad6554135dd3096a04a34e1abddf9ce40088af4856b2ead7d8ca69a4eb251dc3db38286b3
|
|
7
|
+
data.tar.gz: fd1684a85d2ace2191b33bf996a12e8bea6cb535a340732cccee3318abc6649ba2e07548509d1469959e10ee711ecec650f8ec6b4525beb6a66e2c9a75a81c1d
|
|
@@ -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
|
data/.github/workflows/opal.yml
CHANGED
|
@@ -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
|
|
data/.github/workflows/rake.yml
CHANGED
|
@@ -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
|
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -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
|
|
51
|
-
|
|
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)
|
|
@@ -503,7 +507,37 @@ module Moxml
|
|
|
503
507
|
raise Moxml::XsltError, "stylesheet compile failed: #{e.message}"
|
|
504
508
|
end
|
|
505
509
|
|
|
510
|
+
# Bindings >= 1.9.290.0 thread top-level xsl:param overrides
|
|
511
|
+
# (leptris#1478); older bindings raise the typed error (the
|
|
512
|
+
# #360 face gap, face-probed so a misrelease degrades instead
|
|
513
|
+
# of NoMethodError-ing — the 1.9.199.0 lesson).
|
|
514
|
+
NATIVE_XSLT_PARAMS =
|
|
515
|
+
NATIVE_READ_LAYER &&
|
|
516
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.290.0") &&
|
|
517
|
+
::Leptris::XML::XSLT::Stylesheet.instance_method(:apply_to)
|
|
518
|
+
.parameters.size > 1
|
|
519
|
+
|
|
520
|
+
# The seam contract passes the libxslt flat quoted array; the
|
|
521
|
+
# binding wants the raw name => value hash (leptris#1478
|
|
522
|
+
# matches nokogiri's quoted-string semantics internally).
|
|
523
|
+
def self.flat_params_to_hash(params)
|
|
524
|
+
return params if params.is_a?(Hash)
|
|
525
|
+
return {} if params.nil? || params.empty?
|
|
526
|
+
|
|
527
|
+
params.each_slice(2).to_h { |name, value| [name, unquote_param(value)] }
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def self.unquote_param(value)
|
|
531
|
+
str = value.to_s
|
|
532
|
+
str.start_with?("'") && str.end_with?("'") && str.length >= 2 ? str[1..-2] : str
|
|
533
|
+
end
|
|
534
|
+
|
|
506
535
|
def self.xslt_apply_document(sheet, document_native, params)
|
|
536
|
+
if NATIVE_XSLT_PARAMS
|
|
537
|
+
return sheet.apply_to(document_native,
|
|
538
|
+
params: flat_params_to_hash(params))
|
|
539
|
+
end
|
|
540
|
+
|
|
507
541
|
assert_xslt_no_params(params)
|
|
508
542
|
sheet.apply_to(document_native)
|
|
509
543
|
rescue ::Leptris::XML::XPathError => e
|
|
@@ -511,6 +545,11 @@ module Moxml
|
|
|
511
545
|
end
|
|
512
546
|
|
|
513
547
|
def self.xslt_apply_string(sheet, document_native, params)
|
|
548
|
+
if NATIVE_XSLT_PARAMS
|
|
549
|
+
return sheet.serialize(document_native,
|
|
550
|
+
params: flat_params_to_hash(params))
|
|
551
|
+
end
|
|
552
|
+
|
|
514
553
|
assert_xslt_no_params(params)
|
|
515
554
|
sheet.serialize(document_native)
|
|
516
555
|
rescue ::Leptris::XML::XPathError => e
|
|
@@ -521,8 +560,9 @@ module Moxml
|
|
|
521
560
|
return if params.nil? || params.empty?
|
|
522
561
|
|
|
523
562
|
raise Moxml::XsltError,
|
|
524
|
-
"XSLT params are not threaded by
|
|
525
|
-
"(leptris-ruby#360);
|
|
563
|
+
"XSLT params are not threaded by this binding " \
|
|
564
|
+
"(leptris-ruby#360); upgrade to leptris >= 1.9.290.0 or " \
|
|
565
|
+
"pass an empty params hash"
|
|
526
566
|
end
|
|
527
567
|
|
|
528
568
|
def self.native_c14n_byte_safe?
|
|
@@ -1397,6 +1437,19 @@ module Moxml
|
|
|
1397
1437
|
end
|
|
1398
1438
|
end
|
|
1399
1439
|
|
|
1440
|
+
# Element-only children through the engine-level filter: the
|
|
1441
|
+
# C face mints element-kind natives directly (klass-children),
|
|
1442
|
+
# so interleaved text/comment nodes cost nothing. Entity
|
|
1443
|
+
# markers are text-kind — the element filter excludes them by
|
|
1444
|
+
# construction, no marker split needed.
|
|
1445
|
+
def element_children(node)
|
|
1446
|
+
if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
|
|
1447
|
+
return NN_ELEMENT_CHILDREN.bind_call(node).to_a
|
|
1448
|
+
end
|
|
1449
|
+
|
|
1450
|
+
to_binding(node).element_children
|
|
1451
|
+
end
|
|
1452
|
+
|
|
1400
1453
|
def children(node, entity_bearing: false)
|
|
1401
1454
|
# NATIVE_READ_LAYER gates the whole keep-path: it consults
|
|
1402
1455
|
# NN_DOCUMENT (defined only with the native layer), and
|
|
@@ -1596,10 +1649,25 @@ module Moxml
|
|
|
1596
1649
|
[copied, pointers]
|
|
1597
1650
|
end
|
|
1598
1651
|
|
|
1599
|
-
# The binding reports the root element
|
|
1600
|
-
# moxml contract roots at the
|
|
1652
|
+
# The binding reports the root element and the document-level
|
|
1653
|
+
# parts as parentless; the moxml contract roots them at the
|
|
1654
|
+
# document. DocType carries no #parent at all (plain
|
|
1655
|
+
# c_ptr+document value object) and a parentless PI is a
|
|
1656
|
+
# document-level one by construction — in-tree parts have
|
|
1657
|
+
# element parents.
|
|
1601
1658
|
def root_parent(node)
|
|
1602
|
-
|
|
1659
|
+
return node.document if node.is_a?(::Leptris::XML::DocType)
|
|
1660
|
+
|
|
1661
|
+
parent = node.parent
|
|
1662
|
+
return parent if parent
|
|
1663
|
+
|
|
1664
|
+
doc = node.document
|
|
1665
|
+
return nil unless doc
|
|
1666
|
+
|
|
1667
|
+
return doc if doc.root == node ||
|
|
1668
|
+
node.is_a?(::Leptris::XML::ProcessingInstruction)
|
|
1669
|
+
|
|
1670
|
+
nil
|
|
1603
1671
|
end
|
|
1604
1672
|
|
|
1605
1673
|
def next_sibling(node)
|
|
@@ -1949,7 +2017,19 @@ module Moxml
|
|
|
1949
2017
|
node = to_binding(node)
|
|
1950
2018
|
new_node = to_binding(new_node)
|
|
1951
2019
|
end
|
|
1952
|
-
|
|
2020
|
+
if node.is_a?(::Leptris::XML::Element)
|
|
2021
|
+
# NOT Element#replace: it detaches via
|
|
2022
|
+
# leptris_element_remove_child, which leaves the replaced
|
|
2023
|
+
# node's sibling links pointing into the live tree. A later
|
|
2024
|
+
# append of that node splices the stale run under the new
|
|
2025
|
+
# parent, and when the run contains the append target the
|
|
2026
|
+
# tree cycles (standoc term cleanup does
|
|
2027
|
+
# t << n.replace(t2)). Insert + unlink instead: the unlink
|
|
2028
|
+
# path clears the sibling links.
|
|
2029
|
+
node.add_next_sibling(new_node)
|
|
2030
|
+
node.unlink
|
|
2031
|
+
return new_node
|
|
2032
|
+
end
|
|
1953
2033
|
|
|
1954
2034
|
# libleptris only offers element-anchored insertion, so a
|
|
1955
2035
|
# content node (text/comment/CDATA/PI) is replaced by
|
|
@@ -1972,11 +2052,23 @@ module Moxml
|
|
|
1972
2052
|
new_node
|
|
1973
2053
|
end
|
|
1974
2054
|
|
|
2055
|
+
NATIVE_APPEND_CHILDREN =
|
|
2056
|
+
NATIVE_READ_LAYER &&
|
|
2057
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.290.0")
|
|
2058
|
+
|
|
1975
2059
|
def replace_children(node, new_children)
|
|
1976
2060
|
if NATIVE_READ_LAYER
|
|
1977
2061
|
node = to_binding(node)
|
|
1978
2062
|
new_children = new_children.map { |child| to_binding(child) }
|
|
1979
2063
|
end
|
|
2064
|
+
if NATIVE_APPEND_CHILDREN && !new_children.empty?
|
|
2065
|
+
# One crossing to clear, one to append N (leptris-ruby#366's
|
|
2066
|
+
# bulk face; the old path paid an append crossing per child).
|
|
2067
|
+
::Leptris::XML::FFI.check_status(
|
|
2068
|
+
::Leptris::XML::FFI.leptris_element_remove_children(node.c_ptr),
|
|
2069
|
+
)
|
|
2070
|
+
return node.append_children(new_children)
|
|
2071
|
+
end
|
|
1980
2072
|
node.children = new_children
|
|
1981
2073
|
end
|
|
1982
2074
|
|
|
@@ -2175,7 +2267,22 @@ module Moxml
|
|
|
2175
2267
|
node = to_binding(node)
|
|
2176
2268
|
end
|
|
2177
2269
|
return nil unless native_context_node?(node)
|
|
2178
|
-
|
|
2270
|
+
# Hash-bound xmlns rides native on probing bindings
|
|
2271
|
+
# (leptris resolves the reserved prefix from the hash);
|
|
2272
|
+
# empty mappings (xmlns="") select no-namespace elements
|
|
2273
|
+
# and stay on the Ruby engine — the C pairs-set rejects
|
|
2274
|
+
# empty URIs. Everything else keeps the expression-keyed
|
|
2275
|
+
# gate.
|
|
2276
|
+
return nil unless (namespaces.is_a?(Hash) &&
|
|
2277
|
+
namespaces["xmlns"].to_s.length.positive? &&
|
|
2278
|
+
xmlns_native?) ||
|
|
2279
|
+
native_expression?(expression)
|
|
2280
|
+
# The leptris engine mishandles namespace-prefixed node
|
|
2281
|
+
# tests: "m:*" matches elements in ANY namespace and
|
|
2282
|
+
# "m:name" matches none, regardless of the binding. Route
|
|
2283
|
+
# namespace-bound queries to the Ruby engine until the
|
|
2284
|
+
# engine grows correct prefix resolution.
|
|
2285
|
+
return nil if namespaces && !namespaces.empty?
|
|
2179
2286
|
# The binding reports the root element parentless while moxml
|
|
2180
2287
|
# roots it at the document, so parent-axis queries from the
|
|
2181
2288
|
# root element keep the Ruby engine.
|
|
@@ -2311,6 +2418,28 @@ module Moxml
|
|
|
2311
2418
|
# xmlns:name is a nokogiri-compat convention addressing
|
|
2312
2419
|
# elements in the default namespace; only the Ruby engine
|
|
2313
2420
|
# implements it.
|
|
2421
|
+
# One-time capability probe: does the binding's native engine
|
|
2422
|
+
# resolve the reserved xmlns prefix from the namespace hash?
|
|
2423
|
+
# Memoized class-level — the answer never changes for an
|
|
2424
|
+
# install. Only hash-bound xmlns queries may ride native
|
|
2425
|
+
# (the gate below is expression-keyed and cannot see the
|
|
2426
|
+
# call's namespaces); unbound xmlns stays on the Ruby engine,
|
|
2427
|
+
# whose reserved-prefix branch matches default-namespace
|
|
2428
|
+
# elements.
|
|
2429
|
+
def xmlns_native?
|
|
2430
|
+
return @xmlns_native unless @xmlns_native.nil?
|
|
2431
|
+
|
|
2432
|
+
@xmlns_native = begin
|
|
2433
|
+
probe = parse('<r xmlns="urn:p"><a>1</a></r>', {}, nil)
|
|
2434
|
+
raw = to_binding(probe.root.native)
|
|
2435
|
+
res = raw.xpath("./xmlns:a", { "xmlns" => "urn:p" })
|
|
2436
|
+
list = res.respond_to?(:to_a) ? res.to_a : [res].compact
|
|
2437
|
+
list.size == 1 && list.first.text.to_s == "1"
|
|
2438
|
+
rescue StandardError
|
|
2439
|
+
false
|
|
2440
|
+
end
|
|
2441
|
+
end
|
|
2442
|
+
|
|
2314
2443
|
def uses_xmlns_prefix?(ast)
|
|
2315
2444
|
return true if ast.type == :test && ast.value[:namespace] == "xmlns"
|
|
2316
2445
|
|
data/lib/moxml/adapter/libxml.rb
CHANGED
|
@@ -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
|
-
#
|
|
1103
|
-
|
|
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)
|
data/lib/moxml/document.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
568
|
-
#
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
575
|
-
|
|
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
|
|
698
|
+
return name if same_kind.size == 1
|
|
578
699
|
|
|
579
|
-
"#{name}[#{
|
|
700
|
+
"#{name}[#{same_kind.find_index(node) + 1}]"
|
|
580
701
|
end
|
|
581
702
|
|
|
582
703
|
def prepare_node(node)
|
data/lib/moxml/node_set.rb
CHANGED
|
@@ -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
data/lib/moxml/xpath/compiler.rb
CHANGED
|
@@ -892,9 +892,21 @@ module Moxml
|
|
|
892
892
|
end
|
|
893
893
|
end
|
|
894
894
|
|
|
895
|
-
# Union: | (pipe)
|
|
896
|
-
|
|
897
|
-
|
|
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
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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 < 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
|
|
@@ -4,8 +4,9 @@ require "spec_helper"
|
|
|
4
4
|
|
|
5
5
|
# The Moxml::XSLT contract: compile once, apply many. Only engines
|
|
6
6
|
# with an XSLT processor implement it (nokogiri/libxslt; leptris
|
|
7
|
-
# covering XSLT 1.0-3.0).
|
|
8
|
-
#
|
|
7
|
+
# covering XSLT 1.0-3.0). Bindings >= 1.9.290.0 thread top-level
|
|
8
|
+
# params (leptris#1478); older bindings raise Moxml::XsltError
|
|
9
|
+
# citing leptris-ruby#360.
|
|
9
10
|
SHEET_COUNT = <<~XSL
|
|
10
11
|
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
|
|
11
12
|
<xsl:template match="/"><out><xsl:value-of select="count(//i)"/></out></xsl:template>
|
|
@@ -77,10 +78,22 @@ RSpec.describe "Moxml::XSLT" do
|
|
|
77
78
|
expect(sheet.apply_to_string(doc)).to include("plain text result")
|
|
78
79
|
end
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
if Gem::Version.new(Leptris::VERSION) >= Gem::Version.new("1.9.290.0")
|
|
82
|
+
it "threads top-level params (leptris#1478)" do
|
|
83
|
+
sheet = ctx.xslt(<<~XSL)
|
|
84
|
+
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
|
|
85
|
+
<xsl:param name="n" select="'0'"/>
|
|
86
|
+
<xsl:template match="/"><out><xsl:value-of select="$n"/></out></xsl:template>
|
|
87
|
+
</xsl:stylesheet>
|
|
88
|
+
XSL
|
|
89
|
+
expect(sheet.apply_to_string(doc, params: { "n" => "7" })).to include("7")
|
|
90
|
+
end
|
|
91
|
+
else
|
|
92
|
+
it "raises XsltError on non-empty params (leptris-ruby#360)" do
|
|
93
|
+
sheet = ctx.xslt(SHEET_COUNT)
|
|
94
|
+
expect { sheet.apply_to(doc, params: { "n" => "7" }) }
|
|
95
|
+
.to raise_error(Moxml::XsltError, /params.*#360/)
|
|
96
|
+
end
|
|
84
97
|
end
|
|
85
98
|
|
|
86
99
|
it "reports support" do
|
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.
|
|
4
|
+
version: 0.5.103
|
|
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
|