phml.core.formats.compile.reserved

  1from __future__ import annotations
  2
  3from copy import deepcopy
  4from re import match, sub
  5from traceback import print_exc
  6
  7from saimll import SAIML
  8from markdown import Markdown
  9
 10from phml.core.nodes import Root, Element, Text
 11from phml.core.virtual_python import VirtualPython, get_python_result
 12from phml.utilities import (
 13    visit_children,
 14    check,
 15    replace_node,
 16    # sanatize
 17)
 18
 19__all__ = [
 20    "RESERVED"
 21]
 22
 23EXTRAS = [
 24    "cuddled-lists",
 25    "fenced-code-blocks",
 26    "header-ids",
 27    "footnotes",
 28    "strike",
 29]
 30
 31MARKDOWN = Markdown(extensions=["codehilite", "tables", "fenced_code"])
 32
 33def process_loops(node: Root | Element, virtual_python: VirtualPython, **kwargs):
 34    """Expands all `<For />` tags giving their children context for each iteration."""
 35
 36    for_loops = [
 37        loop
 38        for loop in visit_children(node)
 39        if check(loop, {"tag": "For"})
 40    ]
 41
 42    kwargs.update(virtual_python.context)
 43
 44    for loop in for_loops:
 45        each = loop.get(":each", loop.get("each"))
 46        if each is not None and each.strip() != "":
 47            children = run_phml_for(loop, **kwargs)
 48            replace_node(node, loop, children)
 49        else:
 50            replace_node(node, loop, None)
 51
 52def process_markdown(node: Root | Element, virtual_python: VirtualPython, **kwargs):
 53    """Replace the `<Markdown />` element with it's `src` attributes parsed markdown
 54    string."""
 55
 56    from phml import PHML
 57
 58    md_elems: list[Element] = [
 59        loop
 60        for loop in visit_children(node)
 61        if check(loop, {"tag": "Markdown"})
 62    ]
 63
 64    kwargs.update(virtual_python.context)
 65    context = build_locals(node, **kwargs)
 66
 67    # Don't escape the html values from context for html tags in markdown strings
 68    kwargs["safe_vars"] = True
 69
 70    for elem in md_elems:
 71        markdown = MARKDOWN
 72        rendered_html = []
 73
 74        # If extras are provided then add them as extensions. Don't allow configs
 75        # and only allow extensions built into the markdown package.
 76        if "extra" in elem:
 77            markdown = deepcopy(MARKDOWN)
 78            markdown.registerExtensions(
 79                [
 80                    extra.strip()
 81                    for extra in elem["extra"].split(" ")
 82                    if extra.strip() != ""
 83                ],
 84                {}
 85            )
 86        elif ":extra" in elem:
 87            extra_list = get_python_result(elem[":extra"], **context)
 88            if not isinstance(extra_list, list):
 89                raise TypeError("Expected extra's to be a list of strings")
 90            markdown = deepcopy(MARKDOWN)
 91            markdown.registerExtensions(extra_list, {})
 92
 93        # Append the rendered markdown from the children, src, and referenced
 94        # file in that order
 95        if (
 96            not elem.startend
 97            and len(elem.children) == 1
 98            and isinstance(elem.children[0], Text)
 99        ):
100            html = markdown.reset().convert(elem.children[0].normalized())
101            rendered_html.append(html)
102
103        if ":src" in elem or "src" in elem:
104            if ":src" in elem:
105                src = str(
106                    get_python_result(
107                        elem[":src"],
108                        **context
109                    )
110                )
111            else:
112                src = elem["src"]
113            html = markdown.reset().convert(src)
114            rendered_html.append(html)
115
116        if "file" in elem:
117            with open(elem["file"], "r", encoding="utf-8") as md_file:
118                html = markdown.reset().convert(md_file.read())
119
120            rendered_html.append(html)
121
122        # Replace node with rendered nodes of the markdown. Remove node if no
123        # markdown was provided
124        if len(rendered_html) > 0:
125            replace_node(
126                node,
127                elem,
128                PHML().parse('\n'.join(rendered_html)).ast.tree.children
129            )
130        else:
131            replace_node(
132                node,
133                elem,
134                None
135            )
136
137
138def process_html(
139    node: Root | Element,
140    virtual_python: VirtualPython,
141    **kwargs
142):
143    """Replace the `<HTML />` element with it's `src` attributes html string.
144    """
145
146    from phml import PHML
147
148    html_elems: list[Element] = [
149        elem 
150        for elem in visit_children(node)
151        if check(elem, {"tag": "HTML"})
152    ]
153
154    kwargs.update(virtual_python.context)
155    context = build_locals(node, **kwargs)
156
157    # Don't escape the html values from context
158    kwargs["safe_vars"] = True
159
160    for elem in html_elems:
161        if not elem.startend:
162            raise TypeError(
163                f"<HTML /> elements are not allowed to have children \
164elements: {elem.position}"
165            )
166
167        if ":src" in elem or "src" in elem:
168            if ":src" in elem:
169                src = str(get_python_result(elem[":src"], **context))
170            else:
171                src = str(elem["src"])
172
173            ast = PHML().parse(src).ast
174            print(ast.tree)
175            # sanatize(ast)
176
177            replace_node(node, elem, ast.tree.children)
178        else:
179            print("REMOVING HTML NODE")
180            replace_node(node, elem, None)
181
182def build_locals(child, **kwargs) -> dict:
183    """Build a dictionary of local variables from a nodes inherited locals and
184    the passed kwargs.
185    """
186    from phml.utilities import path  # pylint: disable=import-outside-toplevel
187
188    clocals = {**kwargs}
189
190    # Inherit locals from top down
191    for parent in path(child):
192        if parent.type == "element":
193            clocals.update(parent.context)
194
195    if hasattr(child, "context"):
196        clocals.update(child.context)
197    return clocals
198
199def run_phml_for(node: Element, **kwargs) -> list:
200    """Repeat the nested elements inside the `<For />` elements for the iterations provided by the
201    `:each` attribute. The values from the `:each` attribute are exposed as context for each node in
202    the `<For />` element for each iteration.
203
204    Args:
205        node (Element): The `<For />` element that is to be used.
206    """
207    clocals = build_locals(node)
208
209    # Format for loop condition
210    for_loop = sub(r"for |:\s*$", "", node.get(":each", node.get("each"))).strip()
211
212    # Get local var names from for loop condition
213    items = match(r"(for )?(.*)(?<= )in(?= )(.+)", for_loop)
214
215    new_locals = [
216        item.strip()
217        for item in sub(
218            r"\s+",
219            " ",
220            items.group(2),
221        ).split(",")
222    ]
223
224    items.group(3)
225
226    # Formatter for key value pairs
227    key_value = "\"{key}\": {key}"
228
229    # Set children position to 0 since all copies are generated
230    children = node.children
231    for child in children:
232        child.position = None
233
234    def children_with_context(context: dict):
235        new_children = []
236        for child in children:
237            new_child = deepcopy(child)
238            if check(new_child, "element"):
239                new_child.context.update(context)
240            new_children.append(new_child)
241        return new_children
242
243    expression = for_loop # original expression
244
245    for_loop = f'''\
246new_children = []
247for {for_loop}:
248    new_children.extend(
249        children_with_context(
250            {{{", ".join([f"{key_value.format(key=key)}" for key in new_locals])}}}
251        )
252    )
253'''
254
255    # Construct locals for dynamic for loops execution
256    local_env = {
257        "local_vals": clocals,
258    }
259
260    try:
261        # Execute dynamic for loop
262        exec(  # pylint: disable=exec-used
263            for_loop,
264            {
265                **kwargs,
266                **globals(),
267                **clocals,
268                "children_with_context": children_with_context
269            },
270            local_env,
271        )
272    except Exception:  # pylint: disable=broad-except
273        SAIML.print(f"\\[[@Fred]*Error[@]\\] Failed to execute loop expression \
274[@Fblue]@for[@]=[@Fgreen]'[@]{expression}[@Fgreen]'[@]")
275        print_exc()
276
277    # Return the new complete list of children after generation
278    return local_env["new_children"]
279
280RESERVED = {
281    "For": process_loops,
282    "Markdown": process_markdown,
283    "HTML": process_html
284}
RESERVED = {'For': <function process_loops>, 'Markdown': <function process_markdown>, 'HTML': <function process_html>}