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

1from collections.abc import Callable 

2from copy import deepcopy 

3from typing import Any 

4 

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 

9 

10from .steps.base import comp_step 

11 

12from .steps import * 

13 

14__all__ = [ 

15 "SETUP", 

16 "STEPS", 

17 "POST", 

18 "HypertextMarkupCompiler", 

19 "comp_step" 

20] 

21 

22SETUP: list[Callable] = [] 

23 

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] 

32 

33POST: list[Callable] = [ 

34 step_add_cached_component_elements, 

35] 

36 

37 

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)) 

49 

50 return result 

51 

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.""" 

59 

60 # Core compile steps 

61 for _step in STEPS: 

62 _step(node, components, context) 

63 

64 # Recurse steps for each scope 

65 for child in node: 

66 if isinstance(child, Element): 

67 self._process_scope_(child, components, context) 

68 

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) 

78 

79 # Setup steps to collect data before comiling at different scopes 

80 for step in SETUP: 

81 step(node, _components, context) 

82 

83 # Recursively process scopes 

84 context.update(embedded.context) 

85 self._process_scope_(node, _components, context) 

86 

87 # Post compiling steps to finalize the ast 

88 for step in POST: 

89 step(node, _components, context) 

90 

91 return node 

92 

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"' 

98 

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) 

127 

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 

131 

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}>" 

149 

150 return result 

151 

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) 

179 

180 if literal.name == LiteralType.Text: 

181 return offset + content 

182 

183 if literal.name == LiteralType.Comment: 

184 return f"{offset}<!--" + content + "-->" 

185 return "" # pragma: no cover 

186 

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)}") 

204 

205 return _compress.join(result) 

206 

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")