src/pyams_utils/text.py
branchdev-tf
changeset 427 63284c98cdc1
parent 321 247c4f2948ef
child 445 98b00191ce4f
equal deleted inserted replaced
426:2022e4da3ad9 427:63284c98cdc1
     8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
     8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
     9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
     9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    10 # FOR A PARTICULAR PURPOSE.
    10 # FOR A PARTICULAR PURPOSE.
    11 #
    11 #
    12 
    12 
    13 __docformat__ = 'restructuredtext'
    13 """PyAMS_utils.text module
       
    14 
       
    15 This module provides text manipulation and conversion functions, as well as a set of TALES
       
    16 extensions (see :py:class:`ITALESExtension <pyams_utils.interfaces.tales.ITALESExtension>`).
       
    17 """
    14 
    18 
    15 import html
    19 import html
    16 
    20 
    17 import docutils.core
    21 import docutils.core
    18 from markdown import markdown
    22 from markdown import markdown
    19 from pyramid.interfaces import IRequest
    23 from pyramid.interfaces import IRequest
    20 from zope.interface import Interface
    24 from zope.interface import Interface
    21 from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
    25 from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
    22 
    26 
    23 from pyams_utils import _
       
    24 from pyams_utils.adapter import ContextRequestAdapter, ContextRequestViewAdapter, adapter_config
    27 from pyams_utils.adapter import ContextRequestAdapter, ContextRequestViewAdapter, adapter_config
    25 from pyams_utils.interfaces.tales import ITALESExtension
    28 from pyams_utils.interfaces.tales import ITALESExtension
    26 from pyams_utils.interfaces.text import IHTMLRenderer
    29 from pyams_utils.interfaces.text import IHTMLRenderer
    27 from pyams_utils.request import check_request
    30 from pyams_utils.request import check_request
    28 from pyams_utils.vocabulary import vocabulary_config
    31 from pyams_utils.vocabulary import vocabulary_config
    29 
    32 
    30 
    33 
    31 def get_text_start(text, length, max=0):
    34 __docformat__ = 'restructuredtext'
       
    35 
       
    36 
       
    37 from pyams_utils import _
       
    38 
       
    39 
       
    40 def get_text_start(text, length, maxlen=0):
    32     """Get first words of given text with maximum given length
    41     """Get first words of given text with maximum given length
    33 
    42 
    34     If *max* is specified, text is shortened only if remaining text is longer this value
    43     If *max* is specified, text is shortened only if remaining text is longer this value
    35 
    44 
    36     :param str text: initial text
    45     :param str text: initial text
    37     :param integer length: maximum length of resulting text
    46     :param integer length: maximum length of resulting text
    38     :param integer max: if > 0, *text* is shortened only if remaining text is longer than max
    47     :param integer maxlen: if > 0, *text* is shortened only if remaining text is longer than max
    39 
    48 
    40     >>> from pyams_utils.text import get_text_start
    49     >>> from pyams_utils.text import get_text_start
    41     >>> get_text_start('This is a long string', 10)
    50     >>> get_text_start('This is a long string', 10)
    42     'This is a&#133;'
    51     'This is a&#133;'
    43     >>> get_text_start('This is a long string', 20)
    52     >>> get_text_start('This is a long string', 20)
    50         return result
    59         return result
    51     index = length - 1
    60     index = length - 1
    52     text_length = len(result)
    61     text_length = len(result)
    53     while (index > 0) and (result[index] != ' '):
    62     while (index > 0) and (result[index] != ' '):
    54         index -= 1
    63         index -= 1
    55     if (index > 0) and (text_length > index + max):
    64     if (index > 0) and (text_length > index + maxlen):
    56         return result[:index] + '&#133;'
    65         return result[:index] + '&#133;'
    57     return text
    66     return text
    58 
    67 
    59 
    68 
    60 @adapter_config(name='truncate', context=(Interface, Interface, Interface), provides=ITALESExtension)
    69 @adapter_config(name='truncate', context=(Interface, Interface, Interface),
       
    70                 provides=ITALESExtension)
    61 class TruncateCharsTalesExtension(ContextRequestViewAdapter):
    71 class TruncateCharsTalesExtension(ContextRequestViewAdapter):
    62     """extension:truncate(value, length, max) TALES expression
    72     """extension:truncate(value, length, max) TALES expression
    63 
    73 
    64     Truncates a sentence if it is longer than the specified 'length' characters.
    74     Truncates a sentence if it is longer than the specified 'length' characters.
    65     Truncated strings will end with an ellipsis character (“…”)
    75     Truncated strings will end with an ellipsis character (“…”)
    66     See also 'get_text_start'
    76     See also 'get_text_start'
    67     """
    77     """
    68 
    78 
    69     @staticmethod
    79     @staticmethod
    70     def render(value, length=50, max=0):
    80     def render(value, length=50, maxlen=0):
       
    81         """Render TALES extension;
       
    82         see :py:class:`ITALESExtension <pyams_utils.interfaces.tales.ITALESExtension>`
       
    83         """
    71         if not value:
    84         if not value:
    72             return ''
    85             return ''
    73         return get_text_start(value, length, max=max)
    86         return get_text_start(value, length, maxlen=maxlen)
    74 
    87 
    75 
    88 
    76 @adapter_config(name='raw', context=(str, IRequest), provides=IHTMLRenderer)
    89 @adapter_config(name='raw', context=(str, IRequest), provides=IHTMLRenderer)
    77 class BaseHTMLRenderer(ContextRequestAdapter):
    90 class BaseHTMLRenderer(ContextRequestAdapter):
    78     """Raw text HTML renderer
    91     """Raw text HTML renderer
    79 
    92 
    80     This renderer renders input text 'as is', mainly for use in a <pre> tag.
    93     This renderer renders input text 'as is', mainly for use in a <pre> tag.
    81     """
    94     """
    82 
    95 
    83     def render(self, **kwargs):
    96     def render(self, **kwargs):  # pylint: disable=unused-argument
       
    97         """Convert raw code as HTML"""
    84         return self.context
    98         return self.context
    85 
    99 
    86 
   100 
    87 @adapter_config(name='text', context=(str, IRequest), provides=IHTMLRenderer)
   101 @adapter_config(name='text', context=(str, IRequest), provides=IHTMLRenderer)
    88 class TextRenderer(BaseHTMLRenderer):
   102 class TextRenderer(BaseHTMLRenderer):
   150 def text_to_html(text, renderer='text'):
   164 def text_to_html(text, renderer='text'):
   151     """Convert text to HTML using the given renderer
   165     """Convert text to HTML using the given renderer
   152 
   166 
   153     Renderer name can be any registered HTML renderer adapter.
   167     Renderer name can be any registered HTML renderer adapter.
   154 
   168 
   155     You can provide several renderers by giving their names separated by semicolon; renderers will then
   169     You can provide several renderers by giving their names separated by semicolon; renderers
   156     act as in a pipe, each renderer transforming output of the previous one.
   170     will then act as in a pipe, each renderer transforming output of the previous one.
   157     """
   171     """
   158     request = check_request()
   172     request = check_request()
   159     registry = request.registry
   173     registry = request.registry
   160     for renderer_name in renderer.split(';'):
   174     for renderer_name in renderer.split(';'):
   161         renderer = registry.queryMultiAdapter((text, request), IHTMLRenderer, name=renderer_name)
   175         renderer = registry.queryMultiAdapter((text, request), IHTMLRenderer, name=renderer_name)
   162         if renderer is not None:
   176         if renderer is not None:
   163             text = renderer.render() or text
   177             text = renderer.render() or text
   164     return text
   178     return text
   165 
   179 
   166 
   180 
   167 empty_marker = object()
   181 EMPTY_MARKER = object()
   168 
   182 
   169 
   183 
   170 @adapter_config(name='html', context=(Interface, Interface, Interface), provides=ITALESExtension)
   184 @adapter_config(name='html', context=(Interface, Interface, Interface), provides=ITALESExtension)
   171 class HTMLTalesExtension(ContextRequestViewAdapter):
   185 class HTMLTalesExtension(ContextRequestViewAdapter):
   172     """*extension:html* TALES expression
   186     """*extension:html* TALES expression
   173 
   187 
   174     If first *context* argument of the renderer is an object for which an :py:class:`IHTMLRenderer`
   188     If first *context* argument of the renderer is an object for which an
   175     adapter can be found, this adapter is used to render the context to HTML; if *context* is a string,
   189     :py:class:`IHTMLRenderer <pyams_utils.interfaces.text.IHTMLRenderer>`
   176     it is converted to HTML using the renderer defined as second parameter; otherwise, context is just
   190     adapter can be found, this adapter is used to render the context to HTML; if *context* is a
   177     converted to string using the :py:func:`str` function.
   191     string, it is converted to HTML using the renderer defined as second parameter; otherwise,
   178 
   192     context is just converted to string using the :py:func:`str` function.
   179     You can provide several renderers by giving their names separated by semicolon; renderers will then
   193 
   180     act as in a pipe, each renderer transforming output of the previous one.
   194     You can provide several renderers by giving their names separated by semicolon; renderers
   181     """
   195     will then act as in a pipe, each renderer transforming output of the previous one.
   182 
   196     """
   183     def render(self, context=empty_marker, renderer='text'):
   197 
   184         if context is empty_marker:
   198     def render(self, context=EMPTY_MARKER, renderer='text'):
       
   199         """Render TALES extension;
       
   200         see :py:class:`ITALESExtension <pyams_utils.interfaces.tales.ITALESExtension>`
       
   201         """
       
   202         if context is EMPTY_MARKER:
   185             context = self.context
   203             context = self.context
   186         if not context:
   204         if not context:
   187             return ''
   205             return ''
   188         registry = self.request.registry
   206         registry = self.request.registry
   189         adapter = registry.queryMultiAdapter((context, self.request, self.view), IHTMLRenderer)
   207         adapter = registry.queryMultiAdapter((context, self.request, self.view), IHTMLRenderer)
   190         if adapter is None:
   208         if adapter is None:
   191             adapter = registry.queryMultiAdapter((context, self.request), IHTMLRenderer)
   209             adapter = registry.queryMultiAdapter((context, self.request), IHTMLRenderer)
   192         if adapter is not None:
   210         if adapter is not None:
   193             return adapter.render()
   211             return adapter.render()
   194         elif isinstance(context, str):
   212         if isinstance(context, str):
   195             return text_to_html(context, renderer)
   213             return text_to_html(context, renderer)
   196         else:
   214         return str(context)
   197             return str(context)
       
   198 
   215 
   199 
   216 
   200 PYAMS_HTML_RENDERERS_VOCABULARY = 'PyAMS HTML renderers'
   217 PYAMS_HTML_RENDERERS_VOCABULARY = 'PyAMS HTML renderers'
   201 
   218 
   202 
   219 
   203 @vocabulary_config(name=PYAMS_HTML_RENDERERS_VOCABULARY)
   220 @vocabulary_config(name=PYAMS_HTML_RENDERERS_VOCABULARY)
   204 class RenderersVocabulary(SimpleVocabulary):
   221 class RenderersVocabulary(SimpleVocabulary):
   205     """Text renderers vocabulary"""
   222     """Text renderers vocabulary"""
   206 
   223 
   207     def __init__(self, context=None):
   224     def __init__(self, context=None):  # pylint: disable=unused-argument
   208         request = check_request()
   225         request = check_request()
   209         registry = request.registry
   226         registry = request.registry
   210         translate = request.localizer.translate
   227         translate = request.localizer.translate
   211         terms = []
   228         terms = []
   212         append = terms.append
   229         append = terms.append
   224     breaks with given CSS class.
   241     breaks with given CSS class.
   225     """
   242     """
   226 
   243 
   227     @staticmethod
   244     @staticmethod
   228     def render(value, css_class='', character='|', start_tag=None, end_tag=None):
   245     def render(value, css_class='', character='|', start_tag=None, end_tag=None):
       
   246         """Render TALES extension;
       
   247         see :py:class:`ITALESExtension <pyams_utils.interfaces.tales.ITALESExtension>`
       
   248         """
   229         if not value:
   249         if not value:
   230             return ''
   250             return ''
   231         br = '<br {0} />'.format('class="{0}"'.format(css_class) if css_class else '')
   251         br_tag = '<br {0} />'.format('class="{0}"'.format(css_class) if css_class else '')
   232         elements = value.split(character)
   252         elements = value.split(character)
   233         if start_tag:
   253         if start_tag:
   234             elements[0] = '<{0}>{1}</{0}>'.format(start_tag, elements[0])
   254             elements[0] = '<{0}>{1}</{0}>'.format(start_tag, elements[0])
   235         if end_tag:
   255         if end_tag:
   236             elements[-1] = '<{0}>{1}</{0}>'.format(end_tag, elements[-1])
   256             elements[-1] = '<{0}>{1}</{0}>'.format(end_tag, elements[-1])
   237         return br.join(elements)
   257         return br_tag.join(elements)