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

1"""utils.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.nodes import All_Nodes, 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, All_Nodes): 

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 []