moxml 0.5.3 → 0.5.5

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: 6cadb7739071fa9a823f6a08fe5daa33c2313d22e4c4454aa491e975f62da5ee
4
- data.tar.gz: b6682343586e2bf873c9067fccc68f03f2c67c8b10eafc418fcad6663320f2f2
3
+ metadata.gz: 0cc1ee5cd198e8016cd259e62eec132ef73863dbb91125cca55349c8847df01c
4
+ data.tar.gz: 9b3db2921b1acaebaaeccfa6dbba82c78184443c6338e79952e334da0a837b0c
5
5
  SHA512:
6
- metadata.gz: 67da3161b095e47aafe6d7e7327c2125a4458a9ef557c41c5f9d6465fccb01374d29c409178910b39426cf0cf7a42140d6858d4177c3f63b529690f397303796
7
- data.tar.gz: c88f55e02c026e18f4c1a25578d9e27094835288e6ccc0dccff40633ff31029975125d9fcfb51ae5353f00ad5fa54fd8bef135d9e44c64da794b45513f728903
6
+ metadata.gz: 4a4ccdd2949f15fedf7bee4bc2c41b9340250ec58d2d4583e7be7a0a14c397749e034f1ec3e15193c7a2ad93d43de7126338fecf2142ad1180ea50b3de25a97a
7
+ data.tar.gz: 9e4abee61c92cb0a3390804af398e6baaf593107e8bbc9b33b10d5bc40675f9f0b92ef5ded27f0b8948edd8c930b5188496c4c644529a277d8dd71b49f3bae57
@@ -5,16 +5,6 @@ module Moxml
5
5
  class Base
6
6
  # include XmlUtils
7
7
 
8
- # Entity round-trip pipeline lives in Moxml::Entity; these
9
- # constants and methods stay on the adapter protocol so
10
- # adapters and wrappers keep their calling convention.
11
- ENTITY_MARKER = Entity::MARKER
12
- ENTITY_NAME_PATTERN = Entity::NAME_PATTERN
13
- ENTITY_NAME_RE = Entity::NAME_RE
14
- ENTITY_MARKER_RE = Entity::MARKER_RE
15
- SERIALIZED_ENTITY_MARKER_RE = Entity::SERIALIZED_MARKER_RE
16
- STANDARD_ENTITIES = Entity::STANDARD_ENTITIES
17
-
18
8
  class << self
19
9
  include XmlUtils
20
10
 
@@ -183,6 +173,22 @@ namespace_validation_mode: :strict)
183
173
  node
184
174
  end
185
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
+
184
+ # Whether the subtree at native can contain entity markers.
185
+ # Marker-tracking adapters override this so the post-serialize
186
+ # restore can skip its full-output scans on marker-free
187
+ # documents; the default stays conservative.
188
+ def entity_bearing?(_native)
189
+ true
190
+ end
191
+
186
192
  # Check if the native document has an XML declaration
187
193
  # @param native_doc the native document object
188
194
  # @param wrapper [Moxml::Document] the wrapper with has_xml_declaration flag
@@ -46,6 +46,7 @@ module Moxml
46
46
  doc = Document.new(native_doc, ctx)
47
47
 
48
48
  record_source_declaration(native_doc, processed)
49
+ attachments.set(native_doc, :entity_markers, processed.include?(Entity::MARKER))
49
50
 
50
51
  doc
51
52
  end
@@ -207,6 +208,21 @@ module Moxml
207
208
  node.target
208
209
  end
209
210
 
211
+ # Marker presence is a document-level fact: parse records it,
212
+ # the ER builder path flips it, and the split/restore scans
213
+ # consult it. Customized natives only exist as split products
214
+ # of marker-bearing text, so they always report true.
215
+ def entity_bearing?(native)
216
+ case native
217
+ when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype,
218
+ CustomizedLeptris::EntityReference, CustomizedLeptris::TextSegment
219
+ true
220
+ else
221
+ doc = native.document
222
+ doc.nil? || attachments.get(doc, :entity_markers) != false
223
+ end
224
+ end
225
+
210
226
  def node_type(node)
211
227
  case node
212
228
  when ::Leptris::XML::Document then :document
@@ -254,10 +270,25 @@ module Moxml
254
270
  when ::Leptris::XML::Document
255
271
  assemble_document_children(node)
256
272
  when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype,
257
- CustomizedLeptris::EntityReference, CustomizedLeptris::TextSegment
273
+ CustomizedLeptris::EntityReference, CustomizedLeptris::TextSegment,
274
+ # Terminal node kinds pay an FFI round trip for an empty
275
+ # list; unfiltered recursions visit every text node.
276
+ ::Leptris::XML::Text, ::Leptris::XML::Comment,
277
+ ::Leptris::XML::CDATA, ::Leptris::XML::ProcessingInstruction,
278
+ ::Leptris::XML::Attr
258
279
  []
259
280
  else
260
- split_entity_markers(node.children.to_a, node)
281
+ natives = node.children.to_a
282
+ # Parse records whether the preprocessed source held any
283
+ # entity markers, and the ER builder path flips the flag
284
+ # when it mints one. A false flag lets traversal skip the
285
+ # marker split — including the per-text content fetch that
286
+ # dominates cold children cost. Cross-document moves of
287
+ # marker-bearing text into an entity-free document degrade
288
+ # to literal text.
289
+ return natives if attachments.get(node.document, :entity_markers) == false
290
+
291
+ split_entity_markers(natives, node)
261
292
  end
262
293
  end
263
294
 
@@ -385,6 +416,7 @@ module Moxml
385
416
  if child.is_a?(CustomizedLeptris::EntityReference)
386
417
  marker = parent.document.create_text_node("#{Entity::MARKER}#{child.name};")
387
418
  parent.add_child(marker)
419
+ attachments.set(parent.document, :entity_markers, true)
388
420
  return child
389
421
  end
390
422
  child = parent.document.create_text_node(child) if child.is_a?(String)
@@ -698,9 +730,10 @@ module Moxml
698
730
  end
699
731
 
700
732
  def serialize(node, options = {})
701
- xml = raw_serialize(node, options)
702
- xml = restore_entities(xml)
703
- normalize_serialization(xml, options)
733
+ # Entity restoration belongs to the wrapper layer
734
+ # (Node#to_xml runs adapter.restore_entities for every
735
+ # adapter); doing it here scanned the output a second time.
736
+ normalize_serialization(raw_serialize(node, options), options)
704
737
  end
705
738
 
706
739
  def raw_serialize(node, options)
@@ -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?
@@ -105,7 +105,7 @@ module Moxml
105
105
 
106
106
  def create_native_entity_reference(name)
107
107
  text = ::Oga::XML::Text.new
108
- text.text = "#{self::ENTITY_MARKER}#{name};"
108
+ text.text = "#{Entity::MARKER}#{name};"
109
109
  attachments.set(text, :entity_name, name)
110
110
  text
111
111
  end
@@ -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/context.rb CHANGED
@@ -60,7 +60,9 @@ module Moxml
60
60
  xml.rewind if xml.is_a?(IO) || xml.is_a?(StringIO)
61
61
  end
62
62
  end
63
- has_declaration = xml_string.strip.start_with?("<?xml")
63
+ # Allocation-free declaration sniff: String#strip would copy the
64
+ # whole buffer just to test its prefix.
65
+ has_declaration = xml_string.match?(/\A[ \t\r\n\f\v\0]*<\?xml/)
64
66
 
65
67
  # Parse with adapter, passing self (context) so adapter can use our config
66
68
  parsed_options = default_options.merge(options)
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ # Deprecated compatibility facade retained for 0.2-era callers.
5
+ #
6
+ # DocumentBuilder was the parse entry point before 0.3.0 retired it
7
+ # in favor of Context#parse; deleting the constant broke downstream
8
+ # gems that referenced it directly (issue #124). It now delegates to
9
+ # Context#parse, so `build(xml_string)` returns a parsed document —
10
+ # unlike the 0.2-era behavior of silently returning an empty one.
11
+ class DocumentBuilder
12
+ attr_reader :context
13
+
14
+ def initialize(context)
15
+ @context = context
16
+ end
17
+
18
+ def build(xml)
19
+ context.parse(xml)
20
+ end
21
+ end
22
+ end
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
@@ -111,8 +111,10 @@ module Moxml
111
111
  result = adapter.serialize(@native, serialize_options)
112
112
  result = apply_line_ending(result, serialize_options[:line_ending])
113
113
 
114
- # Restore entity markers to named entity references
115
- adapter.restore_entities(result)
114
+ # Restore entity markers to named entity references; skipped
115
+ # when the adapter knows the document carries no markers.
116
+ result = adapter.restore_entities(result) if adapter.entity_bearing?(@native)
117
+ result
116
118
  end
117
119
 
118
120
  def xpath(expression, namespaces = {})
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.3"
4
+ VERSION = "0.5.5"
5
5
  end
data/lib/moxml.rb CHANGED
@@ -80,6 +80,7 @@ module Moxml
80
80
  autoload :Namespace, "moxml/namespace"
81
81
  autoload :Doctype, "moxml/doctype"
82
82
  autoload :EntityReference, "moxml/entity_reference"
83
+ autoload :DocumentBuilder, "moxml/document_builder"
83
84
  autoload :Builder, "moxml/builder"
84
85
  autoload :Entity, "moxml/entity"
85
86
  autoload :EntityRegistry, "moxml/entity_registry"
@@ -80,4 +80,39 @@ RSpec.describe Moxml::Adapter::Leptris do
80
80
  expect { doc.xpath("//item[") }.to raise_error(Moxml::XPathError)
81
81
  end
82
82
  end
83
+
84
+ describe "entity-marker tracking" do
85
+ let(:ctx) { Moxml.new(:leptris) }
86
+
87
+ it "skips the marker split for entity-free documents" do
88
+ doc = ctx.parse("<root>\n <a>text</a>\n <b/>\n</root>")
89
+ kids = doc.root.children.to_a
90
+ expect(kids.map(&:class)).to eq([Moxml::Text, Moxml::Element, Moxml::Text, Moxml::Element, Moxml::Text])
91
+ expect(described_class.entity_bearing?(doc.root.native)).to be(false)
92
+ end
93
+
94
+ it "splits markers when the source carries entities" do
95
+ doc = ctx.parse("<root><a>pre&nbsp;post</a></root>")
96
+ kids = doc.at_xpath("//a").children.to_a
97
+ expect(kids.map(&:class)).to include(Moxml::EntityReference)
98
+ expect(described_class.entity_bearing?(doc.root.native)).to be(true)
99
+ end
100
+
101
+ it "returns serialized markup for entity-free documents" do
102
+ # to_xml must not return nil when the restore scan is skipped
103
+ doc = ctx.parse("<root><a>text</a></root>")
104
+ expect(doc.to_xml).to include("<root>")
105
+ expect(doc.at_xpath("//a").to_xml).to eq("<a>text</a>")
106
+ end
107
+
108
+ it "flips the flag when the builder mints an entity reference" do
109
+ doc = ctx.parse("<root><a/></root>")
110
+ expect(described_class.entity_bearing?(doc.root.native)).to be(false)
111
+
112
+ er = doc.create_entity_reference("nbsp")
113
+ doc.at_xpath("//a").add_child(er)
114
+ expect(described_class.entity_bearing?(doc.root.native)).to be(true)
115
+ expect(doc.at_xpath("//a").children.to_a.map(&:class)).to include(Moxml::EntityReference)
116
+ end
117
+ end
83
118
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::DocumentBuilder do
6
+ # Issue #124: 0.3.0 deleted the class and NameError'd downstream
7
+ # callers (lutaml-xsd parses XML through this constructor shape).
8
+ describe ".new with a context" do
9
+ it "builds a document from XML content" do
10
+ context = Moxml::Context.new
11
+ builder = described_class.new(context)
12
+ doc = builder.build(<<~XML)
13
+ <person xmlns="http://example.com/test" id="123">
14
+ <name>John Doe</name>
15
+ </person>
16
+ XML
17
+
18
+ expect(doc).to be_a(Moxml::Document)
19
+ expect(doc.root.name).to eq("person")
20
+ expect(doc.root["id"]).to eq("123")
21
+ expect(doc.root.children.select(&:element?).map(&:name)).to eq(["name"])
22
+ end
23
+
24
+ it "exposes the context it was created with" do
25
+ context = Moxml::Context.new
26
+ builder = described_class.new(context)
27
+
28
+ expect(builder.context).to equal(context)
29
+ end
30
+
31
+ it "accepts an explicit adapter, matching Context.new(adapter)" do
32
+ context = Moxml::Context.new(:nokogiri)
33
+ doc = described_class.new(context).build("<root><child/></root>")
34
+
35
+ expect(doc.context.config.adapter_name).to eq(:nokogiri)
36
+ expect(doc.root.children.select(&:element?).map(&:name)).to eq(["child"])
37
+ end
38
+ end
39
+ end
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.5.3
4
+ version: 0.5.5
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-08-26 00:00:00.000000000 Z
11
+ date: 2026-08-27 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
@@ -186,6 +186,7 @@ files:
186
186
  - lib/moxml/declaration.rb
187
187
  - lib/moxml/doctype.rb
188
188
  - lib/moxml/document.rb
189
+ - lib/moxml/document_builder.rb
189
190
  - lib/moxml/element.rb
190
191
  - lib/moxml/entity.rb
191
192
  - lib/moxml/entity/reference.rb
@@ -443,6 +444,7 @@ files:
443
444
  - spec/moxml/declaration_preservation_spec.rb
444
445
  - spec/moxml/declaration_spec.rb
445
446
  - spec/moxml/doctype_spec.rb
447
+ - spec/moxml/document_builder_spec.rb
446
448
  - spec/moxml/document_spec.rb
447
449
  - spec/moxml/element_spec.rb
448
450
  - spec/moxml/entity_preservation_spec.rb