2015-12-21 11:51:22 -06:00
|
|
|
#! /usr/bin/python
|
|
|
|
|
# -*- coding: utf-8 -*-
|
2012-07-19 05:33:21 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
Secretary
|
2014-07-19 00:01:13 -06:00
|
|
|
This project is a document engine which make use of LibreOffice
|
|
|
|
|
documents as templates and use the semantics of jinja2 to control
|
|
|
|
|
variable printing and control flow.
|
2012-07-19 05:33:21 -06:00
|
|
|
|
2014-07-19 00:01:13 -06:00
|
|
|
To render a template:
|
2015-12-21 11:54:06 -06:00
|
|
|
engine = Renderer()
|
|
|
|
|
result = engine.render(template_file, foo=bar, ...)
|
2015-12-21 11:51:22 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
Copyright (c) 2012-2015 By:
|
|
|
|
|
* Christopher Ramirez <chris.ramirezg@gmail.com>
|
|
|
|
|
* Andrés Reyes Monge (github.com/armonge)
|
|
|
|
|
* Anton Kochnev (github.com/ak04nv)
|
|
|
|
|
* DieterBuys (github.com/DieterBuys)
|
|
|
|
|
|
2015-12-21 11:54:06 -06:00
|
|
|
Licensed under the MIT license.
|
2012-07-19 05:33:21 -06:00
|
|
|
"""
|
2013-07-20 22:05:37 -06:00
|
|
|
|
2014-07-19 22:05:46 -06:00
|
|
|
from __future__ import unicode_literals, print_function
|
|
|
|
|
|
2014-07-11 14:14:22 -06:00
|
|
|
import io
|
2013-07-20 22:05:37 -06:00
|
|
|
import re
|
|
|
|
|
import sys
|
2014-07-11 14:14:22 -06:00
|
|
|
import logging
|
2013-07-20 22:05:37 -06:00
|
|
|
import zipfile
|
2014-09-07 15:24:48 -06:00
|
|
|
from os import path
|
|
|
|
|
from mimetypes import guess_type, guess_extension
|
2014-09-06 19:12:22 -06:00
|
|
|
from uuid import uuid4
|
2013-07-21 12:45:11 -06:00
|
|
|
from xml.dom.minidom import parseString
|
2015-05-11 13:53:06 -06:00
|
|
|
from xml.parsers.expat import ExpatError, ErrorString
|
2013-07-20 22:05:37 -06:00
|
|
|
from jinja2 import Environment, Undefined
|
2014-07-19 22:04:23 -06:00
|
|
|
|
2015-05-08 12:25:26 -06:00
|
|
|
try:
|
|
|
|
|
if sys.version_info.major == 3:
|
|
|
|
|
xrange = range
|
|
|
|
|
basestring = (str, bytes)
|
|
|
|
|
except AttributeError:
|
|
|
|
|
# On Python 2.6 sys.version_info is a tuple
|
|
|
|
|
if not isinstance(sys.version_info, tuple):
|
|
|
|
|
raise
|
2014-07-19 22:04:23 -06:00
|
|
|
|
2013-07-20 22:05:37 -06:00
|
|
|
|
2014-03-05 23:42:52 -06:00
|
|
|
FLOW_REFERENCES = {
|
|
|
|
|
'text:p' : 'text:p',
|
|
|
|
|
'paragraph' : 'text:p',
|
|
|
|
|
'before::paragraph' : 'text:p',
|
|
|
|
|
'after::paragraph' : 'text:p',
|
|
|
|
|
|
|
|
|
|
'table:table-row' : 'table:table-row',
|
|
|
|
|
'table-row' : 'table:table-row',
|
2014-06-15 20:36:25 -06:00
|
|
|
'row' : 'table:table-row',
|
2014-03-05 23:42:52 -06:00
|
|
|
'before::table-row' : 'table:table-row',
|
|
|
|
|
'after::table-row' : 'table:table-row',
|
2014-06-15 20:36:25 -06:00
|
|
|
'before::row' : 'table:table-row',
|
|
|
|
|
'after::row' : 'table:table-row',
|
2014-03-05 23:42:52 -06:00
|
|
|
|
|
|
|
|
'table:table-cell' : 'table:table-cell',
|
|
|
|
|
'table-cell' : 'table:table-cell',
|
2014-06-15 20:36:25 -06:00
|
|
|
'cell' : 'table:table-cell',
|
2014-03-05 23:42:52 -06:00
|
|
|
'before::table-cell' : 'table:table-cell',
|
|
|
|
|
'after::table-cell' : 'table:table-cell',
|
2014-06-15 20:36:25 -06:00
|
|
|
'before::cell' : 'table:table-cell',
|
|
|
|
|
'after::cell' : 'table:table-cell',
|
2014-03-05 23:42:52 -06:00
|
|
|
}
|
|
|
|
|
|
2013-09-03 17:25:01 -06:00
|
|
|
# ---- Exceptions
|
|
|
|
|
class SecretaryError(Exception):
|
|
|
|
|
pass
|
|
|
|
|
|
2013-07-20 22:05:37 -06:00
|
|
|
class UndefinedSilently(Undefined):
|
|
|
|
|
# Silently undefined,
|
2014-07-19 18:29:51 -06:00
|
|
|
# see http://stackoverflow.com/questions/6182498
|
2013-07-20 22:05:37 -06:00
|
|
|
def silently_undefined(*args, **kwargs):
|
2013-08-24 09:49:42 -06:00
|
|
|
return ''
|
2013-07-20 22:05:37 -06:00
|
|
|
|
|
|
|
|
return_new = lambda *args, **kwargs: UndefinedSilently()
|
|
|
|
|
|
|
|
|
|
__unicode__ = silently_undefined
|
|
|
|
|
__str__ = silently_undefined
|
|
|
|
|
__call__ = return_new
|
|
|
|
|
__getattr__ = return_new
|
|
|
|
|
|
|
|
|
|
# ************************************************
|
2013-07-31 14:49:54 -06:00
|
|
|
#
|
2013-07-20 22:05:37 -06:00
|
|
|
# SECRETARY FILTERS
|
2013-07-31 14:49:54 -06:00
|
|
|
#
|
2013-07-20 22:05:37 -06:00
|
|
|
# ************************************************
|
|
|
|
|
|
2014-09-07 15:24:48 -06:00
|
|
|
def media_loader(f):
|
|
|
|
|
def wrapper(*args, **kwargs):
|
|
|
|
|
Renderer.__media_loader__ = f
|
|
|
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
2013-07-20 22:05:37 -06:00
|
|
|
def pad_string(value, length=5):
|
|
|
|
|
value = str(value)
|
|
|
|
|
return value.zfill(length)
|
|
|
|
|
|
2014-07-19 00:04:07 -06:00
|
|
|
class Renderer(object):
|
2013-07-20 22:05:37 -06:00
|
|
|
"""
|
2013-07-21 13:13:39 -06:00
|
|
|
Main engine to convert and ODT document into a jinja
|
2013-08-07 09:25:23 -06:00
|
|
|
compatible template.
|
2013-07-31 14:49:54 -06:00
|
|
|
|
2013-07-21 13:13:39 -06:00
|
|
|
Basic use example:
|
2015-12-21 12:03:54 -06:00
|
|
|
engine = Renderer()
|
|
|
|
|
result = engine.render(template, var1=val1, var2=val2, ...)
|
2013-08-07 09:25:23 -06:00
|
|
|
|
|
|
|
|
|
2015-12-21 12:03:54 -06:00
|
|
|
Renderer provides an environment property which should be used
|
|
|
|
|
to add custom filters to the ODF render.
|
|
|
|
|
engine = Renderer()
|
|
|
|
|
engine.environment.filters['custom_filter'] = filterFn
|
|
|
|
|
result = engine.render('template.odt', var1=val1, ...)
|
2013-07-20 22:05:37 -06:00
|
|
|
"""
|
|
|
|
|
|
2014-07-19 18:29:51 -06:00
|
|
|
def __init__(self, environment=None, **kwargs):
|
2013-07-20 22:05:37 -06:00
|
|
|
"""
|
2014-07-19 18:29:51 -06:00
|
|
|
Create a Renderer instance.
|
|
|
|
|
|
|
|
|
|
args:
|
2015-12-21 12:03:54 -06:00
|
|
|
environment: Use this jinja2 environment. If not specified, we
|
2014-07-19 18:29:51 -06:00
|
|
|
create a new environment for this class instance.
|
|
|
|
|
|
2013-07-20 22:05:37 -06:00
|
|
|
"""
|
2014-07-11 14:14:22 -06:00
|
|
|
self.log = logging.getLogger(__name__)
|
2014-07-19 18:29:51 -06:00
|
|
|
self.log.debug('Initing a Renderer instance\nTemplate')
|
2013-09-12 15:50:17 -06:00
|
|
|
|
2014-07-19 18:29:51 -06:00
|
|
|
if environment:
|
|
|
|
|
self.environment = environment
|
|
|
|
|
else:
|
|
|
|
|
self.environment = Environment(undefined=UndefinedSilently,
|
|
|
|
|
autoescape=True)
|
|
|
|
|
# Register filters
|
|
|
|
|
self.environment.filters['pad'] = pad_string
|
|
|
|
|
self.environment.filters['markdown'] = self.markdown_filter
|
2014-09-06 19:19:05 -06:00
|
|
|
self.environment.filters['image'] = self.image_filter
|
2014-07-19 18:29:51 -06:00
|
|
|
|
2014-09-07 15:24:48 -06:00
|
|
|
self.media_path = kwargs.pop('media_path', '')
|
|
|
|
|
self.media_callback = self.fs_loader
|
|
|
|
|
|
2015-12-17 16:15:12 -06:00
|
|
|
self._compile_tags_expressions()
|
|
|
|
|
|
2014-09-07 15:24:48 -06:00
|
|
|
|
|
|
|
|
def media_loader(self, callback):
|
|
|
|
|
"""This sets the the media loader. A user defined function which
|
|
|
|
|
loads media. The function should take a template value, optionals
|
|
|
|
|
args and kwargs. Is media exists should return a tuple whose first
|
|
|
|
|
element if a file object type representing the media and its second
|
|
|
|
|
elements is the media mimetype.
|
|
|
|
|
|
|
|
|
|
See Renderer.fs_loader funcion for an example"""
|
|
|
|
|
self.media_callback = callback
|
|
|
|
|
return callback
|
|
|
|
|
|
2014-07-19 18:29:51 -06:00
|
|
|
def _unpack_template(self, template):
|
|
|
|
|
# And Open/libreOffice is just a ZIP file. Here we unarchive the file
|
|
|
|
|
# and return a dict with every file in the archive
|
|
|
|
|
self.log.debug('Unpacking template file')
|
2015-12-04 09:03:25 -06:00
|
|
|
|
2014-07-19 18:29:51 -06:00
|
|
|
archive_files = {}
|
2014-07-19 19:52:40 -06:00
|
|
|
archive = zipfile.ZipFile(template, 'r')
|
|
|
|
|
for zfile in archive.filelist:
|
|
|
|
|
archive_files[zfile.filename] = archive.read(zfile.filename)
|
2013-07-31 14:49:54 -06:00
|
|
|
|
2014-07-19 18:29:51 -06:00
|
|
|
return archive_files
|
2013-07-20 22:05:37 -06:00
|
|
|
|
2014-07-19 18:29:51 -06:00
|
|
|
self.log.debug('Unpack completed')
|
2014-07-11 14:14:22 -06:00
|
|
|
|
2014-07-19 18:29:51 -06:00
|
|
|
def _pack_document(self, files):
|
|
|
|
|
# Store to a zip files in files
|
|
|
|
|
self.log.debug('packing document')
|
|
|
|
|
zip_file = io.BytesIO()
|
2013-07-20 22:05:37 -06:00
|
|
|
|
2017-07-13 09:47:25 +03:00
|
|
|
mimetype = files['mimetype']
|
|
|
|
|
del files['mimetype']
|
|
|
|
|
|
2014-07-19 19:52:40 -06:00
|
|
|
zipdoc = zipfile.ZipFile(zip_file, 'a')
|
2017-07-13 09:47:25 +03:00
|
|
|
|
|
|
|
|
zipdoc.writestr('mimetype', mimetype, zipfile.ZIP_STORED)
|
|
|
|
|
|
2014-07-19 19:54:34 -06:00
|
|
|
for fname, content in files.items():
|
|
|
|
|
if sys.version_info >= (2, 7):
|
|
|
|
|
zipdoc.writestr(fname, content, zipfile.ZIP_DEFLATED)
|
|
|
|
|
else:
|
|
|
|
|
zipdoc.writestr(fname, content)
|
2014-07-19 18:29:51 -06:00
|
|
|
|
|
|
|
|
self.log.debug('Document packing completed')
|
|
|
|
|
|
|
|
|
|
return zip_file
|
|
|
|
|
|
2015-12-16 22:19:19 -06:00
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _inc_node_tags_count(node, is_block=False):
|
|
|
|
|
""" Increase field count of node and its parents """
|
|
|
|
|
|
|
|
|
|
if node is None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
for attr in ['field_count', 'block_count', 'var_count']:
|
|
|
|
|
if not hasattr(node, attr):
|
|
|
|
|
setattr(node, attr, 0)
|
|
|
|
|
|
|
|
|
|
node.field_count += 1
|
|
|
|
|
if is_block:
|
|
|
|
|
node.block_count += 1
|
|
|
|
|
else:
|
|
|
|
|
node.var_count += 1
|
|
|
|
|
|
|
|
|
|
Renderer._inc_node_tags_count(node.parentNode, is_block)
|
|
|
|
|
|
|
|
|
|
|
2015-12-17 16:15:12 -06:00
|
|
|
def _compile_tags_expressions(self):
|
|
|
|
|
self.tag_pattern = re.compile(r'(?is)^({0}|{1}).*({2}|{3})$'.format(
|
|
|
|
|
self.environment.variable_start_string,
|
|
|
|
|
self.environment.block_start_string,
|
|
|
|
|
self.environment.variable_end_string,
|
|
|
|
|
self.environment.block_end_string
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
self.block_pattern = re.compile(r'(?is)^{0}.*{1}$'.format(
|
|
|
|
|
self.environment.block_start_string,
|
|
|
|
|
self.environment.block_end_string
|
|
|
|
|
))
|
|
|
|
|
|
2015-12-21 11:29:41 -06:00
|
|
|
self._compile_escape_expressions()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _compile_escape_expressions(self):
|
|
|
|
|
# Compiles escape expressions
|
|
|
|
|
self.escape_map = dict()
|
|
|
|
|
unescape_rules = {
|
2016-04-12 12:06:30 -06:00
|
|
|
r'>': r'>',
|
|
|
|
|
r'<': r'<',
|
|
|
|
|
r'&': r'&',
|
|
|
|
|
r'"': r'"',
|
|
|
|
|
r''': r'\'',
|
2015-12-21 11:29:41 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for key, value in unescape_rules.items():
|
2016-04-15 12:07:01 -06:00
|
|
|
exp = r'(?is)(({0}|{1})[^{3}{4}]*?)({2})([^{0}{1}]*?({3}|{4}))'
|
2015-12-21 11:29:41 -06:00
|
|
|
key = re.compile(exp.format(
|
|
|
|
|
self.environment.variable_start_string,
|
|
|
|
|
self.environment.block_start_string,
|
|
|
|
|
key,
|
|
|
|
|
self.environment.variable_end_string,
|
|
|
|
|
self.environment.block_end_string
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
self.escape_map[key] = r'\1{0}\4'.format(value)
|
|
|
|
|
|
2015-12-17 16:15:12 -06:00
|
|
|
def _is_jinja_tag(self, tag):
|
2015-12-16 22:19:19 -06:00
|
|
|
"""
|
|
|
|
|
Returns True is tag (str) is a valid jinja instruction tag.
|
|
|
|
|
"""
|
|
|
|
|
|
2015-12-27 08:59:47 -06:00
|
|
|
return len(self.tag_pattern.findall(tag)) > 0
|
2015-12-16 22:19:19 -06:00
|
|
|
|
2015-12-17 16:15:12 -06:00
|
|
|
|
|
|
|
|
def _is_block_tag(self, tag):
|
2015-12-16 22:19:19 -06:00
|
|
|
"""
|
|
|
|
|
Returns True is tag (str) is a jinja flow control tag.
|
|
|
|
|
"""
|
2015-12-27 08:59:47 -06:00
|
|
|
return len(self.block_pattern.findall(tag)) > 0
|
2015-12-16 22:19:19 -06:00
|
|
|
|
|
|
|
|
|
2015-12-17 16:15:12 -06:00
|
|
|
def _tags_in_document(self, document):
|
2015-12-16 22:19:19 -06:00
|
|
|
"""
|
|
|
|
|
Yields a list of available jinja instructions tags in document.
|
|
|
|
|
"""
|
|
|
|
|
tags = document.getElementsByTagName('text:text-input')
|
|
|
|
|
|
|
|
|
|
for tag in tags:
|
|
|
|
|
if not tag.hasChildNodes():
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
content = tag.childNodes[0].data.strip()
|
2015-12-17 16:15:12 -06:00
|
|
|
if not self._is_jinja_tag(content):
|
2015-12-16 22:19:19 -06:00
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
yield tag
|
|
|
|
|
|
|
|
|
|
|
2015-12-17 16:15:12 -06:00
|
|
|
def _census_tags(self, document):
|
2015-12-16 22:19:19 -06:00
|
|
|
"""
|
|
|
|
|
Make a census of all available jinja tags in document. We count all
|
|
|
|
|
the children tags nodes within their parents. This process is necesary
|
|
|
|
|
to automaticaly avoid generating invalid documents when mixing block
|
|
|
|
|
tags in differents parts of a document.
|
|
|
|
|
"""
|
2015-12-17 16:15:12 -06:00
|
|
|
for tag in self._tags_in_document(document):
|
2015-12-16 22:19:19 -06:00
|
|
|
content = tag.childNodes[0].data.strip()
|
2015-12-27 08:57:54 -06:00
|
|
|
block_tag = self._is_block_tag(content)
|
2015-12-16 22:19:19 -06:00
|
|
|
|
2015-12-17 16:15:12 -06:00
|
|
|
self._inc_node_tags_count(tag.parentNode, block_tag)
|
2015-12-16 22:19:19 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _prepare_document_tags(self, document):
|
2014-09-06 19:02:50 -06:00
|
|
|
""" Here we search for every field node present in xml_document.
|
|
|
|
|
For each field we found we do:
|
|
|
|
|
* if field is a print field ({{ field }}), we replace it with a
|
|
|
|
|
<text:span> node.
|
2015-12-04 09:03:25 -06:00
|
|
|
|
2014-09-06 19:02:50 -06:00
|
|
|
* if field is a control flow ({% %}), then we find immediate node of
|
|
|
|
|
type indicated in field's `text:description` attribute and replace
|
|
|
|
|
the whole node and its childrens with field's content.
|
2015-12-04 09:03:25 -06:00
|
|
|
|
2014-09-06 19:02:50 -06:00
|
|
|
If `text:description` attribute starts with `before::` or `after::`,
|
|
|
|
|
then we move field content before or after the node in description.
|
2015-12-04 09:03:25 -06:00
|
|
|
|
2014-09-06 19:02:50 -06:00
|
|
|
If no `text:description` is available, find the immediate common
|
2015-12-04 09:03:25 -06:00
|
|
|
parent of this and any other field and replace its child and
|
2014-09-06 19:02:50 -06:00
|
|
|
original parent of field with the field content.
|
2015-12-04 09:03:25 -06:00
|
|
|
|
2014-09-06 19:02:50 -06:00
|
|
|
e.g.: original
|
|
|
|
|
<table>
|
|
|
|
|
<table:row>
|
|
|
|
|
<field>{% for bar in bars %}</field>
|
|
|
|
|
</table:row>
|
|
|
|
|
<paragraph>
|
|
|
|
|
<field>{{ bar }}</field>
|
|
|
|
|
</paragraph>
|
|
|
|
|
<table:row>
|
|
|
|
|
<field>{% endfor %}</field>
|
|
|
|
|
</table:row>
|
|
|
|
|
</table>
|
2015-12-04 09:03:25 -06:00
|
|
|
|
2014-09-06 19:02:50 -06:00
|
|
|
After processing:
|
|
|
|
|
<table>
|
|
|
|
|
{% for bar in bars %}
|
|
|
|
|
<paragraph>
|
|
|
|
|
<text:span>{{ bar }}</text:span>
|
|
|
|
|
</paragraph>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</table>
|
|
|
|
|
"""
|
2014-07-19 18:29:51 -06:00
|
|
|
|
2015-12-16 22:19:19 -06:00
|
|
|
# -------------------------------------------------------------------- #
|
|
|
|
|
# We have to replace a node, let's call it "placeholder", with the
|
|
|
|
|
# content of our jinja tag. The placeholder can be a node with all its
|
|
|
|
|
# children. Node's "text:description" attribute indicates how far we
|
|
|
|
|
# can scale up in the tree hierarchy to get our placeholder node. When
|
|
|
|
|
# said attribute is not present, then we scale up until we find a
|
|
|
|
|
# common parent for this tag and any other tag.
|
|
|
|
|
# -------------------------------------------------------------------- #
|
|
|
|
|
self.log.debug('Preparing document tags')
|
|
|
|
|
self._census_tags(document)
|
|
|
|
|
|
|
|
|
|
for tag in self._tags_in_document(document):
|
|
|
|
|
placeholder = tag
|
|
|
|
|
content = tag.childNodes[0].data.strip()
|
|
|
|
|
is_block = self._is_block_tag(content)
|
|
|
|
|
scale_to = tag.getAttribute('text:description').strip().lower()
|
|
|
|
|
|
|
|
|
|
if content.lower().find('|markdown') > 0:
|
|
|
|
|
# Take whole paragraph when handling a markdown field
|
|
|
|
|
scale_to = 'text:p'
|
|
|
|
|
|
|
|
|
|
if scale_to:
|
|
|
|
|
if FLOW_REFERENCES.get(scale_to, False):
|
|
|
|
|
placeholder = self._parent_of_type(
|
|
|
|
|
tag, FLOW_REFERENCES[scale_to]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
new_node = self.create_text_node(document, content)
|
|
|
|
|
|
|
|
|
|
elif is_block:
|
|
|
|
|
# expand up the placeholder until a shared parent is found
|
|
|
|
|
while not placeholder.parentNode.field_count > 1:
|
|
|
|
|
placeholder = placeholder.parentNode
|
|
|
|
|
|
|
|
|
|
if placeholder:
|
|
|
|
|
new_node = self.create_text_node(document, content)
|
2014-07-19 18:29:51 -06:00
|
|
|
|
|
|
|
|
else:
|
2015-12-16 22:19:19 -06:00
|
|
|
new_node = self.create_text_span_node(document, content)
|
2014-07-19 18:29:51 -06:00
|
|
|
|
2015-12-16 22:19:19 -06:00
|
|
|
placeholder_parent = placeholder.parentNode
|
|
|
|
|
if not scale_to.startswith('after::'):
|
|
|
|
|
placeholder_parent.insertBefore(new_node, placeholder)
|
2014-07-19 18:29:51 -06:00
|
|
|
else:
|
2015-12-16 22:19:19 -06:00
|
|
|
if placeholder.isSameNode(placeholder_parent.lastChild):
|
|
|
|
|
placeholder_parent.appendChild(new_node)
|
2014-07-19 18:29:51 -06:00
|
|
|
else:
|
2015-12-16 22:19:19 -06:00
|
|
|
placeholder_parent.insertBefore(
|
|
|
|
|
new_node, placeholder.nextSibling
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if scale_to.startswith(('after::', 'before::')):
|
|
|
|
|
# Don't remove whole field tag, only "text:text-input" container
|
|
|
|
|
placeholder = self._parent_of_type(tag, 'text:p')
|
|
|
|
|
placeholder_parent = placeholder.parentNode
|
2014-07-19 18:29:51 -06:00
|
|
|
|
|
|
|
|
|
2015-12-16 22:19:19 -06:00
|
|
|
# Finally, remove the placeholder
|
|
|
|
|
placeholder_parent.removeChild(placeholder)
|
2014-07-19 18:29:51 -06:00
|
|
|
|
|
|
|
|
|
2015-12-21 11:29:41 -06:00
|
|
|
def _unescape_entities(self, xml_text):
|
2015-04-27 17:45:18 -07:00
|
|
|
"""
|
2017-02-13 11:22:22 -06:00
|
|
|
Unescape links and '&', '<', '"' and '>' within jinja
|
|
|
|
|
instructions. The regexs rules used here are compiled in
|
|
|
|
|
_compile_escape_expressions.
|
2015-04-27 17:45:18 -07:00
|
|
|
"""
|
2015-12-21 11:29:41 -06:00
|
|
|
for regexp, replacement in self.escape_map.items():
|
|
|
|
|
while True:
|
|
|
|
|
xml_text, substitutions = regexp.subn(replacement, xml_text)
|
|
|
|
|
if not substitutions:
|
|
|
|
|
break
|
2014-07-19 18:29:51 -06:00
|
|
|
|
2017-02-13 11:22:22 -06:00
|
|
|
return self._unescape_links(xml_text)
|
|
|
|
|
|
|
|
|
|
def _unescape_links(self, xml_text):
|
|
|
|
|
"""Fix Libreoffice auto escaping of xlink:href attribute values.
|
|
|
|
|
This unescaping is only done on 'secretary' scheme URLs."""
|
|
|
|
|
import urllib
|
|
|
|
|
robj = re.compile(r'(?is)(xlink:href=\")secretary:(.*?)(\")')
|
|
|
|
|
|
|
|
|
|
def replacement(match):
|
|
|
|
|
return ''.join([match.group(1), urllib.unquote(match.group(2)),
|
|
|
|
|
match.group(3)])
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
xml_text, rep = robj.subn(replacement, xml_text)
|
|
|
|
|
if not rep:
|
|
|
|
|
break
|
|
|
|
|
|
2014-07-19 18:29:51 -06:00
|
|
|
return xml_text
|
|
|
|
|
|
2015-04-27 16:29:26 -07:00
|
|
|
@staticmethod
|
|
|
|
|
def _encode_escape_chars(xml_text):
|
2015-12-16 22:19:19 -06:00
|
|
|
"""
|
|
|
|
|
Replace line feed and/or tabs within text:span entities.
|
|
|
|
|
"""
|
2017-06-12 12:54:40 -06:00
|
|
|
find_pattern = r'(?is)<text:([\S]+?).*?>([^>]*?([\n\t])[^<]*?)</text:\1>'
|
2014-08-17 19:32:33 -06:00
|
|
|
for m in re.findall(find_pattern, xml_text):
|
|
|
|
|
replacement = m[1].replace('\n', '<text:line-break/>')
|
|
|
|
|
replacement = replacement.replace('\t', '<text:tab/>')
|
|
|
|
|
xml_text = xml_text.replace(m[1], replacement)
|
2014-07-19 18:29:51 -06:00
|
|
|
|
|
|
|
|
return xml_text
|
2014-09-07 10:51:49 -06:00
|
|
|
|
|
|
|
|
|
2014-09-07 15:24:48 -06:00
|
|
|
def add_media_to_archive(self, media, mime, name=''):
|
2015-12-16 22:19:19 -06:00
|
|
|
"""
|
|
|
|
|
Adds to "Pictures" archive folder the file in `media` and register
|
|
|
|
|
it into manifest file.
|
|
|
|
|
"""
|
2014-09-07 15:24:48 -06:00
|
|
|
extension = None
|
|
|
|
|
if hasattr(media, 'name') and not name:
|
|
|
|
|
extension = path.splitext(media.name)
|
|
|
|
|
name = extension[0]
|
|
|
|
|
extension = extension[1]
|
|
|
|
|
|
|
|
|
|
if not extension:
|
|
|
|
|
extension = guess_extension(mime)
|
|
|
|
|
|
|
|
|
|
media_path = 'Pictures/%s%s' % (name, extension)
|
2014-10-10 14:16:36 -06:00
|
|
|
media.seek(0)
|
2014-09-07 15:24:48 -06:00
|
|
|
self.files[media_path] = media.read(-1)
|
|
|
|
|
if hasattr(media, 'close'):
|
|
|
|
|
media.close()
|
|
|
|
|
|
|
|
|
|
files_node = self.manifest.getElementsByTagName('manifest:manifest')[0]
|
|
|
|
|
node = self.create_node(self.manifest, 'manifest:file-entry', files_node)
|
|
|
|
|
node.setAttribute('manifest:full-path', media_path)
|
|
|
|
|
node.setAttribute('manifest:media-type', mime)
|
|
|
|
|
|
|
|
|
|
return media_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def fs_loader(self, media, *args, **kwargs):
|
|
|
|
|
"""Loads a file from the file system.
|
2014-10-10 14:16:36 -06:00
|
|
|
:param media: A file object or a relative or absolute path of a file.
|
2014-09-07 15:24:48 -06:00
|
|
|
:type media: unicode
|
|
|
|
|
"""
|
2014-10-10 14:16:36 -06:00
|
|
|
if hasattr(media, 'seek') and hasattr(media, 'read'):
|
|
|
|
|
return (media, 'image/jpeg')
|
|
|
|
|
elif path.isfile(media):
|
2014-09-07 15:24:48 -06:00
|
|
|
filename = media
|
|
|
|
|
else:
|
|
|
|
|
if not self.media_path:
|
|
|
|
|
self.log.debug('media_path property not specified to load images from.')
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
filename = path.join(self.media_path, media)
|
|
|
|
|
if not path.isfile(filename):
|
|
|
|
|
self.log.debug('Media file "%s" does not exists.' % filename)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
mime = guess_type(filename)
|
|
|
|
|
return (open(filename, 'rb'), mime[0] if mime else None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def replace_images(self, xml_document):
|
2014-09-07 10:51:49 -06:00
|
|
|
"""Perform images replacements"""
|
2014-09-07 15:24:48 -06:00
|
|
|
self.log.debug('Inserting images')
|
|
|
|
|
frames = xml_document.getElementsByTagName('draw:frame')
|
|
|
|
|
|
|
|
|
|
for frame in frames:
|
|
|
|
|
if not frame.hasChildNodes():
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
key = frame.getAttribute('draw:name')
|
|
|
|
|
if key not in self.template_images:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Get frame attributes
|
|
|
|
|
frame_attrs = dict()
|
|
|
|
|
for i in xrange(frame.attributes.length):
|
|
|
|
|
attr = frame.attributes.item(i)
|
2015-12-04 09:03:25 -06:00
|
|
|
frame_attrs[attr.name] = attr.value
|
2014-09-07 15:24:48 -06:00
|
|
|
|
|
|
|
|
# Get child draw:image node and its attrs
|
|
|
|
|
image_node = frame.childNodes[0]
|
|
|
|
|
image_attrs = dict()
|
|
|
|
|
for i in xrange(image_node.attributes.length):
|
|
|
|
|
attr = image_node.attributes.item(i)
|
2015-12-04 09:03:25 -06:00
|
|
|
image_attrs[attr.name] = attr.value
|
2014-09-07 15:24:48 -06:00
|
|
|
|
|
|
|
|
# Request to media loader the image to use
|
|
|
|
|
image = self.media_callback(self.template_images[key]['value'],
|
|
|
|
|
*self.template_images[key]['args'],
|
|
|
|
|
frame_attrs=frame_attrs,
|
|
|
|
|
image_attrs=image_attrs,
|
|
|
|
|
**self.template_images[key]['kwargs'])
|
|
|
|
|
|
|
|
|
|
# Update frame and image node attrs (if they where updated in
|
|
|
|
|
# media_callback call)
|
|
|
|
|
for k, v in frame_attrs.items():
|
|
|
|
|
frame.setAttribute(k, v)
|
2015-12-04 09:03:25 -06:00
|
|
|
|
2014-09-07 15:24:48 -06:00
|
|
|
for k, v in image_attrs.items():
|
|
|
|
|
image_node.setAttribute(k, v)
|
|
|
|
|
|
|
|
|
|
# Keep original image reference value
|
2014-10-10 14:16:36 -06:00
|
|
|
if isinstance(self.template_images[key]['value'], basestring):
|
|
|
|
|
frame.setAttribute('draw:name',
|
|
|
|
|
self.template_images[key]['value'])
|
|
|
|
|
|
2014-09-07 15:24:48 -06:00
|
|
|
# Does the madia loader returned something?
|
|
|
|
|
if not image:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
mname = self.add_media_to_archive(media=image[0], mime=image[1],
|
|
|
|
|
name=key)
|
|
|
|
|
if mname:
|
|
|
|
|
image_node.setAttribute('xlink:href', mname)
|
2014-07-19 18:29:51 -06:00
|
|
|
|
|
|
|
|
def _render_xml(self, xml_document, **kwargs):
|
|
|
|
|
# Prepare the xml object to be processed by jinja2
|
|
|
|
|
self.log.debug('Rendering XML object')
|
2015-12-16 22:19:19 -06:00
|
|
|
template_string = ""
|
2014-07-19 18:29:51 -06:00
|
|
|
|
|
|
|
|
try:
|
2014-09-07 10:51:49 -06:00
|
|
|
self.template_images = dict()
|
2015-12-16 22:19:19 -06:00
|
|
|
self._prepare_document_tags(xml_document)
|
2016-04-12 11:59:58 -06:00
|
|
|
xml_source = xml_document.toxml()
|
|
|
|
|
xml_source = xml_source.encode('ascii', 'xmlcharrefreplace')
|
|
|
|
|
jinja_template = self.environment.from_string(
|
2016-04-12 14:47:35 -06:00
|
|
|
self._unescape_entities(xml_source.decode('utf-8'))
|
2016-04-12 11:59:58 -06:00
|
|
|
)
|
2015-04-27 17:45:18 -07:00
|
|
|
|
2014-07-19 18:29:51 -06:00
|
|
|
result = jinja_template.render(**kwargs)
|
2015-04-29 11:54:35 -06:00
|
|
|
result = self._encode_escape_chars(result)
|
2014-07-19 18:29:51 -06:00
|
|
|
|
2016-04-12 12:26:40 -06:00
|
|
|
final_xml = parseString(result.encode('ascii', 'xmlcharrefreplace'))
|
2014-09-07 10:51:49 -06:00
|
|
|
if self.template_images:
|
|
|
|
|
self.replace_images(final_xml)
|
|
|
|
|
|
2014-09-07 15:24:48 -06:00
|
|
|
return final_xml
|
2015-04-29 11:54:35 -06:00
|
|
|
except ExpatError as e:
|
2017-02-13 08:38:25 -06:00
|
|
|
if not 'result' in locals():
|
|
|
|
|
result = xml_source
|
2016-04-15 12:07:01 -06:00
|
|
|
near = result.split('\n')[e.lineno -1][e.offset-200:e.offset+200]
|
2017-07-13 09:47:25 +03:00
|
|
|
|
2015-05-11 13:53:06 -06:00
|
|
|
raise ExpatError('ExpatError "%s" at line %d, column %d\nNear of: "[...]%s[...]"' % \
|
|
|
|
|
(ErrorString(e.code), e.lineno, e.offset, near))
|
2014-07-19 18:29:51 -06:00
|
|
|
except:
|
2014-08-17 19:32:33 -06:00
|
|
|
self.log.error('Error rendering template:\n%s',
|
|
|
|
|
xml_document.toprettyxml(), exc_info=True)
|
2015-04-27 17:45:18 -07:00
|
|
|
|
2015-12-04 09:03:25 -06:00
|
|
|
self.log.error('Unescaped template was:\n{0}'.format(template_string))
|
2014-07-19 18:29:51 -06:00
|
|
|
raise
|
|
|
|
|
finally:
|
|
|
|
|
self.log.debug('Rendering xml object finished')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render(self, template, **kwargs):
|
2013-07-20 22:05:37 -06:00
|
|
|
"""
|
2014-07-19 18:29:51 -06:00
|
|
|
Render a template
|
|
|
|
|
|
|
|
|
|
args:
|
|
|
|
|
template: A template file. Could be a string or a file instance
|
|
|
|
|
**kwargs: Template variables. Similar to jinja2
|
2014-06-16 12:10:17 -06:00
|
|
|
|
2014-07-19 18:29:51 -06:00
|
|
|
returns:
|
|
|
|
|
A binary stream which contains the rendered document.
|
|
|
|
|
"""
|
2013-07-20 22:05:37 -06:00
|
|
|
|
2014-07-19 18:29:51 -06:00
|
|
|
self.log.debug('Initing a template rendering')
|
|
|
|
|
self.files = self._unpack_template(template)
|
2014-09-06 19:12:22 -06:00
|
|
|
self.render_vars = {}
|
2013-07-20 22:05:37 -06:00
|
|
|
|
2014-07-19 18:29:51 -06:00
|
|
|
# Keep content and styles object since many functions or
|
|
|
|
|
# filters may work with then
|
2015-12-04 09:03:25 -06:00
|
|
|
self.content = parseString(self.files['content.xml'])
|
2014-09-07 15:24:48 -06:00
|
|
|
self.styles = parseString(self.files['styles.xml'])
|
|
|
|
|
self.manifest = parseString(self.files['META-INF/manifest.xml'])
|
2015-12-04 09:03:25 -06:00
|
|
|
|
2015-02-03 16:18:44 -06:00
|
|
|
# Render content.xml keeping just 'office:body' node.
|
|
|
|
|
rendered_content = self._render_xml(self.content, **kwargs)
|
|
|
|
|
self.content.getElementsByTagName('office:document-content')[0].replaceChild(
|
|
|
|
|
rendered_content.getElementsByTagName('office:body')[0],
|
|
|
|
|
self.content.getElementsByTagName('office:body')[0]
|
|
|
|
|
)
|
2013-07-20 22:05:37 -06:00
|
|
|
|
2014-07-11 14:14:22 -06:00
|
|
|
# Render styles.xml
|
2014-07-19 18:29:51 -06:00
|
|
|
self.styles = self._render_xml(self.styles, **kwargs)
|
|
|
|
|
|
|
|
|
|
self.log.debug('Template rendering finished')
|
|
|
|
|
|
2014-09-07 15:24:48 -06:00
|
|
|
self.files['content.xml'] = self.content.toxml().encode('ascii', 'xmlcharrefreplace')
|
|
|
|
|
self.files['styles.xml'] = self.styles.toxml().encode('ascii', 'xmlcharrefreplace')
|
|
|
|
|
self.files['META-INF/manifest.xml'] = self.manifest.toxml().encode('ascii', 'xmlcharrefreplace')
|
2015-12-04 09:03:25 -06:00
|
|
|
|
2014-07-19 18:29:51 -06:00
|
|
|
document = self._pack_document(self.files)
|
|
|
|
|
return document.getvalue()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parent_of_type(self, node, of_type):
|
|
|
|
|
# Returns the first immediate parent of type `of_type`.
|
|
|
|
|
# Returns None if nothing is found.
|
2013-07-20 22:05:37 -06:00
|
|
|
|
|
|
|
|
if hasattr(node, 'parentNode'):
|
2014-07-19 18:29:51 -06:00
|
|
|
if node.parentNode.nodeName.lower() == of_type:
|
2013-07-20 22:05:37 -06:00
|
|
|
return node.parentNode
|
|
|
|
|
else:
|
2014-07-19 18:29:51 -06:00
|
|
|
return self._parent_of_type(node.parentNode, of_type)
|
2013-07-20 22:05:37 -06:00
|
|
|
else:
|
|
|
|
|
return None
|
|
|
|
|
|
2014-09-07 15:24:48 -06:00
|
|
|
def create_node(self, xml_document, node_type, parent=None):
|
|
|
|
|
"""Creates a node in `xml_document` of type `node_type` and specified,
|
|
|
|
|
as child of `parent`."""
|
|
|
|
|
node = xml_document.createElement(node_type)
|
|
|
|
|
if parent:
|
|
|
|
|
parent.appendChild(node)
|
|
|
|
|
|
|
|
|
|
return node
|
2013-07-20 22:05:37 -06:00
|
|
|
|
|
|
|
|
def create_text_span_node(self, xml_document, content):
|
2013-07-21 12:47:41 -06:00
|
|
|
span = xml_document.createElement('text:span')
|
|
|
|
|
text_node = self.create_text_node(xml_document, content)
|
2013-07-20 22:05:37 -06:00
|
|
|
span.appendChild(text_node)
|
|
|
|
|
|
|
|
|
|
return span
|
|
|
|
|
|
|
|
|
|
def create_text_node(self, xml_document, text):
|
|
|
|
|
"""
|
|
|
|
|
Creates a text node
|
|
|
|
|
"""
|
2013-07-21 12:47:41 -06:00
|
|
|
return xml_document.createTextNode(text)
|
2013-07-20 22:05:37 -06:00
|
|
|
|
2015-12-04 09:03:25 -06:00
|
|
|
|
2013-09-12 15:50:17 -06:00
|
|
|
def get_style_by_name(self, style_name):
|
|
|
|
|
"""
|
|
|
|
|
Search in <office:automatic-styles> for style_name.
|
|
|
|
|
Return None if style_name is not found. Otherwise
|
|
|
|
|
return the style node
|
|
|
|
|
"""
|
2013-09-12 13:54:17 -06:00
|
|
|
|
2014-07-19 18:29:51 -06:00
|
|
|
auto_styles = self.content.getElementsByTagName(
|
|
|
|
|
'office:automatic-styles')[0]
|
2013-10-24 11:09:44 -06:00
|
|
|
|
2013-09-12 15:50:17 -06:00
|
|
|
if not auto_styles.hasChildNodes():
|
|
|
|
|
return None
|
2013-10-24 11:09:44 -06:00
|
|
|
|
2013-09-12 15:50:17 -06:00
|
|
|
for style_node in auto_styles.childNodes:
|
|
|
|
|
if style_node.hasAttribute('style:name') and \
|
|
|
|
|
(style_node.getAttribute('style:name') == style_name):
|
|
|
|
|
return style_node
|
2013-09-12 13:54:17 -06:00
|
|
|
|
2013-09-12 15:50:17 -06:00
|
|
|
return None
|
2013-09-12 13:54:17 -06:00
|
|
|
|
2013-09-12 15:50:17 -06:00
|
|
|
def insert_style_in_content(self, style_name, attributes=None,
|
|
|
|
|
**style_properties):
|
|
|
|
|
"""
|
|
|
|
|
Insert a new style into content.xml's <office:automatic-styles> node.
|
|
|
|
|
Returns a reference to the newly created node
|
|
|
|
|
"""
|
2013-09-12 13:54:17 -06:00
|
|
|
|
2013-09-12 15:50:17 -06:00
|
|
|
auto_styles = self.content.getElementsByTagName('office:automatic-styles')[0]
|
|
|
|
|
style_node = self.content.createElement('style:style')
|
2013-09-12 13:54:17 -06:00
|
|
|
|
2013-09-12 15:50:17 -06:00
|
|
|
style_node.setAttribute('style:name', style_name)
|
|
|
|
|
style_node.setAttribute('style:family', 'text')
|
|
|
|
|
style_node.setAttribute('style:parent-style-name', 'Standard')
|
2013-10-24 11:09:44 -06:00
|
|
|
|
2013-09-12 15:50:17 -06:00
|
|
|
if attributes:
|
2014-07-19 21:43:54 -06:00
|
|
|
for k, v in attributes.items():
|
2013-09-12 15:50:17 -06:00
|
|
|
style_node.setAttribute('style:%s' % k, v)
|
2013-09-12 13:54:17 -06:00
|
|
|
|
2013-09-12 15:50:17 -06:00
|
|
|
if style_properties:
|
|
|
|
|
style_prop = self.content.createElement('style:text-properties')
|
2014-07-19 21:43:54 -06:00
|
|
|
for k, v in style_properties.items():
|
2013-09-12 15:50:17 -06:00
|
|
|
style_prop.setAttribute('%s' % k, v)
|
2013-09-12 13:54:17 -06:00
|
|
|
|
2013-09-12 15:50:17 -06:00
|
|
|
style_node.appendChild(style_prop)
|
2013-09-12 13:54:17 -06:00
|
|
|
|
2013-09-12 15:50:17 -06:00
|
|
|
return auto_styles.appendChild(style_node)
|
2013-09-12 13:54:17 -06:00
|
|
|
|
2013-09-12 15:50:17 -06:00
|
|
|
def markdown_filter(self, markdown_text):
|
|
|
|
|
"""
|
|
|
|
|
Convert a markdown text into a ODT formated text
|
|
|
|
|
"""
|
2014-02-03 16:39:27 -06:00
|
|
|
|
|
|
|
|
if not isinstance(markdown_text, basestring):
|
|
|
|
|
return ''
|
|
|
|
|
|
2013-09-12 15:50:17 -06:00
|
|
|
from xml.dom import Node
|
|
|
|
|
from markdown_map import transform_map
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
from markdown2 import markdown
|
|
|
|
|
except ImportError:
|
|
|
|
|
raise SecretaryError('Could not import markdown2 library. Install it using "pip install markdown2"')
|
|
|
|
|
|
2013-10-24 11:09:44 -06:00
|
|
|
styles_cache = {} # cache styles searching
|
2013-09-12 15:50:17 -06:00
|
|
|
html_text = markdown(markdown_text)
|
2014-06-15 20:28:11 -06:00
|
|
|
xml_object = parseString('<html>%s</html>' % html_text.encode('ascii', 'xmlcharrefreplace'))
|
2013-09-12 15:50:17 -06:00
|
|
|
|
|
|
|
|
# Transform HTML tags as specified in transform_map
|
|
|
|
|
# Some tags may require extra attributes in ODT.
|
|
|
|
|
# Additional attributes are indicated in the 'attributes' property
|
|
|
|
|
|
|
|
|
|
for tag in transform_map:
|
|
|
|
|
html_nodes = xml_object.getElementsByTagName(tag)
|
|
|
|
|
for html_node in html_nodes:
|
|
|
|
|
odt_node = xml_object.createElement(transform_map[tag]['replace_with'])
|
|
|
|
|
|
|
|
|
|
# Transfer child nodes
|
|
|
|
|
if html_node.hasChildNodes():
|
2016-11-04 11:03:50 -06:00
|
|
|
# We can't directly insert text into a text:list-item element.
|
|
|
|
|
# The content of the item most be wrapped inside a container
|
|
|
|
|
# like text:p. When there's not a double linebreak separating
|
|
|
|
|
# list elements, markdown2 creates <li> elements without wraping
|
|
|
|
|
# their contents inside a container. Here we automatically create
|
|
|
|
|
# the container if one was not created by markdown2.
|
2016-11-04 11:12:27 -06:00
|
|
|
if (tag=='li' and html_node.childNodes[0].localName != 'p'):
|
2016-11-04 11:03:50 -06:00
|
|
|
container = xml_object.createElement('text:p')
|
|
|
|
|
odt_node.appendChild(container)
|
|
|
|
|
else:
|
|
|
|
|
container = odt_node
|
|
|
|
|
|
2013-09-12 15:50:17 -06:00
|
|
|
for child_node in html_node.childNodes:
|
2016-11-04 11:03:50 -06:00
|
|
|
container.appendChild(child_node.cloneNode(True))
|
2013-09-12 15:50:17 -06:00
|
|
|
|
2013-09-12 18:24:40 -06:00
|
|
|
# Add style-attributes defined in transform_map
|
2013-09-12 17:28:00 -06:00
|
|
|
if 'style_attributes' in transform_map[tag]:
|
2014-07-19 21:43:54 -06:00
|
|
|
for k, v in transform_map[tag]['style_attributes'].items():
|
2013-09-12 15:50:17 -06:00
|
|
|
odt_node.setAttribute('text:%s' % k, v)
|
|
|
|
|
|
2013-09-12 18:24:40 -06:00
|
|
|
# Add defined attributes
|
|
|
|
|
if 'attributes' in transform_map[tag]:
|
2014-07-19 21:43:54 -06:00
|
|
|
for k, v in transform_map[tag]['attributes'].items():
|
2013-09-12 18:24:40 -06:00
|
|
|
odt_node.setAttribute(k, v)
|
|
|
|
|
|
|
|
|
|
# copy original href attribute in <a> tag
|
|
|
|
|
if tag == 'a':
|
|
|
|
|
if html_node.hasAttribute('href'):
|
|
|
|
|
odt_node.setAttribute('xlink:href',
|
|
|
|
|
html_node.getAttribute('href'))
|
|
|
|
|
|
2013-09-12 15:50:17 -06:00
|
|
|
# Does the node need to create an style?
|
|
|
|
|
if 'style' in transform_map[tag]:
|
|
|
|
|
name = transform_map[tag]['style']['name']
|
|
|
|
|
if not name in styles_cache:
|
|
|
|
|
style_node = self.get_style_by_name(name)
|
|
|
|
|
|
|
|
|
|
if style_node is None:
|
|
|
|
|
# Create and cache the style node
|
|
|
|
|
style_node = self.insert_style_in_content(
|
|
|
|
|
name, transform_map[tag]['style'].get('attributes', None),
|
|
|
|
|
**transform_map[tag]['style']['properties'])
|
|
|
|
|
styles_cache[name] = style_node
|
|
|
|
|
|
|
|
|
|
html_node.parentNode.replaceChild(odt_node, html_node)
|
|
|
|
|
|
2013-10-24 11:09:44 -06:00
|
|
|
def node_to_string(node):
|
|
|
|
|
result = node.toxml()
|
|
|
|
|
|
|
|
|
|
# linebreaks in preformated nodes should be converted to <text:line-break/>
|
|
|
|
|
if (node.__class__.__name__ != 'Text') and \
|
|
|
|
|
(node.getAttribute('text:style-name') == 'Preformatted_20_Text'):
|
|
|
|
|
result = result.replace('\n', '<text:line-break/>')
|
|
|
|
|
|
|
|
|
|
# All double linebreak should be replaced with an empty paragraph
|
|
|
|
|
return result.replace('\n\n', '<text:p text:style-name="Standard"/>')
|
2013-09-12 17:06:44 -06:00
|
|
|
|
2013-09-03 17:25:01 -06:00
|
|
|
|
2013-10-24 11:09:44 -06:00
|
|
|
return ''.join(node_as_str for node_as_str in map(node_to_string,
|
|
|
|
|
xml_object.getElementsByTagName('html')[0].childNodes))
|
2013-08-24 09:49:42 -06:00
|
|
|
|
2014-09-07 11:14:10 -06:00
|
|
|
def image_filter(self, value, *args, **kwargs):
|
2014-09-07 10:51:49 -06:00
|
|
|
"""Store value into template_images and return the key name where this
|
2014-09-06 19:12:22 -06:00
|
|
|
method stored it. The value returned it later used to load the image
|
|
|
|
|
from media loader and finally inserted into the final ODT document."""
|
2014-09-07 10:51:49 -06:00
|
|
|
key = uuid4().hex
|
2014-09-07 11:14:10 -06:00
|
|
|
self.template_images[key] = {
|
|
|
|
|
'value': value,
|
|
|
|
|
'args': args,
|
|
|
|
|
'kwargs': kwargs
|
|
|
|
|
}
|
|
|
|
|
|
2014-09-06 19:12:22 -06:00
|
|
|
return key
|
|
|
|
|
|
|
|
|
|
|
2013-07-20 22:05:37 -06:00
|
|
|
def render_template(template, **kwargs):
|
|
|
|
|
"""
|
|
|
|
|
Render a ODF template file
|
|
|
|
|
"""
|
|
|
|
|
|
2014-07-19 00:04:07 -06:00
|
|
|
engine = Renderer(file)
|
2013-07-20 22:05:37 -06:00
|
|
|
return engine.render(**kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2013-09-12 17:06:44 -06:00
|
|
|
import os
|
2013-07-20 22:05:37 -06:00
|
|
|
from datetime import datetime
|
|
|
|
|
|
2013-09-12 17:06:44 -06:00
|
|
|
def read(fname):
|
|
|
|
|
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
|
|
|
|
|
2013-07-20 22:05:37 -06:00
|
|
|
document = {
|
2013-09-12 17:06:44 -06:00
|
|
|
'datetime': datetime.now(),
|
|
|
|
|
'md_sample': read('README.md')
|
2013-07-20 22:05:37 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
countries = [
|
|
|
|
|
{'country': 'United States', 'capital': 'Washington', 'cities': ['miami', 'new york', 'california', 'texas', 'atlanta']},
|
|
|
|
|
{'country': 'England', 'capital': 'London', 'cities': ['gales']},
|
|
|
|
|
{'country': 'Japan', 'capital': 'Tokio', 'cities': ['hiroshima', 'nagazaki']},
|
2014-07-19 00:04:07 -06:00
|
|
|
{'country': 'Nicaragua', 'capital': 'Managua', 'cities': ['leon', 'granada', 'masaya']},
|
2013-07-20 22:05:37 -06:00
|
|
|
{'country': 'Argentina', 'capital': 'Buenos aires'},
|
|
|
|
|
{'country': 'Chile', 'capital': 'Santiago'},
|
|
|
|
|
{'country': 'Mexico', 'capital': 'MExico City', 'cities': ['puebla', 'cancun']},
|
|
|
|
|
]
|
|
|
|
|
|
2014-07-19 18:29:51 -06:00
|
|
|
render = Renderer()
|
2017-07-13 09:47:25 +03:00
|
|
|
result = render.render('simple_template2.odt', countries=countries, document=document)
|
2013-07-31 14:49:54 -06:00
|
|
|
|
2014-09-07 15:24:48 -06:00
|
|
|
output = open('rendered.odt', 'wb')
|
2013-07-22 13:27:42 -06:00
|
|
|
output.write(result)
|
2013-07-20 22:05:37 -06:00
|
|
|
|
2014-09-07 15:24:48 -06:00
|
|
|
print("Template rendering finished! Check rendered.odt file.")
|