phml.utilities.misc.classes

utilities.misc

A collection of utilities that don't fit in with finding, selecting, testing, transforming, traveling, or validating nodes.

  1"""utilities.misc
  2
  3A collection of utilities that don't fit in with finding, selecting, testing,
  4transforming, traveling, or validating nodes.
  5"""
  6
  7from re import split, sub
  8from typing import overload
  9
 10from phml.nodes import Element, Node
 11
 12__all__ = ["classnames", "ClassList"]
 13
 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.
 21
 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'`
 28
 29    Args:
 30        node (Element | None): Node to apply the classes too. If no node is given
 31        then the function returns a string.
 32
 33    Returns:
 34        str: The concat string of classes after processing.
 35    """
 36
 37    node, conditions = validate_node(node, conditionals)
 38
 39    classes = init_classes(node)
 40
 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}")
 72
 73    if node is None:
 74        return " ".join(classes)
 75
 76    node["class"] = " ".join(classes)
 77    return ""
 78
 79
 80class ClassList:
 81    """Utility class to manipulate the class list on a node.
 82
 83    Based on the hast-util-class-list:
 84    https://github.com/brechtcs/hast-util-class-list
 85    """
 86
 87    def __init__(self, node: Element) -> None:
 88        self.node = node
 89        self._classes = str(node["class"]).split(" ") if "class" in node else []
 90
 91    def __contains__(self, klass: str) -> bool:
 92        return klass.strip().replace(" ", "-") in self._classes
 93
 94    def toggle(self, *klasses: str):
 95        """Toggle a class in `class`."""
 96
 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(" ", "-"))
102
103        self.node["class"] = self.classes
104
105    def add(self, *klasses: str):
106        """Add one or more classes to `class`."""
107
108        for klass in klasses:
109            if klass not in self._classes:
110                self._classes.append(klass.strip().replace(" ", "-"))
111
112        self.node["class"] = self.classes
113
114    def replace(self, old_class: str, new_class: str):
115        """Replace a certain class in `class` with
116        another class.
117        """
118
119        old_class = old_class.strip().replace(" ", "-")
120        new_class = new_class.strip().replace(" ", "-")
121
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
126
127    def remove(self, *klasses: str):
128        """Remove one or more classes from `class`."""
129
130        for klass in klasses:
131            if klass in self._classes:
132                self._classes.remove(klass)
133
134        if len(self._classes) == 0:
135            self.node.attributes.pop("class", None)
136        else:
137            self.node["class"] = self.classes
138
139    @property
140    def classes(self) -> str:
141        """Return the formatted string of classes."""
142        return " ".join(self._classes)
143
144
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."""
149
150    if isinstance(node, (str , int , list , dict)):
151        return None, (node, *conditionals)
152    
153    if not isinstance(node, Element):
154        raise TypeError("Node must be an element")
155
156    return node, conditionals
157
158
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(" ")
164
165        node["class"] = ""
166        return []
167
168    return []
def classnames( node: phml.nodes.Element | None = None, *conditionals: str | int | list | dict[str, bool] | phml.nodes.Element) -> str | None:
15def classnames(  # pylint: disable=keyword-arg-before-vararg
16    node: Element | None = None,
17    *conditionals: str | int | list | dict[str, bool] | Element,
18) -> str | None:
19    """Concat a bunch of class names. Can take a str as a class,
20    int which is cast to a str to be a class, a dict of conditional classes,
21    and a list of all the previous conditions including itself.
22
23    Examples:
24        Assume that the current class on node is `bold`
25    * `classnames(node, 'flex')` yields `'bold flex'`
26    * `classnames(node, 13)` yields `'bold 13'`
27    * `classnames(node, {'shadow': True, 'border': 0})` yields `'bold shadow'`
28    * `classnames('a', 13, {'b': True}, ['c', {'d': False}])` yields `'a b c'`
29
30    Args:
31        node (Element | None): Node to apply the classes too. If no node is given
32        then the function returns a string.
33
34    Returns:
35        str: The concat string of classes after processing.
36    """
37
38    node, conditions = validate_node(node, conditionals)
39
40    classes = init_classes(node)
41
42    for condition in conditions:
43        if isinstance(condition, str):
44            classes.extend(
45                [
46                    klass
47                    for klass in split(r" ", sub(r" +", "", condition.strip()))
48                    if klass not in classes
49                ],
50            )
51        elif isinstance(condition, int) and str(condition) not in classes:
52            classes.append(str(condition))
53        elif isinstance(condition, dict):
54            for key, value in condition.items():
55                if value:
56                    classes.extend(
57                        [
58                            klass
59                            for klass in split(r" ", sub(r" +", "", key.strip()))
60                            if klass not in classes
61                        ],
62                    )
63        elif isinstance(condition, list):
64            classes.extend(
65                [
66                    klass
67                    for klass in classnames(*condition).split(" ")
68                    if klass not in classes
69                ],
70            )
71        else:
72            raise TypeError(f"Unkown conditional statement: {condition}")
73
74    if node is None:
75        return " ".join(classes)
76
77    node["class"] = " ".join(classes)
78    return ""

Concat a bunch of class names. Can take a str as a class, int which is cast to a str to be a class, a dict of conditional classes, and a list of all the previous conditions including itself.

Examples

Assume that the current class on node is bold

  • classnames(node, 'flex') yields 'bold flex'
  • classnames(node, 13) yields 'bold 13'
  • classnames(node, {'shadow': True, 'border': 0}) yields 'bold shadow'
  • classnames('a', 13, {'b': True}, ['c', {'d': False}]) yields 'a b c'
Args
  • node (Element | None): Node to apply the classes too. If no node is given
  • then the function returns a string.
Returns

str: The concat string of classes after processing.

class ClassList:
 81class ClassList:
 82    """Utility class to manipulate the class list on a node.
 83
 84    Based on the hast-util-class-list:
 85    https://github.com/brechtcs/hast-util-class-list
 86    """
 87
 88    def __init__(self, node: Element) -> None:
 89        self.node = node
 90        self._classes = str(node["class"]).split(" ") if "class" in node else []
 91
 92    def __contains__(self, klass: str) -> bool:
 93        return klass.strip().replace(" ", "-") in self._classes
 94
 95    def toggle(self, *klasses: str):
 96        """Toggle a class in `class`."""
 97
 98        for klass in klasses:
 99            if klass.strip().replace(" ", "-") in self._classes:
100                self._classes.remove(klass.strip().replace(" ", "-"))
101            else:
102                self._classes.append(klass.strip().replace(" ", "-"))
103
104        self.node["class"] = self.classes
105
106    def add(self, *klasses: str):
107        """Add one or more classes to `class`."""
108
109        for klass in klasses:
110            if klass not in self._classes:
111                self._classes.append(klass.strip().replace(" ", "-"))
112
113        self.node["class"] = self.classes
114
115    def replace(self, old_class: str, new_class: str):
116        """Replace a certain class in `class` with
117        another class.
118        """
119
120        old_class = old_class.strip().replace(" ", "-")
121        new_class = new_class.strip().replace(" ", "-")
122
123        if old_class in self._classes:
124            idx = self._classes.index(old_class)
125            self._classes[idx] = new_class
126            self.node["class"] = self.classes
127
128    def remove(self, *klasses: str):
129        """Remove one or more classes from `class`."""
130
131        for klass in klasses:
132            if klass in self._classes:
133                self._classes.remove(klass)
134
135        if len(self._classes) == 0:
136            self.node.attributes.pop("class", None)
137        else:
138            self.node["class"] = self.classes
139
140    @property
141    def classes(self) -> str:
142        """Return the formatted string of classes."""
143        return " ".join(self._classes)

Utility class to manipulate the class list on a node.

Based on the hast-util-class-list: https://github.com/brechtcs/hast-util-class-list

ClassList(node: phml.nodes.Element)
88    def __init__(self, node: Element) -> None:
89        self.node = node
90        self._classes = str(node["class"]).split(" ") if "class" in node else []
def toggle(self, *klasses: str):
 95    def toggle(self, *klasses: str):
 96        """Toggle a class in `class`."""
 97
 98        for klass in klasses:
 99            if klass.strip().replace(" ", "-") in self._classes:
100                self._classes.remove(klass.strip().replace(" ", "-"))
101            else:
102                self._classes.append(klass.strip().replace(" ", "-"))
103
104        self.node["class"] = self.classes

Toggle a class in class.

def add(self, *klasses: str):
106    def add(self, *klasses: str):
107        """Add one or more classes to `class`."""
108
109        for klass in klasses:
110            if klass not in self._classes:
111                self._classes.append(klass.strip().replace(" ", "-"))
112
113        self.node["class"] = self.classes

Add one or more classes to class.

def replace(self, old_class: str, new_class: str):
115    def replace(self, old_class: str, new_class: str):
116        """Replace a certain class in `class` with
117        another class.
118        """
119
120        old_class = old_class.strip().replace(" ", "-")
121        new_class = new_class.strip().replace(" ", "-")
122
123        if old_class in self._classes:
124            idx = self._classes.index(old_class)
125            self._classes[idx] = new_class
126            self.node["class"] = self.classes

Replace a certain class in class with another class.

def remove(self, *klasses: str):
128    def remove(self, *klasses: str):
129        """Remove one or more classes from `class`."""
130
131        for klass in klasses:
132            if klass in self._classes:
133                self._classes.remove(klass)
134
135        if len(self._classes) == 0:
136            self.node.attributes.pop("class", None)
137        else:
138            self.node["class"] = self.classes

Remove one or more classes from class.

classes: str

Return the formatted string of classes.