phml.core.formats.compile.compile
Helper methods for processing dynamic python attributes and blocks.
1"""Helper methods for processing dynamic python attributes and blocks.""" 2 3from __future__ import annotations 4 5from re import match, search, sub 6from typing import TYPE_CHECKING, Optional 7from pyparsing import Any 8 9from phml.core.nodes import AST, NODE, DocType, Element, Root, Text 10from phml.core.virtual_python import ( 11 VirtualPython, 12 get_python_result, 13 process_python_blocks 14) 15from phml.types.config import ConfigEnable 16from phml.utilities import ( 17 check, 18 find_all, 19 replace_node, 20 visit_children, 21 path_names, 22 classnames 23) 24 25from .reserved import RESERVED 26 27if TYPE_CHECKING: 28 from phml.types.config import Config 29 30# ? Change prefix char for `if`, `elif`, and `else` here 31CONDITION_PREFIX = "@" 32 33# ? Change prefix char for python attributes here 34ATTR_PREFIX = ":" 35 36valid_prev = { 37 f"{CONDITION_PREFIX}if": [ 38 "py-if", 39 "py-elif", 40 "py-else", 41 f"{CONDITION_PREFIX}if", 42 f"{CONDITION_PREFIX}elif", 43 f"{CONDITION_PREFIX}else", 44 ], 45 f"{CONDITION_PREFIX}elif": [ 46 "py-if", 47 "py-elif", 48 f"{CONDITION_PREFIX}if", 49 f"{CONDITION_PREFIX}elif", 50 ], 51 f"{CONDITION_PREFIX}else": [ 52 "py-if", 53 "py-elif", 54 f"{CONDITION_PREFIX}if", 55 f"{CONDITION_PREFIX}elif", 56 ], 57} 58 59EXTRAS = [ 60 "fenced-code-blocks", 61 "cuddled-lists", 62 "footnotes", 63 "header-ids", 64 "strike" 65] 66 67 68def process_reserved_attrs(prop: str, value: Any) -> tuple[str, Any]: 69 """Based on the props name, process/translate the props value.""" 70 71 if prop == "class:list": 72 value = classnames(value) 73 prop = "class" 74 75 return prop, value 76 77 78def process_props( 79 child: Element, 80 virtual_python: VirtualPython, 81 local_vars: dict 82) -> dict: 83 """Process props inline python and reserved value translations.""" 84 85 new_props = {} 86 87 for prop in child.properties: 88 if prop.startswith(ATTR_PREFIX): 89 local_env = {**virtual_python.context} 90 local_env.update(local_vars) 91 92 value = get_python_result(child[prop], **local_env) 93 94 prop = prop.lstrip(ATTR_PREFIX) 95 name, value = process_reserved_attrs(prop, value) 96 97 new_props[name] = value 98 elif match(r".*\{.*\}.*", str(child[prop])) is not None: 99 new_props[prop] = process_python_blocks( 100 child[prop], 101 virtual_python, 102 **local_vars 103 ) 104 else: 105 new_props[prop] = child[prop] 106 return new_props 107 108 109def apply_conditions( 110 node: Root | Element | AST, 111 config: Config, 112 virtual_python: VirtualPython, 113 components: dict, 114 **kwargs, 115): 116 """Applys all `py-if`, `py-elif`, and `py-else` to the node 117 recursively. 118 119 Args: 120 node (Root | Element): The node to recursively apply `py-` attributes 121 too. 122 virtual_python (VirtualPython): All of the data from the python 123 elements. 124 """ 125 from .component import replace_components 126 127 if isinstance(node, AST): 128 node = node.tree 129 130 process_conditions(node, virtual_python, **kwargs) 131 process_reserved_elements( 132 node, 133 virtual_python, 134 config["enabled"], 135 **kwargs 136 ) 137 replace_components(node, components, virtual_python, **kwargs) 138 139 for child in node.children: 140 if isinstance(child, (Root, Element)): 141 apply_conditions( 142 child, 143 config, 144 virtual_python, 145 components, 146 **kwargs 147 ) 148 149 150def process_reserved_elements( 151 node: Root | Element, 152 virtual_python: VirtualPython, 153 enabled: ConfigEnable, 154 **kwargs 155): 156 """Process all reserved elements and replace them with the results.""" 157 tags = [n.tag for n in visit_children(node) if check(n, "element")] 158 reserved_found = False 159 160 for key, value in RESERVED.items(): 161 if key in tags: 162 if key.lower() in enabled and enabled[key.lower()]: 163 value(node, virtual_python, **kwargs) 164 reserved_found = True 165 else: 166 node.children = [ 167 child 168 for child in node.children 169 if not check(child, {"tag": key}) 170 ] 171 172 if reserved_found: 173 process_conditions(node, virtual_python, **kwargs) 174 175 176def apply_python( 177 current: Root | Element | AST, 178 virtual_python: VirtualPython, 179 **kwargs, 180): 181 """Recursively travers the node and search for python blocks. When found 182 process them and apply the results. 183 184 Args: 185 current (Root | Element): The node to traverse 186 virtual_python (VirtualPython): The python elements data 187 """ 188 189 if isinstance(current, AST): 190 current = current.tree 191 192 def process_children(node: Root | Element, local_env: dict): 193 194 for child in node.children: 195 if isinstance(child, Element): 196 if ( 197 "children" in child.context.keys() 198 and len(child.context["children"]) > 0 199 ): 200 replace_node( 201 child, 202 ["element", {"tag": "Slot"}], 203 child.context["children"], 204 ) 205 206 local_vars = {**local_env} 207 local_vars.update(child.context) 208 209 child.properties = process_props( 210 child, 211 virtual_python, 212 local_vars 213 ) 214 process_children(child, {**local_vars}) 215 elif ( 216 isinstance(child, Text) 217 and search(r".*\{.*\}.*", str(child.value)) 218 and child.parent.tag not in ["script", "style"] 219 and "code" not in path_names(child) 220 ): 221 child.value = process_python_blocks( 222 child.value, 223 virtual_python, 224 **local_env 225 ) 226 227 process_children(current, {**kwargs}) 228 229 230def py_condition(node: Element) -> bool: 231 """Return all python condition attributes on an element.""" 232 conditions = [ 233 k 234 for k in node.properties.keys() 235 if k 236 in [ 237 f"{CONDITION_PREFIX}if", 238 f"{CONDITION_PREFIX}elif", 239 f"{CONDITION_PREFIX}else", 240 # f"{CONDITION_PREFIX}for", 241 ] 242 ] 243 if len(conditions) > 1: 244 raise Exception( 245 f"There can only be one python condition statement at a \ 246time:\n{repr(node)}" 247 ) 248 return conditions[0] if len(conditions) == 1 else None 249 250 251def __validate_previous_condition(child: Element) -> Optional[Element]: 252 idx = child.parent.children.index(child) 253 254 def get_previous_condition(idx: int): 255 """Get the last conditional element allowing for comments and text""" 256 previous = None 257 parent = child.parent 258 for i in range(idx - 1, -1, -1): 259 if isinstance(parent.children[i], Element): 260 if py_condition(parent.children[i]) is not None: 261 previous = parent.children[i] 262 break 263 return previous 264 265 previous = get_previous_condition(idx) 266 prev_cond = ( 267 py_condition(previous) 268 if previous is not None and isinstance(previous, Element) 269 else None 270 ) 271 272 if prev_cond is None or prev_cond not in [ 273 f"{CONDITION_PREFIX}elif", 274 f"{CONDITION_PREFIX}if", 275 ]: 276 raise Exception( 277 f"Condition statements that are not @if must have @if or\ 278 @elif as a previous sibling.\n{child.start_tag(self.offset)}\ 279{f' at {child.position}' if child.position is not None else ''}" 280 ) 281 return previous, prev_cond 282 283 284def process_conditions( 285 tree: Root | Element, 286 virtual_python: VirtualPython, 287 **kwargs 288): 289 """Process all python condition attributes in the phml tree. 290 291 Args: 292 tree (Root | Element): The tree to process conditions on. 293 virtual_python (VirtualPython): The collection of information from the 294 python blocks. 295 """ 296 conditional_elements = [] 297 for child in visit_children(tree): 298 if check(child, "element"): 299 condition = py_condition(child) 300 if condition in [ 301 f"{CONDITION_PREFIX}elif", 302 f"{CONDITION_PREFIX}else", 303 ]: 304 __validate_previous_condition(child) 305 306 if condition is not None: 307 conditional_elements.append((condition, child)) 308 309 tree.children = execute_conditions( 310 conditional_elements, 311 tree.children, 312 virtual_python, 313 **kwargs, 314 ) 315 316 317def execute_conditions( 318 cond: list[tuple], 319 children: list, 320 virtual_python: VirtualPython, 321 **kwargs, 322) -> list: 323 """Execute all the conditions. If the condition is a `for` then generate 324 more nodes. All other conditions determine if the node stays or is removed. 325 326 Args: 327 cond (list[tuple]): The list of conditions to apply. Holds tuples of 328 (condition, node). 329 children (list): List of current nodes children. 330 virtual_python (VirtualPython): The collection of information from the 331 python blocks. 332 333 Raises: 334 Exception: An unkown conditional attribute is being parsed. 335 Exception: Condition requirements are not met. 336 337 Returns: 338 list: The newly generated/modified list of children. 339 """ 340 341 # Whether the current conditional branch began with an `if` condition. 342 first_cond = False 343 344 # Previous condition that was run and whether it was successful. 345 previous = (f"{CONDITION_PREFIX}else", True) 346 347 # Add the python blocks locals to kwargs dict 348 kwargs.update(virtual_python.context) 349 350 # Bring python blocks imports into scope 351 for imp in virtual_python.imports: 352 exec(str(imp)) # pylint: disable=exec-used 353 354 # For each element with a python condition 355 for condition, child in cond: 356 if condition == f"{CONDITION_PREFIX}if": 357 previous = run_phml_if(child, condition, children, **kwargs) 358 359 # Start of condition branch 360 first_cond = True 361 362 elif condition == f"{CONDITION_PREFIX}elif": 363 # Can only exist if previous condition in branch failed 364 previous = run_phml_elif( 365 child, 366 children, 367 condition, 368 { 369 "previous": previous, 370 "valid_prev": valid_prev, 371 "first_cond": first_cond, 372 }, 373 **kwargs, 374 ) 375 elif condition == f"{CONDITION_PREFIX}else": 376 377 # Can only exist if previous condition in branch failed 378 previous = run_phml_else( 379 child, 380 children, 381 condition, 382 { 383 "previous": previous, 384 "valid_prev": valid_prev, 385 "first_cond": first_cond, 386 }, 387 ) 388 389 # End any condition branch 390 first_cond = False 391 392 return children 393 394 395def build_locals(child, **kwargs) -> dict: 396 """Build a dictionary of local variables from a nodes inherited locals and 397 the passed kwargs. 398 """ 399 from phml.utilities import path # pylint: disable=import-outside-toplevel 400 401 clocals = {**kwargs} 402 403 # Inherit locals from top down 404 for parent in path(child): 405 if parent.type == "element": 406 clocals.update(parent.context) 407 408 clocals.update(child.context) 409 return clocals 410 411 412def run_phml_if(child: Element, condition: str, children: list, **kwargs): 413 """Run the logic for manipulating the children on a `if` condition.""" 414 415 clocals = build_locals(child, **kwargs) 416 417 result = get_python_result( 418 sub(r"\{|\}", "", child[condition].strip()), 419 **clocals 420 ) 421 422 if result: 423 del child[condition] 424 return (f"{CONDITION_PREFIX}if", True) 425 426 # Condition failed, so remove the node 427 children.remove(child) 428 return (f"{CONDITION_PREFIX}if", False) 429 430 431def run_phml_elif( 432 child: Element, 433 children: list, 434 condition: str, 435 variables: dict, 436 **kwargs, 437): 438 """Run the logic for manipulating the children on a `elif` condition.""" 439 440 clocals = build_locals(child, **kwargs) 441 442 if ( 443 variables["previous"][0] in variables["valid_prev"][condition] 444 and variables["first_cond"] 445 ): 446 if not variables["previous"][1]: 447 result = get_python_result( 448 sub(r"\{|\}", "", child[condition].strip()), 449 **clocals 450 ) 451 if result: 452 del child[condition] 453 return (f"{CONDITION_PREFIX}elif", True) 454 455 children.remove(child) 456 return variables["previous"] 457 458 459def run_phml_else( 460 child: Element, 461 children: list, 462 condition: str, 463 variables: dict 464): 465 """Run the logic for manipulating the children on a `else` condition.""" 466 467 if ( 468 variables["previous"][0] in variables["valid_prev"][condition] 469 and variables["first_cond"] 470 ): 471 if not variables["previous"][1]: 472 del child[condition] 473 return (f"{CONDITION_PREFIX}else", True) 474 475 # Condition failed so remove element 476 children.remove(child) 477 return (f"{CONDITION_PREFIX}else", False) 478 479 480class ASTRenderer: 481 """Compiles an ast to a hypertext markup language. Compiles to a tag based 482 string. 483 """ 484 485 def __init__(self, ast: Optional[AST] = None, _offset: int = 4): 486 self.ast = ast 487 self.offset = _offset 488 489 def compile( 490 self, 491 ast: Optional[AST] = None, 492 _offset: Optional[int] = None, 493 include_doctype: bool = True, 494 ) -> str: 495 """Compile an ast to html. 496 497 Args: 498 ast (AST): The phml ast to compile. 499 offset (int | None): The amount to offset for each nested element 500 include_doctype (bool): Whether to validate for doctype and auto 501 insert if it is missing. 502 """ 503 504 ast = ast or self.ast 505 if ast is None: 506 raise ValueError( 507 "Converting to a file format requires that an ast is provided" 508 ) 509 510 if include_doctype: 511 # Validate doctypes 512 doctypes = find_all(ast.tree, "doctype") 513 514 if any( 515 dt.parent is None 516 or dt.parent.type != "root" 517 for dt in doctypes 518 ): 519 raise ValueError( 520 "Doctypes must be in the root of the file/tree" 521 ) 522 523 if len(doctypes) == 0: 524 ast.tree.children.insert(0, DocType(parent=ast.tree)) 525 526 self.offset = _offset or self.offset 527 lines = self.__compile_children(ast.tree) 528 return "\n".join(lines) 529 530 def __one_line(self, node, indent: int = 0) -> str: 531 return "".join( 532 [ 533 *[" " * indent + line for line in node.start_tag(self.offset)], 534 node.children[0].stringify( 535 indent + self.offset 536 if node.children[0].num_lines > 1 537 else 0 538 ), 539 node.end_tag(), 540 ] 541 ) 542 543 def __many_children(self, node, indent: int = 0) -> list: 544 lines = [] 545 for child in visit_children(node): 546 if child.type == "element": 547 if child.tag == "pre" or "pre" in path_names(child): 548 lines.append(''.join(self.__compile_children(child, 0))) 549 else: 550 lines.extend( 551 [ 552 line 553 for line in self.__compile_children( 554 child, indent + self.offset 555 ) 556 if line != "" 557 ] 558 ) 559 else: 560 lines.append(child.stringify(indent + self.offset)) 561 return lines 562 563 def __construct_element(self, node, indent: int = 0) -> list: 564 lines = [] 565 if ( 566 len(node.children) == 1 567 and node.children[0].type == "text" 568 and node.children[0].num_lines == 1 569 and len(node.properties) <= 1 570 ): 571 lines.append(self.__one_line(node, indent)) 572 elif len(node.children) == 0: 573 lines.extend([*[" " * indent + line for line in node.start_tag(self.offset)], " " * indent + node.end_tag()]) 574 else: 575 lines.extend([" " * indent + line for line in node.start_tag(self.offset)]) 576 lines.extend(self.__many_children(node, indent)) 577 lines.append(" " * indent + node.end_tag()) 578 return lines 579 580 def __compile_children(self, node: NODE, indent: int = 0) -> list[str]: 581 lines = [] 582 if isinstance(node, Element): 583 if node.startend: 584 585 lines.extend([" " * indent + line for line in node.start_tag(self.offset)]) 586 else: 587 lines.extend(self.__construct_element(node, indent)) 588 elif isinstance(node, Root): 589 for child in visit_children(node): 590 lines.extend(self.__compile_children(child)) 591 else: 592 value = node.stringify(indent + self.offset) 593 if value.strip() != "" or "pre" in path_names(node): 594 lines.append(value) 595 596 return lines
69def process_reserved_attrs(prop: str, value: Any) -> tuple[str, Any]: 70 """Based on the props name, process/translate the props value.""" 71 72 if prop == "class:list": 73 value = classnames(value) 74 prop = "class" 75 76 return prop, value
Based on the props name, process/translate the props value.
79def process_props( 80 child: Element, 81 virtual_python: VirtualPython, 82 local_vars: dict 83) -> dict: 84 """Process props inline python and reserved value translations.""" 85 86 new_props = {} 87 88 for prop in child.properties: 89 if prop.startswith(ATTR_PREFIX): 90 local_env = {**virtual_python.context} 91 local_env.update(local_vars) 92 93 value = get_python_result(child[prop], **local_env) 94 95 prop = prop.lstrip(ATTR_PREFIX) 96 name, value = process_reserved_attrs(prop, value) 97 98 new_props[name] = value 99 elif match(r".*\{.*\}.*", str(child[prop])) is not None: 100 new_props[prop] = process_python_blocks( 101 child[prop], 102 virtual_python, 103 **local_vars 104 ) 105 else: 106 new_props[prop] = child[prop] 107 return new_props
Process props inline python and reserved value translations.
110def apply_conditions( 111 node: Root | Element | AST, 112 config: Config, 113 virtual_python: VirtualPython, 114 components: dict, 115 **kwargs, 116): 117 """Applys all `py-if`, `py-elif`, and `py-else` to the node 118 recursively. 119 120 Args: 121 node (Root | Element): The node to recursively apply `py-` attributes 122 too. 123 virtual_python (VirtualPython): All of the data from the python 124 elements. 125 """ 126 from .component import replace_components 127 128 if isinstance(node, AST): 129 node = node.tree 130 131 process_conditions(node, virtual_python, **kwargs) 132 process_reserved_elements( 133 node, 134 virtual_python, 135 config["enabled"], 136 **kwargs 137 ) 138 replace_components(node, components, virtual_python, **kwargs) 139 140 for child in node.children: 141 if isinstance(child, (Root, Element)): 142 apply_conditions( 143 child, 144 config, 145 virtual_python, 146 components, 147 **kwargs 148 )
Applys all py-if
, py-elif
, and py-else
to the node
recursively.
Args
- node (Root | Element): The node to recursively apply
py-
attributes too. - virtual_python (VirtualPython): All of the data from the python elements.
151def process_reserved_elements( 152 node: Root | Element, 153 virtual_python: VirtualPython, 154 enabled: ConfigEnable, 155 **kwargs 156): 157 """Process all reserved elements and replace them with the results.""" 158 tags = [n.tag for n in visit_children(node) if check(n, "element")] 159 reserved_found = False 160 161 for key, value in RESERVED.items(): 162 if key in tags: 163 if key.lower() in enabled and enabled[key.lower()]: 164 value(node, virtual_python, **kwargs) 165 reserved_found = True 166 else: 167 node.children = [ 168 child 169 for child in node.children 170 if not check(child, {"tag": key}) 171 ] 172 173 if reserved_found: 174 process_conditions(node, virtual_python, **kwargs)
Process all reserved elements and replace them with the results.
177def apply_python( 178 current: Root | Element | AST, 179 virtual_python: VirtualPython, 180 **kwargs, 181): 182 """Recursively travers the node and search for python blocks. When found 183 process them and apply the results. 184 185 Args: 186 current (Root | Element): The node to traverse 187 virtual_python (VirtualPython): The python elements data 188 """ 189 190 if isinstance(current, AST): 191 current = current.tree 192 193 def process_children(node: Root | Element, local_env: dict): 194 195 for child in node.children: 196 if isinstance(child, Element): 197 if ( 198 "children" in child.context.keys() 199 and len(child.context["children"]) > 0 200 ): 201 replace_node( 202 child, 203 ["element", {"tag": "Slot"}], 204 child.context["children"], 205 ) 206 207 local_vars = {**local_env} 208 local_vars.update(child.context) 209 210 child.properties = process_props( 211 child, 212 virtual_python, 213 local_vars 214 ) 215 process_children(child, {**local_vars}) 216 elif ( 217 isinstance(child, Text) 218 and search(r".*\{.*\}.*", str(child.value)) 219 and child.parent.tag not in ["script", "style"] 220 and "code" not in path_names(child) 221 ): 222 child.value = process_python_blocks( 223 child.value, 224 virtual_python, 225 **local_env 226 ) 227 228 process_children(current, {**kwargs})
Recursively travers the node and search for python blocks. When found process them and apply the results.
Args
- current (Root | Element): The node to traverse
- virtual_python (VirtualPython): The python elements data
231def py_condition(node: Element) -> bool: 232 """Return all python condition attributes on an element.""" 233 conditions = [ 234 k 235 for k in node.properties.keys() 236 if k 237 in [ 238 f"{CONDITION_PREFIX}if", 239 f"{CONDITION_PREFIX}elif", 240 f"{CONDITION_PREFIX}else", 241 # f"{CONDITION_PREFIX}for", 242 ] 243 ] 244 if len(conditions) > 1: 245 raise Exception( 246 f"There can only be one python condition statement at a \ 247time:\n{repr(node)}" 248 ) 249 return conditions[0] if len(conditions) == 1 else None
Return all python condition attributes on an element.
285def process_conditions( 286 tree: Root | Element, 287 virtual_python: VirtualPython, 288 **kwargs 289): 290 """Process all python condition attributes in the phml tree. 291 292 Args: 293 tree (Root | Element): The tree to process conditions on. 294 virtual_python (VirtualPython): The collection of information from the 295 python blocks. 296 """ 297 conditional_elements = [] 298 for child in visit_children(tree): 299 if check(child, "element"): 300 condition = py_condition(child) 301 if condition in [ 302 f"{CONDITION_PREFIX}elif", 303 f"{CONDITION_PREFIX}else", 304 ]: 305 __validate_previous_condition(child) 306 307 if condition is not None: 308 conditional_elements.append((condition, child)) 309 310 tree.children = execute_conditions( 311 conditional_elements, 312 tree.children, 313 virtual_python, 314 **kwargs, 315 )
Process all python condition attributes in the phml tree.
Args
- tree (Root | Element): The tree to process conditions on.
- virtual_python (VirtualPython): The collection of information from the python blocks.
318def execute_conditions( 319 cond: list[tuple], 320 children: list, 321 virtual_python: VirtualPython, 322 **kwargs, 323) -> list: 324 """Execute all the conditions. If the condition is a `for` then generate 325 more nodes. All other conditions determine if the node stays or is removed. 326 327 Args: 328 cond (list[tuple]): The list of conditions to apply. Holds tuples of 329 (condition, node). 330 children (list): List of current nodes children. 331 virtual_python (VirtualPython): The collection of information from the 332 python blocks. 333 334 Raises: 335 Exception: An unkown conditional attribute is being parsed. 336 Exception: Condition requirements are not met. 337 338 Returns: 339 list: The newly generated/modified list of children. 340 """ 341 342 # Whether the current conditional branch began with an `if` condition. 343 first_cond = False 344 345 # Previous condition that was run and whether it was successful. 346 previous = (f"{CONDITION_PREFIX}else", True) 347 348 # Add the python blocks locals to kwargs dict 349 kwargs.update(virtual_python.context) 350 351 # Bring python blocks imports into scope 352 for imp in virtual_python.imports: 353 exec(str(imp)) # pylint: disable=exec-used 354 355 # For each element with a python condition 356 for condition, child in cond: 357 if condition == f"{CONDITION_PREFIX}if": 358 previous = run_phml_if(child, condition, children, **kwargs) 359 360 # Start of condition branch 361 first_cond = True 362 363 elif condition == f"{CONDITION_PREFIX}elif": 364 # Can only exist if previous condition in branch failed 365 previous = run_phml_elif( 366 child, 367 children, 368 condition, 369 { 370 "previous": previous, 371 "valid_prev": valid_prev, 372 "first_cond": first_cond, 373 }, 374 **kwargs, 375 ) 376 elif condition == f"{CONDITION_PREFIX}else": 377 378 # Can only exist if previous condition in branch failed 379 previous = run_phml_else( 380 child, 381 children, 382 condition, 383 { 384 "previous": previous, 385 "valid_prev": valid_prev, 386 "first_cond": first_cond, 387 }, 388 ) 389 390 # End any condition branch 391 first_cond = False 392 393 return children
Execute all the conditions. If the condition is a for
then generate
more nodes. All other conditions determine if the node stays or is removed.
Args
- cond (list[tuple]): The list of conditions to apply. Holds tuples of (condition, node).
- children (list): List of current nodes children.
- virtual_python (VirtualPython): The collection of information from the python blocks.
Raises
- Exception: An unkown conditional attribute is being parsed.
- Exception: Condition requirements are not met.
Returns
list: The newly generated/modified list of children.
396def build_locals(child, **kwargs) -> dict: 397 """Build a dictionary of local variables from a nodes inherited locals and 398 the passed kwargs. 399 """ 400 from phml.utilities import path # pylint: disable=import-outside-toplevel 401 402 clocals = {**kwargs} 403 404 # Inherit locals from top down 405 for parent in path(child): 406 if parent.type == "element": 407 clocals.update(parent.context) 408 409 clocals.update(child.context) 410 return clocals
Build a dictionary of local variables from a nodes inherited locals and the passed kwargs.
413def run_phml_if(child: Element, condition: str, children: list, **kwargs): 414 """Run the logic for manipulating the children on a `if` condition.""" 415 416 clocals = build_locals(child, **kwargs) 417 418 result = get_python_result( 419 sub(r"\{|\}", "", child[condition].strip()), 420 **clocals 421 ) 422 423 if result: 424 del child[condition] 425 return (f"{CONDITION_PREFIX}if", True) 426 427 # Condition failed, so remove the node 428 children.remove(child) 429 return (f"{CONDITION_PREFIX}if", False)
Run the logic for manipulating the children on a if
condition.
432def run_phml_elif( 433 child: Element, 434 children: list, 435 condition: str, 436 variables: dict, 437 **kwargs, 438): 439 """Run the logic for manipulating the children on a `elif` condition.""" 440 441 clocals = build_locals(child, **kwargs) 442 443 if ( 444 variables["previous"][0] in variables["valid_prev"][condition] 445 and variables["first_cond"] 446 ): 447 if not variables["previous"][1]: 448 result = get_python_result( 449 sub(r"\{|\}", "", child[condition].strip()), 450 **clocals 451 ) 452 if result: 453 del child[condition] 454 return (f"{CONDITION_PREFIX}elif", True) 455 456 children.remove(child) 457 return variables["previous"]
Run the logic for manipulating the children on a elif
condition.
460def run_phml_else( 461 child: Element, 462 children: list, 463 condition: str, 464 variables: dict 465): 466 """Run the logic for manipulating the children on a `else` condition.""" 467 468 if ( 469 variables["previous"][0] in variables["valid_prev"][condition] 470 and variables["first_cond"] 471 ): 472 if not variables["previous"][1]: 473 del child[condition] 474 return (f"{CONDITION_PREFIX}else", True) 475 476 # Condition failed so remove element 477 children.remove(child) 478 return (f"{CONDITION_PREFIX}else", False)
Run the logic for manipulating the children on a else
condition.
481class ASTRenderer: 482 """Compiles an ast to a hypertext markup language. Compiles to a tag based 483 string. 484 """ 485 486 def __init__(self, ast: Optional[AST] = None, _offset: int = 4): 487 self.ast = ast 488 self.offset = _offset 489 490 def compile( 491 self, 492 ast: Optional[AST] = None, 493 _offset: Optional[int] = None, 494 include_doctype: bool = True, 495 ) -> str: 496 """Compile an ast to html. 497 498 Args: 499 ast (AST): The phml ast to compile. 500 offset (int | None): The amount to offset for each nested element 501 include_doctype (bool): Whether to validate for doctype and auto 502 insert if it is missing. 503 """ 504 505 ast = ast or self.ast 506 if ast is None: 507 raise ValueError( 508 "Converting to a file format requires that an ast is provided" 509 ) 510 511 if include_doctype: 512 # Validate doctypes 513 doctypes = find_all(ast.tree, "doctype") 514 515 if any( 516 dt.parent is None 517 or dt.parent.type != "root" 518 for dt in doctypes 519 ): 520 raise ValueError( 521 "Doctypes must be in the root of the file/tree" 522 ) 523 524 if len(doctypes) == 0: 525 ast.tree.children.insert(0, DocType(parent=ast.tree)) 526 527 self.offset = _offset or self.offset 528 lines = self.__compile_children(ast.tree) 529 return "\n".join(lines) 530 531 def __one_line(self, node, indent: int = 0) -> str: 532 return "".join( 533 [ 534 *[" " * indent + line for line in node.start_tag(self.offset)], 535 node.children[0].stringify( 536 indent + self.offset 537 if node.children[0].num_lines > 1 538 else 0 539 ), 540 node.end_tag(), 541 ] 542 ) 543 544 def __many_children(self, node, indent: int = 0) -> list: 545 lines = [] 546 for child in visit_children(node): 547 if child.type == "element": 548 if child.tag == "pre" or "pre" in path_names(child): 549 lines.append(''.join(self.__compile_children(child, 0))) 550 else: 551 lines.extend( 552 [ 553 line 554 for line in self.__compile_children( 555 child, indent + self.offset 556 ) 557 if line != "" 558 ] 559 ) 560 else: 561 lines.append(child.stringify(indent + self.offset)) 562 return lines 563 564 def __construct_element(self, node, indent: int = 0) -> list: 565 lines = [] 566 if ( 567 len(node.children) == 1 568 and node.children[0].type == "text" 569 and node.children[0].num_lines == 1 570 and len(node.properties) <= 1 571 ): 572 lines.append(self.__one_line(node, indent)) 573 elif len(node.children) == 0: 574 lines.extend([*[" " * indent + line for line in node.start_tag(self.offset)], " " * indent + node.end_tag()]) 575 else: 576 lines.extend([" " * indent + line for line in node.start_tag(self.offset)]) 577 lines.extend(self.__many_children(node, indent)) 578 lines.append(" " * indent + node.end_tag()) 579 return lines 580 581 def __compile_children(self, node: NODE, indent: int = 0) -> list[str]: 582 lines = [] 583 if isinstance(node, Element): 584 if node.startend: 585 586 lines.extend([" " * indent + line for line in node.start_tag(self.offset)]) 587 else: 588 lines.extend(self.__construct_element(node, indent)) 589 elif isinstance(node, Root): 590 for child in visit_children(node): 591 lines.extend(self.__compile_children(child)) 592 else: 593 value = node.stringify(indent + self.offset) 594 if value.strip() != "" or "pre" in path_names(node): 595 lines.append(value) 596 597 return lines
Compiles an ast to a hypertext markup language. Compiles to a tag based string.
490 def compile( 491 self, 492 ast: Optional[AST] = None, 493 _offset: Optional[int] = None, 494 include_doctype: bool = True, 495 ) -> str: 496 """Compile an ast to html. 497 498 Args: 499 ast (AST): The phml ast to compile. 500 offset (int | None): The amount to offset for each nested element 501 include_doctype (bool): Whether to validate for doctype and auto 502 insert if it is missing. 503 """ 504 505 ast = ast or self.ast 506 if ast is None: 507 raise ValueError( 508 "Converting to a file format requires that an ast is provided" 509 ) 510 511 if include_doctype: 512 # Validate doctypes 513 doctypes = find_all(ast.tree, "doctype") 514 515 if any( 516 dt.parent is None 517 or dt.parent.type != "root" 518 for dt in doctypes 519 ): 520 raise ValueError( 521 "Doctypes must be in the root of the file/tree" 522 ) 523 524 if len(doctypes) == 0: 525 ast.tree.children.insert(0, DocType(parent=ast.tree)) 526 527 self.offset = _offset or self.offset 528 lines = self.__compile_children(ast.tree) 529 return "\n".join(lines)
Compile an ast to html.
Args
- ast (AST): The phml ast to compile.
- offset (int | None): The amount to offset for each nested element
- include_doctype (bool): Whether to validate for doctype and auto insert if it is missing.