moxml 0.5.142 → 0.5.145
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 +10 -0
- data/lib/moxml/adapter/nokogiri.rb +16 -0
- data/lib/moxml/document.rb +73 -0
- data/lib/moxml/version.rb +1 -1
- data/spec/consistency/xpath_idioms_spec.rb +22 -3
- data/spec/consistency/xslt_spec.rb +3 -3
- data/spec/moxml/c14n/adapter_delegation_spec.rb +2 -2
- data/spec/moxml/c14n/subset_spec.rb +2 -2
- data/spec/moxml/xhtml_output_spec.rb +79 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 042f6cd21672a36e869b19c158819a3624d3a79a53de1fd083e83a377b577442
|
|
4
|
+
data.tar.gz: 7cacbfbd44bb9a87aa252a638b8ac9d25fb5cb67b6e3845c8fdf9c5cf943df24
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 507216b5acdf26ae9cd5b4d69c12d2519b949fbe2017c39a804954a04659780c5432b4fe8b11262642ca1699e09eb453af08e230a653a9b530f55404b6d6f5f1
|
|
7
|
+
data.tar.gz: 5d19c7d3d7e1ad18864d13b0b3eaf69f481b5f1a0c68fd652f29cd9bff4e7b9f95f4adf57698d77849371bc635f9ba3ef36c945f12a1b63e2a350e50fc886bbb
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -89,6 +89,15 @@ module Moxml
|
|
|
89
89
|
# no declaration. indent:/indent_text: thread the
|
|
90
90
|
# pretty-print knobs where the adapter's engine carries
|
|
91
91
|
# them. Adapters without an HTML serializer raise.
|
|
92
|
+
# XHTML-shaped serialization (issue #390): void elements
|
|
93
|
+
# self-closed, non-void paired, indent knobs composing.
|
|
94
|
+
# Adapters with an HTML engine answer this; the default
|
|
95
|
+
# raises so consumers get a named error, not mangled bytes.
|
|
96
|
+
def serialize_xhtml(_node, _options = {})
|
|
97
|
+
raise Moxml::NotImplementedError,
|
|
98
|
+
"XHTML serialization requires an adapter with an HTML engine"
|
|
99
|
+
end
|
|
100
|
+
|
|
92
101
|
def serialize_html(_node, _options = {})
|
|
93
102
|
raise Moxml::AdapterError.new(
|
|
94
103
|
"HTML serialization is not supported by the #{name.split('::').last} adapter",
|
|
@@ -1212,6 +1212,16 @@ module Moxml
|
|
|
1212
1212
|
node.to_html(**kwargs)
|
|
1213
1213
|
end
|
|
1214
1214
|
|
|
1215
|
+
# XHTML shape (issue #390): the binding's XML method on an
|
|
1216
|
+
# HTML document — void elements short-form (<br/>), non-void
|
|
1217
|
+
# paired, raw text escaped, no declaration. The indent knobs
|
|
1218
|
+
# thread the #354 machinery.
|
|
1219
|
+
def serialize_xhtml(node, options = {})
|
|
1220
|
+
node.to_xml(no_decl: true, **options.slice(:indent,
|
|
1221
|
+
:indent_text, :encoding)
|
|
1222
|
+
.compact)
|
|
1223
|
+
end
|
|
1224
|
+
|
|
1215
1225
|
def parse_html_fragment(html, context_tag = :body, _context = nil)
|
|
1216
1226
|
unless HTML_FRAGMENT
|
|
1217
1227
|
raise Moxml::AdapterError.new(
|
|
@@ -189,6 +189,22 @@ module Moxml
|
|
|
189
189
|
node.to_html(**kwargs)
|
|
190
190
|
end
|
|
191
191
|
|
|
192
|
+
# Nokogiri's AS_XHTML save flag is the byte reference for the
|
|
193
|
+
# XHTML shape (issue #390): FORMAT layout, self-closed voids,
|
|
194
|
+
# no declaration.
|
|
195
|
+
def serialize_xhtml(node, options = {})
|
|
196
|
+
save_with = ::Nokogiri::XML::Node::SaveOptions::FORMAT |
|
|
197
|
+
::Nokogiri::XML::Node::SaveOptions::AS_XHTML |
|
|
198
|
+
::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION
|
|
199
|
+
io = StringIO.new
|
|
200
|
+
kwargs = { save_with: save_with }
|
|
201
|
+
kwargs[:indent] = options[:indent] if options[:indent]
|
|
202
|
+
kwargs[:indent_text] = options[:indent_text] if options[:indent_text]
|
|
203
|
+
kwargs[:encoding] = options[:encoding] if options[:encoding]
|
|
204
|
+
node.write_to(io, **kwargs)
|
|
205
|
+
io.string
|
|
206
|
+
end
|
|
207
|
+
|
|
192
208
|
# WHATWG fragment parse through Nokogiri::HTML5.fragment
|
|
193
209
|
# (issue #353) — the same tree construction the leptris
|
|
194
210
|
# lane composes over its WHATWG engine.
|
data/lib/moxml/document.rb
CHANGED
|
@@ -129,12 +129,30 @@ module Moxml
|
|
|
129
129
|
# (leptris >= the #354 ride; nokogiri passes them to the
|
|
130
130
|
# engine). Adapters without the knob raise Moxml::AdapterError.
|
|
131
131
|
def to_html(options = {})
|
|
132
|
+
return to_xhtml(options) if options[:xhtml]
|
|
133
|
+
|
|
132
134
|
result = adapter.serialize_html(@native, options)
|
|
133
135
|
apply_line_ending(result,
|
|
134
136
|
options.fetch(:line_ending, Config::LINE_ENDING_LF))
|
|
135
137
|
result
|
|
136
138
|
end
|
|
137
139
|
|
|
140
|
+
# XHTML-shaped serialization (issue #390): void elements
|
|
141
|
+
# self-closed (<br/>), non-void elements paired, raw text
|
|
142
|
+
# escaped (the XML method's §16.2-compatible escaping), no
|
|
143
|
+
# declaration unless asked. Composes with the indent knobs.
|
|
144
|
+
# Also selected by to_html(xhtml: true).
|
|
145
|
+
#
|
|
146
|
+
# Byte reference: Nokogiri's FORMAT | AS_XHTML | NO_DECLARATION
|
|
147
|
+
# save flag; canon pins engine-vs-Nokogiri parity on its display
|
|
148
|
+
# corpus before flipping its lane.
|
|
149
|
+
def to_xhtml(options = {})
|
|
150
|
+
result = adapter.serialize_xhtml(@native, options)
|
|
151
|
+
apply_line_ending(result,
|
|
152
|
+
options.fetch(:line_ending, Config::LINE_ENDING_LF))
|
|
153
|
+
result
|
|
154
|
+
end
|
|
155
|
+
|
|
138
156
|
def create_element(name)
|
|
139
157
|
Wrappers::Element.new(adapter.create_element(name, owner_doc: @native), context)
|
|
140
158
|
end
|
|
@@ -209,7 +227,21 @@ module Moxml
|
|
|
209
227
|
self
|
|
210
228
|
end
|
|
211
229
|
|
|
230
|
+
# Relative name-test paths on the document resolve laxly, as
|
|
231
|
+
# descendant-or-self anchored at the root — Nokogiri's own
|
|
232
|
+
# Document#at/#search semantics (moxml#393; its #xpath/#at_xpath are
|
|
233
|
+
# strict document-context, where the only element child is the root,
|
|
234
|
+
# and the adapters disagree there anyway: leptris's engine returns []
|
|
235
|
+
# for every relative path — leptris-ruby#502 — while Nokogiri's
|
|
236
|
+
# #at/#xpath disagree with each other). Every strict match survives
|
|
237
|
+
# the lax rewrite (the only strict-matchable first step is the root
|
|
238
|
+
# itself), so this is purely additive. Non-name-test expressions
|
|
239
|
+
# (axes, node types, ".", "..") keep document context.
|
|
240
|
+
RELATIVE_NAME_TEST = /\A\s*(\*|[A-Za-z_][\w.-]*)(\s*\[|\s*\||\z|\/)/
|
|
241
|
+
|
|
212
242
|
def xpath(expression, namespaces = nil)
|
|
243
|
+
expression = lax_document_path(expression) if relative_name_test?(expression)
|
|
244
|
+
|
|
213
245
|
result = adapter.xpath(@native, expression, namespaces)
|
|
214
246
|
|
|
215
247
|
# Handle different result types:
|
|
@@ -228,6 +260,8 @@ module Moxml
|
|
|
228
260
|
end
|
|
229
261
|
|
|
230
262
|
def at_xpath(expression, namespaces = nil)
|
|
263
|
+
expression = lax_document_path(expression) if relative_name_test?(expression)
|
|
264
|
+
|
|
231
265
|
if (native_node = adapter.at_xpath(@native, expression, namespaces))
|
|
232
266
|
Moxml::Node.wrap(native_node, context)
|
|
233
267
|
end
|
|
@@ -250,5 +284,44 @@ module Moxml
|
|
|
250
284
|
def find_all(xpath)
|
|
251
285
|
xpath(xpath).to_a
|
|
252
286
|
end
|
|
287
|
+
|
|
288
|
+
private
|
|
289
|
+
|
|
290
|
+
def relative_name_test?(expression)
|
|
291
|
+
expression.match?(RELATIVE_NAME_TEST)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Anchor every top-level union branch at the document root
|
|
295
|
+
# ("//"= descendant-or-self): "a/b | c" -> "//a/b | //c" — one
|
|
296
|
+
# adapter call, engine document order preserved, and the
|
|
297
|
+
# attribute-terminal idiom (pinned below) stays intact, which the
|
|
298
|
+
# ".//" form does not (libxml2 rejects .//r/a/@id).
|
|
299
|
+
def lax_document_path(expression)
|
|
300
|
+
split_top_level_unions(expression)
|
|
301
|
+
.map { |branch| "//#{branch.strip}" }
|
|
302
|
+
.join(" | ")
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def split_top_level_unions(expression)
|
|
306
|
+
branches = [+""]
|
|
307
|
+
depth = 0
|
|
308
|
+
quote = nil
|
|
309
|
+
expression.each_char do |ch|
|
|
310
|
+
if quote
|
|
311
|
+
quote = nil if ch == quote
|
|
312
|
+
elsif ["'", '"'].include?(ch)
|
|
313
|
+
quote = ch
|
|
314
|
+
elsif ["(", "["].include?(ch)
|
|
315
|
+
depth += 1
|
|
316
|
+
elsif [")", "]"].include?(ch)
|
|
317
|
+
depth -= 1
|
|
318
|
+
elsif ch == "|" && depth.zero?
|
|
319
|
+
branches << +""
|
|
320
|
+
next
|
|
321
|
+
end
|
|
322
|
+
branches.last << ch
|
|
323
|
+
end
|
|
324
|
+
branches
|
|
325
|
+
end
|
|
253
326
|
end
|
|
254
327
|
end
|
data/lib/moxml/version.rb
CHANGED
|
@@ -53,9 +53,28 @@ RSpec.describe "XPath idioms across adapters" do
|
|
|
53
53
|
expect(doc.xpath("//a/@id").first&.value).to eq("x")
|
|
54
54
|
expect(doc.xpath("//@id").first&.value).to eq("x")
|
|
55
55
|
expect(doc.xpath("r/a/@id").first&.value).to eq("x")
|
|
56
|
-
expect(
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
expect(doc.xpath("//*[local-name()='a']/@id").first&.value).to eq("x")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# moxml#393: a relative name-test path on the DOCUMENT resolves
|
|
60
|
+
# laxly, descendant-anchored at the root — Nokogiri's own
|
|
61
|
+
# Document#at/#search semantics, now pinned for every adapter.
|
|
62
|
+
# Axes, node types and "." keep strict document context.
|
|
63
|
+
it "resolves document-context relative name-test paths from the root" do
|
|
64
|
+
doc = ctx.parse(plain_xml)
|
|
65
|
+
expect(doc.at("a/b")&.name).to eq("b")
|
|
66
|
+
expect(doc.at_xpath("a/b")&.name).to eq("b")
|
|
67
|
+
expect(doc.xpath("a/b").map(&:name)).to eq(["b"])
|
|
68
|
+
expect(doc.xpath("a | r").map(&:name).sort).to eq(%w(a r))
|
|
69
|
+
expect(doc.xpath("missing").map(&:name)).to eq([])
|
|
70
|
+
expect(doc.at_xpath("missing")).to be_nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "keeps non-name-test expressions in document context" do
|
|
74
|
+
doc = ctx.parse(plain_xml)
|
|
75
|
+
expect(doc.at_xpath(".")).to be_truthy
|
|
76
|
+
expect(doc.at_xpath("/*")&.name).to eq("r")
|
|
77
|
+
expect(doc.at_xpath("/r")&.name).to eq("r")
|
|
59
78
|
end
|
|
60
79
|
end
|
|
61
80
|
end
|
|
@@ -13,7 +13,7 @@ SUPPORTED = %i[nokogiri leptris].select do |name|
|
|
|
13
13
|
Moxml::Adapter::AVAILABLE_ADAPTERS.include?(name)
|
|
14
14
|
end.freeze
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
XSLT_CORPUS = {
|
|
17
17
|
"value-of with count" => [
|
|
18
18
|
<<~XSL,
|
|
19
19
|
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
|
|
@@ -82,7 +82,7 @@ RSpec.describe "Moxml::XSLT cross-engine agreement" do
|
|
|
82
82
|
output.sub(/\A<\?xml[^>]*\?>\s*/, "")
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
XSLT_CORPUS.each do |label, (sheet_xml, input, expected_re)|
|
|
86
86
|
it "transforms #{label}" do
|
|
87
87
|
sheet = ctx.xslt(sheet_xml)
|
|
88
88
|
doc = ctx.parse(input)
|
|
@@ -97,7 +97,7 @@ RSpec.describe "Moxml::XSLT cross-engine agreement" do
|
|
|
97
97
|
return unless SUPPORTED.size == 2
|
|
98
98
|
|
|
99
99
|
first, second = SUPPORTED.map { |name| Moxml.new(name) }
|
|
100
|
-
|
|
100
|
+
XSLT_CORPUS.each do |label, (sheet_xml, input, _)|
|
|
101
101
|
a = first.xslt(sheet_xml).apply_to_string(first.parse(input))
|
|
102
102
|
b = second.xslt(sheet_xml).apply_to_string(second.parse(input))
|
|
103
103
|
expect(Moxml::C14n.equivalent?(a, b))
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
require "moxml/c14n"
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
C14N_DELEGATION_CORPUS = {
|
|
7
7
|
"cdata content becomes escaped text" => [
|
|
8
8
|
"<doc><![CDATA[a < b]]>tail</doc>",
|
|
9
9
|
"<doc>a < btail</doc>",
|
|
@@ -45,7 +45,7 @@ RSpec.describe "Moxml::C14n cross-adapter canonicalization" do
|
|
|
45
45
|
context "with the #{adapter_name} adapter" do
|
|
46
46
|
let(:ctx) { Moxml.new(adapter_name) }
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
C14N_DELEGATION_CORPUS.each do |label, (input, expected)|
|
|
49
49
|
it "canonicalizes #{label}" do
|
|
50
50
|
root = ctx.parse(input).root
|
|
51
51
|
expect(Moxml::C14n.canonicalize_inclusive10(root)).to eq(expected)
|
|
@@ -9,7 +9,7 @@ ENVELOPED_DOC = %(<env><payload id="p1">data & more</payload>#{SIG}<tail/></
|
|
|
9
9
|
ENVELOPED_EXCLUDE = '//*[not(ancestor-or-self::*[local-name()="Signature"])]'
|
|
10
10
|
SIGNATURE_XPATH = '//*[local-name()="Signature"]'
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
C14N_SUBSET_CORPUS = [
|
|
13
13
|
["apex attraction + text",
|
|
14
14
|
%(<a xml:lang="en"><b id="1"><c>t</c></b><d/></a>), "//b",
|
|
15
15
|
%(<b id="1" xml:lang="en"></b>)],
|
|
@@ -59,7 +59,7 @@ RSpec.describe "Moxml::C14n subset canonicalization" do
|
|
|
59
59
|
context "with the #{adapter_name} adapter" do
|
|
60
60
|
let(:ctx) { Moxml.new(adapter_name) }
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
C14N_SUBSET_CORPUS.each do |label, xml, xpath, expected|
|
|
63
63
|
next if label == "text selection" && adapter_name == :rexml
|
|
64
64
|
|
|
65
65
|
# rexml's adapter xpath does not return text nodes for
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Issue #390: XHTML-shaped serialization — void elements self-closed
|
|
6
|
+
# (<br/>), non-void elements paired, raw text escaped, no XML
|
|
7
|
+
# declaration. Composes with the #354 indent knobs. Nokogiri's
|
|
8
|
+
# FORMAT | AS_XHTML | NO_DECLARATION save flag is the byte
|
|
9
|
+
# reference; canon's fixture_ready display lane rides the face.
|
|
10
|
+
RSpec.describe "XHTML output mode" do
|
|
11
|
+
let(:doc) do
|
|
12
|
+
Moxml.new(ENV.fetch("XHTML_ADAPTER", :leptris)).parse_html(
|
|
13
|
+
'<!DOCTYPE html><html><body><p>a<br>b</p><img src="x.png">' \
|
|
14
|
+
"<hr></body></html>",
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe "Document#to_xhtml" do
|
|
19
|
+
it "self-closes void elements with the XHTML slash" do
|
|
20
|
+
out = doc.to_xhtml
|
|
21
|
+
expect(out).not_to include("<br>")
|
|
22
|
+
expect(out).to include("<br/>")
|
|
23
|
+
expect(out).to include('<img src="x.png"/>')
|
|
24
|
+
expect(out).to include("<hr/>")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "pairs non-void elements" do
|
|
28
|
+
out = doc.to_xhtml
|
|
29
|
+
expect(out).to include("<p>a<br/>b</p>")
|
|
30
|
+
expect(out).not_to include("<p/>")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "omits the XML declaration" do
|
|
34
|
+
expect(doc.to_xhtml).not_to start_with("<?xml")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "keeps the doctype as document content" do
|
|
38
|
+
expect(doc.to_xhtml).to include("<!DOCTYPE html>")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "composes with the indent knobs" do
|
|
42
|
+
out = doc.to_xhtml(indent: 2)
|
|
43
|
+
expect(out).to include("\n <body>")
|
|
44
|
+
expect(out).to include("\n <p>a<br/>b</p>")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "escapes raw text per the XML method" do
|
|
48
|
+
doc = Moxml.new(ENV.fetch("XHTML_ADAPTER", :leptris)).parse_html(
|
|
49
|
+
"<!DOCTYPE html><html><body><script>if (a < b) { x(); }</script>" \
|
|
50
|
+
"</body></html>",
|
|
51
|
+
)
|
|
52
|
+
out = doc.to_xhtml
|
|
53
|
+
expect(out).not_to include("a < b")
|
|
54
|
+
expect(out).to include("<") if out.include?("script")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe "Document#to_html(xhtml: true)" do
|
|
59
|
+
it "selects the same shape" do
|
|
60
|
+
expect(doc.to_html(xhtml: true)).to eq(doc.to_xhtml)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "still serves the HTML shape without the flag" do
|
|
64
|
+
expect(doc.to_html).to include("<br>")
|
|
65
|
+
expect(doc.to_html).not_to include("<br/>")
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
describe "adapter coverage" do
|
|
70
|
+
it "serves the nokogiri lane (the byte reference)" do
|
|
71
|
+
doc = Moxml.new(:nokogiri).parse_html(
|
|
72
|
+
"<!DOCTYPE html><html><body><p>a<br>b</p></body></html>",
|
|
73
|
+
)
|
|
74
|
+
out = doc.to_xhtml
|
|
75
|
+
expect(out).to include("<br/>")
|
|
76
|
+
expect(out).to include("<p>a<br/>b</p>")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
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.145
|
|
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-19 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
|
|
@@ -560,6 +560,7 @@ files:
|
|
|
560
560
|
- spec/moxml/text_spec.rb
|
|
561
561
|
- spec/moxml/to_html_spec.rb
|
|
562
562
|
- spec/moxml/version_spec.rb
|
|
563
|
+
- spec/moxml/xhtml_output_spec.rb
|
|
563
564
|
- spec/moxml/xml_emitter_spec.rb
|
|
564
565
|
- spec/moxml/xml_utils/.gitkeep
|
|
565
566
|
- spec/moxml/xml_utils/encoder_spec.rb
|