root/relatorio/templates/opendocument.py @ 53:b5e24837a3ba

Revision 53:b5e24837a3ba, 12.6 kB (checked in by Nicolas ?vrard <nicoe@…>, 5 years ago)

Use whitespace class from python in regexp

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 os
24import re
25import md5
26import urllib
27import zipfile
28from cStringIO import StringIO
29
30import warnings
31warnings.filterwarnings('always', module='relatorio.templates.opendocument')
32
33import lxml.etree
34import genshi
35import genshi.output
36from genshi.template import MarkupTemplate
37
38GENSHI_EXPR = re.compile(r'''((/)?(for|choose|otherwise|when|if|with)\s*(\s(\w+)=["'](.*)["']|$)|.*)''')
39EXTENSIONS = {'image/png': 'png',
40              'image/jpeg': 'jpg',
41              'image/bmp': 'bmp',
42              'image/gif': 'gif',
43              'image/tiff': 'tif',
44              'image/xbm': 'xbm',
45             }
46
47_encode = genshi.output.encode
48ETElement = lxml.etree.Element
49
50
51class OOTemplateError(genshi.template.base.TemplateSyntaxError):
52    pass
53
54
55class ImageHref:
56   
57    def __init__(self, zipfile):
58        self.zip = zipfile
59
60    def __call__(self, expr, name):
61        bitstream, mimetype = expr
62        bitstream.seek(0)
63        file_content = bitstream.read()
64        name = md5.new(file_content).hexdigest()
65        path = 'Pictures/%s.%s' % (name, EXTENSIONS[mimetype])
66        if path not in self.zip.namelist():
67            self.zip.writestr(path, file_content)
68        return {'{http://www.w3.org/1999/xlink}href': path}
69
70
71class Template(MarkupTemplate):
72
73    def __init__(self, source, filepath=None, filename=None, loader=None,
74                 encoding=None, lookup='strict', allow_exec=True):
75        self.namespaces = {}
76        self.inner_docs = []
77        super(Template, self).__init__(source, filepath, filename, loader,
78                                       encoding, lookup, allow_exec)
79
80    def _parse(self, source, encoding):
81        inzip = zipfile.ZipFile(self.filepath)
82        content = inzip.read('content.xml')
83        styles = inzip.read('styles.xml')
84
85        genshi_obj = super(Template, self)
86        content = genshi_obj._parse(self.add_directives(content), encoding)
87        styles = genshi_obj._parse(self.add_directives(styles), encoding)
88        content_files= [('content.xml', content)]
89        styles_files = [('styles.xml', styles)]
90
91        while self.inner_docs:
92            doc = self.inner_docs.pop()
93            c_path, s_path = doc + '/content.xml', doc + '/styles.xml'
94            content = inzip.read(c_path)
95            styles = inzip.read(s_path)
96           
97            c_parsed = genshi_obj._parse(self.add_directives(content), encoding)
98            s_parsed = genshi_obj._parse(self.add_directives(styles), encoding)
99
100            content_files.append((c_path, c_parsed))
101            styles_files.append((s_path, s_parsed))
102
103        inzip.close()
104        parsed = []
105        for fpath, fparsed in content_files + styles_files:
106            parsed.append((genshi.core.PI, ('relatorio', fpath), None))
107            parsed += fparsed
108
109        return parsed
110
111    def add_directives(self, content):
112        tree = lxml.etree.parse(StringIO(content))
113        root = tree.getroot()
114        self.namespaces = root.nsmap.copy()
115        self.namespaces['py'] = 'http://genshi.edgewall.org/'
116
117        self._invert_style(tree)
118        self._handle_relatorio_tags(tree)
119        self._handle_images(tree)
120        self._handle_innerdocs(tree)
121        return StringIO(lxml.etree.tostring(tree))
122
123    def _invert_style(self, tree):
124        xpath_expr = "//text:a[starts-with(@xlink:href, 'relatorio://')]"\
125                     "/text:span"
126        for span in tree.xpath(xpath_expr, namespaces=self.namespaces):
127            text_a = span.getparent()
128            outer = text_a.getparent()
129            text_a.text = span.text
130            span.text = ''
131            text_a.remove(span)
132            outer.replace(text_a, span)
133            span.append(text_a)
134
135    def _relatorio_statements(self, tree):
136        # If this node href matches the relatorio URL it is kept.
137        # If this node href matches a genshi directive it is kept for further
138        # processing.
139        r_statements, genshi_dir = [], []
140        xlink_href_attrib = '{%s}href' % self.namespaces['xlink']
141        text_a = '{%s}a' % self.namespaces['text']
142        placeholder = '{%s}placeholder' % self.namespaces['text']
143
144        s_xpath = "//text:a[starts-with(@xlink:href, 'relatorio://')]" \
145                  "| //text:placeholder"
146        for statement in tree.xpath(s_xpath, namespaces=self.namespaces):
147            if statement.tag == placeholder:
148                expr = statement.text[1:-1]
149            elif statement.tag == text_a:
150                expr = urllib.unquote(statement.attrib[xlink_href_attrib][12:])
151
152            if not expr:
153                raise OOTemplateError("No expression in the tag",
154                                      self.filepath)
155            elif not statement.text:
156                warnings.warn('No statement text in %s' % self.filepath)
157            elif expr != statement.text and statement.tag == text_a:
158                warnings.warn('url and text do not match in %s: %s != %s' 
159                              % (self.filepath, expr,
160                                 statement.text.encode('utf-8')))
161
162            match_obj = GENSHI_EXPR.match(expr)
163
164            expr, closing, directive, _, attr, attr_val = match_obj.groups()
165            if directive is not None:
166                genshi_dir.append((statement, closing))
167            r_statements.append((statement, 
168                                 (expr, closing, directive, attr, attr_val)))
169
170        return r_statements, genshi_dir
171
172    def _handle_relatorio_tags(self, tree):
173        """
174        Will treat all relatorio tag (py:if/for/choose/when/otherwise)
175        tags
176        """
177        # Some tag name constants
178        table_cell_tag = '{%s}table-cell' % self.namespaces['table']
179        attrib_name = '{%s}attrs' % self.namespaces['py']
180        office_name = '{%s}value' % self.namespaces['office']
181        office_valuetype = '{%s}value-type' % self.namespaces['office']
182        genshi_replace = '{%s}replace' % self.namespaces['py']
183
184        r_statements, genshi_directives = self._relatorio_statements(tree)
185        # We match the opening and closing directives together
186        idx = 0
187        genshi_pairs, inserted = [], []
188        for statement, closing in genshi_directives:
189            if closing is None:
190                genshi_pairs.append([statement, None])
191                inserted.append(idx)
192                idx += 1
193            else:
194                genshi_pairs[inserted.pop()][1] = statement
195
196        for r_node, parsed in r_statements:
197            expr, c_dir, directive, attr, a_val = parsed
198
199            if directive is not None:
200                # If the node is a genshi directive statement:
201                #    - we operate only on opening statement
202                #    - we find the nearest ancestor of the closing and opening
203                #      statement
204                #    - we create a <py:xxx> node
205                #    - we add all the node between the opening and closing
206                #      statements to this new node
207                #    - we replace the opening statement by the <py:for> node
208                #    - we delete the closing statement
209
210                if c_dir is not None:
211                    # pass the closing statements
212                    continue
213                for pair in genshi_pairs:
214                    if pair[0] == r_node:
215                        break
216                opening, closing = pair
217
218                o_ancestors = list(opening.iterancestors())
219                c_ancestors = list(closing.iterancestors())
220                for n in o_ancestors:
221                    if n in c_ancestors:
222                        ancestor = n
223                        break
224
225                genshi_node = ETElement('{%s}%s' % (self.namespaces['py'],
226                                                    directive), 
227                                        attrib={attr: a_val},
228                                        nsmap=self.namespaces)
229                can_append = False
230                for node in ancestor.iterchildren():
231                    if node in o_ancestors:
232                        outermost_o_ancestor = node
233                        can_append = True
234                        continue
235                    if node in c_ancestors:
236                        outermost_c_ancestor = node
237                        break
238                    if can_append:
239                        genshi_node.append(node)
240                ancestor.replace(outermost_o_ancestor, genshi_node)
241                ancestor.remove(outermost_c_ancestor)
242            else:
243                # It's not a genshi statement it's a python expression
244                r_node.attrib[genshi_replace] = expr
245                parent = r_node.getparent().getparent()
246                if parent is None or parent.tag != table_cell_tag:
247                    continue
248                if parent.attrib.get(office_valuetype, 'string') != 'string':
249                    # The grand-parent tag is a table cell we set the
250                    # office:value attribute of this cell
251                    dico = "{'%s': %s}" % (office_name, expr)
252                    parent.attrib[attrib_name] = dico
253                    parent.attrib.pop(office_name, None)
254
255    def _handle_images(self, tree):
256        draw_name = '{%s}name' % self.namespaces['draw']
257        draw_image = '{%s}image' % self.namespaces['draw']
258        python_attrs = '{%s}attrs' % self.namespaces['py']
259        xpath_expr = "//draw:frame[starts-with(@draw:name, 'image:')]"
260        for draw in tree.xpath(xpath_expr, namespaces=self.namespaces):
261            d_name = draw.attrib[draw_name]
262            attr_expr = "make_href(%s, %r)" % (d_name[7:], d_name[7:])
263            image_node = ETElement(draw_image, 
264                                   attrib={python_attrs: attr_expr},
265                                   nsmap=self.namespaces)
266            draw.replace(draw[0], image_node)
267
268    def _handle_innerdocs(self, tree):
269        href_attrib = '{%s}href' % self.namespaces['xlink']
270        xpath_expr = "//draw:object[starts-with(@xlink:href, './')" \
271                     "and @xlink:show='embed']"
272        for draw in tree.xpath(xpath_expr, namespaces=self.namespaces):
273            self.inner_docs.append(draw.attrib[href_attrib][2:])
274
275    def generate(self, *args, **kwargs):
276        serializer = OOSerializer(self.filepath)
277        kwargs['make_href'] = ImageHref(serializer.outzip)
278        generate_all = super(Template, self).generate(*args, **kwargs)
279
280        return OOStream(generate_all, serializer)
281
282
283class OOStream(genshi.core.Stream):
284
285    def __init__(self, content_stream, serializer):
286        self.events = content_stream
287        self.serializer = serializer
288
289    def render(self, method=None, encoding='utf-8', out=None, **kwargs):
290        return self.serializer(self.events)
291
292    def serialize(self, method, **kwargs):
293        return self.render(method, **kwargs)
294
295    def __or__(self, function):
296        return OOStream(self.events | function, self.serializer)
297
298
299class OOSerializer:
300
301    def __init__(self, oo_path):
302        self.inzip = zipfile.ZipFile(oo_path)
303        self.new_oo = StringIO()
304        self.outzip = zipfile.ZipFile(self.new_oo, 'w')
305        self.xml_serializer = genshi.output.XMLSerializer()
306
307    def __call__(self, stream):
308        files = {}
309        for kind, data, pos in stream:
310            if kind == genshi.core.PI and data[0] == 'relatorio':
311                stream_for = data[1]
312                continue
313            files.setdefault(stream_for, []).append((kind, data, pos))
314
315        for f in self.inzip.infolist():
316            if f.filename.startswith('ObjectReplacements'):
317                continue
318            elif f.filename in files:
319                stream = files[f.filename]
320                self.outzip.writestr(f.filename, 
321                                     _encode(self.xml_serializer(stream)))
322            else:
323                self.outzip.writestr(f, self.inzip.read(f.filename))
324        self.inzip.close()
325        self.outzip.close()
326
327        return self.new_oo
Note: See TracBrowser for help on using the browser.