ruby_pptx 0.1.0 → 0.2.0

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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/README.md +83 -2
  4. data/lib/ruby_pptx/action.rb +12 -4
  5. data/lib/ruby_pptx/autoshape_spec.rb +6 -2
  6. data/lib/ruby_pptx/builder.rb +3 -1
  7. data/lib/ruby_pptx/chart/categories.rb +45 -15
  8. data/lib/ruby_pptx/chart/chart.rb +77 -27
  9. data/lib/ruby_pptx/chart/combo.rb +9 -3
  10. data/lib/ruby_pptx/chart/data.rb +45 -15
  11. data/lib/ruby_pptx/chart/format.rb +229 -76
  12. data/lib/ruby_pptx/chart/workbook_writer.rb +9 -3
  13. data/lib/ruby_pptx/chart/xml_writer.rb +54 -18
  14. data/lib/ruby_pptx/chart/xy_data.rb +68 -22
  15. data/lib/ruby_pptx/dml/color.rb +27 -9
  16. data/lib/ruby_pptx/dml/effect.rb +6 -2
  17. data/lib/ruby_pptx/dml/fill.rb +42 -14
  18. data/lib/ruby_pptx/element_proxy.rb +12 -4
  19. data/lib/ruby_pptx/enum/base.rb +30 -10
  20. data/lib/ruby_pptx/enum/prog_id.rb +3 -1
  21. data/lib/ruby_pptx/geometry.rb +32 -10
  22. data/lib/ruby_pptx/image.rb +27 -9
  23. data/lib/ruby_pptx/length.rb +94 -27
  24. data/lib/ruby_pptx/media.rb +12 -4
  25. data/lib/ruby_pptx/numeric_lengths.rb +34 -9
  26. data/lib/ruby_pptx/opc/oxml.rb +7 -5
  27. data/lib/ruby_pptx/opc/pack_uri.rb +15 -5
  28. data/lib/ruby_pptx/opc/package.rb +97 -31
  29. data/lib/ruby_pptx/opc/serialized.rb +15 -5
  30. data/lib/ruby_pptx/oxml/backend.rb +585 -0
  31. data/lib/ruby_pptx/oxml/chart.rb +112 -42
  32. data/lib/ruby_pptx/oxml/content_model.rb +6 -2
  33. data/lib/ruby_pptx/oxml/dml/color.rb +6 -2
  34. data/lib/ruby_pptx/oxml/dml/fill.rb +6 -2
  35. data/lib/ruby_pptx/oxml/element.rb +138 -101
  36. data/lib/ruby_pptx/oxml/ns.rb +7 -7
  37. data/lib/ruby_pptx/oxml/presentation.rb +9 -3
  38. data/lib/ruby_pptx/oxml/section.rb +3 -1
  39. data/lib/ruby_pptx/oxml/shapes/autoshape.rb +39 -13
  40. data/lib/ruby_pptx/oxml/shapes/groupshape.rb +15 -7
  41. data/lib/ruby_pptx/oxml/shapes/other.rb +47 -16
  42. data/lib/ruby_pptx/oxml/shapes/shared.rb +96 -30
  43. data/lib/ruby_pptx/oxml/simple_types.rb +169 -52
  44. data/lib/ruby_pptx/oxml/slide.rb +24 -8
  45. data/lib/ruby_pptx/oxml/table.rb +39 -13
  46. data/lib/ruby_pptx/oxml/text.rb +37 -15
  47. data/lib/ruby_pptx/oxml/theme.rb +13 -4
  48. data/lib/ruby_pptx/package.rb +9 -3
  49. data/lib/ruby_pptx/parts/chart.rb +6 -2
  50. data/lib/ruby_pptx/parts/image.rb +12 -4
  51. data/lib/ruby_pptx/parts/media.rb +3 -1
  52. data/lib/ruby_pptx/parts/presentation.rb +15 -5
  53. data/lib/ruby_pptx/parts/slide.rb +33 -11
  54. data/lib/ruby_pptx/parts/theme.rb +3 -1
  55. data/lib/ruby_pptx/pattern_matching.rb +6 -2
  56. data/lib/ruby_pptx/presentation.rb +24 -8
  57. data/lib/ruby_pptx/section.rb +30 -10
  58. data/lib/ruby_pptx/shapes/adjustments.rb +9 -3
  59. data/lib/ruby_pptx/shapes/authoring.rb +3 -1
  60. data/lib/ruby_pptx/shapes/base.rb +74 -24
  61. data/lib/ruby_pptx/shapes/freeform.rb +36 -12
  62. data/lib/ruby_pptx/shapes/placeholder.rb +44 -15
  63. data/lib/ruby_pptx/shapes/shape.rb +91 -30
  64. data/lib/ruby_pptx/shapes/shape_tree.rb +66 -22
  65. data/lib/ruby_pptx/slide.rb +123 -41
  66. data/lib/ruby_pptx/table.rb +105 -35
  67. data/lib/ruby_pptx/text/font_metrics.rb +30 -9
  68. data/lib/ruby_pptx/text/text.rb +108 -36
  69. data/lib/ruby_pptx/theme.rb +30 -10
  70. data/lib/ruby_pptx/version.rb +1 -1
  71. data/lib/ruby_pptx.rb +8 -1
  72. metadata +22 -1
@@ -0,0 +1,585 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/errors"
4
+
5
+ module Pptx
6
+ module Oxml
7
+ # The XML library underneath {Element}.
8
+ #
9
+ # Nokogiri is fast but native, so it cannot run everywhere -- ruby.wasm in
10
+ # particular. REXML is pure Ruby and runs anywhere Ruby does, at a cost in
11
+ # speed. {Element} talks to whichever is active through the functions
12
+ # below and never touches a node directly; everything above {Element}
13
+ # sees only elements.
14
+ #
15
+ # Nokogiri is used when it can be loaded, REXML otherwise. Set
16
+ # `RUBY_PPTX_XML_BACKEND` to `nokogiri` or `rexml` to choose explicitly;
17
+ # it is read once, when the library loads, because nodes from the two
18
+ # cannot be mixed in one document.
19
+ module Backend
20
+ ENV_VAR = "RUBY_PPTX_XML_BACKEND"
21
+
22
+ # @return [Module] {NokogiriBackend} or {RexmlBackend}
23
+ def self.select(requested = ENV.fetch(ENV_VAR, nil))
24
+ case requested&.downcase
25
+ when nil, ""
26
+ nokogiri_available? ? NokogiriBackend : RexmlBackend.tap(&:load)
27
+ when "nokogiri" then NokogiriBackend.tap { require "nokogiri" }
28
+ when "rexml" then RexmlBackend.tap(&:load)
29
+ else raise ArgumentError, "#{ENV_VAR} must be nokogiri or rexml, not #{requested.inspect}"
30
+ end
31
+ end
32
+
33
+ def self.nokogiri_available?
34
+ require "nokogiri"
35
+ true
36
+ rescue LoadError
37
+ false
38
+ end
39
+ private_class_method :nokogiri_available?
40
+
41
+ # Escaping shared by both serializers where they must agree: the
42
+ # characters libxml2 escapes, so REXML output matches Nokogiri's.
43
+ ATTR_ESCAPES = { "&" => "&amp;", "<" => "&lt;", ">" => "&gt;", '"' => "&quot;",
44
+ "\n" => "&#10;", "\r" => "&#13;", "\t" => "&#9;" }.freeze
45
+ TEXT_ESCAPES = { "&" => "&amp;", "<" => "&lt;", ">" => "&gt;", "\r" => "&#13;" }.freeze
46
+
47
+ # -----------------------------------------------------------------------
48
+ # Nokogiri
49
+ # -----------------------------------------------------------------------
50
+ module NokogiriBackend
51
+ module_function
52
+
53
+ def name
54
+ :nokogiri
55
+ end
56
+
57
+ def parse(xml)
58
+ doc = Nokogiri::XML(xml) { |config| config.noblanks.strict }
59
+ raise InvalidXmlError, doc.errors.first.to_s if doc.errors.any?
60
+
61
+ doc.root
62
+ end
63
+
64
+ # Parse +xml+ in the context of +node+, so prefixes resolve against the
65
+ # namespaces in scope there. noblanks matches {parse}, so an indented
66
+ # XML literal does not smuggle whitespace text nodes into the tree.
67
+ def parse_fragment(node, xml)
68
+ node.parse(xml, &:noblanks).first
69
+ end
70
+
71
+ # A new, detached element in +context+'s document.
72
+ #
73
+ # Creating it anywhere else would be a bug: Nokogiri re-creates a node
74
+ # adopted from another document, so the object built is not the object
75
+ # that ends up in the tree, and every wrapper held on it goes stale.
76
+ #
77
+ # Resolve against +context+'s scope, not the detached child's, so the
78
+ # new element joins an existing declaration instead of introducing a
79
+ # second one. Where the context holds the namespace as its *default*
80
+ # -- as .rels and [Content_Types].xml do -- the child is created
81
+ # unprefixed to match, which matters because C14N preserves prefixes.
82
+ def build(context, prefix, local, uri)
83
+ child = Nokogiri::XML::Node.new(local, context.document)
84
+ scopes = context.namespace_scopes
85
+ inherited = scopes.find { |ns| ns.href == uri && ns.prefix == prefix } ||
86
+ scopes.find { |ns| ns.href == uri }
87
+ child.namespace = inherited || child.add_namespace_definition(prefix, uri)
88
+ child
89
+ end
90
+
91
+ def document(node)
92
+ node.document
93
+ end
94
+
95
+ # Per-document wrapper cache, keyed on node identity. Lives on the
96
+ # document so it dies with it.
97
+ def wrapper_cache(node)
98
+ doc = node.document
99
+ doc.instance_variable_get(:@pptx_wrappers) ||
100
+ doc.instance_variable_set(:@pptx_wrappers, {}.compare_by_identity)
101
+ end
102
+
103
+ def element?(object)
104
+ object.is_a?(Nokogiri::XML::Element)
105
+ end
106
+
107
+ def local_name(node)
108
+ node.name
109
+ end
110
+
111
+ def namespace_uri(node)
112
+ node.namespace&.href
113
+ end
114
+
115
+ def parent_element(node)
116
+ parent = node.parent
117
+ parent.is_a?(Nokogiri::XML::Element) ? parent : nil
118
+ end
119
+
120
+ def element_children(node)
121
+ node.element_children.to_a
122
+ end
123
+
124
+ def xpath(node, expression, namespaces)
125
+ node.xpath(expression, namespaces).to_a
126
+ end
127
+
128
+ def append_child(parent, child)
129
+ parent.add_child(child)
130
+ end
131
+
132
+ def insert_before(reference, node)
133
+ reference.add_previous_sibling(node)
134
+ end
135
+
136
+ def insert_after(reference, node)
137
+ reference.add_next_sibling(node)
138
+ end
139
+
140
+ def prepend_child(parent, child)
141
+ parent.prepend_child(child)
142
+ end
143
+
144
+ def unlink(node)
145
+ node.unlink
146
+ end
147
+
148
+ def text(node)
149
+ node.text
150
+ end
151
+
152
+ def set_text(node, value)
153
+ node.content = value
154
+ end
155
+
156
+ def get_attribute(node, name)
157
+ node[name]
158
+ end
159
+
160
+ def set_attribute(node, name, value)
161
+ node[name] = value
162
+ end
163
+
164
+ def remove_attribute(node, name)
165
+ node.remove_attribute(name)
166
+ end
167
+
168
+ def get_attribute_ns(node, local, uri)
169
+ node.attribute_with_ns(local, uri)&.value
170
+ end
171
+
172
+ # Sets +prefix+:+local+, declaring +prefix+ on +node+ when not in scope.
173
+ def set_attribute_ns(node, prefix, local, uri, value)
174
+ if (existing = node.attribute_with_ns(local, uri))
175
+ existing.value = value
176
+ else
177
+ declare_namespace(node, prefix, uri)
178
+ node["#{prefix}:#{local}"] = value
179
+ end
180
+ end
181
+
182
+ def remove_attribute_ns(node, local, uri)
183
+ node.attribute_with_ns(local, uri)&.unlink
184
+ end
185
+
186
+ # Reuse an in-scope declaration when there is one, so the output does
187
+ # not sprout a redundant xmlns on every new element.
188
+ def declare_namespace(node, prefix, uri)
189
+ existing = node.namespace_scopes.find { |ns| ns.href == uri && ns.prefix == prefix }
190
+ existing || node.add_namespace_definition(prefix, uri)
191
+ end
192
+
193
+ def to_xml(node)
194
+ node.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML |
195
+ Nokogiri::XML::Node::SaveOptions::NO_DECLARATION)
196
+ end
197
+
198
+ def pretty_xml(node)
199
+ node.to_xml(indent: 2)
200
+ end
201
+ end
202
+
203
+ # -----------------------------------------------------------------------
204
+ # REXML
205
+ # -----------------------------------------------------------------------
206
+ #
207
+ # REXML resolves a prefix by walking up to the nearest declaration, so a
208
+ # node outside a tree has no namespace at all. A node built, parsed or
209
+ # removed for insertion elsewhere therefore carries its own declarations
210
+ # of the prefixes it uses. Inserting a node drops every declaration in
211
+ # it that repeats one in scope where it lands -- which is what Nokogiri
212
+ # does -- so the output matches Nokogiri's.
213
+ module RexmlBackend
214
+ OWNER = :@pptx_owner
215
+ WRAPPER = :@pptx_wrapper
216
+ FRAGMENT_ROOT = "pptx-fragment"
217
+
218
+ module_function
219
+
220
+ def name
221
+ :rexml
222
+ end
223
+
224
+ def load
225
+ require "rexml/document"
226
+ require "rexml/xpath"
227
+ end
228
+
229
+ def parse(xml)
230
+ load
231
+ doc = REXML::Document.new(utf8(xml))
232
+ root = doc.root or raise InvalidXmlError, "no root element"
233
+ strip_blanks(root)
234
+ root
235
+ rescue REXML::ParseException, REXML::UndefinedNamespaceException => e
236
+ raise InvalidXmlError, e.message.lines.first.to_s.strip
237
+ end
238
+
239
+ def parse_fragment(context, xml)
240
+ scope = in_scope_namespaces(context)
241
+ decls = scope.map { |prefix, uri| prefix.empty? ? %(xmlns="#{uri}") : %(xmlns:#{prefix}="#{uri}") }
242
+ wrapper = parse(%(<#{FRAGMENT_ROOT} #{decls.join(" ")}>#{utf8(xml)}</#{FRAGMENT_ROOT}>))
243
+ fragment = element_children(wrapper).first
244
+ return nil if fragment.nil?
245
+
246
+ # As libxml2 does when parsing in context: a declaration the context
247
+ # already makes is dropped, at any depth; a new namespace or a
248
+ # rebinding of a prefix is kept. The root keeps its own until it is
249
+ # inserted, so the detached fragment still resolves.
250
+ element_children(fragment).each { |child| drop_redundant_declarations(child) }
251
+ fragment.remove
252
+ carry_namespaces(fragment, scope)
253
+ fragment.instance_variable_set(OWNER, document(context))
254
+ fragment
255
+ end
256
+
257
+ def build(context, prefix, local, uri)
258
+ scope = in_scope_namespaces(context)
259
+ bound = if scope[prefix] == uri
260
+ prefix
261
+ else
262
+ scope.find { |_, scoped_uri| scoped_uri == uri }&.first
263
+ end
264
+ child = REXML::Element.new(bound.nil? || bound.empty? ? local : "#{bound}:#{local}")
265
+ declare(child, bound || prefix, uri)
266
+ child.instance_variable_set(OWNER, document(context))
267
+ child
268
+ end
269
+
270
+ def document(node)
271
+ top = node
272
+ top = top.parent while top.parent
273
+ top.is_a?(REXML::Document) ? top : top.instance_variable_get(OWNER)
274
+ end
275
+
276
+ # A wrapper per node, held on the node itself. REXML nodes keep their
277
+ # identity when moved, so no per-document table is needed.
278
+ def wrapper_cache(_node)
279
+ WrapperSlot
280
+ end
281
+
282
+ # Duck-types the Hash#[] / #[]= that {Element.wrap} uses.
283
+ module WrapperSlot
284
+ def self.[](node)
285
+ node.instance_variable_get(WRAPPER)
286
+ end
287
+
288
+ def self.[]=(node, wrapper)
289
+ node.instance_variable_set(WRAPPER, wrapper)
290
+ end
291
+ end
292
+
293
+ def element?(object)
294
+ object.is_a?(REXML::Element) && !object.is_a?(REXML::Document)
295
+ end
296
+
297
+ def local_name(node)
298
+ node.name
299
+ end
300
+
301
+ def namespace_uri(node)
302
+ uri = resolve(node, node.prefix.to_s)
303
+ uri.nil? || uri.empty? ? nil : uri
304
+ end
305
+
306
+ def parent_element(node)
307
+ parent = node.parent
308
+ element?(parent) ? parent : nil
309
+ end
310
+
311
+ def element_children(node)
312
+ node.children.grep(REXML::Element)
313
+ end
314
+
315
+ def xpath(node, expression, namespaces)
316
+ REXML::XPath.match(node, expression, namespaces)
317
+ end
318
+
319
+ def append_child(parent, child)
320
+ child.remove if child.parent
321
+ parent.add(child)
322
+ settle(child)
323
+ end
324
+
325
+ def insert_before(reference, node)
326
+ node.remove if node.parent
327
+ reference.parent.insert_before(reference, node)
328
+ settle(node)
329
+ end
330
+
331
+ def insert_after(reference, node)
332
+ node.remove if node.parent
333
+ reference.parent.insert_after(reference, node)
334
+ settle(node)
335
+ end
336
+
337
+ def prepend_child(parent, child)
338
+ first = parent.children.first
339
+ return append_child(parent, child) if first.nil?
340
+
341
+ insert_before(first, child)
342
+ end
343
+
344
+ # A removed node keeps working, as Nokogiri's does: it takes along
345
+ # declarations for the prefixes it used from its old scope, so it can
346
+ # still be read -- or inserted again.
347
+ def unlink(node)
348
+ owner = document(node)
349
+ scope = node.parent ? in_scope_namespaces(node.parent) : {}
350
+ node.remove
351
+ carry_namespaces(node, scope)
352
+ node.instance_variable_set(OWNER, owner)
353
+ node
354
+ end
355
+
356
+ def text(node)
357
+ node.children.each_with_object(+"") do |child, out|
358
+ case child
359
+ when REXML::Text then out << child.value
360
+ when REXML::Element then out << text(child)
361
+ end
362
+ end
363
+ end
364
+
365
+ def set_text(node, value)
366
+ node.children.dup.each(&:remove)
367
+ node.add_text(REXML::Text.new(value, true, nil, false)) unless value.empty?
368
+ end
369
+
370
+ def get_attribute(node, name)
371
+ node.attributes.each_attribute do |attribute|
372
+ return attribute.value if attribute.prefix.empty? && attribute.name == name
373
+ end
374
+ nil
375
+ end
376
+
377
+ def set_attribute(node, name, value)
378
+ node.add_attribute(name, value)
379
+ end
380
+
381
+ def remove_attribute(node, name)
382
+ node.delete_attribute(name)
383
+ end
384
+
385
+ def get_attribute_ns(node, local, uri)
386
+ find_attribute_ns(node, local, uri)&.value
387
+ end
388
+
389
+ def set_attribute_ns(node, prefix, local, uri, value)
390
+ if (existing = find_attribute_ns(node, local, uri))
391
+ node.add_attribute(existing.expanded_name, value)
392
+ else
393
+ declare_namespace(node, prefix, uri)
394
+ node.add_attribute("#{prefix}:#{local}", value)
395
+ end
396
+ end
397
+
398
+ def remove_attribute_ns(node, local, uri)
399
+ attribute = find_attribute_ns(node, local, uri)
400
+ node.attributes.delete(attribute) if attribute
401
+ end
402
+
403
+ def declare_namespace(node, prefix, uri)
404
+ declare(node, prefix, uri) unless resolve(node, prefix) == uri
405
+ end
406
+
407
+ def to_xml(node)
408
+ write(node, +"", nil, 0).force_encoding(Encoding::UTF_8)
409
+ end
410
+
411
+ def pretty_xml(node)
412
+ write(node, +"", " ", 0).force_encoding(Encoding::UTF_8)
413
+ end
414
+
415
+ # -- internals ---------------------------------------------------------
416
+
417
+ def utf8(xml)
418
+ xml = xml.dup if xml.frozen?
419
+ xml.force_encoding(Encoding::UTF_8)
420
+ end
421
+
422
+ # libxml2's noblanks, which Nokogiri parses with: whitespace between
423
+ # elements goes, whitespace that is an element's only content stays --
424
+ # `<a:t> </a:t>` is a space, not formatting.
425
+ def strip_blanks(node)
426
+ children = node.children
427
+ if children.any?(REXML::Element) && children.none? { |c| c.is_a?(REXML::Text) && !blank?(c) }
428
+ children.grep(REXML::Text).each(&:remove)
429
+ end
430
+ element_children(node).each { |child| strip_blanks(child) }
431
+ end
432
+
433
+ def blank?(text)
434
+ text.to_s.match?(/\A[ \t\r\n]*\z/)
435
+ end
436
+
437
+ # The URI +prefix+ ("" for the default namespace) is bound to at
438
+ # +node+, or nil.
439
+ def resolve(node, prefix)
440
+ name = prefix.empty? ? "xmlns" : "xmlns:#{prefix}"
441
+ current = node
442
+ while current.is_a?(REXML::Element) && !current.is_a?(REXML::Document)
443
+ declared = current.attributes.get_attribute(name)
444
+ return declared.value if declared
445
+
446
+ current = current.parent
447
+ end
448
+ prefix == "xml" ? "http://www.w3.org/XML/1998/namespace" : nil
449
+ end
450
+
451
+ # prefix => URI for every namespace in scope at +node+, nearest first.
452
+ def in_scope_namespaces(node)
453
+ scope = {}
454
+ current = node
455
+ while current.is_a?(REXML::Element) && !current.is_a?(REXML::Document)
456
+ current.attributes.each_attribute do |attribute|
457
+ if attribute.prefix == "xmlns"
458
+ scope[attribute.name] ||= attribute.value
459
+ elsif attribute.expanded_name == "xmlns"
460
+ scope[""] ||= attribute.value
461
+ end
462
+ end
463
+ current = current.parent
464
+ end
465
+ scope
466
+ end
467
+
468
+ def in_scope_namespaces_of_self(node)
469
+ node.attributes.each_attribute.with_object({}) do |attribute, decls|
470
+ if attribute.prefix == "xmlns"
471
+ decls[attribute.name] = attribute.value
472
+ elsif attribute.expanded_name == "xmlns"
473
+ decls[""] = attribute.value
474
+ end
475
+ end
476
+ end
477
+
478
+ def used_prefixes(node, found = Set.new)
479
+ found << node.prefix.to_s
480
+ node.attributes.each_attribute do |attribute|
481
+ prefix = attribute.prefix.to_s
482
+ found << prefix unless prefix.empty? || prefix == "xmlns"
483
+ end
484
+ element_children(node).each { |child| used_prefixes(child, found) }
485
+ found.to_a
486
+ end
487
+
488
+ # Declare on detached +node+ each prefix it uses that +scope+ bound for
489
+ # it and it does not declare itself.
490
+ def carry_namespaces(node, scope)
491
+ added = used_prefixes(node).select { |prefix| scope.key?(prefix) } -
492
+ in_scope_namespaces_of_self(node).keys
493
+ return if added.empty?
494
+
495
+ added.each { |prefix| declare(node, prefix, scope[prefix]) }
496
+ end
497
+
498
+ def drop_redundant_declarations(node)
499
+ in_scope_namespaces_of_self(node).each do |prefix, uri|
500
+ next unless resolve(node.parent, prefix) == uri
501
+
502
+ node.attributes.delete(node.attributes.get_attribute(prefix.empty? ? "xmlns" : "xmlns:#{prefix}"))
503
+ end
504
+ element_children(node).each { |child| drop_redundant_declarations(child) }
505
+ end
506
+
507
+ def declare(node, prefix, uri)
508
+ if prefix.nil? || prefix.empty?
509
+ node.add_attribute("xmlns", uri)
510
+ else
511
+ node.add_attribute("xmlns:#{prefix}", uri)
512
+ end
513
+ end
514
+
515
+ # What Nokogiri does on every insertion: drop each declaration in the
516
+ # inserted subtree that repeats one already in scope where it landed.
517
+ def settle(node)
518
+ node.remove_instance_variable(OWNER) if node.instance_variable_defined?(OWNER)
519
+ drop_redundant_declarations(node)
520
+ node
521
+ end
522
+
523
+ def find_attribute_ns(node, local, uri)
524
+ node.attributes.each_attribute do |attribute|
525
+ prefix = attribute.prefix.to_s
526
+ next if prefix.empty? || prefix == "xmlns" || attribute.name != local
527
+
528
+ return attribute if resolve(node, prefix) == uri
529
+ end
530
+ nil
531
+ end
532
+
533
+ # Serializes as libxml2 does with AS_XML: namespace declarations
534
+ # first, double-quoted attributes, empty elements self-closed.
535
+ # +indent+ nil writes compactly; otherwise elements holding only
536
+ # elements are laid out one child per line.
537
+ def write(node, out, indent, depth)
538
+ case node
539
+ when REXML::Element then write_element(node, out, indent, depth)
540
+ when REXML::CData then out << "<![CDATA[" << node.value << "]]>"
541
+ when REXML::Text then out << node.value.gsub(/[&<>\r]/, TEXT_ESCAPES)
542
+ when REXML::Comment then out << "<!--" << node.string << "-->"
543
+ when REXML::Instruction then out << "<?" << node.target << " " << node.content.to_s << "?>"
544
+ end
545
+ out
546
+ end
547
+
548
+ def write_element(node, out, indent, depth)
549
+ out << "<" << node.expanded_name
550
+ write_attributes(node, out)
551
+ return out << "/>" if node.children.empty?
552
+
553
+ out << ">"
554
+ write_children(node.children, out, indent, depth)
555
+ out << "</" << node.expanded_name << ">"
556
+ end
557
+
558
+ def write_attributes(node, out)
559
+ declarations, plain = node.attributes.each_attribute.partition do |a|
560
+ a.prefix == "xmlns" || a.expanded_name == "xmlns"
561
+ end
562
+ (declarations + plain).each do |attribute|
563
+ out << " " << attribute.expanded_name << '="'
564
+ out << attribute.value.gsub(/[&<>"\n\r\t]/, ATTR_ESCAPES) << '"'
565
+ end
566
+ end
567
+
568
+ # Mixed content is written as is, as libxml2 does: indenting it would
569
+ # add text the document does not have.
570
+ def write_children(children, out, indent, depth)
571
+ unless indent && children.all?(REXML::Element)
572
+ children.each { |child| write(child, out, nil, depth + 1) }
573
+ return
574
+ end
575
+
576
+ children.each do |child|
577
+ out << "\n" << (indent * (depth + 1))
578
+ write(child, out, indent, depth + 1)
579
+ end
580
+ out << "\n" << (indent * depth)
581
+ end
582
+ end
583
+ end
584
+ end
585
+ end