Coverage for phml\compiler\steps\components.py: 94%
144 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-12 14:26 -0500
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-12 14:26 -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 scoped_step, setup_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: str) -> 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@setup_step
73def step_add_cached_component_elements(node: AST, components: ComponentManager, _):
74 """Step to add the cached script and style elements from components."""
75 from phml.utilities import query
77 n_style = query(node, "head > style")
78 n_script = query(node, "head > script")
79 target = query(node, "head") or node
81 cache = components.get_cache()
82 style = ""
83 script = ""
84 for cmpt in cache:
85 style += f'\n{scope_styles(cache[cmpt]["styles"], cache[cmpt]["hash"])}'
87 scripts = "\n".join(
88 normalize_indent(s[0].content) for s in cache[cmpt]["scripts"]
89 )
90 script += f"\n{scripts}"
92 if len(style.strip()) > 0:
93 if n_style is not None:
94 if len(n_style) == 0 or not Literal.is_text(n_style[0]):
95 n_style.children = []
96 n_style.append(Literal(LiteralType.Text, ""))
97 n_style[0].content = normalize_indent(n_style[0].content) + style
98 else:
99 style = Element("style", children=[Literal(LiteralType.Text, style)])
100 target.append(style)
102 if len(script.strip()) > 0:
103 if n_script is not None:
104 if len(n_script) == 0 or not Literal.is_text(n_script[0]):
105 n_script.children = []
106 n_script.append(Literal(LiteralType.Text, ""))
107 n_script[0].content = normalize_indent(n_script[0].content) + script
108 else:
109 script = Element("script", children=[Literal(LiteralType.Text, script)])
110 target.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@scoped_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])