moxml 0.5.127 → 0.5.128
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/lib/moxml/adapter/base.rb +9 -0
- data/lib/moxml/adapter/leptris.rb +32 -6
- data/lib/moxml/adapter/nokogiri.rb +56 -0
- data/lib/moxml/css.rb +275 -0
- data/lib/moxml/node.rb +17 -6
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xsd.rb +58 -2
- data/lib/moxml.rb +1 -0
- data/spec/moxml/css_spec.rb +121 -0
- data/spec/moxml/xsd_error_contract_spec.rb +72 -0
- data/spec/moxml/xsd_release_spec.rb +76 -0
- data/spec/moxml/xsd_spec.rb +5 -2
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5b80e9601eb4a227d342aa484359a4724f3273a561c9f74cd8d375ffd5321186
|
|
4
|
+
data.tar.gz: 987e9faa827418fe3471456b1b939ced916467c895454670c3ba1273ee5fd616
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0f059f790d6a76ba2213d83ced97289d837ab0ad1b4448dd1f026bb991a5e90b8131fabc31d6f2f0eeb948564693231ea89c3899a542618d65de153db687bde7
|
|
7
|
+
data.tar.gz: edae98439cf51d03ea590709a22fa05848f9e7b62fa7f274bf6e1ffdd34f0115eca0622b165e3f1a884d8f9a73ba4047338061d6281c4c88161ce13bbd44a7ae
|
data/Gemfile
CHANGED
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -327,6 +327,15 @@ module Moxml
|
|
|
327
327
|
|
|
328
328
|
def xsd_compile_error(_xsd_handle); end
|
|
329
329
|
|
|
330
|
+
# Deterministic drop (#362.3): true when the engine-side
|
|
331
|
+
# schema was freed NOW, false when the adapter can only
|
|
332
|
+
# wait for GC (the default — document the capability per
|
|
333
|
+
# adapter). moxml's Schema#release surfaces this. The name
|
|
334
|
+
# is the contract (release), not a predicate.
|
|
335
|
+
def xsd_release(_xsd_handle) # rubocop:disable Naming/PredicateMethod
|
|
336
|
+
false
|
|
337
|
+
end
|
|
338
|
+
|
|
330
339
|
def xsd_simple_valid?(_xsd_handle, _type_name, _lexical)
|
|
331
340
|
raise Moxml::NotImplementedError,
|
|
332
341
|
"XSD validation requires an adapter with an XSD engine"
|
|
@@ -198,6 +198,15 @@ module Moxml
|
|
|
198
198
|
|
|
199
199
|
XSD_SUPPORTED = NATIVE_XSD
|
|
200
200
|
|
|
201
|
+
# Schema#release (moxml#362.3, binding 1.9.342+): the
|
|
202
|
+
# deterministic drop face — disarms the AutoPointer's GC
|
|
203
|
+
# finalizer and frees now. Face-probed with the version
|
|
204
|
+
# floor (the NATIVE_DOC_ABSORB pairing).
|
|
205
|
+
XSD_RELEASE =
|
|
206
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.342.0") &&
|
|
207
|
+
defined?(::Leptris::XML::XSD::Schema) &&
|
|
208
|
+
::Leptris::XML::XSD::Schema.method_defined?(:release)
|
|
209
|
+
|
|
201
210
|
NATIVE_BUILDER =
|
|
202
211
|
defined?(::Leptris::XML::Builder) &&
|
|
203
212
|
defined?(::Leptris::XML::Document) &&
|
|
@@ -282,6 +291,12 @@ module Moxml
|
|
|
282
291
|
xsd_handle.declaration_count
|
|
283
292
|
end
|
|
284
293
|
|
|
294
|
+
def xsd_release(xsd_handle)
|
|
295
|
+
return false unless XSD_RELEASE
|
|
296
|
+
|
|
297
|
+
xsd_handle.release
|
|
298
|
+
end
|
|
299
|
+
|
|
285
300
|
def xsd_compile_error(xsd_handle)
|
|
286
301
|
xsd_handle.error
|
|
287
302
|
end
|
|
@@ -300,17 +315,28 @@ module Moxml
|
|
|
300
315
|
|
|
301
316
|
def xsd_validate(xsd_handle, native_document)
|
|
302
317
|
# +xsd_handle+ is the binding's Leptris::XML::XSD::Schema;
|
|
303
|
-
# validate_errors takes the binding document. Entries
|
|
304
|
-
#
|
|
305
|
-
#
|
|
306
|
-
#
|
|
307
|
-
#
|
|
318
|
+
# validate_errors takes the binding document. Entries are
|
|
319
|
+
# the #362.2 ValidationError contract; the engine payload
|
|
320
|
+
# is message strings today (line/column/path ride
|
|
321
|
+
# leptris-ruby#419 — the mapping fills them the day the
|
|
322
|
+
# binding exposes them).
|
|
308
323
|
doc = if native_document.is_a?(::Leptris::XML::Document)
|
|
309
324
|
native_document
|
|
310
325
|
else
|
|
311
326
|
to_binding(native_document)
|
|
312
327
|
end
|
|
313
|
-
|
|
328
|
+
structured = defined?(::Leptris::XML::XSD::ValidationError)
|
|
329
|
+
xsd_handle.validate_errors(doc).map do |entry|
|
|
330
|
+
if structured &&
|
|
331
|
+
entry.is_a?(::Leptris::XML::XSD::ValidationError)
|
|
332
|
+
Moxml::XSD::ValidationError.new(
|
|
333
|
+
message: entry.message, line: entry.line,
|
|
334
|
+
column: entry.column, path: entry.path
|
|
335
|
+
)
|
|
336
|
+
else
|
|
337
|
+
Moxml::XSD::ValidationError.new(message: entry.to_s)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
314
340
|
end
|
|
315
341
|
|
|
316
342
|
def builder_face(native_document)
|
|
@@ -10,6 +10,11 @@ module Moxml
|
|
|
10
10
|
# libxslt-backed XSLT 1.0 (Moxml::XSLT contract).
|
|
11
11
|
XSLT_SUPPORTED = true
|
|
12
12
|
|
|
13
|
+
# XSD over libxml2 (issue #362.2): structured #362
|
|
14
|
+
# ValidationError entries — libxml2 carries line/column in
|
|
15
|
+
# the message ("L:C: SEV: ..."), which the mapping lifts.
|
|
16
|
+
XSD_SUPPORTED = true
|
|
17
|
+
|
|
13
18
|
class << self
|
|
14
19
|
def attachments
|
|
15
20
|
@attachments ||= Moxml::NativeAttachment.new
|
|
@@ -193,6 +198,57 @@ module Moxml
|
|
|
193
198
|
native_fragment.children.map(&:to_html).join
|
|
194
199
|
end
|
|
195
200
|
|
|
201
|
+
def xsd_compile(schema_source)
|
|
202
|
+
::Nokogiri::XML::Schema.new(schema_source.to_s)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def xsd_compile_file(path)
|
|
206
|
+
path = path.to_s
|
|
207
|
+
# libxml2 resolves schemaLocation hrefs against the
|
|
208
|
+
# process CWD (the reader form carries no base) — chdir
|
|
209
|
+
# to the file's directory so relative hrefs resolve like
|
|
210
|
+
# leptris's engine-side base_dir.
|
|
211
|
+
dir = File.dirname(File.expand_path(path))
|
|
212
|
+
Dir.chdir(dir) { ::Nokogiri::XML::Schema(File.read(path)) }
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Compile failures RAISE from Schema.new; a constructed
|
|
216
|
+
# handle carries any pooled errors here (empty = none).
|
|
217
|
+
# Nokogiri/libxml2's schema object does not expose a
|
|
218
|
+
# declaration census — the base's nil default documents
|
|
219
|
+
# the capability gap rather than guessing.
|
|
220
|
+
def xsd_declarations(_xsd_handle)
|
|
221
|
+
nil
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def xsd_compile_error(xsd_handle)
|
|
225
|
+
msgs = xsd_handle.errors.map(&:to_s)
|
|
226
|
+
msgs.empty? ? nil : msgs.join("; ")
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def xsd_validate(xsd_handle, native_document)
|
|
230
|
+
xsd_handle.validate(native_document).map do |entry|
|
|
231
|
+
line = entry.line
|
|
232
|
+
line = line.to_i if line.is_a?(String)
|
|
233
|
+
Moxml::XSD::ValidationError.new(
|
|
234
|
+
message: strip_libxml2_position(entry.message),
|
|
235
|
+
line: line&.positive? ? line : nil,
|
|
236
|
+
column: nil,
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# libxml2's nokogiri face prefixes "L:C: SEVERITY: " onto
|
|
242
|
+
# the message; the position rides the structured fields.
|
|
243
|
+
def strip_libxml2_position(message)
|
|
244
|
+
msg = message.to_s
|
|
245
|
+
if (m = msg.match(/\A\d+:\d+: (?:ERROR|WARNING|FATAL): (.*)\z/m))
|
|
246
|
+
m[1]
|
|
247
|
+
else
|
|
248
|
+
msg
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
196
252
|
# Nokogiri parses with `config.recover` unless strict, so
|
|
197
253
|
# recoverable syntax errors land on `doc.errors` instead of
|
|
198
254
|
# raising (issue #147).
|
data/lib/moxml/css.rb
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
# CSS selector -> XPath 1.0 translation (#362.1): the shared
|
|
5
|
+
# implementation behind Node#css/at_css. One translator for
|
|
6
|
+
# every adapter — CSS semantics converge by construction instead
|
|
7
|
+
# of per-engine differences. Supported: type/universal, #id,
|
|
8
|
+
# .class, attribute predicates ([a], [a=v], ^=, $=, *=, ~=, |=),
|
|
9
|
+
# the descendant/child/adjacent-sibling/following-sibling
|
|
10
|
+
# combinators, :first-child/:last-child/:nth-child(n|odd|even),
|
|
11
|
+
# :not(simple), and comma groups (union NodeSets).
|
|
12
|
+
module Css
|
|
13
|
+
class Error < Moxml::Error; end
|
|
14
|
+
|
|
15
|
+
SIMPLE_PART = /
|
|
16
|
+
(?<tag>^[A-Za-z][\w-]*|\*)
|
|
17
|
+
| \.(?<class>[\w-]+)
|
|
18
|
+
| \#(?<id>[\w-]+)
|
|
19
|
+
| :(?<pseudo>[\w-]+(?:\([^)]*\))?)
|
|
20
|
+
| \[(?<attr>[^\]]+)\]
|
|
21
|
+
/x
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# absolute: true when the context node is a DOCUMENT — the
|
|
26
|
+
# "//" anchor addresses the whole tree (and avoids engines
|
|
27
|
+
# that resolve "." at a document node to the root element);
|
|
28
|
+
# false (an element) scopes to the subtree with ".//".
|
|
29
|
+
def to_xpath(selector, absolute: false)
|
|
30
|
+
selectors = split_top_level(selector.to_s)
|
|
31
|
+
raise Error, "empty selector" if selectors.empty?
|
|
32
|
+
|
|
33
|
+
anchor = absolute ? "" : "."
|
|
34
|
+
selectors.map { |s| "#{anchor}#{to_xpath_one(s.strip)}" }
|
|
35
|
+
.uniq.join(" | ")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Bracket- and quote-aware tokenization: combinator characters
|
|
39
|
+
# only split OUTSIDE [...] (a ~= inside an attribute selector
|
|
40
|
+
# is not the sibling combinator).
|
|
41
|
+
def tokenize(selector)
|
|
42
|
+
tokens = []
|
|
43
|
+
current = +""
|
|
44
|
+
depth = 0
|
|
45
|
+
quote = nil
|
|
46
|
+
flush = lambda do
|
|
47
|
+
unless current.empty?
|
|
48
|
+
tokens << [:simple, current]
|
|
49
|
+
current = +""
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
selector.each_char do |c|
|
|
53
|
+
if quote
|
|
54
|
+
current << c
|
|
55
|
+
quote = nil if c == quote
|
|
56
|
+
elsif ["'", '"'].include?(c)
|
|
57
|
+
quote = c
|
|
58
|
+
current << c
|
|
59
|
+
elsif c == "[" || c == "]" || depth.positive?
|
|
60
|
+
depth += 1 if c == "["
|
|
61
|
+
depth -= 1 if c == "]"
|
|
62
|
+
current << c
|
|
63
|
+
elsif c.match?(/[\s>+~]/)
|
|
64
|
+
flush.call
|
|
65
|
+
if tokens.last && tokens.last[0] == :comb
|
|
66
|
+
# a concrete combinator replaces the whitespace run;
|
|
67
|
+
# extra whitespace after one collapses
|
|
68
|
+
if c.match?(/[>+~]/)
|
|
69
|
+
tokens[-1] = [:comb, c]
|
|
70
|
+
end
|
|
71
|
+
next
|
|
72
|
+
end
|
|
73
|
+
tokens << [:comb, c.strip.empty? ? " " : c]
|
|
74
|
+
elsif c == ","
|
|
75
|
+
flush.call
|
|
76
|
+
tokens << [:comma, ","]
|
|
77
|
+
else
|
|
78
|
+
current << c
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
flush.call
|
|
82
|
+
tokens
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def to_xpath_one(selector)
|
|
86
|
+
xpath = ""
|
|
87
|
+
seen_step = false
|
|
88
|
+
pending_combinator = :descendant
|
|
89
|
+
|
|
90
|
+
tokenize(selector).each do |kind, value|
|
|
91
|
+
if kind == :comb
|
|
92
|
+
raise Error, "selector starts with a combinator" unless seen_step
|
|
93
|
+
|
|
94
|
+
pending_combinator =
|
|
95
|
+
case value.strip
|
|
96
|
+
when ">" then :child
|
|
97
|
+
when "+" then :adjacent
|
|
98
|
+
when "~" then :sibling
|
|
99
|
+
else :descendant
|
|
100
|
+
end
|
|
101
|
+
elsif kind == :comma
|
|
102
|
+
raise Error, "trailing comma"
|
|
103
|
+
else
|
|
104
|
+
step = simple_selector_to_xpath(value, pending_combinator)
|
|
105
|
+
sep = if !seen_step
|
|
106
|
+
pending_combinator == :descendant ? "//" : "/"
|
|
107
|
+
elsif pending_combinator == :descendant
|
|
108
|
+
"//"
|
|
109
|
+
else
|
|
110
|
+
"/"
|
|
111
|
+
end
|
|
112
|
+
xpath = "#{xpath}#{sep}#{step}"
|
|
113
|
+
seen_step = true
|
|
114
|
+
pending_combinator = :descendant
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
raise Error, "selector ends with a combinator" if
|
|
119
|
+
seen_step && pending_combinator != :descendant
|
|
120
|
+
raise Error, "empty selector" unless seen_step
|
|
121
|
+
|
|
122
|
+
xpath
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def simple_selector_to_xpath(simple, combinator)
|
|
126
|
+
tag, preds = parse_simple(simple)
|
|
127
|
+
|
|
128
|
+
predicate = preds.empty? ? "" : "[#{preds.join(' and ')}]"
|
|
129
|
+
|
|
130
|
+
case combinator
|
|
131
|
+
when :adjacent
|
|
132
|
+
# The FIRST following sibling, constrained to the test +
|
|
133
|
+
# predicates (the CSS + semantics) — one combined
|
|
134
|
+
# predicate, which oga's XPath subset parses (chained
|
|
135
|
+
# [1][self::x] it does not).
|
|
136
|
+
combined = (["position() = 1", "name() = '#{tag}'"] +
|
|
137
|
+
(preds.empty? ? [] : [preds.join(" and ")]))
|
|
138
|
+
.join(" and ")
|
|
139
|
+
"following-sibling::*[#{combined}]"
|
|
140
|
+
when :sibling
|
|
141
|
+
"following-sibling::#{tag}#{predicate}"
|
|
142
|
+
else
|
|
143
|
+
"#{tag}#{predicate}"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def parse_simple(simple)
|
|
148
|
+
tag = "*"
|
|
149
|
+
preds = []
|
|
150
|
+
|
|
151
|
+
simple.scan(SIMPLE_PART) do |tag_m, klass, id, pseudo, attr|
|
|
152
|
+
if tag_m
|
|
153
|
+
tag = tag_m
|
|
154
|
+
elsif klass
|
|
155
|
+
preds << "contains(concat(' ', normalize-space(@class), ' '), ' #{klass} ')"
|
|
156
|
+
elsif id
|
|
157
|
+
preds << "@id = '#{id}'"
|
|
158
|
+
elsif pseudo
|
|
159
|
+
preds << pseudo_predicate(pseudo)
|
|
160
|
+
elsif attr
|
|
161
|
+
preds << attr_predicate(attr)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
[tag, preds]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def pseudo_predicate(pseudo)
|
|
169
|
+
case pseudo
|
|
170
|
+
when "first-child"
|
|
171
|
+
"count(preceding-sibling::*) = 0"
|
|
172
|
+
when "last-child"
|
|
173
|
+
"count(following-sibling::*) = 0"
|
|
174
|
+
when /\Anth-child\((\d+)\)\z/
|
|
175
|
+
"count(preceding-sibling::*) = #{Regexp.last_match(1).to_i - 1}"
|
|
176
|
+
when /\Anth-child\(odd\)\z/i
|
|
177
|
+
"count(preceding-sibling::*) mod 2 = 0"
|
|
178
|
+
when /\Anth-child\(even\)\z/i
|
|
179
|
+
"count(preceding-sibling::*) mod 2 = 1"
|
|
180
|
+
when /\Anot\((.+)\)\z/
|
|
181
|
+
inner = Regexp.last_match(1).strip
|
|
182
|
+
negate(inner)
|
|
183
|
+
else
|
|
184
|
+
raise Error, "unsupported pseudo :#{pseudo}"
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def negate(inner_selector)
|
|
189
|
+
raise Error, ":not supports a single simple selector" if
|
|
190
|
+
inner_selector.match?(/[\s>+~,]/)
|
|
191
|
+
|
|
192
|
+
tag, preds = parse_simple(inner_selector)
|
|
193
|
+
return "false()" if tag == "*" && preds.empty?
|
|
194
|
+
|
|
195
|
+
checks = []
|
|
196
|
+
checks << "not(self::#{tag})" unless tag == "*"
|
|
197
|
+
checks.concat(preds.map { |p| "not(#{p})" })
|
|
198
|
+
"(#{checks.join(' and ')})"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def attr_predicate(expr)
|
|
202
|
+
# Linear string parsing — CodeQL rb/polynomial-redos flags
|
|
203
|
+
# the equivalent greedy regex (optional groups + .*\z
|
|
204
|
+
# backtrack polynomially on adversarial input).
|
|
205
|
+
expr = expr.strip
|
|
206
|
+
name = expr[/\A[\w:-]+/]
|
|
207
|
+
raise Error, "bad attribute selector: [#{expr}]" unless name
|
|
208
|
+
|
|
209
|
+
rest = expr[name.length..].to_s.strip
|
|
210
|
+
op = rest[/\A[~^$*|]?=/]
|
|
211
|
+
value = (op ? rest[op.length..] : "").to_s.strip
|
|
212
|
+
# No-backreference unquote (CodeQL rb/polynomial-redos: the
|
|
213
|
+
# ('(["']).*\1\z shape backtracks polynomially).
|
|
214
|
+
if value.length > 1 &&
|
|
215
|
+
((value.start_with?("'") && value.end_with?("'")) ||
|
|
216
|
+
(value.start_with?('"') && value.end_with?('"')))
|
|
217
|
+
value = value[1..-2]
|
|
218
|
+
end
|
|
219
|
+
literal = xpath_string_literal(value)
|
|
220
|
+
|
|
221
|
+
case op
|
|
222
|
+
when nil, "" then "@#{name}"
|
|
223
|
+
when "=" then "@#{name} = #{literal}"
|
|
224
|
+
when "^=" then "starts-with(@#{name}, #{literal})"
|
|
225
|
+
when "$=" then "substring(@#{name}, string-length(@#{name}) - string-length(#{literal}) + 1) = #{literal}"
|
|
226
|
+
when "*=" then "contains(@#{name}, #{literal})"
|
|
227
|
+
when "~=" then "contains(concat(' ', normalize-space(@#{name}), ' '), concat(' ', #{literal}, ' '))"
|
|
228
|
+
when "|=" then "@#{name} = #{literal} or starts-with(@#{name}, concat(#{literal}, '-'))"
|
|
229
|
+
else raise Error, "unsupported attribute operator #{op}"
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# An XPath 1.0 string literal: values containing apostrophes
|
|
234
|
+
# cannot escape the delimiter — they go through concat().
|
|
235
|
+
def xpath_string_literal(value)
|
|
236
|
+
return "'#{value}'" unless value.include?("'")
|
|
237
|
+
|
|
238
|
+
parts = value.split("'", -1).map { |seg| "'#{seg}'" }
|
|
239
|
+
"concat(#{parts.join(%(, "'", ))})"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def split_top_level(selector)
|
|
243
|
+
out = []
|
|
244
|
+
depth = 0
|
|
245
|
+
quote = nil
|
|
246
|
+
current = +""
|
|
247
|
+
selector.each_char do |c|
|
|
248
|
+
if quote
|
|
249
|
+
current << c
|
|
250
|
+
quote = nil if c == quote
|
|
251
|
+
elsif ["'", '"'].include?(c)
|
|
252
|
+
quote = c
|
|
253
|
+
current << c
|
|
254
|
+
elsif c == "["
|
|
255
|
+
depth += 1
|
|
256
|
+
current << c
|
|
257
|
+
elsif c == "]"
|
|
258
|
+
depth -= 1
|
|
259
|
+
raise Error, "unbalanced ]" if depth.negative?
|
|
260
|
+
|
|
261
|
+
current << c
|
|
262
|
+
elsif c == "," && depth.zero?
|
|
263
|
+
out << current
|
|
264
|
+
current = +""
|
|
265
|
+
else
|
|
266
|
+
current << c
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
raise Error, "unbalanced [" if depth.positive?
|
|
270
|
+
|
|
271
|
+
out << current
|
|
272
|
+
out.map(&:strip).reject(&:empty?)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
data/lib/moxml/node.rb
CHANGED
|
@@ -384,14 +384,25 @@ module Moxml
|
|
|
384
384
|
xpath(expression, namespaces)
|
|
385
385
|
end
|
|
386
386
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
387
|
+
# CSS through the shared Moxml::Css translator (#362.1): the
|
|
388
|
+
# SAME selector semantics on every adapter — the translation
|
|
389
|
+
# happens once, inside Moxml, so engines cannot diverge.
|
|
390
|
+
# NodeSet results document-order (the engines' native order).
|
|
391
|
+
def css(expression)
|
|
392
|
+
NodeSet.new(
|
|
393
|
+
adapter.xpath(@native, Css.to_xpath(expression,
|
|
394
|
+
absolute: document?)),
|
|
395
|
+
context,
|
|
396
|
+
)
|
|
390
397
|
end
|
|
391
398
|
|
|
392
|
-
def at_css(
|
|
393
|
-
|
|
394
|
-
|
|
399
|
+
def at_css(expression)
|
|
400
|
+
Node.wrap(
|
|
401
|
+
adapter.at_xpath(@native,
|
|
402
|
+
Css.to_xpath(expression,
|
|
403
|
+
absolute: document?)),
|
|
404
|
+
context,
|
|
405
|
+
)
|
|
395
406
|
end
|
|
396
407
|
|
|
397
408
|
def at_css_or_xpath(expression, namespaces = {})
|
data/lib/moxml/version.rb
CHANGED
data/lib/moxml/xsd.rb
CHANGED
|
@@ -19,14 +19,45 @@ module Moxml
|
|
|
19
19
|
# </xs:schema>
|
|
20
20
|
# XSD
|
|
21
21
|
# schema.valid?(doc) # => true/false
|
|
22
|
-
# schema.validate(doc) # => [] or [
|
|
22
|
+
# schema.validate(doc) # => [] or [ValidationError(...)]
|
|
23
23
|
#
|
|
24
24
|
# Adapters without an engine raise Moxml::NotImplementedError at
|
|
25
25
|
# compile; Moxml::XSD.supported?(context) answers without
|
|
26
26
|
# raising. The tier-1 scope is the engine's: documents outside it
|
|
27
27
|
# validate laxly (see the binding's xsd_validate_spec).
|
|
28
28
|
module XSD
|
|
29
|
-
SUPPORTED_ADAPTERS = %i[leptris].freeze
|
|
29
|
+
SUPPORTED_ADAPTERS = %i[leptris nokogiri].freeze
|
|
30
|
+
|
|
31
|
+
# One validation finding, harmonized across adapters (#362.2):
|
|
32
|
+
# +message+ always present; +line+ / +column+ when the engine
|
|
33
|
+
# reports them; +path+ (the element path) when the engine
|
|
34
|
+
# reports it — nil where the payload lacks the datum rather
|
|
35
|
+
# than a guessed placeholder. #to_s preserves the plain-string
|
|
36
|
+
# consumer flows the pre-#362 string contract fed.
|
|
37
|
+
class ValidationError
|
|
38
|
+
attr_reader :message, :line, :column, :path
|
|
39
|
+
|
|
40
|
+
def initialize(message:, line: nil, column: nil, path: nil)
|
|
41
|
+
@message = message.to_s
|
|
42
|
+
@line = line
|
|
43
|
+
@column = column
|
|
44
|
+
@path = path
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_s
|
|
48
|
+
if line
|
|
49
|
+
"#{line}: #{message}"
|
|
50
|
+
else
|
|
51
|
+
message
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def ==(other)
|
|
56
|
+
other.is_a?(ValidationError) &&
|
|
57
|
+
message == other.message && line == other.line &&
|
|
58
|
+
column == other.column && path == other.path
|
|
59
|
+
end
|
|
60
|
+
end
|
|
30
61
|
|
|
31
62
|
class << self
|
|
32
63
|
# Whether the context's adapter ships an XSD validator.
|
|
@@ -51,6 +82,13 @@ module Moxml
|
|
|
51
82
|
# A compiled schema (adapter-owned handle). Validation results
|
|
52
83
|
# are the engine's accumulated messages; [] means valid.
|
|
53
84
|
class Schema
|
|
85
|
+
def check_released!
|
|
86
|
+
return unless released?
|
|
87
|
+
|
|
88
|
+
raise Moxml::AdapterError,
|
|
89
|
+
"schema released (deterministic drop); compile a new one"
|
|
90
|
+
end
|
|
91
|
+
|
|
54
92
|
def initialize(handle, context)
|
|
55
93
|
@handle = handle
|
|
56
94
|
@context = context
|
|
@@ -65,6 +103,7 @@ module Moxml
|
|
|
65
103
|
# visible); pass a Moxml::Document to control the parse
|
|
66
104
|
# yourself.
|
|
67
105
|
def validate(node_or_xml)
|
|
106
|
+
check_released!
|
|
68
107
|
doc = case node_or_xml
|
|
69
108
|
when Moxml::Document then node_or_xml
|
|
70
109
|
when Moxml::Node then node_or_xml.document
|
|
@@ -85,18 +124,35 @@ module Moxml
|
|
|
85
124
|
end
|
|
86
125
|
|
|
87
126
|
def valid?(node_or_xml)
|
|
127
|
+
check_released!
|
|
88
128
|
validate(node_or_xml).empty?
|
|
89
129
|
end
|
|
90
130
|
|
|
91
131
|
# Number of top-level schema declarations the compiler
|
|
92
132
|
# recognized.
|
|
93
133
|
def declarations
|
|
134
|
+
check_released!
|
|
94
135
|
@context.config.adapter.xsd_declarations(@handle)
|
|
95
136
|
end
|
|
96
137
|
|
|
138
|
+
# Deterministic drop (#362.3): true when the adapter freed
|
|
139
|
+
# the engine-side schema NOW; false when the adapter can only
|
|
140
|
+
# wait for GC (documented per adapter). After a true release
|
|
141
|
+
# the faces raise AdapterError — the handle is gone.
|
|
142
|
+
def release
|
|
143
|
+
dropped = @context.config.adapter.xsd_release(@handle)
|
|
144
|
+
@released = true if dropped
|
|
145
|
+
dropped
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def released?
|
|
149
|
+
@released == true
|
|
150
|
+
end
|
|
151
|
+
|
|
97
152
|
# The engine's schema-level compile error, or nil when the
|
|
98
153
|
# schema compiled cleanly.
|
|
99
154
|
def compile_error
|
|
155
|
+
check_released!
|
|
100
156
|
@context.config.adapter.xsd_compile_error(@handle)
|
|
101
157
|
end
|
|
102
158
|
|
data/lib/moxml.rb
CHANGED
|
@@ -84,6 +84,7 @@ module Moxml
|
|
|
84
84
|
autoload :EntityReference, "moxml/entity_reference"
|
|
85
85
|
autoload :DocumentBuilder, "moxml/document_builder"
|
|
86
86
|
autoload :HtmlFragment, "moxml/html_fragment"
|
|
87
|
+
autoload :Css, "moxml/css"
|
|
87
88
|
autoload :Builder, "moxml/builder"
|
|
88
89
|
autoload :Entity, "moxml/entity"
|
|
89
90
|
autoload :EntityRegistry, "moxml/entity_registry"
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# #362.1: CSS selection through the shared Moxml::Css translator —
|
|
6
|
+
# one implementation for every adapter, so selector semantics
|
|
7
|
+
# converge by construction. The correctness oracle is
|
|
8
|
+
# nokogiri-native CSS: the translated XPath must return the same
|
|
9
|
+
# node sets for the corpus.
|
|
10
|
+
RSpec.describe "Moxml CSS selection (#362.1)" do
|
|
11
|
+
let(:source) do
|
|
12
|
+
<<~HTML
|
|
13
|
+
<doc>
|
|
14
|
+
<p id="first" class="lead intro">one</p>
|
|
15
|
+
<p class="lead">two <b>bold</b></p>
|
|
16
|
+
<div class="wrap">
|
|
17
|
+
<p data-k="v">three</p>
|
|
18
|
+
<span>four</span>
|
|
19
|
+
<p>five</p>
|
|
20
|
+
</div>
|
|
21
|
+
<section><article>deep</article></section>
|
|
22
|
+
</doc>
|
|
23
|
+
HTML
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe "Moxml::Css.to_xpath" do
|
|
27
|
+
def xp(sel, absolute: false)
|
|
28
|
+
Moxml::Css.to_xpath(sel, absolute: absolute)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "translates the core shapes" do
|
|
32
|
+
expect(xp("p", absolute: true)).to eq("//p")
|
|
33
|
+
expect(xp(".lead", absolute: true))
|
|
34
|
+
.to include("contains(concat(' ', normalize-space(@class)")
|
|
35
|
+
expect(xp("#first", absolute: true)).to eq("//*[@id = 'first']")
|
|
36
|
+
expect(xp("[data-k]", absolute: true)).to eq("//p/*[@data-k]").or include("[@data-k]")
|
|
37
|
+
expect(xp("div > p", absolute: true)).to eq("//div/p")
|
|
38
|
+
expect(xp("div p", absolute: true)).to eq("//div//p")
|
|
39
|
+
expect(xp("span + p", absolute: true))
|
|
40
|
+
.to eq("//span/following-sibling::*[position() = 1 and name() = 'p']")
|
|
41
|
+
expect(xp("p ~ span", absolute: true))
|
|
42
|
+
.to eq("//p/following-sibling::span")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "supports attribute operators" do
|
|
46
|
+
expect(xp("[href^='h']", absolute: true))
|
|
47
|
+
.to include("starts-with(@href, 'h')")
|
|
48
|
+
expect(xp("[href$='z']", absolute: true)).to include("substring(@href")
|
|
49
|
+
expect(xp("[href*='mid']", absolute: true)).to include("contains(@href, 'mid')")
|
|
50
|
+
expect(xp("[lang|=en]", absolute: true))
|
|
51
|
+
.to include("starts-with(@lang, concat('en', '-')")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "supports positional pseudos" do
|
|
55
|
+
expect(xp("p:first-child", absolute: true))
|
|
56
|
+
.to include("count(preceding-sibling::*) = 0")
|
|
57
|
+
expect(xp("p:nth-child(3)", absolute: true))
|
|
58
|
+
.to include("count(preceding-sibling::*) = 2")
|
|
59
|
+
expect(xp("p:nth-child(odd)", absolute: true))
|
|
60
|
+
.to include("mod 2 = 0")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "negates simple selectors with :not" do
|
|
64
|
+
expect(xp("p:not(.lead)", absolute: true))
|
|
65
|
+
.to include("not(contains(")
|
|
66
|
+
expect(xp(":not(p)", absolute: true)).to include("not(self::p)")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "groups comma selectors into a union" do
|
|
70
|
+
expect(xp("div, section", absolute: true)).to include(" | ")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "raises Css::Error on unsupported shapes" do
|
|
74
|
+
expect { xp("p:hover") }.to raise_error(Moxml::Css::Error, /pseudo/)
|
|
75
|
+
expect { xp("> p") }.to raise_error(Moxml::Css::Error)
|
|
76
|
+
expect { xp("p >>") }.to raise_error(Moxml::Css::Error)
|
|
77
|
+
expect { xp("") }.to raise_error(Moxml::Css::Error)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe "Node#css / at_css" do
|
|
82
|
+
let(:expected) do
|
|
83
|
+
{
|
|
84
|
+
"p" => 4, "#first" => 1, ".lead" => 2, "p.lead" => 2,
|
|
85
|
+
"div p" => 2, "div > p" => 2, "[data-k]" => 1,
|
|
86
|
+
"[data-k=v]" => 1, "p:not(.lead)" => 2,
|
|
87
|
+
"p:first-child" => 2, "p:nth-child(3)" => 1,
|
|
88
|
+
"section article" => 1, "div, section" => 2,
|
|
89
|
+
"span + p" => 1, "p ~ span" => 1
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
%i[nokogiri leptris oga ox rexml].each do |adapter_name|
|
|
94
|
+
describe "on #{adapter_name}" do
|
|
95
|
+
let(:ctx) { Moxml.new(adapter_name) }
|
|
96
|
+
let(:doc) { ctx.parse(source) }
|
|
97
|
+
|
|
98
|
+
it "returns the nokogiri-native CSS node sets" do
|
|
99
|
+
expected.each do |sel, want|
|
|
100
|
+
expect(doc.css(sel).size).to eq(want),
|
|
101
|
+
"#{sel} => #{doc.css(sel).size}, want #{want}"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "at_css returns the first match" do
|
|
106
|
+
expect(doc.at_css("p").text).to eq("one")
|
|
107
|
+
expect(doc.at_css("div p").text).to eq("three")
|
|
108
|
+
expect(doc.at_css("nosuch")).to be_nil
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it "scopes element-subtree selection" do
|
|
114
|
+
ctx = Moxml.new(:leptris)
|
|
115
|
+
doc = ctx.parse(source)
|
|
116
|
+
wrap = doc.at_css("div")
|
|
117
|
+
expect(wrap.css("p").size).to eq(2)
|
|
118
|
+
expect(doc.at_css("doc").css("section article").size).to eq(1)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# #362.2: Schema#validate returns one error shape on every
|
|
6
|
+
# adapter — Moxml::XSD::ValidationError(message:, line:, column:,
|
|
7
|
+
# path:) — so consumers present findings uniformly. Each adapter
|
|
8
|
+
# fills what its engine reports; missing data is nil, never a
|
|
9
|
+
# guessed placeholder.
|
|
10
|
+
RSpec.describe "Moxml::XSD::ValidationError contract (#362.2)" do
|
|
11
|
+
let(:schema_text) do
|
|
12
|
+
<<~XSD
|
|
13
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
14
|
+
<xs:element name="r">
|
|
15
|
+
<xs:complexType>
|
|
16
|
+
<xs:sequence>
|
|
17
|
+
<xs:element name="a"/>
|
|
18
|
+
<xs:element name="b"/>
|
|
19
|
+
</xs:sequence>
|
|
20
|
+
</xs:complexType>
|
|
21
|
+
</xs:element>
|
|
22
|
+
</xs:schema>
|
|
23
|
+
XSD
|
|
24
|
+
end
|
|
25
|
+
let(:bad_doc) { "<r><a/></r>" }
|
|
26
|
+
|
|
27
|
+
%i[nokogiri leptris].each do |adapter_name|
|
|
28
|
+
describe "on #{adapter_name}" do
|
|
29
|
+
let(:ctx) { Moxml.new(adapter_name) }
|
|
30
|
+
let(:schema) { ctx.xsd(schema_text) }
|
|
31
|
+
let(:errors) { schema.validate(ctx.parse(bad_doc)) }
|
|
32
|
+
|
|
33
|
+
it "returns the structured contract" do
|
|
34
|
+
expect(errors).not_to be_empty
|
|
35
|
+
expect(errors).to all(be_a(Moxml::XSD::ValidationError))
|
|
36
|
+
expect(errors.first.message).not_to be_empty
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "to_s preserves plain-string flows" do
|
|
40
|
+
expect(errors.map(&:to_s)).to all(be_a(String))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe "adapter payloads" do
|
|
46
|
+
it "nokogiri carries the line (libxml2 structured errors)" do
|
|
47
|
+
ctx = Moxml.new(:nokogiri)
|
|
48
|
+
errors = ctx.xsd(schema_text).validate(ctx.parse(bad_doc))
|
|
49
|
+
expect(errors.first.line).to eq(1)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "leptris reports message only (line/path ride leptris-ruby#419)" do
|
|
53
|
+
ctx = Moxml.new(:leptris)
|
|
54
|
+
errors = ctx.xsd(schema_text).validate(ctx.parse(bad_doc))
|
|
55
|
+
expect(errors.first.line).to be_nil
|
|
56
|
+
expect(errors.first.path).to be_nil
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe "the ValidationError object" do
|
|
61
|
+
it "compares fieldwise and formats with the line" do
|
|
62
|
+
a = Moxml::XSD::ValidationError.new(message: "boom", line: 3)
|
|
63
|
+
b = Moxml::XSD::ValidationError.new(message: "boom", line: 3)
|
|
64
|
+
c = Moxml::XSD::ValidationError.new(message: "other", line: 3)
|
|
65
|
+
expect(a).to eq(b)
|
|
66
|
+
expect(a).not_to eq(c)
|
|
67
|
+
expect(a.to_s).to eq("3: boom")
|
|
68
|
+
expect(Moxml::XSD::ValidationError.new(message: "bare").to_s)
|
|
69
|
+
.to eq("bare")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# #362.3: Schema#release — deterministic drop where the engine
|
|
6
|
+
# supports it, an explicit no-op-with-false where it cannot, and
|
|
7
|
+
# a documented contract for long-lived validation services.
|
|
8
|
+
RSpec.describe "Moxml::XSD::Schema#release (#362.3)" do
|
|
9
|
+
let(:schema_text) do
|
|
10
|
+
<<~XSD
|
|
11
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
12
|
+
<xs:element name="a" type="xs:string"/>
|
|
13
|
+
</xs:schema>
|
|
14
|
+
XSD
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe "on leptris" do
|
|
18
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
19
|
+
|
|
20
|
+
context "with the release ride armed (binding >= 1.9.342.0)" do
|
|
21
|
+
before do
|
|
22
|
+
unless Moxml::Adapter::Leptris::XSD_RELEASE
|
|
23
|
+
skip "binding #{Leptris::VERSION} predates Schema#release"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "drops the engine schema deterministically" do
|
|
28
|
+
schema = ctx.xsd(schema_text)
|
|
29
|
+
expect(schema.valid?(ctx.parse("<a/>"))).to be(true)
|
|
30
|
+
expect(schema.release).to be(true)
|
|
31
|
+
expect(schema.released?).to be(true)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "raises on faces after release" do
|
|
35
|
+
schema = ctx.xsd(schema_text)
|
|
36
|
+
schema.release
|
|
37
|
+
expect { schema.valid?(ctx.parse("<a/>")) }
|
|
38
|
+
.to raise_error(Moxml::AdapterError, /released/)
|
|
39
|
+
expect { schema.declarations }
|
|
40
|
+
.to raise_error(Moxml::AdapterError, /released/)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "frees without a double free at GC" do
|
|
44
|
+
schema = ctx.xsd(schema_text)
|
|
45
|
+
schema.release
|
|
46
|
+
GC.start
|
|
47
|
+
expect(schema.released?).to be(true)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context "when the binding predates the ride" do
|
|
52
|
+
before do
|
|
53
|
+
if Moxml::Adapter::Leptris::XSD_RELEASE
|
|
54
|
+
skip "binding #{Leptris::VERSION} carries Schema#release"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "answers false (GC-only) and keeps the schema usable" do
|
|
59
|
+
schema = ctx.xsd(schema_text)
|
|
60
|
+
expect(schema.release).to be(false)
|
|
61
|
+
expect(schema).not_to be_released
|
|
62
|
+
expect(schema.valid?(ctx.parse("<a/>"))).to be(true)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
describe "on nokogiri" do
|
|
68
|
+
it "answers false — libxml2 frees at GC, documented" do
|
|
69
|
+
ctx = Moxml.new(:nokogiri)
|
|
70
|
+
schema = ctx.xsd(schema_text)
|
|
71
|
+
expect(schema.release).to be(false)
|
|
72
|
+
expect(schema).not_to be_released
|
|
73
|
+
expect(schema.valid?(ctx.parse("<a/>"))).to be(true)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/spec/moxml/xsd_spec.rb
CHANGED
|
@@ -123,11 +123,14 @@ RSpec.describe Moxml::XSD do
|
|
|
123
123
|
expect(schema.instance_variable_get(:@parsed_source)).to be_nil
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
-
it "normalizes validate entries to
|
|
126
|
+
it "normalizes validate entries to the #362.2 ValidationError contract" do
|
|
127
127
|
schema = ctx.xsd(schema_text)
|
|
128
128
|
errors = schema.validate("<note><wrong/></note>")
|
|
129
129
|
expect(errors).not_to be_empty
|
|
130
|
-
expect(errors).to all(be_a(
|
|
130
|
+
expect(errors).to all(be_a(Moxml::XSD::ValidationError))
|
|
131
|
+
# to_s preserves the plain-string consumer flows.
|
|
132
|
+
expect(errors.map(&:to_s)).to all(be_a(String))
|
|
133
|
+
expect(errors.first.message).not_to be_empty
|
|
131
134
|
end
|
|
132
135
|
|
|
133
136
|
it "derives XSD_SUPPORTED from the engine face gate" do
|
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.128
|
|
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-10-
|
|
11
|
+
date: 2026-10-12 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
|
|
@@ -202,6 +202,7 @@ files:
|
|
|
202
202
|
- lib/moxml/comment.rb
|
|
203
203
|
- lib/moxml/config.rb
|
|
204
204
|
- lib/moxml/context.rb
|
|
205
|
+
- lib/moxml/css.rb
|
|
205
206
|
- lib/moxml/declaration.rb
|
|
206
207
|
- lib/moxml/doctype.rb
|
|
207
208
|
- lib/moxml/document.rb
|
|
@@ -487,6 +488,7 @@ files:
|
|
|
487
488
|
- spec/moxml/comment_spec.rb
|
|
488
489
|
- spec/moxml/config_spec.rb
|
|
489
490
|
- spec/moxml/context_spec.rb
|
|
491
|
+
- spec/moxml/css_spec.rb
|
|
490
492
|
- spec/moxml/cycle_rejection_spec.rb
|
|
491
493
|
- spec/moxml/declaration_preservation_spec.rb
|
|
492
494
|
- spec/moxml/declaration_spec.rb
|
|
@@ -575,6 +577,8 @@ files:
|
|
|
575
577
|
- spec/moxml/xpath/union_spec.rb
|
|
576
578
|
- spec/moxml/xpath_capabilities_spec.rb
|
|
577
579
|
- spec/moxml/xpath_spec.rb
|
|
580
|
+
- spec/moxml/xsd_error_contract_spec.rb
|
|
581
|
+
- spec/moxml/xsd_release_spec.rb
|
|
578
582
|
- spec/moxml/xsd_spec.rb
|
|
579
583
|
- spec/moxml/xslt/stylesheet_spec.rb
|
|
580
584
|
- spec/performance/README.md
|