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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd5c99b547004d2f6521f882b006da2b612ee74cabf92775a3b061d9f096bc20
4
- data.tar.gz: 652bce88c043068ecd97ef7008044557f764661a2e792f1128d435d4e83d715a
3
+ metadata.gz: 3b15f3ffeac134fbaa674a8e2970ae5606dd1e08985f288d01c48323ffeca4f1
4
+ data.tar.gz: 43f616e34f485d21bbc431cd42da7347e404647a71e0e84af697e205e24067a1
5
5
  SHA512:
6
- metadata.gz: ba8297809db361e15cac3cbfbc132aa5545ceee6fe4e3d8b7df9a6250e7585ce930eaf7ba7d15de155975848f0d2deb205dcf8b0ebbdd3e00d96cbfc8d758633
7
- data.tar.gz: 982406a0ff5f858aeed1569fd4ca1d4536bbb2b9841d791b4f560475b19b77641db0831fac7efd6760c3493216a9db8635de55d5e146f158cc0f16297fc97895
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 if attachments.get(doc, :declaration) ||
145
- attachments.get(doc, :doctype) ||
146
- attachments.get(doc, :document_text) ||
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
- root_seen = false
154
- doc.children.each do |child|
155
- if child.is_a?(::Leptris::XML::Element)
156
- return nil if root_seen
157
-
158
- root_seen = true
159
- elsif root_seen
160
- # Epilog parts glue directly to the root in the engine's
161
- # document output — compose those.
162
- return nil
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
 
@@ -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
@@ -43,6 +43,13 @@ module Moxml
43
43
  end
44
44
  end
45
45
 
46
+ def none_set?(native, keys)
47
+ h = @data[native.object_id]
48
+ return true if h.nil?
49
+
50
+ keys.none? { |key| h.key?(key) }
51
+ end
52
+
46
53
  def key?(native, key)
47
54
  h = @data[native.object_id]
48
55
  !h.nil? && h.key?(key)
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.44"
4
+ VERSION = "0.5.46"
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.44
4
+ version: 0.5.46
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.