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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -0
- data/README.md +83 -2
- data/lib/ruby_pptx/action.rb +12 -4
- data/lib/ruby_pptx/autoshape_spec.rb +6 -2
- data/lib/ruby_pptx/builder.rb +3 -1
- data/lib/ruby_pptx/chart/categories.rb +45 -15
- data/lib/ruby_pptx/chart/chart.rb +77 -27
- data/lib/ruby_pptx/chart/combo.rb +9 -3
- data/lib/ruby_pptx/chart/data.rb +45 -15
- data/lib/ruby_pptx/chart/format.rb +229 -76
- data/lib/ruby_pptx/chart/workbook_writer.rb +9 -3
- data/lib/ruby_pptx/chart/xml_writer.rb +54 -18
- data/lib/ruby_pptx/chart/xy_data.rb +68 -22
- data/lib/ruby_pptx/dml/color.rb +27 -9
- data/lib/ruby_pptx/dml/effect.rb +6 -2
- data/lib/ruby_pptx/dml/fill.rb +42 -14
- data/lib/ruby_pptx/element_proxy.rb +12 -4
- data/lib/ruby_pptx/enum/base.rb +30 -10
- data/lib/ruby_pptx/enum/prog_id.rb +3 -1
- data/lib/ruby_pptx/geometry.rb +32 -10
- data/lib/ruby_pptx/image.rb +27 -9
- data/lib/ruby_pptx/length.rb +94 -27
- data/lib/ruby_pptx/media.rb +12 -4
- data/lib/ruby_pptx/numeric_lengths.rb +34 -9
- data/lib/ruby_pptx/opc/oxml.rb +7 -5
- data/lib/ruby_pptx/opc/pack_uri.rb +15 -5
- data/lib/ruby_pptx/opc/package.rb +97 -31
- data/lib/ruby_pptx/opc/serialized.rb +15 -5
- data/lib/ruby_pptx/oxml/backend.rb +585 -0
- data/lib/ruby_pptx/oxml/chart.rb +112 -42
- data/lib/ruby_pptx/oxml/content_model.rb +6 -2
- data/lib/ruby_pptx/oxml/dml/color.rb +6 -2
- data/lib/ruby_pptx/oxml/dml/fill.rb +6 -2
- data/lib/ruby_pptx/oxml/element.rb +138 -101
- data/lib/ruby_pptx/oxml/ns.rb +7 -7
- data/lib/ruby_pptx/oxml/presentation.rb +9 -3
- data/lib/ruby_pptx/oxml/section.rb +3 -1
- data/lib/ruby_pptx/oxml/shapes/autoshape.rb +39 -13
- data/lib/ruby_pptx/oxml/shapes/groupshape.rb +15 -7
- data/lib/ruby_pptx/oxml/shapes/other.rb +47 -16
- data/lib/ruby_pptx/oxml/shapes/shared.rb +96 -30
- data/lib/ruby_pptx/oxml/simple_types.rb +169 -52
- data/lib/ruby_pptx/oxml/slide.rb +24 -8
- data/lib/ruby_pptx/oxml/table.rb +39 -13
- data/lib/ruby_pptx/oxml/text.rb +37 -15
- data/lib/ruby_pptx/oxml/theme.rb +13 -4
- data/lib/ruby_pptx/package.rb +9 -3
- data/lib/ruby_pptx/parts/chart.rb +6 -2
- data/lib/ruby_pptx/parts/image.rb +12 -4
- data/lib/ruby_pptx/parts/media.rb +3 -1
- data/lib/ruby_pptx/parts/presentation.rb +15 -5
- data/lib/ruby_pptx/parts/slide.rb +33 -11
- data/lib/ruby_pptx/parts/theme.rb +3 -1
- data/lib/ruby_pptx/pattern_matching.rb +6 -2
- data/lib/ruby_pptx/presentation.rb +24 -8
- data/lib/ruby_pptx/section.rb +30 -10
- data/lib/ruby_pptx/shapes/adjustments.rb +9 -3
- data/lib/ruby_pptx/shapes/authoring.rb +3 -1
- data/lib/ruby_pptx/shapes/base.rb +74 -24
- data/lib/ruby_pptx/shapes/freeform.rb +36 -12
- data/lib/ruby_pptx/shapes/placeholder.rb +44 -15
- data/lib/ruby_pptx/shapes/shape.rb +91 -30
- data/lib/ruby_pptx/shapes/shape_tree.rb +66 -22
- data/lib/ruby_pptx/slide.rb +123 -41
- data/lib/ruby_pptx/table.rb +105 -35
- data/lib/ruby_pptx/text/font_metrics.rb +30 -9
- data/lib/ruby_pptx/text/text.rb +108 -36
- data/lib/ruby_pptx/theme.rb +30 -10
- data/lib/ruby_pptx/version.rb +1 -1
- data/lib/ruby_pptx.rb +8 -1
- 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 = { "&" => "&", "<" => "<", ">" => ">", '"' => """,
|
|
44
|
+
"\n" => " ", "\r" => " ", "\t" => "	" }.freeze
|
|
45
|
+
TEXT_ESCAPES = { "&" => "&", "<" => "<", ">" => ">", "\r" => " " }.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
|