| 1 | ############################################################################### |
|---|
| 2 | # |
|---|
| 3 | # Copyright (c) 2009 Cedric Krier. |
|---|
| 4 | # Copyright (c) 2007, 2008 OpenHex SPRL. (http://openhex.com) All Rights |
|---|
| 5 | # Reserved. |
|---|
| 6 | # |
|---|
| 7 | # This program is free software; you can redistribute it and/or modify it under |
|---|
| 8 | # the terms of the GNU General Public License as published by the Free Software |
|---|
| 9 | # Foundation; either version 3 of the License, or (at your option) any later |
|---|
| 10 | # version. |
|---|
| 11 | # |
|---|
| 12 | # This program is distributed in the hope that it will be useful, but WITHOUT |
|---|
| 13 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|---|
| 14 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
|---|
| 15 | # details. |
|---|
| 16 | # |
|---|
| 17 | # You should have received a copy of the GNU General Public License along with |
|---|
| 18 | # this program. If not, see <http://www.gnu.org/licenses/>. |
|---|
| 19 | # |
|---|
| 20 | ############################################################################### |
|---|
| 21 | |
|---|
| 22 | __metaclass__ = type |
|---|
| 23 | |
|---|
| 24 | import re |
|---|
| 25 | try: |
|---|
| 26 | # requires python 2.5+ |
|---|
| 27 | from hashlib import md5 |
|---|
| 28 | except ImportError: |
|---|
| 29 | from md5 import md5 |
|---|
| 30 | |
|---|
| 31 | import time |
|---|
| 32 | import urllib |
|---|
| 33 | import zipfile |
|---|
| 34 | from cStringIO import StringIO |
|---|
| 35 | from copy import deepcopy |
|---|
| 36 | |
|---|
| 37 | |
|---|
| 38 | import warnings |
|---|
| 39 | warnings.filterwarnings('always', module='relatorio.templates.opendocument') |
|---|
| 40 | |
|---|
| 41 | import lxml.etree |
|---|
| 42 | import genshi |
|---|
| 43 | import genshi.output |
|---|
| 44 | from genshi.template import MarkupTemplate |
|---|
| 45 | from genshi.filters import Transformer |
|---|
| 46 | from genshi.filters.transform import ENTER, EXIT |
|---|
| 47 | from genshi.core import Stream |
|---|
| 48 | from genshi.template.interpolation import PREFIX |
|---|
| 49 | |
|---|
| 50 | |
|---|
| 51 | from relatorio.templates.base import RelatorioStream |
|---|
| 52 | from relatorio.reporting import Report, MIMETemplateLoader |
|---|
| 53 | try: |
|---|
| 54 | from relatorio.templates.chart import Template as ChartTemplate |
|---|
| 55 | except ImportError: |
|---|
| 56 | ChartTemplate = type(None) |
|---|
| 57 | |
|---|
| 58 | GENSHI_EXPR = re.compile(r''' |
|---|
| 59 | (/)? # is this a closing tag? |
|---|
| 60 | (for|if|choose|when|otherwise|with) # tag directive |
|---|
| 61 | \s* |
|---|
| 62 | (?:\s(\w+)=["'](.*)["']|$) # match a single attr & its value |
|---|
| 63 | | |
|---|
| 64 | .* # or anything else |
|---|
| 65 | ''', re.VERBOSE) |
|---|
| 66 | |
|---|
| 67 | EXTENSIONS = {'image/png': 'png', |
|---|
| 68 | 'image/jpeg': 'jpg', |
|---|
| 69 | 'image/bmp': 'bmp', |
|---|
| 70 | 'image/gif': 'gif', |
|---|
| 71 | 'image/tiff': 'tif', |
|---|
| 72 | 'image/xbm': 'xbm', |
|---|
| 73 | } |
|---|
| 74 | |
|---|
| 75 | RELATORIO_URI = 'http://relatorio.openhex.org/' |
|---|
| 76 | GENSHI_URI = 'http://genshi.edgewall.org/' |
|---|
| 77 | MANIFEST = 'META-INF/manifest.xml' |
|---|
| 78 | output_encode = genshi.output.encode |
|---|
| 79 | EtreeElement = lxml.etree.Element |
|---|
| 80 | |
|---|
| 81 | # A note regarding OpenDocument namespaces: |
|---|
| 82 | # |
|---|
| 83 | # The current code assumes the original OpenOffice document uses default |
|---|
| 84 | # namespace prefix ("table", "xlink", "draw", ...). We derive the actual |
|---|
| 85 | # namespaces URIs from their prefix, instead of the other way round. This has |
|---|
| 86 | # the advantage that if a new version of the format use different namespaces |
|---|
| 87 | # (this is not the case for ODF 1.1 but could be the case in the future since |
|---|
| 88 | # there is a version number in those namespaces after all), Relatorio will |
|---|
| 89 | # support those new formats out of the box. |
|---|
| 90 | |
|---|
| 91 | |
|---|
| 92 | # A note about attribute namespaces: |
|---|
| 93 | # |
|---|
| 94 | # Ideally, we should update the namespace map of all the nodes we add |
|---|
| 95 | # (Genshi) attributes to, so that the attributes use a nice "py" prefix instead |
|---|
| 96 | # of a generated one (eg. "ns0", which is correct but ugly) in the case no |
|---|
| 97 | # parent node defines it. Unfortunately, lxml doesn't support this: |
|---|
| 98 | # the nsmap attribute of Element objects is (currently) readonly. |
|---|
| 99 | |
|---|
| 100 | def guess_type(val): |
|---|
| 101 | if isinstance(val, (str, unicode)): |
|---|
| 102 | return 'string' |
|---|
| 103 | elif isinstance(val, (int, float, long)): |
|---|
| 104 | return 'float' |
|---|
| 105 | |
|---|
| 106 | class OOTemplateError(genshi.template.base.TemplateSyntaxError): |
|---|
| 107 | "Error to raise when there is a SyntaxError in the genshi template" |
|---|
| 108 | |
|---|
| 109 | |
|---|
| 110 | class ImageHref: |
|---|
| 111 | "A class used to add images in the odf zipfile" |
|---|
| 112 | |
|---|
| 113 | def __init__(self, zfile, manifest, context): |
|---|
| 114 | self.zip = zfile |
|---|
| 115 | self.manifest = manifest |
|---|
| 116 | self.context = context.copy() |
|---|
| 117 | |
|---|
| 118 | def __call__(self, expr): |
|---|
| 119 | bitstream, mimetype = expr[:2] |
|---|
| 120 | if isinstance(bitstream, Report): |
|---|
| 121 | bitstream = bitstream(**self.context).render() |
|---|
| 122 | elif isinstance(bitstream, ChartTemplate): |
|---|
| 123 | bitstream = bitstream.generate(**self.context).render() |
|---|
| 124 | bitstream.seek(0) |
|---|
| 125 | file_content = bitstream.read() |
|---|
| 126 | name = md5(file_content).hexdigest() |
|---|
| 127 | path = 'Pictures/%s.%s' % (name, EXTENSIONS[mimetype]) |
|---|
| 128 | if path not in self.zip.namelist(): |
|---|
| 129 | self.zip.writestr(path, file_content) |
|---|
| 130 | self.manifest.add_file_entry(path, mimetype) |
|---|
| 131 | return {'{http://www.w3.org/1999/xlink}href': path} |
|---|
| 132 | |
|---|
| 133 | |
|---|
| 134 | class ImageDimension: |
|---|
| 135 | "A class used to set dimension in draw tags" |
|---|
| 136 | |
|---|
| 137 | def __init__(self, namespaces): |
|---|
| 138 | self.namespaces = namespaces |
|---|
| 139 | |
|---|
| 140 | def __call__(self, expr, width, height): |
|---|
| 141 | # expr could be (bitstream, mimetype) |
|---|
| 142 | # or (bitstreamm mimetype, width, height) |
|---|
| 143 | if len(expr) == 4: |
|---|
| 144 | width, height = expr[2:] |
|---|
| 145 | attrs = {} |
|---|
| 146 | if width: |
|---|
| 147 | attrs['{%s}width' % self.namespaces['svg']] = width |
|---|
| 148 | if height: |
|---|
| 149 | attrs['{%s}height' % self.namespaces['svg']] = height |
|---|
| 150 | return attrs |
|---|
| 151 | |
|---|
| 152 | |
|---|
| 153 | class ColumnCounter: |
|---|
| 154 | """A class used to count the actual maximum number of cells (and thus |
|---|
| 155 | columns) a table contains accross its rows. |
|---|
| 156 | """ |
|---|
| 157 | def __init__(self): |
|---|
| 158 | self.temp_counters = {} |
|---|
| 159 | self.counters = {} |
|---|
| 160 | |
|---|
| 161 | def reset(self, loop_id): |
|---|
| 162 | self.temp_counters[loop_id] = 0 |
|---|
| 163 | |
|---|
| 164 | def inc(self, loop_id): |
|---|
| 165 | self.temp_counters[loop_id] += 1 |
|---|
| 166 | |
|---|
| 167 | def store(self, loop_id, table_name): |
|---|
| 168 | self.counters[table_name] = max(self.temp_counters.pop(loop_id), |
|---|
| 169 | self.counters.get(table_name, 0)) |
|---|
| 170 | |
|---|
| 171 | |
|---|
| 172 | def wrap_nodes_between(first, last, new_parent): |
|---|
| 173 | """An helper function to move all nodes between two nodes to a new node |
|---|
| 174 | and add that new node to their former parent. The boundary nodes are |
|---|
| 175 | removed in the process. |
|---|
| 176 | """ |
|---|
| 177 | old_parent = first.getparent() |
|---|
| 178 | |
|---|
| 179 | # Any text after the opening tag (and not within a tag) need to be handled |
|---|
| 180 | # explicitly. For example in <if>xxx<span>yyy</span>zzz</if>, zzz is |
|---|
| 181 | # copied along the span tag, but not xxx, which corresponds to the tail |
|---|
| 182 | # attribute of the opening tag. |
|---|
| 183 | if first.tail: |
|---|
| 184 | new_parent.text = first.tail |
|---|
| 185 | for node in first.itersiblings(): |
|---|
| 186 | if node is last: |
|---|
| 187 | break |
|---|
| 188 | # appending a node to a new parent also |
|---|
| 189 | # remove it from its previous parent |
|---|
| 190 | new_parent.append(node) |
|---|
| 191 | old_parent.replace(first, new_parent) |
|---|
| 192 | old_parent.remove(last) |
|---|
| 193 | |
|---|
| 194 | |
|---|
| 195 | def update_py_attrs(node, value): |
|---|
| 196 | """An helper function to update py_attrs of a node. |
|---|
| 197 | """ |
|---|
| 198 | if not value: |
|---|
| 199 | return |
|---|
| 200 | py_attrs_attr = '{%s}attrs' % GENSHI_URI |
|---|
| 201 | if not py_attrs_attr in node.attrib: |
|---|
| 202 | node.attrib[py_attrs_attr] = value |
|---|
| 203 | else: |
|---|
| 204 | node.attrib[py_attrs_attr] = \ |
|---|
| 205 | "(lambda x, y: x.update(y) or x)(%s or {}, %s or {})" % \ |
|---|
| 206 | (node.attrib[py_attrs_attr], value) |
|---|
| 207 | |
|---|
| 208 | |
|---|
| 209 | class Template(MarkupTemplate): |
|---|
| 210 | |
|---|
| 211 | def __init__(self, source, filepath=None, filename=None, loader=None, |
|---|
| 212 | encoding=None, lookup='strict', allow_exec=True): |
|---|
| 213 | self.namespaces = {} |
|---|
| 214 | self.inner_docs = [] |
|---|
| 215 | self.has_col_loop = False |
|---|
| 216 | super(Template, self).__init__(source, filepath, filename, loader, |
|---|
| 217 | encoding, lookup, allow_exec) |
|---|
| 218 | |
|---|
| 219 | def _parse(self, source, encoding): |
|---|
| 220 | """parses the odf file. |
|---|
| 221 | |
|---|
| 222 | It adds genshi directives and finds the inner docs. |
|---|
| 223 | """ |
|---|
| 224 | zf = zipfile.ZipFile(self.filepath) |
|---|
| 225 | content = zf.read('content.xml') |
|---|
| 226 | styles = zf.read('styles.xml') |
|---|
| 227 | |
|---|
| 228 | template = super(Template, self) |
|---|
| 229 | content = template._parse(self.insert_directives(content), encoding) |
|---|
| 230 | styles = template._parse(self.insert_directives(styles), encoding) |
|---|
| 231 | content_files = [('content.xml', content)] |
|---|
| 232 | styles_files = [('styles.xml', styles)] |
|---|
| 233 | |
|---|
| 234 | while self.inner_docs: |
|---|
| 235 | doc = self.inner_docs.pop() |
|---|
| 236 | c_path, s_path = doc + '/content.xml', doc + '/styles.xml' |
|---|
| 237 | content = zf.read(c_path) |
|---|
| 238 | styles = zf.read(s_path) |
|---|
| 239 | |
|---|
| 240 | c_parsed = template._parse(self.insert_directives(content), |
|---|
| 241 | encoding) |
|---|
| 242 | s_parsed = template._parse(self.insert_directives(styles), |
|---|
| 243 | encoding) |
|---|
| 244 | content_files.append((c_path, c_parsed)) |
|---|
| 245 | styles_files.append((s_path, s_parsed)) |
|---|
| 246 | |
|---|
| 247 | zf.close() |
|---|
| 248 | parsed = [] |
|---|
| 249 | for fpath, fparsed in content_files + styles_files: |
|---|
| 250 | parsed.append((genshi.core.PI, ('relatorio', fpath), None)) |
|---|
| 251 | parsed += fparsed |
|---|
| 252 | |
|---|
| 253 | return parsed |
|---|
| 254 | |
|---|
| 255 | def insert_directives(self, content): |
|---|
| 256 | """adds the genshi directives, handle the images and the innerdocs. |
|---|
| 257 | """ |
|---|
| 258 | tree = lxml.etree.parse(StringIO(content)) |
|---|
| 259 | root = tree.getroot() |
|---|
| 260 | |
|---|
| 261 | # assign default/fake namespaces so that documents do not need to |
|---|
| 262 | # define them if they don't use them |
|---|
| 263 | self.namespaces = { |
|---|
| 264 | "text": "urn:text", |
|---|
| 265 | "draw": "urn:draw", |
|---|
| 266 | "table": "urn:table", |
|---|
| 267 | "office": "urn:office", |
|---|
| 268 | "xlink": "urn:xlink", |
|---|
| 269 | "svg": "urn:svg", |
|---|
| 270 | } |
|---|
| 271 | # but override them with the real namespaces |
|---|
| 272 | self.namespaces.update(root.nsmap) |
|---|
| 273 | |
|---|
| 274 | # remove any "root" namespace as lxml.xpath do not support them |
|---|
| 275 | self.namespaces.pop(None, None) |
|---|
| 276 | |
|---|
| 277 | self.namespaces['py'] = GENSHI_URI |
|---|
| 278 | self.namespaces['relatorio'] = RELATORIO_URI |
|---|
| 279 | |
|---|
| 280 | self._invert_style(tree) |
|---|
| 281 | self._handle_relatorio_tags(tree) |
|---|
| 282 | self._handle_images(tree) |
|---|
| 283 | self._handle_innerdocs(tree) |
|---|
| 284 | self._escape_values(tree) |
|---|
| 285 | return StringIO(lxml.etree.tostring(tree)) |
|---|
| 286 | |
|---|
| 287 | def _invert_style(self, tree): |
|---|
| 288 | "inverts the text:a and text:span" |
|---|
| 289 | xpath_expr = "//text:a[starts-with(@xlink:href, 'relatorio://')]" \ |
|---|
| 290 | "/text:span" |
|---|
| 291 | for span in tree.xpath(xpath_expr, namespaces=self.namespaces): |
|---|
| 292 | text_a = span.getparent() |
|---|
| 293 | outer = text_a.getparent() |
|---|
| 294 | text_a.text = span.text |
|---|
| 295 | span.text = '' |
|---|
| 296 | text_a.remove(span) |
|---|
| 297 | outer.replace(text_a, span) |
|---|
| 298 | span.append(text_a) |
|---|
| 299 | |
|---|
| 300 | def _relatorio_statements(self, tree): |
|---|
| 301 | "finds the relatorio statements (text:a/text:placeholder)" |
|---|
| 302 | # If this node href matches the relatorio URL it is kept. |
|---|
| 303 | # If this node href matches a genshi directive it is kept for further |
|---|
| 304 | # processing. |
|---|
| 305 | xlink_href_attrib = '{%s}href' % self.namespaces['xlink'] |
|---|
| 306 | text_a = '{%s}a' % self.namespaces['text'] |
|---|
| 307 | placeholder = '{%s}placeholder' % self.namespaces['text'] |
|---|
| 308 | s_xpath = "//text:a[starts-with(@xlink:href, 'relatorio://')]" \ |
|---|
| 309 | "| //text:placeholder" |
|---|
| 310 | |
|---|
| 311 | r_statements = [] |
|---|
| 312 | opened_tags = [] |
|---|
| 313 | # We map each opening tag with its closing tag |
|---|
| 314 | closing_tags = {} |
|---|
| 315 | for statement in tree.xpath(s_xpath, namespaces=self.namespaces): |
|---|
| 316 | if statement.tag == placeholder: |
|---|
| 317 | expr = statement.text[1:-1] |
|---|
| 318 | elif statement.tag == text_a: |
|---|
| 319 | expr = urllib.unquote(statement.attrib[xlink_href_attrib][12:]) |
|---|
| 320 | |
|---|
| 321 | if not expr: |
|---|
| 322 | raise OOTemplateError("No expression in the tag", |
|---|
| 323 | self.filepath) |
|---|
| 324 | closing, directive, attr, attr_val = \ |
|---|
| 325 | GENSHI_EXPR.match(expr).groups() |
|---|
| 326 | is_opening = closing != '/' |
|---|
| 327 | |
|---|
| 328 | warn_msg = None |
|---|
| 329 | if not statement.text: |
|---|
| 330 | warn_msg = "No statement text in '%s' for '%s'" \ |
|---|
| 331 | % (self.filepath, expr) |
|---|
| 332 | elif expr != statement.text and statement.tag == text_a: |
|---|
| 333 | warn_msg = "url and text do not match in %s: %s != %s" \ |
|---|
| 334 | % (self.filepath, expr, |
|---|
| 335 | statement.text.encode('utf-8')) |
|---|
| 336 | if warn_msg: |
|---|
| 337 | if directive is not None and not is_opening: |
|---|
| 338 | warn_msg += " corresponding to opening tag '%s'" \ |
|---|
| 339 | % opened_tags[-1].text |
|---|
| 340 | warnings.warn(warn_msg) |
|---|
| 341 | |
|---|
| 342 | if directive is not None: |
|---|
| 343 | # map closing tags with their opening tag |
|---|
| 344 | if is_opening: |
|---|
| 345 | opened_tags.append(statement) |
|---|
| 346 | else: |
|---|
| 347 | closing_tags[id(opened_tags.pop())] = statement |
|---|
| 348 | # - we only need to return opening statements |
|---|
| 349 | if is_opening: |
|---|
| 350 | r_statements.append((statement, |
|---|
| 351 | (expr, directive, attr, attr_val)) |
|---|
| 352 | ) |
|---|
| 353 | assert not opened_tags |
|---|
| 354 | return r_statements, closing_tags |
|---|
| 355 | |
|---|
| 356 | def _handle_relatorio_tags(self, tree): |
|---|
| 357 | """ |
|---|
| 358 | Will treat all relatorio tag (py:if/for/choose/when/otherwise) |
|---|
| 359 | tags |
|---|
| 360 | """ |
|---|
| 361 | # Some tag/attribute name constants |
|---|
| 362 | table_namespace = self.namespaces['table'] |
|---|
| 363 | table_row_tag = '{%s}table-row' % table_namespace |
|---|
| 364 | table_cell_tag = '{%s}table-cell' % table_namespace |
|---|
| 365 | |
|---|
| 366 | office_name = '{%s}value' % self.namespaces['office'] |
|---|
| 367 | office_valuetype = '{%s}value-type' % self.namespaces['office'] |
|---|
| 368 | |
|---|
| 369 | py_attrs_attr = '{%s}attrs' % GENSHI_URI |
|---|
| 370 | py_replace = '{%s}replace' % GENSHI_URI |
|---|
| 371 | |
|---|
| 372 | r_statements, closing_tags = self._relatorio_statements(tree) |
|---|
| 373 | |
|---|
| 374 | for r_node, parsed in r_statements: |
|---|
| 375 | expr, directive, attr, a_val = parsed |
|---|
| 376 | |
|---|
| 377 | # If the node is a genshi directive statement: |
|---|
| 378 | if directive is not None: |
|---|
| 379 | opening = r_node |
|---|
| 380 | closing = closing_tags[id(r_node)] |
|---|
| 381 | |
|---|
| 382 | # - we find the nearest common ancestor of the closing and |
|---|
| 383 | # opening statements |
|---|
| 384 | o_ancestors = [opening] |
|---|
| 385 | c_ancestors = [closing] + list(closing.iterancestors()) |
|---|
| 386 | ancestor = None |
|---|
| 387 | for node in opening.iterancestors(): |
|---|
| 388 | try: |
|---|
| 389 | idx = c_ancestors.index(node) |
|---|
| 390 | assert c_ancestors[idx] == node |
|---|
| 391 | # we only need ancestors up to the common one |
|---|
| 392 | del c_ancestors[idx:] |
|---|
| 393 | ancestor = node |
|---|
| 394 | break |
|---|
| 395 | except ValueError: |
|---|
| 396 | # c_ancestors.index(node) raise ValueError if node is |
|---|
| 397 | # not a child of c_ancestors |
|---|
| 398 | pass |
|---|
| 399 | o_ancestors.append(node) |
|---|
| 400 | assert ancestor is not None, \ |
|---|
| 401 | "No common ancestor found for opening and closing tag" |
|---|
| 402 | |
|---|
| 403 | outermost_o_ancestor = o_ancestors[-1] |
|---|
| 404 | outermost_c_ancestor = c_ancestors[-1] |
|---|
| 405 | |
|---|
| 406 | # handle horizontal repetitions (over columns) |
|---|
| 407 | if directive == "for" and ancestor.tag == table_row_tag: |
|---|
| 408 | a_val = self._handle_column_loops(parsed, ancestor, |
|---|
| 409 | opening, |
|---|
| 410 | outermost_o_ancestor, |
|---|
| 411 | outermost_c_ancestor) |
|---|
| 412 | |
|---|
| 413 | # - we create a <py:xxx> node |
|---|
| 414 | if attr is not None: |
|---|
| 415 | attribs = {attr: a_val} |
|---|
| 416 | else: |
|---|
| 417 | attribs = {} |
|---|
| 418 | genshi_node = EtreeElement('{%s}%s' % (GENSHI_URI, |
|---|
| 419 | directive), |
|---|
| 420 | attrib=attribs, |
|---|
| 421 | nsmap={'py': GENSHI_URI}) |
|---|
| 422 | |
|---|
| 423 | # - we move all the nodes between the opening and closing |
|---|
| 424 | # statements to this new node (append also removes from old |
|---|
| 425 | # parent) |
|---|
| 426 | # - we replace the opening statement by the <py:xxx> node |
|---|
| 427 | # - we delete the closing statement (and its ancestors) |
|---|
| 428 | wrap_nodes_between(outermost_o_ancestor, outermost_c_ancestor, |
|---|
| 429 | genshi_node) |
|---|
| 430 | else: |
|---|
| 431 | # It's not a genshi statement it's a python expression |
|---|
| 432 | r_node.attrib[py_replace] = expr |
|---|
| 433 | parent = r_node.getparent().getparent() |
|---|
| 434 | if parent is None or parent.tag != table_cell_tag: |
|---|
| 435 | continue |
|---|
| 436 | |
|---|
| 437 | # The grand-parent tag is a table cell we should set the |
|---|
| 438 | # correct value and type for this cell. |
|---|
| 439 | dico = "{'%s': %s, '%s': __relatorio_guess_type(%s)}" |
|---|
| 440 | update_py_attrs(parent, dico % |
|---|
| 441 | (office_name, expr, office_valuetype, expr)) |
|---|
| 442 | parent.attrib.pop(office_valuetype, None) |
|---|
| 443 | parent.attrib.pop(office_name, None) |
|---|
| 444 | |
|---|
| 445 | def _handle_column_loops(self, statement, ancestor, opening, |
|---|
| 446 | outer_o_node, outer_c_node): |
|---|
| 447 | _, directive, attr, a_val = statement |
|---|
| 448 | |
|---|
| 449 | self.has_col_loop = True |
|---|
| 450 | |
|---|
| 451 | table_namespace = self.namespaces['table'] |
|---|
| 452 | table_col_tag = '{%s}table-column' % table_namespace |
|---|
| 453 | table_num_col_attr = '{%s}number-columns-repeated' % table_namespace |
|---|
| 454 | |
|---|
| 455 | py_attrs_attr = '{%s}attrs' % GENSHI_URI |
|---|
| 456 | repeat_tag = '{%s}repeat' % RELATORIO_URI |
|---|
| 457 | |
|---|
| 458 | # table node (it is not necessarily the direct parent of ancestor) |
|---|
| 459 | table_node = ancestor.iterancestors('{%s}table' % table_namespace) \ |
|---|
| 460 | .next() |
|---|
| 461 | table_name = table_node.attrib['{%s}name' % table_namespace] |
|---|
| 462 | |
|---|
| 463 | # add counting instructions |
|---|
| 464 | loop_id = id(opening) |
|---|
| 465 | |
|---|
| 466 | # 1) add reset counter code on the row opening tag |
|---|
| 467 | # (through a py:attrs attribute). |
|---|
| 468 | # Note that table_name is not needed in the first two |
|---|
| 469 | # operations, but a unique id within the table is required |
|---|
| 470 | # to support nested column repetition |
|---|
| 471 | update_py_attrs(ancestor, "__relatorio_reset_col_count(%d)" % loop_id) |
|---|
| 472 | |
|---|
| 473 | # 2) add increment code (through a py:attrs attribute) on |
|---|
| 474 | # the first cell node after the opening (cell node) |
|---|
| 475 | # ancestor |
|---|
| 476 | enclosed_cell = outer_o_node.getnext() |
|---|
| 477 | assert enclosed_cell.tag == '{%s}table-cell' % table_namespace |
|---|
| 478 | update_py_attrs(enclosed_cell, "__relatorio_inc_col_count(%d)" % |
|---|
| 479 | loop_id) |
|---|
| 480 | |
|---|
| 481 | # 3) add "store count" code as a py:replace node, as the |
|---|
| 482 | # last child of the row |
|---|
| 483 | attr_value = "__relatorio_store_col_count(%d, %r)" \ |
|---|
| 484 | % (loop_id, table_name) |
|---|
| 485 | replace_node = EtreeElement('{%s}replace' % GENSHI_URI, |
|---|
| 486 | attrib={'value': attr_value}, |
|---|
| 487 | nsmap={'py': GENSHI_URI}) |
|---|
| 488 | ancestor.append(replace_node) |
|---|
| 489 | |
|---|
| 490 | # find the position in the row of the cells holding the |
|---|
| 491 | # <for> and </for> instructions |
|---|
| 492 | # We use "*" so as to count both normal cells and covered/hidden cells |
|---|
| 493 | position_xpath_expr = 'count(preceding-sibling::*)' |
|---|
| 494 | opening_pos = \ |
|---|
| 495 | int(outer_o_node.xpath(position_xpath_expr, |
|---|
| 496 | namespaces=self.namespaces)) |
|---|
| 497 | closing_pos = \ |
|---|
| 498 | int(outer_c_node.xpath(position_xpath_expr, |
|---|
| 499 | namespaces=self.namespaces)) |
|---|
| 500 | |
|---|
| 501 | # check whether or not the opening tag spans several rows |
|---|
| 502 | a_val = self._handle_row_spanned_column_loops( |
|---|
| 503 | statement, outer_o_node, opening_pos, closing_pos) |
|---|
| 504 | |
|---|
| 505 | # check if this table's headers were already processed |
|---|
| 506 | repeat_node = table_node.find(repeat_tag) |
|---|
| 507 | if repeat_node is not None: |
|---|
| 508 | prev_pos = (int(repeat_node.attrib['opening']), |
|---|
| 509 | int(repeat_node.attrib['closing'])) |
|---|
| 510 | if (opening_pos, closing_pos) != prev_pos: |
|---|
| 511 | raise Exception( |
|---|
| 512 | 'Incoherent column repetition found! ' |
|---|
| 513 | 'If a table has several lines with repeated ' |
|---|
| 514 | 'columns, the repetition need to be on the ' |
|---|
| 515 | 'same columns across all lines.') |
|---|
| 516 | else: |
|---|
| 517 | # compute splits: oo collapses the headers of adjacent |
|---|
| 518 | # columns which use the same style. We need to split |
|---|
| 519 | # any column header which is repeated so many times |
|---|
| 520 | # that it encompasses any of the column headers that |
|---|
| 521 | # we need to repeat |
|---|
| 522 | to_split = [] |
|---|
| 523 | idx = 0 |
|---|
| 524 | childs = list(table_node.iterchildren(table_col_tag)) |
|---|
| 525 | for tag in childs: |
|---|
| 526 | inc = int(tag.attrib.get(table_num_col_attr, 1)) |
|---|
| 527 | oldidx = idx |
|---|
| 528 | idx += inc |
|---|
| 529 | if oldidx < opening_pos < idx or \ |
|---|
| 530 | oldidx < closing_pos < idx: |
|---|
| 531 | to_split.append(tag) |
|---|
| 532 | |
|---|
| 533 | # split tags |
|---|
| 534 | for tag in to_split: |
|---|
| 535 | tag_pos = table_node.index(tag) |
|---|
| 536 | num = int(tag.attrib.pop(table_num_col_attr)) |
|---|
| 537 | new_tags = [deepcopy(tag) for _ in range(num)] |
|---|
| 538 | table_node[tag_pos:tag_pos+1] = new_tags |
|---|
| 539 | |
|---|
| 540 | # recompute the list of column headers as it could |
|---|
| 541 | # have changed. |
|---|
| 542 | coldefs = list(table_node.iterchildren(table_col_tag)) |
|---|
| 543 | |
|---|
| 544 | # compute the column header nodes corresponding to |
|---|
| 545 | # the opening and closing tags. |
|---|
| 546 | first = table_node[opening_pos] |
|---|
| 547 | last = table_node[closing_pos] |
|---|
| 548 | |
|---|
| 549 | # add a <relatorio:repeat> node around the column |
|---|
| 550 | # definitions nodes |
|---|
| 551 | attribs = { |
|---|
| 552 | "opening": str(opening_pos), |
|---|
| 553 | "closing": str(closing_pos), |
|---|
| 554 | "table": table_name |
|---|
| 555 | } |
|---|
| 556 | repeat_node = EtreeElement(repeat_tag, attrib=attribs, |
|---|
| 557 | nsmap={'relatorio': RELATORIO_URI}) |
|---|
| 558 | wrap_nodes_between(first, last, repeat_node) |
|---|
| 559 | return a_val |
|---|
| 560 | |
|---|
| 561 | def _handle_row_spanned_column_loops(self, statement, outer_o_node, |
|---|
| 562 | opening_pos, closing_pos): |
|---|
| 563 | """handles column repetitions which span several rows, by duplicating |
|---|
| 564 | the py:for node for each row, and make the loops work on a copy of the |
|---|
| 565 | original iterable as to not exhaust generators.""" |
|---|
| 566 | |
|---|
| 567 | _, directive, attr, a_val = statement |
|---|
| 568 | table_namespace = self.namespaces['table'] |
|---|
| 569 | table_rowspan_attr = '{%s}number-rows-spanned' % table_namespace |
|---|
| 570 | |
|---|
| 571 | # checks wether there is a (meaningful) rowspan |
|---|
| 572 | rows_spanned = int(outer_o_node.attrib.get(table_rowspan_attr, 1)) |
|---|
| 573 | if rows_spanned == 1: |
|---|
| 574 | return a_val |
|---|
| 575 | |
|---|
| 576 | table_row_tag = '{%s}table-row' % table_namespace |
|---|
| 577 | table_cov_cell_tag = '{%s}covered-table-cell' % table_namespace |
|---|
| 578 | |
|---|
| 579 | # if so, we need to: |
|---|
| 580 | |
|---|
| 581 | # 1) create a with node to define a temporary variable |
|---|
| 582 | temp_var = "__relatorio_temp%d" % id(outer_o_node) |
|---|
| 583 | # a_val == "target in iterable" |
|---|
| 584 | target, iterable = a_val.split(' in ', 1) |
|---|
| 585 | vars = "%s = list(%s)" % (temp_var, iterable.strip()) |
|---|
| 586 | with_node = EtreeElement('{%s}with' % GENSHI_URI, |
|---|
| 587 | attrib={"vars": vars}, |
|---|
| 588 | nsmap={'py': GENSHI_URI}) |
|---|
| 589 | |
|---|
| 590 | # 2) transform a_val to use that temporary variable |
|---|
| 591 | a_val = "%s in %s" % (target, temp_var) |
|---|
| 592 | |
|---|
| 593 | # 3) wrap the corresponding cells on the next row(s) |
|---|
| 594 | # (those should be covered-table-cell) inside a |
|---|
| 595 | # duplicate py:for node (looping on the temporary |
|---|
| 596 | # variable). |
|---|
| 597 | row_node = outer_o_node.getparent() |
|---|
| 598 | row_node.addprevious(with_node) |
|---|
| 599 | rows_to_wrap = [row_node] |
|---|
| 600 | assert row_node.tag == table_row_tag |
|---|
| 601 | next_rows = row_node.itersiblings(table_row_tag) |
|---|
| 602 | for row_idx in range(rows_spanned-1): |
|---|
| 603 | next_row_node = next_rows.next() |
|---|
| 604 | rows_to_wrap.append(next_row_node) |
|---|
| 605 | # compute the start and end nodes |
|---|
| 606 | first = next_row_node[opening_pos] |
|---|
| 607 | last = next_row_node[closing_pos] |
|---|
| 608 | assert first.tag == table_cov_cell_tag |
|---|
| 609 | assert last.tag == table_cov_cell_tag |
|---|
| 610 | # wrap them |
|---|
| 611 | tag = '{%s}%s' % (GENSHI_URI, directive) |
|---|
| 612 | for_node = EtreeElement(tag, |
|---|
| 613 | attrib={attr: a_val}, |
|---|
| 614 | nsmap={'py': GENSHI_URI}) |
|---|
| 615 | wrap_nodes_between(first, last, for_node) |
|---|
| 616 | |
|---|
| 617 | # 4) wrap all the corresponding rows indide the "with" |
|---|
| 618 | # node |
|---|
| 619 | for node in rows_to_wrap: |
|---|
| 620 | with_node.append(node) |
|---|
| 621 | return a_val |
|---|
| 622 | |
|---|
| 623 | def _handle_images(self, tree): |
|---|
| 624 | "replaces all draw:frame named 'image: ...' by draw:image nodes" |
|---|
| 625 | draw_namespace = self.namespaces['draw'] |
|---|
| 626 | draw_name = '{%s}name' % draw_namespace |
|---|
| 627 | draw_image = '{%s}image' % draw_namespace |
|---|
| 628 | py_attrs = '{%s}attrs' % self.namespaces['py'] |
|---|
| 629 | svg_namespace = self.namespaces['svg'] |
|---|
| 630 | svg_width = '{%s}width' % svg_namespace |
|---|
| 631 | svg_height = '{%s}height' % svg_namespace |
|---|
| 632 | xpath_expr = "//draw:frame[starts-with(@draw:name, 'image:')]" |
|---|
| 633 | for draw in tree.xpath(xpath_expr, namespaces=self.namespaces): |
|---|
| 634 | d_name = draw.attrib[draw_name][6:].strip() |
|---|
| 635 | attr_expr = "__relatorio_make_href(%s)" % d_name |
|---|
| 636 | image_node = EtreeElement(draw_image, |
|---|
| 637 | attrib={py_attrs: attr_expr}, |
|---|
| 638 | nsmap={'draw': draw_namespace, |
|---|
| 639 | 'py': GENSHI_URI}) |
|---|
| 640 | draw.replace(draw[0], image_node) |
|---|
| 641 | width = draw.attrib.pop(svg_width, None) |
|---|
| 642 | height = draw.attrib.pop(svg_height, None) |
|---|
| 643 | attr_expr = "__relatorio_make_dimension(%s, '%s', '%s')" % \ |
|---|
| 644 | (d_name, width, height) |
|---|
| 645 | draw.attrib[py_attrs] = attr_expr |
|---|
| 646 | |
|---|
| 647 | def _handle_innerdocs(self, tree): |
|---|
| 648 | "finds inner_docs and adds them to the processing stack." |
|---|
| 649 | href_attrib = '{%s}href' % self.namespaces['xlink'] |
|---|
| 650 | xpath_expr = "//draw:object[starts-with(@xlink:href, './')" \ |
|---|
| 651 | "and @xlink:show='embed']" |
|---|
| 652 | for draw in tree.xpath(xpath_expr, namespaces=self.namespaces): |
|---|
| 653 | self.inner_docs.append(draw.attrib[href_attrib][2:]) |
|---|
| 654 | |
|---|
| 655 | def _escape_values(self, tree): |
|---|
| 656 | "escapes element values" |
|---|
| 657 | for element in tree.iter(): |
|---|
| 658 | for attrs in element.keys(): |
|---|
| 659 | if not attrs.startswith('{%s}' % GENSHI_URI): |
|---|
| 660 | element.attrib[attrs] = element.attrib[attrs]\ |
|---|
| 661 | .replace(PREFIX, PREFIX * 2) |
|---|
| 662 | if element.text: |
|---|
| 663 | element.text = element.text.replace(PREFIX, PREFIX * 2) |
|---|
| 664 | |
|---|
| 665 | def generate(self, *args, **kwargs): |
|---|
| 666 | "creates the RelatorioStream." |
|---|
| 667 | serializer = OOSerializer(self.filepath) |
|---|
| 668 | kwargs['__relatorio_make_href'] = ImageHref(serializer.outzip, |
|---|
| 669 | serializer.manifest, |
|---|
| 670 | kwargs) |
|---|
| 671 | kwargs['__relatorio_make_dimension'] = ImageDimension(self.namespaces) |
|---|
| 672 | kwargs['__relatorio_guess_type'] = guess_type |
|---|
| 673 | |
|---|
| 674 | counter = ColumnCounter() |
|---|
| 675 | kwargs['__relatorio_reset_col_count'] = counter.reset |
|---|
| 676 | kwargs['__relatorio_inc_col_count'] = counter.inc |
|---|
| 677 | kwargs['__relatorio_store_col_count'] = counter.store |
|---|
| 678 | |
|---|
| 679 | stream = super(Template, self).generate(*args, **kwargs) |
|---|
| 680 | if self.has_col_loop: |
|---|
| 681 | # Note that we can't simply add a "number-columns-repeated" |
|---|
| 682 | # attribute and then fill it with the correct number of columns |
|---|
| 683 | # because that wouldn't work if more than one column is repeated. |
|---|
| 684 | transformation = DuplicateColumnHeaders(counter) |
|---|
| 685 | col_filter = Transformer('//repeat[namespace-uri()="%s"]' |
|---|
| 686 | % RELATORIO_URI) |
|---|
| 687 | col_filter = col_filter.apply(transformation) |
|---|
| 688 | stream = Stream(list(stream), self.serializer) | col_filter |
|---|
| 689 | return RelatorioStream(stream, serializer) |
|---|
| 690 | |
|---|
| 691 | |
|---|
| 692 | class DuplicateColumnHeaders(object): |
|---|
| 693 | def __init__(self, counter): |
|---|
| 694 | self.counter = counter |
|---|
| 695 | |
|---|
| 696 | def __call__(self, stream): |
|---|
| 697 | for mark, (kind, data, pos) in stream: |
|---|
| 698 | # for each repeat tag found |
|---|
| 699 | if mark is ENTER: |
|---|
| 700 | # get the number of columns for that table |
|---|
| 701 | attrs = data[1] |
|---|
| 702 | table = attrs.get('table') |
|---|
| 703 | col_count = self.counter.counters[table] |
|---|
| 704 | |
|---|
| 705 | # collect events (column header tags) to repeat |
|---|
| 706 | events = [] |
|---|
| 707 | for submark, event in stream: |
|---|
| 708 | if submark is EXIT: |
|---|
| 709 | break |
|---|
| 710 | events.append(event) |
|---|
| 711 | |
|---|
| 712 | # repeat them |
|---|
| 713 | for _ in range(col_count): |
|---|
| 714 | for event in events: |
|---|
| 715 | yield None, event |
|---|
| 716 | else: |
|---|
| 717 | yield mark, (kind, data, pos) |
|---|
| 718 | |
|---|
| 719 | |
|---|
| 720 | class Manifest(object): |
|---|
| 721 | |
|---|
| 722 | def __init__(self, content): |
|---|
| 723 | self.tree = lxml.etree.parse(StringIO(content)) |
|---|
| 724 | self.root = self.tree.getroot() |
|---|
| 725 | self.namespaces = self.root.nsmap |
|---|
| 726 | |
|---|
| 727 | def __str__(self): |
|---|
| 728 | return lxml.etree.tostring(self.tree, encoding='UTF-8', |
|---|
| 729 | xml_declaration=True) |
|---|
| 730 | |
|---|
| 731 | def add_file_entry(self, path, mimetype=None): |
|---|
| 732 | manifest_namespace = self.namespaces['manifest'] |
|---|
| 733 | attribs = {'media-type': mimetype or '', |
|---|
| 734 | 'full-path': path} |
|---|
| 735 | entry_node = EtreeElement('{%s}%s' % (manifest_namespace, |
|---|
| 736 | 'file-entry'), |
|---|
| 737 | attrib=attribs, |
|---|
| 738 | nsmap={'manifest': manifest_namespace}) |
|---|
| 739 | self.root.append(entry_node) |
|---|
| 740 | |
|---|
| 741 | |
|---|
| 742 | class OOSerializer: |
|---|
| 743 | |
|---|
| 744 | def __init__(self, oo_path): |
|---|
| 745 | self.inzip = zipfile.ZipFile(oo_path) |
|---|
| 746 | self.manifest = Manifest(self.inzip.read(MANIFEST)) |
|---|
| 747 | self.new_oo = StringIO() |
|---|
| 748 | self.outzip = zipfile.ZipFile(self.new_oo, 'w') |
|---|
| 749 | self.xml_serializer = genshi.output.XMLSerializer() |
|---|
| 750 | |
|---|
| 751 | def __call__(self, stream): |
|---|
| 752 | files = {} |
|---|
| 753 | for kind, data, pos in stream: |
|---|
| 754 | if kind == genshi.core.PI and data[0] == 'relatorio': |
|---|
| 755 | stream_for = data[1] |
|---|
| 756 | continue |
|---|
| 757 | files.setdefault(stream_for, []).append((kind, data, pos)) |
|---|
| 758 | |
|---|
| 759 | now = time.localtime()[:6] |
|---|
| 760 | for f_info in self.inzip.infolist(): |
|---|
| 761 | if f_info.filename.startswith('ObjectReplacements'): |
|---|
| 762 | continue |
|---|
| 763 | elif f_info.filename in files: |
|---|
| 764 | stream = files[f_info.filename] |
|---|
| 765 | # create a new file descriptor, copying some attributes from |
|---|
| 766 | # the original file |
|---|
| 767 | new_info = zipfile.ZipInfo(f_info.filename, now) |
|---|
| 768 | for attr in ('compress_type', 'flag_bits', 'create_system'): |
|---|
| 769 | setattr(new_info, attr, getattr(f_info, attr)) |
|---|
| 770 | serialized_stream = output_encode(self.xml_serializer(stream)) |
|---|
| 771 | self.outzip.writestr(new_info, serialized_stream) |
|---|
| 772 | elif f_info.filename == MANIFEST: |
|---|
| 773 | self.outzip.writestr(f_info, str(self.manifest)) |
|---|
| 774 | else: |
|---|
| 775 | self.outzip.writestr(f_info, self.inzip.read(f_info.filename)) |
|---|
| 776 | self.inzip.close() |
|---|
| 777 | self.outzip.close() |
|---|
| 778 | |
|---|
| 779 | return self.new_oo |
|---|
| 780 | |
|---|
| 781 | MIMETemplateLoader.add_factory('oo.org', Template) |
|---|