moxml 0.5.44 → 0.5.46
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/docs/_pages/adapters/ox.adoc +15 -0
- data/lib/moxml/adapter/leptris/document_parts.rb +30 -14
- data/lib/moxml/adapter/ox.rb +9 -1
- data/lib/moxml/config.rb +39 -1
- data/lib/moxml/context.rb +2 -0
- data/lib/moxml/native_attachment/native.rb +7 -0
- data/lib/moxml/native_attachment/opal.rb +4 -0
- data/lib/moxml/native_attachment.rb +6 -0
- data/lib/moxml/version.rb +1 -1
- data/spec/integration/shared_examples/edge_cases.rb +7 -0
- data/spec/moxml/adapter/ox_spec.rb +41 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3b15f3ffeac134fbaa674a8e2970ae5606dd1e08985f288d01c48323ffeca4f1
|
|
4
|
+
data.tar.gz: 43f616e34f485d21bbc431cd42da7347e404647a71e0e84af697e205e24067a1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7007190eea32272f8be0de72b5a11f9b4d66cc0108cff530986443012a46814a886b34f925649593139c3d0cd7d4bf13d91d4088ae19be71ad150cc8464436de
|
|
7
|
+
data.tar.gz: f967159613aa7c650d3d58c05b1dc7cb5376d975c01496e69aabc6f3c8d5631fcd9bb8dcb3c05320b3e345cf44fdd5cce922da0ca38079c4d7c322e39fb9dd72
|
|
@@ -17,6 +17,21 @@ Ox is the fastest XML parser available for Ruby, providing excellent performance
|
|
|
17
17
|
* Memory-constrained environments
|
|
18
18
|
* When XPath usage is minimal
|
|
19
19
|
|
|
20
|
+
=== Parse options (issue #189)
|
|
21
|
+
|
|
22
|
+
The adapter parses with `Ox.load` (not `Ox.parse`, which hardcodes `skip: :skip_white` and collapses whitespace runs inside text content). Two knobs flow through the parse options:
|
|
23
|
+
|
|
24
|
+
* `ox_skip` (default `Moxml::Config::OX_DEFAULT_SKIP`, `:skip_none`) — Ox's whitespace policy. `:skip_none` keeps every character of text content, so `xml:space="preserve"` documents round-trip intact; `:skip_white` restores the collapsing behavior. Inter-element indentation is dropped by Ox's generic mode under every skip setting, so child lists are identical either way.
|
|
25
|
+
* `ox_mode` (default `:generic`) — Ox's load mode. Only `:generic` yields the tree the adapter walks; `:hash` and `:object` fail loudly rather than corrupt.
|
|
26
|
+
|
|
27
|
+
Set the policy once per context or per parse:
|
|
28
|
+
|
|
29
|
+
[source,ruby]
|
|
30
|
+
----
|
|
31
|
+
ctx = Moxml.new(:ox) { |c| c.ox_skip = :skip_white } # context default
|
|
32
|
+
doc = ctx.parse(xml, ox_skip: :skip_none) # per-parse wins
|
|
33
|
+
----
|
|
34
|
+
|
|
20
35
|
== Specific adapter limitations
|
|
21
36
|
|
|
22
37
|
=== Ox adapter
|
|
@@ -139,27 +139,43 @@ module Moxml
|
|
|
139
139
|
# facade's canonical form (checked post-hoc: a source
|
|
140
140
|
# declaration carrying standalone or another version would
|
|
141
141
|
# diverge).
|
|
142
|
+
# 1.9.174 attached leptris_document_first_child (libleptris
|
|
143
|
+
# 1.9.174): pointer probe for the single-child document
|
|
144
|
+
# shape without wrapping the child chain.
|
|
145
|
+
DOCUMENT_CHILD_PTRS =
|
|
146
|
+
::Leptris::XML::FFI.respond_to?(:leptris_document_first_child)
|
|
147
|
+
|
|
142
148
|
def fast_document_output(doc, options)
|
|
143
149
|
return nil unless LIBXML2_LAYOUT_PARITY
|
|
144
|
-
return nil
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
attachments.get(doc, :entity_markers)
|
|
150
|
+
return nil unless attachments.none_set?(
|
|
151
|
+
doc, %i[declaration doctype document_text entity_markers]
|
|
152
|
+
)
|
|
148
153
|
|
|
149
154
|
include_decl = !options[:no_declaration] && options.fetch(:declaration) do
|
|
150
155
|
document_has_declaration?(doc)
|
|
151
156
|
end
|
|
152
157
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
# Exactly one document child — the root — is the common
|
|
159
|
+
# parsed shape; pointer probes decide it without wrapping
|
|
160
|
+
# the child chain. Anything else (prolog/epilog parts,
|
|
161
|
+
# multi-root) falls back to the scan.
|
|
162
|
+
root = doc.root
|
|
163
|
+
single_child = DOCUMENT_CHILD_PTRS && root &&
|
|
164
|
+
::Leptris::XML::FFI.leptris_document_first_child(doc.c_ptr)
|
|
165
|
+
.address == root.c_ptr.address &&
|
|
166
|
+
root.next_sibling.nil?
|
|
167
|
+
unless single_child
|
|
168
|
+
root_seen = false
|
|
169
|
+
doc.children.each do |child|
|
|
170
|
+
if child.is_a?(::Leptris::XML::Element)
|
|
171
|
+
return nil if root_seen
|
|
172
|
+
|
|
173
|
+
root_seen = true
|
|
174
|
+
elsif root_seen
|
|
175
|
+
# Epilog parts glue directly to the root in the engine's
|
|
176
|
+
# document output — compose those.
|
|
177
|
+
return nil
|
|
178
|
+
end
|
|
163
179
|
end
|
|
164
180
|
end
|
|
165
181
|
|
data/lib/moxml/adapter/ox.rb
CHANGED
|
@@ -39,7 +39,15 @@ module Moxml
|
|
|
39
39
|
def parse(xml, options = {}, _context = nil)
|
|
40
40
|
processed_xml = preprocess_entities(xml)
|
|
41
41
|
native_doc = begin
|
|
42
|
-
|
|
42
|
+
# ::Ox.parse has arity 1 and hardcodes skip: :skip_white;
|
|
43
|
+
# ::Ox.load takes the policy (issue #189). Both knobs flow
|
|
44
|
+
# from Config through Context#default_options; the fetch
|
|
45
|
+
# defaults keep direct adapter calls context-free.
|
|
46
|
+
result = ::Ox.load(
|
|
47
|
+
processed_xml,
|
|
48
|
+
skip: options.fetch(:ox_skip, Moxml::Config::OX_DEFAULT_SKIP),
|
|
49
|
+
mode: options.fetch(:ox_mode, Moxml::Config::OX_DEFAULT_MODE),
|
|
50
|
+
)
|
|
43
51
|
|
|
44
52
|
# result can be either Document or Element
|
|
45
53
|
if result.is_a?(::Ox::Document)
|
data/lib/moxml/config.rb
CHANGED
|
@@ -93,6 +93,18 @@ module Moxml
|
|
|
93
93
|
# - :strict — only restore DTD-declared entities (falls back to lenient until DTD parsing is implemented)
|
|
94
94
|
ENTITY_RESTORATION_MODES = %i[strict lenient].freeze
|
|
95
95
|
|
|
96
|
+
# Ox whitespace policy (issue #189): ::Ox.parse hardcodes
|
|
97
|
+
# skip: :skip_white, which collapses whitespace runs inside text
|
|
98
|
+
# content — xml:space="preserve" documents round-trip mangled.
|
|
99
|
+
# The adapter parses with ::Ox.load instead; these defaults keep
|
|
100
|
+
# every character of text content (inter-element indentation is
|
|
101
|
+
# dropped by Ox's generic mode under every skip setting, so the
|
|
102
|
+
# child lists stay identical to the old parse).
|
|
103
|
+
OX_DEFAULT_SKIP = :skip_none
|
|
104
|
+
OX_DEFAULT_MODE = :generic
|
|
105
|
+
OX_VALID_SKIPS = %i[skip_none skip_white skip_off].freeze
|
|
106
|
+
OX_VALID_MODES = %i[generic hash object].freeze
|
|
107
|
+
|
|
96
108
|
attr_reader :adapter_name, :default_line_ending
|
|
97
109
|
attr_accessor :strict_parsing,
|
|
98
110
|
:default_encoding,
|
|
@@ -102,7 +114,27 @@ module Moxml
|
|
|
102
114
|
:entity_load_mode,
|
|
103
115
|
:entity_provider,
|
|
104
116
|
:namespace_validation_mode,
|
|
105
|
-
:entity_restoration_mode
|
|
117
|
+
:entity_restoration_mode,
|
|
118
|
+
:ox_skip,
|
|
119
|
+
:ox_mode
|
|
120
|
+
|
|
121
|
+
def ox_skip=(value)
|
|
122
|
+
unless OX_VALID_SKIPS.include?(value)
|
|
123
|
+
raise ArgumentError,
|
|
124
|
+
"Invalid ox_skip: #{value.inspect}. Must be one of #{OX_VALID_SKIPS.inspect}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
@ox_skip = value
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def ox_mode=(value)
|
|
131
|
+
unless OX_VALID_MODES.include?(value)
|
|
132
|
+
raise ArgumentError,
|
|
133
|
+
"Invalid ox_mode: #{value.inspect}. Must be one of #{OX_VALID_MODES.inspect}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
@ox_mode = value
|
|
137
|
+
end
|
|
106
138
|
|
|
107
139
|
def default_line_ending=(value)
|
|
108
140
|
unless VALID_LINE_ENDINGS.include?(value)
|
|
@@ -118,6 +150,12 @@ module Moxml
|
|
|
118
150
|
default_encoding = nil)
|
|
119
151
|
self.adapter = adapter_name || Config.default.adapter_name
|
|
120
152
|
@strict_parsing = strict_parsing || Config.default.strict_parsing
|
|
153
|
+
# Constant-initialized (not Config.default fallback): the
|
|
154
|
+
# default Config constructs via Config.default — a fallback
|
|
155
|
+
# read there recurses. Global-default propagation for these
|
|
156
|
+
# knobs is not part of the contract; per-context is (#189).
|
|
157
|
+
@ox_skip = OX_DEFAULT_SKIP
|
|
158
|
+
@ox_mode = OX_DEFAULT_MODE
|
|
121
159
|
@default_encoding = default_encoding || Config.default.default_encoding
|
|
122
160
|
@default_indent = 2
|
|
123
161
|
@default_line_ending = LINE_ENDING_LF
|
data/lib/moxml/context.rb
CHANGED
|
@@ -13,6 +13,10 @@ module Moxml
|
|
|
13
13
|
native.instance_variable_set(attachment_ivar_name(key), value)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
def none_set?(native, keys)
|
|
17
|
+
keys.none? { |key| native.instance_variable_defined?(attachment_ivar_name(key)) }
|
|
18
|
+
end
|
|
19
|
+
|
|
16
20
|
def key?(native, key)
|
|
17
21
|
native.instance_variable_defined?(attachment_ivar_name(key))
|
|
18
22
|
end
|
|
@@ -20,6 +20,12 @@ module Moxml
|
|
|
20
20
|
@backend.get(native, key)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# One probe for "none of these keys are set" — the document
|
|
24
|
+
# serialize gate asks about four keys per call.
|
|
25
|
+
def none_set?(native, keys)
|
|
26
|
+
@backend.none_set?(native, keys)
|
|
27
|
+
end
|
|
28
|
+
|
|
23
29
|
def set(native, key, value)
|
|
24
30
|
@backend.set(native, key, value)
|
|
25
31
|
end
|
data/lib/moxml/version.rb
CHANGED
|
@@ -157,6 +157,13 @@ RSpec.shared_examples "Moxml Edge Cases" do
|
|
|
157
157
|
end
|
|
158
158
|
end
|
|
159
159
|
|
|
160
|
+
describe "text content whitespace preservation" do
|
|
161
|
+
it "preserves whitespace runs inside text content (xml:space, issue #189)" do
|
|
162
|
+
doc = context.parse(%(<r xml:space="preserve"><t> spaced </t></r>))
|
|
163
|
+
expect(doc.root.children.first.text).to eq(" spaced ")
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
160
167
|
describe "whitespace text node preservation" do
|
|
161
168
|
# Ox/HeadedOx do not generate whitespace-only text nodes in their parser,
|
|
162
169
|
# so these tests only apply to adapters that do (Nokogiri, OGA, REXML, LibXML)
|
|
@@ -68,5 +68,46 @@ RSpec.describe Moxml::Adapter::Ox do
|
|
|
68
68
|
count = described_class.xpath(doc, "count(//child)")
|
|
69
69
|
expect(count).to eq(2)
|
|
70
70
|
end
|
|
71
|
+
|
|
72
|
+
describe "parse whitespace policy" do
|
|
73
|
+
it "preserves whitespace runs inside text content by default (issue #189)" do
|
|
74
|
+
doc = described_class.parse(%(<r xml:space="preserve"><t> spaced </t></r>))
|
|
75
|
+
expect(doc.root.children.first.text).to eq(" spaced ")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "collapses runs with a per-parse ox_skip: :skip_white" do
|
|
79
|
+
doc = described_class.parse(%(<r><t> spaced </t></r>), ox_skip: :skip_white)
|
|
80
|
+
expect(doc.root.children.first.text).to eq(" spaced ")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "honors a context default set once on the config" do
|
|
84
|
+
context = Moxml.new(:ox) { |c| c.ox_skip = :skip_white }
|
|
85
|
+
doc = context.parse(%(<r><t> s </t></r>))
|
|
86
|
+
expect(doc.root.children.first.text).to eq(" s ")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it "lets a per-parse option beat the context default" do
|
|
90
|
+
context = Moxml.new(:ox) { |c| c.ox_skip = :skip_white }
|
|
91
|
+
doc = context.parse(%(<r><t> s </t></r>), ox_skip: :skip_none)
|
|
92
|
+
expect(doc.root.children.first.text).to eq(" s ")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "fails loudly on ox_mode: :hash (the adapter walks a tree, not a Hash)" do
|
|
96
|
+
expect { described_class.parse("<r><t> x </t></r>", ox_mode: :hash) }
|
|
97
|
+
.to raise_error(RuntimeError, /must be a String or Ox::Node/)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "surfaces an invalid ox_skip as Moxml::ParseError" do
|
|
101
|
+
expect { described_class.parse("<r/>", ox_skip: :bogus) }
|
|
102
|
+
.to raise_error(Moxml::ParseError)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "validates Config ox_skip/ox_mode assignments" do
|
|
106
|
+
expect { Moxml::Config.new.ox_skip = :bogus }
|
|
107
|
+
.to raise_error(ArgumentError, /Invalid ox_skip/)
|
|
108
|
+
expect { Moxml::Config.new.ox_mode = :bogus }
|
|
109
|
+
.to raise_error(ArgumentError, /Invalid ox_mode/)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
71
112
|
end
|
|
72
113
|
end
|