moxml 0.1.14 → 0.1.16

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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +117 -66
  3. data/Gemfile +1 -0
  4. data/README.adoc +11 -9
  5. data/Rakefile +34 -1
  6. data/TODO.remaining/1-entity-reference-adapter-support.md +157 -0
  7. data/TODO.remaining/2-entity-restoration-model-driven.md +169 -0
  8. data/TODO.remaining/3-entity-reference-test-coverage.md +170 -0
  9. data/TODO.remaining/4-lenient-entities-mode.md +106 -0
  10. data/TODO.remaining/5-fixture-integrity.md +65 -0
  11. data/TODO.remaining/6-ox-element-ordering-bug.md +36 -0
  12. data/TODO.remaining/7-headed-ox-limitations.md +95 -0
  13. data/TODO.remaining/8-xpath-predicate-gaps.md +68 -0
  14. data/TODO.remaining/9-cleanup-hygiene.md +42 -0
  15. data/TODO.remaining/README.md +54 -0
  16. data/benchmarks/generate_report.rb +1 -1
  17. data/docs/_pages/configuration.adoc +22 -19
  18. data/docs/_tutorials/namespace-handling.adoc +5 -5
  19. data/lib/moxml/adapter/base.rb +22 -3
  20. data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -1
  21. data/lib/moxml/adapter/customized_libxml/entity_reference.rb +23 -0
  22. data/lib/moxml/adapter/customized_libxml.rb +18 -0
  23. data/lib/moxml/adapter/customized_oga.rb +10 -0
  24. data/lib/moxml/adapter/customized_ox/entity_reference.rb +25 -0
  25. data/lib/moxml/adapter/customized_ox.rb +12 -0
  26. data/lib/moxml/adapter/customized_rexml/entity_reference.rb +19 -0
  27. data/lib/moxml/adapter/customized_rexml/formatter.rb +44 -20
  28. data/lib/moxml/adapter/customized_rexml.rb +11 -0
  29. data/lib/moxml/adapter/headed_ox.rb +37 -14
  30. data/lib/moxml/adapter/libxml.rb +233 -119
  31. data/lib/moxml/adapter/nokogiri.rb +22 -11
  32. data/lib/moxml/adapter/oga.rb +64 -25
  33. data/lib/moxml/adapter/ox.rb +198 -42
  34. data/lib/moxml/adapter/rexml.rb +64 -13
  35. data/lib/moxml/attribute.rb +3 -0
  36. data/lib/moxml/builder.rb +78 -24
  37. data/lib/moxml/config.rb +24 -7
  38. data/lib/moxml/declaration.rb +4 -2
  39. data/lib/moxml/document.rb +8 -1
  40. data/lib/moxml/document_builder.rb +44 -37
  41. data/lib/moxml/element.rb +18 -5
  42. data/lib/moxml/entity_registry.rb +51 -1
  43. data/lib/moxml/native_attachment.rb +65 -0
  44. data/lib/moxml/node.rb +39 -50
  45. data/lib/moxml/node_set.rb +43 -15
  46. data/lib/moxml/version.rb +1 -1
  47. data/lib/moxml/xml_utils.rb +1 -1
  48. data/lib/moxml/xpath/compiler.rb +4 -1
  49. data/lib/moxml.rb +1 -0
  50. data/scripts/format_xml.rb +16 -0
  51. data/scripts/pretty_format_xml.rb +14 -0
  52. data/spec/consistency/round_trip_spec.rb +3 -30
  53. data/spec/integration/all_adapters_spec.rb +1 -0
  54. data/spec/integration/headed_ox_integration_spec.rb +0 -2
  55. data/spec/integration/shared_examples/edge_cases.rb +7 -4
  56. data/spec/integration/shared_examples/integration_workflows.rb +3 -3
  57. data/spec/integration/shared_examples/node_wrappers/cdata_behavior.rb +1 -1
  58. data/spec/integration/shared_examples/node_wrappers/entity_reference_behavior.rb +224 -0
  59. data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +1 -1
  60. data/spec/moxml/adapter/headed_ox_spec.rb +8 -8
  61. data/spec/moxml/adapter/oga_spec.rb +46 -0
  62. data/spec/moxml/adapter/shared_examples/adapter_contract.rb +1 -12
  63. data/spec/moxml/allocation_benchmark_spec.rb +96 -0
  64. data/spec/moxml/allocation_guard_spec.rb +282 -0
  65. data/spec/moxml/builder_spec.rb +256 -0
  66. data/spec/moxml/config_spec.rb +11 -11
  67. data/spec/moxml/doctype_spec.rb +41 -0
  68. data/spec/moxml/lazy_parse_spec.rb +115 -0
  69. data/spec/moxml/namespace_uri_validation_spec.rb +11 -3
  70. data/spec/moxml/node_cache_spec.rb +110 -0
  71. data/spec/moxml/node_set_cache_spec.rb +90 -0
  72. data/spec/moxml/xml_utils_spec.rb +32 -0
  73. data/spec/moxml/xpath/axes_spec.rb +1 -1
  74. data/spec/moxml/xpath/compiler_spec.rb +2 -2
  75. data/spec/moxml/xpath/functions/position_functions_spec.rb +5 -5
  76. data/spec/moxml/xpath/functions/special_functions_spec.rb +1 -1
  77. data/spec/performance/memory_usage_spec.rb +0 -4
  78. data/spec/support/allocation_helper.rb +165 -0
  79. data/spec/support/w3c_namespace_helpers.rb +2 -1
  80. metadata +29 -2
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "support/allocation_helper"
5
+
6
+ # Allocation guard specs — these run in CI by default (no :performance tag).
7
+ #
8
+ # These specs enforce allocation budgets per adapter per operation.
9
+ # If an adapter exceeds its threshold, the spec fails with a diagnostic
10
+ # message showing the actual vs expected allocation count.
11
+ #
12
+ # Thresholds are in AllocationHelper::THRESHOLDS and should be tightened
13
+ # as optimizations are confirmed.
14
+ RSpec.describe "Allocation guards", order: :defined do
15
+ def threshold_for(adapter_name, operation)
16
+ AllocationHelper.threshold(adapter_name, operation)
17
+ end
18
+
19
+ def guard_allocations(adapter_name, operation, &block)
20
+ allocs = AllocationHelper.count_allocations(&block)
21
+ limit = threshold_for(adapter_name, operation)
22
+
23
+ if allocs > limit
24
+ # Generate StackProf diagnostic on failure
25
+ profile = AllocationHelper.profile_allocations(&block)
26
+ raise(<<~MSG)
27
+ #{adapter_name}/#{operation}: #{allocs} allocations exceeds limit of #{limit}
28
+
29
+ #{profile}
30
+ MSG
31
+ end
32
+ allocs
33
+ end
34
+
35
+ shared_examples "allocation guard" do |adapter_name|
36
+ let(:ctx) { Moxml::Context.new(adapter_name) }
37
+
38
+ # ----------------------------------------------------------------
39
+ # Parse allocation guard
40
+ # ----------------------------------------------------------------
41
+ describe "parse allocations" do
42
+ it "parses 100-element document within allocation budget" do
43
+ xml = generate_xml(100)
44
+ allocs = guard_allocations(adapter_name, :parse_100) { ctx.parse(xml) }
45
+ expect(allocs).to be <= threshold_for(adapter_name, :parse_100)
46
+ end
47
+
48
+ it "parses 50-element document within allocation budget" do
49
+ xml = generate_xml(50)
50
+ allocs = guard_allocations(adapter_name, :parse_50) { ctx.parse(xml) }
51
+ expect(allocs).to be <= threshold_for(adapter_name, :parse_50)
52
+ end
53
+
54
+ it "parse + root.name is allocation-efficient" do
55
+ xml = generate_xml(100)
56
+ allocs = guard_allocations(adapter_name, :parse_and_root) do
57
+ doc = ctx.parse(xml)
58
+ doc.root.name
59
+ end
60
+ expect(allocs).to be <= threshold_for(adapter_name, :parse_and_root)
61
+ end
62
+ end
63
+
64
+ # ----------------------------------------------------------------
65
+ # Cache hit guards — second access should allocate near-zero objects
66
+ # ----------------------------------------------------------------
67
+ describe "cache hit guards" do
68
+ let(:simple_xml) { "<root><a/><b/><c/></root>" }
69
+ let(:attr_xml) { '<root x="1" y="2" z="3"><child k="4"/></root>' }
70
+
71
+ it "children access is cached after first call" do
72
+ doc = ctx.parse(simple_xml)
73
+ root = doc.root
74
+ # Warm the cache
75
+ root.children.to_a
76
+
77
+ allocs = AllocationHelper.count_allocations { root.children.to_a }
78
+ expect(allocs).to be <= threshold_for(adapter_name, :cached_children_access),
79
+ "Second children access (#{allocs}) should allocate <= #{threshold_for(
80
+ adapter_name, :cached_children_access
81
+ )}"
82
+ end
83
+
84
+ it "attributes access is cached after first call" do
85
+ doc = ctx.parse(attr_xml)
86
+ root = doc.root
87
+ # Warm the cache
88
+ root.attributes
89
+
90
+ allocs = AllocationHelper.count_allocations { root.attributes }
91
+ expect(allocs).to be <= threshold_for(adapter_name, :cached_attributes_access),
92
+ "Second attributes access (#{allocs}) should allocate <= #{threshold_for(
93
+ adapter_name, :cached_attributes_access
94
+ )}"
95
+ end
96
+
97
+ it "NodeSet iteration is cached on second pass" do
98
+ xml = generate_xml(20)
99
+ doc = ctx.parse(xml)
100
+ root = doc.root
101
+ # Warm the cache
102
+ root.children.each { |_| nil }
103
+
104
+ allocs = AllocationHelper.count_allocations do
105
+ root.children.each do |_|
106
+ nil
107
+ end
108
+ end
109
+ expect(allocs).to be <= threshold_for(adapter_name, :cached_iteration),
110
+ "Second NodeSet iteration (#{allocs}) should allocate <= #{threshold_for(
111
+ adapter_name, :cached_iteration
112
+ )}"
113
+ end
114
+ end
115
+
116
+ # ----------------------------------------------------------------
117
+ # Round-trip allocation guard
118
+ # ----------------------------------------------------------------
119
+ describe "round-trip allocations" do
120
+ it "parse → serialize → parse stays within budget" do
121
+ xml = generate_xml(50)
122
+ allocs = guard_allocations(adapter_name, :round_trip) do
123
+ doc = ctx.parse(xml)
124
+ serialized = doc.to_xml
125
+ ctx.parse(serialized)
126
+ end
127
+ expect(allocs).to be <= threshold_for(adapter_name, :round_trip)
128
+ end
129
+ end
130
+
131
+ # ----------------------------------------------------------------
132
+ # Scalability guard — allocations grow linearly, not quadratically
133
+ # ----------------------------------------------------------------
134
+ describe "scalability" do
135
+ it "allocation growth is linear with document size" do
136
+ xml_100 = generate_xml(100)
137
+ xml_200 = generate_xml(200)
138
+
139
+ allocs_100 = AllocationHelper.count_allocations { ctx.parse(xml_100) }
140
+ allocs_200 = AllocationHelper.count_allocations { ctx.parse(xml_200) }
141
+
142
+ ratio = allocs_200.to_f / allocs_100
143
+ max_ratio = threshold_for(adapter_name, :scalability_ratio)
144
+
145
+ expect(ratio).to be <= max_ratio,
146
+ "200-element parse (#{allocs_200}) vs 100-element (#{allocs_100}) = #{ratio.round(2)}x, " \
147
+ "expected <= #{max_ratio}x (linear growth)"
148
+ end
149
+ end
150
+
151
+ # ----------------------------------------------------------------
152
+ # Cache invalidation guards — mutation breaks cache
153
+ # ----------------------------------------------------------------
154
+ describe "cache invalidation" do
155
+ it "adding a child invalidates children cache" do
156
+ xml = "<root><a/></root>"
157
+ doc = ctx.parse(xml)
158
+ root = doc.root
159
+ children_before = root.children
160
+
161
+ new_elem = ctx.parse("<b/>").root
162
+ root.add_child(new_elem)
163
+
164
+ children_after = root.children
165
+ expect(children_before).not_to equal(children_after),
166
+ "Children cache should be invalidated after add_child"
167
+ expect(children_after.size).to eq(2)
168
+ end
169
+
170
+ it "setting text invalidates children cache" do
171
+ xml = "<root><a/></root>"
172
+ doc = ctx.parse(xml)
173
+ root = doc.root
174
+ children_before = root.children
175
+
176
+ root.text = "replaced"
177
+
178
+ children_after = root.children
179
+ expect(children_before).not_to equal(children_after),
180
+ "Children cache should be invalidated after text="
181
+ end
182
+
183
+ it "setting attribute invalidates attributes cache" do
184
+ xml = '<root a="1"/>'
185
+ doc = ctx.parse(xml)
186
+ root = doc.root
187
+ attrs_before = root.attributes
188
+
189
+ root["b"] = "2"
190
+
191
+ attrs_after = root.attributes
192
+ expect(attrs_before).not_to equal(attrs_after),
193
+ "Attributes cache should be invalidated after []="
194
+ expect(attrs_after.size).to eq(2)
195
+ end
196
+
197
+ it "removing attribute invalidates attributes cache" do
198
+ xml = '<root a="1" b="2"/>'
199
+ doc = ctx.parse(xml)
200
+ root = doc.root
201
+ attrs_before = root.attributes
202
+
203
+ root.remove_attribute("a")
204
+
205
+ attrs_after = root.attributes
206
+ expect(attrs_before).not_to equal(attrs_after),
207
+ "Attributes cache should be invalidated after remove_attribute"
208
+ expect(attrs_after.size).to eq(1)
209
+ end
210
+
211
+ it "removing a child invalidates parent's children cache" do
212
+ xml = "<root><a/><b/></root>"
213
+ doc = ctx.parse(xml)
214
+ root = doc.root
215
+ children_before = root.children
216
+ child_a = root.children.first
217
+
218
+ child_a.remove
219
+
220
+ children_after = root.children
221
+ expect(children_before).not_to equal(children_after),
222
+ "Parent's children cache should be invalidated after child.remove"
223
+ expect(children_after.size).to eq(1)
224
+ end
225
+ end
226
+
227
+ # ----------------------------------------------------------------
228
+ # NodeSet wrap cache guards
229
+ # ----------------------------------------------------------------
230
+ describe "NodeSet wrap cache" do
231
+ it "returns the same wrapper for same index" do
232
+ doc = ctx.parse("<root><a/><b/><c/></root>")
233
+ children = doc.root.children
234
+
235
+ first_access = children[0]
236
+ second_access = children[0]
237
+ expect(first_access).to equal(second_access),
238
+ "Same index should return identical wrapper object"
239
+ end
240
+
241
+ it "returns the same wrapper from each as from []" do
242
+ doc = ctx.parse("<root><a/><b/><c/></root>")
243
+ children = doc.root.children
244
+
245
+ from_each = nil
246
+ children.each { |c| from_each = c if c.name == "b" }
247
+ from_index = children[1]
248
+
249
+ expect(from_each).to equal(from_index),
250
+ "Node from #each should be identical to same index from #[]"
251
+ end
252
+
253
+ it "preserves cache across multiple iterations" do
254
+ doc = ctx.parse("<root><a/><b/><c/></root>")
255
+ children = doc.root.children
256
+
257
+ pass1 = children.map(&:name)
258
+ pass2 = children.map(&:name)
259
+
260
+ expect(pass1).to eq(pass2)
261
+ # Also verify object identity between passes
262
+ pass1_nodes = children.to_a
263
+ pass2_nodes = children.to_a
264
+ pass1_nodes.each_with_index do |node, i|
265
+ expect(node).to equal(pass2_nodes[i]),
266
+ "Node #{i} should be identical across iterations"
267
+ end
268
+ end
269
+ end
270
+ end
271
+
272
+ # Run guards for each adapter
273
+ AllocationHelper::GUARDED_ADAPTERS.each do |adapter_name|
274
+ describe "#{adapter_name} adapter" do
275
+ before(:all) do
276
+ skip("#{adapter_name} adapter not available") unless AllocationHelper.adapter_available?(adapter_name)
277
+ end
278
+
279
+ it_behaves_like "allocation guard", adapter_name
280
+ end
281
+ end
282
+ end
@@ -5,6 +5,28 @@ require "spec_helper"
5
5
  RSpec.describe Moxml::Builder do
6
6
  let(:context) { Moxml.new }
7
7
 
8
+ describe "#document" do
9
+ it "exposes the underlying document via document accessor" do
10
+ builder = described_class.new(context)
11
+ expect(builder.document).to be_a(Moxml::Document)
12
+ end
13
+
14
+ it "exposes the underlying document via doc alias" do
15
+ builder = described_class.new(context)
16
+ expect(builder.doc).to be_a(Moxml::Document)
17
+ expect(builder.doc).to equal(builder.document)
18
+ end
19
+
20
+ it "returns the same document before and after build" do
21
+ builder = described_class.new(context)
22
+ doc_before = builder.document
23
+ builder.build do
24
+ element "root"
25
+ end
26
+ expect(builder.document).to equal(doc_before)
27
+ end
28
+ end
29
+
8
30
  describe "#build" do
9
31
  it "builds a document with DSL" do
10
32
  doc = described_class.new(context).build do
@@ -30,4 +52,238 @@ RSpec.describe Moxml::Builder do
30
52
  expect(doc.root.children.length).to eq(2)
31
53
  end
32
54
  end
55
+
56
+ describe "#element with no arguments" do
57
+ it "raises ArgumentError" do
58
+ builder = described_class.new(context)
59
+ expect do
60
+ builder.build { element }
61
+ end.to raise_error(ArgumentError, "element requires a tag name")
62
+ end
63
+ end
64
+
65
+ describe "#element with Hash (tag name collision)" do
66
+ it "creates an <element> tag when called with a Hash" do
67
+ doc = described_class.new(context).build do
68
+ element(name: "foo", type: "bar")
69
+ end
70
+
71
+ el = doc.root
72
+ expect(el.name).to eq("element")
73
+ expect(el["name"]).to eq("foo")
74
+ expect(el["type"]).to eq("bar")
75
+ end
76
+ end
77
+
78
+ describe "method_missing DSL" do
79
+ it "creates elements via bare method calls inside build block" do
80
+ doc = described_class.new(context).build do
81
+ root do
82
+ title("Hello")
83
+ end
84
+ end
85
+
86
+ expect(doc.root.name).to eq("root")
87
+ expect(doc.root.children.first.name).to eq("title")
88
+ expect(doc.root.children.first.text).to eq("Hello")
89
+ end
90
+
91
+ it "creates elements via explicit receiver" do
92
+ builder = described_class.new(context)
93
+ doc = builder.build do
94
+ element "root" do
95
+ builder.title("Hello")
96
+ end
97
+ end
98
+
99
+ title_el = doc.root.children.first
100
+ expect(title_el.name).to eq("title")
101
+ expect(title_el.text).to eq("Hello")
102
+ end
103
+
104
+ it "creates elements with attributes" do
105
+ builder = described_class.new(context)
106
+ doc = builder.build do
107
+ element "root" do
108
+ builder.item(id: "1", class: "active")
109
+ end
110
+ end
111
+
112
+ item_el = doc.root.children.first
113
+ expect(item_el.name).to eq("item")
114
+ expect(item_el["id"]).to eq("1")
115
+ expect(item_el["class"]).to eq("active")
116
+ end
117
+
118
+ it "creates elements with both text content and attributes" do
119
+ builder = described_class.new(context)
120
+ doc = builder.build do
121
+ element "root" do
122
+ builder.title("Hello", lang: "en")
123
+ end
124
+ end
125
+
126
+ title_el = doc.root.children.first
127
+ expect(title_el.name).to eq("title")
128
+ expect(title_el.text).to eq("Hello")
129
+ expect(title_el["lang"]).to eq("en")
130
+ end
131
+
132
+ it "creates nested elements with blocks" do
133
+ builder = described_class.new(context)
134
+ doc = builder.build do
135
+ element "root" do
136
+ builder.parent do
137
+ builder.child("text")
138
+ end
139
+ end
140
+ end
141
+
142
+ parent_el = doc.root.children.first
143
+ expect(parent_el.name).to eq("parent")
144
+ child_el = parent_el.children.first
145
+ expect(child_el.name).to eq("child")
146
+ expect(child_el.text).to eq("text")
147
+ end
148
+
149
+ it "creates elements with namespace attributes" do
150
+ builder = described_class.new(context)
151
+ doc = builder.build do
152
+ element "root" do
153
+ builder.schema(xmlns: "http://www.w3.org/2001/XMLSchema")
154
+ end
155
+ end
156
+
157
+ schema_el = doc.root.children.first
158
+ expect(schema_el.name).to eq("schema")
159
+ end
160
+ end
161
+
162
+ describe "trailing underscore stripping (special tags)" do
163
+ it "strips trailing underscore to allow reserved method names as tags" do
164
+ builder = described_class.new(context)
165
+ doc = builder.build do
166
+ element "root" do
167
+ builder.type_("Object")
168
+ builder.class_("String")
169
+ builder.id_("42")
170
+ end
171
+ end
172
+
173
+ children = doc.root.children
174
+ expect(children[0].name).to eq("type")
175
+ expect(children[0].text).to eq("Object")
176
+ expect(children[1].name).to eq("class")
177
+ expect(children[1].text).to eq("String")
178
+ expect(children[2].name).to eq("id")
179
+ expect(children[2].text).to eq("42")
180
+ end
181
+
182
+ it "strips trailing underscore with attributes" do
183
+ builder = described_class.new(context)
184
+ doc = builder.build do
185
+ element "root" do
186
+ builder.type_(name: "foo")
187
+ end
188
+ end
189
+
190
+ el = doc.root.children.first
191
+ expect(el.name).to eq("type")
192
+ expect(el["name"]).to eq("foo")
193
+ end
194
+
195
+ it "strips trailing underscore with block" do
196
+ builder = described_class.new(context)
197
+ doc = builder.build do
198
+ element "root" do
199
+ builder.class_ do
200
+ builder.name_("MyClass")
201
+ end
202
+ end
203
+ end
204
+
205
+ class_el = doc.root.children.first
206
+ expect(class_el.name).to eq("class")
207
+ expect(class_el.children.first.name).to eq("name")
208
+ end
209
+
210
+ it "works with bare method calls inside build block" do
211
+ doc = described_class.new(context).build do
212
+ root do
213
+ type_("Object")
214
+ end
215
+ end
216
+
217
+ expect(doc.root.children.first.name).to eq("type")
218
+ expect(doc.root.children.first.text).to eq("Object")
219
+ end
220
+
221
+ it "does not strip underscore from middle of name" do
222
+ builder = described_class.new(context)
223
+ doc = builder.build do
224
+ element "root" do
225
+ builder.my_element("text")
226
+ end
227
+ end
228
+
229
+ expect(doc.root.children.first.name).to eq("my_element")
230
+ end
231
+ end
232
+
233
+ describe "respond_to_missing?" do
234
+ it "returns true for arbitrary element names" do
235
+ builder = described_class.new(context)
236
+ expect(builder.respond_to?(:schema)).to be true
237
+ expect(builder.respond_to?(:custom_element)).to be true
238
+ end
239
+
240
+ it "returns false for reserved Ruby conversion and protocol methods" do
241
+ builder = described_class.new(context)
242
+ expect(builder.respond_to?(:to_ary)).to be false
243
+ expect(builder.respond_to?(:to_hash)).to be false
244
+ expect(builder.respond_to?(:to_str)).to be false
245
+ expect(builder.respond_to?(:to_xml)).to be false
246
+ end
247
+ end
248
+
249
+ describe "method_missing argument validation" do
250
+ it "raises ArgumentError for unexpected extra arguments" do
251
+ builder = described_class.new(context)
252
+ expect do
253
+ builder.build do
254
+ element "root" do
255
+ builder.foo("text", { id: "1" }, "extra")
256
+ end
257
+ end
258
+ end.to raise_error(ArgumentError, /unexpected arguments for foo/)
259
+ end
260
+
261
+ it "raises ArgumentError when combining text content with a block" do
262
+ builder = described_class.new(context)
263
+ expect do
264
+ builder.build do
265
+ element "root" do
266
+ builder.title("Hello") { builder.child }
267
+ end
268
+ end
269
+ end.to raise_error(ArgumentError, /title: cannot combine text content with a block/)
270
+ end
271
+ end
272
+
273
+ describe "@current restoration on error" do
274
+ it "restores @current when block raises" do
275
+ builder = described_class.new(context)
276
+ expect do
277
+ builder.build do
278
+ element "root" do
279
+ element "child" do
280
+ raise "test error"
281
+ end
282
+ end
283
+ end
284
+ end.to raise_error(RuntimeError, "test error")
285
+
286
+ expect(builder.instance_variable_get(:@current)).to equal(builder.document)
287
+ end
288
+ end
33
289
  end
@@ -17,8 +17,8 @@ RSpec.describe Moxml::Config do
17
17
  expect(config.entity_load_mode).to eq(:required)
18
18
  end
19
19
 
20
- it "sets default namespace_uri_mode to :strict" do
21
- expect(config.namespace_uri_mode).to eq(:strict)
20
+ it "sets default namespace_validation_mode to :strict" do
21
+ expect(config.namespace_validation_mode).to eq(:strict)
22
22
  end
23
23
  end
24
24
 
@@ -66,26 +66,26 @@ RSpec.describe Moxml::Config do
66
66
  end
67
67
  end
68
68
 
69
- describe "#namespace_uri_mode=" do
69
+ describe "#namespace_validation_mode=" do
70
70
  it "accepts :strict" do
71
- config.namespace_uri_mode = :strict
72
- expect(config.namespace_uri_mode).to eq(:strict)
71
+ config.namespace_validation_mode = :strict
72
+ expect(config.namespace_validation_mode).to eq(:strict)
73
73
  end
74
74
 
75
75
  it "accepts :lenient" do
76
- config.namespace_uri_mode = :lenient
77
- expect(config.namespace_uri_mode).to eq(:lenient)
76
+ config.namespace_validation_mode = :lenient
77
+ expect(config.namespace_validation_mode).to eq(:lenient)
78
78
  end
79
79
 
80
80
  it "accepts string values" do
81
- config.namespace_uri_mode = "lenient"
82
- expect(config.namespace_uri_mode).to eq(:lenient)
81
+ config.namespace_validation_mode = "lenient"
82
+ expect(config.namespace_validation_mode).to eq(:lenient)
83
83
  end
84
84
 
85
85
  it "raises error for invalid mode" do
86
86
  expect do
87
- config.namespace_uri_mode = :invalid
88
- end.to raise_error(ArgumentError, /Invalid namespace_uri_mode/)
87
+ config.namespace_validation_mode = :invalid
88
+ end.to raise_error(ArgumentError, /Invalid namespace_validation_mode/)
89
89
  end
90
90
  end
91
91
 
@@ -46,4 +46,45 @@ RSpec.describe Moxml::Doctype do
46
46
  expect(doctype.name).to eq("html")
47
47
  end
48
48
  end
49
+
50
+ describe "parsing" do
51
+ %i[nokogiri oga rexml ox].each do |adapter_name|
52
+ context "with #{adapter_name} adapter" do
53
+ let(:ctx) { Moxml.new(adapter_name) }
54
+
55
+ it "parses PUBLIC doctype with external and system identifiers" do
56
+ xml = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html/>'
57
+ doc = ctx.parse(xml)
58
+ doctype = doc.children.find { |c| c.is_a?(described_class) }
59
+
60
+ expect(doctype).not_to be_nil
61
+ expect(doctype.name).to eq("html")
62
+ expect(doctype.external_id).to eq("-//W3C//DTD XHTML 1.0 Strict//EN")
63
+ expect(doctype.system_id).to eq("http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd")
64
+ end
65
+
66
+ it "parses SYSTEM doctype with system identifier only" do
67
+ xml = '<!DOCTYPE config SYSTEM "config.dtd"><config/>'
68
+ doc = ctx.parse(xml)
69
+ doctype = doc.children.find { |c| c.is_a?(described_class) }
70
+
71
+ expect(doctype).not_to be_nil
72
+ expect(doctype.name).to eq("config")
73
+ expect(doctype.external_id).to be_nil
74
+ expect(doctype.system_id).to eq("config.dtd")
75
+ end
76
+
77
+ it "parses simple doctype without identifiers" do
78
+ xml = "<!DOCTYPE html><html/>"
79
+ doc = ctx.parse(xml)
80
+ doctype = doc.children.find { |c| c.is_a?(described_class) }
81
+
82
+ expect(doctype).not_to be_nil
83
+ expect(doctype.name).to eq("html")
84
+ expect(doctype.external_id).to be_nil
85
+ expect(doctype.system_id).to be_nil
86
+ end
87
+ end
88
+ end
89
+ end
49
90
  end