Run update-copyright.py
[pycalendar.git] / pycalendar / text.py
1 # Copyright (C) 2013 W. Trevor King <wking@tremily.us>
2 #
3 # This file is part of pycalender.
4 #
5 # pycalender is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free Software
7 # Foundation, either version 3 of the License, or (at your option) any later
8 # version.
9 #
10 # pycalender is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # pycalender.  If not, see <http://www.gnu.org/licenses/>.
16
17 """Functions for processing text
18
19 As defined in :RFC:`5545`, section 3.3.11 (Text).
20 """
21
22 import logging as _logging
23 import re as _re
24
25
26 _LOG = _logging.getLogger(__name__)
27
28 _ESCAPES = {
29     '\\': [r'\\'],
30     '\n': [r'\n', r'\N'],
31     ';': [r'\;'],
32     ',': [r'\,'],
33     }
34
35 _UNESCAPES = None
36 _ESCAPE_REGEXP = None
37 _UNESCAPE_REGEXP = None
38
39
40 def _backslash_escape(text):
41     r"""Escape backslashes, but nothing else
42
43     This is used in ``_setup_escapes`` to build regular
44     expressions.
45
46     >>> _backslash_escape('\\')
47     '\\\\'
48     >>> _backslash_escape('stuff')
49     'stuff'
50     """
51     if text == '\\':
52         return r'\\'
53     return text
54
55
56 def _setup_escapes():
57     global _UNESCAPES
58     global _ESCAPE_REGEXP
59     global _UNESCAPE_REGEXP
60     _UNESCAPES = {}
61     for key,values in _ESCAPES.items():
62         for value in values:
63             if len(value) != 2:
64                 raise NotImplementedError(
65                     '{!r} escape value too long ({})'.format(
66                         value, len(value)))
67             if value[0] != '\\':
68                 raise NotImplementedError(
69                     '{!r} escape does not begin with a backslash'.format(
70                         value))
71             _UNESCAPES[value] = key
72     escape_regexp = '({})'.format('|'.join(
73             _backslash_escape(char) for char in _ESCAPES.keys()))
74     _LOG.debug('text-escape regexp: {!r}'.format(escape_regexp))
75     _ESCAPE_REGEXP = _re.compile(escape_regexp)
76     unescape_regexp =  r'(\\({}))'.format('|'.join(
77             _backslash_escape(escape[1]) for escape in _UNESCAPES.keys()))
78     _LOG.debug('text-unescape regexp: {!r}'.format(unescape_regexp))
79     _UNESCAPE_REGEXP = _re.compile(unescape_regexp)
80 _setup_escapes()
81
82
83 def _escape_replacer(match):
84     return _ESCAPES[match.group(1)][0]
85
86
87 def _unescape_replacer(match):
88     return _UNESCAPES[match.group(1)][0]
89
90
91 def escape(text):
92     r"""Convert a Python string to :RFC:`5545`-compliant text
93
94     Conforming to section 3.3.11 (text)
95
96     >>> print(escape(text='Hello!\nLook: newlines!'))
97     Hello!\nLook: newlines!
98     >>> print(escape(text='Single backslashes \\ may be tricky\n'))
99     Single backslashes \\ may be tricky\n
100     """
101     return _ESCAPE_REGEXP.subn(
102         repl=_escape_replacer, string=text)[0]
103
104
105 def unescape(text):
106     r"""Convert :RFC:`5545`-compliant text to a Python string
107
108     Conforming to section 3.3.11 (text)
109
110     >>> for text in [
111     ...         'Hello!\nLook: newlines!',
112     ...         ]:
113     ...     escaped = escape(text=text)
114     ...     unescaped = unescape(text=escaped)
115     ...     if unescaped != text:
116     ...         raise ValueError(unescaped)
117     """
118     return _UNESCAPE_REGEXP.subn(
119         repl=_unescape_replacer, string=text)[0]