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 +4 -4
- data/lib/moxml/adapter/base.rb +16 -10
- data/lib/moxml/adapter/leptris.rb +38 -5
- data/lib/moxml/adapter/libxml.rb +4 -0
- data/lib/moxml/adapter/oga.rb +1 -1
- data/lib/moxml/adapter/ox.rb +4 -0
- data/lib/moxml/config.rb +18 -2
- data/lib/moxml/context.rb +3 -1
- data/lib/moxml/document_builder.rb +22 -0
- data/lib/moxml/node.rb +9 -7
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml.rb +1 -0
- data/spec/moxml/adapter/leptris_spec.rb +35 -0
- data/spec/moxml/document_builder_spec.rb +39 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0cc1ee5cd198e8016cd259e62eec132ef73863dbb91125cca55349c8847df01c
|
|
4
|
+
data.tar.gz: 9b3db2921b1acaebaaeccfa6dbba82c78184443c6338e79952e334da0a837b0c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a4ccdd2949f15fedf7bee4bc2c41b9340250ec58d2d4583e7be7a0a14c397749e034f1ec3e15193c7a2ad93d43de7126338fecf2142ad1180ea50b3de25a97a
|
|
7
|
+
data.tar.gz: 9e4abee61c92cb0a3390804af398e6baaf593107e8bbc9b33b10d5bc40675f9f0b92ef5ded27f0b8948edd8c930b5188496c4c644529a277d8dd71b49f3bae57
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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)
|
data/lib/moxml/adapter/libxml.rb
CHANGED
data/lib/moxml/adapter/oga.rb
CHANGED
data/lib/moxml/adapter/ox.rb
CHANGED
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.
|
|
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
|
|
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
|
-
|
|
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 ||=
|
|
42
|
-
|
|
43
|
-
|
|
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.
|
|
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
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 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.
|
|
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-
|
|
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
|