html.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. import re
  2. import nh3
  3. from django.utils.html import escape
  4. from .constants import HTML_ALLOWED_ATTRIBUTES, HTML_ALLOWED_TAGS, IMAGE_URL_SCHEMES
  5. __all__ = (
  6. 'clean_html',
  7. 'foreground_color',
  8. 'highlight',
  9. )
  10. SCHEME_RE = re.compile(r'^([a-zA-Z][a-zA-Z0-9+.-]*):')
  11. # Per the URL spec, browsers ignore leading/trailing C0 control characters & space, and strip any tab or
  12. # newline characters appearing within a URL. We must normalize accordingly before checking the scheme.
  13. URL_STRIP_CHARS = ''.join(chr(c) for c in range(0x21))
  14. URL_REMOVE_CHARS = str.maketrans('', '', '\t\r\n')
  15. def _attribute_filter(tag, attr, value):
  16. """Returns str to keep/modify attribute, None to remove it."""
  17. if tag == 'img' and attr == 'src':
  18. match = SCHEME_RE.match(value.strip(URL_STRIP_CHARS).translate(URL_REMOVE_CHARS))
  19. if match and match.group(1).lower() not in IMAGE_URL_SCHEMES:
  20. return None
  21. return value
  22. def clean_html(html, schemes):
  23. """
  24. Sanitizes HTML based on a whitelist of allowed tags and attributes.
  25. Also takes a list of allowed URI schemes.
  26. """
  27. url_schemes = set(schemes)
  28. attribute_filter = None if url_schemes <= IMAGE_URL_SCHEMES else _attribute_filter
  29. return nh3.clean(
  30. html,
  31. tags=HTML_ALLOWED_TAGS,
  32. attributes=HTML_ALLOWED_ATTRIBUTES,
  33. url_schemes=url_schemes,
  34. attribute_filter=attribute_filter,
  35. )
  36. def foreground_color(bg_color, dark='000000', light='ffffff'):
  37. """
  38. Return the ideal foreground color (dark or light) for a given background color in hexadecimal RGB format.
  39. :param dark: RBG color code for dark text
  40. :param light: RBG color code for light text
  41. """
  42. THRESHOLD = 150
  43. bg_color = bg_color.strip('#')
  44. r, g, b = [int(bg_color[c:c + 2], 16) for c in (0, 2, 4)]
  45. if r * 0.299 + g * 0.587 + b * 0.114 > THRESHOLD:
  46. return dark
  47. return light
  48. def highlight(value, highlight, trim_pre=None, trim_post=None, trim_placeholder='...'):
  49. """
  50. Highlight a string within a string and optionally trim the pre/post portions of the original string.
  51. Args:
  52. value: The body of text being searched against
  53. highlight: The string of compiled regex pattern to highlight in `value`
  54. trim_pre: Maximum length of pre-highlight text to include
  55. trim_post: Maximum length of post-highlight text to include
  56. trim_placeholder: String value to swap in for trimmed pre/post text
  57. """
  58. # Split value on highlight string
  59. try:
  60. if type(highlight) is re.Pattern:
  61. pre, match, post = highlight.split(value, maxsplit=1)
  62. else:
  63. highlight = re.escape(highlight)
  64. pre, match, post = re.split(fr'({highlight})', value, maxsplit=1, flags=re.IGNORECASE)
  65. except ValueError:
  66. # Match not found
  67. return escape(value)
  68. # Trim pre/post sections to length
  69. if trim_pre and len(pre) > trim_pre:
  70. pre = trim_placeholder + pre[-trim_pre:]
  71. if trim_post and len(post) > trim_post:
  72. post = post[:trim_post] + trim_placeholder
  73. return f'{escape(pre)}<mark>{escape(match)}</mark>{escape(post)}'