1# encoding: utf-8
2
3"""
4Temporary stand-in for main oxml module that came across with the
5PackageReader transplant. Probably much will get replaced with objects from
6the pptx.oxml.core and then this module will either get deleted or only hold
7the package related custom element classes.
8"""
9
10from __future__ import absolute_import, print_function, unicode_literals
11
12from lxml import etree
13
14from .constants import NAMESPACE as NS, RELATIONSHIP_TARGET_MODE as RTM
15
16
17# configure XML parser
18element_class_lookup = etree.ElementNamespaceClassLookup()
19oxml_parser = etree.XMLParser(remove_blank_text=True, resolve_entities=False)
20oxml_parser.set_element_class_lookup(element_class_lookup)
21
22nsmap = {
23    'ct': NS.OPC_CONTENT_TYPES,
24    'pr': NS.OPC_RELATIONSHIPS,
25    'r':  NS.OFC_RELATIONSHIPS,
26}
27
28
29# ===========================================================================
30# functions
31# ===========================================================================
32
33def parse_xml(text):
34    """
35    ``etree.fromstring()`` replacement that uses oxml parser
36    """
37    return etree.fromstring(text, oxml_parser)
38
39
40def qn(tag):
41    """
42    Stands for "qualified name", a utility function to turn a namespace
43    prefixed tag name into a Clark-notation qualified tag name for lxml. For
44    example, ``qn('p:cSld')`` returns ``'{http://schemas.../main}cSld'``.
45    """
46    prefix, tagroot = tag.split(':')
47    uri = nsmap[prefix]
48    return '{%s}%s' % (uri, tagroot)
49
50
51def serialize_part_xml(part_elm):
52    """
53    Serialize *part_elm* etree element to XML suitable for storage as an XML
54    part. That is to say, no insignificant whitespace added for readability,
55    and an appropriate XML declaration added with UTF-8 encoding specified.
56    """
57    return etree.tostring(part_elm, encoding='UTF-8', standalone=True)
58
59
60def serialize_for_reading(element):
61    """
62    Serialize *element* to human-readable XML suitable for tests. No XML
63    declaration.
64    """
65    return etree.tostring(element, encoding='unicode', pretty_print=True)
66
67
68# ===========================================================================
69# Custom element classes
70# ===========================================================================
71
72class BaseOxmlElement(etree.ElementBase):
73    """
74    Base class for all custom element classes, to add standardized behavior
75    to all classes in one place.
76    """
77    @property
78    def xml(self):
79        """
80        Return XML string for this element, suitable for testing purposes.
81        Pretty printed for readability and without an XML declaration at the
82        top.
83        """
84        return serialize_for_reading(self)
85
86
87class CT_Default(BaseOxmlElement):
88    """
89    ``<Default>`` element, specifying the default content type to be applied
90    to a part with the specified extension.
91    """
92    @property
93    def content_type(self):
94        """
95        String held in the ``ContentType`` attribute of this ``<Default>``
96        element.
97        """
98        return self.get('ContentType')
99
100    @property
101    def extension(self):
102        """
103        String held in the ``Extension`` attribute of this ``<Default>``
104        element.
105        """
106        return self.get('Extension')
107
108    @staticmethod
109    def new(ext, content_type):
110        """
111        Return a new ``<Default>`` element with attributes set to parameter
112        values.
113        """
114        xml = '<Default xmlns="%s"/>' % nsmap['ct']
115        default = parse_xml(xml)
116        default.set('Extension', ext)
117        default.set('ContentType', content_type)
118        return default
119
120
121class CT_Override(BaseOxmlElement):
122    """
123    ``<Override>`` element, specifying the content type to be applied for a
124    part with the specified partname.
125    """
126    @property
127    def content_type(self):
128        """
129        String held in the ``ContentType`` attribute of this ``<Override>``
130        element.
131        """
132        return self.get('ContentType')
133
134    @staticmethod
135    def new(partname, content_type):
136        """
137        Return a new ``<Override>`` element with attributes set to parameter
138        values.
139        """
140        xml = '<Override xmlns="%s"/>' % nsmap['ct']
141        override = parse_xml(xml)
142        override.set('PartName', partname)
143        override.set('ContentType', content_type)
144        return override
145
146    @property
147    def partname(self):
148        """
149        String held in the ``PartName`` attribute of this ``<Override>``
150        element.
151        """
152        return self.get('PartName')
153
154
155class CT_Relationship(BaseOxmlElement):
156    """
157    ``<Relationship>`` element, representing a single relationship from a
158    source to a target part.
159    """
160    @staticmethod
161    def new(rId, reltype, target, target_mode=RTM.INTERNAL):
162        """
163        Return a new ``<Relationship>`` element.
164        """
165        xml = '<Relationship xmlns="%s"/>' % nsmap['pr']
166        relationship = parse_xml(xml)
167        relationship.set('Id', rId)
168        relationship.set('Type', reltype)
169        relationship.set('Target', target)
170        if target_mode == RTM.EXTERNAL:
171            relationship.set('TargetMode', RTM.EXTERNAL)
172        return relationship
173
174    @property
175    def rId(self):
176        """
177        String held in the ``Id`` attribute of this ``<Relationship>``
178        element.
179        """
180        return self.get('Id')
181
182    @property
183    def reltype(self):
184        """
185        String held in the ``Type`` attribute of this ``<Relationship>``
186        element.
187        """
188        return self.get('Type')
189
190    @property
191    def target_ref(self):
192        """
193        String held in the ``Target`` attribute of this ``<Relationship>``
194        element.
195        """
196        return self.get('Target')
197
198    @property
199    def target_mode(self):
200        """
201        String held in the ``TargetMode`` attribute of this
202        ``<Relationship>`` element, either ``Internal`` or ``External``.
203        Defaults to ``Internal``.
204        """
205        return self.get('TargetMode', RTM.INTERNAL)
206
207
208class CT_Relationships(BaseOxmlElement):
209    """
210    ``<Relationships>`` element, the root element in a .rels file.
211    """
212    def add_rel(self, rId, reltype, target, is_external=False):
213        """
214        Add a child ``<Relationship>`` element with attributes set according
215        to parameter values.
216        """
217        target_mode = RTM.EXTERNAL if is_external else RTM.INTERNAL
218        relationship = CT_Relationship.new(rId, reltype, target, target_mode)
219        self.append(relationship)
220
221    @staticmethod
222    def new():
223        """
224        Return a new ``<Relationships>`` element.
225        """
226        xml = '<Relationships xmlns="%s"/>' % nsmap['pr']
227        relationships = parse_xml(xml)
228        return relationships
229
230    @property
231    def Relationship_lst(self):
232        """
233        Return a list containing all the ``<Relationship>`` child elements.
234        """
235        return self.findall(qn('pr:Relationship'))
236
237    @property
238    def xml(self):
239        """
240        Return XML string for this element, suitable for saving in a .rels
241        stream, not pretty printed and with an XML declaration at the top.
242        """
243        return serialize_part_xml(self)
244
245
246class CT_Types(BaseOxmlElement):
247    """
248    ``<Types>`` element, the container element for Default and Override
249    elements in [Content_Types].xml.
250    """
251    def add_default(self, ext, content_type):
252        """
253        Add a child ``<Default>`` element with attributes set to parameter
254        values.
255        """
256        default = CT_Default.new(ext, content_type)
257        self.append(default)
258
259    def add_override(self, partname, content_type):
260        """
261        Add a child ``<Override>`` element with attributes set to parameter
262        values.
263        """
264        override = CT_Override.new(partname, content_type)
265        self.append(override)
266
267    @property
268    def defaults(self):
269        return self.findall(qn('ct:Default'))
270
271    @staticmethod
272    def new():
273        """
274        Return a new ``<Types>`` element.
275        """
276        xml = '<Types xmlns="%s"/>' % nsmap['ct']
277        types = parse_xml(xml)
278        return types
279
280    @property
281    def overrides(self):
282        return self.findall(qn('ct:Override'))
283
284
285ct_namespace = element_class_lookup.get_namespace(nsmap['ct'])
286ct_namespace['Default'] = CT_Default
287ct_namespace['Override'] = CT_Override
288ct_namespace['Types'] = CT_Types
289
290pr_namespace = element_class_lookup.get_namespace(nsmap['pr'])
291pr_namespace['Relationship'] = CT_Relationship
292pr_namespace['Relationships'] = CT_Relationships
293