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