diff --git a/.gitignore b/.gitignore index 4e6917a..e1e7a78 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,11 @@ *.*~ *.egg *.egg-info +*.odt +*.txt +.tox +.cache +.vscode dist build -eggs +eggs \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index f07d49e..c5c2fe9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,10 @@ +install: pip install -U tox language: python -python: - - "2.6" - - "2.7" - - "3.3" -install: "python setup.py develop" -script: "python setup.py test" +python: + - 2.6 + - 2.7 + - 3.3 + - 3.4 + - 3.5 + - 3.6 +script: tox -e py${TRAVIS_PYTHON_VERSION//./} diff --git a/README.md b/README.md index a67c802..7bbcf96 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,13 @@ # SECRETARY + + + + + + + + #### Take the power of Jinja2 templates to OpenOffice and LibreOffice and create reports in your web applications. @@ -93,6 +101,10 @@ Although most of the time the automatic handling of control flow in secretary ma * `after::cell`: Same as `after::row` but for a table cell. > Field content is the control flow tag you insert with the Writer *input field* +### Hyperlink Support +LibreOffice by default escapes every URL in links, pictures or any other element supporting hyperlink functionallity. This can be a problem if you need to generate dynamic links because your template logic is URL encoded and impossible to be handled by the Jinja engine. Secretary solves this problem by reserving the `secretary` URI scheme. If you need to create dynamic links in your documents, prepend every link with the `secretary:` scheme. + +So for example if you have the following dynamic link: `https://mysite/products/{{ product.id }}`, prepend it with the **`secretary:`** scheme, leaving the final link as `secretary:https://mysite/products/{{ product.id }}`. ### Image Support Secretary allows you to use placeholder images in templates that will be replaced when rendering the final document. To create a placeholder image on your template: @@ -147,6 +159,9 @@ Pad zeroes to `value` to the left until output value's length be equal to `lengt Secretary supports most of the jinja2 control structure/flow tags. But please avoid using the following tags since they are not supported: `block`, `extends`, `macro`, `call`, `include` and `import`. ### Version History +* **0.2.15**: Fix bug reported in #39 escaping Line-Feed and Tab chars inside `text:` elements. +* **0.2.14**: Implement dynamic links escaping and fix #33. +* **0.2.13**: Fix reported bug in markdown filter outputing emply lists. * **0.2.11**: Fix bug when unescaping `"`, `'`, `<`, `>` and '&' inside Jinja expressions. * **0.2.10**: --- * **0.2.9**: --- diff --git a/secretary.py b/secretary.py index 0922f52..41e6bdd 100644 --- a/secretary.py +++ b/secretary.py @@ -177,12 +177,19 @@ class Renderer(object): self.log.debug('packing document') zip_file = io.BytesIO() - zipdoc = zipfile.ZipFile(zip_file, 'a') + mimetype = files['mimetype'] + del files['mimetype'] + + zipdoc = zipfile.ZipFile(zip_file, 'a', zipfile.ZIP_DEFLATED) + + # Store mimetype without without compression using a ZipInfo object + # for compatibility with Py2.6 which doesn't have compress_type + # parameter in ZipFile.writestr function + mime_zipinfo = zipfile.ZipInfo('mimetype') + zipdoc.writestr(mime_zipinfo, mimetype) + for fname, content in files.items(): - if sys.version_info >= (2, 7): - zipdoc.writestr(fname, content, zipfile.ZIP_DEFLATED) - else: - zipdoc.writestr(fname, content) + zipdoc.writestr(fname, content) self.log.debug('Document packing completed') @@ -397,8 +404,9 @@ class Renderer(object): def _unescape_entities(self, xml_text): """ - Unescape '&', '<', '"' and '>' within jinja instructions. - The regexs rules used here are compiled in _compile_escape_expressions. + Unescape links and '&', '<', '"' and '>' within jinja + instructions. The regexs rules used here are compiled in + _compile_escape_expressions. """ for regexp, replacement in self.escape_map.items(): while True: @@ -406,6 +414,23 @@ class Renderer(object): if not substitutions: break + 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 + return xml_text @staticmethod @@ -413,7 +438,11 @@ class Renderer(object): """ Replace line feed and/or tabs within text:span entities. """ +<<<<<<< HEAD find_pattern = r'(?is)([^>]*?([\n|\t|\r|\x0b|\x0c])[^<]*?)' +======= + find_pattern = r'(?is)([^>]*?([\n\t])[^<]*?)' +>>>>>>> f74046fb09401facbece39056dd53dafe3f814e9 for m in re.findall(find_pattern, xml_text): replacement = m[1].replace('\n', '') replacement = replacement.replace('\t', '') @@ -554,7 +583,10 @@ class Renderer(object): return final_xml except ExpatError as e: + if not 'result' in locals(): + result = xml_source near = result.split('\n')[e.lineno -1][e.offset-200:e.offset+200] + raise ExpatError('ExpatError "%s" at line %d, column %d\nNear of: "[...]%s[...]"' % \ (ErrorString(e.code), e.lineno, e.offset, near)) except: @@ -722,8 +754,20 @@ class Renderer(object): # Transfer child nodes if html_node.hasChildNodes(): + # 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
  • elements without wraping + # their contents inside a container. Here we automatically create + # the container if one was not created by markdown2. + if (tag=='li' and html_node.childNodes[0].localName != 'p'): + container = xml_object.createElement('text:p') + odt_node.appendChild(container) + else: + container = odt_node + for child_node in html_node.childNodes: - odt_node.appendChild(child_node.cloneNode(True)) + container.appendChild(child_node.cloneNode(True)) # Add style-attributes defined in transform_map if 'style_attributes' in transform_map[tag]: diff --git a/setup.py b/setup.py index f00ba4f..f667bf1 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ class PyTest(TestCommand): setup( name='secretary', - version='0.2.11', + version='0.2.15', url='https://github.com/christopher-ramirez/secretary', license='MIT', author='Christopher Ramírez', diff --git a/test_secretary.py b/test_secretary.py index 9a5d629..9000a0b 100644 --- a/test_secretary.py +++ b/test_secretary.py @@ -18,6 +18,7 @@ def test_pad_string(): assert pad_string('TEST', 4) == 'TEST' assert pad_string(1) == '00001' + class RenderTestCase(TestCase): def setUp(self): root = os.path.dirname(__file__) @@ -63,7 +64,6 @@ class RenderTestCase(TestCase): for test, expect in test_samples.items(): assert self.engine._encode_escape_chars(test) == expect - def _test_is_jinja_tag(self): assert self._is_jinja_tag('{{ foo }}')==True assert self._is_jinja_tag('{ foo }')==False @@ -79,3 +79,23 @@ class RenderTestCase(TestCase): def test_create_text_span_node(self): assert self.engine.create_text_span_node(self.document, 'text').toxml() == 'text' + +class EncodeLFAndFWithinTextNamespace(TestCase): + """Test encoding of line feed and tab chars within text: namespace""" + def test_encode_linefeed_char(self): + xml = 'This\nLF' + espected = 'ThisLF' + assert (Renderer._encode_escape_chars(xml) == espected) + + def test_encode_tab_char(self): + xml = 'This\tTab' + espected = 'ThisTab' + assert (Renderer._encode_escape_chars(xml) == espected) + + def test_escape_elem_with_attributes(self): + """A bug in _encode_escape_chars was preventing it from escaping + LF and tabs inside text elements with tag attributes. See: + https://github.com/christopher-ramirez/secretary/issues/39""" + xml = 'This\nLF' + espected = 'ThisLF' + assert (Renderer._encode_escape_chars(xml) == espected) diff --git a/tox.ini b/tox.ini index 6fb3487..fbb1117 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] -envlist = py26, py27, py3, py33, py34 +envlist = py26, py27, py3, py33, py34, py35, py36 [testenv] deps = pytest -commands= py.test +commands = py.test