Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/webcolors/_html5.py: 11%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2HTML5 color algorithms.
4Note that these functions are written in a way that may seem strange to developers
5familiar with Python, because they do not use the most efficient or idiomatic way of
6accomplishing their tasks. This is because, for compliance, these functions are written
7as literal translations into Python of the algorithms in HTML5:
9https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#colours
11For ease of understanding, the relevant steps of the algorithm from the standard are
12included as comments interspersed in the implementation.
14"""
16# SPDX-License-Identifier: BSD-3-Clause
18import string
20from ._definitions import _CSS3_NAMES_TO_HEX
21from ._types import HTML5SimpleColor, IntTuple
24def html5_parse_simple_color(value: str) -> HTML5SimpleColor:
25 """
26 Apply the HTML5 simple color parsing algorithm.
28 Examples:
30 .. doctest::
32 >>> html5_parse_simple_color("#ffffff")
33 HTML5SimpleColor(red=255, green=255, blue=255)
34 >>> html5_parse_simple_color("#fff")
35 Traceback (most recent call last):
36 ...
37 ValueError: An HTML5 simple color must be a string seven characters long.
39 :param value: The color to parse.
40 :type value: :class:`str`, which must consist of exactly the character ``"#"``
41 followed by six hexadecimal digits.
42 :raises ValueError: when the given value is not a Unicode string of length 7,
43 consisting of exactly the character ``#`` followed by six hexadecimal digits.
45 """
46 # 1. Let input be the string being parsed.
47 #
48 # 2. If input is not exactly seven characters long, then return an error.
49 if not isinstance(value, str) or len(value) != 7:
50 raise ValueError(
51 "An HTML5 simple color must be a Unicode string seven characters long."
52 )
54 # 3. If the first character in input is not a U+0023 NUMBER SIGN character (#), then
55 # return an error.
56 if not value.startswith("#"):
57 raise ValueError(
58 "An HTML5 simple color must begin with the character '#' (U+0023)."
59 )
61 # 4. If the last six characters of input are not all ASCII hex digits, then return
62 # an error.
63 if not all(c in string.hexdigits for c in value[1:]):
64 raise ValueError(
65 "An HTML5 simple color must contain exactly six ASCII hex digits."
66 )
68 # 5. Let result be a simple color.
69 #
70 # 6. Interpret the second and third characters as a hexadecimal number and let the
71 # result be the red component of result.
72 #
73 # 7. Interpret the fourth and fifth characters as a hexadecimal number and let the
74 # result be the green component of result.
75 #
76 # 8. Interpret the sixth and seventh characters as a hexadecimal number and let the
77 # result be the blue component of result.
78 #
79 # 9. Return result.
80 return HTML5SimpleColor(
81 int(value[1:3], 16), int(value[3:5], 16), int(value[5:7], 16)
82 )
85def html5_serialize_simple_color(simple_color: IntTuple) -> str:
86 """
87 Apply the HTML5 simple color serialization algorithm.
89 Examples:
91 .. doctest::
93 >>> html5_serialize_simple_color((0, 0, 0))
94 '#000000'
95 >>> html5_serialize_simple_color((255, 255, 255))
96 '#ffffff'
98 :param simple_color: The color to serialize.
100 """
101 red, green, blue = simple_color
103 # 1. Let result be a string consisting of a single "#" (U+0023) character.
104 result = "#"
106 # 2. Convert the red, green, and blue components in turn to two-digit hexadecimal
107 # numbers using lowercase ASCII hex digits, zero-padding if necessary, and append
108 # these numbers to result, in the order red, green, blue.
109 format_string = "{:02x}"
110 result += format_string.format(red)
111 result += format_string.format(green)
112 result += format_string.format(blue)
114 # 3. Return result, which will be a valid lowercase simple color.
115 return result
118def html5_parse_legacy_color(value: str) -> HTML5SimpleColor:
119 """
120 Apply the HTML5 legacy color parsing algorithm.
122 Note that, since this algorithm is intended to handle many _types of
123 malformed color values present in real-world Web documents, it is
124 *extremely* forgiving of input, but the results of parsing inputs
125 with high levels of "junk" (i.e., text other than a color value)
126 may be surprising.
128 Examples:
130 .. doctest::
132 >>> html5_parse_legacy_color("black")
133 HTML5SimpleColor(red=0, green=0, blue=0)
134 >>> html5_parse_legacy_color("chucknorris")
135 HTML5SimpleColor(red=192, green=0, blue=0)
136 >>> html5_parse_legacy_color("Window")
137 HTML5SimpleColor(red=0, green=13, blue=0)
139 :param value: The color to parse.
141 :raises ValueError: when the given value is not a Unicode string, when it is the
142 empty string, or when it is precisely the string ``"transparent"``.
144 """
145 # 1. Let input be the string being parsed.
146 if not isinstance(value, str):
147 raise ValueError(
148 "HTML5 legacy color parsing requires a Unicode string as input."
149 )
151 # 2. If input is the empty string, then return an error.
152 if value == "":
153 raise ValueError("HTML5 legacy color parsing forbids empty string as a value.")
155 # 3. Strip leading and trailing ASCII whitespace from input.
156 value = value.strip()
158 # 4. If input is an ASCII case-insensitive match for the string "transparent", then
159 # return an error.
160 if value.lower() == "transparent":
161 raise ValueError('HTML5 legacy color parsing forbids "transparent" as a value.')
163 # 5. If input is an ASCII case-insensitive match for one of the named colors, then
164 # return the simple color corresponding to that keyword.
165 #
166 # Note: CSS2 System Colors are not recognized.
167 if keyword_hex := _CSS3_NAMES_TO_HEX.get(value.lower()):
168 return html5_parse_simple_color(keyword_hex)
170 # 6. If input's code point length is four, and the first character in input is
171 # U+0023 (#), and the last three characters of input are all ASCII hex digits,
172 # then:
173 if (
174 len(value) == 4
175 and value.startswith("#")
176 and all(c in string.hexdigits for c in value[1:])
177 ):
178 # 1. Let result be a simple color.
179 #
180 # 2. Interpret the second character of input as a hexadecimal digit; let the red
181 # component of result be the resulting number multiplied by 17.
182 #
183 # 3. Interpret the third character of input as a hexadecimal digit; let the
184 # green component of result be the resulting number multiplied by 17.
185 #
186 # 4. Interpret the fourth character of input as a hexadecimal digit; let the
187 # blue component of result be the resulting number multiplied by 17.
188 result = HTML5SimpleColor(
189 int(value[1], 16) * 17, int(value[2], 16) * 17, int(value[3], 16) * 17
190 )
192 # 5. Return result.
193 return result
195 # 7. Replace any code points greater than U+FFFF in input (i.e., any characters that
196 # are not in the basic multilingual plane) with the two-character string "00".
197 value = "".join("00" if ord(c) > 0xFFFF else c for c in value)
199 # 8. If input's code point length is greater than 128, truncate input, leaving only
200 # the first 128 characters.
201 if len(value) > 128:
202 value = value[:128]
204 # 9. If the first character in input is a U+0023 NUMBER SIGN character (#), remove
205 # it.
206 if value.startswith("#"):
207 value = value[1:]
209 # 10. Replace any character in input that is not an ASCII hex digit with the
210 # character U+0030 DIGIT ZERO (0).
211 value = "".join(c if c in string.hexdigits else "0" for c in value)
213 # 11. While input's code point length is zero or not a multiple of three, append a
214 # U+0030 DIGIT ZERO (0) character to input.
215 while (len(value) == 0) or (len(value) % 3 != 0):
216 value += "0"
218 # 12. Split input into three strings of equal code point length, to obtain three
219 # components. Let length be the code point length that all of those components
220 # have (one third the code point length of input).
221 length = int(len(value) / 3)
222 red = value[:length]
223 green = value[length : length * 2]
224 blue = value[length * 2 :]
226 # 13. If length is greater than 8, then remove the leading length-8 characters in
227 # each component, and let length be 8.
228 if length > 8:
229 red, green, blue = (red[length - 8 :], green[length - 8 :], blue[length - 8 :])
230 length = 8
232 # 14. While length is greater than two and the first character in each component is
233 # a U+0030 DIGIT ZERO (0) character, remove that character and reduce length by
234 # one.
235 while (length > 2) and (red[0] == "0" and green[0] == "0" and blue[0] == "0"):
236 red, green, blue = (red[1:], green[1:], blue[1:])
237 length -= 1
239 # 15. If length is still greater than two, truncate each component, leaving only the
240 # first two characters in each.
241 if length > 2:
242 red, green, blue = (red[:2], green[:2], blue[:2])
244 # 16. Let result be a simple color.
245 #
246 # 17. Interpret the first component as a hexadecimal number; let the red component
247 # of result be the resulting number.
248 #
249 # 18. Interpret the second component as a hexadecimal number; let the green
250 # component of result be the resulting number.
251 #
252 # 19. Interpret the third component as a hexadecimal number; let the blue component
253 # of result be the resulting number.
254 #
255 # 20. Return result.
256 return HTML5SimpleColor(int(red, 16), int(green, 16), int(blue, 16))