Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/odf/element.py: 42%
319 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3# Copyright (C) 2007-2010 Søren Roug, European Environment Agency
4#
5# This library is free software; you can redistribute it and/or
6# modify it under the terms of the GNU Lesser General Public
7# License as published by the Free Software Foundation; either
8# version 2.1 of the License, or (at your option) any later version.
9#
10# This library is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13# Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public
16# License along with this library; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18#
19# Contributor(s):
20#
22# Note: This script has copied a lot of text from xml.dom.minidom.
23# Whatever license applies to that file also applies to this file.
24#
25import sys, os.path
26sys.path.append(os.path.dirname(__file__))
27import re
28import xml.dom
29from xml.dom.minicompat import *
30from odf.namespaces import nsdict
31import odf.grammar as grammar
32from odf.attrconverters import AttrConverters
34if sys.version_info[0] == 3: 34 ↛ 38line 34 didn't jump to line 38, because the condition on line 34 was never false
35 unicode=str # unicode function does not exist
36 unichr=chr # unichr does not exist
38_xml11_illegal_ranges = (
39 (0x0, 0x0,),
40 (0xd800, 0xdfff,),
41 (0xfffe, 0xffff,),
42)
44_xml10_illegal_ranges = _xml11_illegal_ranges + (
45 (0x01, 0x08,),
46 (0x0b, 0x0c,),
47 (0x0e, 0x1f,),
48)
50_xml_discouraged_ranges = (
51 (0x7f, 0x84,),
52 (0x86, 0x9f,),
53)
55if sys.maxunicode >= 0x10000: 55 ↛ 77line 55 didn't jump to line 77, because the condition on line 55 was never false
56 # modern or "wide" python build
57 _xml_discouraged_ranges = _xml_discouraged_ranges + (
58 (0x1fffe, 0x1ffff,),
59 (0x2fffe, 0x2ffff,),
60 (0x3fffe, 0x3ffff,),
61 (0x4fffe, 0x4ffff,),
62 (0x5fffe, 0x5ffff,),
63 (0x6fffe, 0x6ffff,),
64 (0x7fffe, 0x7ffff,),
65 (0x8fffe, 0x8ffff,),
66 (0x9fffe, 0x9ffff,),
67 (0xafffe, 0xaffff,),
68 (0xbfffe, 0xbffff,),
69 (0xcfffe, 0xcffff,),
70 (0xdfffe, 0xdffff,),
71 (0xefffe, 0xeffff,),
72 (0xffffe, 0xfffff,),
73 (0x10fffe, 0x10ffff,),
74 )
75# else "narrow" python build - only possible with old versions
77def _range_seq_to_re(range_seq):
78 # range pairs are specified as closed intervals
79 return re.compile(u"[{}]".format(
80 u"".join(
81 u"{}-{}".format(re.escape(unichr(lo)), re.escape(unichr(hi)))
82 for lo, hi in range_seq
83 )
84 ), flags=re.UNICODE)
86_xml_filtered_chars_re = _range_seq_to_re(_xml10_illegal_ranges + _xml_discouraged_ranges)
88def _handle_unrepresentable(data):
89 return _xml_filtered_chars_re.sub(u"\ufffd", data)
91# The following code is pasted form xml.sax.saxutils
92# Tt makes it possible to run the code without the xml sax package installed
93# To make it possible to have <rubbish> in your text elements, it is necessary to escape the texts
94def _escape(data, entities={}):
95 """ Escape &, <, and > in a string of data.
97 You can escape other strings of data by passing a dictionary as
98 the optional entities parameter. The keys and values must all be
99 strings; each key will be replaced with its corresponding value.
100 """
101 data = data.replace("&", "&")
102 data = data.replace("<", "<")
103 data = data.replace(">", ">")
104 for chars, entity in entities.items():
105 data = data.replace(chars, entity)
106 return data
108def _sanitize(data, entities={}):
109 return _escape(_handle_unrepresentable(data), entities=entities)
111def _quoteattr(data, entities={}):
112 """ Escape and quote an attribute value.
114 Escape &, <, and > in a string of data, then quote it for use as
115 an attribute value. The \" character will be escaped as well, if
116 necessary.
118 You can escape other strings of data by passing a dictionary as
119 the optional entities parameter. The keys and values must all be
120 strings; each key will be replaced with its corresponding value.
121 """
122 entities['\n']=' '
123 entities['\r']=''
124 data = _sanitize(data, entities)
125 if '"' in data:
126 if "'" in data:
127 data = '"%s"' % data.replace('"', """)
128 else:
129 data = "'%s'" % data
130 else:
131 data = '"%s"' % data
132 return data
134def _nssplit(qualifiedName):
135 """ Split a qualified name into namespace part and local part. """
136 fields = qualifiedName.split(':', 1)
137 if len(fields) == 2:
138 return fields
139 else:
140 return (None, fields[0])
142def _nsassign(namespace):
143 return nsdict.setdefault(namespace,"ns" + str(len(nsdict)))
146# Exceptions
147class IllegalChild(Exception):
148 """ Complains if you add an element to a parent where it is not allowed """
149class IllegalText(Exception):
150 """ Complains if you add text or cdata to an element where it is not allowed """
152class Node(xml.dom.Node):
153 """ super class for more specific nodes """
154 parentNode = None
155 nextSibling = None
156 previousSibling = None
158 def hasChildNodes(self):
159 """ Tells whether this element has any children; text nodes,
160 subelements, whatever.
161 """
162 if self.childNodes:
163 return True
164 else:
165 return False
167 def _get_childNodes(self):
168 return self.childNodes
170 def _get_firstChild(self):
171 if self.childNodes:
172 return self.childNodes[0]
174 def _get_lastChild(self):
175 if self.childNodes:
176 return self.childNodes[-1]
178 def insertBefore(self, newChild, refChild):
179 """ Inserts the node newChild before the existing child node refChild.
180 If refChild is null, insert newChild at the end of the list of children.
181 """
182 if newChild.nodeType not in self._child_node_types:
183 raise IllegalChild( "%s cannot be child of %s" % (newChild.tagName, self.tagName))
184 if newChild.parentNode is not None:
185 newChild.parentNode.removeChild(newChild)
186 if refChild is None:
187 self.appendChild(newChild)
188 else:
189 try:
190 index = self.childNodes.index(refChild)
191 except ValueError:
192 raise xml.dom.NotFoundErr()
193 self.childNodes.insert(index, newChild)
194 newChild.nextSibling = refChild
195 refChild.previousSibling = newChild
196 if index:
197 node = self.childNodes[index-1]
198 node.nextSibling = newChild
199 newChild.previousSibling = node
200 else:
201 newChild.previousSibling = None
202 newChild.parentNode = self
203 return newChild
205 def appendChild(self, newChild):
206 """ Adds the node newChild to the end of the list of children of this node.
207 If the newChild is already in the tree, it is first removed.
208 """
209 if newChild.nodeType == self.DOCUMENT_FRAGMENT_NODE: 209 ↛ 210line 209 didn't jump to line 210, because the condition on line 209 was never true
210 for c in tuple(newChild.childNodes):
211 self.appendChild(c)
212 ### The DOM does not clearly specify what to return in this case
213 return newChild
214 if newChild.nodeType not in self._child_node_types: 214 ↛ 215line 214 didn't jump to line 215, because the condition on line 214 was never true
215 raise IllegalChild( "<%s> is not allowed in %s" % ( newChild.tagName, self.tagName))
216 if newChild.parentNode is not None: 216 ↛ 217line 216 didn't jump to line 217, because the condition on line 216 was never true
217 newChild.parentNode.removeChild(newChild)
218 _append_child(self, newChild)
219 newChild.nextSibling = None
220 return newChild
222 def removeChild(self, oldChild):
223 """ Removes the child node indicated by oldChild from the list of children, and returns it.
224 """
225 #FIXME: update ownerDocument.element_dict or find other solution
226 try:
227 self.childNodes.remove(oldChild)
228 except ValueError:
229 raise xml.dom.NotFoundErr()
230 if oldChild.nextSibling is not None:
231 oldChild.nextSibling.previousSibling = oldChild.previousSibling
232 if oldChild.previousSibling is not None:
233 oldChild.previousSibling.nextSibling = oldChild.nextSibling
234 oldChild.nextSibling = oldChild.previousSibling = None
235 if self.ownerDocument:
236 self.ownerDocument.remove_from_caches(oldChild)
237 oldChild.parentNode = None
238 return oldChild
240 def __str__(self):
241 val = []
242 for c in self.childNodes:
243 val.append(str(c))
244 return ''.join(val)
246 def __unicode__(self):
247 val = []
248 for c in self.childNodes:
249 val.append(unicode(c))
250 return u''.join(val)
252defproperty(Node, "firstChild", doc="First child node, or None.")
253defproperty(Node, "lastChild", doc="Last child node, or None.")
255def _append_child(self, node):
256 # fast path with less checks; usable by DOM builders if careful
257 childNodes = self.childNodes
258 if childNodes: 258 ↛ 259line 258 didn't jump to line 259, because the condition on line 258 was never true
259 last = childNodes[-1]
260 node.__dict__["previousSibling"] = last
261 last.__dict__["nextSibling"] = node
262 childNodes.append(node)
263 node.__dict__["parentNode"] = self
265class Childless:
266 """ Mixin that makes childless-ness easy to implement and avoids
267 the complexity of the Node methods that deal with children.
268 """
270 attributes = None
271 childNodes = EmptyNodeList()
272 firstChild = None
273 lastChild = None
275 def _get_firstChild(self):
276 return None
278 def _get_lastChild(self):
279 return None
281 def appendChild(self, node):
282 """ Raises an error """
283 raise xml.dom.HierarchyRequestErr(
284 self.tagName + " nodes cannot have children")
286 def hasChildNodes(self):
287 return False
289 def insertBefore(self, newChild, refChild):
290 """ Raises an error """
291 raise xml.dom.HierarchyRequestErr(
292 self.tagName + " nodes do not have children")
294 def removeChild(self, oldChild):
295 """ Raises an error """
296 raise xml.dom.NotFoundErr(
297 self.tagName + " nodes do not have children")
299 def replaceChild(self, newChild, oldChild):
300 """ Raises an error """
301 raise xml.dom.HierarchyRequestErr(
302 self.tagName + " nodes do not have children")
304class Text(Childless, Node):
305 nodeType = Node.TEXT_NODE
306 tagName = "Text"
308 def __init__(self, data):
309 self.data = data
311 def __str__(self):
312 return self.data
314 def __unicode__(self):
315 return self.data
317 def toXml(self,level,f):
318 """ Write XML in UTF-8 """
319 if self.data:
320 f.write(_sanitize(unicode(self.data)))
322class CDATASection(Text, Childless):
323 nodeType = Node.CDATA_SECTION_NODE
325 def toXml(self,level,f):
326 """ Generate XML output of the node. If the text contains "]]>", then
327 escape it by going out of CDATA mode (]]>), then write the string
328 and then go into CDATA mode again. (<![CDATA[)
329 """
330 if self.data:
331 f.write('<![CDATA[%s]]>' % self.data.replace(']]>',']]>]]><![CDATA['))
333class Element(Node):
334 """ Creates a arbitrary element and is intended to be subclassed not used on its own.
335 This element is the base of every element it defines a class which resembles
336 a xml-element. The main advantage of this kind of implementation is that you don't
337 have to create a toXML method for every different object. Every element
338 consists of an attribute, optional subelements, optional text and optional cdata.
339 """
341 nodeType = Node.ELEMENT_NODE
342 namespaces = {} # Due to shallow copy this is a static variable
344 _child_node_types = (Node.ELEMENT_NODE,
345 Node.PROCESSING_INSTRUCTION_NODE,
346 Node.COMMENT_NODE,
347 Node.TEXT_NODE,
348 Node.CDATA_SECTION_NODE,
349 Node.ENTITY_REFERENCE_NODE)
351 def __init__(self, attributes=None, text=None, cdata=None, qname=None, qattributes=None, check_grammar=True, **args):
352 if qname is not None: 352 ↛ 354line 352 didn't jump to line 354, because the condition on line 352 was never false
353 self.qname = qname
354 assert(hasattr(self, 'qname'))
355 self.ownerDocument = None
356 self.childNodes=[]
357 self.allowed_children = grammar.allowed_children.get(self.qname)
358 prefix = self.get_nsprefix(self.qname[0])
359 self.tagName = prefix + ":" + self.qname[1]
360 if text is not None: 360 ↛ 361line 360 didn't jump to line 361, because the condition on line 360 was never true
361 self.addText(text)
362 if cdata is not None: 362 ↛ 363line 362 didn't jump to line 363, because the condition on line 362 was never true
363 self.addCDATA(cdata)
365 allowed_attrs = self.allowed_attributes()
366 if allowed_attrs is not None: 366 ↛ 368line 366 didn't jump to line 368, because the condition on line 366 was never false
367 allowed_args = [ a[1].lower().replace('-','') for a in allowed_attrs]
368 self.attributes={}
369 # Load the attributes from the 'attributes' argument
370 if attributes: 370 ↛ 371line 370 didn't jump to line 371, because the condition on line 370 was never true
371 for attr, value in attributes.items():
372 self.setAttribute(attr, value)
373 # Load the qualified attributes
374 if qattributes: 374 ↛ 375line 374 didn't jump to line 375, because the condition on line 374 was never true
375 for attr, value in qattributes.items():
376 self.setAttrNS(attr[0], attr[1], value)
377 if allowed_attrs is not None: 377 ↛ 382line 377 didn't jump to line 382, because the condition on line 377 was never false
378 # Load the attributes from the 'args' argument
379 for arg in args.keys():
380 self.setAttribute(arg, args[arg])
381 else:
382 for arg in args.keys(): # If any attribute is allowed
383 self.attributes[arg]=args[arg]
384 if not check_grammar: 384 ↛ 385line 384 didn't jump to line 385, because the condition on line 384 was never true
385 return
386 # Test that all mandatory attributes have been added.
387 required = grammar.required_attributes.get(self.qname)
388 if required:
389 for r in required:
390 if self.getAttrNS(r[0],r[1]) is None: 390 ↛ 391line 390 didn't jump to line 391, because the condition on line 390 was never true
391 raise AttributeError( "Required attribute missing: %s in <%s>" % (r[1].lower().replace('-',''), self.tagName))
393 def get_knownns(self, prefix):
394 """ Odfpy maintains a list of known namespaces. In some cases a prefix is used, and
395 we need to know which namespace it resolves to.
396 """
397 global nsdict
398 for ns,p in nsdict.items():
399 if p == prefix: return ns
400 return None
402 def get_nsprefix(self, namespace):
403 """ Odfpy maintains a list of known namespaces. In some cases we have a namespace URL,
404 and needs to look up or assign the prefix for it.
405 """
406 if namespace is None: namespace = ""
407 prefix = _nsassign(namespace)
408 if not namespace in self.namespaces:
409 self.namespaces[namespace] = prefix
410 return prefix
412 def allowed_attributes(self):
413 return grammar.allowed_attributes.get(self.qname)
415 def _setOwnerDoc(self, element):
416 element.ownerDocument = self.ownerDocument
417 for child in element.childNodes: 417 ↛ 418line 417 didn't jump to line 418, because the loop on line 417 never started
418 self._setOwnerDoc(child)
420 def addElement(self, element, check_grammar=True):
421 """ adds an element to an Element
423 Element.addElement(Element)
424 """
425 if check_grammar and self.allowed_children is not None: 425 ↛ 428line 425 didn't jump to line 428, because the condition on line 425 was never false
426 if element.qname not in self.allowed_children: 426 ↛ 427line 426 didn't jump to line 427, because the condition on line 426 was never true
427 raise IllegalChild( "<%s> is not allowed in <%s>" % ( element.tagName, self.tagName))
428 self.appendChild(element)
429 self._setOwnerDoc(element)
430 if self.ownerDocument: 430 ↛ 431line 430 didn't jump to line 431, because the condition on line 430 was never true
431 self.ownerDocument.rebuild_caches(element)
433 def addText(self, text, check_grammar=True):
434 """ Adds text to an element
435 Setting check_grammar=False turns off grammar checking
436 """
437 if check_grammar and self.qname not in grammar.allows_text:
438 raise IllegalText( "The <%s> element does not allow text" % self.tagName)
439 else:
440 if text != '':
441 self.appendChild(Text(text))
443 def addCDATA(self, cdata, check_grammar=True):
444 """ Adds CDATA to an element
445 Setting check_grammar=False turns off grammar checking
446 """
447 if check_grammar and self.qname not in grammar.allows_text:
448 raise IllegalText( "The <%s> element does not allow text" % self.tagName)
449 else:
450 self.appendChild(CDATASection(cdata))
452 def removeAttribute(self, attr, check_grammar=True):
453 """ Removes an attribute by name. """
454 allowed_attrs = self.allowed_attributes()
455 if allowed_attrs is None:
456 if type(attr) == type(()):
457 prefix, localname = attr
458 self.removeAttrNS(prefix, localname)
459 else:
460 raise AttributeError( "Unable to add simple attribute - use (namespace, localpart)")
461 else:
462 # Construct a list of allowed arguments
463 allowed_args = [ a[1].lower().replace('-','') for a in allowed_attrs]
464 if check_grammar and attr not in allowed_args:
465 raise AttributeError( "Attribute %s is not allowed in <%s>" % ( attr, self.tagName))
466 i = allowed_args.index(attr)
467 self.removeAttrNS(allowed_attrs[i][0], allowed_attrs[i][1])
469 def setAttribute(self, attr, value, check_grammar=True):
470 """ Add an attribute to the element
471 This is sort of a convenience method. All attributes in ODF have
472 namespaces. The library knows what attributes are legal and then allows
473 the user to provide the attribute as a keyword argument and the
474 library will add the correct namespace.
475 Must overwrite, If attribute already exists.
476 """
477 if attr == 'parent' and value is not None: 477 ↛ 478line 477 didn't jump to line 478, because the condition on line 477 was never true
478 value.addElement(self)
479 else:
480 allowed_attrs = self.allowed_attributes()
481 if allowed_attrs is None: 481 ↛ 482line 481 didn't jump to line 482, because the condition on line 481 was never true
482 if type(attr) == type(()):
483 prefix, localname = attr
484 self.setAttrNS(prefix, localname, value)
485 else:
486 raise AttributeError( "Unable to add simple attribute - use (namespace, localpart)")
487 else:
488 # Construct a list of allowed arguments
489 allowed_args = [ a[1].lower().replace('-','') for a in allowed_attrs]
490 if check_grammar and attr not in allowed_args: 490 ↛ 491line 490 didn't jump to line 491, because the condition on line 490 was never true
491 raise AttributeError( "Attribute %s is not allowed in <%s>" % ( attr, self.tagName))
492 i = allowed_args.index(attr)
493 self.setAttrNS(allowed_attrs[i][0], allowed_attrs[i][1], value)
495 def setAttrNS(self, namespace, localpart, value):
496 """ Add an attribute to the element
497 In case you need to add an attribute the library doesn't know about
498 then you must provide the full qualified name
499 It will not check that the attribute is legal according to the schema.
500 Must overwrite, If attribute already exists.
501 """
502 allowed_attrs = self.allowed_attributes()
503 prefix = self.get_nsprefix(namespace)
504# if allowed_attrs and (namespace, localpart) not in allowed_attrs:
505# raise AttributeError( "Attribute %s:%s is not allowed in element <%s>" % ( prefix, localpart, self.tagName))
506 c = AttrConverters()
507 self.attributes[(namespace, localpart)] = c.convert((namespace, localpart), value, self)
509 def getAttrNS(self, namespace, localpart):
510 """
511 gets an attribute, given a namespace and a key
512 @param namespace a unicode string or a bytes: the namespace
513 @param localpart a unicode string or a bytes:
514 the key to get the attribute
515 @return an attribute as a unicode string or a bytes: if both paramters
516 are byte strings, it will be a bytes; if both attributes are
517 unicode strings, it will be a unicode string
518 """
519 prefix = self.get_nsprefix(namespace)
520 result = self.attributes.get((namespace, localpart))
522 assert(
523 (type(namespace), type(namespace), type(namespace) == \
524 type(b""), type(b""), type(b"")) or
525 (type(namespace), type(namespace), type(namespace) == \
526 type(u""), type(u""), type(u""))
527 )
529 return result
531 def removeAttrNS(self, namespace, localpart):
532 del self.attributes[(namespace, localpart)]
534 def getAttribute(self, attr):
535 """ Get an attribute value. The method knows which namespace the attribute is in
536 """
537 allowed_attrs = self.allowed_attributes()
538 if allowed_attrs is None:
539 if type(attr) == type(()):
540 prefix, localname = attr
541 return self.getAttrNS(prefix, localname)
542 else:
543 raise AttributeError( "Unable to get simple attribute - use (namespace, localpart)")
544 else:
545 # Construct a list of allowed arguments
546 allowed_args = [ a[1].lower().replace('-','') for a in allowed_attrs]
547 i = allowed_args.index(attr)
548 return self.getAttrNS(allowed_attrs[i][0], allowed_attrs[i][1])
550 def write_open_tag(self, level, f):
551 f.write(('<'+self.tagName))
552 if level == 0:
553 for namespace, prefix in self.namespaces.items():
554 f.write(u' xmlns:' + prefix + u'="'+ _sanitize(str(namespace))+'"')
555 for qname in self.attributes.keys():
556 prefix = self.get_nsprefix(qname[0])
557 f.write(u' '+_sanitize(str(prefix+u':'+qname[1]))+u'='+_quoteattr(unicode(self.attributes[qname])))
558 f.write(u'>')
560 def write_close_tag(self, level, f):
561 f.write('</'+self.tagName+'>')
563 def toXml(self, level, f):
564 """
565 Generate an XML stream out of the tree structure
566 @param level integer: level in the XML tree; zero at root of the tree
567 @param f an open writable file able to accept unicode strings
568 """
569 f.write(u'<'+self.tagName)
570 if level == 0:
571 for namespace, prefix in self.namespaces.items():
572 f.write(u' xmlns:' + prefix + u'="'+ _sanitize(str(namespace))+u'"')
573 for qname in self.attributes.keys():
574 prefix = self.get_nsprefix(qname[0])
575 f.write(u' '+_sanitize(unicode(prefix+':'+qname[1]))+u'='+_quoteattr(unicode(self.attributes[qname])))
576 if self.childNodes:
577 f.write(u'>')
578 for element in self.childNodes:
579 element.toXml(level+1,f)
580 f.write(u'</'+self.tagName+'>')
581 else:
582 f.write(u'/>')
584 def _getElementsByObj(self, obj, accumulator):
585 if self.qname == obj.qname:
586 accumulator.append(self)
587 for e in self.childNodes:
588 if e.nodeType == Node.ELEMENT_NODE:
589 accumulator = e._getElementsByObj(obj, accumulator)
590 return accumulator
592 def getElementsByType(self, element):
593 """ Gets elements based on the type, which is function from text.py, draw.py etc. """
594 obj = element(check_grammar=False)
595 return self._getElementsByObj(obj,[])
597 def isInstanceOf(self, element):
598 """ This is a check to see if the object is an instance of a type """
599 obj = element(check_grammar=False)
600 return self.qname == obj.qname