moxml 0.5.12 → 0.5.13
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 +113 -0
- data/docs/_pages/adapters/index.adoc +11 -5
- data/docs/_pages/adapters/leptris.adoc +69 -0
- data/docs/_pages/adapters/libxml.adoc +1 -1
- data/docs/_pages/adapters/nokogiri.adoc +1 -1
- data/docs/_pages/conversion-apis.adoc +78 -0
- data/docs/_pages/index.adoc +6 -0
- data/lib/moxml/adapter/leptris/document_parts.rb +208 -0
- data/lib/moxml/adapter/leptris/markers.rb +51 -0
- data/lib/moxml/adapter/leptris/materialize.rb +110 -0
- data/lib/moxml/adapter/leptris/sax_bridge.rb +56 -0
- data/lib/moxml/adapter/leptris/serialize.rb +127 -0
- data/lib/moxml/adapter/leptris.rb +14 -518
- data/lib/moxml/adapter/libxml/serialize.rb +497 -0
- data/lib/moxml/adapter/libxml.rb +20 -462
- data/lib/moxml/materializer.rb +39 -17
- data/lib/moxml/version.rb +1 -1
- data/spec/moxml/adapter/leptris_spec.rb +14 -4
- metadata +11 -2
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Adapter
|
|
5
|
+
class Libxml
|
|
6
|
+
# Wire-format knowledge: the C-engine fast path with its guards,
|
|
7
|
+
# the Ruby fallback walker family, namespace-aware emission, and
|
|
8
|
+
# the ReDoS-hardened empty-element expansion.
|
|
9
|
+
module Serialize
|
|
10
|
+
# Possessive-quantifier form: no backtracking, so pathological
|
|
11
|
+
# attribute content cannot blow up the expansion pass.
|
|
12
|
+
EMPTY_ELEMENT_EXPANSION_RE = %r{
|
|
13
|
+
<([A-Za-z_][\w.:-]*+)
|
|
14
|
+
((?:"[^"]*+"|'[^']*+'|[^<>"'/]++)*+)
|
|
15
|
+
/>
|
|
16
|
+
}x
|
|
17
|
+
private_constant :EMPTY_ELEMENT_EXPANSION_RE
|
|
18
|
+
|
|
19
|
+
NON_WHITESPACE_RE = /\S/
|
|
20
|
+
private_constant :NON_WHITESPACE_RE
|
|
21
|
+
|
|
22
|
+
def serialize(node, options = {})
|
|
23
|
+
# FIRST: Check if node is any kind of wrapper with custom to_xml
|
|
24
|
+
if node.is_a?(CustomizedLibxml::Node) || node.is_a?(DoctypeWrapper)
|
|
25
|
+
return node.to_xml
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
native_node = unpatch_node(node)
|
|
29
|
+
return "" unless native_node
|
|
30
|
+
|
|
31
|
+
if native_node.is_a?(::LibXML::XML::Document)
|
|
32
|
+
output = +""
|
|
33
|
+
|
|
34
|
+
# Check if we should include declaration
|
|
35
|
+
# Priority: explicit no_declaration option > default (include)
|
|
36
|
+
should_include_decl = if options.key?(:no_declaration)
|
|
37
|
+
!options[:no_declaration]
|
|
38
|
+
else
|
|
39
|
+
# Default: include declaration
|
|
40
|
+
true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if should_include_decl
|
|
44
|
+
# Check if declaration was explicitly managed
|
|
45
|
+
decl = attachments.get(native_node, :declaration)
|
|
46
|
+
if decl
|
|
47
|
+
# Only output declaration if it exists and wasn't removed
|
|
48
|
+
output << decl.to_xml unless decl.removed
|
|
49
|
+
else
|
|
50
|
+
# No declaration stored - create default
|
|
51
|
+
version = native_node.version || "1.0"
|
|
52
|
+
encoding_val = options[:encoding] ||
|
|
53
|
+
encoding_to_string(native_node.encoding) ||
|
|
54
|
+
"UTF-8"
|
|
55
|
+
|
|
56
|
+
# Don't add standalone="yes" by default - only if explicitly set
|
|
57
|
+
decl = CustomizedLibxml::Declaration.new(
|
|
58
|
+
native_node,
|
|
59
|
+
version,
|
|
60
|
+
encoding_val,
|
|
61
|
+
nil, # No standalone by default
|
|
62
|
+
)
|
|
63
|
+
attachments.set(native_node, :declaration, decl)
|
|
64
|
+
output << decl.to_xml
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Add DOCTYPE if stored on document
|
|
69
|
+
doctype_wrapper = attachments.get(native_node, :doctype)
|
|
70
|
+
if doctype_wrapper
|
|
71
|
+
output << "\n" unless output.empty?
|
|
72
|
+
output << doctype_wrapper.to_xml
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Parse-time document-level parts live on the chain around
|
|
76
|
+
# the root (prolog/epilog PIs and comments); programmatic
|
|
77
|
+
# ones stay in attachments. The chain nodes serialize in
|
|
78
|
+
# document order around the root output below.
|
|
79
|
+
chain_pre = []
|
|
80
|
+
chain_post = []
|
|
81
|
+
if native_node.root
|
|
82
|
+
past_root = false
|
|
83
|
+
chain_node = native_node.child
|
|
84
|
+
while chain_node
|
|
85
|
+
case chain_node.node_type
|
|
86
|
+
when ::LibXML::XML::Node::ELEMENT_NODE then past_root = true
|
|
87
|
+
when ::LibXML::XML::Node::PI_NODE, ::LibXML::XML::Node::COMMENT_NODE
|
|
88
|
+
(past_root ? chain_post : chain_pre) << chain_node.to_s
|
|
89
|
+
end
|
|
90
|
+
chain_node = chain_node.next
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
chain_pre.each do |part|
|
|
94
|
+
output << "\n" unless output.empty?
|
|
95
|
+
output << part
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Add document-level processing instructions if stored
|
|
99
|
+
pis = attachments.get(native_node, :pis)
|
|
100
|
+
if pis && !pis.empty?
|
|
101
|
+
pis.each do |pi|
|
|
102
|
+
output << "\n" unless output.empty?
|
|
103
|
+
output << pi.to_xml
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Add text nodes if stored (for documents without root)
|
|
108
|
+
texts = attachments.get(native_node, :texts)
|
|
109
|
+
if texts && !texts.empty?
|
|
110
|
+
texts.each do |text|
|
|
111
|
+
output << "\n" unless output.empty?
|
|
112
|
+
output << text.to_xml
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
if native_node.root
|
|
117
|
+
indent_size = options[:indent].is_a?(Integer) && options[:indent].positive? ? options[:indent] : 0
|
|
118
|
+
# `eref_active` is computed once here and threaded through the
|
|
119
|
+
# recursion so that the per-element `attachments.key?` Monitor
|
|
120
|
+
# sync only fires for docs that actually have entity refs.
|
|
121
|
+
eref_active = entity_ref_registry(native_node).active?
|
|
122
|
+
root_output = native_root_output(native_node, indent_size)
|
|
123
|
+
root_output ||= serialize_element_with_namespaces(
|
|
124
|
+
native_node.root,
|
|
125
|
+
include_ns: true,
|
|
126
|
+
indent_size: indent_size,
|
|
127
|
+
depth: 0,
|
|
128
|
+
eref_active: eref_active,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
output << "\n" << root_output unless output.empty?
|
|
132
|
+
output << root_output if output.empty?
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
unless chain_post.empty?
|
|
136
|
+
output << "\n" unless output.empty?
|
|
137
|
+
output << chain_post.join("\n") << "\n"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
output
|
|
141
|
+
else
|
|
142
|
+
serialize_element_with_namespaces(native_node, include_ns: true)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Serialize the root subtree with libxml's C serializer plus
|
|
147
|
+
# moxml-canonical corrections — roughly 25x faster and ~2600x
|
|
148
|
+
# fewer allocations than the Ruby walker. Returns nil whenever
|
|
149
|
+
# a guard says the walker is still required.
|
|
150
|
+
def native_root_output(native_doc, indent_size)
|
|
151
|
+
return nil unless indent_size == 2
|
|
152
|
+
return nil if entity_ref_registry(native_doc).active?
|
|
153
|
+
|
|
154
|
+
output = native_doc.root.to_s
|
|
155
|
+
|
|
156
|
+
# The walker strips prefixed names on parsed namespaced
|
|
157
|
+
# children; that behavior is load-bearing, so namespaced
|
|
158
|
+
# output keeps the walker.
|
|
159
|
+
return nil if output.include?("xmlns")
|
|
160
|
+
|
|
161
|
+
# The native serializer escapes non-ASCII codepoints in
|
|
162
|
+
# attribute values as numeric character references
|
|
163
|
+
# (x="©"); the walker emits literal UTF-8
|
|
164
|
+
return nil if output.include?("&#x")
|
|
165
|
+
|
|
166
|
+
# Layout: pull closing tags onto the last child's line
|
|
167
|
+
output = output.gsub(/\n[ \t]*(<\/[\w.:-]+>)/, '\1')
|
|
168
|
+
|
|
169
|
+
if output.include?("/>")
|
|
170
|
+
# A literal "/>" inside a comment or CDATA section would be
|
|
171
|
+
# falsely expanded — the segment-aware walker is correct here
|
|
172
|
+
return nil if output.include?("<!--") || output.include?("<![CDATA[")
|
|
173
|
+
|
|
174
|
+
output = output.gsub(EMPTY_ELEMENT_EXPANSION_RE, '<\1\2></\1>')
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Native escapes attribute apostrophes; moxml keeps them literal
|
|
178
|
+
output.gsub("'", "'")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def serialize_element(elem)
|
|
182
|
+
output = "<#{elem.name}"
|
|
183
|
+
|
|
184
|
+
# Add namespace definitions (only on this element, not ancestors)
|
|
185
|
+
if elem.is_a?(::LibXML::XML::Node)
|
|
186
|
+
seen_ns = {}
|
|
187
|
+
elem.namespaces.each do |ns|
|
|
188
|
+
prefix = ns.prefix
|
|
189
|
+
uri = ns.href
|
|
190
|
+
next if seen_ns.key?(prefix)
|
|
191
|
+
|
|
192
|
+
seen_ns[prefix] = true
|
|
193
|
+
output << if prefix.nil? || prefix.empty?
|
|
194
|
+
" xmlns=\"#{XmlEmitter.escape_attribute(uri)}\""
|
|
195
|
+
else
|
|
196
|
+
" xmlns:#{prefix}=\"#{XmlEmitter.escape_attribute(uri)}\""
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Add attributes
|
|
202
|
+
if elem.attributes?
|
|
203
|
+
elem.each_attr do |attr|
|
|
204
|
+
next if attr.name.start_with?("xmlns")
|
|
205
|
+
|
|
206
|
+
# Include namespace prefix if attribute has one
|
|
207
|
+
attr_name = if attr.ns&.prefix
|
|
208
|
+
"#{attr.ns.prefix}:#{attr.name}"
|
|
209
|
+
else
|
|
210
|
+
attr.name
|
|
211
|
+
end
|
|
212
|
+
output << " #{attr_name}=\"#{XmlEmitter.escape_attribute(attr.value)}\""
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Always use verbose format <tag></tag> for consistency with other adapters
|
|
217
|
+
output << ">"
|
|
218
|
+
if elem.children?
|
|
219
|
+
elem.each_child do |child|
|
|
220
|
+
# Skip whitespace-only text nodes
|
|
221
|
+
next if blank_text_node?(child)
|
|
222
|
+
|
|
223
|
+
output << serialize_node(child)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Append any EntityReference wrappers stored on the document
|
|
228
|
+
doc = elem.doc
|
|
229
|
+
entity_refs = entity_ref_registry(doc).refs_for(elem)
|
|
230
|
+
entity_refs&.each { |ref| output << ref.to_xml }
|
|
231
|
+
|
|
232
|
+
output << "</#{elem.name}>"
|
|
233
|
+
|
|
234
|
+
output
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def serialize_node(node)
|
|
238
|
+
# Check if node is a wrapper with to_xml method
|
|
239
|
+
case node
|
|
240
|
+
when CustomizedLibxml::ProcessingInstruction,
|
|
241
|
+
CustomizedLibxml::Comment,
|
|
242
|
+
CustomizedLibxml::Cdata,
|
|
243
|
+
CustomizedLibxml::Text,
|
|
244
|
+
CustomizedLibxml::EntityReference
|
|
245
|
+
return node.to_xml
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
case node.node_type
|
|
249
|
+
when ::LibXML::XML::Node::ELEMENT_NODE
|
|
250
|
+
serialize_element(node)
|
|
251
|
+
when ::LibXML::XML::Node::TEXT_NODE
|
|
252
|
+
XmlEmitter.escape_text(node.content)
|
|
253
|
+
when ::LibXML::XML::Node::CDATA_SECTION_NODE
|
|
254
|
+
"<![CDATA[#{node.content}]]>"
|
|
255
|
+
when ::LibXML::XML::Node::COMMENT_NODE
|
|
256
|
+
"<!-- #{node.content} -->"
|
|
257
|
+
when ::LibXML::XML::Node::PI_NODE
|
|
258
|
+
"<?#{node.name} #{node.content}?>"
|
|
259
|
+
else
|
|
260
|
+
node.to_s
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def serialize_element_with_namespaces(elem, include_ns: true,
|
|
265
|
+
indent_size: 0, depth: 0,
|
|
266
|
+
eref_active: nil)
|
|
267
|
+
# Cache elem.name — it's a libxml C call we'd otherwise make
|
|
268
|
+
# twice (open tag + close tag). Concat with `<<` instead of
|
|
269
|
+
# `"<#{name}"` to avoid the interpolated intermediate string.
|
|
270
|
+
name = elem.name
|
|
271
|
+
output = +"<"
|
|
272
|
+
output << name
|
|
273
|
+
emit_namespace_definitions(output, elem, include_ns)
|
|
274
|
+
emit_attributes(output, elem)
|
|
275
|
+
|
|
276
|
+
# `eref_active` is precomputed at the top-level `serialize` call
|
|
277
|
+
# and threaded down — when nil (top-level non-recursive call into
|
|
278
|
+
# this method), look it up; when false, skip the per-element doc
|
|
279
|
+
# attachment query that otherwise fires for every element under
|
|
280
|
+
# Monitor#synchronize.
|
|
281
|
+
eref_active = doc_eref_active?(elem.doc) if eref_active.nil?
|
|
282
|
+
entity_refs, child_sequence = if eref_active
|
|
283
|
+
lookup_entity_ref_serialization(elem)
|
|
284
|
+
else
|
|
285
|
+
[
|
|
286
|
+
nil, nil
|
|
287
|
+
]
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Always use verbose format <tag></tag> for consistency with other adapters
|
|
291
|
+
output << ">"
|
|
292
|
+
|
|
293
|
+
if entity_refs && child_sequence
|
|
294
|
+
emit_eref_interleaved_children(output, elem, entity_refs, child_sequence,
|
|
295
|
+
indent_size, depth, eref_active: eref_active)
|
|
296
|
+
elsif elem.children?
|
|
297
|
+
emit_children_with_layout(output, elem, indent_size, depth,
|
|
298
|
+
eref_active: eref_active)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
output << "</" << name << ">"
|
|
302
|
+
output
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Emit `xmlns`/`xmlns:foo` declarations onto `output`. On the root
|
|
306
|
+
# (`include_ns: true`) we emit ALL definitions; on children we
|
|
307
|
+
# emit only definitions that OVERRIDE a parent's same-prefix URI.
|
|
308
|
+
# Skips the whole block when the element has no local definitions,
|
|
309
|
+
# which is the common case for child elements in unnamespaced docs.
|
|
310
|
+
def emit_namespace_definitions(output, elem, include_ns)
|
|
311
|
+
return unless elem.is_a?(::LibXML::XML::Node)
|
|
312
|
+
|
|
313
|
+
ns_list = elem.namespaces
|
|
314
|
+
return unless ns_list.is_a?(::LibXML::XML::Namespaces)
|
|
315
|
+
|
|
316
|
+
definitions = ns_list.definitions
|
|
317
|
+
return if definitions.empty?
|
|
318
|
+
|
|
319
|
+
parent_ns_defs = include_ns ? nil : parent_namespace_defs(elem)
|
|
320
|
+
seen_ns = nil
|
|
321
|
+
|
|
322
|
+
definitions.each do |ns|
|
|
323
|
+
prefix = ns.prefix
|
|
324
|
+
uri = ns.href
|
|
325
|
+
next unless include_ns ||
|
|
326
|
+
(parent_ns_defs&.key?(prefix) && parent_ns_defs[prefix] != uri)
|
|
327
|
+
|
|
328
|
+
seen_ns ||= {}
|
|
329
|
+
next if seen_ns.key?(prefix)
|
|
330
|
+
|
|
331
|
+
seen_ns[prefix] = true
|
|
332
|
+
output << format_ns_declaration(prefix, uri)
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def parent_namespace_defs(elem)
|
|
337
|
+
parent = elem.parent
|
|
338
|
+
return nil unless parent.is_a?(::LibXML::XML::Node)
|
|
339
|
+
|
|
340
|
+
defs = {}
|
|
341
|
+
parent.namespaces.each { |ns| defs[ns.prefix] = ns.href }
|
|
342
|
+
defs
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def format_ns_declaration(prefix, uri)
|
|
346
|
+
if prefix.nil? || prefix.empty?
|
|
347
|
+
" xmlns=\"#{XmlEmitter.escape_attribute(uri)}\""
|
|
348
|
+
else
|
|
349
|
+
" xmlns:#{prefix}=\"#{XmlEmitter.escape_attribute(uri)}\""
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def emit_attributes(output, elem)
|
|
354
|
+
return unless elem.attributes?
|
|
355
|
+
|
|
356
|
+
elem.each_attr do |attr|
|
|
357
|
+
next if attr.name.start_with?("xmlns")
|
|
358
|
+
|
|
359
|
+
attr_name = attr.ns&.prefix ? "#{attr.ns.prefix}:#{attr.name}" : attr.name
|
|
360
|
+
output << " #{attr_name}=\"#{XmlEmitter.escape_attribute(attr.value)}\""
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Returns [entity_refs, child_sequence] when the element has
|
|
365
|
+
# interleaved entity references that the serializer needs to
|
|
366
|
+
# weave back into the native child stream — otherwise [nil, nil].
|
|
367
|
+
#
|
|
368
|
+
# The caller is responsible for gating this with `eref_active`
|
|
369
|
+
# (precomputed once per `serialize` call). When `eref_active` is
|
|
370
|
+
# false this method is never entered, so the per-element doc
|
|
371
|
+
# attachment query never fires.
|
|
372
|
+
def lookup_entity_ref_serialization(elem)
|
|
373
|
+
doc = elem.doc
|
|
374
|
+
return [nil, nil] unless doc
|
|
375
|
+
|
|
376
|
+
entity_ref_registry(doc).serialization_for(elem)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def emit_eref_interleaved_children(output, elem, entity_refs, child_sequence,
|
|
380
|
+
indent_size, depth, eref_active:)
|
|
381
|
+
native_children = collect_non_blank_children(elem)
|
|
382
|
+
child_pad = indent_size.positive? ? " " * (indent_size * (depth + 1)) : nil
|
|
383
|
+
eref_idx = 0
|
|
384
|
+
native_idx = 0
|
|
385
|
+
prev_block = true
|
|
386
|
+
|
|
387
|
+
child_sequence.each do |type|
|
|
388
|
+
case type
|
|
389
|
+
when :native
|
|
390
|
+
if native_idx < native_children.size
|
|
391
|
+
child = native_children[native_idx]
|
|
392
|
+
is_text_like = child.text? || child.cdata?
|
|
393
|
+
if prev_block && !is_text_like
|
|
394
|
+
output << "\n"
|
|
395
|
+
output << child_pad if child_pad
|
|
396
|
+
end
|
|
397
|
+
prev_block = !is_text_like
|
|
398
|
+
|
|
399
|
+
output << serialize_child_to_xml(
|
|
400
|
+
child, indent_size: indent_size, depth: depth,
|
|
401
|
+
eref_active: eref_active
|
|
402
|
+
)
|
|
403
|
+
native_idx += 1
|
|
404
|
+
end
|
|
405
|
+
when :eref
|
|
406
|
+
if eref_idx < entity_refs.size
|
|
407
|
+
output << entity_refs[eref_idx].to_xml
|
|
408
|
+
eref_idx += 1
|
|
409
|
+
prev_block = false
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def blank_text_node?(child)
|
|
416
|
+
child.text? && blank_content?(child.content)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def blank_content?(content)
|
|
420
|
+
content.nil? || !content.match?(NON_WHITESPACE_RE)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def collect_non_blank_children(elem)
|
|
424
|
+
children = []
|
|
425
|
+
return children unless elem.children?
|
|
426
|
+
|
|
427
|
+
elem.each_child do |c|
|
|
428
|
+
children << c unless blank_text_node?(c)
|
|
429
|
+
end
|
|
430
|
+
children
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# Walk native children once and emit them with the same newline +
|
|
434
|
+
# indentation layout the old `add_newlines_to_xml` + `indent_xml`
|
|
435
|
+
# post-passes produced — but in a single recursion with no string
|
|
436
|
+
# rescanning.
|
|
437
|
+
#
|
|
438
|
+
# Newline rule (matching `>(?=<(?!/))` with CDATA-placeholder
|
|
439
|
+
# protection): emit `\n` + per-level padding before a child iff
|
|
440
|
+
# the previous emitted sibling was block-level (ended with `>`)
|
|
441
|
+
# AND the current sibling is block-level. Text and CDATA count
|
|
442
|
+
# as text-like and suppress the newline on both sides (the
|
|
443
|
+
# original CDATA placeholder broke the `>...<` adjacency
|
|
444
|
+
# symmetrically).
|
|
445
|
+
def emit_children_with_layout(output, elem, indent_size, depth,
|
|
446
|
+
eref_active:)
|
|
447
|
+
child_pad = indent_size.positive? ? " " * (indent_size * (depth + 1)) : nil
|
|
448
|
+
prev_block = true
|
|
449
|
+
|
|
450
|
+
elem.each_child do |child|
|
|
451
|
+
# Cache text? — used twice per child (whitespace skip + is_text_like).
|
|
452
|
+
# For element children (the common case) both calls return false, so
|
|
453
|
+
# caching saves a libxml C call.
|
|
454
|
+
is_text = child.text?
|
|
455
|
+
next if is_text && blank_content?(child.content)
|
|
456
|
+
|
|
457
|
+
is_text_like = is_text || child.cdata?
|
|
458
|
+
if prev_block && !is_text_like
|
|
459
|
+
output << "\n"
|
|
460
|
+
output << child_pad if child_pad
|
|
461
|
+
end
|
|
462
|
+
prev_block = !is_text_like
|
|
463
|
+
|
|
464
|
+
output << serialize_child_to_xml(child, indent_size: indent_size, depth: depth,
|
|
465
|
+
eref_active: eref_active)
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Serialize one child node. Elements recurse into the layout-aware
|
|
470
|
+
# path; non-element wrappers route through their own `to_xml`;
|
|
471
|
+
# everything else falls through to the per-type serializer.
|
|
472
|
+
# `indent_size:` and `depth:` are required to force callers to
|
|
473
|
+
# decide whether the child should inherit the parent's indent
|
|
474
|
+
# state — the entity-ref interleave path deliberately passes 0/0.
|
|
475
|
+
#
|
|
476
|
+
# Element fast-path checked first to avoid allocating a wrapper
|
|
477
|
+
# we'd immediately throw away (elements always recurse on the
|
|
478
|
+
# raw native node, not the wrapper). For a typical document this
|
|
479
|
+
# skips wrapper allocation for the majority of children.
|
|
480
|
+
def serialize_child_to_xml(child, indent_size:, depth:, eref_active:)
|
|
481
|
+
if child.element?
|
|
482
|
+
return serialize_element_with_namespaces(child, include_ns: false,
|
|
483
|
+
indent_size: indent_size, depth: depth + 1,
|
|
484
|
+
eref_active: eref_active)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
wrapped_child = patch_node(child)
|
|
488
|
+
if wrapped_child.is_a?(CustomizedLibxml::Node)
|
|
489
|
+
wrapped_child.to_xml
|
|
490
|
+
else
|
|
491
|
+
serialize_node(child)
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
end
|