Merge branch 'alias' into 'master'
[mailman.git] / src / mailman / utilities / string.py
blob2ca884a3329bb2ca48ac21ca6626c434384ee196
1 # Copyright (C) 2009-2019 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman 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
7 # Software Foundation, either version 3 of the License, or (at your option)
8 # any later version.
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 # more details.
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
18 """String utilities."""
20 import logging
22 from email.errors import HeaderParseError
23 from email.header import decode_header, make_header
24 from mailman.config import config
25 from public import public
26 from string import Template, whitespace
27 from textwrap import TextWrapper, dedent
30 EMPTYSTRING = ''
31 NL = '\n'
33 log = logging.getLogger('mailman.error')
36 @public
37 def expand(template, mlist=None, extras=None, template_class=Template):
38 """Expand string template with substitutions.
40 :param template: A PEP 292 $-string template.
41 :type template: string
42 :param mlist: Optional mailing list. If given, the standard set of
43 list-specific substitution variables are used automatically.
44 :type mlist: `IMailingList`
45 :param extras: An additional substitutions dictionary. These are used to
46 augment any standard, list-specific substitutions.
47 :type extras: dict
48 :param template_class: The template class to use.
49 :type template_class: class
50 :return: The substituted string.
51 :rtype: string
52 """
53 substitutions = dict(
54 site_email=config.mailman.site_owner,
56 if mlist is not None:
57 substitutions.update(dict(
58 listname=mlist.fqdn_listname,
59 list_id=mlist.list_id,
60 display_name=mlist.display_name,
61 short_listname=mlist.list_name,
62 domain=mlist.mail_host,
63 description=mlist.description,
64 info=mlist.info,
65 request_email=mlist.request_address,
66 owner_email=mlist.owner_address,
67 language=mlist.preferred_language.code,
69 if extras is not None:
70 substitutions.update(extras)
71 return template_class(template).safe_substitute(substitutions)
74 @public
75 def oneline(s, cset='us-ascii', in_unicode=False):
76 """Decode a header string in one line and convert into specified charset.
78 :param s: The header string
79 :type s: string
80 :param cset: The character set (encoding) to use.
81 :type cset: string
82 :param in_unicode: Flag specifying whether to return the converted string
83 as a unicode (True) or an 8-bit string (False, the default).
84 :type in_unicode: bool
85 :return: The decoded header string. If an error occurs while converting
86 the input string, return the string undecoded, as an 8-bit string.
87 :rtype: string
88 """
89 try:
90 h = str(make_header(decode_header(s)))
91 line = EMPTYSTRING.join(h.splitlines())
92 if in_unicode:
93 return line
94 else:
95 return line.encode(cset, 'replace')
96 except (LookupError, UnicodeError, ValueError, HeaderParseError):
97 # possibly charset problem. return with undecoded string in one line.
98 return EMPTYSTRING.join(s.splitlines())
101 @public
102 def wrap(text, column=70, honor_leading_ws=True):
103 """Wrap and fill the text to the specified column.
105 The input text is wrapped and filled as done by the standard library
106 textwrap module. The differences here being that this function is capable
107 of filling multiple paragraphs (as defined by text separated by blank
108 lines). Also, when `honor_leading_ws` is True (the default), paragraphs
109 that being with whitespace are not wrapped. This is the algorithm that
110 the Python FAQ wizard used.
112 # First, split the original text into paragraph, keeping all blank lines
113 # between them.
114 paragraphs = []
115 paragraph = []
116 last_indented = False
117 for line in text.splitlines(True):
118 is_indented = (len(line) > 0 and line[0] in whitespace)
119 if line == NL:
120 if len(paragraph) > 0:
121 paragraphs.append(EMPTYSTRING.join(paragraph))
122 paragraphs.append(line)
123 last_indented = False
124 paragraph = []
125 elif last_indented != is_indented:
126 # The indentation level changed. We treat this as a paragraph
127 # break but no blank line will be issued between paragraphs.
128 if len(paragraph) > 0:
129 paragraphs.append(EMPTYSTRING.join(paragraph))
130 # The next paragraph starts with this line.
131 paragraph = [line]
132 last_indented = is_indented
133 else:
134 # This line does not constitute a paragraph break.
135 paragraph.append(line)
136 # We've consumed all the lines in the original text. Transfer the last
137 # paragraph we were collecting to the full set of paragraphs, but only if
138 # it's not empty.
139 if len(paragraph) > 0:
140 paragraphs.append(EMPTYSTRING.join(paragraph))
141 # Now iterate through all paragraphs, wrapping as necessary.
142 wrapped_paragraphs = []
143 # The dedented wrapper.
144 wrapper = TextWrapper(width=column,
145 break_on_hyphens=False,
146 fix_sentence_endings=True)
147 # The indented wrapper. For this one, we'll clobber initial_indent and
148 # subsequent_indent as needed per indented chunk of text.
149 iwrapper = TextWrapper(width=column,
150 break_on_hyphens=False,
151 fix_sentence_endings=True,
153 add_paragraph_break = False
154 for paragraph in paragraphs:
155 if add_paragraph_break:
156 wrapped_paragraphs.append(NL)
157 add_paragraph_break = False
158 paragraph_text = EMPTYSTRING.join(paragraph)
159 # Just copy the blank lines to the final set of paragraphs.
160 if len(paragraph) == 0 or paragraph == NL:
161 wrapped_paragraphs.append(NL)
162 # Choose the wrapper based on whether the paragraph is indented or
163 # not. Also, do not wrap indented paragraphs if honor_leading_ws is
164 # set.
165 elif paragraph[0] in whitespace:
166 if honor_leading_ws:
167 # Leave the indented paragraph verbatim.
168 wrapped_paragraphs.append(paragraph_text)
169 else:
170 # The paragraph should be wrapped, but it must first be
171 # dedented. The leading whitespace on the first line of the
172 # original text will be used as the indentation for all lines
173 # in the wrapped text.
174 for i, ch in enumerate(paragraph_text): # pragma: no branch
175 if ch not in whitespace:
176 break
177 leading_ws = paragraph[:i]
178 iwrapper.initial_indent = leading_ws
179 iwrapper.subsequent_indent = leading_ws
180 paragraph_text = dedent(paragraph_text)
181 wrapped_paragraphs.append(iwrapper.fill(paragraph_text))
182 add_paragraph_break = True
183 else:
184 # Fill this paragraph. fill() consumes the trailing newline.
185 wrapped_paragraphs.append(wrapper.fill(paragraph_text))
186 add_paragraph_break = True
187 return EMPTYSTRING.join(wrapped_paragraphs)