Coverage for phml\utilities\misc\classes.py: 100%
62 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-06 17:03 -0500
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-06 17:03 -0500
1"""utilities.misc
3A collection of utilities that don't fit in with finding, selecting, testing,
4transforming, traveling, or validating nodes.
5"""
7from re import split, sub
8from typing import overload
10from phml.nodes import Element, Node
12__all__ = ["classnames", "ClassList"]
14def classnames( # pylint: disable=keyword-arg-before-vararg
15 node: Element | None = None,
16 *conditionals: str | int | list | dict[str, bool] | Element,
17) -> str | None:
18 """Concat a bunch of class names. Can take a str as a class,
19 int which is cast to a str to be a class, a dict of conditional classes,
20 and a list of all the previous conditions including itself.
22 Examples:
23 Assume that the current class on node is `bold`
24 * `classnames(node, 'flex')` yields `'bold flex'`
25 * `classnames(node, 13)` yields `'bold 13'`
26 * `classnames(node, {'shadow': True, 'border': 0})` yields `'bold shadow'`
27 * `classnames('a', 13, {'b': True}, ['c', {'d': False}])` yields `'a b c'`
29 Args:
30 node (Element | None): Node to apply the classes too. If no node is given
31 then the function returns a string.
33 Returns:
34 str: The concat string of classes after processing.
35 """
37 node, conditions = validate_node(node, conditionals)
39 classes = init_classes(node)
41 for condition in conditions:
42 if isinstance(condition, str):
43 classes.extend(
44 [
45 klass
46 for klass in split(r" ", sub(r" +", "", condition.strip()))
47 if klass not in classes
48 ],
49 )
50 elif isinstance(condition, int) and str(condition) not in classes:
51 classes.append(str(condition))
52 elif isinstance(condition, dict):
53 for key, value in condition.items():
54 if value:
55 classes.extend(
56 [
57 klass
58 for klass in split(r" ", sub(r" +", "", key.strip()))
59 if klass not in classes
60 ],
61 )
62 elif isinstance(condition, list):
63 classes.extend(
64 [
65 klass
66 for klass in classnames(*condition).split(" ")
67 if klass not in classes
68 ],
69 )
70 else:
71 raise TypeError(f"Unkown conditional statement: {condition}")
73 if node is None:
74 return " ".join(classes)
76 node["class"] = " ".join(classes)
77 return ""
80class ClassList:
81 """Utility class to manipulate the class list on a node.
83 Based on the hast-util-class-list:
84 https://github.com/brechtcs/hast-util-class-list
85 """
87 def __init__(self, node: Element) -> None:
88 self.node = node
89 self._classes = str(node["class"]).split(" ") if "class" in node else []
91 def __contains__(self, klass: str) -> bool:
92 return klass.strip().replace(" ", "-") in self._classes
94 def toggle(self, *klasses: str):
95 """Toggle a class in `class`."""
97 for klass in klasses:
98 if klass.strip().replace(" ", "-") in self._classes:
99 self._classes.remove(klass.strip().replace(" ", "-"))
100 else:
101 self._classes.append(klass.strip().replace(" ", "-"))
103 self.node["class"] = self.classes
105 def add(self, *klasses: str):
106 """Add one or more classes to `class`."""
108 for klass in klasses:
109 if klass not in self._classes:
110 self._classes.append(klass.strip().replace(" ", "-"))
112 self.node["class"] = self.classes
114 def replace(self, old_class: str, new_class: str):
115 """Replace a certain class in `class` with
116 another class.
117 """
119 old_class = old_class.strip().replace(" ", "-")
120 new_class = new_class.strip().replace(" ", "-")
122 if old_class in self._classes:
123 idx = self._classes.index(old_class)
124 self._classes[idx] = new_class
125 self.node["class"] = self.classes
127 def remove(self, *klasses: str):
128 """Remove one or more classes from `class`."""
130 for klass in klasses:
131 if klass in self._classes:
132 self._classes.remove(klass)
134 if len(self._classes) == 0:
135 self.node.attributes.pop("class", None)
136 else:
137 self.node["class"] = self.classes
139 @property
140 def classes(self) -> str:
141 """Return the formatted string of classes."""
142 return " ".join(self._classes)
145def validate_node(
146 node: Element | None, conditionals: tuple
147) -> tuple[Element | None, tuple]:
148 """Validate a node is a node and that it is an element."""
150 if isinstance(node, (str , int , list , dict)):
151 return None, (node, *conditionals)
153 if not isinstance(node, Element):
154 raise TypeError("Node must be an element")
156 return node, conditionals
159def init_classes(node) -> list[str]:
160 """Get the list of classes from an element."""
161 if node is not None:
162 if "class" in node.attributes:
163 return sub(r" +", " ", node["class"]).split(" ")
165 node["class"] = ""
166 return []
168 return []