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
def process_reserved_attrs(prop: str, value: Any) -> tuple[str, typing.Any]:
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.

def process_props( child: phml.core.nodes.nodes.Element, virtual_python: phml.core.virtual_python.vp.VirtualPython, local_vars: dict) -> dict:
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.

def apply_conditions( node: phml.core.nodes.nodes.Root | phml.core.nodes.nodes.Element | phml.core.nodes.AST.AST, virtual_python: phml.core.virtual_python.vp.VirtualPython, components: dict, **kwargs):
 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.
def process_reserved_elements( node: phml.core.nodes.nodes.Root | phml.core.nodes.nodes.Element, virtual_python: phml.core.virtual_python.vp.VirtualPython, **kwargs):
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.

def apply_python( current: phml.core.nodes.nodes.Root | phml.core.nodes.nodes.Element | phml.core.nodes.AST.AST, virtual_python: phml.core.virtual_python.vp.VirtualPython, **kwargs):
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
def py_condition(node: phml.core.nodes.nodes.Element) -> bool:
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.

def process_conditions( tree: phml.core.nodes.nodes.Root | phml.core.nodes.nodes.Element, virtual_python: phml.core.virtual_python.vp.VirtualPython, **kwargs):
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.
def execute_conditions( cond: list[tuple], children: list, virtual_python: phml.core.virtual_python.vp.VirtualPython, **kwargs) -> list:
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.

def build_locals(child, **kwargs) -> dict:
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.

def run_phml_if( child: phml.core.nodes.nodes.Element, condition: str, children: list, **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.

def run_phml_elif( child: phml.core.nodes.nodes.Element, children: list, condition: str, variables: dict, **kwargs):
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.

def run_phml_else( child: phml.core.nodes.nodes.Element, children: list, condition: str, variables: dict):
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.

class ASTRenderer:
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.

ASTRenderer(ast: Optional[phml.core.nodes.AST.AST] = None, _offset: int = 4)
395    def __init__(self, ast: Optional[AST] = None, _offset: int = 4):
396        self.ast = ast
397        self.offset = _offset
def compile( self, ast: Optional[phml.core.nodes.AST.AST] = None, _offset: Optional[int] = None, include_doctype: bool = True) -> str:
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.