Coverage for phml\builder.py: 100%

60 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-04-06 16:04 -0500

1"""phml.utilities.builder 

2 

3This module serves as a utility to make building elements and ast's easier. 

4""" 

5 

6from __future__ import annotations 

7 

8from typing import Literal as Lit 

9from typing import overload 

10 

11from phml.nodes import AST, Element, Literal, LiteralType, Node, Parent 

12 

13__all__ = ["p"] 

14 

15 

16def __process_children(node, children: list[str | list | int | Node]): 

17 for child in children: 

18 if isinstance(child, (str, float, int)): 

19 if ( 

20 isinstance(child, str) 

21 and child.startswith("<!--") 

22 and child.endswith("-->") 

23 ): 

24 child = child.strip() 

25 node.append( 

26 Literal(LiteralType.Comment, child.lstrip("<!--").rstrip("-->")) 

27 ) 

28 else: 

29 node.append(Literal(LiteralType.Text, str(child))) 

30 elif isinstance(child, Node): 

31 node.append(child) 

32 elif isinstance(child, list): 

33 for nested_child in child: 

34 if isinstance(nested_child, (str, float, int)): 

35 if ( 

36 isinstance(nested_child, str) 

37 and nested_child.startswith("<!--") 

38 and nested_child.endswith("-->") 

39 ): 

40 nested_child = nested_child.strip() 

41 node.append( 

42 Literal( 

43 LiteralType.Comment, 

44 nested_child.lstrip("<!--").rstrip("-->"), 

45 ) 

46 ) 

47 else: 

48 node.append(Literal(LiteralType.Text, str(nested_child))) 

49 elif isinstance(nested_child, Node): 

50 node.append(nested_child) 

51 else: 

52 raise TypeError( 

53 f"Unkown type <{type(nested_child).__name__}> in {child}:\ 

54 {nested_child}", 

55 ) 

56 

57 

58@overload 

59def p(selector: Node | None = None, *args: str | list | dict | int | Node) -> AST: 

60 ... 

61 

62 

63@overload 

64def p(selector: str, *args: str | list | dict | int | Node) -> Element: 

65 ... 

66 

67 

68@overload 

69def p(selector: Lit["text", "comment"], *args: str) -> Literal: 

70 ... 

71 

72 

73def p( # pylint: disable=[invalid-name,keyword-arg-before-vararg] 

74 selector: str | Node | None = None, 

75 *args: str | list | dict | int | Node | None, 

76) -> Node | AST | Parent: 

77 """Generic factory for creating phml nodes.""" 

78 

79 # Get all children | non dict objects 

80 children = [child for child in args if isinstance(child, (str, list, int, Node))] 

81 

82 # Get all properties | dict objects 

83 props = { 

84 key: value 

85 for prop in args 

86 if isinstance(prop, dict) 

87 for key, value in prop.items() 

88 } 

89 

90 if selector is not None: 

91 # Is a comment 

92 # if isinstance(selector, str) and selector.startswith("<!--"): 

93 # return Literal(LiteralType.Comment, selector.replace("<!--", "").replace("-->", "")) 

94 # Is a text node 

95 # if ( 

96 # isinstance(selector, str) 

97 # and (len(selector.split(" ")) > 1 or len(selector.split("\n")) > 1) 

98 # and len(args) == 0 

99 # ): 

100 # return Literal(LiteralType.Text, selector) 

101 if not isinstance(selector, str): 

102 args = (selector, *args) 

103 selector = None 

104 

105 children = [ 

106 child for child in args if isinstance(child, (str, list, int, Node)) 

107 ] 

108 return parse_root(children) 

109 return parse_node(selector, props, children) 

110 

111 return parse_root(children) 

112 

113 

114def parse_root(children: list): 

115 """From the given information return a built root node.""" 

116 

117 node = AST() 

118 __process_children(node, children) 

119 return node 

120 

121 

122def parse_node(selector: str, props: dict, children: list): 

123 """From the provided selector, props, and children build an element node.""" 

124 from phml.utilities import ( 

125 parse_specifiers, # pylint: disable=import-outside-toplevel 

126 ) 

127 

128 node = parse_specifiers(selector) 

129 if not isinstance(node[0], dict) or len(node[0]["attributes"]) > 0: 

130 raise TypeError("Selector must be of the format `tag?[#id]?[.classes...]?`") 

131 

132 node = node[0] 

133 

134 node["tag"] = "div" if node["tag"] == "*" else node["tag"] 

135 

136 if node["tag"].lower() == "doctype": 

137 return Element("doctype", {"html": True}) 

138 

139 if node["tag"].lower().strip() == "text": 

140 return Literal( 

141 LiteralType.Text, 

142 " ".join( 

143 [ 

144 str(child) 

145 for child in children 

146 if isinstance(child, (str, int, float)) 

147 ] 

148 ), 

149 ) 

150 if node["tag"].lower().strip() == "comment": 

151 return Literal( 

152 LiteralType.Comment, 

153 " ".join( 

154 [ 

155 str(child) 

156 for child in children 

157 if isinstance(child, (str, int, float)) 

158 ] 

159 ), 

160 ) 

161 

162 properties = {**props} 

163 

164 if len(node["classList"]) > 0: 

165 properties["class"] = "" if "class" not in properties else properties["class"] 

166 properties["class"] += " ".join(node["classList"]) 

167 if node["id"] is not None: 

168 properties["id"] = node["id"] 

169 

170 element = Element( 

171 node["tag"], 

172 attributes=properties, 

173 children=[] if len(children) > 0 else None, 

174 ) 

175 

176 __process_children(element, children) 

177 return element