moxml 0.5.58 → 0.5.60
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/lib/moxml/adapter/base.rb +9 -0
- data/lib/moxml/adapter/leptris.rb +66 -2
- data/lib/moxml/node.rb +3 -15
- data/lib/moxml/plan.rb +133 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml.rb +1 -0
- data/spec/moxml/plan_spec.rb +102 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 70fbb27b6a314c772f06699958b27c017a5033c33a62f84ff98d15c3c8a866a0
|
|
4
|
+
data.tar.gz: 4222b8eb5f128d74bc55a3284596f11bc975684eae28ff3f0cb8fe56bb2a4899
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '08307faaef4bfdfbda347f2f860f71f4aedabfc1b30bf81bcdfe876296ee68df6138e42efcf92c04cb94f3696a8ff33b038ce40bf826ad87491a1ee07364e2c3'
|
|
7
|
+
data.tar.gz: 1f084671c8983ed53172652e6322670b09550758b76378b30905bf1c1c30e7cc531d9fdd46edc3ecd7d9bf2ffa6cbd23e79a989a9d0237eae020191265c29641
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -358,6 +358,15 @@ namespace_validation_mode: :strict)
|
|
|
358
358
|
nil
|
|
359
359
|
end
|
|
360
360
|
|
|
361
|
+
# Plan row stream (Moxml::Plan): yields |name, attrs_pairs
|
|
362
|
+
# (flat [k, v, ...]), first-text, depth| per element in
|
|
363
|
+
# document order (pre-order). Returns true when the adapter
|
|
364
|
+
# provided the stream; nil/false lets the plan run its
|
|
365
|
+
# generic wrapper walk.
|
|
366
|
+
def plan_rows(_native)
|
|
367
|
+
nil
|
|
368
|
+
end
|
|
369
|
+
|
|
361
370
|
# Deterministic native-memory release for adapters backed by
|
|
362
371
|
# C trees (issue #134). GC-managed engines no-op; released
|
|
363
372
|
# documents raise the engine's use-after-free error on
|
|
@@ -222,10 +222,30 @@ module Moxml
|
|
|
222
222
|
ELEMENT_CONTRACT_READ =
|
|
223
223
|
::Moxml::ElementBehavior.instance_method(:[])
|
|
224
224
|
|
|
225
|
+
# Marker presence is a DOCUMENT fact, but the wrapper
|
|
226
|
+
# memo re-derives it per node (the per-read machinery
|
|
227
|
+
# was ~a quarter of the consumer walk). Doc-level
|
|
228
|
+
# WeakMap, generation-stamped into one Integer
|
|
229
|
+
# (immediates make safe weak values): one C document
|
|
230
|
+
# read plus one map hit per bare read. Entries die
|
|
231
|
+
# with their documents; a stale generation re-derives.
|
|
232
|
+
ENTITY_DOC_MEMO = ::ObjectSpace::WeakMap.new
|
|
233
|
+
|
|
234
|
+
def doc_entity_bearing?
|
|
235
|
+
doc = NN_DOCUMENT.bind_call(self)
|
|
236
|
+
memo = ENTITY_DOC_MEMO[doc]
|
|
237
|
+
gen = ::Moxml::Adapter::Leptris.serialize_generation
|
|
238
|
+
return memo.allbits?(1) if memo && (memo >> 1) == gen
|
|
239
|
+
|
|
240
|
+
bearing = ::Moxml::Adapter::Leptris.entity_bearing?(self)
|
|
241
|
+
ENTITY_DOC_MEMO[doc] = (gen << 1) | (bearing ? 1 : 0)
|
|
242
|
+
bearing
|
|
243
|
+
end
|
|
244
|
+
|
|
225
245
|
def [](key)
|
|
226
246
|
if key.is_a?(String) && !key.include?(":")
|
|
227
247
|
value = NN_ATTRIBUTE.bind_call(self, key)
|
|
228
|
-
return value unless value.is_a?(String) &&
|
|
248
|
+
return value unless value.is_a?(String) && doc_entity_bearing?
|
|
229
249
|
|
|
230
250
|
return ::Moxml::Adapter::Leptris.restore_entities(value)
|
|
231
251
|
end
|
|
@@ -235,7 +255,7 @@ module Moxml
|
|
|
235
255
|
|
|
236
256
|
def text
|
|
237
257
|
value = NN_CONTENT.bind_call(self)
|
|
238
|
-
value.is_a?(String) &&
|
|
258
|
+
value.is_a?(String) && doc_entity_bearing? ? ::Moxml::Adapter::Leptris.restore_entities(value) : value
|
|
239
259
|
end
|
|
240
260
|
end
|
|
241
261
|
ELEMENT.include(Reads)
|
|
@@ -473,6 +493,50 @@ module Moxml
|
|
|
473
493
|
extend Markers
|
|
474
494
|
extend Materialize
|
|
475
495
|
|
|
496
|
+
# Plan row stream (Moxml::Plan): the engine's one-pass C
|
|
497
|
+
# snapshot is pre-order with the first text child on the row —
|
|
498
|
+
# exactly the plan's shape, with no wrapper minting at all.
|
|
499
|
+
# Marker-bearing documents stay on the generic path (the bulk
|
|
500
|
+
# stream has no marker split).
|
|
501
|
+
NATIVE_PLAN_ROWS =
|
|
502
|
+
NATIVE_READ_LAYER &&
|
|
503
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.193.4")
|
|
504
|
+
|
|
505
|
+
def self.plan_rows(native)
|
|
506
|
+
return nil unless NATIVE_READ_LAYER
|
|
507
|
+
|
|
508
|
+
doc = if native.is_a?(::Leptris::XML::Document)
|
|
509
|
+
native
|
|
510
|
+
else
|
|
511
|
+
doc_for(native)
|
|
512
|
+
end
|
|
513
|
+
return nil if doc.nil? || attachments.get(doc, :entity_markers)
|
|
514
|
+
|
|
515
|
+
root_binding = if native.is_a?(::Leptris::XML::Document)
|
|
516
|
+
doc.root
|
|
517
|
+
else
|
|
518
|
+
to_binding(native)
|
|
519
|
+
end
|
|
520
|
+
return nil unless root_binding
|
|
521
|
+
|
|
522
|
+
rows = if NATIVE_PLAN_ROWS
|
|
523
|
+
::Leptris::XML::Native.snapshot_rows(doc,
|
|
524
|
+
root_binding.c_address)
|
|
525
|
+
else
|
|
526
|
+
doc.snapshot(root_binding)
|
|
527
|
+
end
|
|
528
|
+
rows.each do |row|
|
|
529
|
+
if row.is_a?(::Array)
|
|
530
|
+
yield(row[0], row[1], row[2], row[3])
|
|
531
|
+
else
|
|
532
|
+
next unless row[:kind] == "element"
|
|
533
|
+
|
|
534
|
+
yield(row[:name], row[:attrs].flatten, row[:text], row[:depth])
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
true
|
|
538
|
+
end
|
|
539
|
+
|
|
476
540
|
class << self
|
|
477
541
|
def attachments
|
|
478
542
|
@attachments ||= Moxml::NativeAttachment.new
|
data/lib/moxml/node.rb
CHANGED
|
@@ -164,30 +164,18 @@ module Moxml
|
|
|
164
164
|
# entity-marker flag flips at parse and entity-reference mint,
|
|
165
165
|
# both adapter-level, and the generation bump is the invalidation
|
|
166
166
|
# signal. Adapters with static answers (base class) never bump.
|
|
167
|
+
# The adapter resolves the owning document itself (doc_for is a
|
|
168
|
+
# single C read on the native layer); wrappers no longer climb.
|
|
167
169
|
def entity_bearing?
|
|
168
170
|
gen = adapter.serialize_generation
|
|
169
171
|
if @entity_bearing_gen == gen
|
|
170
172
|
@entity_bearing
|
|
171
173
|
else
|
|
172
174
|
@entity_bearing_gen = gen
|
|
173
|
-
@entity_bearing = adapter.entity_bearing?(@native
|
|
175
|
+
@entity_bearing = adapter.entity_bearing?(@native)
|
|
174
176
|
end
|
|
175
177
|
end
|
|
176
178
|
|
|
177
|
-
# Binding document through the wrapper parent chain — pure Ruby
|
|
178
|
-
# attr reads; the native-layer fallback climbs C parents per
|
|
179
|
-
# call (the doc_for walk was the largest wrapper-layer cost on
|
|
180
|
-
# the consumer pipeline after the native adoption).
|
|
181
|
-
def document_native_for_markers
|
|
182
|
-
node = self
|
|
183
|
-
while node
|
|
184
|
-
return node.native if node.document?
|
|
185
|
-
|
|
186
|
-
node = node.parent_node
|
|
187
|
-
end
|
|
188
|
-
nil
|
|
189
|
-
end
|
|
190
|
-
|
|
191
179
|
def xpath(expression, namespaces = {})
|
|
192
180
|
result = adapter.xpath(@native, expression, namespaces)
|
|
193
181
|
# Adapter contract: Array<native> | LazyNodeSet | scalar.
|
data/lib/moxml/plan.rb
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
# Plan-compiled materialization: declare the document shape once,
|
|
5
|
+
# execute it against the adapter's bulk row stream — no wrapper
|
|
6
|
+
# tree, no NodeSet, no per-node contract dispatch. The compiled
|
|
7
|
+
# grammar instead of the interpreted walk.
|
|
8
|
+
#
|
|
9
|
+
# plan = Moxml::Plan.new do
|
|
10
|
+
# on("record") { |attrs, _text, fields| Record.new(attrs["id"], attrs["kind"], fields) }
|
|
11
|
+
# on("field") { |attrs, text, _kids| Field.new(attrs["name"], attrs["unit"], text) }
|
|
12
|
+
# end
|
|
13
|
+
# records = plan.parse(xml, ctx)
|
|
14
|
+
#
|
|
15
|
+
# Handlers run bottom-up as each element completes; +children+ is
|
|
16
|
+
# the array of handler values for matched child elements whose
|
|
17
|
+
# parent ALSO matched (values of unmatched parents are dropped —
|
|
18
|
+
# matching a parent is the consumer's decision to keep a subtree).
|
|
19
|
+
# +attrs+ is a Hash of the element's attributes (local name =>
|
|
20
|
+
# value); +text+ is the first text child's content, or nil.
|
|
21
|
+
#
|
|
22
|
+
# Name matching is global (flat table, not path-relative): element
|
|
23
|
+
# names unique per role — the dominant consumer shape — work as-is;
|
|
24
|
+
# distinct roles sharing a name need distinct documents or a
|
|
25
|
+
# rename upstream.
|
|
26
|
+
#
|
|
27
|
+
# Adapters with a bulk stream answer plan_rows (leptris: the
|
|
28
|
+
# engine's one-pass C snapshot — pre-order, first-text on the
|
|
29
|
+
# row); the rest run the generic pre-order wrapper walk below,
|
|
30
|
+
# emitting the identical stream — spec-pinned equal.
|
|
31
|
+
class Plan
|
|
32
|
+
def initialize(&block)
|
|
33
|
+
@handlers = {}
|
|
34
|
+
instance_eval(&block) if block
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Registers a handler for elements named +name+ (local name).
|
|
38
|
+
# Returns self so registrations chain.
|
|
39
|
+
def on(name, &handler)
|
|
40
|
+
raise ArgumentError, "on(#{name.inspect}) requires a block" unless handler
|
|
41
|
+
|
|
42
|
+
@handlers[name] = handler
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Parses +xml+ and materializes the plan against it.
|
|
47
|
+
def parse(xml, context)
|
|
48
|
+
materialize(context.parse(xml))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Materializes the plan against a document (or element) wrapper.
|
|
52
|
+
# Returns the values of top-level matched elements.
|
|
53
|
+
def materialize(node)
|
|
54
|
+
results = []
|
|
55
|
+
frames = [] # per open depth: children values (nil = unmatched)
|
|
56
|
+
meta = [] # per open depth: [name, attrs, text] (nil = unmatched)
|
|
57
|
+
|
|
58
|
+
close_top = lambda do
|
|
59
|
+
m = meta.pop
|
|
60
|
+
children = frames.pop
|
|
61
|
+
next unless m
|
|
62
|
+
|
|
63
|
+
value = @handlers[m[0]].call(m[1], m[2], children)
|
|
64
|
+
if frames.empty? || frames.last.nil?
|
|
65
|
+
results << value
|
|
66
|
+
else
|
|
67
|
+
frames.last << value
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
row = lambda do |name, attrs_pairs, text, depth|
|
|
72
|
+
close_top.call while frames.size > depth
|
|
73
|
+
handler = @handlers[name]
|
|
74
|
+
if handler
|
|
75
|
+
attrs = {}
|
|
76
|
+
i = 0
|
|
77
|
+
while i < attrs_pairs.length
|
|
78
|
+
attrs[attrs_pairs[i]] = attrs_pairs[i + 1]
|
|
79
|
+
i += 2
|
|
80
|
+
end
|
|
81
|
+
frames << []
|
|
82
|
+
meta << [name, attrs, text]
|
|
83
|
+
else
|
|
84
|
+
frames << nil
|
|
85
|
+
meta << nil
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
adapter = node.context.config.adapter
|
|
90
|
+
ran = adapter.plan_rows(node.native) do |name, pairs, text, depth|
|
|
91
|
+
row.call(name, pairs, text, depth)
|
|
92
|
+
end
|
|
93
|
+
unless ran
|
|
94
|
+
walk_wrappers(node) do |name, pairs, text, depth|
|
|
95
|
+
row.call(name, pairs, text, depth)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
close_top.call while meta.any?
|
|
100
|
+
results
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# Generic pre-order walk over the wrapper tree — the fallback
|
|
106
|
+
# stream for adapters without a bulk path. Same protocol as
|
|
107
|
+
# plan_rows: |name, attrs_pairs (flat [k, v, ...]), first-text,
|
|
108
|
+
# depth|.
|
|
109
|
+
def walk_wrappers(node, depth = 0)
|
|
110
|
+
case node
|
|
111
|
+
when Document
|
|
112
|
+
(root = node.root) && walk_wrappers(root, depth) { |*a| yield(*a) }
|
|
113
|
+
when Element
|
|
114
|
+
first_text = nil
|
|
115
|
+
node.children.each do |child|
|
|
116
|
+
first_text ||= child.content if child.is_a?(Text)
|
|
117
|
+
end
|
|
118
|
+
yield(node.name, attribute_pairs(node), first_text, depth)
|
|
119
|
+
node.children.each do |child|
|
|
120
|
+
walk_wrappers(child, depth + 1) { |*a| yield(*a) } if child.is_a?(Element)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def attribute_pairs(element)
|
|
126
|
+
pairs = []
|
|
127
|
+
element.attributes.each do |attr|
|
|
128
|
+
pairs << attr.name << attr.value
|
|
129
|
+
end
|
|
130
|
+
pairs
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
data/lib/moxml/version.rb
CHANGED
data/lib/moxml.rb
CHANGED
|
@@ -93,6 +93,7 @@ module Moxml
|
|
|
93
93
|
autoload :XmlUtils, "moxml/xml_utils"
|
|
94
94
|
autoload :XmlEmitter, "moxml/xml_emitter"
|
|
95
95
|
autoload :Materializer, "moxml/materializer"
|
|
96
|
+
autoload :Plan, "moxml/plan"
|
|
96
97
|
autoload :Adapter, "moxml/adapter"
|
|
97
98
|
autoload :XPath, "moxml/xpath"
|
|
98
99
|
autoload :SAX, "moxml/sax"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Moxml::Plan: shape-compiled materialization over the adapter's
|
|
6
|
+
# bulk row stream (leptris C snapshot) and the generic wrapper walk.
|
|
7
|
+
# Both paths must produce identical values.
|
|
8
|
+
RSpec.describe Moxml::Plan do
|
|
9
|
+
PlanRow = Struct.new(:id, :kind, :fields) # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
10
|
+
PlanCell = Struct.new(:name, :unit, :value) # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
11
|
+
|
|
12
|
+
let(:xml) do
|
|
13
|
+
<<~XML
|
|
14
|
+
<?xml version="1.0"?>
|
|
15
|
+
<catalog>
|
|
16
|
+
<record id="r0" kind="k0"><field name="f0" unit="u0">v 0.0</field><field name="f1" unit="u1">v 0.1</field></record>
|
|
17
|
+
<record id="r1" kind="k1"><field name="f0" unit="u0">v 1.0</field></record>
|
|
18
|
+
</catalog>
|
|
19
|
+
XML
|
|
20
|
+
end
|
|
21
|
+
let(:plan) do
|
|
22
|
+
described_class.new do
|
|
23
|
+
on("record") do |attrs, _text, fields|
|
|
24
|
+
PlanRow.new(attrs["id"], attrs["kind"], fields)
|
|
25
|
+
end
|
|
26
|
+
on("field") do |attrs, text, _kids|
|
|
27
|
+
PlanCell.new(attrs["name"], attrs["unit"], text)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
shared_examples "plan materialization" do |adapter_name|
|
|
33
|
+
let(:ctx) { Moxml.new(adapter_name) }
|
|
34
|
+
|
|
35
|
+
it "builds the typed shape" do
|
|
36
|
+
rows = plan.parse(xml, ctx)
|
|
37
|
+
|
|
38
|
+
expect(rows.size).to eq(2)
|
|
39
|
+
expect(rows[0].id).to eq("r0")
|
|
40
|
+
expect(rows[0].kind).to eq("k0")
|
|
41
|
+
expect(rows[0].fields.size).to eq(2)
|
|
42
|
+
expect(rows[0].fields[0]).to eq(PlanCell.new("f0", "u0", "v 0.0"))
|
|
43
|
+
expect(rows[0].fields[1]).to eq(PlanCell.new("f1", "u1", "v 0.1"))
|
|
44
|
+
expect(rows[1].fields[0].value).to eq("v 1.0")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "drops values of unmatched parents" do
|
|
48
|
+
orphans = described_class.new do
|
|
49
|
+
on("field") { |attrs, text, _| PlanCell.new(attrs["name"], attrs["unit"], text) }
|
|
50
|
+
end.parse(xml, ctx)
|
|
51
|
+
|
|
52
|
+
# fields match everywhere; no record handler keeps them
|
|
53
|
+
expect(orphans.size).to eq(3)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "returns top-level values for a matched root" do
|
|
57
|
+
roots = described_class.new do
|
|
58
|
+
on("catalog") { |_a, _t, kids| [:catalog, kids] }
|
|
59
|
+
on("record") { |attrs, _t, fields| [attrs["id"], fields] }
|
|
60
|
+
on("field") { |attrs, text, _| [attrs["name"], text] }
|
|
61
|
+
end.parse(xml, ctx)
|
|
62
|
+
|
|
63
|
+
expect(roots.size).to eq(1)
|
|
64
|
+
expect(roots[0][0]).to eq(:catalog)
|
|
65
|
+
expect(roots[0][1].size).to eq(2)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "handles depth-3 nesting" do
|
|
69
|
+
deep = described_class.new do
|
|
70
|
+
on("catalog") { |_a, _t, records| records }
|
|
71
|
+
on("record") { |attrs, _t, fields| [attrs["id"], fields] }
|
|
72
|
+
on("field") { |attrs, text, _| [attrs["name"], text] }
|
|
73
|
+
end.parse(xml, ctx)
|
|
74
|
+
|
|
75
|
+
expect(deep.size).to eq(1)
|
|
76
|
+
expect(deep[0]).to eq(
|
|
77
|
+
[["r0", [["f0", "v 0.0"], ["f1", "v 0.1"]]],
|
|
78
|
+
["r1", [["f0", "v 1.0"]]]],
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "is reusable across documents" do
|
|
83
|
+
first = plan.parse(xml, ctx)
|
|
84
|
+
second = plan.parse(xml, ctx)
|
|
85
|
+
|
|
86
|
+
expect(first).to eq(second)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe "bulk path (leptris)" do
|
|
91
|
+
it_behaves_like "plan materialization", :leptris
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
describe "generic path" do
|
|
95
|
+
it_behaves_like "plan materialization", :nokogiri
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "requires a handler block" do
|
|
99
|
+
expect { described_class.new.on("x") }
|
|
100
|
+
.to raise_error(ArgumentError, /requires a block/)
|
|
101
|
+
end
|
|
102
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: moxml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.60
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
Moxml is a unified XML manipulation library that provides a common API
|
|
@@ -214,6 +214,7 @@ files:
|
|
|
214
214
|
- lib/moxml/native_attachment/opal.rb
|
|
215
215
|
- lib/moxml/node.rb
|
|
216
216
|
- lib/moxml/node_set.rb
|
|
217
|
+
- lib/moxml/plan.rb
|
|
217
218
|
- lib/moxml/processing_instruction.rb
|
|
218
219
|
- lib/moxml/sax.rb
|
|
219
220
|
- lib/moxml/sax/block_handler.rb
|
|
@@ -487,6 +488,7 @@ files:
|
|
|
487
488
|
- spec/moxml/opal_oga_smoke_spec.rb
|
|
488
489
|
- spec/moxml/opal_rexml_adapter_spec.rb
|
|
489
490
|
- spec/moxml/opal_smoke_spec.rb
|
|
491
|
+
- spec/moxml/plan_spec.rb
|
|
490
492
|
- spec/moxml/processing_instruction_spec.rb
|
|
491
493
|
- spec/moxml/readonly_parse_spec.rb
|
|
492
494
|
- spec/moxml/sax/namespace_splitter_spec.rb
|