PageRenderTime 56ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/python-packages/django/utils/text.py

https://gitlab.com/gregtyka/ka-lite
Python | 428 lines | 358 code | 15 blank | 55 comment | 29 complexity | e886b673bbaa8ae3c182162b674518ce MD5 | raw file
  1. from __future__ import unicode_literals
  2. import re
  3. import unicodedata
  4. import warnings
  5. from gzip import GzipFile
  6. from io import BytesIO
  7. from django.utils.encoding import force_text
  8. from django.utils.functional import allow_lazy, SimpleLazyObject
  9. from django.utils import six
  10. from django.utils.six.moves import html_entities
  11. from django.utils.translation import ugettext_lazy, ugettext as _, pgettext
  12. from django.utils.safestring import mark_safe
  13. if not six.PY3:
  14. # Import force_unicode even though this module doesn't use it, because some
  15. # people rely on it being here.
  16. from django.utils.encoding import force_unicode
  17. # Capitalizes the first letter of a string.
  18. capfirst = lambda x: x and force_text(x)[0].upper() + force_text(x)[1:]
  19. capfirst = allow_lazy(capfirst, six.text_type)
  20. # Set up regular expressions
  21. re_words = re.compile(r'&.*?;|<.*?>|(\w[\w-]*)', re.U|re.S)
  22. re_tag = re.compile(r'<(/)?([^ ]+?)(?: (/)| .*?)?>', re.S)
  23. def wrap(text, width):
  24. """
  25. A word-wrap function that preserves existing line breaks and most spaces in
  26. the text. Expects that existing line breaks are posix newlines.
  27. """
  28. text = force_text(text)
  29. def _generator():
  30. it = iter(text.split(' '))
  31. word = next(it)
  32. yield word
  33. pos = len(word) - word.rfind('\n') - 1
  34. for word in it:
  35. if "\n" in word:
  36. lines = word.split('\n')
  37. else:
  38. lines = (word,)
  39. pos += len(lines[0]) + 1
  40. if pos > width:
  41. yield '\n'
  42. pos = len(lines[-1])
  43. else:
  44. yield ' '
  45. if len(lines) > 1:
  46. pos = len(lines[-1])
  47. yield word
  48. return ''.join(_generator())
  49. wrap = allow_lazy(wrap, six.text_type)
  50. class Truncator(SimpleLazyObject):
  51. """
  52. An object used to truncate text, either by characters or words.
  53. """
  54. def __init__(self, text):
  55. super(Truncator, self).__init__(lambda: force_text(text))
  56. def add_truncation_text(self, text, truncate=None):
  57. if truncate is None:
  58. truncate = pgettext(
  59. 'String to return when truncating text',
  60. '%(truncated_text)s...')
  61. truncate = force_text(truncate)
  62. if '%(truncated_text)s' in truncate:
  63. return truncate % {'truncated_text': text}
  64. # The truncation text didn't contain the %(truncated_text)s string
  65. # replacement argument so just append it to the text.
  66. if text.endswith(truncate):
  67. # But don't append the truncation text if the current text already
  68. # ends in this.
  69. return text
  70. return '%s%s' % (text, truncate)
  71. def chars(self, num, truncate=None):
  72. """
  73. Returns the text truncated to be no longer than the specified number
  74. of characters.
  75. Takes an optional argument of what should be used to notify that the
  76. string has been truncated, defaulting to a translatable string of an
  77. ellipsis (...).
  78. """
  79. length = int(num)
  80. text = unicodedata.normalize('NFC', self._wrapped)
  81. # Calculate the length to truncate to (max length - end_text length)
  82. truncate_len = length
  83. for char in self.add_truncation_text('', truncate):
  84. if not unicodedata.combining(char):
  85. truncate_len -= 1
  86. if truncate_len == 0:
  87. break
  88. s_len = 0
  89. end_index = None
  90. for i, char in enumerate(text):
  91. if unicodedata.combining(char):
  92. # Don't consider combining characters
  93. # as adding to the string length
  94. continue
  95. s_len += 1
  96. if end_index is None and s_len > truncate_len:
  97. end_index = i
  98. if s_len > length:
  99. # Return the truncated string
  100. return self.add_truncation_text(text[:end_index or 0],
  101. truncate)
  102. # Return the original string since no truncation was necessary
  103. return text
  104. chars = allow_lazy(chars)
  105. def words(self, num, truncate=None, html=False):
  106. """
  107. Truncates a string after a certain number of words. Takes an optional
  108. argument of what should be used to notify that the string has been
  109. truncated, defaulting to ellipsis (...).
  110. """
  111. length = int(num)
  112. if html:
  113. return self._html_words(length, truncate)
  114. return self._text_words(length, truncate)
  115. words = allow_lazy(words)
  116. def _text_words(self, length, truncate):
  117. """
  118. Truncates a string after a certain number of words.
  119. Newlines in the string will be stripped.
  120. """
  121. words = self._wrapped.split()
  122. if len(words) > length:
  123. words = words[:length]
  124. return self.add_truncation_text(' '.join(words), truncate)
  125. return ' '.join(words)
  126. def _html_words(self, length, truncate):
  127. """
  128. Truncates HTML to a certain number of words (not counting tags and
  129. comments). Closes opened tags if they were correctly closed in the
  130. given HTML.
  131. Newlines in the HTML are preserved.
  132. """
  133. if length <= 0:
  134. return ''
  135. html4_singlets = (
  136. 'br', 'col', 'link', 'base', 'img',
  137. 'param', 'area', 'hr', 'input'
  138. )
  139. # Count non-HTML words and keep note of open tags
  140. pos = 0
  141. end_text_pos = 0
  142. words = 0
  143. open_tags = []
  144. while words <= length:
  145. m = re_words.search(self._wrapped, pos)
  146. if not m:
  147. # Checked through whole string
  148. break
  149. pos = m.end(0)
  150. if m.group(1):
  151. # It's an actual non-HTML word
  152. words += 1
  153. if words == length:
  154. end_text_pos = pos
  155. continue
  156. # Check for tag
  157. tag = re_tag.match(m.group(0))
  158. if not tag or end_text_pos:
  159. # Don't worry about non tags or tags after our truncate point
  160. continue
  161. closing_tag, tagname, self_closing = tag.groups()
  162. # Element names are always case-insensitive
  163. tagname = tagname.lower()
  164. if self_closing or tagname in html4_singlets:
  165. pass
  166. elif closing_tag:
  167. # Check for match in open tags list
  168. try:
  169. i = open_tags.index(tagname)
  170. except ValueError:
  171. pass
  172. else:
  173. # SGML: An end tag closes, back to the matching start tag,
  174. # all unclosed intervening start tags with omitted end tags
  175. open_tags = open_tags[i + 1:]
  176. else:
  177. # Add it to the start of the open tags list
  178. open_tags.insert(0, tagname)
  179. if words <= length:
  180. # Don't try to close tags if we don't need to truncate
  181. return self._wrapped
  182. out = self._wrapped[:end_text_pos]
  183. truncate_text = self.add_truncation_text('', truncate)
  184. if truncate_text:
  185. out += truncate_text
  186. # Close any tags still open
  187. for tag in open_tags:
  188. out += '</%s>' % tag
  189. # Return string
  190. return out
  191. def truncate_words(s, num, end_text='...'):
  192. warnings.warn('This function has been deprecated. Use the Truncator class '
  193. 'in django.utils.text instead.', category=DeprecationWarning)
  194. truncate = end_text and ' %s' % end_text or ''
  195. return Truncator(s).words(num, truncate=truncate)
  196. truncate_words = allow_lazy(truncate_words, six.text_type)
  197. def truncate_html_words(s, num, end_text='...'):
  198. warnings.warn('This function has been deprecated. Use the Truncator class '
  199. 'in django.utils.text instead.', category=DeprecationWarning)
  200. truncate = end_text and ' %s' % end_text or ''
  201. return Truncator(s).words(num, truncate=truncate, html=True)
  202. truncate_html_words = allow_lazy(truncate_html_words, six.text_type)
  203. def get_valid_filename(s):
  204. """
  205. Returns the given string converted to a string that can be used for a clean
  206. filename. Specifically, leading and trailing spaces are removed; other
  207. spaces are converted to underscores; and anything that is not a unicode
  208. alphanumeric, dash, underscore, or dot, is removed.
  209. >>> get_valid_filename("john's portrait in 2004.jpg")
  210. 'johns_portrait_in_2004.jpg'
  211. """
  212. s = force_text(s).strip().replace(' ', '_')
  213. return re.sub(r'(?u)[^-\w.]', '', s)
  214. get_valid_filename = allow_lazy(get_valid_filename, six.text_type)
  215. def get_text_list(list_, last_word=ugettext_lazy('or')):
  216. """
  217. >>> get_text_list(['a', 'b', 'c', 'd'])
  218. 'a, b, c or d'
  219. >>> get_text_list(['a', 'b', 'c'], 'and')
  220. 'a, b and c'
  221. >>> get_text_list(['a', 'b'], 'and')
  222. 'a and b'
  223. >>> get_text_list(['a'])
  224. 'a'
  225. >>> get_text_list([])
  226. ''
  227. """
  228. if len(list_) == 0: return ''
  229. if len(list_) == 1: return force_text(list_[0])
  230. return '%s %s %s' % (
  231. # Translators: This string is used as a separator between list elements
  232. _(', ').join([force_text(i) for i in list_][:-1]),
  233. force_text(last_word), force_text(list_[-1]))
  234. get_text_list = allow_lazy(get_text_list, six.text_type)
  235. def normalize_newlines(text):
  236. return force_text(re.sub(r'\r\n|\r|\n', '\n', text))
  237. normalize_newlines = allow_lazy(normalize_newlines, six.text_type)
  238. def recapitalize(text):
  239. "Recapitalizes text, placing caps after end-of-sentence punctuation."
  240. text = force_text(text).lower()
  241. capsRE = re.compile(r'(?:^|(?<=[\.\?\!] ))([a-z])')
  242. text = capsRE.sub(lambda x: x.group(1).upper(), text)
  243. return text
  244. recapitalize = allow_lazy(recapitalize)
  245. def phone2numeric(phone):
  246. "Converts a phone number with letters into its numeric equivalent."
  247. char2number = {'a': '2', 'b': '2', 'c': '2', 'd': '3', 'e': '3', 'f': '3',
  248. 'g': '4', 'h': '4', 'i': '4', 'j': '5', 'k': '5', 'l': '5', 'm': '6',
  249. 'n': '6', 'o': '6', 'p': '7', 'q': '7', 'r': '7', 's': '7', 't': '8',
  250. 'u': '8', 'v': '8', 'w': '9', 'x': '9', 'y': '9', 'z': '9',
  251. }
  252. return ''.join(char2number.get(c, c) for c in phone.lower())
  253. phone2numeric = allow_lazy(phone2numeric)
  254. # From http://www.xhaus.com/alan/python/httpcomp.html#gzip
  255. # Used with permission.
  256. def compress_string(s):
  257. zbuf = BytesIO()
  258. zfile = GzipFile(mode='wb', compresslevel=6, fileobj=zbuf)
  259. zfile.write(s)
  260. zfile.close()
  261. return zbuf.getvalue()
  262. class StreamingBuffer(object):
  263. def __init__(self):
  264. self.vals = []
  265. def write(self, val):
  266. self.vals.append(val)
  267. def read(self):
  268. ret = b''.join(self.vals)
  269. self.vals = []
  270. return ret
  271. def flush(self):
  272. return
  273. def close(self):
  274. return
  275. # Like compress_string, but for iterators of strings.
  276. def compress_sequence(sequence):
  277. buf = StreamingBuffer()
  278. zfile = GzipFile(mode='wb', compresslevel=6, fileobj=buf)
  279. # Output headers...
  280. yield buf.read()
  281. for item in sequence:
  282. zfile.write(item)
  283. zfile.flush()
  284. yield buf.read()
  285. zfile.close()
  286. yield buf.read()
  287. ustring_re = re.compile("([\u0080-\uffff])")
  288. def javascript_quote(s, quote_double_quotes=False):
  289. def fix(match):
  290. return "\\u%04x" % ord(match.group(1))
  291. if type(s) == bytes:
  292. s = s.decode('utf-8')
  293. elif type(s) != six.text_type:
  294. raise TypeError(s)
  295. s = s.replace('\\', '\\\\')
  296. s = s.replace('\r', '\\r')
  297. s = s.replace('\n', '\\n')
  298. s = s.replace('\t', '\\t')
  299. s = s.replace("'", "\\'")
  300. if quote_double_quotes:
  301. s = s.replace('"', '&quot;')
  302. return str(ustring_re.sub(fix, s))
  303. javascript_quote = allow_lazy(javascript_quote, six.text_type)
  304. # Expression to match some_token and some_token="with spaces" (and similarly
  305. # for single-quoted strings).
  306. smart_split_re = re.compile(r"""
  307. ((?:
  308. [^\s'"]*
  309. (?:
  310. (?:"(?:[^"\\]|\\.)*" | '(?:[^'\\]|\\.)*')
  311. [^\s'"]*
  312. )+
  313. ) | \S+)
  314. """, re.VERBOSE)
  315. def smart_split(text):
  316. r"""
  317. Generator that splits a string by spaces, leaving quoted phrases together.
  318. Supports both single and double quotes, and supports escaping quotes with
  319. backslashes. In the output, strings will keep their initial and trailing
  320. quote marks and escaped quotes will remain escaped (the results can then
  321. be further processed with unescape_string_literal()).
  322. >>> list(smart_split(r'This is "a person\'s" test.'))
  323. ['This', 'is', '"a person\\\'s"', 'test.']
  324. >>> list(smart_split(r"Another 'person\'s' test."))
  325. ['Another', "'person\\'s'", 'test.']
  326. >>> list(smart_split(r'A "\"funky\" style" test.'))
  327. ['A', '"\\"funky\\" style"', 'test.']
  328. """
  329. text = force_text(text)
  330. for bit in smart_split_re.finditer(text):
  331. yield bit.group(0)
  332. smart_split = allow_lazy(smart_split, six.text_type)
  333. def _replace_entity(match):
  334. text = match.group(1)
  335. if text[0] == '#':
  336. text = text[1:]
  337. try:
  338. if text[0] in 'xX':
  339. c = int(text[1:], 16)
  340. else:
  341. c = int(text)
  342. return unichr(c)
  343. except ValueError:
  344. return match.group(0)
  345. else:
  346. try:
  347. return unichr(html_entities.name2codepoint[text])
  348. except (ValueError, KeyError):
  349. return match.group(0)
  350. _entity_re = re.compile(r"&(#?[xX]?(?:[0-9a-fA-F]+|\w{1,8}));")
  351. def unescape_entities(text):
  352. return _entity_re.sub(_replace_entity, text)
  353. unescape_entities = allow_lazy(unescape_entities, six.text_type)
  354. def unescape_string_literal(s):
  355. r"""
  356. Convert quoted string literals to unquoted strings with escaped quotes and
  357. backslashes unquoted::
  358. >>> unescape_string_literal('"abc"')
  359. 'abc'
  360. >>> unescape_string_literal("'abc'")
  361. 'abc'
  362. >>> unescape_string_literal('"a \"bc\""')
  363. 'a "bc"'
  364. >>> unescape_string_literal("'\'ab\' c'")
  365. "'ab' c"
  366. """
  367. if s[0] not in "\"'" or s[-1] != s[0]:
  368. raise ValueError("Not a string literal: %r" % s)
  369. quote = s[0]
  370. return s[1:-1].replace(r'\%s' % quote, quote).replace(r'\\', '\\')
  371. unescape_string_literal = allow_lazy(unescape_string_literal)
  372. def slugify(value):
  373. """
  374. Converts to lowercase, removes non-word characters (alphanumerics and
  375. underscores) and converts spaces to hyphens. Also strips leading and
  376. trailing whitespace.
  377. """
  378. value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
  379. value = re.sub('[^\w\s-]', '', value).strip().lower()
  380. return mark_safe(re.sub('[-\s]+', '-', value))
  381. slugify = allow_lazy(slugify, six.text_type)