phml.core.formats.compile.component

  1from __future__ import annotations
  2
  3from copy import deepcopy
  4from re import sub
  5from phml.core.nodes import Root, Element, AST, Text
  6from phml.core.virtual_python import VirtualPython, process_python_blocks, get_python_result
  7from phml.utilities import find, find_all, offset, normalize_indent, query, replace_node, check
  8
  9from .compile import py_condition, CONDITION_PREFIX, valid_prev
 10
 11__all__ = ["substitute_component", "replace_components", "combine_component_elements"]
 12
 13
 14WRAPPER_TAG = ["template", ""]
 15
 16def substitute_component(
 17    node: Root | Element | AST,
 18    component: tuple[str, dict],
 19    virtual_python: VirtualPython,
 20    **kwargs,
 21):
 22    """Replace the first occurance of a component.
 23
 24    Args:
 25        node (Root | Element | AST): The starting point.
 26        virtual_python (VirtualPython): The python state to use while evaluating prop values
 27    """
 28
 29    if isinstance(node, AST):
 30        node = node.tree
 31
 32    curr_node = find(node, ["element", {"tag": component[0]}])
 33    used_components: dict[str, tuple[dict, dict]] = {}
 34
 35    if curr_node is not None:
 36        context, used_components[component[0]] = process_context(*component, kwargs)
 37        cmpt_props = used_components[component[0]][1].get("Props", None)
 38
 39        # Assign props to locals and remaining attributes stay
 40        curr_node.parent.children = apply_component(
 41            curr_node,
 42            component[0],
 43            component[1],
 44            cmpt_props,
 45            virtual_python,
 46            context,
 47            kwargs
 48        )
 49
 50        __add_component_elements(node, used_components, "style")
 51        __add_component_elements(node, used_components, "script")
 52
 53def replace_components(
 54    node: Root | Element | AST,
 55    components: dict[str, dict],
 56    virtual_python: VirtualPython,
 57    **kwargs,
 58):
 59    """Iterate through components and replace all of each component in the nodes children.
 60    Non-recursive.
 61
 62    Args:
 63        node (Root | Element | AST): The starting point.
 64        virtual_python (VirtualPython): Temp
 65    """
 66
 67    if isinstance(node, AST):
 68        node = node.tree
 69
 70    used_components: dict[str, tuple[dict, dict]] = {}
 71
 72    for name, value in components.items():
 73        elements = [element for element in node.children if check(element, {"tag": name})]
 74
 75        context = {}
 76        cmpt_props = None
 77        if len(elements) > 0:
 78            if components[name]["cache"] is not None:
 79                context.update({
 80                    key:value
 81                    for key,value in components[name]["cache"][1].items()
 82                    if key != "Props"
 83                })
 84                used_components[name] = components[name]["cache"]
 85                context.update(kwargs)
 86            else:
 87                context, used_components[name] = process_context(name, value["data"], kwargs)
 88                components[name]["cache"] = (
 89                    used_components[name][0],
 90                    {
 91                        key:value
 92                        for key,value in used_components[name][1].items()
 93                        if key not in kwargs
 94                    }
 95                )
 96
 97            cmpt_props = components[name]["cache"][1].get("Props", None)
 98
 99        if "Props" in context and "data" in context["Props"]:
100            print(context["Props"], kwargs)
101
102        for curr_node in elements:
103            curr_node.parent.children = apply_component(
104                curr_node,
105                name,
106                value["data"],
107                cmpt_props,
108                virtual_python,
109                context,
110                kwargs
111            )
112
113    # Optimize, python, style, and script tags from components
114    __add_component_elements(node, used_components, "style")
115    __add_component_elements(node, used_components, "script")
116
117def get_props(
118    node,
119    name,
120    value,
121    virtual_python,
122    props: dict | None = None,
123    **kwargs
124) -> dict[str, str]:
125    """Extract props from a phml component."""
126    props = dict(props or {})
127    extra_props = {}
128    attrs = value["component"].properties
129
130    attributes = node.properties
131    for item in attributes:
132        attr_name = item.lstrip(":")
133        if attr_name.startswith("py-"):
134            attr_name = attr_name.lstrip("py-")
135
136        if attr_name in props:
137            # Get value if pythonic
138            context = build_locals(node, **kwargs)
139            if item.startswith((":", "py-")):
140                # process as python
141                context.update(virtual_python.context)
142                result = get_python_result(attributes[item], **context)
143            else:
144                # process python blocks
145                result = process_python_blocks(
146                    attributes[item],
147                    virtual_python,
148                    **context
149                )
150            if (
151                isinstance(result, str)
152                and result.lower() in ["true", "false", "yes", "no"]
153            ):
154                result = True if result.lower() in ["true", "yes"] else False
155            props[attr_name] = result
156        elif attr_name not in attrs and item not in attrs:
157            # Add value to attributes
158            if (
159                isinstance(attributes[item], str)
160                and attributes[item].lower() in ["true", "false", "yes", "no"]
161            ):
162                attributes[item] = True if attributes[item].lower() in ["true", "yes"] else False
163            extra_props[attr_name] = attributes[item]
164
165    if len(extra_props) > 0:
166        props["props"] = extra_props
167
168    return props, attrs
169
170def execute_condition(
171    condition: str,
172    child: Element,
173    virtual_python: VirtualPython,
174    **kwargs,
175) -> Element|None:
176    """Execute python conditions for node to determine what will happen with the component."""
177    conditions = __get_previous_conditions(child)
178
179    first_cond = (
180        conditions[0] in [f"{CONDITION_PREFIX}if"]
181        if len(conditions) > 0 else False
182    )
183
184    previous = (conditions[-1] if len(conditions) > 0 else f"{CONDITION_PREFIX}else", True)
185
186    # Add the python blocks locals to kwargs dict
187    kwargs.update(virtual_python.context)
188
189    # Bring python blocks imports into scope
190    for imp in virtual_python.imports:
191        exec(str(imp))  # pylint: disable=exec-used
192
193    # For each element with a python condition
194    if condition == f"{CONDITION_PREFIX}if":
195        child = run_phml_if(child, condition, **kwargs)
196        return child
197    
198    if condition == f"{CONDITION_PREFIX}elif":
199        # Can only exist if previous condition in branch failed
200        child = run_phml_elif(
201            child,
202            condition,
203            {
204                "previous": previous,
205                "valid_prev": valid_prev,
206                "first_cond": first_cond,
207            },
208            **kwargs,
209        )
210        return child
211    
212    if condition == f"{CONDITION_PREFIX}else":
213
214        # Can only exist if previous condition in branch failed
215        child = run_phml_else(
216            child,
217            condition,
218            {
219                "previous": previous,
220                "valid_prev": valid_prev,
221                "first_cond": first_cond,
222            },
223            **kwargs
224        )
225        return child
226    return None
227
228def process_context(name, value, kwargs: dict | None = None):
229    """Process the python elements and context of the component and extract the relavent context."""
230    context = {}
231    local_virtual_python = VirtualPython(context=dict(kwargs or {}))
232    for python in value["python"]:
233        if len(python.children) == 1 and check(python.children[0], "text"):
234            text = python.children[0].normalized()
235            local_virtual_python += VirtualPython(text, context=local_virtual_python.context)
236            
237    if "Props" in local_virtual_python.context:
238        if not isinstance(local_virtual_python.context["Props"], dict):
239            raise Exception(
240                "Props must be a dict was "
241                + f"{type(local_virtual_python.context['Props']).__name__}: <{name} />"
242            )
243
244    context.update({
245        key:value
246        for key,value in local_virtual_python.context.items()
247        if key != "Props"
248    })
249
250    return context, (value, local_virtual_python.context)
251
252def apply_component(node, name, value, cmpt_props, virtual_python, context, kwargs) -> list:
253    """Get the props, execute conditions and replace components in the node tree."""
254    props, attrs = get_props(
255        node,
256        name,
257        value,
258        virtual_python,
259        cmpt_props,
260        **kwargs
261    )
262
263    node.properties = attrs
264    node.context.update(props)
265
266    condition = py_condition(node)
267    result = None
268    if condition is not None:
269        result = execute_condition(condition, node, virtual_python, **kwargs)
270
271    # replace the valid components in the results list
272    new_children = []
273    # get props and locals from current node
274    properties, attributes = node.context, result.properties if result is not None else node.properties
275    properties.update(context)
276    # properties["children"] = node.children
277
278    component = deepcopy(value["component"])
279
280    if len(node.children) > 0:
281        slots: dict[str, tuple[Element, list]] = get_slots(component)
282        for child in node.children:
283            if isinstance(child, Element) and "slot" in child:
284                if child.get("slot") in slots:
285                    slots[child["slot"]][1].append(child)
286                    if isinstance(child, Element):
287                        child.parent = slots[child["slot"]][0]
288                        del child["slot"] 
289            elif "" in slots:
290                if isinstance(child, Element):
291                    child.parent = slots[""][0]
292                    del child["slot"] 
293                slots[""][1].append(child)
294
295        for slot in slots.values():
296            replace_node(
297                component,
298                slot[0],
299                slot[1] if len(slot[1]) > 0 else None
300            )
301
302    if component.tag in WRAPPER_TAG:
303        # Create a copy of the component
304        for sub_child in component.children:
305            if isinstance(sub_child, Element):
306                sub_child.context.update(properties)
307                sub_child.parent = node.parent
308
309        new_children.extend(component.children)
310    else:
311        component.context = properties
312        component.properties = attributes
313        component.parent = node.parent
314        new_children.append(component)
315
316    # replace the curr_node with the list of replaced nodes
317    parent = node.parent
318    index = parent.children.index(node)
319    return parent.children[:index] + new_children + parent.children[index+1:]
320
321def get_slots(node: Element) -> dict[str, tuple[Element, list]]:
322    """Get the slots found recursively in a Element."""
323
324    slots = {}
325    for slot in find_all(node, {"tag": "Slot"}):
326        name = slot.get("name", "")
327        if name == "":
328            if "" in slots:
329                raise KeyError("Can only have one slot without a name: <Slot />")
330            slots[""] = (slot, [])
331        else:
332            if name in slots:
333                raise KeyError("Must have unique slot names in components: <Slot name='unique' />")
334            slots[name] = (slot, [])
335    return slots
336
337def __add_component_elements(node, used_components: dict, tag: str):
338    if find(node, {"tag": tag}) is not None:
339        new_elements = __retrieve_component_elements(used_components, tag)
340        if len(new_elements) > 0:
341            replace_node(
342                node,
343                {"tag": tag},
344                combine_component_elements(
345                    [
346                        find(node, {"tag": tag}),
347                        *new_elements,
348                    ],
349                    tag,
350                ),
351            )
352    else:
353        new_element = combine_component_elements(
354            __retrieve_component_elements(used_components, tag),
355            tag,
356        )
357        if new_element.children[0].value.strip() != "":
358            if tag == "style":
359                head = query(node, "head")
360                if head is not None:
361                    head.append(new_element)
362                else:
363                    node.append(new_element)
364            else:
365                html = query(node, "html")
366                if html is not None:
367                    html.append(new_element)
368                else:
369                    node.append(new_element)
370
371
372def combine_component_elements(elements: list[Element], tag: str) -> Element:
373    """Combine text from elements like python, script, and style.
374
375    Returns:
376        Element: With tag of element list but with combined text content
377    """
378
379    values = []
380
381    indent = -1
382    for element in elements:
383        if len(element.children) == 1 and isinstance(element.children[0], Text):
384            # normalize values
385            if indent == -1:
386                indent = offset(element.children[0].value)
387            values.append(normalize_indent(element.children[0].value, indent))
388
389    return Element(tag, children=[Text("\n\n".join(values))])
390
391
392def __retrieve_component_elements(collection: dict, element: str) -> list[Element]:
393    result = []
394    for value in collection.values():
395        if element in value[0]:
396            result.extend(value[0][element])
397    return result
398
399def run_phml_if(child: Element, condition: str, **kwargs):
400    """Run the logic for manipulating the children on a `if` condition."""
401
402    clocals = build_locals(child, **kwargs)
403    result = get_python_result(sub(r"\{|\}", "", child[condition].strip()), **clocals)
404
405    if result:
406        return child
407
408    # Condition failed, so remove the node
409    return child
410
411
412def run_phml_elif(
413    child: Element,
414    condition: str,
415    variables: dict,
416    **kwargs,
417):
418    """Run the logic for manipulating the children on a `elif` condition."""
419
420    clocals = build_locals(child, **kwargs)
421
422    if variables["previous"][0] in variables["valid_prev"][condition] and variables["first_cond"]:
423        if not variables["previous"][1]:
424            result = get_python_result(sub(r"\{|\}", "", child[condition].strip()), **clocals)
425            if result:
426                return child
427
428    return child
429
430
431def run_phml_else(child: Element, condition: str, variables: dict, **kwargs):
432    """Run the logic for manipulating the children on a `else` condition."""
433
434    if variables["previous"][0] in variables["valid_prev"][condition] and variables["first_cond"]:
435        if not variables["previous"][1]:
436            clocals = build_locals(child, **kwargs)
437            result = get_python_result(sub(r"\{|\}", "", child[condition].strip()), **clocals)
438            if result:
439                return child
440
441    # Condition failed so remove element
442    return child
443
444def build_locals(child, **kwargs) -> dict:
445    """Build a dictionary of local variables from a nodes inherited locals and
446    the passed kwargs.
447    """
448    from phml.utilities import path  # pylint: disable=import-outside-toplevel
449
450    clocals = {**kwargs}
451
452    # Inherit locals from top down
453    for parent in path(child):
454        if parent.type == "element":
455            clocals.update(parent.context)
456
457    clocals.update(child.context)
458    return clocals
459
460def __get_previous_conditions(child: Element) -> list[str]:
461    idx = child.parent.children.index(child)
462    conditions = []
463    for i in range(0, idx):
464        if isinstance(child.parent.children[i], Element):
465            condition = py_condition(child.parent.children[i])
466            if condition is not None:
467                conditions.append(condition)
468
469    return conditions
def substitute_component( node: phml.core.nodes.nodes.Root | phml.core.nodes.nodes.Element | phml.core.nodes.AST.AST, component: tuple[str, dict], virtual_python: phml.core.virtual_python.vp.VirtualPython, **kwargs):
17def substitute_component(
18    node: Root | Element | AST,
19    component: tuple[str, dict],
20    virtual_python: VirtualPython,
21    **kwargs,
22):
23    """Replace the first occurance of a component.
24
25    Args:
26        node (Root | Element | AST): The starting point.
27        virtual_python (VirtualPython): The python state to use while evaluating prop values
28    """
29
30    if isinstance(node, AST):
31        node = node.tree
32
33    curr_node = find(node, ["element", {"tag": component[0]}])
34    used_components: dict[str, tuple[dict, dict]] = {}
35
36    if curr_node is not None:
37        context, used_components[component[0]] = process_context(*component, kwargs)
38        cmpt_props = used_components[component[0]][1].get("Props", None)
39
40        # Assign props to locals and remaining attributes stay
41        curr_node.parent.children = apply_component(
42            curr_node,
43            component[0],
44            component[1],
45            cmpt_props,
46            virtual_python,
47            context,
48            kwargs
49        )
50
51        __add_component_elements(node, used_components, "style")
52        __add_component_elements(node, used_components, "script")

Replace the first occurance of a component.

Args
  • node (Root | Element | AST): The starting point.
  • virtual_python (VirtualPython): The python state to use while evaluating prop values
def replace_components( node: phml.core.nodes.nodes.Root | phml.core.nodes.nodes.Element | phml.core.nodes.AST.AST, components: dict[str, dict], virtual_python: phml.core.virtual_python.vp.VirtualPython, **kwargs):
 54def replace_components(
 55    node: Root | Element | AST,
 56    components: dict[str, dict],
 57    virtual_python: VirtualPython,
 58    **kwargs,
 59):
 60    """Iterate through components and replace all of each component in the nodes children.
 61    Non-recursive.
 62
 63    Args:
 64        node (Root | Element | AST): The starting point.
 65        virtual_python (VirtualPython): Temp
 66    """
 67
 68    if isinstance(node, AST):
 69        node = node.tree
 70
 71    used_components: dict[str, tuple[dict, dict]] = {}
 72
 73    for name, value in components.items():
 74        elements = [element for element in node.children if check(element, {"tag": name})]
 75
 76        context = {}
 77        cmpt_props = None
 78        if len(elements) > 0:
 79            if components[name]["cache"] is not None:
 80                context.update({
 81                    key:value
 82                    for key,value in components[name]["cache"][1].items()
 83                    if key != "Props"
 84                })
 85                used_components[name] = components[name]["cache"]
 86                context.update(kwargs)
 87            else:
 88                context, used_components[name] = process_context(name, value["data"], kwargs)
 89                components[name]["cache"] = (
 90                    used_components[name][0],
 91                    {
 92                        key:value
 93                        for key,value in used_components[name][1].items()
 94                        if key not in kwargs
 95                    }
 96                )
 97
 98            cmpt_props = components[name]["cache"][1].get("Props", None)
 99
100        if "Props" in context and "data" in context["Props"]:
101            print(context["Props"], kwargs)
102
103        for curr_node in elements:
104            curr_node.parent.children = apply_component(
105                curr_node,
106                name,
107                value["data"],
108                cmpt_props,
109                virtual_python,
110                context,
111                kwargs
112            )
113
114    # Optimize, python, style, and script tags from components
115    __add_component_elements(node, used_components, "style")
116    __add_component_elements(node, used_components, "script")

Iterate through components and replace all of each component in the nodes children. Non-recursive.

Args
  • node (Root | Element | AST): The starting point.
  • virtual_python (VirtualPython): Temp
def combine_component_elements( elements: list[phml.core.nodes.nodes.Element], tag: str) -> phml.core.nodes.nodes.Element:
373def combine_component_elements(elements: list[Element], tag: str) -> Element:
374    """Combine text from elements like python, script, and style.
375
376    Returns:
377        Element: With tag of element list but with combined text content
378    """
379
380    values = []
381
382    indent = -1
383    for element in elements:
384        if len(element.children) == 1 and isinstance(element.children[0], Text):
385            # normalize values
386            if indent == -1:
387                indent = offset(element.children[0].value)
388            values.append(normalize_indent(element.children[0].value, indent))
389
390    return Element(tag, children=[Text("\n\n".join(values))])

Combine text from elements like python, script, and style.

Returns

Element: With tag of element list but with combined text content