blob: 1045e3f4642a7be376e6c004f6ee79143382df19 [file] [log] [blame]
Armin Ronacher07bc6842008-03-31 14:18:49 +02001# -*- coding: utf-8 -*-
2"""
3 jinja2.utils
4 ~~~~~~~~~~~~
5
6 Utility functions.
7
Armin Ronacher55494e42010-01-22 09:41:48 +01008 :copyright: (c) 2010 by the Jinja Team.
Armin Ronacher07bc6842008-03-31 14:18:49 +02009 :license: BSD, see LICENSE for more details.
10"""
Christoph Hack80909862008-04-14 01:35:10 +020011import re
Armin Ronacherccae0552008-10-05 23:08:58 +020012import errno
Armin Ronacher814f6c22008-04-17 15:52:23 +020013from collections import deque
Armin Ronacherf15b8142013-05-20 17:06:41 +010014from threading import Lock
Armin Ronacherce779a52013-05-20 02:11:16 +010015from jinja2._compat import text_type, string_types, implements_iterator, \
Armin Ronacherf15b8142013-05-20 17:06:41 +010016 url_quote
Armin Ronachere9098672013-05-19 14:16:13 +010017
Armin Ronacher8edbe492008-04-10 20:43:43 +020018
Armin Ronacherbe4ae242008-04-18 09:49:08 +020019_word_split_re = re.compile(r'(\s+)')
20_punctuation_re = re.compile(
21 '^(?P<lead>(?:%s)*)(?P<middle>.*?)(?P<trail>(?:%s)*)$' % (
Thomas Waldmann7d295622013-05-18 00:06:22 +020022 '|'.join(map(re.escape, ('(', '<', '&lt;'))),
23 '|'.join(map(re.escape, ('.', ',', ')', '>', '\n', '&gt;')))
Armin Ronacherbe4ae242008-04-18 09:49:08 +020024 )
25)
26_simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$')
Armin Ronacher76c280b2008-05-04 12:31:48 +020027_striptags_re = re.compile(r'(<!--.*?-->|<[^>]*>)')
28_entity_re = re.compile(r'&([^;]+);')
Armin Ronacher9a0078d2008-08-13 18:24:17 +020029_letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
30_digits = '0123456789'
Armin Ronacherbe4ae242008-04-18 09:49:08 +020031
Armin Ronacher7259c762008-04-30 13:03:59 +020032# special singleton representing missing values for the runtime
33missing = type('MissingType', (), {'__repr__': lambda x: 'missing'})()
34
Armin Ronacherd416a972009-02-24 22:58:00 +010035# internal code
36internal_code = set()
37
Thomas Waldmann07a7d552013-05-18 00:47:41 +020038concat = u''.join
Armin Ronacher7ceced52008-05-03 10:15:31 +020039
40
Armin Ronacher4f7d2d52008-04-22 10:40:26 +020041def contextfunction(f):
Armin Ronacher9bb7e472008-05-28 11:26:59 +020042 """This decorator can be used to mark a function or method context callable.
43 A context callable is passed the active :class:`Context` as first argument when
44 called from the template. This is useful if a function wants to get access
45 to the context or functions provided on the context object. For example
46 a function that returns a sorted list of template variables the current
47 template exports could look like this::
48
Armin Ronacher58f351d2008-05-28 21:30:14 +020049 @contextfunction
Armin Ronacher9bb7e472008-05-28 11:26:59 +020050 def get_exported_names(context):
51 return sorted(context.exported_vars)
Armin Ronacher4f7d2d52008-04-22 10:40:26 +020052 """
53 f.contextfunction = True
54 return f
55
56
Armin Ronacher8346bd72010-03-14 19:43:47 +010057def evalcontextfunction(f):
Florent Xicluna0ec4f762012-02-05 13:09:15 +010058 """This decorator can be used to mark a function or method as an eval
Armin Ronacher8346bd72010-03-14 19:43:47 +010059 context callable. This is similar to the :func:`contextfunction`
60 but instead of passing the context, an evaluation context object is
Armin Ronacherfe150f32010-03-15 02:42:41 +010061 passed. For more information about the eval context, see
62 :ref:`eval-context`.
Armin Ronacher8346bd72010-03-14 19:43:47 +010063
64 .. versionadded:: 2.4
65 """
66 f.evalcontextfunction = True
67 return f
68
69
Armin Ronacher203bfcb2008-04-24 21:54:44 +020070def environmentfunction(f):
Armin Ronacher9bb7e472008-05-28 11:26:59 +020071 """This decorator can be used to mark a function or method as environment
72 callable. This decorator works exactly like the :func:`contextfunction`
73 decorator just that the first argument is the active :class:`Environment`
74 and not context.
Armin Ronacher203bfcb2008-04-24 21:54:44 +020075 """
76 f.environmentfunction = True
77 return f
78
79
Armin Ronacherd416a972009-02-24 22:58:00 +010080def internalcode(f):
81 """Marks the function as internally used"""
Thomas Waldmanne0003552013-05-17 23:52:14 +020082 internal_code.add(f.__code__)
Armin Ronacherd416a972009-02-24 22:58:00 +010083 return f
84
85
Armin Ronacher9bb7e472008-05-28 11:26:59 +020086def is_undefined(obj):
87 """Check if the object passed is undefined. This does nothing more than
88 performing an instance check against :class:`Undefined` but looks nicer.
89 This can be used for custom filters or tests that want to react to
90 undefined variables. For example a custom default filter can look like
91 this::
92
93 def default(var, default=''):
94 if is_undefined(var):
95 return default
96 return var
97 """
98 from jinja2.runtime import Undefined
99 return isinstance(obj, Undefined)
100
101
Armin Ronacherba6e25a2008-11-02 15:58:14 +0100102def consume(iterable):
103 """Consumes an iterable without doing anything with it."""
104 for event in iterable:
105 pass
106
107
Armin Ronacher187bde12008-05-01 18:19:16 +0200108def clear_caches():
109 """Jinja2 keeps internal caches for environments and lexers. These are
110 used so that Jinja2 doesn't have to recreate environments and lexers all
111 the time. Normally you don't have to care about that but if you are
112 messuring memory consumption you may want to clean the caches.
113 """
114 from jinja2.environment import _spontaneous_environments
115 from jinja2.lexer import _lexer_cache
116 _spontaneous_environments.clear()
117 _lexer_cache.clear()
118
119
Armin Ronacherf59bac22008-04-20 13:11:43 +0200120def import_string(import_name, silent=False):
Florent Xicluna0ec4f762012-02-05 13:09:15 +0100121 """Imports an object based on a string. This is useful if you want to
Armin Ronacherf59bac22008-04-20 13:11:43 +0200122 use import paths as endpoints or something similar. An import path can
123 be specified either in dotted notation (``xml.sax.saxutils.escape``)
124 or with a colon as object delimiter (``xml.sax.saxutils:escape``).
125
126 If the `silent` is True the return value will be `None` if the import
127 fails.
128
129 :return: imported object
Armin Ronacher9a027f42008-04-17 11:13:40 +0200130 """
Armin Ronacherf59bac22008-04-20 13:11:43 +0200131 try:
132 if ':' in import_name:
133 module, obj = import_name.split(':', 1)
134 elif '.' in import_name:
135 items = import_name.split('.')
136 module = '.'.join(items[:-1])
137 obj = items[-1]
138 else:
139 return __import__(import_name)
140 return getattr(__import__(module, None, None, [obj]), obj)
141 except (ImportError, AttributeError):
142 if not silent:
143 raise
Armin Ronacher9a027f42008-04-17 11:13:40 +0200144
145
Armin Ronacher0faa8612010-02-09 15:04:51 +0100146def open_if_exists(filename, mode='rb'):
Armin Ronacherccae0552008-10-05 23:08:58 +0200147 """Returns a file descriptor for the filename if that file exists,
148 otherwise `None`.
149 """
150 try:
Armin Ronacher790b8a82010-02-10 00:05:46 +0100151 return open(filename, mode)
Thomas Waldmanne0003552013-05-17 23:52:14 +0200152 except IOError as e:
Armin Ronacherccae0552008-10-05 23:08:58 +0200153 if e.errno not in (errno.ENOENT, errno.EISDIR):
154 raise
155
156
Armin Ronacher98dbf5f2010-04-12 15:49:59 +0200157def object_type_repr(obj):
158 """Returns the name of the object's type. For some recognized
159 singletons the name of the object is returned instead. (For
160 example for `None` and `Ellipsis`).
161 """
162 if obj is None:
163 return 'None'
164 elif obj is Ellipsis:
165 return 'Ellipsis'
Armin Ronacher802f4722010-04-20 19:48:46 +0200166 # __builtin__ in 2.x, builtins in 3.x
167 if obj.__class__.__module__ in ('__builtin__', 'builtins'):
Armin Ronacher800ac7f2010-04-20 13:45:11 +0200168 name = obj.__class__.__name__
Armin Ronacher98dbf5f2010-04-12 15:49:59 +0200169 else:
Armin Ronacher800ac7f2010-04-20 13:45:11 +0200170 name = obj.__class__.__module__ + '.' + obj.__class__.__name__
Armin Ronacher98dbf5f2010-04-12 15:49:59 +0200171 return '%s object' % name
172
173
Christoph Hacke9e43bb2008-04-13 23:35:48 +0200174def pformat(obj, verbose=False):
Armin Ronacherbe4ae242008-04-18 09:49:08 +0200175 """Prettyprint an object. Either use the `pretty` library or the
Christoph Hacke9e43bb2008-04-13 23:35:48 +0200176 builtin `pprint`.
177 """
178 try:
179 from pretty import pretty
180 return pretty(obj, verbose=verbose)
181 except ImportError:
182 from pprint import pformat
183 return pformat(obj)
Christoph Hack80909862008-04-14 01:35:10 +0200184
185
Berker Peksagd55e1e52013-08-21 13:13:48 +0300186def urlize(text, trim_url_limit=None, nofollow=False, target=None):
Armin Ronacherbe4ae242008-04-18 09:49:08 +0200187 """Converts any URLs in text into clickable links. Works on http://,
Christoph Hack80909862008-04-14 01:35:10 +0200188 https:// and www. links. Links can have trailing punctuation (periods,
189 commas, close-parens) and leading punctuation (opening parens) and
190 it'll still do the right thing.
191
192 If trim_url_limit is not None, the URLs in link text will be limited
193 to trim_url_limit characters.
194
195 If nofollow is True, the URLs in link text will get a rel="nofollow"
196 attribute.
Berker Peksagd55e1e52013-08-21 13:13:48 +0300197
198 If target is not None, a target attribute will be added to the link.
Christoph Hack80909862008-04-14 01:35:10 +0200199 """
200 trim_url = lambda x, limit=trim_url_limit: limit is not None \
201 and (x[:limit] + (len(x) >=limit and '...'
202 or '')) or x
Armin Ronachere9098672013-05-19 14:16:13 +0100203 words = _word_split_re.split(text_type(escape(text)))
Christoph Hack80909862008-04-14 01:35:10 +0200204 nofollow_attr = nofollow and ' rel="nofollow"' or ''
Berker Peksagd55e1e52013-08-21 13:13:48 +0300205 if target is not None and isinstance(target, string_types):
206 target_attr = ' target="%s"' % target
207 else:
208 target_attr = ''
Christoph Hack80909862008-04-14 01:35:10 +0200209 for i, word in enumerate(words):
210 match = _punctuation_re.match(word)
211 if match:
212 lead, middle, trail = match.groups()
213 if middle.startswith('www.') or (
214 '@' not in middle and
215 not middle.startswith('http://') and
mozillazg66448932013-03-18 14:27:54 +0800216 not middle.startswith('https://') and
Christoph Hack80909862008-04-14 01:35:10 +0200217 len(middle) > 0 and
Armin Ronacher9a0078d2008-08-13 18:24:17 +0200218 middle[0] in _letters + _digits and (
Christoph Hack80909862008-04-14 01:35:10 +0200219 middle.endswith('.org') or
220 middle.endswith('.net') or
221 middle.endswith('.com')
222 )):
Berker Peksagd55e1e52013-08-21 13:13:48 +0300223 middle = '<a href="http://%s"%s%s>%s</a>' % (middle,
224 nofollow_attr, target_attr, trim_url(middle))
Christoph Hack80909862008-04-14 01:35:10 +0200225 if middle.startswith('http://') or \
226 middle.startswith('https://'):
Berker Peksagd55e1e52013-08-21 13:13:48 +0300227 middle = '<a href="%s"%s%s>%s</a>' % (middle,
228 nofollow_attr, target_attr, trim_url(middle))
Christoph Hack80909862008-04-14 01:35:10 +0200229 if '@' in middle and not middle.startswith('www.') and \
230 not ':' in middle and _simple_email_re.match(middle):
231 middle = '<a href="mailto:%s">%s</a>' % (middle, middle)
232 if lead + middle + trail != word:
233 words[i] = lead + middle + trail
234 return u''.join(words)
Armin Ronacher18c6ca02008-04-17 10:03:29 +0200235
236
Armin Ronacher4f7d2d52008-04-22 10:40:26 +0200237def generate_lorem_ipsum(n=5, html=True, min=20, max=100):
238 """Generate some lorem impsum for the template."""
239 from jinja2.constants import LOREM_IPSUM_WORDS
Georg Brandl95632c42009-11-22 18:35:18 +0100240 from random import choice, randrange
Armin Ronacher4f7d2d52008-04-22 10:40:26 +0200241 words = LOREM_IPSUM_WORDS.split()
242 result = []
243
Thomas Waldmanne0003552013-05-17 23:52:14 +0200244 for _ in range(n):
Armin Ronacher4f7d2d52008-04-22 10:40:26 +0200245 next_capitalized = True
246 last_comma = last_fullstop = 0
247 word = None
248 last = None
249 p = []
250
251 # each paragraph contains out of 20 to 100 words.
Thomas Waldmanne0003552013-05-17 23:52:14 +0200252 for idx, _ in enumerate(range(randrange(min, max))):
Armin Ronacher4f7d2d52008-04-22 10:40:26 +0200253 while True:
254 word = choice(words)
255 if word != last:
256 last = word
257 break
258 if next_capitalized:
259 word = word.capitalize()
260 next_capitalized = False
261 # add commas
262 if idx - randrange(3, 8) > last_comma:
263 last_comma = idx
264 last_fullstop += 2
265 word += ','
266 # add end of sentences
267 if idx - randrange(10, 20) > last_fullstop:
268 last_comma = last_fullstop = idx
269 word += '.'
270 next_capitalized = True
271 p.append(word)
272
273 # ensure that the paragraph ends with a dot.
274 p = u' '.join(p)
275 if p.endswith(','):
276 p = p[:-1] + '.'
277 elif not p.endswith('.'):
278 p += '.'
279 result.append(p)
280
281 if not html:
282 return u'\n\n'.join(result)
283 return Markup(u'\n'.join(u'<p>%s</p>' % escape(x) for x in result))
284
285
Armin Ronacher51454012012-01-07 17:47:56 +0100286def unicode_urlencode(obj, charset='utf-8'):
Armin Ronacher1d4c6382012-01-07 17:46:40 +0100287 """URL escapes a single bytestring or unicode string with the
288 given charset if applicable to URL safe quoting under all rules
289 that need to be considered under all supported Python versions.
290
291 If non strings are provided they are converted to their unicode
292 representation first.
293 """
Armin Ronachere9098672013-05-19 14:16:13 +0100294 if not isinstance(obj, string_types):
295 obj = text_type(obj)
296 if isinstance(obj, text_type):
Armin Ronacher1d4c6382012-01-07 17:46:40 +0100297 obj = obj.encode(charset)
Armin Ronachere9098672013-05-19 14:16:13 +0100298 return text_type(url_quote(obj))
Armin Ronacher1d4c6382012-01-07 17:46:40 +0100299
300
Armin Ronacher814f6c22008-04-17 15:52:23 +0200301class LRUCache(object):
302 """A simple LRU Cache implementation."""
Armin Ronacher58f351d2008-05-28 21:30:14 +0200303
304 # this is fast for small capacities (something below 1000) but doesn't
305 # scale. But as long as it's only used as storage for templates this
306 # won't do any harm.
Armin Ronacher814f6c22008-04-17 15:52:23 +0200307
308 def __init__(self, capacity):
309 self.capacity = capacity
310 self._mapping = {}
311 self._queue = deque()
Armin Ronacher7962ce72008-05-20 17:52:52 +0200312 self._postinit()
Armin Ronacher814f6c22008-04-17 15:52:23 +0200313
Armin Ronacher7962ce72008-05-20 17:52:52 +0200314 def _postinit(self):
Armin Ronacher814f6c22008-04-17 15:52:23 +0200315 # alias all queue methods for faster lookup
316 self._popleft = self._queue.popleft
317 self._pop = self._queue.pop
Thomas Waldmann07a7d552013-05-18 00:47:41 +0200318 self._remove = self._queue.remove
Armin Ronacherf15b8142013-05-20 17:06:41 +0100319 self._wlock = Lock()
Armin Ronacher814f6c22008-04-17 15:52:23 +0200320 self._append = self._queue.append
321
Armin Ronacher7962ce72008-05-20 17:52:52 +0200322 def __getstate__(self):
323 return {
324 'capacity': self.capacity,
325 '_mapping': self._mapping,
326 '_queue': self._queue
327 }
328
329 def __setstate__(self, d):
330 self.__dict__.update(d)
331 self._postinit()
332
333 def __getnewargs__(self):
334 return (self.capacity,)
335
Armin Ronacher814f6c22008-04-17 15:52:23 +0200336 def copy(self):
Florent Xicluna0ec4f762012-02-05 13:09:15 +0100337 """Return a shallow copy of the instance."""
Armin Ronacherbe4ae242008-04-18 09:49:08 +0200338 rv = self.__class__(self.capacity)
Armin Ronacher814f6c22008-04-17 15:52:23 +0200339 rv._mapping.update(self._mapping)
Armin Ronacherbe4ae242008-04-18 09:49:08 +0200340 rv._queue = deque(self._queue)
Armin Ronacher814f6c22008-04-17 15:52:23 +0200341 return rv
342
343 def get(self, key, default=None):
344 """Return an item from the cache dict or `default`"""
Armin Ronacher000b4912008-05-01 18:40:15 +0200345 try:
Armin Ronacher814f6c22008-04-17 15:52:23 +0200346 return self[key]
Armin Ronacher000b4912008-05-01 18:40:15 +0200347 except KeyError:
348 return default
Armin Ronacher814f6c22008-04-17 15:52:23 +0200349
350 def setdefault(self, key, default=None):
Armin Ronacherbe4ae242008-04-18 09:49:08 +0200351 """Set `default` if the key is not in the cache otherwise
Armin Ronacher814f6c22008-04-17 15:52:23 +0200352 leave unchanged. Return the value of this key.
353 """
Armin Ronacherd4e54382013-04-13 00:38:27 +0100354 self._wlock.acquire()
Armin Ronacher000b4912008-05-01 18:40:15 +0200355 try:
Armin Ronacherd4e54382013-04-13 00:38:27 +0100356 try:
357 return self[key]
358 except KeyError:
359 self[key] = default
360 return default
361 finally:
362 self._wlock.release()
Armin Ronacher814f6c22008-04-17 15:52:23 +0200363
364 def clear(self):
365 """Clear the cache."""
Armin Ronacher000b4912008-05-01 18:40:15 +0200366 self._wlock.acquire()
367 try:
368 self._mapping.clear()
369 self._queue.clear()
370 finally:
371 self._wlock.release()
Armin Ronacher814f6c22008-04-17 15:52:23 +0200372
373 def __contains__(self, key):
374 """Check if a key exists in this cache."""
375 return key in self._mapping
376
377 def __len__(self):
378 """Return the current size of the cache."""
379 return len(self._mapping)
380
381 def __repr__(self):
382 return '<%s %r>' % (
383 self.__class__.__name__,
384 self._mapping
385 )
386
387 def __getitem__(self, key):
388 """Get an item from the cache. Moves the item up so that it has the
389 highest priority then.
390
Florent Xicluna0ec4f762012-02-05 13:09:15 +0100391 Raise a `KeyError` if it does not exist.
Armin Ronacher814f6c22008-04-17 15:52:23 +0200392 """
Armin Ronacherd4e54382013-04-13 00:38:27 +0100393 self._wlock.acquire()
394 try:
395 rv = self._mapping[key]
396 if self._queue[-1] != key:
397 try:
398 self._remove(key)
399 except ValueError:
400 # if something removed the key from the container
401 # when we read, ignore the ValueError that we would
402 # get otherwise.
403 pass
404 self._append(key)
405 return rv
406 finally:
407 self._wlock.release()
Armin Ronacher814f6c22008-04-17 15:52:23 +0200408
409 def __setitem__(self, key, value):
410 """Sets the value for an item. Moves the item up so that it
411 has the highest priority then.
412 """
Armin Ronacher000b4912008-05-01 18:40:15 +0200413 self._wlock.acquire()
414 try:
415 if key in self._mapping:
Armin Ronacherd4e54382013-04-13 00:38:27 +0100416 self._remove(key)
Armin Ronacher000b4912008-05-01 18:40:15 +0200417 elif len(self._mapping) == self.capacity:
418 del self._mapping[self._popleft()]
419 self._append(key)
420 self._mapping[key] = value
421 finally:
422 self._wlock.release()
Armin Ronacher814f6c22008-04-17 15:52:23 +0200423
424 def __delitem__(self, key):
425 """Remove an item from the cache dict.
Florent Xicluna0ec4f762012-02-05 13:09:15 +0100426 Raise a `KeyError` if it does not exist.
Armin Ronacher814f6c22008-04-17 15:52:23 +0200427 """
Armin Ronacher000b4912008-05-01 18:40:15 +0200428 self._wlock.acquire()
429 try:
430 del self._mapping[key]
Armin Ronachere7c72bc2009-09-14 12:20:33 -0700431 try:
432 self._remove(key)
433 except ValueError:
434 # __getitem__ is not locked, it might happen
435 pass
Armin Ronacher000b4912008-05-01 18:40:15 +0200436 finally:
437 self._wlock.release()
Armin Ronacher814f6c22008-04-17 15:52:23 +0200438
Armin Ronachere25f24d2008-05-19 11:20:41 +0200439 def items(self):
440 """Return a list of items."""
441 result = [(key, self._mapping[key]) for key in list(self._queue)]
442 result.reverse()
443 return result
444
445 def iteritems(self):
446 """Iterate over all items."""
447 return iter(self.items())
448
449 def values(self):
450 """Return a list of all values."""
451 return [x[1] for x in self.items()]
452
453 def itervalue(self):
454 """Iterate over all values."""
455 return iter(self.values())
456
457 def keys(self):
458 """Return a list of all keys ordered by most recent usage."""
459 return list(self)
460
461 def iterkeys(self):
462 """Iterate over all keys in the cache dict, ordered by
Armin Ronacher814f6c22008-04-17 15:52:23 +0200463 the most recent usage.
464 """
Armin Ronachere2244882008-05-19 09:25:57 +0200465 return reversed(tuple(self._queue))
Armin Ronacher814f6c22008-04-17 15:52:23 +0200466
Armin Ronachere25f24d2008-05-19 11:20:41 +0200467 __iter__ = iterkeys
468
Armin Ronacher814f6c22008-04-17 15:52:23 +0200469 def __reversed__(self):
470 """Iterate over the values in the cache dict, oldest items
471 coming first.
472 """
Armin Ronachere2244882008-05-19 09:25:57 +0200473 return iter(tuple(self._queue))
Armin Ronacher814f6c22008-04-17 15:52:23 +0200474
475 __copy__ = copy
476
Armin Ronacherbd33f112008-04-18 09:17:32 +0200477
Armin Ronacher9bb7e472008-05-28 11:26:59 +0200478# register the LRU cache as mutable mapping if possible
479try:
480 from collections import MutableMapping
481 MutableMapping.register(LRUCache)
482except ImportError:
483 pass
484
485
Armin Ronacher28c74882013-05-20 01:51:26 +0100486@implements_iterator
487class Cycler(object):
Armin Ronacherccae0552008-10-05 23:08:58 +0200488 """A cycle helper for templates."""
489
490 def __init__(self, *items):
491 if not items:
492 raise RuntimeError('at least one item has to be provided')
493 self.items = items
494 self.reset()
495
496 def reset(self):
497 """Resets the cycle."""
498 self.pos = 0
499
500 @property
501 def current(self):
502 """Returns the current item."""
503 return self.items[self.pos]
504
Thomas Waldmann7d295622013-05-18 00:06:22 +0200505 def __next__(self):
Armin Ronacherccae0552008-10-05 23:08:58 +0200506 """Goes one item ahead and returns it."""
507 rv = self.current
508 self.pos = (self.pos + 1) % len(self.items)
509 return rv
510
511
Armin Ronacherd34eb122008-10-13 23:47:51 +0200512class Joiner(object):
513 """A joining helper for templates."""
514
515 def __init__(self, sep=u', '):
516 self.sep = sep
517 self.used = False
518
519 def __call__(self):
520 if not self.used:
521 self.used = True
522 return u''
523 return self.sep
524
525
Armin Ronacher294f2eb2013-05-19 13:49:12 +0100526# Imported here because that's where it was in the past
527from markupsafe import Markup, escape, soft_unicode