Coverage for phml\utilities\validate\validate.py: 100%
68 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-12 14:26 -0500
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-12 14:26 -0500
1from re import match, split, sub
2from typing import Any
4from phml.nodes import Element, Literal, Node, Parent
6__all__ = [
7 "validate",
8 "generated",
9 "is_heading",
10 "is_css_link",
11 "is_css_style",
12 "is_javascript",
13 "is_element",
14 "is_embedded",
15 "is_interactive",
16 "is_phrasing",
17 "is_event_handler",
18 "blank",
19]
22def validate(node: Node) -> bool:
23 """Validate a node based on attributes and type."""
25 if isinstance(node, Parent) and not all(isinstance(child, Node) for child in node):
26 raise AssertionError("Children must be a node type")
28 if isinstance(node, Element):
29 if not all(isinstance(node[prop], (bool, str)) for prop in node.attributes):
30 raise AssertionError("Element 'attributes' must be of type 'bool' or 'str'")
32 if isinstance(node, Literal) and not isinstance(node.content, str):
33 raise AssertionError("Literal 'content' must be of type 'str'")
35 return True
38def generated(node: Node) -> bool:
39 """Checks if a node has been generated. A node is concidered
40 generated if it does not have a position.
42 Args:
43 node (Node): Node to check for position with.
45 Returns:
46 bool: Whether a node has a position or not.
47 """
48 return node.position is None
51def is_heading(node: Element) -> bool:
52 """Check if an element is a heading."""
54 if node.type == "element":
55 if match(r"h[1-6]", node.tag) is not None:
56 return True
57 return False
58 raise TypeError("Node must be an element.")
61def is_css_link(node: Element) -> bool:
62 """Check if an element is a `link` to a css file.
64 Returns `true` if `node` is a `<link>` element with a `rel` list that
65 contains `'stylesheet'` and has no `type`, an empty `type`, or `'text/css'`
66 as its `type`
67 """
69 return (
70 # Verify it is a element with a `link` tag
71 is_element(node, "link")
72 # Must have a rel list with stylesheet
73 and "rel" in node
74 and "stylesheet" in split(r" ", sub(r" +", " ", node["rel"]))
75 and (
76 # Can have a `type` of `text/css` or empty or no `type`
77 "type" not in node
78 or ("type" in node and (node["type"] in ["text/css", ""]))
79 )
80 )
83def is_css_style(node: Element) -> bool:
84 """Check if an element is a css `style` element.
86 Returns `true` if `node` is a `<style>` element that
87 has no `type`, an empty `type`, or `'text/css'` as its `type`.
88 """
90 return is_element(node, "style") and (
91 "type" not in node or ("type" in node and (node["type"] in ["", "text/css"]))
92 )
95def is_javascript(node: Element) -> bool:
96 """Check if an element is a javascript `script` element.
98 Returns `true` if `node` is a `<script>` element that has a valid JavaScript `type`, has no
99 `type` and a valid JavaScript `language`, or has neither.
100 """
101 return is_element(node, "script") and (
102 (
103 "type" in node
104 and node["type"] in ["text/ecmascript", "text/javascript"]
105 and "language" not in node
106 )
107 or (
108 "language" in node
109 and node["language"] in ["ecmascript", "javascript"]
110 and "type" not in node
111 )
112 or ("type" not in node and "language" not in node)
113 )
116def is_element(node: Node, *conditions: str | list) -> bool:
117 """Checks if the given node is a certain element.
119 When providing a str it will check that the elements tag matches.
120 If a list is provided it checks that one of the conditions in the list
121 passes.
122 """
124 if isinstance(node, Element):
125 if len(conditions) > 0:
126 return any(
127 bool(
128 (isinstance(condition, str) and node.tag == condition)
129 or (
130 isinstance(condition, list)
131 and any(node.tag == nested for nested in condition)
132 ),
133 )
134 for condition in conditions
135 )
136 else:
137 return True
138 return False
141def is_event_handler(attribute: str) -> bool:
142 """Takes a attribute name and returns true if
143 it starts with `on` and its length is `5` or more.
144 """
145 return attribute.startswith("on") and len(attribute) >= 5
148def is_embedded(node: Element) -> bool:
149 """Check to see if an element is an embedded element.
151 Embedded Elements:
153 * audio
154 * canvas
155 * embed
156 * iframe
157 * img
158 * MathML math
159 * object
160 * picture
161 * SVG svg
162 * video
164 Returns:
165 True if emedded
166 """
167 # audio,canvas,embed,iframe,img,MathML math,object,picture,SVG svg,video
169 return is_element(
170 node,
171 "audio",
172 "canvas",
173 "embed",
174 "iframe",
175 "img",
176 "math",
177 "object",
178 "picture",
179 "svg",
180 "video",
181 )
184def is_interactive(node: Element) -> bool:
185 """Check if the element is intended for user interaction.
187 Conditions:
189 * a (if the href attribute is present)
190 * audio (if the controls attribute is present)
191 * button, details, embed, iframe, img (if the usemap attribute is present)
192 * input (if the type attribute is not in the Hidden state)
193 * label, select, text, area, video (if the controls attribute is present)
195 Returns:
196 True if element is interactive
197 """
199 if is_element(node, "a"):
200 return "href" in node
202 if is_element(node, "input"):
203 return "type" in node and str(node["type"]).lower() != "hidden"
205 if is_element(node, "img"):
206 return "usemap" in node and node["usemap"] is True
208 if is_element(node, "video"):
209 return "controls" in node
211 if is_element(
212 node, "button", "details", "embed", "iframe", "label", "select", "textarea"
213 ):
214 return True
216 return False
219def is_phrasing(node: Element) -> bool:
220 """Check if a node is phrasing text according to
221 https://html.spec.whatwg.org/#phrasing-content-2.
223 Phrasing content is the text of the document, as well as elements that mark up that text at the
224 intra-paragraph level. Runs of phrasing content form paragraphs.
226 * area (if it is a descendant of a map element)
227 * link (if it is allowed in the body)
228 * meta (if the itemprop attribute is present)
229 * map, mark, math, audio, b, bdi, bdo, br, button, canvas, cite, code, data, datalist, del, dfn,
230 em, embed, i, iframe, img, input, ins, kbd, label, a, abbr, meter, noscript, object, output,
231 picture, progress, q, ruby, s, samp, script, select, slot, small, span, strong, sub, sup, svg,
232 template, textarea, time, u, var, video, wbr, text (true)
234 Returns:
235 True if the element is phrasing text
236 """
238 if Literal.is_text(node):
239 return True
241 if is_element(node, "area"):
242 return node.parent is not None and is_element(node.parent, "map")
244 if is_element(node, "meta"):
245 return "itemprop" in node
247 if is_element(node, "link"):
248 body_ok = [
249 "dns-prefetch",
250 "modulepreload",
251 "pingback",
252 "preconnect",
253 "prefetch",
254 "preload",
255 "prerender",
256 "stylesheet",
257 ]
259 return bool(
260 "itemprop" in node
261 or (
262 "rel" in node
263 and all(
264 token in body_ok
265 for token in str(node["rel"]).split(" ")
266 if token.strip() != ""
267 )
268 ),
269 )
271 if is_element(
272 node,
273 "node",
274 "map",
275 "mark",
276 "math",
277 "audio",
278 "b",
279 "bdi",
280 "bdo",
281 "br",
282 "button",
283 "canvas",
284 "cite",
285 "code",
286 "data",
287 "datalist",
288 "del",
289 "dfn",
290 "em",
291 "embed",
292 "i",
293 "iframe",
294 "img",
295 "input",
296 "ins",
297 "kbd",
298 "label",
299 "a",
300 "abbr",
301 "meter",
302 "noscript",
303 "object",
304 "output",
305 "picture",
306 "progress",
307 "q",
308 "ruby",
309 "s",
310 "samp",
311 "script",
312 "select",
313 "slot",
314 "small",
315 "span",
316 "strong",
317 "sub",
318 "sup",
319 "svg",
320 "template",
321 "textarea",
322 "time",
323 "u",
324 "var",
325 "video",
326 "wbr",
327 ):
328 return True
330 return False
333def blank(value: Any) -> bool:
334 """Takes any value type and returns whether it is blank/None.
335 For strings if the value is stripped and is equal to '' then it is blank.
336 Otherwise if len > 0 and is not None then not blank.
338 Args:
339 value (Any): The value to check if it is blank.
341 Returns:
342 bool: True if value is blank
343 """
345 if value is None or not hasattr(value, "__len__"):
346 return True
348 if isinstance(value, str):
349 value = value.strip()
351 return len(value) == 0