Coverage for phml\__init__.py: 100%
54 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-08 16:33 -0600
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-08 16:33 -0600
1"""Python Hypertext Markup Language (phml)
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.
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:
13```python
14<python>
15 if True:
16 print("Hello World")
17</python>
18```
20This phml python block will adjust the offset so that the python is executed as seen below:
22```python
23if True:
24 print("Hello World")
25```
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:
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>
45...
47<a href="{URL('youtube')}">Youtube</a>
48```
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.
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>`.
60> Note: Inline python blocks are only rendered in a Text element or inside an html attribute.
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.
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.
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:
82```html
83<ul>
84 <li py-for='i in range(3)'>
85 <p>{i}</p>
86 </li>
87</ul>
88```
90The compiled html will be:
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```
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`.
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')"`.
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.
126> :warning: This language is in early planning and development stages. All forms of feedback are
127encouraged.
128"""
130from pathlib import Path
131from typing import Callable, Optional
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 *
141__version__ = "1.0.0"
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 """
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 """
160 @property
161 def ast(self) -> AST:
162 """Reference to the parser attributes ast value."""
163 return self.parser.ast
165 @ast.setter
166 def ast(self, _ast: AST):
167 self.parser.ast = _ast
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 []
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.
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.
196 Note:
197 Any duplicate components will be replaced.
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 """
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
213 def remove(self, *components: str | All_Nodes):
214 """Remove an element from the list of element replacements.
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
223 def load(self, file_path: str | Path, handler: Optional[Callable] = None):
224 """Load a source files data and parse it to phml.
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
232 def parse(self, data: str | dict, handler: Optional[Callable] = None):
233 """Parse a str or dict object into phml.
235 Args:
236 data (str | dict): Object to parse to phml
237 """
238 self.parser.parse(data, handler)
239 return self
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.
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.
256 Returns:
257 str: The rendered content in the appropriate format.
258 """
260 scopes = scopes or ["./"]
261 for scope in self.scopes:
262 if scope not in scopes:
263 scopes.append(scope)
265 return self.compiler.compile(
266 self.parser.ast,
267 to_format=file_type,
268 indent=indent,
269 scopes=scopes,
270 **kwargs,
271 )
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.
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 """
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