phml.core.formats.compile.component
1from __future__ import annotations 2 3from copy import deepcopy 4from re import sub 5from phml.core.nodes import Root, Element, AST, Text 6from phml.core.virtual_python import VirtualPython, process_python_blocks, get_python_result 7from phml.utilities import find, find_all, offset, normalize_indent, query, replace_node, check 8 9from .compile import py_condition, CONDITION_PREFIX, valid_prev 10 11__all__ = ["substitute_component", "replace_components", "combine_component_elements"] 12 13 14WRAPPER_TAG = ["template", ""] 15 16def substitute_component( 17 node: Root | Element | AST, 18 component: tuple[str, dict], 19 virtual_python: VirtualPython, 20 **kwargs, 21): 22 """Replace the first occurance of a component. 23 24 Args: 25 node (Root | Element | AST): The starting point. 26 virtual_python (VirtualPython): The python state to use while evaluating prop values 27 """ 28 29 if isinstance(node, AST): 30 node = node.tree 31 32 curr_node = find(node, ["element", {"tag": component[0]}]) 33 used_components: dict[str, tuple[dict, dict]] = {} 34 35 if curr_node is not None: 36 context, used_components[component[0]] = process_context(*component, kwargs) 37 cmpt_props = used_components[component[0]][1].get("Props", None) 38 39 # Assign props to locals and remaining attributes stay 40 curr_node.parent.children = apply_component( 41 curr_node, 42 component[0], 43 component[1], 44 cmpt_props, 45 virtual_python, 46 context, 47 kwargs 48 ) 49 50 __add_component_elements(node, used_components, "style") 51 __add_component_elements(node, used_components, "script") 52 53def replace_components( 54 node: Root | Element | AST, 55 components: dict[str, dict], 56 virtual_python: VirtualPython, 57 **kwargs, 58): 59 """Iterate through components and replace all of each component in the nodes children. 60 Non-recursive. 61 62 Args: 63 node (Root | Element | AST): The starting point. 64 virtual_python (VirtualPython): Temp 65 """ 66 67 if isinstance(node, AST): 68 node = node.tree 69 70 used_components: dict[str, tuple[dict, dict]] = {} 71 72 for name, value in components.items(): 73 elements = [element for element in node.children if check(element, {"tag": name})] 74 75 context = {} 76 cmpt_props = None 77 if len(elements) > 0: 78 if components[name]["cache"] is not None: 79 context.update({ 80 key:value 81 for key,value in components[name]["cache"][1].items() 82 if key != "Props" 83 }) 84 used_components[name] = components[name]["cache"] 85 context.update(kwargs) 86 else: 87 context, used_components[name] = process_context(name, value["data"], kwargs) 88 components[name]["cache"] = ( 89 used_components[name][0], 90 { 91 key:value 92 for key,value in used_components[name][1].items() 93 if key not in kwargs 94 } 95 ) 96 97 cmpt_props = components[name]["cache"][1].get("Props", None) 98 99 if "Props" in context and "data" in context["Props"]: 100 print(context["Props"], kwargs) 101 102 for curr_node in elements: 103 curr_node.parent.children = apply_component( 104 curr_node, 105 name, 106 value["data"], 107 cmpt_props, 108 virtual_python, 109 context, 110 kwargs 111 ) 112 113 # Optimize, python, style, and script tags from components 114 __add_component_elements(node, used_components, "style") 115 __add_component_elements(node, used_components, "script") 116 117def get_props( 118 node, 119 name, 120 value, 121 virtual_python, 122 props: dict | None = None, 123 **kwargs 124) -> dict[str, str]: 125 """Extract props from a phml component.""" 126 props = dict(props or {}) 127 extra_props = {} 128 attrs = value["component"].properties 129 130 attributes = node.properties 131 for item in attributes: 132 attr_name = item.lstrip(":") 133 if attr_name.startswith("py-"): 134 attr_name = attr_name.lstrip("py-") 135 136 if attr_name in props: 137 # Get value if pythonic 138 context = build_locals(node, **kwargs) 139 if item.startswith((":", "py-")): 140 # process as python 141 context.update(virtual_python.context) 142 result = get_python_result(attributes[item], **context) 143 else: 144 # process python blocks 145 result = process_python_blocks( 146 attributes[item], 147 virtual_python, 148 **context 149 ) 150 if ( 151 isinstance(result, str) 152 and result.lower() in ["true", "false", "yes", "no"] 153 ): 154 result = True if result.lower() in ["true", "yes"] else False 155 props[attr_name] = result 156 elif attr_name not in attrs and item not in attrs: 157 # Add value to attributes 158 if ( 159 isinstance(attributes[item], str) 160 and attributes[item].lower() in ["true", "false", "yes", "no"] 161 ): 162 attributes[item] = True if attributes[item].lower() in ["true", "yes"] else False 163 extra_props[attr_name] = attributes[item] 164 165 if len(extra_props) > 0: 166 props["props"] = extra_props 167 168 return props, attrs 169 170def execute_condition( 171 condition: str, 172 child: Element, 173 virtual_python: VirtualPython, 174 **kwargs, 175) -> Element|None: 176 """Execute python conditions for node to determine what will happen with the component.""" 177 conditions = __get_previous_conditions(child) 178 179 first_cond = ( 180 conditions[0] in [f"{CONDITION_PREFIX}if"] 181 if len(conditions) > 0 else False 182 ) 183 184 previous = (conditions[-1] if len(conditions) > 0 else f"{CONDITION_PREFIX}else", True) 185 186 # Add the python blocks locals to kwargs dict 187 kwargs.update(virtual_python.context) 188 189 # Bring python blocks imports into scope 190 for imp in virtual_python.imports: 191 exec(str(imp)) # pylint: disable=exec-used 192 193 # For each element with a python condition 194 if condition == f"{CONDITION_PREFIX}if": 195 child = run_phml_if(child, condition, **kwargs) 196 return child 197 198 if condition == f"{CONDITION_PREFIX}elif": 199 # Can only exist if previous condition in branch failed 200 child = run_phml_elif( 201 child, 202 condition, 203 { 204 "previous": previous, 205 "valid_prev": valid_prev, 206 "first_cond": first_cond, 207 }, 208 **kwargs, 209 ) 210 return child 211 212 if condition == f"{CONDITION_PREFIX}else": 213 214 # Can only exist if previous condition in branch failed 215 child = run_phml_else( 216 child, 217 condition, 218 { 219 "previous": previous, 220 "valid_prev": valid_prev, 221 "first_cond": first_cond, 222 }, 223 **kwargs 224 ) 225 return child 226 return None 227 228def process_context(name, value, kwargs: dict | None = None): 229 """Process the python elements and context of the component and extract the relavent context.""" 230 context = {} 231 local_virtual_python = VirtualPython(context=dict(kwargs or {})) 232 for python in value["python"]: 233 if len(python.children) == 1 and check(python.children[0], "text"): 234 text = python.children[0].normalized() 235 local_virtual_python += VirtualPython(text, context=local_virtual_python.context) 236 237 if "Props" in local_virtual_python.context: 238 if not isinstance(local_virtual_python.context["Props"], dict): 239 raise Exception( 240 "Props must be a dict was " 241 + f"{type(local_virtual_python.context['Props']).__name__}: <{name} />" 242 ) 243 244 context.update({ 245 key:value 246 for key,value in local_virtual_python.context.items() 247 if key != "Props" 248 }) 249 250 return context, (value, local_virtual_python.context) 251 252def apply_component(node, name, value, cmpt_props, virtual_python, context, kwargs) -> list: 253 """Get the props, execute conditions and replace components in the node tree.""" 254 props, attrs = get_props( 255 node, 256 name, 257 value, 258 virtual_python, 259 cmpt_props, 260 **kwargs 261 ) 262 263 node.properties = attrs 264 node.context.update(props) 265 266 condition = py_condition(node) 267 result = None 268 if condition is not None: 269 result = execute_condition(condition, node, virtual_python, **kwargs) 270 271 # replace the valid components in the results list 272 new_children = [] 273 # get props and locals from current node 274 properties, attributes = node.context, result.properties if result is not None else node.properties 275 properties.update(context) 276 # properties["children"] = node.children 277 278 component = deepcopy(value["component"]) 279 280 if len(node.children) > 0: 281 slots: dict[str, tuple[Element, list]] = get_slots(component) 282 for child in node.children: 283 if isinstance(child, Element) and "slot" in child: 284 if child.get("slot") in slots: 285 slots[child["slot"]][1].append(child) 286 if isinstance(child, Element): 287 child.parent = slots[child["slot"]][0] 288 del child["slot"] 289 elif "" in slots: 290 if isinstance(child, Element): 291 child.parent = slots[""][0] 292 del child["slot"] 293 slots[""][1].append(child) 294 295 for slot in slots.values(): 296 replace_node( 297 component, 298 slot[0], 299 slot[1] if len(slot[1]) > 0 else None 300 ) 301 302 if component.tag in WRAPPER_TAG: 303 # Create a copy of the component 304 for sub_child in component.children: 305 if isinstance(sub_child, Element): 306 sub_child.context.update(properties) 307 sub_child.parent = node.parent 308 309 new_children.extend(component.children) 310 else: 311 component.context = properties 312 component.properties = attributes 313 component.parent = node.parent 314 new_children.append(component) 315 316 # replace the curr_node with the list of replaced nodes 317 parent = node.parent 318 index = parent.children.index(node) 319 return parent.children[:index] + new_children + parent.children[index+1:] 320 321def get_slots(node: Element) -> dict[str, tuple[Element, list]]: 322 """Get the slots found recursively in a Element.""" 323 324 slots = {} 325 for slot in find_all(node, {"tag": "Slot"}): 326 name = slot.get("name", "") 327 if name == "": 328 if "" in slots: 329 raise KeyError("Can only have one slot without a name: <Slot />") 330 slots[""] = (slot, []) 331 else: 332 if name in slots: 333 raise KeyError("Must have unique slot names in components: <Slot name='unique' />") 334 slots[name] = (slot, []) 335 return slots 336 337def __add_component_elements(node, used_components: dict, tag: str): 338 if find(node, {"tag": tag}) is not None: 339 new_elements = __retrieve_component_elements(used_components, tag) 340 if len(new_elements) > 0: 341 replace_node( 342 node, 343 {"tag": tag}, 344 combine_component_elements( 345 [ 346 find(node, {"tag": tag}), 347 *new_elements, 348 ], 349 tag, 350 ), 351 ) 352 else: 353 new_element = combine_component_elements( 354 __retrieve_component_elements(used_components, tag), 355 tag, 356 ) 357 if new_element.children[0].value.strip() != "": 358 if tag == "style": 359 head = query(node, "head") 360 if head is not None: 361 head.append(new_element) 362 else: 363 node.append(new_element) 364 else: 365 html = query(node, "html") 366 if html is not None: 367 html.append(new_element) 368 else: 369 node.append(new_element) 370 371 372def combine_component_elements(elements: list[Element], tag: str) -> Element: 373 """Combine text from elements like python, script, and style. 374 375 Returns: 376 Element: With tag of element list but with combined text content 377 """ 378 379 values = [] 380 381 indent = -1 382 for element in elements: 383 if len(element.children) == 1 and isinstance(element.children[0], Text): 384 # normalize values 385 if indent == -1: 386 indent = offset(element.children[0].value) 387 values.append(normalize_indent(element.children[0].value, indent)) 388 389 return Element(tag, children=[Text("\n\n".join(values))]) 390 391 392def __retrieve_component_elements(collection: dict, element: str) -> list[Element]: 393 result = [] 394 for value in collection.values(): 395 if element in value[0]: 396 result.extend(value[0][element]) 397 return result 398 399def run_phml_if(child: Element, condition: str, **kwargs): 400 """Run the logic for manipulating the children on a `if` condition.""" 401 402 clocals = build_locals(child, **kwargs) 403 result = get_python_result(sub(r"\{|\}", "", child[condition].strip()), **clocals) 404 405 if result: 406 return child 407 408 # Condition failed, so remove the node 409 return child 410 411 412def run_phml_elif( 413 child: Element, 414 condition: str, 415 variables: dict, 416 **kwargs, 417): 418 """Run the logic for manipulating the children on a `elif` condition.""" 419 420 clocals = build_locals(child, **kwargs) 421 422 if variables["previous"][0] in variables["valid_prev"][condition] and variables["first_cond"]: 423 if not variables["previous"][1]: 424 result = get_python_result(sub(r"\{|\}", "", child[condition].strip()), **clocals) 425 if result: 426 return child 427 428 return child 429 430 431def run_phml_else(child: Element, condition: str, variables: dict, **kwargs): 432 """Run the logic for manipulating the children on a `else` condition.""" 433 434 if variables["previous"][0] in variables["valid_prev"][condition] and variables["first_cond"]: 435 if not variables["previous"][1]: 436 clocals = build_locals(child, **kwargs) 437 result = get_python_result(sub(r"\{|\}", "", child[condition].strip()), **clocals) 438 if result: 439 return child 440 441 # Condition failed so remove element 442 return child 443 444def build_locals(child, **kwargs) -> dict: 445 """Build a dictionary of local variables from a nodes inherited locals and 446 the passed kwargs. 447 """ 448 from phml.utilities import path # pylint: disable=import-outside-toplevel 449 450 clocals = {**kwargs} 451 452 # Inherit locals from top down 453 for parent in path(child): 454 if parent.type == "element": 455 clocals.update(parent.context) 456 457 clocals.update(child.context) 458 return clocals 459 460def __get_previous_conditions(child: Element) -> list[str]: 461 idx = child.parent.children.index(child) 462 conditions = [] 463 for i in range(0, idx): 464 if isinstance(child.parent.children[i], Element): 465 condition = py_condition(child.parent.children[i]) 466 if condition is not None: 467 conditions.append(condition) 468 469 return conditions
def
substitute_component( node: phml.core.nodes.nodes.Root | phml.core.nodes.nodes.Element | phml.core.nodes.AST.AST, component: tuple[str, dict], virtual_python: phml.core.virtual_python.vp.VirtualPython, **kwargs):
17def substitute_component( 18 node: Root | Element | AST, 19 component: tuple[str, dict], 20 virtual_python: VirtualPython, 21 **kwargs, 22): 23 """Replace the first occurance of a component. 24 25 Args: 26 node (Root | Element | AST): The starting point. 27 virtual_python (VirtualPython): The python state to use while evaluating prop values 28 """ 29 30 if isinstance(node, AST): 31 node = node.tree 32 33 curr_node = find(node, ["element", {"tag": component[0]}]) 34 used_components: dict[str, tuple[dict, dict]] = {} 35 36 if curr_node is not None: 37 context, used_components[component[0]] = process_context(*component, kwargs) 38 cmpt_props = used_components[component[0]][1].get("Props", None) 39 40 # Assign props to locals and remaining attributes stay 41 curr_node.parent.children = apply_component( 42 curr_node, 43 component[0], 44 component[1], 45 cmpt_props, 46 virtual_python, 47 context, 48 kwargs 49 ) 50 51 __add_component_elements(node, used_components, "style") 52 __add_component_elements(node, used_components, "script")
Replace the first occurance of a component.
Args
- node (Root | Element | AST): The starting point.
- virtual_python (VirtualPython): The python state to use while evaluating prop values
def
replace_components( node: phml.core.nodes.nodes.Root | phml.core.nodes.nodes.Element | phml.core.nodes.AST.AST, components: dict[str, dict], virtual_python: phml.core.virtual_python.vp.VirtualPython, **kwargs):
54def replace_components( 55 node: Root | Element | AST, 56 components: dict[str, dict], 57 virtual_python: VirtualPython, 58 **kwargs, 59): 60 """Iterate through components and replace all of each component in the nodes children. 61 Non-recursive. 62 63 Args: 64 node (Root | Element | AST): The starting point. 65 virtual_python (VirtualPython): Temp 66 """ 67 68 if isinstance(node, AST): 69 node = node.tree 70 71 used_components: dict[str, tuple[dict, dict]] = {} 72 73 for name, value in components.items(): 74 elements = [element for element in node.children if check(element, {"tag": name})] 75 76 context = {} 77 cmpt_props = None 78 if len(elements) > 0: 79 if components[name]["cache"] is not None: 80 context.update({ 81 key:value 82 for key,value in components[name]["cache"][1].items() 83 if key != "Props" 84 }) 85 used_components[name] = components[name]["cache"] 86 context.update(kwargs) 87 else: 88 context, used_components[name] = process_context(name, value["data"], kwargs) 89 components[name]["cache"] = ( 90 used_components[name][0], 91 { 92 key:value 93 for key,value in used_components[name][1].items() 94 if key not in kwargs 95 } 96 ) 97 98 cmpt_props = components[name]["cache"][1].get("Props", None) 99 100 if "Props" in context and "data" in context["Props"]: 101 print(context["Props"], kwargs) 102 103 for curr_node in elements: 104 curr_node.parent.children = apply_component( 105 curr_node, 106 name, 107 value["data"], 108 cmpt_props, 109 virtual_python, 110 context, 111 kwargs 112 ) 113 114 # Optimize, python, style, and script tags from components 115 __add_component_elements(node, used_components, "style") 116 __add_component_elements(node, used_components, "script")
Iterate through components and replace all of each component in the nodes children. Non-recursive.
Args
- node (Root | Element | AST): The starting point.
- virtual_python (VirtualPython): Temp
def
combine_component_elements( elements: list[phml.core.nodes.nodes.Element], tag: str) -> phml.core.nodes.nodes.Element:
373def combine_component_elements(elements: list[Element], tag: str) -> Element: 374 """Combine text from elements like python, script, and style. 375 376 Returns: 377 Element: With tag of element list but with combined text content 378 """ 379 380 values = [] 381 382 indent = -1 383 for element in elements: 384 if len(element.children) == 1 and isinstance(element.children[0], Text): 385 # normalize values 386 if indent == -1: 387 indent = offset(element.children[0].value) 388 values.append(normalize_indent(element.children[0].value, indent)) 389 390 return Element(tag, children=[Text("\n\n".join(values))])
Combine text from elements like python, script, and style.
Returns
Element: With tag of element list but with combined text content