moxml 0.5.45 → 0.5.47
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/Gemfile +1 -1
- data/docs/_pages/adapters/ox.adoc +15 -0
- 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/element.rb +8 -1
- data/lib/moxml/node.rb +44 -38
- 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: 83f4ac58d2c22de225ec143ff3e78eb95e3460863b8fb1d67e8230a872bfad2a
|
|
4
|
+
data.tar.gz: c7401f4154a32e534a1c6a53f067a25406b16056de651eec8e768ac04c602dcc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d0ad7a04cbac82f8195be3be4318ee7d381974a812c7c2fce45110778875abfcb0be7ebb0c8e19339ddf2c2d8d9f4bd575254062b492a9cf4ad35072d8fe67fd
|
|
7
|
+
data.tar.gz: 301188cc44df6af9339502db3694651cc2a69238e3c222f6c0cda92393e72b80d5353cca335671be6b6560d5d64da4c32ad3bc4484f91d12239ddebf677e6aec
|
data/Gemfile
CHANGED
|
@@ -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
|
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
data/lib/moxml/element.rb
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Moxml
|
|
4
|
-
|
|
4
|
+
# Instance behavior for Moxml::Element, extracted so the leptris
|
|
5
|
+
# adapter can extend natives with it in place (issue #230); the
|
|
6
|
+
# class remains the consumer-facing contract.
|
|
7
|
+
module ElementBehavior
|
|
5
8
|
def name
|
|
6
9
|
# The adapter read is an FFI call plus (on several adapters) a
|
|
7
10
|
# defensive string copy; names only change through name= and
|
|
@@ -359,4 +362,8 @@ module Moxml
|
|
|
359
362
|
context.bump_namespace_scope_generation
|
|
360
363
|
end
|
|
361
364
|
end
|
|
365
|
+
|
|
366
|
+
class Element < Node
|
|
367
|
+
include ElementBehavior
|
|
368
|
+
end
|
|
362
369
|
end
|
data/lib/moxml/node.rb
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Moxml
|
|
4
|
-
|
|
4
|
+
# Instance behavior for Moxml::Node, extracted so the leptris
|
|
5
|
+
# adapter can extend natives with it in place (issue #230); the
|
|
6
|
+
# class remains the consumer-facing contract and the wrap factory.
|
|
7
|
+
module NodeBehavior
|
|
5
8
|
include XmlUtils
|
|
6
9
|
include Enumerable
|
|
7
10
|
|
|
@@ -441,37 +444,6 @@ module Moxml
|
|
|
441
444
|
nil
|
|
442
445
|
end
|
|
443
446
|
|
|
444
|
-
# Registry mapping node type symbols to wrapper classes.
|
|
445
|
-
# Built lazily to avoid load-order issues with subclasses.
|
|
446
|
-
def self.node_type_map
|
|
447
|
-
@node_type_map ||= {
|
|
448
|
-
element: Element,
|
|
449
|
-
text: Text,
|
|
450
|
-
cdata: Cdata,
|
|
451
|
-
comment: Comment,
|
|
452
|
-
processing_instruction: ProcessingInstruction,
|
|
453
|
-
document: Document,
|
|
454
|
-
declaration: Declaration,
|
|
455
|
-
doctype: Doctype,
|
|
456
|
-
attribute: Attribute,
|
|
457
|
-
entity_reference: EntityReference,
|
|
458
|
-
}.freeze
|
|
459
|
-
end
|
|
460
|
-
|
|
461
|
-
def self.wrap(node, context)
|
|
462
|
-
return nil if node.nil?
|
|
463
|
-
|
|
464
|
-
cached = context.wrapper_for(node)
|
|
465
|
-
return cached if cached
|
|
466
|
-
|
|
467
|
-
adapter = adapter(context)
|
|
468
|
-
type = adapter.node_type(node)
|
|
469
|
-
klass = node_type_map[type] || self
|
|
470
|
-
|
|
471
|
-
klass.new(node, context, adapter, type)
|
|
472
|
-
.tap { |wrapper| context.register_wrapper(node, wrapper) }
|
|
473
|
-
end
|
|
474
|
-
|
|
475
447
|
# Internal: Set the parent node for cache invalidation tracking.
|
|
476
448
|
# Called by NodeSet, Document, Element when establishing parent-child
|
|
477
449
|
# relationships. Public to allow cross-class usage within Moxml internals.
|
|
@@ -485,12 +457,6 @@ module Moxml
|
|
|
485
457
|
|
|
486
458
|
protected
|
|
487
459
|
|
|
488
|
-
def self.adapter(context)
|
|
489
|
-
context.config.adapter
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
# Invalidate cached children. Called by mutation methods
|
|
493
|
-
# and by Element attribute/namespace caches.
|
|
494
460
|
def invalidate_children_cache!
|
|
495
461
|
@children = nil
|
|
496
462
|
end
|
|
@@ -545,4 +511,44 @@ module Moxml
|
|
|
545
511
|
xml.gsub(/\r?\n/, line_ending)
|
|
546
512
|
end
|
|
547
513
|
end
|
|
514
|
+
|
|
515
|
+
class Node
|
|
516
|
+
include NodeBehavior
|
|
517
|
+
|
|
518
|
+
def self.node_type_map
|
|
519
|
+
@node_type_map ||= {
|
|
520
|
+
element: Element,
|
|
521
|
+
text: Text,
|
|
522
|
+
cdata: Cdata,
|
|
523
|
+
comment: Comment,
|
|
524
|
+
processing_instruction: ProcessingInstruction,
|
|
525
|
+
document: Document,
|
|
526
|
+
declaration: Declaration,
|
|
527
|
+
doctype: Doctype,
|
|
528
|
+
attribute: Attribute,
|
|
529
|
+
entity_reference: EntityReference,
|
|
530
|
+
}.freeze
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def self.wrap(node, context)
|
|
534
|
+
return nil if node.nil?
|
|
535
|
+
|
|
536
|
+
cached = context.wrapper_for(node)
|
|
537
|
+
return cached if cached
|
|
538
|
+
|
|
539
|
+
adapter = adapter(context)
|
|
540
|
+
type = adapter.node_type(node)
|
|
541
|
+
klass = node_type_map[type] || self
|
|
542
|
+
|
|
543
|
+
klass.new(node, context, adapter, type)
|
|
544
|
+
.tap { |wrapper| context.register_wrapper(node, wrapper) }
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def self.adapter(context)
|
|
548
|
+
context.config.adapter
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# Invalidate cached children. Called by mutation methods
|
|
552
|
+
# and by Element attribute/namespace caches.
|
|
553
|
+
end
|
|
548
554
|
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
|