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, 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) -> list: 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 227def process_context(name, value, kwargs: dict | None = None): 228 """Process the python elements and context of the component and extract the relavent context.""" 229 context = {} 230 local_virtual_python = VirtualPython(context=dict(kwargs or {})) 231 for python in value["python"]: 232 if len(python.children) == 1 and check(python.children[0], "text"): 233 text = python.children[0].normalized() 234 local_virtual_python += VirtualPython(text, context=local_virtual_python.context) 235 236 if "Props" in local_virtual_python.context: 237 if not isinstance(local_virtual_python.context["Props"], dict): 238 raise Exception( 239 f"Props must be a dict was " 240 + f"{type(local_virtual_python.context['Props']).__name__}: <{name} />" 241 ) 242 243 context.update({ 244 key:value 245 for key,value in local_virtual_python.context.items() 246 if key != "Props" 247 }) 248 249 return context, (value, local_virtual_python.context) 250 251def apply_component(node, name, value, cmpt_props, virtual_python, context, kwargs) -> list: 252 """Get the props, execute conditions and replace components in the node tree.""" 253 props, attrs = get_props( 254 node, 255 name, 256 value, 257 virtual_python, 258 cmpt_props, 259 **kwargs 260 ) 261 262 node.properties = attrs 263 node.context.update(props) 264 265 condition = py_condition(node) 266 results = [node] 267 if condition is not None: 268 results = execute_condition(condition, node, virtual_python, **kwargs) 269 270 # replace the valid components in the results list 271 new_children = [] 272 for child in results: 273 # get props and locals from current node 274 properties, attributes = node.context, child.properties 275 properties.update(context) 276 properties["children"] = node.children 277 278 component = deepcopy(value["component"]) 279 if component.tag in WRAPPER_TAG: 280 # Create a copy of the component 281 for sub_child in component.children: 282 if isinstance(sub_child, Element): 283 sub_child.context.update(properties) 284 sub_child.parent = node.parent 285 286 new_children.extend(component.children) 287 else: 288 component.context = properties 289 component.properties = attributes 290 component.parent = node.parent 291 new_children.append(component) 292 293 # replace the curr_node with the list of replaced nodes 294 parent = node.parent 295 index = parent.children.index(node) 296 return parent.children[:index] + new_children + parent.children[index+1:] 297 298def __add_component_elements(node, used_components: dict, tag: str): 299 if find(node, {"tag": tag}) is not None: 300 new_elements = __retrieve_component_elements(used_components, tag) 301 if len(new_elements) > 0: 302 replace_node( 303 node, 304 {"tag": tag}, 305 combine_component_elements( 306 [ 307 find(node, {"tag": tag}), 308 *new_elements, 309 ], 310 tag, 311 ), 312 ) 313 else: 314 new_element = combine_component_elements( 315 __retrieve_component_elements(used_components, tag), 316 tag, 317 ) 318 if new_element.children[0].value.strip() != "": 319 if tag == "style": 320 head = query(node, "head") 321 if head is not None: 322 head.append(new_element) 323 else: 324 node.append(new_element) 325 else: 326 html = query(node, "html") 327 if html is not None: 328 html.append(new_element) 329 else: 330 node.append(new_element) 331 332 333def combine_component_elements(elements: list[Element], tag: str) -> Element: 334 """Combine text from elements like python, script, and style. 335 336 Returns: 337 Element: With tag of element list but with combined text content 338 """ 339 340 values = [] 341 342 indent = -1 343 for element in elements: 344 if len(element.children) == 1 and isinstance(element.children[0], Text): 345 # normalize values 346 if indent == -1: 347 indent = offset(element.children[0].value) 348 values.append(normalize_indent(element.children[0].value, indent)) 349 350 return Element(tag, children=[Text("\n\n".join(values))]) 351 352 353def __retrieve_component_elements(collection: dict, element: str) -> list[Element]: 354 result = [] 355 for value in collection.values(): 356 if element in value[0]: 357 result.extend(value[0][element]) 358 return result 359 360def run_phml_if(child: Element, condition: str, **kwargs): 361 """Run the logic for manipulating the children on a `if` condition.""" 362 363 clocals = build_locals(child, **kwargs) 364 result = get_python_result(sub(r"\{|\}", "", child[condition].strip()), **clocals) 365 366 if result: 367 return child 368 369 # Condition failed, so remove the node 370 return child 371 372 373def run_phml_elif( 374 child: Element, 375 condition: str, 376 variables: dict, 377 **kwargs, 378): 379 """Run the logic for manipulating the children on a `elif` condition.""" 380 381 clocals = build_locals(child, **kwargs) 382 383 if variables["previous"][0] in variables["valid_prev"][condition] and variables["first_cond"]: 384 if not variables["previous"][1]: 385 result = get_python_result(sub(r"\{|\}", "", child[condition].strip()), **clocals) 386 if result: 387 return child 388 389 return child 390 391 392def run_phml_else(child: Element, condition: str, variables: dict, **kwargs): 393 """Run the logic for manipulating the children on a `else` condition.""" 394 395 if variables["previous"][0] in variables["valid_prev"][condition] and variables["first_cond"]: 396 if not variables["previous"][1]: 397 clocals = build_locals(child, **kwargs) 398 result = get_python_result(sub(r"\{|\}", "", child[condition].strip()), **clocals) 399 if result: 400 return child 401 402 # Condition failed so remove element 403 return child 404 405def build_locals(child, **kwargs) -> dict: 406 """Build a dictionary of local variables from a nodes inherited locals and 407 the passed kwargs. 408 """ 409 from phml.utilities import path # pylint: disable=import-outside-toplevel 410 411 clocals = {**kwargs} 412 413 # Inherit locals from top down 414 for parent in path(child): 415 if parent.type == "element": 416 clocals.update(parent.context) 417 418 clocals.update(child.context) 419 return clocals 420 421def __get_previous_conditions(child: Element) -> list[str]: 422 idx = child.parent.children.index(child) 423 conditions = [] 424 for i in range(0, idx): 425 if isinstance(child.parent.children[i], Element): 426 condition = py_condition(child.parent.children[i]) 427 if condition is not None: 428 conditions.append(condition) 429 430 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.
Arguments:
- 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.
Arguments:
- 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:
334def combine_component_elements(elements: list[Element], tag: str) -> Element: 335 """Combine text from elements like python, script, and style. 336 337 Returns: 338 Element: With tag of element list but with combined text content 339 """ 340 341 values = [] 342 343 indent = -1 344 for element in elements: 345 if len(element.children) == 1 and isinstance(element.children[0], Text): 346 # normalize values 347 if indent == -1: 348 indent = offset(element.children[0].value) 349 values.append(normalize_indent(element.children[0].value, indent)) 350 351 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