Coverage for phml\__init__.py: 100%

54 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-08 16:33 -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 

4generation with direct access to python. This language pulls directly from frameworks like VueJS. 

5There is conditional rendering, components, python elements, inline/embedded python blocks, and much 

6more. Now let's dive into more about this language. 

7 

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

9has the challenge of maintaining the indentation in an appropriate way. With phml, I have made the 

10decision to allow you to have as much leading whitespace as you want as long as the indentation is 

11consistent. This means that indentation is based on the first lines offset. Take this phml example: 

12 

13```python 

14<python> 

15 if True: 

16 print("Hello World") 

17</python> 

18``` 

19 

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

21 

22```python 

23if True: 

24 print("Hello World") 

25``` 

26 

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

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

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

30You can define the `URL` function in the `python` element and it can be accessed in an element. So 

31the code would look like this: 

32 

33```html 

34<python> 

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

36 links = { 

37 "youtube": "https://youtube.com" 

38 } 

39 if link in links: 

40 return links[link] 

41 else: 

42 return "" 

43</python> 

44 

45... 

46 

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

48``` 

49 

50phml combines all `python` elements and treats them as a python file. All local variables and 

51imports are parsed and stored so that they may be accessed later. With that in mind that means you 

52have the full power of the python programming language. 

53 

54Next up is inline python blocks. These are represented with `{}`. Any text in-between the brackets 

55will be processed as python. This is mostly useful when you want to inject a value from python. 

56Assume that there is a variable defined in the `python` element called `message` 

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

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

59 

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

61 

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

63You can do whatever you like inside this block, however if you expect a value to come from the block 

64you must have at least one local variable. The last local variable defined in this block is used at 

65the result/value. 

66 

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

68`py-if` can be used alone and that the python inside it's value must be truthy for the element to be 

69rendered. `py-elif` requires an element with a `py-if` or `py-elif` attribute immediately before 

70it, and it's condition is rendered the same as `py-if` but only rendered if a `py-if` or `py-elif` 

71first 

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

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

74the previous element isn't a `py-if` or `py-elif` then an exception will occur. Most importantly, 

75the first element in a chain of conditions must be a `py-if`. For ease of use, instead of writing 

76`py-if`, `py-elif`, or `py-else` can be written as `@if`, `@elif`, or `@else` respectively. 

77 

78Other than conditions, there is also a built in `py-for` attribute. Any element with py-for will 

79take a python for-loop expression that will be applied to that element. So if you did something like 

80this: 

81 

82```html 

83<ul> 

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

85 <p>{i}</p> 

86 </li> 

87</ul> 

88``` 

89 

90The compiled html will be: 

91 

92```html 

93<ul> 

94 <li> 

95 <p>1</p> 

96 </li> 

97 <li> 

98 <p>2</p> 

99 </li> 

100 <li> 

101 <p>3</p> 

102 </li> 

103</ul> 

104``` 

105 

106The `for` and `:` in the for loops condition are optional. So you can combine `for`, 

107`i in range(10)`, and `:` or leave out `for` and `:` at your discretion. `py-for` can also be 

108written as `@for`. 

109 

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

111phml, you would inject python logic into an attribute similar to this: `src="{url('youtube')}"`. If 

112you would like to make the whole attribute value a python expression you may prefix any attribute 

113with a `py-` or `:`. This keeps the attribute name the same after the prefix, but tells 

114the parser that the entire value should be processed as python. So the previous example can also be 

115expressed as `py-src="URL('youtube')"` or `:src="URL('youtube')"`. 

116 

117This language also has the ability to convert back to html and json with converting to html having 

118more features. Converting to json is just a json representation of a phml ast. However, converting 

119to html is where the magic happens. The compiler executes python blocks, substitutes components, and 

120processes conditions to create a final html string that is dynamic to its original ast. A user may 

121pass additional kwargs to the compiler to expose additional data to the execution of python blocks. 

122If you wish to compile to a non supported language the compiler can take a callable that returns the 

123final string. It passes all the data; components, kwargs, ast, etc… So if a user wishes to extend 

124the language thay may. 

125 

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

127encouraged. 

128""" 

129 

130from pathlib import Path 

131from typing import Callable, Optional 

132 

133from .core import Compiler, Parser, file_types 

134from .locate import * 

135from .misc import * 

136from .nodes import AST, All_Nodes 

137from .transform import * 

138from .travel import * 

139from .validate import * 

140 

141__version__ = "1.0.0" 

142 

143 

144class PHMLCore: 

145 """A helper class that bundles the functionality 

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

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

148 writing the results of a render to a file. 

149 """ 

150 

151 parser: Parser 

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

153 compiler: Compiler 

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

155 scopes: Optional[list[str]] 

156 """List of paths from cwd to auto add to python path. This helps with 

157 importing inside of phml files. 

158 """ 

159 

160 @property 

161 def ast(self) -> AST: 

162 """Reference to the parser attributes ast value.""" 

163 return self.parser.ast 

164 

165 @ast.setter 

166 def ast(self, _ast: AST): 

167 self.parser.ast = _ast 

168 

169 def __init__( 

170 self, 

171 scopes: Optional[list[str]] = None, 

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

173 ): 

174 self.parser = Parser() 

175 self.compiler = Compiler(components=components) 

176 self.scopes = scopes or [] 

177 

178 def add( 

179 self, 

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

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

182 | Path, 

183 ): 

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

185 

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

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

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

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

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

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

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

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

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

195 

196 Note: 

197 Any duplicate components will be replaced. 

198 

199 Args: 

200 components: Any number values indicating 

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

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

203 """ 

204 

205 for component in components: 

206 if isinstance(component, Path): 

207 self.parser.load(component) 

208 self.compiler.add((filename_from_path(component), parse_component(self.parser.ast))) 

209 elif isinstance(component, dict): 

210 self.compiler.add(*list(component.items())) 

211 return self 

212 

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

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

215 

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

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

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

219 """ 

220 self.compiler.remove(*components) 

221 return self 

222 

223 def load(self, file_path: str | Path, handler: Optional[Callable] = None): 

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

225 

226 Args: 

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

228 """ 

229 self.parser.load(file_path, handler) 

230 return self 

231 

232 def parse(self, data: str | dict, handler: Optional[Callable] = None): 

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

234 

235 Args: 

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

237 """ 

238 self.parser.parse(data, handler) 

239 return self 

240 

241 def render( 

242 self, 

243 file_type: str = file_types.HTML, 

244 indent: Optional[int] = None, 

245 scopes: Optional[list[str]] = None, 

246 **kwargs, 

247 ) -> str: 

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

249 

250 Args: 

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

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

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

254 has 2 spaces. 

255 

256 Returns: 

257 str: The rendered content in the appropriate format. 

258 """ 

259 

260 scopes = scopes or ["./"] 

261 for scope in self.scopes: 

262 if scope not in scopes: 

263 scopes.append(scope) 

264 

265 return self.compiler.compile( 

266 self.parser.ast, 

267 to_format=file_type, 

268 indent=indent, 

269 scopes=scopes, 

270 **kwargs, 

271 ) 

272 

273 def write( 

274 self, 

275 dest: str | Path, 

276 file_type: str = file_types.HTML, 

277 indent: Optional[int] = None, 

278 scopes: Optional[list[str]] = None, 

279 **kwargs, 

280 ): 

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

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

283 

284 Args: 

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

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

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

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

289 has 2 spaces. 

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

291 phml files. 

292 """ 

293 

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

295 dest_file.write( 

296 self.render(file_type=file_type, indent=indent, scopes=scopes, **kwargs) 

297 ) 

298 return self