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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f552fee96842dca761f5e88ba1f5074e6cfdaef32411b012e2bf30a6736a16c4
4
- data.tar.gz: 68c734aa63688134b897a1e29e8abfa292a02534daefd278b727122ad952e4e3
3
+ metadata.gz: 83f4ac58d2c22de225ec143ff3e78eb95e3460863b8fb1d67e8230a872bfad2a
4
+ data.tar.gz: c7401f4154a32e534a1c6a53f067a25406b16056de651eec8e768ac04c602dcc
5
5
  SHA512:
6
- metadata.gz: ff775d10b05c5e60fcbf59c66e49284b841e4148b7da7766393b351b47bb626697a34bb5f30cf0d697d8a578ed07c1a9b8b7987507364c436c7c274be380843f
7
- data.tar.gz: 7bbaef0118ea13de356dc95041dca057d59373f96eec6a4621a38d1cce07b283ca682b3fd2db77eea34f8d9252aaca821cf2651cbbbebbf0e7e171dd0d51f0f5
6
+ metadata.gz: d0ad7a04cbac82f8195be3be4318ee7d381974a812c7c2fce45110778875abfcb0be7ebb0c8e19339ddf2c2d8d9f4bd575254062b492a9cf4ad35072d8fe67fd
7
+ data.tar.gz: 301188cc44df6af9339502db3694651cc2a69238e3c222f6c0cda92393e72b80d5353cca335671be6b6560d5d64da4c32ad3bc4484f91d12239ddebf677e6aec
data/Gemfile CHANGED
@@ -9,7 +9,7 @@ gemspec
9
9
  gem "benchmark"
10
10
  gem "benchmark-ips"
11
11
  gem "get_process_mem"
12
- gem "leptris", "~> 1.9.174"
12
+ gem "leptris", "~> 1.9.174.1"
13
13
  gem "libxml-ruby", "~> 5.0"
14
14
  gem "nokogiri", "~> 1.18"
15
15
  gem "openssl", "~> 3.0"
@@ -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
@@ -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
- result = ::Ox.parse(processed_xml)
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
@@ -233,6 +233,8 @@ module Moxml
233
233
  encoding: config.default_encoding,
234
234
  strict: config.strict_parsing,
235
235
  indent: config.default_indent,
236
+ ox_skip: config.ox_skip,
237
+ ox_mode: config.ox_mode,
236
238
  }
237
239
  end
238
240
  end
data/lib/moxml/element.rb CHANGED
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- class Element < Node
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
- class Node
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.45"
4
+ VERSION = "0.5.47"
5
5
  end
@@ -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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.45
4
+ version: 0.5.47
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.