Coverage for phml\builder.py: 44%

105 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-30 09:38 -0600

1from __future__ import annotations 

2 

3from typing import Optional 

4 

5from phml.nodes import * 

6 

7 

8def p( 

9 selector: Optional[str] = None, 

10 *args: str | list | int | All_Nodes, 

11): 

12 def __process_children(node, children: list[str | list | int | All_Nodes]): 

13 for child in children: 

14 if isinstance(child, str): 

15 node.children.append(Text(child, node)) 

16 elif isinstance(child, int): 

17 node.children.append(Text(str(child), node)) 

18 elif isinstance(child, All_Nodes): 

19 child.parent = node 

20 node.children.append(child) 

21 elif isinstance(child, list): 

22 for c in child: 

23 if isinstance(c, str): 

24 node.children.append(Text(c, node)) 

25 elif isinstance(c, int): 

26 node.children.append(Text(str(c), node)) 

27 elif isinstance(c, All_Nodes): 

28 c.parent = node 

29 node.children.append(c) 

30 else: 

31 raise TypeError(f"Unkown type <{type(c).__name__}> in {child}: {c}") 

32 

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

34 return Comment(selector.replace("<!--", "").replace("-->", "")) 

35 if selector is not None and ( 

36 not isinstance(selector, str) or len(selector.replace("\n", " ").split(" ")) > 1 

37 ): 

38 if ( 

39 isinstance(selector, str) 

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

41 and len(args) == 0 

42 ): 

43 return Text(selector) 

44 args = [selector, *args] 

45 selector = None 

46 

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

48 props = [prop for prop in args if isinstance(prop, dict)] 

49 

50 if selector is not None: 

51 node = __parse_specifiers(selector) 

52 if len(node) > 1: 

53 raise Exception("Selector can not be a complex selector") 

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

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

56 

57 node = node[0] 

58 

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

60 

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

62 str_children = [child for child in children if isinstance(child, str)] 

63 if len(str_children) > 0: 

64 return DocType(str_children[0]) 

65 else: 

66 return DocType() 

67 elif node["tag"].lower() == "text": 

68 return Text(" ".join([child for child in children if isinstance(child, str)])) 

69 else: 

70 properties = {} 

71 for prop in props: 

72 properties.update(prop) 

73 

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

75 properties["class"] = properties["class"] or "" 

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

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

78 properties["id"] = node["id"] 

79 

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

81 

82 node = Element( 

83 node["tag"], 

84 properties=properties, 

85 startend=len(children) == 0, 

86 ) 

87 else: 

88 node = Root() 

89 

90 if len(children) > 0: 

91 __process_children(node, children) 

92 

93 return node 

94 

95 

96def __parse_specifiers(specifier: str) -> dict: 

97 """ 

98 Rules: 

99 * `*` = any element 

100 * `>` = Everything with certain parent child relationship 

101 * `+` = first sibling 

102 * `~` = All after 

103 * `.` = class 

104 * `#` = id 

105 * `[attribute]` = all elements with attribute 

106 * `[attribute=value]` = all elements with attribute=value 

107 * `[attribute~=value]` = all elements with attribute containing value 

108 * `[attribute|=value]` = all elements with attribute=value or attribute starting with value- 

109 * `node[attribute^=value]` = all elements with attribute starting with value 

110 * `node[attribute$=value]` = all elements with attribute ending with value 

111 * `node[attribute*=value]` = all elements with attribute containing value 

112 

113 """ 

114 from re import compile 

115 

116 splitter = compile(r"([~>*+])|(([.#]?[a-zA-Z0-9_-]+)+((\[[^\[\]]+\]))*)|(\[[^\[\]]+\])+") 

117 

118 el_with_attr = compile(r"([.#]?[a-zA-Z0-9_-]+)+(\[[^\[\]]+\])*") 

119 el_only_attr = compile(r"((\[[^\[\]]+\]))+") 

120 

121 el_classid_from_attr = compile(r"([a-zA-Z0-9_#.-]+)((\[.*\])*)") 

122 el_from_class_from_id = compile(r"(#|\.)?([a-zA-Z0-9_-]+)") 

123 attr_compare_val = compile(r"\[([a-zA-Z0-9_-]+)([~|^$*]?=)?(\"[^\"]+\"|'[^']+'|[^'\"]+)?\]") 

124 

125 tokens = [] 

126 for token in splitter.finditer(specifier): 

127 

128 if token in ["*", ">", "+", "~"]: 

129 tokens.append(token.group()) 

130 elif el_with_attr.match(token.group()): 

131 element = { 

132 "tag": None, 

133 "classList": [], 

134 "id": None, 

135 "attributes": [], 

136 } 

137 

138 res = el_classid_from_attr.match(token.group()) 

139 

140 el_class_id, attrs = res.group(1), res.group(2) 

141 

142 if attrs not in ["", None]: 

143 for attr in attr_compare_val.finditer(attrs): 

144 name, compare, value = attr.groups() 

145 if value is not None: 

146 value = value.lstrip("'\"").rstrip("'\"") 

147 element["attributes"].append( 

148 { 

149 "name": name, 

150 "compare": compare, 

151 "value": value, 

152 } 

153 ) 

154 

155 if el_class_id not in ["", None]: 

156 for item in el_from_class_from_id.finditer(el_class_id): 

157 if item.group(1) == ".": 

158 if item.group(2) not in element["classList"]: 

159 element["classList"].append(item.group(2)) 

160 elif item.group(1) == "#": 

161 if element["id"] is None: 

162 element["id"] = item.group(2) 

163 else: 

164 raise Exception( 

165 f"There may only be one id per element specifier.\n{token.group()}" 

166 ) 

167 else: 

168 element["tag"] = item.group(2) 

169 

170 tokens.append(element) 

171 elif el_only_attr.match(token.group()): 

172 element = { 

173 "tag": None, 

174 "classList": [], 

175 "id": None, 

176 "attributes": [], 

177 } 

178 

179 element["tag"] = "*" 

180 

181 if token.group() not in ["", None]: 

182 for attr in attr_compare_val.finditer(token.group()): 

183 name, compare, value = attr.groups() 

184 if value is not None: 

185 value = value.lstrip("'\"").rstrip("'\"") 

186 element["attributes"].append( 

187 { 

188 "name": name, 

189 "compare": compare, 

190 "value": value, 

191 } 

192 ) 

193 

194 tokens.append(element) 

195 

196 return tokens