root/relatorio/templates/opendocument.py @ 89:757b097c61a5

Revision 89:757b097c61a5, 14.3 kB (checked in by Ga?tan de Menten <ged@…>, 4 years ago)

- simplified and optimized opening/closing tags matching code, and moved it

into the _relatorio_statements method

- optimized genshi tags replacement loop (common ancestor block)

Line 
1###############################################################################
2#
3# Copyright (c) 2007, 2008 OpenHex SPRL. (http://openhex.com) All Rights
4# Reserved.
5#
6# This program is free software; you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation; either version 2 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program.  If not, see <http://www.gnu.org/licenses/>.
18#
19###############################################################################
20
21__metaclass__ = type
22
23import re
24import md5
25import urllib
26import zipfile
27from cStringIO import StringIO
28
29import warnings
30warnings.filterwarnings('always', module='relatorio.templates.opendocument')
31
32import lxml.etree
33import genshi
34import genshi.output
35from genshi.template import MarkupTemplate
36
37from relatorio.templates.base import RelatorioStream
38from relatorio.reporting import Report, MIMETemplateLoader
39try:
40    from relatorio.templates.chart import Template as ChartTemplate
41except ImportError:
42    ChartTemplate = type(None)
43
44GENSHI_EXPR = re.compile(r'''
45        (/)?                                 # is this a closing tag?
46        (for|choose|otherwise|when|if|with)  # tag directive
47        \s*
48        (?:\s(\w+)=["'](.*)["']|$)           # match a single attr & its value
49        |
50        .*                                   # or anything else
51        ''', re.VERBOSE)
52
53EXTENSIONS = {'image/png': 'png',
54              'image/jpeg': 'jpg',
55              'image/bmp': 'bmp',
56              'image/gif': 'gif',
57              'image/tiff': 'tif',
58              'image/xbm': 'xbm',
59             }
60
61output_encode = genshi.output.encode
62EtreeElement = lxml.etree.Element
63
64def guess_type(val):
65    if isinstance(val, (str, unicode)):
66        return 'string'
67    elif isinstance(val, (int, float)):
68        return 'float'
69
70class OOTemplateError(genshi.template.base.TemplateSyntaxError):
71    "Error to raise when there is a SyntaxError in the genshi template"
72
73
74class ImageHref:
75    "A class used to add images in the odf zipfile"
76
77    def __init__(self, zfile, context):
78        self.zip = zfile
79        self.context = context.copy()
80
81    def __call__(self, expr, name):
82        #FIXME: name argument is unused
83        bitstream, mimetype = expr
84        if isinstance(bitstream, Report):
85            bitstream = bitstream(**self.context).render()
86        elif isinstance(bitstream, ChartTemplate):
87            bitstream = bitstream.generate(**self.context).render()
88        bitstream.seek(0)
89        file_content = bitstream.read()
90        name = md5.new(file_content).hexdigest()
91        path = 'Pictures/%s.%s' % (name, EXTENSIONS[mimetype])
92        if path not in self.zip.namelist():
93            self.zip.writestr(path, file_content)
94        return {'{http://www.w3.org/1999/xlink}href': path}
95
96
97class Template(MarkupTemplate):
98
99    def __init__(self, source, filepath=None, filename=None, loader=None,
100                 encoding=None, lookup='strict', allow_exec=True):
101        self.namespaces = {}
102        self.inner_docs = []
103        super(Template, self).__init__(source, filepath, filename, loader,
104                                       encoding, lookup, allow_exec)
105
106    def _parse(self, source, encoding):
107        """parses the odf file.
108
109        It adds genshi directives and finds the inner docs.
110        """
111        zf = zipfile.ZipFile(self.filepath)
112        content = zf.read('content.xml')
113        styles = zf.read('styles.xml')
114
115        template = super(Template, self)
116        content = template._parse(self.insert_directives(content), encoding)
117        styles = template._parse(self.insert_directives(styles), encoding)
118        content_files = [('content.xml', content)]
119        styles_files = [('styles.xml', styles)]
120
121        while self.inner_docs:
122            doc = self.inner_docs.pop()
123            c_path, s_path = doc + '/content.xml', doc + '/styles.xml'
124            content = zf.read(c_path)
125            styles = zf.read(s_path)
126
127            c_parsed = template._parse(self.insert_directives(zf.read(c_path)),
128                                       encoding)
129            s_parsed = template._parse(self.insert_directives(zf.read(s_path)),
130                                       encoding)
131            content_files.append((c_path, c_parsed))
132            styles_files.append((s_path, s_parsed))
133
134        zf.close()
135        parsed = []
136        for fpath, fparsed in content_files + styles_files:
137            parsed.append((genshi.core.PI, ('relatorio', fpath), None))
138            parsed += fparsed
139
140        return parsed
141
142    def insert_directives(self, content):
143        """adds the genshi directives, handle the images and the innerdocs.
144        """
145        tree = lxml.etree.parse(StringIO(content))
146        root = tree.getroot()
147        self.namespaces = root.nsmap.copy()
148        self.namespaces['py'] = 'http://genshi.edgewall.org/'
149
150        self._invert_style(tree)
151        self._handle_relatorio_tags(tree)
152        self._handle_images(tree)
153        self._handle_innerdocs(tree)
154        return StringIO(lxml.etree.tostring(tree))
155
156    def _invert_style(self, tree):
157        "inverts the text:a and text:span"
158        xpath_expr = "//text:a[starts-with(@xlink:href, 'relatorio://')]" \
159                     "/text:span"
160        for span in tree.xpath(xpath_expr, namespaces=self.namespaces):
161            text_a = span.getparent()
162            outer = text_a.getparent()
163            text_a.text = span.text
164            span.text = ''
165            text_a.remove(span)
166            outer.replace(text_a, span)
167            span.append(text_a)
168
169    def _relatorio_statements(self, tree):
170        "finds the relatorio statements (text:a/text:placeholder)"
171        # If this node href matches the relatorio URL it is kept.
172        # If this node href matches a genshi directive it is kept for further
173        # processing.
174        xlink_href_attrib = '{%s}href' % self.namespaces['xlink']
175        text_a = '{%s}a' % self.namespaces['text']
176        placeholder = '{%s}placeholder' % self.namespaces['text']
177        s_xpath = "//text:a[starts-with(@xlink:href, 'relatorio://')]" \
178                  "| //text:placeholder"
179
180        r_statements = []
181        opened_tags = []
182        # We map each opening tag with its closing tag
183        closing_tags = {}
184        for statement in tree.xpath(s_xpath, namespaces=self.namespaces):
185            if statement.tag == placeholder:
186                expr = statement.text[1:-1]
187            elif statement.tag == text_a:
188                expr = urllib.unquote(statement.attrib[xlink_href_attrib][12:])
189
190            if not expr:
191                raise OOTemplateError("No expression in the tag",
192                                      self.filepath)
193            elif not statement.text:
194                warnings.warn('No statement text in %s' % self.filepath)
195            elif expr != statement.text and statement.tag == text_a:
196                warnings.warn('url and text do not match in %s: %s != %s'
197                              % (self.filepath, expr,
198                                 statement.text.encode('utf-8')))
199
200            closing, directive, attr, attr_val = \
201                    GENSHI_EXPR.match(expr).groups()
202            is_opening = closing != '/'
203            if directive is not None:
204                # map closing tags with their opening tag
205                if is_opening:
206                    opened_tags.append(statement)
207                else:
208                    closing_tags[id(opened_tags.pop())] = statement
209            # - we operate only on opening statements
210            if is_opening:
211                r_statements.append((statement,
212                                     (expr, directive, attr, attr_val))
213                                   )
214        assert not opened_tags
215
216        return r_statements, closing_tags
217
218    def _handle_relatorio_tags(self, tree):
219        """
220        Will treat all relatorio tag (py:if/for/choose/when/otherwise)
221        tags
222        """
223        # Some tag name constants
224        table_cell_tag = '{%s}table-cell' % self.namespaces['table']
225        attrib_name = '{%s}attrs' % self.namespaces['py']
226        office_name = '{%s}value' % self.namespaces['office']
227        office_valuetype = '{%s}value-type' % self.namespaces['office']
228        genshi_replace = '{%s}replace' % self.namespaces['py']
229
230        r_statements, closing_tags = self._relatorio_statements(tree)
231
232        for r_node, parsed in r_statements:
233            expr, directive, attr, a_val = parsed
234
235            # If the node is a genshi directive statement:
236            if directive is not None:
237
238                opening = r_node
239                closing = closing_tags[id(r_node)]
240
241                # - we find the nearest common ancestor of the closing and
242                #   opening statements
243                o_ancestors = []
244                c_ancestors = list(closing.iterancestors())
245                ancestor = None
246                for node in opening.iterancestors():
247                    try:
248                        idx = c_ancestors.index(node)
249                        assert c_ancestors[idx] == node
250                        del c_ancestors[idx:]
251                        ancestor = node
252                        break
253                    except ValueError:
254                        pass
255                    o_ancestors.append(node)
256                assert ancestor is not None, \
257                       "No common ancestor found for opening and closing tag"
258
259                # - we create a <py:xxx> node
260                genshi_node = EtreeElement('{%s}%s' % (self.namespaces['py'],
261                                                       directive),
262                                           attrib={attr: a_val},
263                                           nsmap=self.namespaces)
264
265                # - we add all the nodes between the opening and closing
266                #   statements to this new node
267                can_append = False
268                for node in ancestor.iterchildren():
269                    if node in o_ancestors:
270                        outermost_o_ancestor = node
271                        assert outermost_o_ancestor == o_ancestors[-1]
272                        can_append = True
273                        continue
274                    if node in c_ancestors:
275                        outermost_c_ancestor = node
276                        assert outermost_c_ancestor == c_ancestors[-1]
277                        break
278                    if can_append:
279                        # we are between the opening and closing node
280                        genshi_node.append(node)
281
282                # - we replace the opening statement by the <py:xxx> node
283                ancestor.replace(outermost_o_ancestor, genshi_node)
284
285                # - we delete the closing statement (and its ancestors)
286                ancestor.remove(outermost_c_ancestor)
287            else:
288                # It's not a genshi statement it's a python expression
289                r_node.attrib[genshi_replace] = expr
290                parent = r_node.getparent().getparent()
291                if parent is None or parent.tag != table_cell_tag:
292                    continue
293
294                # The grand-parent tag is a table cell we should set the
295                # correct value and type for this cell.
296                dico = "{'%s': %s, '%s': guess_type(%s)}"
297                parent.attrib[attrib_name] = dico % (office_name, expr,
298                                                     office_valuetype, expr)
299                parent.attrib.pop(office_valuetype, None)
300                parent.attrib.pop(office_name, None)
301
302    def _handle_images(self, tree):
303        "replaces all draw:frame named 'image: ...' by a draw:image node"
304        draw_name = '{%s}name' % self.namespaces['draw']
305        draw_image = '{%s}image' % self.namespaces['draw']
306        python_attrs = '{%s}attrs' % self.namespaces['py']
307        xpath_expr = "//draw:frame[starts-with(@draw:name, 'image:')]"
308        for draw in tree.xpath(xpath_expr, namespaces=self.namespaces):
309            d_name = draw.attrib[draw_name]
310            attr_expr = "make_href(%s, %r)" % (d_name[7:], d_name[7:])
311            image_node = EtreeElement(draw_image,
312                                      attrib={python_attrs: attr_expr},
313                                      nsmap=self.namespaces)
314            draw.replace(draw[0], image_node)
315
316    def _handle_innerdocs(self, tree):
317        "finds inner_docs and adds them to the processing stack."
318        href_attrib = '{%s}href' % self.namespaces['xlink']
319        xpath_expr = "//draw:object[starts-with(@xlink:href, './')" \
320                     "and @xlink:show='embed']"
321        for draw in tree.xpath(xpath_expr, namespaces=self.namespaces):
322            self.inner_docs.append(draw.attrib[href_attrib][2:])
323
324    def generate(self, *args, **kwargs):
325        "creates the RelatorioStream."
326        serializer = OOSerializer(self.filepath)
327        kwargs['make_href'] = ImageHref(serializer.outzip, kwargs)
328        kwargs['guess_type'] = guess_type
329        generate_all = super(Template, self).generate(*args, **kwargs)
330
331        return RelatorioStream(generate_all, serializer)
332
333
334class OOSerializer:
335
336    def __init__(self, oo_path):
337        self.inzip = zipfile.ZipFile(oo_path)
338        self.new_oo = StringIO()
339        self.outzip = zipfile.ZipFile(self.new_oo, 'w')
340        self.xml_serializer = genshi.output.XMLSerializer()
341
342    def __call__(self, stream):
343        files = {}
344        for kind, data, pos in stream:
345            if kind == genshi.core.PI and data[0] == 'relatorio':
346                stream_for = data[1]
347                continue
348            files.setdefault(stream_for, []).append((kind, data, pos))
349
350        for f_info in self.inzip.infolist():
351            if f_info.filename.startswith('ObjectReplacements'):
352                continue
353            elif f_info.filename in files:
354                stream = files[f_info.filename]
355                self.outzip.writestr(f_info.filename,
356                                     output_encode(self.xml_serializer(stream)))
357            else:
358                self.outzip.writestr(f_info, self.inzip.read(f_info.filename))
359        self.inzip.close()
360        self.outzip.close()
361
362        return self.new_oo
363
364MIMETemplateLoader.add_factory('oo.org', Template)
Note: See TracBrowser for help on using the browser.