Coverage for phml\compiler\steps\components.py: 100%

141 statements  

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

1import re 

2from copy import deepcopy 

3from typing import Any, TypedDict 

4 

5from phml.components import ComponentManager 

6from phml.helpers import iterate_nodes, normalize_indent 

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

8 

9from .base import boundry_step, comp_step 

10 

11re_selector = re.compile(r"(\n|\}| *)([^}@/]+)(\s*{)") 

12re_split_selector = re.compile(r"(?:\)(?:.|\s)*|(?<!\()(?:.|\s)*)(,)") 

13 

14 

15def lstrip(value: str) -> tuple[str, str]: 

16 offset = len(value) - len(value.lstrip()) 

17 return value[:offset], value[offset:] 

18 

19 

20def scope_style(style: str, scope: str) -> str: 

21 """Takes a styles string and adds a scope to the selectors.""" 

22 

23 next_style = re_selector.search(style) 

24 result = "" 

25 while next_style is not None: 

26 start, end = next_style.start(), next_style.start() + len(next_style.group(0)) 

27 leading, selector, trail = next_style.groups() 

28 if start > 0: 

29 result += style[:start] 

30 result += leading 

31 

32 parts = [""] 

33 balance = 0 

34 for char in selector: 

35 if char == "," and balance == 0: 

36 parts.append("") 

37 continue 

38 elif char == "(": 

39 balance += 1 

40 elif char == ")": 

41 balance = min(0, balance - 1) 

42 parts[-1] += char 

43 

44 for i, part in enumerate(parts): 

45 w, s = lstrip(part) 

46 parts[i] = w + f"{scope} {s}" 

47 result += ",".join(parts) + trail 

48 

49 style = style[end:] 

50 next_style = re_selector.search(style) 

51 if len(style) > 0: 

52 result += style 

53 

54 return result 

55 

56 

57def scope_styles(styles: list[Element], hash: int) -> str: 

58 """Parse styles and find selectors with regex. When a selector is found then add scoped 

59 hashed data attribute to the selector. 

60 """ 

61 result = [] 

62 for style in styles: 

63 content = normalize_indent(style[0].content) 

64 if "scoped" in style: 

65 content = scope_style(content, f"[data-phml-cmpt-scope='{hash}']") 

66 

67 result.append(content) 

68 

69 return "\n".join(result) 

70 

71 

72@boundry_step 

73def step_add_cached_component_elements( 

74 node: AST, 

75 components: ComponentManager, 

76 _ 

77): 

78 """Step to add the cached script and style elements from components.""" 

79 target = None 

80 for child in node: 

81 if isinstance(child, Element) and child.tag == "html": 

82 target = child 

83 for c in child: 

84 if isinstance(c, Element) and c.tag == "head": 

85 target = c 

86 

87 cache = components.get_cache() 

88 style = "" 

89 script = "" 

90 for cmpt in cache: 

91 style += f'\n{scope_styles(cache[cmpt]["styles"], cache[cmpt]["hash"])}' 

92 

93 scripts = "\n".join( 

94 normalize_indent(s[0].content) for s in cache[cmpt]["scripts"] 

95 ) 

96 script += f"\n{scripts}" 

97 

98 if len(style.strip()) > 0: 

99 style = Element("style", children=[Literal(LiteralType.Text, style)]) 

100 if target is not None: 

101 target.append(style) 

102 else: 

103 node.append(style) 

104 

105 if len(script.strip()) > 0: 

106 script = Element("script", children=[Literal(LiteralType.Text, script)]) 

107 if target is not None: 

108 target.append(script) 

109 else: 

110 node.append(script) 

111 

112 

113class SlotNames(TypedDict): 

114 __blank__: Node | None 

115 named: dict[str, Node] 

116 

117 

118class SlotChildren(TypedDict): 

119 __blank__: list[Node] 

120 named: dict[str, list[Node]] 

121 

122 

123def replace_slots(child: Element, component: Element): 

124 slots: SlotNames = {"__blank__": None, "named": {}} 

125 for node in iterate_nodes(component): 

126 if isinstance(node, Element) and node.tag == "Slot": 

127 if "name" in node: 

128 name = str(node["name"]) 

129 if name in slots["named"]: 

130 raise ValueError( 

131 "Can not have more that one of the same named slot in a component" 

132 ) 

133 slots["named"][name] = node 

134 else: 

135 if slots["__blank__"] is not None: 

136 raise ValueError( 

137 "Can not have more that one catch all slot in a component" 

138 ) 

139 slots["__blank__"] = node 

140 

141 children: SlotChildren = {"__blank__": [], "named": {}} 

142 for node in child: 

143 if isinstance(node, Element) and "slot" in node: 

144 slot = str(node["slot"]) 

145 if slot not in children["named"]: 

146 children["named"][slot] = [] 

147 node.pop("slot", None) 

148 children["named"][slot].append(node) 

149 elif isinstance(node, Element): 

150 children["__blank__"].append(node) 

151 elif isinstance(node, Literal): 

152 children["__blank__"].append(node) 

153 

154 if slots["__blank__"] is not None: 

155 slot = slots["__blank__"] 

156 parent = slot.parent 

157 if parent is not None: 

158 idx = parent.index(slot) 

159 parent.remove(slot) 

160 parent.insert(idx, children["__blank__"]) 

161 

162 for slot in slots["named"]: 

163 node = slots["named"][slot] 

164 parent = node.parent 

165 if parent is not None: 

166 if slot in children["named"]: 

167 idx = parent.index(node) 

168 parent.remove(node) 

169 parent.insert(idx, children["named"][slot]) 

170 else: 

171 parent.remove(node) 

172 

173 

174@comp_step 

175def step_substitute_components( 

176 node: Parent, 

177 components: ComponentManager, 

178 context: dict[str, Any], 

179): 

180 """Step to substitute components in for matching nodes.""" 

181 

182 for child in node: 

183 if isinstance(child, Element) and child.tag in components: 

184 # Need a deep copy of the component as to not manipulate the cached comonent data 

185 elements = deepcopy(components[child.tag]["elements"]) 

186 props = components[child.tag]["props"] 

187 context = {**child.context, **components[child.tag]["context"]} 

188 

189 attrs = { 

190 key: value 

191 for key, value in child.attributes.items() 

192 if key.lstrip(":") in props 

193 } 

194 props.update(attrs) 

195 context.update(props) 

196 

197 component = Element( 

198 "div", 

199 attributes={"data-phml-cmpt-scope": f"{components[child.tag]['hash']}"}, 

200 children=[], 

201 ) 

202 

203 for elem in elements: 

204 elem.parent = component 

205 if isinstance(elem, Element): 

206 elem.context.update(context) 

207 

208 component.extend(elements) 

209 

210 if child.parent is not None: 

211 idx = child.parent.index(child) 

212 replace_slots(child, component) 

213 child.parent[idx] = component 

214 

215 components.cache(child.tag, components[child.tag])