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
« 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
5from phml.components import ComponentManager
6from phml.helpers import iterate_nodes, normalize_indent
7from phml.nodes import AST, Element, Literal, LiteralType, Node, Parent
9from .base import boundry_step, comp_step
11re_selector = re.compile(r"(\n|\}| *)([^}@/]+)(\s*{)")
12re_split_selector = re.compile(r"(?:\)(?:.|\s)*|(?<!\()(?:.|\s)*)(,)")
15def lstrip(value: str) -> tuple[str, str]:
16 offset = len(value) - len(value.lstrip())
17 return value[:offset], value[offset:]
20def scope_style(style: str, scope: str) -> str:
21 """Takes a styles string and adds a scope to the selectors."""
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
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
44 for i, part in enumerate(parts):
45 w, s = lstrip(part)
46 parts[i] = w + f"{scope} {s}"
47 result += ",".join(parts) + trail
49 style = style[end:]
50 next_style = re_selector.search(style)
51 if len(style) > 0:
52 result += style
54 return result
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}']")
67 result.append(content)
69 return "\n".join(result)
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
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"])}'
93 scripts = "\n".join(
94 normalize_indent(s[0].content) for s in cache[cmpt]["scripts"]
95 )
96 script += f"\n{scripts}"
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)
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)
113class SlotNames(TypedDict):
114 __blank__: Node | None
115 named: dict[str, Node]
118class SlotChildren(TypedDict):
119 __blank__: list[Node]
120 named: dict[str, list[Node]]
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
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)
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__"])
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)
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."""
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"]}
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)
197 component = Element(
198 "div",
199 attributes={"data-phml-cmpt-scope": f"{components[child.tag]['hash']}"},
200 children=[],
201 )
203 for elem in elements:
204 elem.parent = component
205 if isinstance(elem, Element):
206 elem.context.update(context)
208 component.extend(elements)
210 if child.parent is not None:
211 idx = child.parent.index(child)
212 replace_slots(child, component)
213 child.parent[idx] = component
215 components.cache(child.tag, components[child.tag])