Coverage for phml\compiler\__init__.py: 100%
99 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-05 15:06 -0500
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-05 15:06 -0500
1from collections.abc import Callable
2from copy import deepcopy
3from typing import Any
5from phml.components import ComponentManager
6from phml.embedded import Embedded
7from phml.helpers import normalize_indent
8from phml.nodes import AST, Element, Literal, LiteralType, Parent
10from .steps.base import comp_step
12from .steps import *
14__all__ = [
15 "SETUP",
16 "STEPS",
17 "POST",
18 "HypertextMarkupCompiler",
19 "comp_step"
20]
22SETUP: list[Callable] = []
24STEPS: list[Callable] = [
25 step_replace_phml_wrapper,
26 step_expand_loop_tags,
27 step_execute_conditions,
28 step_compile_markdown,
29 step_execute_embedded_python,
30 step_substitute_components,
31]
33POST: list[Callable] = [
34 step_add_cached_component_elements,
35]
38class HypertextMarkupCompiler:
39 def _get_python_elements(self, node: Parent) -> list[Element]:
40 result = []
41 for child in node:
42 if isinstance(child, Element):
43 if child.tag == "python":
44 result.append(child)
45 idx = node.index(child)
46 del node[idx]
47 else:
48 result.extend(self._get_python_elements(child))
50 return result
52 def _process_scope_(
53 self,
54 node: Parent,
55 components: ComponentManager,
56 context: dict,
57 ):
58 """Process steps for a given scope/parent node."""
60 # Core compile steps
61 for _step in STEPS:
62 _step(node, components, context)
64 # Recurse steps for each scope
65 for child in node:
66 if isinstance(child, Element):
67 self._process_scope_(child, components, context)
69 def compile(
70 self, node: Parent, _components: ComponentManager, **context: Any
71 ) -> Parent:
72 # get all python elements and process them
73 node = deepcopy(node)
74 p_elems = self._get_python_elements(node)
75 embedded = Embedded("")
76 for p_elem in p_elems:
77 embedded += Embedded(p_elem)
79 # Setup steps to collect data before comiling at different scopes
80 for step in SETUP:
81 step(node, _components, context)
83 # Recursively process scopes
84 context.update(embedded.context)
85 self._process_scope_(node, _components, context)
87 # Post compiling steps to finalize the ast
88 for step in POST:
89 step(node, _components, context)
91 return node
93 def _render_attribute(self, key: str, value: str | bool) -> str:
94 if isinstance(value, str):
95 return f'{key}="{value}"'
96 else:
97 return str(key) if value else f'{key}="false"'
99 def _render_element(
100 self,
101 element: Element,
102 indent: int = 0,
103 compress: str = "\n",
104 ) -> str:
105 attr_idt = 2
106 attrs = ""
107 lead_space = " " if len(element.attributes) > 0 else ""
108 if element.in_pre:
109 attrs = lead_space + " ".join(
110 self._render_attribute(key, value)
111 for key, value in element.attributes.items()
112 )
113 elif len(element.attributes) > 1:
114 idt = indent + attr_idt if compress == "\n" else 1
115 attrs = (
116 f"{compress}"
117 + " " * (idt)
118 + f'{compress}{" "*(idt)}'.join(
119 self._render_attribute(key, value)
120 for key, value in element.attributes.items()
121 )
122 + f"{compress}{' '*(indent)}"
123 )
124 elif len(element.attributes) == 1:
125 key, value = list(element.attributes.items())[0]
126 attrs = lead_space + self._render_attribute(key, value)
128 result = f"{' '*indent if not element.in_pre else ''}<{element.tag}{attrs}{'' if len(element) > 0 else '/'}>"
129 if len(element) == 0:
130 return result
132 if (
133 compress != "\n"
134 or element.in_pre
135 or (
136 element.tag not in ["script", "style", "python"]
137 and len(element) == 1
138 and Literal.is_text(element[0])
139 and "\n" not in element[0].content
140 and "\n" not in result
141 )
142 ):
143 children = self._render_tree_(element, _compress=compress)
144 result += children + f"</{element.tag}>"
145 else:
146 children = self._render_tree_(element, indent + 2, _compress=compress)
147 result += compress + children
148 result += f"{compress}{' '*indent}</{element.tag}>"
150 return result
152 def _render_literal(
153 self,
154 literal: Literal,
155 indent: int = 0,
156 compress: str = "\n",
157 ) -> str:
158 offset = " " * indent
159 if literal.in_pre:
160 offset = ""
161 compress = ""
162 content = literal.content
163 else:
164 content = literal.content.strip()
165 if compress == "\n":
166 content = normalize_indent(literal.content, indent)
167 content = content.strip()
168 elif not isinstance(literal.parent, AST) and literal.parent.tag in [
169 "python",
170 "script",
171 "style",
172 ]:
173 content = normalize_indent(literal.content)
174 content = content.strip()
175 offset = ""
176 else:
177 lines = content.split("\n")
178 content = f"{compress}{offset}".join(lines)
180 if literal.name == LiteralType.Text:
181 return offset + content
183 if literal.name == LiteralType.Comment:
184 return f"{offset}<!--" + content + "-->"
185 return "" # pragma: no cover
187 def _render_tree_(
188 self,
189 node: Parent,
190 indent: int = 0,
191 _compress: str = "\n",
192 ):
193 result = []
194 for child in node:
195 if isinstance(child, Element):
196 if child.tag == "doctype":
197 result.append("<!DOCTYPE html>")
198 else:
199 result.append(self._render_element(child, indent, _compress))
200 elif isinstance(child, Literal):
201 result.append(self._render_literal(child, indent, _compress))
202 else:
203 raise TypeError(f"Unknown renderable node type {type(child)}")
205 return _compress.join(result)
207 def render(
208 self,
209 node: Parent,
210 _compress: bool = False,
211 indent: int = 0,
212 ) -> str:
213 return self._render_tree_(node, indent, "" if _compress else "\n")