moxml 0.5.16 → 0.5.18
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/docs/_pages/adapter-protocol.adoc +7 -2
- data/docs/_pages/adapters/leptris.adoc +17 -10
- data/docs/_pages/conversion-apis.adoc +6 -1
- data/lib/moxml/adapter/leptris/document_parts.rb +66 -0
- data/lib/moxml/adapter/leptris/serialize.rb +10 -3
- data/lib/moxml/adapter/leptris.rb +29 -6
- data/lib/moxml/adapter/nokogiri.rb +6 -2
- data/lib/moxml/version.rb +1 -1
- data/spec/moxml/adapter/leptris_spec.rb +46 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c793d84315c4005a5141698aeb716c001d23b7157b768c77cbdb3539a4b08f53
|
|
4
|
+
data.tar.gz: 35c1d60e6d3a8e17e9768c733d028962bcb6e5798e3c8528b18deaebd855ae5f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '092b193420e9117adbd6dade8718474c9a72de401eb0b7a741d36639bbd7e9285c7e53d55f96b482b17bea3382d1da3adb1cae6bbc7b787c552859d80377db39'
|
|
7
|
+
data.tar.gz: 18a342a60a7dda872730dd2a9158aa852948596e21fc4613961d0336ece2f12977d02fe434e7fa64429f3ce73015a4434285f3f052425c4eb6d88c20a9571db7
|
|
@@ -18,7 +18,9 @@ code disagree.
|
|
|
18
18
|
== Method groups
|
|
19
19
|
|
|
20
20
|
Parse and document lifecycle::
|
|
21
|
-
`parse(xml, options, context)
|
|
21
|
+
`parse(xml, options, context)` — `noblanks: true` drops
|
|
22
|
+
whitespace-only text nodes where the engine has the flag
|
|
23
|
+
(Nokogiri, leptris), `create_document`, `create` factories
|
|
22
24
|
(`create_native_element`, `create_native_text`, `create_native_cdata`,
|
|
23
25
|
`create_native_comment`, `create_native_processing_instruction`,
|
|
24
26
|
`create_native_declaration`, `create_native_doctype`,
|
|
@@ -55,7 +57,10 @@ the tracked native; wrappers re-key through `refresh_native!`.
|
|
|
55
57
|
|
|
56
58
|
Serialization::
|
|
57
59
|
`serialize(node, options)`. Options: `indent`, `encoding`,
|
|
58
|
-
`declaration`, `no_declaration`, `expand_empty`, `line_ending
|
|
60
|
+
`declaration`, `no_declaration`, `expand_empty`, `line_ending`,
|
|
61
|
+
`indent_text` (the indent-unit string — Nokogiri and
|
|
62
|
+
leptris >= 1.9.45; leptris text-bearing leaves wait on
|
|
63
|
+
leptris/leptris#658).
|
|
59
64
|
Entity-marker restoration is owned by the wrapper layer
|
|
60
65
|
(`Node#to_xml`), guarded by `entity_bearing?`.
|
|
61
66
|
|
|
@@ -30,6 +30,11 @@ ctx.config.adapter_name # => :leptris / :nokogiri / :rexml (Opal)
|
|
|
30
30
|
|
|
31
31
|
== Parse options
|
|
32
32
|
|
|
33
|
+
`noblanks: true`::
|
|
34
|
+
Drop whitespace-only text nodes (libxml2 `XML_PARSE_NOBLANKS`
|
|
35
|
+
parity, issue #153) — the parse shape canon's pretty-printer
|
|
36
|
+
compares against `Nokogiri::XML(xml, &:noblanks)`.
|
|
37
|
+
|
|
33
38
|
`readonly: true`::
|
|
34
39
|
The binding memoizes every read and refuses mutations — the
|
|
35
40
|
parse-and-read lifecycle for multi-pass consumers (comparison, diff,
|
|
@@ -62,16 +67,18 @@ implementation over one `leptris_node_traverse` call.
|
|
|
62
67
|
|
|
63
68
|
== Known limitations (tracked upstream)
|
|
64
69
|
|
|
65
|
-
* Tab indentation
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
post-hoc rewriting is impossible in mixed content.
|
|
70
|
-
* DOCTYPE internal subsets serialize inline; libxml2 separates the
|
|
71
|
-
declarations with newlines.
|
|
72
|
-
* Document-level PIs are read-only natives (`target=`/`data=`/
|
|
73
|
-
`unlink` rejected); write-through and removal are pending C
|
|
74
|
-
surface (leptris/leptris#612).
|
|
70
|
+
* Tab indentation honors the indent-unit string on leptris >= 1.9.45
|
|
71
|
+
(`indent_text:`), byte-identical to Nokogiri on element-only
|
|
72
|
+
trees; text-bearing leaves still emit spaces until the engine fix
|
|
73
|
+
(leptris/leptris#658).
|
|
75
74
|
* A PI preceding the DOCTYPE reorders on serialization — libleptris
|
|
76
75
|
normalizes DOCTYPE-first and the document node does not record its
|
|
77
76
|
relative position.
|
|
77
|
+
|
|
78
|
+
Resolved with leptris-ruby 1.9.42 (leptris/leptris#636/#612): child
|
|
79
|
+
PIs serialize on their own indented lines, DOCTYPE internal subsets
|
|
80
|
+
newline-separate their declarations (moxml re-formats the subset to
|
|
81
|
+
the libxml2 layout), the stray trailing newline after non-ASCII
|
|
82
|
+
ending elements is gone engine-side, and document-level PIs accept
|
|
83
|
+
`target=`/`content=`/removal with full round-trip. The moxml-side
|
|
84
|
+
compat guards remain as no-ops for older floor bindings.
|
|
@@ -100,4 +100,9 @@ context.parse(src).to_xml(
|
|
|
100
100
|
) == Nokogiri::XML(src).to_xml(indent: 2, encoding: "UTF-8") # => true
|
|
101
101
|
----
|
|
102
102
|
|
|
103
|
-
Known residual divergences (engine-level, tracked upstream):
|
|
103
|
+
Known residual divergences (engine-level, tracked upstream): a PI
|
|
104
|
+
preceding the DOCTYPE reorders (the engine normalizes DOCTYPE-first),
|
|
105
|
+
and tab indentation waits on the binding exposing an indent-unit
|
|
106
|
+
parameter (leptris-ruby#109) — child-PI lines, DOCTYPE subset
|
|
107
|
+
layout, and the non-ASCII trailing-newline quirk were resolved with
|
|
108
|
+
leptris-ruby 1.9.42.
|
|
@@ -113,9 +113,75 @@ module Moxml
|
|
|
113
113
|
return nil unless dt
|
|
114
114
|
|
|
115
115
|
subset = dt.internal_subset if dt.class.method_defined?(:internal_subset)
|
|
116
|
+
subset = format_internal_subset(subset) if LIBXML2_LAYOUT_PARITY
|
|
116
117
|
XmlEmitter.doctype_xml(dt.root_name, dt.public_id, dt.system_id, subset)
|
|
117
118
|
end
|
|
118
119
|
|
|
120
|
+
# libxml2's DTD dump layout (leptris/leptris#636): newline
|
|
121
|
+
# after "[", one after every markup declaration, none after
|
|
122
|
+
# comments (they glue to both neighbors); an empty subset
|
|
123
|
+
# drops the brackets. Returns the INNER text for
|
|
124
|
+
# XmlEmitter.doctype_xml, nil when there is nothing to emit.
|
|
125
|
+
# The engine reports internal_subset as raw source text, so
|
|
126
|
+
# the declarations are re-tokenized — quote-aware, since an
|
|
127
|
+
# attribute default can contain ">".
|
|
128
|
+
def format_internal_subset(subset)
|
|
129
|
+
return nil if subset.nil? || subset.empty?
|
|
130
|
+
|
|
131
|
+
out = +"\n"
|
|
132
|
+
pos = 0
|
|
133
|
+
length = subset.length
|
|
134
|
+
while pos < length
|
|
135
|
+
start = subset.index("<", pos)
|
|
136
|
+
break if start.nil?
|
|
137
|
+
|
|
138
|
+
terminator, skip = if subset[start, 4] == "<!--"
|
|
139
|
+
["-->", 4]
|
|
140
|
+
elsif subset[start, 2] == "<?"
|
|
141
|
+
["?>", 2]
|
|
142
|
+
else
|
|
143
|
+
[nil, 0]
|
|
144
|
+
end
|
|
145
|
+
if terminator
|
|
146
|
+
stop = subset.index(terminator, start + skip)
|
|
147
|
+
break if stop.nil?
|
|
148
|
+
|
|
149
|
+
item_end = stop + terminator.length
|
|
150
|
+
else
|
|
151
|
+
item_end = markup_decl_end(subset, start)
|
|
152
|
+
break if item_end.nil?
|
|
153
|
+
end
|
|
154
|
+
out << subset[start...item_end]
|
|
155
|
+
# Comments carry no trailing newline; declarations do.
|
|
156
|
+
out << "\n" unless subset[start, 4] == "<!--"
|
|
157
|
+
pos = item_end
|
|
158
|
+
end
|
|
159
|
+
out == "\n" ? nil : out
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# End index of a markup declaration starting at `start`:
|
|
163
|
+
# the first ">" outside quotes.
|
|
164
|
+
QUOTE_CHARS = ['"', "'"].freeze
|
|
165
|
+
private_constant :QUOTE_CHARS
|
|
166
|
+
|
|
167
|
+
def markup_decl_end(subset, start)
|
|
168
|
+
quote = nil
|
|
169
|
+
i = start
|
|
170
|
+
length = subset.length
|
|
171
|
+
while i < length
|
|
172
|
+
ch = subset[i]
|
|
173
|
+
if quote
|
|
174
|
+
quote = nil if ch == quote
|
|
175
|
+
elsif QUOTE_CHARS.include?(ch)
|
|
176
|
+
quote = ch
|
|
177
|
+
elsif ch == ">"
|
|
178
|
+
return i + 1
|
|
179
|
+
end
|
|
180
|
+
i += 1
|
|
181
|
+
end
|
|
182
|
+
nil
|
|
183
|
+
end
|
|
184
|
+
|
|
119
185
|
def default_declaration_xml(doc, options)
|
|
120
186
|
encoding = options[:encoding] || doc.encoding
|
|
121
187
|
encoding = "UTF-8" if encoding.to_s.empty?
|
|
@@ -63,14 +63,21 @@ module Moxml
|
|
|
63
63
|
include_decl = options.fetch(:declaration) do
|
|
64
64
|
options[:no_declaration] ? false : document_has_declaration?(node)
|
|
65
65
|
end
|
|
66
|
-
|
|
66
|
+
kwargs = {
|
|
67
67
|
indent: options.fetch(:indent, 0),
|
|
68
68
|
no_decl: !include_decl,
|
|
69
69
|
encoding: options[:encoding],
|
|
70
|
-
|
|
70
|
+
}
|
|
71
|
+
if INDENT_UNIT_SUPPORTED && options[:indent_text].is_a?(String)
|
|
72
|
+
kwargs[:indent_text] = options[:indent_text]
|
|
73
|
+
end
|
|
74
|
+
xml = node.to_xml(**kwargs)
|
|
71
75
|
# Element output always ends with the close tag — but the
|
|
72
76
|
# engine's serializer appends a stray trailing newline when
|
|
73
|
-
# the element's last text child is non-ASCII
|
|
77
|
+
# the element's last text child is non-ASCII (fixed engine
|
|
78
|
+
# side in 1.9.42; kept for older floor bindings). Note: the
|
|
79
|
+
# binding's indent_text kwarg is the display-form boolean,
|
|
80
|
+
# not Nokogiri's indent-unit string — do not forward it.
|
|
74
81
|
xml.sub(/\n+\z/, "")
|
|
75
82
|
end
|
|
76
83
|
|
|
@@ -31,6 +31,24 @@ module Moxml
|
|
|
31
31
|
PREFIXED_ATTR_PREDICATES_NATIVE =
|
|
32
32
|
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.40")
|
|
33
33
|
|
|
34
|
+
# leptris/leptris#636 (leptris-ruby 1.9.42): libxml2-compatible
|
|
35
|
+
# pretty-print — child-PI lines, DOCTYPE internal-subset layout,
|
|
36
|
+
# no stray trailing newline after non-ASCII text, and the indent
|
|
37
|
+
# unit (`to_xml`'s indent_text takes the unit string). Older
|
|
38
|
+
# floor bindings treat indent_text as a display-form boolean, so
|
|
39
|
+
# the passthrough and the parity pins gate on this.
|
|
40
|
+
LIBXML2_LAYOUT_PARITY =
|
|
41
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.42")
|
|
42
|
+
|
|
43
|
+
# leptris-ruby#109 (1.9.45): Element#to_xml takes the
|
|
44
|
+
# indent-unit string, so moxml's per-child document composition
|
|
45
|
+
# can carry `indent_text:` through. Text-bearing leaves still
|
|
46
|
+
# emit spaces until the engine fix (leptris/leptris#658); the
|
|
47
|
+
# boolean display form stays document-level and is never
|
|
48
|
+
# forwarded (the element face raises on it).
|
|
49
|
+
INDENT_UNIT_SUPPORTED =
|
|
50
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.45")
|
|
51
|
+
|
|
34
52
|
NO_PARSE_ERRORS = [].freeze
|
|
35
53
|
private_constant :NO_PARSE_ERRORS
|
|
36
54
|
|
|
@@ -72,7 +90,7 @@ module Moxml
|
|
|
72
90
|
::Leptris::XML::Document.parse(
|
|
73
91
|
processed,
|
|
74
92
|
readonly: options[:readonly] == true,
|
|
75
|
-
options:
|
|
93
|
+
options: parse_flags(options),
|
|
76
94
|
)
|
|
77
95
|
rescue ::Leptris::XML::ParseError => e
|
|
78
96
|
# libleptris has no recovery mode that survives unclosed
|
|
@@ -95,11 +113,16 @@ module Moxml
|
|
|
95
113
|
doc
|
|
96
114
|
end
|
|
97
115
|
|
|
98
|
-
# nil
|
|
99
|
-
# options hash as plain defaults (
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
# nil when no parse flag is requested — the binding treats a
|
|
117
|
+
# nil options hash as plain defaults (matching libxml2/
|
|
118
|
+
# Nokogiri semantics: blanks kept, no ATTLIST defaults).
|
|
119
|
+
# noblanks (issue #153) drops whitespace-only text nodes,
|
|
120
|
+
# libxml2 XML_PARSE_NOBLANKS parity.
|
|
121
|
+
def parse_flags(options)
|
|
122
|
+
flags = 0
|
|
123
|
+
flags |= ::Leptris::XML::ParseOptions::DTDATTR if options[:dtdattr] == true
|
|
124
|
+
flags |= ::Leptris::XML::ParseOptions::NOBLANKS if options[:noblanks] == true
|
|
125
|
+
flags.zero? ? nil : ::Leptris::XML::ParseOptions.new(flags)
|
|
103
126
|
end
|
|
104
127
|
|
|
105
128
|
def parse_errors(native_doc)
|
|
@@ -26,11 +26,13 @@ module Moxml
|
|
|
26
26
|
::Nokogiri::XML::DocumentFragment.parse(processed_xml) do |config|
|
|
27
27
|
config.strict.nonet
|
|
28
28
|
config.recover unless options[:strict]
|
|
29
|
+
config.noblanks if options[:noblanks]
|
|
29
30
|
end
|
|
30
31
|
else
|
|
31
32
|
::Nokogiri::XML(processed_xml, nil, "UTF-8") do |config|
|
|
32
33
|
config.strict.nonet
|
|
33
34
|
config.recover unless options[:strict]
|
|
35
|
+
config.noblanks if options[:noblanks]
|
|
34
36
|
end
|
|
35
37
|
end
|
|
36
38
|
rescue ::Nokogiri::XML::SyntaxError => e
|
|
@@ -429,11 +431,13 @@ module Moxml
|
|
|
429
431
|
custom_decl = "<?xml #{attrs.join(' ')}?>"
|
|
430
432
|
end
|
|
431
433
|
|
|
432
|
-
|
|
434
|
+
serialize_kwargs = {
|
|
433
435
|
indent: options[:indent],
|
|
434
436
|
encoding: options[:encoding],
|
|
435
437
|
save_with: save_options,
|
|
436
|
-
|
|
438
|
+
}
|
|
439
|
+
serialize_kwargs[:indent_text] = options[:indent_text] if options[:indent_text]
|
|
440
|
+
result = node.to_xml(**serialize_kwargs)
|
|
437
441
|
|
|
438
442
|
if custom_decl
|
|
439
443
|
result = "#{custom_decl}\n#{result}"
|
data/lib/moxml/version.rb
CHANGED
|
@@ -157,6 +157,15 @@ RSpec.describe Moxml::Adapter::Leptris do
|
|
|
157
157
|
selfclosing: %(<r><a/><b/><c>t</c><d/></r>),
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
# leptris/leptris#636 (binding 1.9.42): child-PI lines and
|
|
161
|
+
# DOCTYPE internal-subset layout match libxml2.
|
|
162
|
+
if defined?(described_class::LIBXML2_LAYOUT_PARITY) && described_class::LIBXML2_LAYOUT_PARITY
|
|
163
|
+
cases[:pi_child] = %(<r><?pi data?><e/></r>)
|
|
164
|
+
cases[:pi_child_mixed] = %(<r>t<?pi d?><e>x</e>u</r>)
|
|
165
|
+
cases[:doctype_subset] = %(<?xml version="1.0"?><!DOCTYPE r [<!ELEMENT r (#PCDATA)>]><r>t</r>)
|
|
166
|
+
cases[:doctype_subset_multi] = %(<?xml version="1.0"?><!DOCTYPE r [<!ELEMENT r (#PCDATA)><!ATTLIST e a CDATA "d">]><r><e/></r>)
|
|
167
|
+
end
|
|
168
|
+
|
|
160
169
|
cases.each do |name, source|
|
|
161
170
|
target = Nokogiri::XML(source).to_xml(indent: 2, encoding: "UTF-8")
|
|
162
171
|
output = ctx.parse(source).to_xml(
|
|
@@ -166,6 +175,43 @@ RSpec.describe Moxml::Adapter::Leptris do
|
|
|
166
175
|
end
|
|
167
176
|
end
|
|
168
177
|
|
|
178
|
+
it "round-trips document-level PI mutations through serialization" do
|
|
179
|
+
# leptris/leptris#612: parse-created document PIs carry doc
|
|
180
|
+
# linkage — the setters work and the tree round-trips.
|
|
181
|
+
doc = ctx.parse("<?pi-prolog before?><root/>")
|
|
182
|
+
pi = doc.children.to_a.first
|
|
183
|
+
pi.target = "renamed"
|
|
184
|
+
pi.content = "changed"
|
|
185
|
+
expect(doc.to_xml).to include("<?renamed changed?>")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it "matches raw Nokogiri with tab units on element-only trees" do
|
|
189
|
+
skip "requires the indent unit (leptris 1.9.45+)" unless described_class::INDENT_UNIT_SUPPORTED
|
|
190
|
+
|
|
191
|
+
source = %(<r><a><b/></a><c><d/><e/></c></r>)
|
|
192
|
+
target = Nokogiri::XML(source).to_xml(indent: 2, indent_text: "\t", encoding: "UTF-8")
|
|
193
|
+
output = ctx.parse(source).to_xml(
|
|
194
|
+
indent: 2, declaration: true, expand_empty: false,
|
|
195
|
+
encoding: "UTF-8", indent_text: "\t"
|
|
196
|
+
)
|
|
197
|
+
expect(output).to eq(target)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "drops space-only text nodes with noblanks and matches Nokogiri (issue #153)" do
|
|
201
|
+
source = %(<a><t>1</t> <n/></a>)
|
|
202
|
+
recipe = {
|
|
203
|
+
indent: 2, declaration: true, expand_empty: false, encoding: "UTF-8"
|
|
204
|
+
}
|
|
205
|
+
target = Nokogiri::XML(source, &:noblanks).to_xml(indent: 2, encoding: "UTF-8")
|
|
206
|
+
|
|
207
|
+
doc = ctx.parse(source, noblanks: true)
|
|
208
|
+
expect(doc.root.children.to_a.select(&:text?)).to be_empty
|
|
209
|
+
expect(doc.to_xml(recipe)).to eq(target)
|
|
210
|
+
|
|
211
|
+
via_nokogiri = Moxml.new(:nokogiri).parse(source, noblanks: true).to_xml(recipe)
|
|
212
|
+
expect(via_nokogiri).to eq(target)
|
|
213
|
+
end
|
|
214
|
+
|
|
169
215
|
it "reports the tracked native for a re-added document PI" do
|
|
170
216
|
doc = ctx.parse("<?pi-src orig?><root/>")
|
|
171
217
|
moved = doc.children.to_a[0]
|
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.18
|
|
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-08-
|
|
11
|
+
date: 2026-08-31 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
|