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