moxml 0.5.3 → 0.5.4

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: e9c1c880ebd136229eb81109ecbfc504a27b9bc101805688231525587a1601e8
4
+ data.tar.gz: c09ed1d4c857d7a78006f4c2ade2bcb662bac233578858e7aeb0ca0b0d2f09f7
5
5
  SHA512:
6
- metadata.gz: 67da3161b095e47aafe6d7e7327c2125a4458a9ef557c41c5f9d6465fccb01374d29c409178910b39426cf0cf7a42140d6858d4177c3f63b529690f397303796
7
- data.tar.gz: c88f55e02c026e18f4c1a25578d9e27094835288e6ccc0dccff40633ff31029975125d9fcfb51ae5353f00ad5fa54fd8bef135d9e44c64da794b45513f728903
6
+ metadata.gz: d334d7e9789817b8891a78b77cf2bbaab05af83252a432cdc4bffb6728c37938a9f430098561ab8a8fa2db77f9ec0f089fc563922a1ceab046f26e7b2a1e01ed
7
+ data.tar.gz: 1feb278e326095475c68bea001bd68f3ac76e8423fdf021bc3a54b765b4e9069ec5cf403c282687fb3843b6ca6411868d9208e6a50a5ea071ca5e443485358f0
@@ -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,14 @@ namespace_validation_mode: :strict)
183
173
  node
184
174
  end
185
175
 
176
+ # Whether the subtree at native can contain entity markers.
177
+ # Marker-tracking adapters override this so the post-serialize
178
+ # restore can skip its full-output scans on marker-free
179
+ # documents; the default stays conservative.
180
+ def entity_bearing?(_native)
181
+ true
182
+ end
183
+
186
184
  # Check if the native document has an XML declaration
187
185
  # @param native_doc the native document object
188
186
  # @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
@@ -257,7 +273,17 @@ module Moxml
257
273
  CustomizedLeptris::EntityReference, CustomizedLeptris::TextSegment
258
274
  []
259
275
  else
260
- split_entity_markers(node.children.to_a, node)
276
+ natives = node.children.to_a
277
+ # Parse records whether the preprocessed source held any
278
+ # entity markers, and the ER builder path flips the flag
279
+ # when it mints one. A false flag lets traversal skip the
280
+ # marker split — including the per-text content fetch that
281
+ # dominates cold children cost. Cross-document moves of
282
+ # marker-bearing text into an entity-free document degrade
283
+ # to literal text.
284
+ return natives if attachments.get(node.document, :entity_markers) == false
285
+
286
+ split_entity_markers(natives, node)
261
287
  end
262
288
  end
263
289
 
@@ -385,6 +411,7 @@ module Moxml
385
411
  if child.is_a?(CustomizedLeptris::EntityReference)
386
412
  marker = parent.document.create_text_node("#{Entity::MARKER}#{child.name};")
387
413
  parent.add_child(marker)
414
+ attachments.set(parent.document, :entity_markers, true)
388
415
  return child
389
416
  end
390
417
  child = parent.document.create_text_node(child) if child.is_a?(String)
@@ -698,9 +725,10 @@ module Moxml
698
725
  end
699
726
 
700
727
  def serialize(node, options = {})
701
- xml = raw_serialize(node, options)
702
- xml = restore_entities(xml)
703
- normalize_serialization(xml, options)
728
+ # Entity restoration belongs to the wrapper layer
729
+ # (Node#to_xml runs adapter.restore_entities for every
730
+ # adapter); doing it here scanned the output a second time.
731
+ normalize_serialization(raw_serialize(node, options), options)
704
732
  end
705
733
 
706
734
  def raw_serialize(node, options)
@@ -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
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
@@ -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.4"
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.4
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