Coverage for phml\__init__.py: 80%

44 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-30 09:38 -0600

1"""Python Hypertext Markup Language (phml) 

2 

3The idea behind the creation of Python in Hypertext Markup Language (phml), is to allow for web page generation with direct access to python. This language pulls directly from frameworks like VueJS. There is conditional rendering, components, python elements, inline/embedded python blocks, and much more. Now let's dive into more about this language. 

4 

5Let's start with the new `python` element. Python is a whitespace language. As such phml 

6has the challenge of maintaining the indentation in an appropriate way. With phml, I have made the decision to allow you to have as much leading whitespace as you want as long as the indentation is consistent. This means that indentation is based on the first lines offset. Take this phml example: 

7 

8```python 

9<python> 

10 if True: 

11 print("Hello World") 

12</python> 

13``` 

14 

15This phml python block will adjust the offset so that the python is executed as seen below: 

16 

17```python 

18if True: 

19 print("Hello World") 

20``` 

21 

22So now we can write python code, now what? You can define functions and variables 

23how you normally would and they are now available to the scope of the entire file. 

24Take, for instance, the example from above, the one with `py-src="urls('youtube')"`. 

25You can define the `URL` function in the `python` element and it can be accessed in an element. So the code would look like this: 

26 

27```html 

28<python> 

29def URL(link: str) -> str: 

30 links = { 

31 "youtube": "https://youtube.com" 

32 } 

33 if link in links: 

34 return links[link] 

35 else: 

36 return "" 

37</python> 

38 

39... 

40 

41<a href="{URL('youtube')}">Youtube</a> 

42``` 

43 

44phml combines all `python` elements and treats them as a python file. All local variables and imports are parsed and stored so that they may be accessed later. With that in mind that means you have the full power of the python programming language. 

45 

46Next up is inline python blocks. These are represented with `{}`. Any text in-between the brackets will be processed as python. This is mostly useful when you want to inject a value from python. Assume that there is a variable defined in the `python` element called `message` 

47and it contains `Hello World!`. Now this variable can be used like this, `<p>{ message }</p>`, 

48which renders to, `<p>Hello World!</p>`. 

49 

50> Note: Inline python blocks are only rendered in a Text element or inside an html attribute. 

51 

52Multiline blocks are a lot like inline python blocks, but they also have some differences. 

53You can do whatever you like inside this block, however if you expect a value to come from the block you must have at least one local variable. The last local variable defined in this block is used at the result/value. 

54 

55Conditional Rendering with `py-if`, `py-elif`, and `py-else` is an extremely helpful tool in phml. 

56`py-if` can be used alone and that the python inside it's value must be truthy for the element to be rendered. `py-elif` requires an element with a `py-if` or `py-elif` attribute immediately before it, and it's condition is rendered the same as `py-if` but only rendered if a `py-if` or `py-elif` first 

57fails. `py-else` requires there to be either a `py-if` or a `py-else` immediately before it. It only 

58renders if the previous element's condition fails. If `py-elif` or `py-else` is on an element, but 

59the previous element isn't a `py-if` or `py-elif` then an exception will occur. Most importantly, the first element in a chain of conditions must be a `py-if`. For ease of use, instead of writing `py-if`, `py-elif`, or `py-else` can be written as `@if`, `@elif`, or `@else` respectively. 

60 

61Other than conditions, there is also a built in `py-for` attribute. Any element with py-for will take a python for-loop expression that will be applied to that element. So if you did something like this: 

62 

63```html 

64<ul> 

65 <li py-for='i in range(3)'> 

66 <p>{i}</p> 

67 </li> 

68</ul> 

69``` 

70 

71The compiled html will be: 

72 

73```html 

74<ul> 

75 <li> 

76 <p>1</p> 

77 </li> 

78 <li> 

79 <p>2</p> 

80 </li> 

81 <li> 

82 <p>3</p> 

83 </li> 

84</ul> 

85``` 

86 

87The `for` and `:` in the for loops condition are optional. So you can combine `for`, `i in range(10)`, and `:` or leave out `for` and `:` at your discretion. `py-for` can also be written as `@for`. 

88 

89Python attributes are shortcuts for using inline python blocks in html attributes. Normally, in 

90phml, you would inject python logic into an attribute similar to this: `src="{url('youtube')}"`. If you would like to make the whole attribute value a python expression you may prefix any attribute with a `py-` or `:`. This keeps the attribute name the same after the prefix, but tells 

91the parser that the entire value should be processed as python. So the previous example can also be expressed as `py-src="URL('youtube')"` or `:src="URL('youtube')"`. 

92 

93This language also has the ability to convert back to html and json with converting to html having more features. Converting to json is just a json representation of a phml ast. However, converting to html is where the magic happens. The compiler executes python blocks, substitutes components, and processes conditions to create a final html string that is dynamic to its original ast. A user may pass additional kwargs to the compiler to expose additional data to the execution of python blocks. If you wish to compile to a non supported language the compiler can take a callable that returns the final string. It passes all the data; components, kwargs, ast, etc… So if a user wishes to extend the language thay may. 

94 

95> :warning: This language is in early planning and development stages. All forms of feedback are encouraged. 

96""" 

97 

98from pathlib import Path 

99from typing import Optional 

100 

101from . import builder, core, nodes, utils, virtual_python 

102from .core import Compiler, Parser, file_types 

103from .nodes import AST, All_Nodes 

104 

105__version__ = "0.1.0" 

106__all__ = [ 

107 "PHMLCore", 

108 "Compiler", 

109 "Parser", 

110 "file_types", 

111 "AST", 

112 "core", 

113 "nodes", 

114 "utils", 

115 "virtual_python", 

116 "file_types", 

117 "builder", 

118] 

119 

120 

121class PHMLCore: 

122 """A helper class that bundles the functionality 

123 of the parser and compiler together. Allows for loading source files, 

124 parsing strings and dicts, rendering to a different format, and finally 

125 writing the results of a render to a file. 

126 """ 

127 

128 parser: Parser 

129 """Instance of a [Parser][phml.parser.Parser].""" 

130 compiler: Compiler 

131 """Instance of a [Compiler][phml.compile.Compiler].""" 

132 

133 @property 

134 def ast(self) -> AST: 

135 return self.parser.ast 

136 

137 @ast.setter 

138 def ast(self, _ast: AST): 

139 self.parser.ast = _ast 

140 

141 def __init__( 

142 self, 

143 components: Optional[dict[str, dict[str, list | All_Nodes]]] = None, 

144 ): 

145 self.parser = Parser() 

146 self.compiler = Compiler(components=components) 

147 

148 def add( 

149 self, 

150 *components: dict[str, dict[str, list | All_Nodes] | AST] 

151 | tuple[str, dict[str, list | All_Nodes] | AST] 

152 | Path, 

153 ): 

154 """Add a component to the element replacement list. 

155 

156 Components passed in can be of a few types. The first type it can be is a 

157 pathlib.Path type. This will allow for automatic parsing of the file at the 

158 path and then the filename and parsed ast are passed to the compiler. It can 

159 also be a dictionary of str being the name of the element to be replaced. 

160 The name can be snake case, camel case, or pascal cased. The value can either 

161 be the parsed result of the component from phml.utils.parse_component() or the 

162 parsed ast of the component. Lastely, the component can be a tuple. The first 

163 value is the name of the element to be replaced; with the second value being 

164 either the parsed result of the component or the component's ast. 

165 

166 Note: 

167 Any duplicate components will be replaced. 

168 

169 Args: 

170 components: Any number values indicating 

171 name of the component and the the component. The name is used 

172 to replace a element with the tag==name. 

173 """ 

174 from phml.utils import filename_from_path 

175 

176 for component in components: 

177 if isinstance(component, Path): 

178 self.parser.load(component) 

179 self.compiler.add((filename_from_path(component), self.parser.ast)) 

180 else: 

181 self.compiler.add(component) 

182 return self 

183 

184 def remove(self, *components: str | All_Nodes): 

185 """Remove an element from the list of element replacements. 

186 

187 Takes any number of strings or node objects. If a string is passed 

188 it is used as the key that will be removed. If a node object is passed 

189 it will attempt to find a matching node and remove it. 

190 """ 

191 self.compiler.remove(*components) 

192 return self 

193 

194 def load(self, path: str | Path): 

195 """Load a source files data and parse it to phml. 

196 

197 Args: 

198 path (str | Path): The path to the source file. 

199 """ 

200 self.parser.load(path) 

201 return self 

202 

203 def parse(self, data: str | dict): 

204 """Parse a str or dict object into phml. 

205 

206 Args: 

207 data (str | dict): Object to parse to phml 

208 """ 

209 self.parser.parse(data) 

210 return self 

211 

212 def render( 

213 self, file_type: str = file_types.HTML, indent: Optional[int] = None, **kwargs 

214 ) -> str: 

215 """Render the parsed ast to a different format. Defaults to rendering to html. 

216 

217 Args: 

218 file_type (str): The format to render to. Currently support html, phml, and json. 

219 indent (Optional[int], optional): The number of spaces per indent. By default it will 

220 use the standard for the given format. HTML has 4 spaces, phml has 4 spaces, and json 

221 has 2 spaces. 

222 

223 Returns: 

224 str: The rendered content in the appropriate format. 

225 """ 

226 return self.compiler.compile(self.parser.ast, to_format=file_type, indent=indent, **kwargs) 

227 

228 def write( 

229 self, 

230 dest: str | Path, 

231 file_type: str = file_types.HTML, 

232 indent: Optional[int] = None, 

233 **kwargs, 

234 ): 

235 """Renders the parsed ast to a different format, then writes 

236 it to a given file. Defaults to rendering and writing out as html. 

237 

238 Args: 

239 dest (str | Path): The path to the file to be written to. 

240 file_type (str): The format to render the ast as. 

241 indent (Optional[int], optional): The number of spaces per indent. By default it will 

242 use the standard for the given format. HTML has 4 spaces, phml has 4 spaces, and json 

243 has 2 spaces. 

244 kwargs: Any additional data to pass to the compiler that will be exposed to the 

245 phml files. 

246 """ 

247 with open(dest, "+w", encoding="utf-8") as dest_file: 

248 dest_file.write(self.render(file_type=file_type, indent=indent, **kwargs)) 

249 return self