moxml 0.1.25 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +9 -0
- data/.github/workflows/windows.yml +62 -0
- data/Gemfile +3 -1
- data/README.adoc +133 -6
- data/docs/signature/algorithms.md +108 -0
- data/docs/signature/architecture.md +148 -0
- data/docs/signature/c14n.md +110 -0
- data/docs/signature/examples.md +109 -0
- data/docs/signature/flows.md +132 -0
- data/docs/signature/quick-reference.md +115 -0
- data/docs/signature/security.md +143 -0
- data/examples/signature/auto_key_extraction.rb +24 -0
- data/examples/signature/enveloped_rsa.rb +45 -0
- data/examples/signature/hmac.rb +33 -0
- data/lib/compat/opal/moxml_boot.rb +5 -0
- data/lib/moxml/adapter/base.rb +53 -49
- data/lib/moxml/adapter/customized_leptris/declaration.rb +32 -0
- data/lib/moxml/adapter/customized_leptris/doctype.rb +31 -0
- data/lib/moxml/adapter/customized_leptris/entity_reference.rb +12 -0
- data/lib/moxml/adapter/customized_leptris/text_segment.rb +26 -0
- data/lib/moxml/adapter/customized_leptris.rb +16 -0
- data/lib/moxml/adapter/customized_libxml/cdata.rb +3 -12
- data/lib/moxml/adapter/customized_libxml/comment.rb +2 -9
- data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -10
- data/lib/moxml/adapter/customized_libxml/node.rb +8 -0
- data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +2 -10
- data/lib/moxml/adapter/customized_libxml/text.rb +2 -9
- data/lib/moxml/adapter/customized_oga/entity_decoder.rb +37 -0
- data/lib/moxml/adapter/customized_oga/raw_value_override.rb +52 -0
- data/lib/moxml/adapter/customized_oga/xml_generator.rb +4 -34
- data/lib/moxml/adapter/customized_oga.rb +2 -0
- data/lib/moxml/adapter/customized_ox/entity_reference.rb +1 -17
- data/lib/moxml/adapter/customized_rexml/entity_reference.rb +1 -11
- data/lib/moxml/adapter/headed_ox.rb +9 -128
- data/lib/moxml/adapter/leptris.rb +965 -0
- data/lib/moxml/adapter/libxml.rb +131 -99
- data/lib/moxml/adapter/nokogiri.rb +29 -8
- data/lib/moxml/adapter/oga.rb +91 -54
- data/lib/moxml/adapter/ox.rb +93 -137
- data/lib/moxml/adapter/rexml.rb +61 -50
- data/lib/moxml/adapter.rb +2 -1
- data/lib/moxml/attribute.rb +8 -4
- data/lib/moxml/attribute_resolver.rb +189 -0
- data/lib/moxml/c14n/attribute_handler.rb +71 -0
- data/lib/moxml/c14n/character_encoder.rb +29 -0
- data/lib/moxml/c14n/data_model.rb +145 -0
- data/lib/moxml/c14n/exclusive.rb +188 -0
- data/lib/moxml/c14n/inclusive_10.rb +42 -0
- data/lib/moxml/c14n/inclusive_11.rb +21 -0
- data/lib/moxml/c14n/namespace_context.rb +68 -0
- data/lib/moxml/c14n/namespace_handler.rb +71 -0
- data/lib/moxml/c14n/node.rb +42 -0
- data/lib/moxml/c14n/nodes/attribute_node.rb +50 -0
- data/lib/moxml/c14n/nodes/comment_node.rb +29 -0
- data/lib/moxml/c14n/nodes/element_node.rb +62 -0
- data/lib/moxml/c14n/nodes/namespace_node.rb +41 -0
- data/lib/moxml/c14n/nodes/processing_instruction_node.rb +30 -0
- data/lib/moxml/c14n/nodes/root_node.rb +22 -0
- data/lib/moxml/c14n/nodes/text_node.rb +29 -0
- data/lib/moxml/c14n/nodes.rb +17 -0
- data/lib/moxml/c14n/processor.rb +118 -0
- data/lib/moxml/c14n/writer.rb +121 -0
- data/lib/moxml/c14n/xml_base_handler.rb +140 -0
- data/lib/moxml/c14n.rb +96 -0
- data/lib/moxml/config.rb +29 -3
- data/lib/moxml/context.rb +12 -0
- data/lib/moxml/element.rb +29 -12
- data/lib/moxml/entity/reference.rb +28 -0
- data/lib/moxml/entity.rb +125 -0
- data/lib/moxml/node.rb +90 -1
- data/lib/moxml/sax/namespace_splitter.rb +5 -2
- data/lib/moxml/signature/algorithms/base64_transform.rb +40 -0
- data/lib/moxml/signature/algorithms/canonicalization_base.rb +87 -0
- data/lib/moxml/signature/algorithms/digest_base.rb +51 -0
- data/lib/moxml/signature/algorithms/dsa_sha.rb +107 -0
- data/lib/moxml/signature/algorithms/ecdsa_sha.rb +125 -0
- data/lib/moxml/signature/algorithms/enveloped_signature_transform.rb +79 -0
- data/lib/moxml/signature/algorithms/exc_c14n_10.rb +23 -0
- data/lib/moxml/signature/algorithms/hmac_sha.rb +117 -0
- data/lib/moxml/signature/algorithms/inclusive_c14n_10.rb +21 -0
- data/lib/moxml/signature/algorithms/inclusive_c14n_11.rb +23 -0
- data/lib/moxml/signature/algorithms/rsa_pkcs1_sha.rb +56 -0
- data/lib/moxml/signature/algorithms/sha1.rb +13 -0
- data/lib/moxml/signature/algorithms/sha224.rb +13 -0
- data/lib/moxml/signature/algorithms/sha256.rb +13 -0
- data/lib/moxml/signature/algorithms/sha384.rb +13 -0
- data/lib/moxml/signature/algorithms/sha512.rb +13 -0
- data/lib/moxml/signature/algorithms/signature_method_base.rb +58 -0
- data/lib/moxml/signature/algorithms/transform_base.rb +40 -0
- data/lib/moxml/signature/algorithms.rb +113 -0
- data/lib/moxml/signature/errors.rb +99 -0
- data/lib/moxml/signature/key_extractor.rb +172 -0
- data/lib/moxml/signature/model/algorithm_method.rb +18 -0
- data/lib/moxml/signature/model/digest_method.rb +15 -0
- data/lib/moxml/signature/model/key/dsa_key_value.rb +28 -0
- data/lib/moxml/signature/model/key/ec_key_value.rb +22 -0
- data/lib/moxml/signature/model/key/rsa_key_value.rb +19 -0
- data/lib/moxml/signature/model/key/x509_data.rb +32 -0
- data/lib/moxml/signature/model/key/x509_digest.rb +19 -0
- data/lib/moxml/signature/model/key/x509_issuer_serial.rb +19 -0
- data/lib/moxml/signature/model/key_info.rb +21 -0
- data/lib/moxml/signature/model/key_value.rb +20 -0
- data/lib/moxml/signature/model/object_element.rb +18 -0
- data/lib/moxml/signature/model/reference.rb +22 -0
- data/lib/moxml/signature/model/signature.rb +21 -0
- data/lib/moxml/signature/model/signature_value.rb +16 -0
- data/lib/moxml/signature/model/signed_info.rb +20 -0
- data/lib/moxml/signature/model/transform.rb +16 -0
- data/lib/moxml/signature/model/transforms.rb +36 -0
- data/lib/moxml/signature/model.rb +35 -0
- data/lib/moxml/signature/parser.rb +270 -0
- data/lib/moxml/signature/reference_resolver.rb +79 -0
- data/lib/moxml/signature/reference_result.rb +22 -0
- data/lib/moxml/signature/serializer.rb +163 -0
- data/lib/moxml/signature/signer.rb +73 -0
- data/lib/moxml/signature/single_verification_result.rb +31 -0
- data/lib/moxml/signature/transform_pipeline.rb +106 -0
- data/lib/moxml/signature/verification_result.rb +27 -0
- data/lib/moxml/signature/verifier.rb +129 -0
- data/lib/moxml/signature.rb +94 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_emitter.rb +71 -0
- data/lib/moxml/xpath/compiler.rb +60 -7
- data/lib/moxml/xpath/parser.rb +22 -15
- data/lib/moxml.rb +5 -0
- data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
- data/reference-docs/w3c-xmldsig-core.md +400 -0
- data/spec/examples/readme_examples_spec.rb +0 -4
- data/spec/examples/xpath_examples_spec.rb +0 -11
- data/spec/fixtures/xmldsig/keys/rsa_private.pem +28 -0
- data/spec/fixtures/xmldsig/keys/rsa_public.pem +9 -0
- data/spec/fixtures/xmldsig/keys/rsa_ref.pem +15 -0
- data/spec/fixtures/xmldsig/keys/rsa_ref.pub +6 -0
- data/spec/fixtures/xmldsig/sign2-doc.xml +6 -0
- data/spec/fixtures/xmldsig/sign2-result.xml +25 -0
- data/spec/fixtures/xmldsig/sign3-result.xml +39 -0
- data/spec/integration/all_adapters_spec.rb +17 -0
- data/spec/integration/sax_parity_spec.rb +58 -0
- data/spec/integration/shared_examples/edge_cases.rb +0 -10
- data/spec/integration/shared_examples/integration_workflows.rb +0 -10
- data/spec/integration/shared_examples/node_wrappers/attribute_behavior.rb +98 -0
- data/spec/integration/shared_examples/node_wrappers/namespace_behavior.rb +26 -0
- data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +0 -8
- data/spec/moxml/adapter/leptris_spec.rb +75 -0
- data/spec/moxml/adapter/ox_spec.rb +20 -6
- data/spec/moxml/adapter/platform_spec.rb +15 -2
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +180 -0
- data/spec/moxml/attribute_resolver_spec.rb +108 -0
- data/spec/moxml/c14n/api_spec.rb +117 -0
- data/spec/moxml/c14n/c14n_spec.rb +88 -0
- data/spec/moxml/c14n/comments_pis_spec.rb +65 -0
- data/spec/moxml/c14n/inclusive10_spec.rb +60 -0
- data/spec/moxml/c14n/namespace_edge_cases_spec.rb +88 -0
- data/spec/moxml/c14n/xml_attributes_spec.rb +54 -0
- data/spec/moxml/doctype_spec.rb +1 -1
- data/spec/moxml/entity_spec.rb +76 -0
- data/spec/moxml/node_spec.rb +104 -0
- data/spec/moxml/sax_entity_parity_spec.rb +358 -0
- data/spec/moxml/signature/algorithms/base64_transform_spec.rb +26 -0
- data/spec/moxml/signature/algorithms/digest_base_spec.rb +72 -0
- data/spec/moxml/signature/algorithms/dsa_sha_spec.rb +46 -0
- data/spec/moxml/signature/algorithms/ecdsa_sha_spec.rb +72 -0
- data/spec/moxml/signature/algorithms/enveloped_signature_transform_spec.rb +40 -0
- data/spec/moxml/signature/algorithms/hmac_sha_spec.rb +71 -0
- data/spec/moxml/signature/algorithms/rsa_pkcs1_sha_spec.rb +55 -0
- data/spec/moxml/signature/algorithms_spec.rb +76 -0
- data/spec/moxml/signature/cross_verify_spec.rb +64 -0
- data/spec/moxml/signature/edge_cases_spec.rb +284 -0
- data/spec/moxml/signature/fixtures_spec.rb +80 -0
- data/spec/moxml/signature/key_extractor_spec.rb +96 -0
- data/spec/moxml/signature/model_spec.rb +54 -0
- data/spec/moxml/signature/round_trip_spec.rb +113 -0
- data/spec/moxml/xml_emitter_spec.rb +64 -0
- data/spec/moxml/xpath/axes_spec.rb +72 -0
- data/spec/moxml/xpath/parser_spec.rb +6 -0
- data/spec/moxml/xpath_capabilities_spec.rb +5 -3
- data/spec/performance/benchmark_spec.rb +13 -6
- data/spec/performance/thread_safety_spec.rb +0 -4
- data/spec/spec_helper.rb +5 -1
- metadata +130 -2
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/c14n"
|
|
5
|
+
|
|
6
|
+
# Edge cases for namespace rendering per W3C C14N 1.0 §2.3 / 1.1 §2.3.
|
|
7
|
+
# These cover the bugs that historically plague C14N implementations.
|
|
8
|
+
RSpec.describe "C14N namespace rendering" do
|
|
9
|
+
let(:ctx) { Moxml.new(:nokogiri) }
|
|
10
|
+
|
|
11
|
+
describe "default namespace" do
|
|
12
|
+
it "renders xmlns= declaration when default ns is set" do
|
|
13
|
+
doc = ctx.parse(%(<root xmlns="urn:foo"/>))
|
|
14
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
15
|
+
expect(result).to eq('<root xmlns="urn:foo"></root>')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "renders xmlns='' when transitioning to empty default" do
|
|
19
|
+
doc = ctx.parse(%(<a xmlns="urn:foo"><b xmlns=""><c/></b></a>))
|
|
20
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
21
|
+
# <b> has empty default; <a> has urn:foo → xmlns="" appears
|
|
22
|
+
expect(result).to include('xmlns=""')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "does not render xmlns when default ns is empty everywhere" do
|
|
26
|
+
doc = ctx.parse(%(<root><child/></root>))
|
|
27
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
28
|
+
expect(result).to eq("<root><child></child></root>")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe "prefix redeclaration" do
|
|
33
|
+
it "renders both declarations when URI changes" do
|
|
34
|
+
doc = ctx.parse(%(<a xmlns:x="urn:1"><b xmlns:x="urn:2"/></a>))
|
|
35
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
36
|
+
expect(result.scan("xmlns:x=").length).to eq(2)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "renders only once when URI is unchanged" do
|
|
40
|
+
doc = ctx.parse(%(<a xmlns:x="urn:1"><b xmlns:x="urn:1"/></a>))
|
|
41
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
42
|
+
expect(result.scan("xmlns:x=").length).to eq(1)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe "xml namespace" do
|
|
47
|
+
it "does not render xmlns:xml declaration" do
|
|
48
|
+
doc = ctx.parse(%(<root xmlns:xml="http://www.w3.org/XML/1998/namespace"/>))
|
|
49
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
50
|
+
expect(result).not_to include("xmlns:xml=")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe "namespace sorting" do
|
|
55
|
+
it "sorts xmlns declarations by prefix" do
|
|
56
|
+
doc = ctx.parse(%(<root xmlns:z="urn:z" xmlns:a="urn:a"/>))
|
|
57
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
58
|
+
# a should appear before z
|
|
59
|
+
expect(result.index("xmlns:a=")).to be < result.index("xmlns:z=")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "renders default namespace first" do
|
|
63
|
+
doc = ctx.parse(%(<root xmlns="urn:default" xmlns:a="urn:a"/>))
|
|
64
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
65
|
+
# Default (xmlns=) before prefixed
|
|
66
|
+
expect(result.index("xmlns=")).to be < result.index("xmlns:a=")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe "deeply nested inheritance" do
|
|
71
|
+
it "renders each prefix at the level it is first declared" do
|
|
72
|
+
xml = <<~XML.strip
|
|
73
|
+
<root xmlns:a="urn:a">
|
|
74
|
+
<level1 xmlns:b="urn:b">
|
|
75
|
+
<level2 xmlns:c="urn:c">
|
|
76
|
+
<apex/>
|
|
77
|
+
</level2>
|
|
78
|
+
</level1>
|
|
79
|
+
</root>
|
|
80
|
+
XML
|
|
81
|
+
doc = ctx.parse(xml)
|
|
82
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
83
|
+
# Inclusive: all in-scope ns render on apex. But we're canonicalizing
|
|
84
|
+
# the whole tree, so each ns renders at its declaration point.
|
|
85
|
+
expect(result.scan("xmlns:").length).to eq(3)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/c14n"
|
|
5
|
+
|
|
6
|
+
# xml:lang and xml:space are "simple inheritable" attributes per W3C C14N
|
|
7
|
+
# 1.1 §2.4. When an element is in the node-set but an ancestor is not,
|
|
8
|
+
# the canonical form inherits these attributes from the nearest ancestor
|
|
9
|
+
# in which they were declared.
|
|
10
|
+
#
|
|
11
|
+
# These specs exercise the inclusive behavior at full-document
|
|
12
|
+
# canonicalization (where everything is in the set, so inheritance
|
|
13
|
+
# doesn't trigger). For subset scenarios, see TODO.c14n/07.
|
|
14
|
+
RSpec.describe "C14N xml:* inheritable attributes" do
|
|
15
|
+
let(:ctx) { Moxml.new(:nokogiri) }
|
|
16
|
+
|
|
17
|
+
it "renders xml:lang attribute" do
|
|
18
|
+
doc = ctx.parse(%(<root xml:lang="en">hello</root>))
|
|
19
|
+
expect(Moxml::C14n.canonicalize(doc.root))
|
|
20
|
+
.to eq(%(<root xml:lang="en">hello</root>))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "renders xml:space attribute" do
|
|
24
|
+
doc = ctx.parse(%(<root xml:space="preserve">data</root>))
|
|
25
|
+
expect(Moxml::C14n.canonicalize(doc.root))
|
|
26
|
+
.to eq(%(<root xml:space="preserve">data</root>))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "renders xml:lang on descendant that declares its own" do
|
|
30
|
+
doc = ctx.parse(%(<a xml:lang="en"><b xml:lang="fr"/></a>))
|
|
31
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
32
|
+
expect(result).to include('<b xml:lang="fr">')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "renders xml:id attribute" do
|
|
36
|
+
doc = ctx.parse(%(<root xml:id="foo"/>))
|
|
37
|
+
expect(Moxml::C14n.canonicalize(doc.root))
|
|
38
|
+
.to eq(%(<root xml:id="foo"></root>))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "renders xml:base attribute" do
|
|
42
|
+
doc = ctx.parse(%(<root xml:base="http://example.com/"/>))
|
|
43
|
+
expect(Moxml::C14n.canonicalize(doc.root))
|
|
44
|
+
.to eq(%(<root xml:base="http://example.com/"></root>))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "sorts xml:* attributes by namespace URI then local name" do
|
|
48
|
+
doc = ctx.parse(%(<root xml:space="default" xml:lang="en" xml:id="x"/>))
|
|
49
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
50
|
+
# All in XML namespace; sort by local name: id < lang < space
|
|
51
|
+
expect(result.index("xml:id=")).to be < result.index("xml:lang=")
|
|
52
|
+
expect(result.index("xml:lang=")).to be < result.index("xml:space=")
|
|
53
|
+
end
|
|
54
|
+
end
|
data/spec/moxml/doctype_spec.rb
CHANGED
|
@@ -48,7 +48,7 @@ RSpec.describe Moxml::Doctype do
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
describe "parsing" do
|
|
51
|
-
%i[nokogiri oga rexml ox].each do |adapter_name|
|
|
51
|
+
%i[nokogiri oga rexml ox libxml].each do |adapter_name|
|
|
52
52
|
context "with #{adapter_name} adapter" do
|
|
53
53
|
let(:ctx) { Moxml.new(adapter_name) }
|
|
54
54
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Moxml::Entity do
|
|
6
|
+
describe ".preprocess_entities" do
|
|
7
|
+
it "marks non-standard entities and leaves the five standard ones" do
|
|
8
|
+
marked = described_class.preprocess_entities("a © b & c <")
|
|
9
|
+
expect(marked).to include("#{described_class::MARKER}copy;")
|
|
10
|
+
expect(marked).to include("&")
|
|
11
|
+
expect(marked).to include("<")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "returns the input untouched when it contains no ampersand" do
|
|
15
|
+
input = "plain text"
|
|
16
|
+
expect(described_class.preprocess_entities(input)).to equal(input)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "always returns UTF-8" do
|
|
20
|
+
binary = "caf\xC3\xA9 ©".b
|
|
21
|
+
result = described_class.preprocess_entities(binary)
|
|
22
|
+
expect(result.encoding).to eq(Encoding::UTF_8)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "returns an empty string for nil" do
|
|
26
|
+
expect(described_class.preprocess_entities(nil)).to eq("")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe ".decode_entities" do
|
|
31
|
+
it "decodes numeric and standard named references in one pass" do
|
|
32
|
+
expect(described_class.decode_entities("AB&<"))
|
|
33
|
+
.to eq("AB&<")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "preserves invalid numeric codepoints verbatim" do
|
|
37
|
+
expect(described_class.decode_entities("��")).to eq("��")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "skips strings without ampersands" do
|
|
41
|
+
input = "plain"
|
|
42
|
+
expect(described_class.decode_entities(input)).to equal(input)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe ".restore_entities" do
|
|
47
|
+
it "maps markers back to named references" do
|
|
48
|
+
restored = described_class.restore_entities(
|
|
49
|
+
"a#{described_class::MARKER}copy;b",
|
|
50
|
+
)
|
|
51
|
+
expect(restored).to eq("a©b")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "restores markers a native serializer rendered as character references" do
|
|
55
|
+
expect(described_class.restore_entities("acopy;b"))
|
|
56
|
+
.to eq("a©b")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "returns marker-free strings without a regex pass" do
|
|
60
|
+
input = "no markers here"
|
|
61
|
+
expect(described_class.restore_entities(input)).to equal(input)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe Moxml::Entity::Reference do
|
|
66
|
+
it "round-trips as a serialized entity reference" do
|
|
67
|
+
expect(described_class.new("copy").to_xml).to eq("©")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "compares by name" do
|
|
71
|
+
reference = described_class.new("copy")
|
|
72
|
+
expect(reference).to eq(described_class.new("copy"))
|
|
73
|
+
expect(reference).not_to eq(described_class.new("nbsp"))
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/spec/moxml/node_spec.rb
CHANGED
|
@@ -92,4 +92,108 @@ RSpec.describe Moxml::Node do
|
|
|
92
92
|
expect(child.content).to eq("text")
|
|
93
93
|
end
|
|
94
94
|
end
|
|
95
|
+
|
|
96
|
+
describe "#ancestors" do
|
|
97
|
+
let(:doc) { context.parse("<library><section><book><title>Book 1</title></book></section></library>") }
|
|
98
|
+
let(:title) { doc.at_xpath("//title") }
|
|
99
|
+
|
|
100
|
+
it "returns ancestors from parent up to and including the document" do
|
|
101
|
+
ancestors = title.ancestors
|
|
102
|
+
expect(ancestors).to be_a(Moxml::NodeSet)
|
|
103
|
+
expect(ancestors.select(&:element?).map(&:name)).to eq(["book", "section", "library"])
|
|
104
|
+
expect(ancestors[3]).to be_a(Moxml::Document)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "returns empty for the document" do
|
|
108
|
+
expect(doc.ancestors).to be_empty
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "returns the document for the root element" do
|
|
112
|
+
ancestors = doc.root.ancestors
|
|
113
|
+
expect(ancestors.size).to eq(1)
|
|
114
|
+
expect(ancestors.first).to be_a(Moxml::Document)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe "#descendants" do
|
|
119
|
+
let(:doc) { context.parse("<section><book><title>Book 1</title></book></section>") }
|
|
120
|
+
let(:section) { doc.at_xpath("//section") }
|
|
121
|
+
|
|
122
|
+
it "returns all descendants excluding self" do
|
|
123
|
+
descendants = section.descendants
|
|
124
|
+
expect(descendants).to be_a(Moxml::NodeSet)
|
|
125
|
+
element_names = descendants.select(&:element?).map(&:name)
|
|
126
|
+
expect(element_names).to eq(["book", "title"])
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "includes text nodes" do
|
|
130
|
+
expect(section.descendants.any?(&:text?)).to be true
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "returns no elements for a leaf element" do
|
|
134
|
+
expect(doc.at_xpath("//title").descendants.select(&:element?)).to be_empty
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
describe "#path" do
|
|
139
|
+
%i[nokogiri oga rexml ox libxml].each do |adapter_name|
|
|
140
|
+
context "with #{adapter_name} adapter" do
|
|
141
|
+
let(:ctx) { Moxml.new(adapter_name) }
|
|
142
|
+
let(:doc) do
|
|
143
|
+
ctx.parse("<library><section><book id='1'/><book id='2'><title>Book 2</title></book></section></library>")
|
|
144
|
+
end
|
|
145
|
+
let(:first_book) { doc.root.children.find(&:element?).children.find { |c| c.element? && c.name == "book" && c["id"] == "1" } }
|
|
146
|
+
let(:second_book) { doc.root.children.find(&:element?).children.find { |c| c.element? && c.name == "book" && c["id"] == "2" } }
|
|
147
|
+
let(:title) { second_book.children.find(&:element?) }
|
|
148
|
+
|
|
149
|
+
it "builds the path without a predicate for uniquely named elements" do
|
|
150
|
+
expect(title.path).to eq("/library/section/book[2]/title")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "adds a positional predicate for same-named siblings" do
|
|
154
|
+
expect(first_book.path).to eq("/library/section/book[1]")
|
|
155
|
+
expect(second_book.path).to eq("/library/section/book[2]")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "locates the node via its path" do
|
|
159
|
+
# Ox translates only a limited XPath subset (no positional predicates)
|
|
160
|
+
skip "ox xpath translation does not support positional predicates" if adapter_name == :ox
|
|
161
|
+
|
|
162
|
+
expect(doc.at_xpath(title.path)).to eq(title)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "returns / for the document" do
|
|
166
|
+
expect(doc.path).to eq("/")
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it "raises for node types without a path representation" do
|
|
172
|
+
text = doc.root.children.first.children.first
|
|
173
|
+
expect { text.path }.to raise_error(Moxml::NotImplementedError, /element and document/)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
describe "#line_number" do
|
|
178
|
+
%i[nokogiri oga rexml ox libxml].each do |adapter_name|
|
|
179
|
+
tracks_source_lines = adapter_name.eql?(:nokogiri) || adapter_name.eql?(:libxml)
|
|
180
|
+
context "with #{adapter_name} adapter" do
|
|
181
|
+
let(:ctx) { Moxml.new(adapter_name) }
|
|
182
|
+
let(:doc) do
|
|
183
|
+
ctx.parse("<?xml version='1.0'?>\n<library>\n <book>\n <title>Book 1</title>\n </book>\n</library>\n")
|
|
184
|
+
end
|
|
185
|
+
let(:title) { doc.root.children.find(&:element?).children.find(&:element?) }
|
|
186
|
+
|
|
187
|
+
if tracks_source_lines
|
|
188
|
+
it "returns the 1-based source line" do
|
|
189
|
+
expect(title.line_number).to eq(4)
|
|
190
|
+
end
|
|
191
|
+
else
|
|
192
|
+
it "returns nil when the adapter does not track lines" do
|
|
193
|
+
expect(title.line_number).to be_nil
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
95
199
|
end
|