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

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

def apply_conditions( node: phml.core.nodes.nodes.Root | phml.core.nodes.nodes.Element | phml.core.nodes.AST.AST, config: dict[typing.Literal['enabled'], dict[typing.Literal['html', 'markdown'], bool]], virtual_python: phml.core.virtual_python.vp.VirtualPython, components: dict, **kwargs):
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.
def process_reserved_elements( node: phml.core.nodes.nodes.Root | phml.core.nodes.nodes.Element, virtual_python: phml.core.virtual_python.vp.VirtualPython, enabled: dict[typing.Literal['html', 'markdown'], bool], **kwargs):
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.

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):
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
def py_condition(node: phml.core.nodes.nodes.Element) -> bool:
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.

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

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

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

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

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

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

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