moxml 0.1.26 → 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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/windows.yml +62 -0
  3. data/Gemfile +3 -1
  4. data/README.adoc +69 -6
  5. data/lib/compat/opal/moxml_boot.rb +5 -0
  6. data/lib/moxml/adapter/base.rb +53 -49
  7. data/lib/moxml/adapter/customized_leptris/declaration.rb +32 -0
  8. data/lib/moxml/adapter/customized_leptris/doctype.rb +31 -0
  9. data/lib/moxml/adapter/customized_leptris/entity_reference.rb +12 -0
  10. data/lib/moxml/adapter/customized_leptris/text_segment.rb +26 -0
  11. data/lib/moxml/adapter/customized_leptris.rb +16 -0
  12. data/lib/moxml/adapter/customized_libxml/cdata.rb +3 -12
  13. data/lib/moxml/adapter/customized_libxml/comment.rb +2 -9
  14. data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -10
  15. data/lib/moxml/adapter/customized_libxml/node.rb +8 -0
  16. data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +2 -10
  17. data/lib/moxml/adapter/customized_libxml/text.rb +2 -9
  18. data/lib/moxml/adapter/customized_oga/entity_decoder.rb +37 -0
  19. data/lib/moxml/adapter/customized_oga/raw_value_override.rb +52 -0
  20. data/lib/moxml/adapter/customized_oga/xml_generator.rb +4 -34
  21. data/lib/moxml/adapter/customized_oga.rb +2 -0
  22. data/lib/moxml/adapter/customized_ox/entity_reference.rb +1 -17
  23. data/lib/moxml/adapter/customized_rexml/entity_reference.rb +1 -11
  24. data/lib/moxml/adapter/headed_ox.rb +9 -128
  25. data/lib/moxml/adapter/leptris.rb +965 -0
  26. data/lib/moxml/adapter/libxml.rb +131 -99
  27. data/lib/moxml/adapter/nokogiri.rb +29 -8
  28. data/lib/moxml/adapter/oga.rb +91 -54
  29. data/lib/moxml/adapter/ox.rb +93 -137
  30. data/lib/moxml/adapter/rexml.rb +61 -50
  31. data/lib/moxml/adapter.rb +2 -1
  32. data/lib/moxml/attribute.rb +8 -4
  33. data/lib/moxml/attribute_resolver.rb +189 -0
  34. data/lib/moxml/config.rb +29 -3
  35. data/lib/moxml/context.rb +12 -0
  36. data/lib/moxml/element.rb +29 -12
  37. data/lib/moxml/entity/reference.rb +28 -0
  38. data/lib/moxml/entity.rb +125 -0
  39. data/lib/moxml/node.rb +90 -1
  40. data/lib/moxml/sax/namespace_splitter.rb +5 -2
  41. data/lib/moxml/signature/model/key/dsa_key_value.rb +1 -2
  42. data/lib/moxml/version.rb +1 -1
  43. data/lib/moxml/xml_emitter.rb +71 -0
  44. data/lib/moxml/xpath/compiler.rb +60 -7
  45. data/lib/moxml/xpath/parser.rb +22 -15
  46. data/lib/moxml.rb +3 -0
  47. data/spec/examples/readme_examples_spec.rb +0 -4
  48. data/spec/examples/xpath_examples_spec.rb +0 -11
  49. data/spec/integration/all_adapters_spec.rb +17 -0
  50. data/spec/integration/sax_parity_spec.rb +58 -0
  51. data/spec/integration/shared_examples/edge_cases.rb +0 -10
  52. data/spec/integration/shared_examples/integration_workflows.rb +0 -10
  53. data/spec/integration/shared_examples/node_wrappers/attribute_behavior.rb +98 -0
  54. data/spec/integration/shared_examples/node_wrappers/namespace_behavior.rb +26 -0
  55. data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +0 -8
  56. data/spec/moxml/adapter/leptris_spec.rb +75 -0
  57. data/spec/moxml/adapter/ox_spec.rb +20 -6
  58. data/spec/moxml/adapter/platform_spec.rb +15 -2
  59. data/spec/moxml/adapter/shared_examples/adapter_contract.rb +180 -0
  60. data/spec/moxml/attribute_resolver_spec.rb +108 -0
  61. data/spec/moxml/doctype_spec.rb +1 -1
  62. data/spec/moxml/entity_spec.rb +76 -0
  63. data/spec/moxml/node_spec.rb +104 -0
  64. data/spec/moxml/sax_entity_parity_spec.rb +358 -0
  65. data/spec/moxml/xml_emitter_spec.rb +64 -0
  66. data/spec/moxml/xpath/axes_spec.rb +72 -0
  67. data/spec/moxml/xpath/parser_spec.rb +6 -0
  68. data/spec/moxml/xpath_capabilities_spec.rb +5 -3
  69. data/spec/performance/benchmark_spec.rb +13 -6
  70. data/spec/performance/thread_safety_spec.rb +0 -4
  71. data/spec/spec_helper.rb +5 -1
  72. metadata +21 -2
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::XmlEmitter do
6
+ describe ".escape_text" do
7
+ it "escapes only & < > in a single pass" do
8
+ expect(described_class.escape_text("a & < > ' \""))
9
+ .to eq("a &amp; &lt; &gt; ' \"")
10
+ end
11
+
12
+ it "returns clean strings unallocated" do
13
+ input = "clean"
14
+ expect(described_class.escape_text(input)).to equal(input)
15
+ end
16
+
17
+ it "coerces non-strings" do
18
+ expect(described_class.escape_text(1)).to eq("1")
19
+ end
20
+ end
21
+
22
+ describe ".escape_attribute" do
23
+ it "escapes & < > and double quotes, keeping apostrophes literal" do
24
+ expect(described_class.escape_attribute(%q{& < > ' "}))
25
+ .to eq("&amp; &lt; &gt; ' &quot;")
26
+ end
27
+ end
28
+
29
+ describe ".cdata" do
30
+ it "wraps content in a CDATA section" do
31
+ expect(described_class.cdata("data")).to eq("<![CDATA[data]]>")
32
+ end
33
+
34
+ it "splits an embedded end sequence into adjacent sections" do
35
+ expect(described_class.cdata("a]]>b"))
36
+ .to eq("<![CDATA[a]]]]><![CDATA[>b]]>")
37
+ end
38
+ end
39
+
40
+ describe ".declaration_xml" do
41
+ it "renders only the set attributes" do
42
+ expect(described_class.declaration_xml("1.0", nil, nil))
43
+ .to eq('<?xml version="1.0"?>')
44
+ expect(described_class.declaration_xml("1.0", "UTF-8", "yes"))
45
+ .to eq('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>')
46
+ end
47
+
48
+ it "omits empty encodings" do
49
+ expect(described_class.declaration_xml("1.0", "", nil))
50
+ .to eq('<?xml version="1.0"?>')
51
+ end
52
+ end
53
+
54
+ describe ".doctype_xml" do
55
+ it "renders PUBLIC and SYSTEM forms and bare names" do
56
+ expect(described_class.doctype_xml("r", "pub", "sys"))
57
+ .to eq('<!DOCTYPE r PUBLIC "pub" "sys">')
58
+ expect(described_class.doctype_xml("r", nil, "sys"))
59
+ .to eq('<!DOCTYPE r SYSTEM "sys">')
60
+ expect(described_class.doctype_xml("r", nil, nil))
61
+ .to eq("<!DOCTYPE r>")
62
+ end
63
+ end
64
+ end
@@ -163,6 +163,78 @@ RSpec.describe "XPath Axes" do
163
163
  end
164
164
  end
165
165
 
166
+ describe "attribute axis with namespaces" do
167
+ let(:ns_doc) do
168
+ xml = <<~XML
169
+ <root xmlns:p="http://x.org" xmlns:q="http://x.org">
170
+ <item q:type="namespaced" type="bare" other="o"/>
171
+ </root>
172
+ XML
173
+ context.parse(xml)
174
+ end
175
+
176
+ def select(expression, namespaces = nil)
177
+ ast = Moxml::XPath::Parser.parse(expression)
178
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast,
179
+ namespaces: namespaces)
180
+ proc.call(ns_doc).map(&:value)
181
+ end
182
+
183
+ it "matches bare names against no-namespace attributes only" do
184
+ expect(select("/root/item/@type")).to eq(["bare"])
185
+ end
186
+
187
+ it "matches prefixed names by namespace URI, not by prefix spelling" do
188
+ expect(select("/root/item/@p:type")).to eq(["namespaced"])
189
+ expect(select("/root/item/@q:type")).to eq(["namespaced"])
190
+ end
191
+
192
+ it "matches any local name within a prefix's namespace" do
193
+ expect(select("/root/item/@q:*")).to eq(["namespaced"])
194
+ end
195
+
196
+ it "matches nothing for an undeclared prefix" do
197
+ expect(select("/root/item/@z:type")).to be_empty
198
+ end
199
+
200
+ it "prefers context prefix mappings over document declarations" do
201
+ expect(select("/root/item/@p:type", "p" => "http://other.org"))
202
+ .to be_empty
203
+ expect(select("/root/item/@zzz:type", "zzz" => "http://x.org"))
204
+ .to eq(["namespaced"])
205
+ end
206
+ end
207
+
208
+ describe "descendant-or-self shorthand from an element" do
209
+ it "selects descendants with .//step" do
210
+ ast = Moxml::XPath::Parser.parse(".//grandchild")
211
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
212
+ result = proc.call(doc.root.children.find(&:element?))
213
+
214
+ expect(result.map { |n| n.attribute("id").value })
215
+ .to contain_exactly("g1", "g2")
216
+ end
217
+ end
218
+
219
+ describe "node type tests as steps" do
220
+ it "selects text nodes with text()" do
221
+ ast = Moxml::XPath::Parser.parse("//grandchild/text()")
222
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
223
+ result = proc.call(doc)
224
+
225
+ expect(result.map(&:content)).to contain_exactly("text1", "text2")
226
+ end
227
+
228
+ it "selects comments with comment()" do
229
+ commented = context.parse("<root><!-- c1 --><a/><!-- c2 --></root>")
230
+ ast = Moxml::XPath::Parser.parse("//comment()")
231
+ proc = Moxml::XPath::Compiler.compile_with_cache(ast)
232
+ result = proc.call(commented)
233
+
234
+ expect(result.map(&:content)).to contain_exactly(" c1 ", " c2 ")
235
+ end
236
+ end
237
+
166
238
  describe "descendant axis" do
167
239
  it "finds descendants without self" do
168
240
  ast = Moxml::XPath::Parser.parse("/root/descendant::child")
@@ -60,6 +60,12 @@ RSpec.describe Moxml::XPath::Parser do
60
60
  expect(ast.type).to eq(:relative_path)
61
61
  end
62
62
 
63
+ it "parses prefixed attribute names" do
64
+ ast = described_class.parse("@xmi:type")
65
+ expect(ast.type).to eq(:relative_path)
66
+ expect { described_class.parse("@xmi:*") }.not_to raise_error
67
+ end
68
+
63
69
  it "parses path with parent reference" do
64
70
  ast = described_class.parse("../book")
65
71
  expect(ast.type).to eq(:relative_path)
@@ -131,11 +131,13 @@ RSpec.describe "XPath Capabilities" do
131
131
  end
132
132
 
133
133
  describe "XPath functions" do
134
- it "supports count() function" do
134
+ it "supports count() function, returning the scalar unwrapped" do
135
135
  doc = Moxml.new.parse(simple_xml)
136
- # NOTE: count() returns a number, not nodes
137
136
  result = doc.xpath("count(//book)")
138
- expect(result).to be_a(Numeric) if result.is_a?(Numeric)
137
+ # REXML's XPath has no function support and yields an empty set
138
+ skip "count() function not supported on #{adapter_name}" if result.is_a?(Moxml::NodeSet)
139
+
140
+ expect(result).to eq(2)
139
141
  rescue Moxml::XPathError, NoMethodError
140
142
  skip "count() function not supported on #{adapter_name}"
141
143
  end
@@ -23,14 +23,21 @@ RSpec.shared_examples "Performance Examples" do
23
23
  context "measures performance" do
24
24
  let(:doc) { context.parse(large_xml) }
25
25
 
26
+ # Absolute ips varies more than 2x across machines and even between
27
+ # runs on the same machine (observed: rexml serializer 1.5-2.9 ips on
28
+ # one machine). These thresholds sit at roughly half of typical
29
+ # modern-runner throughput so they only trip on asymptotic
30
+ # regressions (accidental O(n^2), broken caching), not on hardware
31
+ # variance or a few percent of legitimate overhead.
26
32
  let(:thresholds) do
27
33
  {
28
- nokogiri: { parser: 15, serializer: 1000 },
29
- oga: { parser: 10, serializer: 100 },
30
- rexml: { parser: 0, serializer: 5 },
31
- ox: { parser: 2, serializer: 1000 },
32
- headed_ox: { parser: 2, serializer: 1000 },
33
- libxml: { parser: 500, serializer: 60 },
34
+ nokogiri: { parser: 8, serializer: 500 },
35
+ oga: { parser: 5, serializer: 50 },
36
+ rexml: { parser: 0, serializer: 1 },
37
+ ox: { parser: 1, serializer: 400 },
38
+ headed_ox: { parser: 1, serializer: 400 },
39
+ libxml: { parser: 200, serializer: 30 },
40
+ leptris: { parser: 1300, serializer: 600 },
34
41
  }
35
42
  end
36
43
 
@@ -20,10 +20,6 @@ RSpec.shared_examples "Thread Safety Examples" do
20
20
  end
21
21
 
22
22
  it "handles concurrent processing" do
23
- if Moxml.new.config.adapter_name == :ox
24
- skip "Ox doesn't have a native XPath"
25
- end
26
-
27
23
  processor = processor_class.new
28
24
  threads = []
29
25
  results = Queue.new
data/spec/spec_helper.rb CHANGED
@@ -101,7 +101,11 @@ RSpec.configure do |config|
101
101
  end
102
102
 
103
103
  Moxml.configure do |config|
104
- config.adapter = RUBY_ENGINE == "opal" ? Moxml::Config::OPAL_DEFAULT_ADAPTER : Moxml::Config::DEFAULT_ADAPTER
104
+ # Pin the suite baseline to the fallback adapter so results are
105
+ # reproducible regardless of whether the local environment has a
106
+ # leptris new enough to win the runtime-default probe. Leptris gets
107
+ # full coverage through its explicit adapter/integration matrices.
108
+ config.adapter = RUBY_ENGINE == "opal" ? Moxml::Config::OPAL_DEFAULT_ADAPTER : Moxml::Config::FALLBACK_ADAPTER
105
109
  config.strict_parsing = true
106
110
  config.default_encoding = "UTF-8"
107
111
  config.entity_load_mode = :optional if RUBY_ENGINE == "opal"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.26
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-22 00:00:00.000000000 Z
11
+ date: 2026-08-25 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  Moxml is a unified XML manipulation library that provides a common API
@@ -27,6 +27,7 @@ files:
27
27
  - ".github/workflows/rake.yml"
28
28
  - ".github/workflows/release.yml"
29
29
  - ".github/workflows/round-trip.yml"
30
+ - ".github/workflows/windows.yml"
30
31
  - ".gitignore"
31
32
  - ".gitmodules"
32
33
  - ".rspec"
@@ -119,6 +120,11 @@ files:
119
120
  - lib/moxml.rb
120
121
  - lib/moxml/adapter.rb
121
122
  - lib/moxml/adapter/base.rb
123
+ - lib/moxml/adapter/customized_leptris.rb
124
+ - lib/moxml/adapter/customized_leptris/declaration.rb
125
+ - lib/moxml/adapter/customized_leptris/doctype.rb
126
+ - lib/moxml/adapter/customized_leptris/entity_reference.rb
127
+ - lib/moxml/adapter/customized_leptris/text_segment.rb
122
128
  - lib/moxml/adapter/customized_libxml.rb
123
129
  - lib/moxml/adapter/customized_libxml/cdata.rb
124
130
  - lib/moxml/adapter/customized_libxml/comment.rb
@@ -129,6 +135,8 @@ files:
129
135
  - lib/moxml/adapter/customized_libxml/processing_instruction.rb
130
136
  - lib/moxml/adapter/customized_libxml/text.rb
131
137
  - lib/moxml/adapter/customized_oga.rb
138
+ - lib/moxml/adapter/customized_oga/entity_decoder.rb
139
+ - lib/moxml/adapter/customized_oga/raw_value_override.rb
132
140
  - lib/moxml/adapter/customized_oga/xml_declaration.rb
133
141
  - lib/moxml/adapter/customized_oga/xml_generator.rb
134
142
  - lib/moxml/adapter/customized_ox.rb
@@ -140,6 +148,7 @@ files:
140
148
  - lib/moxml/adapter/customized_rexml/entity_reference.rb
141
149
  - lib/moxml/adapter/customized_rexml/formatter.rb
142
150
  - lib/moxml/adapter/headed_ox.rb
151
+ - lib/moxml/adapter/leptris.rb
143
152
  - lib/moxml/adapter/libxml.rb
144
153
  - lib/moxml/adapter/libxml/entity_ref_registry.rb
145
154
  - lib/moxml/adapter/libxml/entity_restorer.rb
@@ -148,6 +157,7 @@ files:
148
157
  - lib/moxml/adapter/ox.rb
149
158
  - lib/moxml/adapter/rexml.rb
150
159
  - lib/moxml/attribute.rb
160
+ - lib/moxml/attribute_resolver.rb
151
161
  - lib/moxml/builder.rb
152
162
  - lib/moxml/c14n.rb
153
163
  - lib/moxml/c14n/attribute_handler.rb
@@ -179,6 +189,8 @@ files:
179
189
  - lib/moxml/document.rb
180
190
  - lib/moxml/document_builder.rb
181
191
  - lib/moxml/element.rb
192
+ - lib/moxml/entity.rb
193
+ - lib/moxml/entity/reference.rb
182
194
  - lib/moxml/entity_reference.rb
183
195
  - lib/moxml/entity_registry.rb
184
196
  - lib/moxml/entity_registry_opal_data.rb
@@ -246,6 +258,7 @@ files:
246
258
  - lib/moxml/signature/verifier.rb
247
259
  - lib/moxml/text.rb
248
260
  - lib/moxml/version.rb
261
+ - lib/moxml/xml_emitter.rb
249
262
  - lib/moxml/xml_utils.rb
250
263
  - lib/moxml/xml_utils/encoder.rb
251
264
  - lib/moxml/xpath.rb
@@ -371,6 +384,7 @@ files:
371
384
  - spec/integration/README.md
372
385
  - spec/integration/all_adapters_spec.rb
373
386
  - spec/integration/headed_ox_integration_spec.rb
387
+ - spec/integration/sax_parity_spec.rb
374
388
  - spec/integration/shared_examples/edge_cases.rb
375
389
  - spec/integration/shared_examples/entity_reference_whitespace.rb
376
390
  - spec/integration/shared_examples/high_level/.gitkeep
@@ -401,6 +415,7 @@ files:
401
415
  - spec/moxml/adapter/base_spec.rb
402
416
  - spec/moxml/adapter/entity_restoration_spec.rb
403
417
  - spec/moxml/adapter/headed_ox_spec.rb
418
+ - spec/moxml/adapter/leptris_spec.rb
404
419
  - spec/moxml/adapter/libxml_internals_spec.rb
405
420
  - spec/moxml/adapter/libxml_spec.rb
406
421
  - spec/moxml/adapter/nokogiri_spec.rb
@@ -413,6 +428,7 @@ files:
413
428
  - spec/moxml/adapter_spec.rb
414
429
  - spec/moxml/allocation_benchmark_spec.rb
415
430
  - spec/moxml/allocation_guard_spec.rb
431
+ - spec/moxml/attribute_resolver_spec.rb
416
432
  - spec/moxml/attribute_spec.rb
417
433
  - spec/moxml/builder_spec.rb
418
434
  - spec/moxml/c14n/api_spec.rb
@@ -434,6 +450,7 @@ files:
434
450
  - spec/moxml/entity_preservation_spec.rb
435
451
  - spec/moxml/entity_reference_spec.rb
436
452
  - spec/moxml/entity_registry_spec.rb
453
+ - spec/moxml/entity_spec.rb
437
454
  - spec/moxml/error_spec.rb
438
455
  - spec/moxml/lazy_parse_spec.rb
439
456
  - spec/moxml/moxml_spec.rb
@@ -455,6 +472,7 @@ files:
455
472
  - spec/moxml/opal_smoke_spec.rb
456
473
  - spec/moxml/processing_instruction_spec.rb
457
474
  - spec/moxml/sax/namespace_splitter_spec.rb
475
+ - spec/moxml/sax_entity_parity_spec.rb
458
476
  - spec/moxml/sax_spec.rb
459
477
  - spec/moxml/signature/algorithms/base64_transform_spec.rb
460
478
  - spec/moxml/signature/algorithms/digest_base_spec.rb
@@ -472,6 +490,7 @@ files:
472
490
  - spec/moxml/signature/round_trip_spec.rb
473
491
  - spec/moxml/text_spec.rb
474
492
  - spec/moxml/version_spec.rb
493
+ - spec/moxml/xml_emitter_spec.rb
475
494
  - spec/moxml/xml_utils/.gitkeep
476
495
  - spec/moxml/xml_utils/encoder_spec.rb
477
496
  - spec/moxml/xml_utils_spec.rb