Coverage for phml\utilities\misc\classes.py: 100%
61 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
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
9from phml.nodes import Element
11__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 []