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, 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) -> list:
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
227def process_context(name, value, kwargs: dict | None = None):
228    """Process the python elements and context of the component and extract the relavent context."""
229    context = {}
230    local_virtual_python = VirtualPython(context=dict(kwargs or {}))
231    for python in value["python"]:
232        if len(python.children) == 1 and check(python.children[0], "text"):
233            text = python.children[0].normalized()
234            local_virtual_python += VirtualPython(text, context=local_virtual_python.context)
235            
236    if "Props" in local_virtual_python.context:
237        if not isinstance(local_virtual_python.context["Props"], dict):
238            raise Exception(
239                f"Props must be a dict was "
240                + f"{type(local_virtual_python.context['Props']).__name__}: <{name} />"
241            )
242
243    context.update({
244        key:value
245        for key,value in local_virtual_python.context.items()
246        if key != "Props"
247    })
248
249    return context, (value, local_virtual_python.context)
250
251def apply_component(node, name, value, cmpt_props, virtual_python, context, kwargs) -> list:
252    """Get the props, execute conditions and replace components in the node tree."""
253    props, attrs = get_props(
254        node,
255        name,
256        value,
257        virtual_python,
258        cmpt_props,
259        **kwargs
260    )
261
262    node.properties = attrs
263    node.context.update(props)
264
265    condition = py_condition(node)
266    results = [node]
267    if condition is not None:
268        results = execute_condition(condition, node, virtual_python, **kwargs)
269
270    # replace the valid components in the results list
271    new_children = []
272    for child in results:
273        # get props and locals from current node
274        properties, attributes = node.context, child.properties
275        properties.update(context)
276        properties["children"] = node.children
277
278        component = deepcopy(value["component"])
279        if component.tag in WRAPPER_TAG:
280            # Create a copy of the component
281            for sub_child in component.children:
282                if isinstance(sub_child, Element):
283                    sub_child.context.update(properties)
284                    sub_child.parent = node.parent
285
286            new_children.extend(component.children)
287        else:
288            component.context = properties
289            component.properties = attributes
290            component.parent = node.parent
291            new_children.append(component)
292
293    # replace the curr_node with the list of replaced nodes
294    parent = node.parent
295    index = parent.children.index(node)
296    return parent.children[:index] + new_children + parent.children[index+1:]
297
298def __add_component_elements(node, used_components: dict, tag: str):
299    if find(node, {"tag": tag}) is not None:
300        new_elements = __retrieve_component_elements(used_components, tag)
301        if len(new_elements) > 0:
302            replace_node(
303                node,
304                {"tag": tag},
305                combine_component_elements(
306                    [
307                        find(node, {"tag": tag}),
308                        *new_elements,
309                    ],
310                    tag,
311                ),
312            )
313    else:
314        new_element = combine_component_elements(
315            __retrieve_component_elements(used_components, tag),
316            tag,
317        )
318        if new_element.children[0].value.strip() != "":
319            if tag == "style":
320                head = query(node, "head")
321                if head is not None:
322                    head.append(new_element)
323                else:
324                    node.append(new_element)
325            else:
326                html = query(node, "html")
327                if html is not None:
328                    html.append(new_element)
329                else:
330                    node.append(new_element)
331
332
333def combine_component_elements(elements: list[Element], tag: str) -> Element:
334    """Combine text from elements like python, script, and style.
335
336    Returns:
337        Element: With tag of element list but with combined text content
338    """
339
340    values = []
341
342    indent = -1
343    for element in elements:
344        if len(element.children) == 1 and isinstance(element.children[0], Text):
345            # normalize values
346            if indent == -1:
347                indent = offset(element.children[0].value)
348            values.append(normalize_indent(element.children[0].value, indent))
349
350    return Element(tag, children=[Text("\n\n".join(values))])
351
352
353def __retrieve_component_elements(collection: dict, element: str) -> list[Element]:
354    result = []
355    for value in collection.values():
356        if element in value[0]:
357            result.extend(value[0][element])
358    return result
359
360def run_phml_if(child: Element, condition: str, **kwargs):
361    """Run the logic for manipulating the children on a `if` condition."""
362
363    clocals = build_locals(child, **kwargs)
364    result = get_python_result(sub(r"\{|\}", "", child[condition].strip()), **clocals)
365
366    if result:
367        return child
368
369    # Condition failed, so remove the node
370    return child
371
372
373def run_phml_elif(
374    child: Element,
375    condition: str,
376    variables: dict,
377    **kwargs,
378):
379    """Run the logic for manipulating the children on a `elif` condition."""
380
381    clocals = build_locals(child, **kwargs)
382
383    if variables["previous"][0] in variables["valid_prev"][condition] and variables["first_cond"]:
384        if not variables["previous"][1]:
385            result = get_python_result(sub(r"\{|\}", "", child[condition].strip()), **clocals)
386            if result:
387                return child
388
389    return child
390
391
392def run_phml_else(child: Element, condition: str, variables: dict, **kwargs):
393    """Run the logic for manipulating the children on a `else` condition."""
394
395    if variables["previous"][0] in variables["valid_prev"][condition] and variables["first_cond"]:
396        if not variables["previous"][1]:
397            clocals = build_locals(child, **kwargs)
398            result = get_python_result(sub(r"\{|\}", "", child[condition].strip()), **clocals)
399            if result:
400                return child
401
402    # Condition failed so remove element
403    return child
404
405def build_locals(child, **kwargs) -> dict:
406    """Build a dictionary of local variables from a nodes inherited locals and
407    the passed kwargs.
408    """
409    from phml.utilities import path  # pylint: disable=import-outside-toplevel
410
411    clocals = {**kwargs}
412
413    # Inherit locals from top down
414    for parent in path(child):
415        if parent.type == "element":
416            clocals.update(parent.context)
417
418    clocals.update(child.context)
419    return clocals
420
421def __get_previous_conditions(child: Element) -> list[str]:
422    idx = child.parent.children.index(child)
423    conditions = []
424    for i in range(0, idx):
425        if isinstance(child.parent.children[i], Element):
426            condition = py_condition(child.parent.children[i])
427            if condition is not None:
428                conditions.append(condition)
429
430    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.

Arguments:
  • 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.

Arguments:
  • 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:
334def combine_component_elements(elements: list[Element], tag: str) -> Element:
335    """Combine text from elements like python, script, and style.
336
337    Returns:
338        Element: With tag of element list but with combined text content
339    """
340
341    values = []
342
343    indent = -1
344    for element in elements:
345        if len(element.children) == 1 and isinstance(element.children[0], Text):
346            # normalize values
347            if indent == -1:
348                indent = offset(element.children[0].value)
349            values.append(normalize_indent(element.children[0].value, indent))
350
351    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