879c6efd5b1886f1e19d5dc9f4a40283405008cf
[pycalendar.git] / pycalendar / text.py
1 # Copyright
2
3 """Functions for processing text
4
5 As defined in :RFC:`5545`, section 3.3.11 (Text).
6 """
7
8 import logging as _logging
9 import re as _re
10
11
12 _LOG = _logging.getLogger(__name__)
13
14 _ESCAPES = {
15     '\\': [r'\\'],
16     '\n': [r'\n', r'\N'],
17     ';': [r'\;'],
18     ',': [r'\,'],
19     }
20
21 _UNESCAPES = None
22 _ESCAPE_REGEXP = None
23 _UNESCAPE_REGEXP = None
24
25
26 def _backslash_escape(text):
27     r"""Escape backslashes, but nothing else
28
29     This is used in ``_setup_escapes`` to build regular
30     expressions.
31
32     >>> _backslash_escape('\\')
33     '\\\\'
34     >>> _backslash_escape('stuff')
35     'stuff'
36     """
37     if text == '\\':
38         return r'\\'
39     return text
40
41
42 def _setup_escapes():
43     global _UNESCAPES
44     global _ESCAPE_REGEXP
45     global _UNESCAPE_REGEXP
46     _UNESCAPES = {}
47     for key,values in _ESCAPES.items():
48         for value in values:
49             if len(value) != 2:
50                 raise NotImplementedError(
51                     '{!r} escape value too long ({})'.format(
52                         value, len(value)))
53             if value[0] != '\\':
54                 raise NotImplementedError(
55                     '{!r} escape does not begin with a backslash'.format(
56                         value))
57             _UNESCAPES[value] = key
58     escape_regexp = '({})'.format('|'.join(
59             _backslash_escape(char) for char in _ESCAPES.keys()))
60     _LOG.debug('text-escape regexp: {!r}'.format(escape_regexp))
61     _ESCAPE_REGEXP = _re.compile(escape_regexp)
62     unescape_regexp =  r'(\\({}))'.format('|'.join(
63             _backslash_escape(escape[1]) for escape in _UNESCAPES.keys()))
64     _LOG.debug('text-unescape regexp: {!r}'.format(unescape_regexp))
65     _UNESCAPE_REGEXP = _re.compile(unescape_regexp)
66 _setup_escapes()
67
68
69 def _escape_replacer(match):
70     return _ESCAPES[match.group(1)][0]
71
72
73 def _unescape_replacer(match):
74     return _UNESCAPES[match.group(1)][0]
75
76
77 def escape(text):
78     r"""Convert a Python string to :RFC:`5545`-compliant text
79
80     Conforming to section 3.3.11 (text)
81
82     >>> print(escape(text='Hello!\nLook: newlines!'))
83     Hello!\nLook: newlines!
84     >>> print(escape(text='Single backslashes \\ may be tricky\n'))
85     Single backslashes \\ may be tricky\n
86     """
87     return _ESCAPE_REGEXP.subn(
88         repl=_escape_replacer, string=text)[0]
89
90
91 def unescape(text):
92     r"""Convert :RFC:`5545`-compliant text to a Python string
93
94     Conforming to section 3.3.11 (text)
95
96     >>> for text in [
97     ...         'Hello!\nLook: newlines!',
98     ...         ]:
99     ...     escaped = escape(text=text)
100     ...     unescaped = unescape(text=escaped)
101     ...     if unescaped != text:
102     ...         raise ValueError(unescaped)
103     """
104     return _UNESCAPE_REGEXP.subn(
105         repl=_unescape_replacer, string=text)[0]