Coverage for phml\utils\misc\classes.py: 91%
69 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-08 11:07 -0600
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-08 11:07 -0600
1"""utils.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 Optional
10from phml.nodes import All_Nodes, Element
12__all__ = ["classnames", "ClassList"]
15def classnames( # pylint: disable=keyword-arg-before-vararg
16 node: Optional[Element] = None, *conditionals: str | int | list | dict[str, bool]
17) -> str:
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, conditionals = validate_node(node, conditionals)
39 classes = init_classes(node)
41 for condition in conditionals:
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 [klass for klass in classnames(*condition).split(" ") if klass not in classes]
65 )
66 else:
67 raise TypeError(f"Unkown conditional statement: {condition}")
69 if node is None:
70 return " ".join(classes)
72 node["class"] = " ".join(classes)
73 return None
76class ClassList:
77 """Utility class to manipulate the class list on a node.
79 Based on the hast-util-class-list:
80 https://github.com/brechtcs/hast-util-class-list
81 """
83 def __init__(self, node: Element):
84 self.node = node
85 self.classes = node["class"].split(" ") if "class" in node.properties else []
87 def contains(self, klass: str):
88 """Check if `class` contains a certain class."""
90 return klass.strip().replace(" ", "-") in self.classes
92 def toggle(self, *klasses: str):
93 """Toggle a class in `class`."""
95 for klass in klasses:
96 if klass.strip().replace(" ", "-") in self.classes:
97 self.classes.remove(klass.strip().replace(" ", "-"))
98 else:
99 self.classes.append(klass.strip().replace(" ", "-"))
101 self.node["class"] = self.class_list()
103 def add(self, *klasses: str):
104 """Add one or more classes to `class`."""
106 for klass in klasses:
107 if klass not in self.classes:
108 self.classes.append(klass.strip().replace(" ", "-"))
110 self.node["class"] = self.class_list()
112 def replace(self, old_class: str, new_class: str):
113 """Replace a certain class in `class` with
114 another class.
115 """
117 old_class = old_class.strip().replace(" ", "-")
118 new_class = new_class.strip().replace(" ", "-")
120 if old_class in self.classes:
121 idx = self.classes.index(old_class)
122 self.classes[idx] = new_class
123 self.node["class"] = self.class_list()
125 def remove(self, *klasses: str):
126 """Remove one or more classes from `class`."""
128 for klass in klasses:
129 if klass in self.classes:
130 self.classes.remove(klass)
132 if len(self.classes) == 0:
133 self.node.properties.pop("class", None)
134 else:
135 self.node["class"] = self.class_list()
137 def class_list(self) -> str:
138 """Return the formatted string of classes."""
139 return ' '.join(self.classes)
142def validate_node(node, conditionals: list) -> bool:
143 """Validate a node is a node and that it is an element."""
144 if not isinstance(node, All_Nodes):
145 return None, [node, *conditionals]
147 if not isinstance(node, Element):
148 raise TypeError("Node must be an element")
150 return node, conditionals
153def init_classes(node) -> list[str]:
154 """Get the list of classes from an element."""
155 if node is not None:
156 if "class" in node.properties:
157 return sub(r" +", " ", node["class"]).split(" ")
159 node["class"] = ""
160 return []
162 return []