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 Optional
  9
 10from phml.core.nodes import NODE, Element
 11
 12__all__ = ["classnames", "ClassList"]
 13
 14
 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.
 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, conditionals = validate_node(node, conditionals)
 38
 39    classes = init_classes(node)
 40
 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}")
 68
 69    if node is None:
 70        return " ".join(classes)
 71
 72    node["class"] = " ".join(classes)
 73    return None
 74
 75
 76class ClassList:
 77    """Utility class to manipulate the class list on a node.
 78
 79    Based on the hast-util-class-list:
 80    https://github.com/brechtcs/hast-util-class-list
 81    """
 82
 83    def __init__(self, node: Element):
 84        self.node = node
 85        self.classes = node["class"].split(" ") if "class" in node.properties else []
 86
 87    def contains(self, klass: str):
 88        """Check if `class` contains a certain class."""
 89
 90        return klass.strip().replace(" ", "-") in self.classes
 91
 92    def toggle(self, *klasses: str):
 93        """Toggle a class in `class`."""
 94
 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(" ", "-"))
100
101        self.node["class"] = self.class_list()
102
103    def add(self, *klasses: str):
104        """Add one or more classes to `class`."""
105
106        for klass in klasses:
107            if klass not in self.classes:
108                self.classes.append(klass.strip().replace(" ", "-"))
109
110        self.node["class"] = self.class_list()
111
112    def replace(self, old_class: str, new_class: str):
113        """Replace a certain class in `class` with
114        another class.
115        """
116
117        old_class = old_class.strip().replace(" ", "-")
118        new_class = new_class.strip().replace(" ", "-")
119
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()
124
125    def remove(self, *klasses: str):
126        """Remove one or more classes from `class`."""
127
128        for klass in klasses:
129            if klass in self.classes:
130                self.classes.remove(klass)
131
132        if len(self.classes) == 0:
133            self.node.properties.pop("class", None)
134        else:
135            self.node["class"] = self.class_list()
136
137    def class_list(self) -> str:
138        """Return the formatted string of classes."""
139        return ' '.join(self.classes)
140
141
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, NODE):
145        return None, [node, *conditionals]
146
147    if not isinstance(node, Element):
148        raise TypeError("Node must be an element")
149
150    return node, conditionals
151
152
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(" ")
158
159        node["class"] = ""
160        return []
161
162    return []
def classnames( node: Optional[phml.core.nodes.nodes.Element] = None, *conditionals: str | int | list | dict[str, bool]) -> str:
16def classnames(  # pylint: disable=keyword-arg-before-vararg
17    node: Optional[Element] = None, *conditionals: str | int | list | dict[str, bool]
18) -> str:
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, conditionals = validate_node(node, conditionals)
39
40    classes = init_classes(node)
41
42    for condition in conditionals:
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                [klass for klass in classnames(*condition).split(" ") if klass not in classes]
66            )
67        else:
68            raise TypeError(f"Unkown conditional statement: {condition}")
69
70    if node is None:
71        return " ".join(classes)
72
73    node["class"] = " ".join(classes)
74    return None

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

Check if class contains a certain class.

def toggle(self, *klasses: str):
 93    def toggle(self, *klasses: str):
 94        """Toggle a class in `class`."""
 95
 96        for klass in klasses:
 97            if klass.strip().replace(" ", "-") in self.classes:
 98                self.classes.remove(klass.strip().replace(" ", "-"))
 99            else:
100                self.classes.append(klass.strip().replace(" ", "-"))
101
102        self.node["class"] = self.class_list()

Toggle a class in class.

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

Add one or more classes to class.

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

Replace a certain class in class with another class.

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

Remove one or more classes from class.

def class_list(self) -> str:
138    def class_list(self) -> str:
139        """Return the formatted string of classes."""
140        return ' '.join(self.classes)

Return the formatted string of classes.