moxml 0.5.4 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9c1c880ebd136229eb81109ecbfc504a27b9bc101805688231525587a1601e8
4
- data.tar.gz: c09ed1d4c857d7a78006f4c2ade2bcb662bac233578858e7aeb0ca0b0d2f09f7
3
+ metadata.gz: 7f2b8ff3a745345a5d0462bd246f0ee8b463713fcad2dcf99590813ffd49aa27
4
+ data.tar.gz: 8930bcdaee831095c3a5b576d4f19a36591fd1a3884e0fb1f0aebb1c4c91485f
5
5
  SHA512:
6
- metadata.gz: d334d7e9789817b8891a78b77cf2bbaab05af83252a432cdc4bffb6728c37938a9f430098561ab8a8fa2db77f9ec0f089fc563922a1ceab046f26e7b2a1e01ed
7
- data.tar.gz: 1feb278e326095475c68bea001bd68f3ac76e8423fdf021bc3a54b765b4e9069ec5cf403c282687fb3843b6ca6411868d9208e6a50a5ea071ca5e443485358f0
6
+ metadata.gz: d7c64568fa5c5a013bee81b37e9329590817c5d0db567c6f8c609de48e6782d75544e10bfe00929d0768e04a10591eef54c3a79e1ed3acfab2aa34dc1a11eb64
7
+ data.tar.gz: 6eebca4d31617a36da0df9f471bafc7085556b7216af8bb6138df0c9e5c2d8982fdfd6bd8ad897a3c88bdc06367c30fef9ccc9418575480ce0b97546a40af38c
@@ -30,6 +30,8 @@ jobs:
30
30
  with:
31
31
  next_version: ${{ github.event.inputs.next_version }}
32
32
  gated: ${{ inputs.gated == true }}
33
+ # No rubygems-api-key: the gem publishes through its configured
34
+ # RubyGems OIDC trusted publisher. pat_token covers the version
35
+ # bump push to main.
33
36
  secrets:
34
- rubygems-api-key: ${{ secrets.LUTAML_CI_RUBYGEMS_API_KEY }}
35
37
  pat_token: ${{ secrets.GITHUB_TOKEN }}
@@ -173,6 +173,14 @@ namespace_validation_mode: :strict)
173
173
  node
174
174
  end
175
175
 
176
+ # Whether children() results need per-child patch_node
177
+ # rewriting. Adapters whose natives arrive pre-wrapped (all but
178
+ # ox and libxml) answer false so the wrapper layer can skip
179
+ # the identity map over every child list.
180
+ def patches_children?
181
+ false
182
+ end
183
+
176
184
  # Whether the subtree at native can contain entity markers.
177
185
  # Marker-tracking adapters override this so the post-serialize
178
186
  # restore can skip its full-output scans on marker-free
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Adapter
5
+ module CustomizedLeptris
6
+ # Document-level processing instruction. libleptris stores these
7
+ # outside the element tree (a flat, document-ordered list with no
8
+ # prolog/epilog anchoring — its serializer emits them all before
9
+ # the root), reachable through Document#processing_instructions
10
+ # as [target, data] pairs. This pseudo-native gives the pairs a
11
+ # node identity so the moxml contract can list them as document
12
+ # children.
13
+ class DocumentPI
14
+ attr_accessor :target, :data, :parent_doc
15
+ alias content data
16
+ alias content= data=
17
+
18
+ def initialize(target, data = "", parent_doc = nil)
19
+ @target = target
20
+ @data = data.to_s
21
+ @parent_doc = parent_doc
22
+ end
23
+
24
+ def to_xml
25
+ data.empty? ? "<?#{target}?>" : "<?#{target} #{data}?>"
26
+ end
27
+
28
+ def ==(other)
29
+ other.is_a?(self.class) && target == other.target && data == other.data
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -3,7 +3,8 @@
3
3
  module Moxml
4
4
  module Adapter
5
5
  # Pure-Ruby node kinds that libleptris has no native representation
6
- # for: XML declarations, programmatic DOCTYPEs, and entity references.
6
+ # for: XML declarations, programmatic DOCTYPEs, entity references,
7
+ # and document-level processing instructions.
7
8
  # The adapter stores them via NativeAttachment and serializes them
8
9
  # from #to_xml.
9
10
  module CustomizedLeptris
@@ -11,6 +12,7 @@ module Moxml
11
12
  autoload :Doctype, "moxml/adapter/customized_leptris/doctype"
12
13
  autoload :EntityReference, "moxml/adapter/customized_leptris/entity_reference"
13
14
  autoload :TextSegment, "moxml/adapter/customized_leptris/text_segment"
15
+ autoload :DocumentPI, "moxml/adapter/customized_leptris/document_pi"
14
16
  end
15
17
  end
16
18
  end
@@ -215,7 +215,8 @@ module Moxml
215
215
  def entity_bearing?(native)
216
216
  case native
217
217
  when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype,
218
- CustomizedLeptris::EntityReference, CustomizedLeptris::TextSegment
218
+ CustomizedLeptris::EntityReference, CustomizedLeptris::TextSegment,
219
+ CustomizedLeptris::DocumentPI
219
220
  true
220
221
  else
221
222
  doc = native.document
@@ -231,7 +232,7 @@ module Moxml
231
232
  when CustomizedLeptris::EntityReference then :entity_reference
232
233
  when ::Leptris::XML::CDATA then :cdata
233
234
  when ::Leptris::XML::Comment then :comment
234
- when ::Leptris::XML::ProcessingInstruction then :processing_instruction
235
+ when ::Leptris::XML::ProcessingInstruction, CustomizedLeptris::DocumentPI then :processing_instruction
235
236
  when ::Leptris::XML::Text, CustomizedLeptris::TextSegment then :text
236
237
  when ::Leptris::XML::Element then :element
237
238
  when ::Leptris::XML::Attr then :attribute
@@ -241,13 +242,14 @@ module Moxml
241
242
 
242
243
  def node_name(node)
243
244
  return node.root_name if node.is_a?(::Leptris::XML::DocType)
245
+ return node.target if node.is_a?(CustomizedLeptris::DocumentPI)
244
246
 
245
247
  node.name.to_s.dup.force_encoding("UTF-8")
246
248
  end
247
249
 
248
250
  def set_node_name(node, name)
249
251
  case node
250
- when ::Leptris::XML::ProcessingInstruction then node.target = name
252
+ when ::Leptris::XML::ProcessingInstruction, CustomizedLeptris::DocumentPI then node.target = name
251
253
  else node.name = name
252
254
  end
253
255
  end
@@ -260,6 +262,8 @@ module Moxml
260
262
  CustomizedLeptris::Doctype.new(node.name, node.external_id, node.system_id)
261
263
  when CustomizedLeptris::EntityReference
262
264
  CustomizedLeptris::EntityReference.new(node.name)
265
+ when CustomizedLeptris::DocumentPI
266
+ CustomizedLeptris::DocumentPI.new(node.target, node.data, node.parent_doc)
263
267
  else
264
268
  node.dup
265
269
  end
@@ -270,7 +274,13 @@ module Moxml
270
274
  when ::Leptris::XML::Document
271
275
  assemble_document_children(node)
272
276
  when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype,
273
- CustomizedLeptris::EntityReference, CustomizedLeptris::TextSegment
277
+ CustomizedLeptris::EntityReference, CustomizedLeptris::TextSegment,
278
+ CustomizedLeptris::DocumentPI,
279
+ # Terminal node kinds pay an FFI round trip for an empty
280
+ # list; unfiltered recursions visit every text node.
281
+ ::Leptris::XML::Text, ::Leptris::XML::Comment,
282
+ ::Leptris::XML::CDATA, ::Leptris::XML::ProcessingInstruction,
283
+ ::Leptris::XML::Attr
274
284
  []
275
285
  else
276
286
  natives = node.children.to_a
@@ -316,7 +326,8 @@ module Moxml
316
326
  def parent(node)
317
327
  case node
318
328
  when ::Leptris::XML::Document then nil
319
- when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype then node.parent_doc
329
+ when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype,
330
+ CustomizedLeptris::DocumentPI then node.parent_doc
320
331
  when CustomizedLeptris::TextSegment, CustomizedLeptris::EntityReference then node.parent
321
332
  else
322
333
  # The binding reports the root element as parentless; the
@@ -442,6 +453,12 @@ module Moxml
442
453
  attachments.delete(node.parent_doc, :doctype) if node.parent_doc
443
454
  when CustomizedLeptris::EntityReference
444
455
  marker_text_for(node.parent, node.name)&.unlink
456
+ when CustomizedLeptris::DocumentPI
457
+ raise Moxml::NotImplementedError.new(
458
+ "libleptris has no document-level PI removal",
459
+ adapter: :leptris,
460
+ feature: :remove,
461
+ )
445
462
  else
446
463
  node.unlink
447
464
  end
@@ -736,7 +753,7 @@ module Moxml
736
753
  # binding, so a Text branch first would swallow CDATA nodes.
737
754
  case node
738
755
  when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype,
739
- CustomizedLeptris::EntityReference
756
+ CustomizedLeptris::EntityReference, CustomizedLeptris::DocumentPI
740
757
  return node.to_xml
741
758
  when ::Leptris::XML::CDATA
742
759
  return XmlEmitter.cdata(node.content)
@@ -858,9 +875,7 @@ module Moxml
858
875
  native = native_doctype_xml(doc)
859
876
  parts << native if native
860
877
 
861
- (doc.processing_instructions || []).each do |(target, data)|
862
- parts << (data.to_s.empty? ? "<?#{target}?>" : "<?#{target} #{data}?>")
863
- end
878
+ document_pi_nodes(doc).each { |pi| parts << pi.to_xml }
864
879
 
865
880
  parts << raw_serialize(doc.root, options) if doc.root
866
881
 
@@ -930,6 +945,21 @@ module Moxml
930
945
  attachments.get(native, :had_source_declaration) ? true : false
931
946
  end
932
947
 
948
+ # Document-level PI pseudo-nodes, materialized once from the C
949
+ # list and cached per document: children and serialization read
950
+ # the same objects, so wrapper mutations round-trip. Build the
951
+ # cache BEFORE appending a PI with add_pi, or the C-side
952
+ # addition would be double-counted.
953
+ def document_pi_nodes(doc)
954
+ attachments.get(doc, :doc_pi_nodes) || begin
955
+ nodes = doc.processing_instructions.map do |(target, data)|
956
+ CustomizedLeptris::DocumentPI.new(target, data, doc)
957
+ end
958
+ attachments.set(doc, :doc_pi_nodes, nodes)
959
+ nodes
960
+ end
961
+ end
962
+
933
963
  def assemble_document_children(doc)
934
964
  children = []
935
965
 
@@ -939,6 +969,15 @@ module Moxml
939
969
  doctype_wrapper = attachments.get(doc, :doctype)
940
970
  children << doctype_wrapper if doctype_wrapper
941
971
 
972
+ # Document-level PIs live outside the element tree in
973
+ # libleptris (a flat list; its serializer emits them before
974
+ # the root). Listed after the DOCTYPE to match the order
975
+ # serialize_document emits, so children and serialization
976
+ # agree. Epilog anchoring is not representable in the C
977
+ # model — a known divergence from the Nokogiri-shaped
978
+ # contract the other adapters expose.
979
+ children.concat(document_pi_nodes(doc))
980
+
942
981
  children << doc.root if doc.root
943
982
 
944
983
  texts = attachments.get(doc, :document_text)
@@ -961,7 +1000,15 @@ module Moxml
961
1000
  when ::Leptris::XML::Element
962
1001
  doc.root = child
963
1002
  when ::Leptris::XML::ProcessingInstruction
1003
+ document_pi_nodes(doc)
964
1004
  doc.add_pi(child.target, child.content.to_s)
1005
+ document_pi_nodes(doc) << CustomizedLeptris::DocumentPI.new(
1006
+ child.target, child.content.to_s, doc
1007
+ )
1008
+ when CustomizedLeptris::DocumentPI
1009
+ document_pi_nodes(doc)
1010
+ doc.add_pi(child.target, child.data)
1011
+ document_pi_nodes(doc) << child
965
1012
  when ::Leptris::XML::Text
966
1013
  texts = attachments.get(doc, :document_text) || []
967
1014
  texts << child
@@ -1134,6 +1134,10 @@ module Moxml
1134
1134
  end
1135
1135
  end
1136
1136
 
1137
+ def patches_children?
1138
+ true
1139
+ end
1140
+
1137
1141
  def patch_node(node, _parent = nil)
1138
1142
  # Wrap native LibXML nodes in our wrapper classes
1139
1143
  return node if node.nil?
@@ -243,6 +243,10 @@ module Moxml
243
243
  Marshal.load(Marshal.dump(node))
244
244
  end
245
245
 
246
+ def patches_children?
247
+ true
248
+ end
249
+
246
250
  def patch_node(node, parent = nil)
247
251
  new_node =
248
252
  case node
data/lib/moxml/config.rb CHANGED
@@ -11,10 +11,13 @@ module Moxml
11
11
  # when the installed leptris supports programmatic document
12
12
  # construction (Leptris::XML::Document.create); otherwise moxml
13
13
  # falls back to FALLBACK_ADAPTER so released-gem environments keep
14
- # working. Oga remains the Opal default (pure Ruby).
14
+ # working. Under Opal the default is oga only when the vendored
15
+ # pure-Ruby fork was compiled into the bundle — stock oga requires
16
+ # its C extension and cannot load there; otherwise rexml.
15
17
  PREFERRED_ADAPTER = :leptris
16
18
  FALLBACK_ADAPTER = :nokogiri
17
19
  OPAL_DEFAULT_ADAPTER = :oga
20
+ OPAL_FALLBACK_ADAPTER = :rexml
18
21
 
19
22
  # Entity loading modes:
20
23
  # - :required - Must load entities, raise error if unavailable (default)
@@ -35,13 +38,26 @@ module Moxml
35
38
  end
36
39
 
37
40
  def runtime_default_adapter
38
- return OPAL_DEFAULT_ADAPTER if RUBY_ENGINE == "opal"
41
+ return opal_runtime_adapter if RUBY_ENGINE == "opal"
39
42
 
40
43
  return PREFERRED_ADAPTER if leptris_preferred_available?
41
44
 
42
45
  detect_loaded_adapter || FALLBACK_ADAPTER
43
46
  end
44
47
 
48
+ # Stock oga requires its C extension, so under Opal the oga
49
+ # adapter can only come from the vendored pure-Ruby fork — a
50
+ # repo-only artifact the released gem does not ship. Bundles
51
+ # that compiled it in (moxml's own harness) keep oga; everyone
52
+ # else gets the rexml adapter, whose Opal compat ships in-gem.
53
+ # `defined?` is safe here: this branch never runs under MRI,
54
+ # where it would trigger autoload resolution.
55
+ def opal_runtime_adapter
56
+ return OPAL_DEFAULT_ADAPTER if defined?(Moxml::Adapter::Oga)
57
+
58
+ OPAL_FALLBACK_ADAPTER
59
+ end
60
+
45
61
  # True when the leptris gem is installed AND new enough for the
46
62
  # adapter (it needs programmatic document construction, which
47
63
  # released 1.1.x lacks). Memoized: the require probe runs once.
data/lib/moxml/node.rb CHANGED
@@ -38,11 +38,11 @@ module Moxml
38
38
  end
39
39
 
40
40
  def children
41
- @children ||= NodeSet.new(
42
- adapter.children(@native).map { adapter.patch_node(_1, @native) },
43
- context,
44
- self,
45
- )
41
+ @children ||= begin
42
+ natives = adapter.children(@native)
43
+ natives = natives.map { adapter.patch_node(_1, @native) } if adapter.patches_children?
44
+ NodeSet.new(natives, context, self)
45
+ end
46
46
  end
47
47
 
48
48
  def next_sibling
data/lib/moxml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.4"
4
+ VERSION = "0.5.6"
5
5
  end
@@ -81,6 +81,54 @@ RSpec.describe Moxml::Adapter::Leptris do
81
81
  end
82
82
  end
83
83
 
84
+ describe "document-level processing instructions" do
85
+ let(:ctx) { Moxml.new(:leptris) }
86
+
87
+ it "lists document PIs as children with the root" do
88
+ doc = ctx.parse('<?xml version="1.0"?><?pi-prolog before?><root/><?pi-epilog after?>')
89
+ kids = doc.children.to_a
90
+
91
+ expect(kids.map(&:class)).to eq(
92
+ [Moxml::ProcessingInstruction, Moxml::ProcessingInstruction, Moxml::Element],
93
+ )
94
+ expect(kids.select(&:processing_instruction?).map(&:target)).to eq(%w[pi-prolog pi-epilog])
95
+ expect(kids[0].content).to eq("before")
96
+ end
97
+
98
+ it "round-trips mutations and additions through serialization" do
99
+ doc = ctx.parse("<?pi-original x?><root/>")
100
+ pi = doc.children.to_a[0]
101
+ pi.target = "renamed"
102
+ pi.content = "changed"
103
+ expect(doc.to_xml).to include("<?renamed changed?>")
104
+
105
+ doc.add_child(doc.create_processing_instruction("added", "now"))
106
+ expect(doc.to_xml).to include("<?added now?>")
107
+ expect(doc.children.to_a.select(&:processing_instruction?).map(&:target)).to eq(%w[renamed added])
108
+ end
109
+
110
+ it "serializes children and document output in agreement" do
111
+ # libleptris stores document PIs as one flat pre-root list (no
112
+ # epilog anchoring); children and to_xml must at least agree.
113
+ doc = ctx.parse('<?xml version="1.0"?><?pi-a 1?><root/><?pi-b 2?>')
114
+ from_children = doc.children.to_a.select(&:processing_instruction?)
115
+ .map(&:to_xml).join
116
+ from_document = doc.to_xml.sub(%r{\A<\?xml[^>]*\?>}, "")
117
+
118
+ expect(from_document).to start_with(from_children)
119
+ end
120
+
121
+ it "reports the tracked native for a re-added document PI" do
122
+ doc = ctx.parse("<?pi-src orig?><root/>")
123
+ moved = doc.children.to_a[0]
124
+ target_doc = ctx.parse("<other/>")
125
+ target_doc.add_child(moved)
126
+
127
+ expect(target_doc.to_xml).to include("<?pi-src orig?>")
128
+ expect(target_doc.children.to_a.select(&:processing_instruction?).map(&:target)).to include("pi-src")
129
+ end
130
+ end
131
+
84
132
  describe "entity-marker tracking" do
85
133
  let(:ctx) { Moxml.new(:leptris) }
86
134
 
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
4
+ version: 0.5.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -123,6 +123,7 @@ files:
123
123
  - lib/moxml/adapter/customized_leptris.rb
124
124
  - lib/moxml/adapter/customized_leptris/declaration.rb
125
125
  - lib/moxml/adapter/customized_leptris/doctype.rb
126
+ - lib/moxml/adapter/customized_leptris/document_pi.rb
126
127
  - lib/moxml/adapter/customized_leptris/entity_reference.rb
127
128
  - lib/moxml/adapter/customized_leptris/text_segment.rb
128
129
  - lib/moxml/adapter/customized_libxml.rb