#
# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
"""PyAMS_utils.text module
This module provides text manipulation and conversion functions, as well as a set of TALES
extensions (see :py:class:`ITALESExtension <pyams_utils.interfaces.tales.ITALESExtension>`).
"""
import html
import docutils.core
from markdown import markdown
from pyramid.interfaces import IRequest
from zope.interface import Interface
from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
from pyams_utils.adapter import ContextRequestAdapter, ContextRequestViewAdapter, adapter_config
from pyams_utils.interfaces.tales import ITALESExtension
from pyams_utils.interfaces.text import IHTMLRenderer
from pyams_utils.request import check_request
from pyams_utils.vocabulary import vocabulary_config
__docformat__ = 'restructuredtext'
from pyams_utils import _
[docs]def get_text_start(text, length, maxlen=0):
"""Get first words of given text with maximum given length
If *max* is specified, text is shortened only if remaining text is longer this value
:param str text: initial text
:param integer length: maximum length of resulting text
:param integer maxlen: if > 0, *text* is shortened only if remaining text is longer than max
>>> from pyams_utils.text import get_text_start
>>> get_text_start('This is a long string', 10)
'This is a…'
>>> get_text_start('This is a long string', 20)
'This is a long…'
>>> get_text_start('This is a long string', 20, 7)
'This is a long string'
"""
result = text or ''
if length > len(result):
return result
index = length - 1
text_length = len(result)
while (index > 0) and (result[index] != ' '):
index -= 1
if (index > 0) and (text_length > index + maxlen):
return result[:index] + '…'
return text
[docs]@adapter_config(name='truncate', context=(Interface, Interface, Interface),
provides=ITALESExtension)
class TruncateCharsTalesExtension(ContextRequestViewAdapter):
"""extension:truncate(value, length, max) TALES expression
Truncates a sentence if it is longer than the specified 'length' characters.
Truncated strings will end with an ellipsis character (“…”)
See also 'get_text_start'
"""
[docs] @staticmethod
def render(value, length=50, maxlen=0):
"""Render TALES extension;
see :py:class:`ITALESExtension <pyams_utils.interfaces.tales.ITALESExtension>`
"""
if not value:
return ''
return get_text_start(value, length, maxlen=maxlen)
[docs]@adapter_config(name='raw', context=(str, IRequest), provides=IHTMLRenderer)
class BaseHTMLRenderer(ContextRequestAdapter):
"""Raw text HTML renderer
This renderer renders input text 'as is', mainly for use in a <pre> tag.
"""
[docs] def render(self, **kwargs): # pylint: disable=unused-argument
"""Convert raw code as HTML"""
return self.context
[docs]@adapter_config(name='text', context=(str, IRequest), provides=IHTMLRenderer)
class TextRenderer(BaseHTMLRenderer):
"""Basic text HTML renderer
This renderer only replace newlines with HTML breaks.
"""
label = _("Simple text")
[docs] def render(self, **kwargs):
return html.escape(self.context).replace('\n', '<br />\n')
[docs]@adapter_config(name='js', context=(str, IRequest), provides=IHTMLRenderer)
class JsRenderer(BaseHTMLRenderer):
"""Custom Javascript HTML renderer
This renderer replaces single quotes with escaped ones
"""
[docs] def render(self, **kwargs):
return self.context.replace("'", "\\'")
[docs]@adapter_config(name='rest', context=(str, IRequest), provides=IHTMLRenderer)
class ReStructuredTextRenderer(BaseHTMLRenderer):
"""reStructuredText HTML renderer
This renderer is using *docutils* to convert text to HTML output.
"""
label = _("ReStructured text")
[docs] def render(self, **kwargs):
"""Render reStructuredText to HTML"""
overrides = {
'halt_level': 6,
'input_encoding': 'unicode',
'output_encoding': 'unicode',
'initial_header_level': 3,
}
if 'settings' in kwargs:
overrides.update(kwargs['settings'])
parts = docutils.core.publish_parts(self.context,
writer_name='html',
settings_overrides=overrides)
return ''.join((parts['body_pre_docinfo'], parts['docinfo'], parts['body']))
[docs]@adapter_config(name='markdown', context=(str, IRequest), provides=IHTMLRenderer)
class MarkdownTextRenderer(BaseHTMLRenderer):
"""Markdown HTML renderer
This renderer is converting *Markdown* formatted text to HTML.
"""
label = _("Markdown text")
[docs] def render(self, **kwargs):
"""Render Markdown code to HTML"""
return markdown(self.context)
[docs]def text_to_html(text, renderer='text'):
"""Convert text to HTML using the given renderer
Renderer name can be any registered HTML renderer adapter.
You can provide several renderers by giving their names separated by semicolon; renderers
will then act as in a pipe, each renderer transforming output of the previous one.
"""
request = check_request()
registry = request.registry
for renderer_name in renderer.split(';'):
renderer = registry.queryMultiAdapter((text, request), IHTMLRenderer, name=renderer_name)
if renderer is not None:
text = renderer.render() or text
return text
EMPTY_MARKER = object()
[docs]@adapter_config(name='html', context=(Interface, Interface, Interface), provides=ITALESExtension)
class HTMLTalesExtension(ContextRequestViewAdapter):
"""*extension:html* TALES expression
If first *context* argument of the renderer is an object for which an
:py:class:`IHTMLRenderer <pyams_utils.interfaces.text.IHTMLRenderer>`
adapter can be found, this adapter is used to render the context to HTML; if *context* is a
string, it is converted to HTML using the renderer defined as second parameter; otherwise,
context is just converted to string using the :py:func:`str` function.
You can provide several renderers by giving their names separated by semicolon; renderers
will then act as in a pipe, each renderer transforming output of the previous one.
"""
[docs] def render(self, context=EMPTY_MARKER, renderer='text'):
"""Render TALES extension;
see :py:class:`ITALESExtension <pyams_utils.interfaces.tales.ITALESExtension>`
"""
if context is EMPTY_MARKER:
context = self.context
if not context:
return ''
registry = self.request.registry
adapter = registry.queryMultiAdapter((context, self.request, self.view), IHTMLRenderer)
if adapter is None:
adapter = registry.queryMultiAdapter((context, self.request), IHTMLRenderer)
if adapter is not None:
return adapter.render()
if isinstance(context, str):
return text_to_html(context, renderer)
return str(context)
PYAMS_HTML_RENDERERS_VOCABULARY = 'PyAMS HTML renderers'
[docs]@vocabulary_config(name=PYAMS_HTML_RENDERERS_VOCABULARY)
class RenderersVocabulary(SimpleVocabulary):
"""Text renderers vocabulary"""
def __init__(self, context=None): # pylint: disable=unused-argument
request = check_request()
registry = request.registry
translate = request.localizer.translate
terms = []
append = terms.append
for name, adapter in registry.getAdapters(('', request), IHTMLRenderer):
if hasattr(adapter, 'label'):
append(SimpleTerm(name, title=translate(adapter.label)))
super(RenderersVocabulary, self).__init__(terms)
[docs]@adapter_config(name='br', context=(Interface, Interface, Interface), provides=ITALESExtension)
class BrTalesExtension(ContextRequestViewAdapter):
"""extension:br(value, class) TALES expression
This expression can be used to context a given character ('|' by default) into HTML
breaks with given CSS class.
"""
[docs] @staticmethod
def render(value, css_class='', character='|', start_tag=None, end_tag=None):
"""Render TALES extension;
see :py:class:`ITALESExtension <pyams_utils.interfaces.tales.ITALESExtension>`
"""
if not value:
return ''
br_tag = '<br {0} />'.format('class="{0}"'.format(css_class) if css_class else '')
elements = value.split(character)
if start_tag:
elements[0] = '<{0}>{1}</{0}>'.format(start_tag, elements[0])
if end_tag:
elements[-1] = '<{0}>{1}</{0}>'.format(end_tag, elements[-1])
return br_tag.join(elements)