phml
Python Hypertext Markup Language (phml)
The 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.
Let's start with the new python
element. Python is a whitespace language. As such phml
has 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:
<python>
if True:
print("Hello World")
</python>
This phml python block will adjust the offset so that the python is executed as seen below:
if True:
print("Hello World")
So now we can write python code, now what? You can define functions and variables
how you normally would and they are now available to the scope of the entire file.
Take, for instance, the example from above, the one with py-src="urls('youtube')"
.
You can define the URL
function in the python
element and it can be accessed in an element. So
the code would look like this:
<python>
def URL(link: str) -> str:
links = {
"youtube": "https://youtube.com"
}
if link in links:
return links[link]
else:
return ""
</python>
...
<a href="{URL('youtube')}">Youtube</a>
phml 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.
Next 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
and it contains Hello World!
. Now this variable can be used like this, <p>{ message }</p>
,
which renders to, <p>Hello World!</p>
.
Note: Inline python blocks are only rendered in a Text element or inside an html attribute.
Multiline blocks are a lot like inline python blocks, but they also have some differences. You 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.
Conditional Rendering with py-if
, py-elif
, and py-else
is an extremely helpful tool in phml.
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
fails. py-else
requires there to be either a py-if
or a py-else
immediately before it. It only
renders if the previous element's condition fails. If py-elif
or py-else
is on an element, but
the 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.
Other 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:
<ul>
<li py-for='i in range(3)'>
<p>{i}</p>
</li>
</ul>
The compiled html will be:
<ul>
<li>
<p>1</p>
</li>
<li>
<p>2</p>
</li>
<li>
<p>3</p>
</li>
</ul>
The 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
.
Python attributes are shortcuts for using inline python blocks in html attributes. Normally, in
phml, 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
the 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')"
.
This 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.
:warning: This language is in early planning and development stages. All forms of feedback are encouraged.
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
145class PHMLCore: 146 """A helper class that bundles the functionality 147 of the parser and compiler together. Allows for loading source files, 148 parsing strings and dicts, rendering to a different format, and finally 149 writing the results of a render to a file. 150 """ 151 152 parser: Parser 153 """Instance of a [Parser][phml.parser.Parser].""" 154 compiler: Compiler 155 """Instance of a [Compiler][phml.compile.Compiler].""" 156 scopes: Optional[list[str]] 157 """List of paths from cwd to auto add to python path. This helps with 158 importing inside of phml files. 159 """ 160 161 @property 162 def ast(self) -> AST: 163 """Reference to the parser attributes ast value.""" 164 return self.parser.ast 165 166 @ast.setter 167 def ast(self, _ast: AST): 168 self.parser.ast = _ast 169 170 def __init__( 171 self, 172 scopes: Optional[list[str]] = None, 173 components: Optional[dict[str, dict[str, list | All_Nodes]]] = None, 174 ): 175 self.parser = Parser() 176 self.compiler = Compiler(components=components) 177 self.scopes = scopes or [] 178 179 def add( 180 self, 181 *components: dict[str, dict[str, list | All_Nodes] | AST] 182 | tuple[str, dict[str, list | All_Nodes] | AST] 183 | Path, 184 ): 185 """Add a component to the element replacement list. 186 187 Components passed in can be of a few types. The first type it can be is a 188 pathlib.Path type. This will allow for automatic parsing of the file at the 189 path and then the filename and parsed ast are passed to the compiler. It can 190 also be a dictionary of str being the name of the element to be replaced. 191 The name can be snake case, camel case, or pascal cased. The value can either 192 be the parsed result of the component from phml.utils.parse_component() or the 193 parsed ast of the component. Lastely, the component can be a tuple. The first 194 value is the name of the element to be replaced; with the second value being 195 either the parsed result of the component or the component's ast. 196 197 Note: 198 Any duplicate components will be replaced. 199 200 Args: 201 components: Any number values indicating 202 name of the component and the the component. The name is used 203 to replace a element with the tag==name. 204 """ 205 206 for component in components: 207 if isinstance(component, Path): 208 self.parser.load(component) 209 self.compiler.add((filename_from_path(component), parse_component(self.parser.ast))) 210 elif isinstance(component, dict): 211 self.compiler.add(*list(component.items())) 212 return self 213 214 def remove(self, *components: str | All_Nodes): 215 """Remove an element from the list of element replacements. 216 217 Takes any number of strings or node objects. If a string is passed 218 it is used as the key that will be removed. If a node object is passed 219 it will attempt to find a matching node and remove it. 220 """ 221 self.compiler.remove(*components) 222 return self 223 224 def load(self, file_path: str | Path, handler: Optional[Callable] = None): 225 """Load a source files data and parse it to phml. 226 227 Args: 228 file_path (str | Path): The file path to the source file. 229 """ 230 self.parser.load(file_path, handler) 231 return self 232 233 def parse(self, data: str | dict, handler: Optional[Callable] = None): 234 """Parse a str or dict object into phml. 235 236 Args: 237 data (str | dict): Object to parse to phml 238 """ 239 self.parser.parse(data, handler) 240 return self 241 242 def render( 243 self, 244 file_type: str = file_types.HTML, 245 indent: Optional[int] = None, 246 scopes: Optional[list[str]] = None, 247 **kwargs, 248 ) -> str: 249 """Render the parsed ast to a different format. Defaults to rendering to html. 250 251 Args: 252 file_type (str): The format to render to. Currently support html, phml, and json. 253 indent (Optional[int], optional): The number of spaces per indent. By default it will 254 use the standard for the given format. HTML has 4 spaces, phml has 4 spaces, and json 255 has 2 spaces. 256 257 Returns: 258 str: The rendered content in the appropriate format. 259 """ 260 261 scopes = scopes or ["./"] 262 for scope in self.scopes: 263 if scope not in scopes: 264 scopes.append(scope) 265 266 return self.compiler.compile( 267 self.parser.ast, 268 to_format=file_type, 269 indent=indent, 270 scopes=scopes, 271 **kwargs, 272 ) 273 274 def write( 275 self, 276 dest: str | Path, 277 file_type: str = file_types.HTML, 278 indent: Optional[int] = None, 279 scopes: Optional[list[str]] = None, 280 **kwargs, 281 ): 282 """Renders the parsed ast to a different format, then writes 283 it to a given file. Defaults to rendering and writing out as html. 284 285 Args: 286 dest (str | Path): The path to the file to be written to. 287 file_type (str): The format to render the ast as. 288 indent (Optional[int], optional): The number of spaces per indent. By default it will 289 use the standard for the given format. HTML has 4 spaces, phml has 4 spaces, and json 290 has 2 spaces. 291 kwargs: Any additional data to pass to the compiler that will be exposed to the 292 phml files. 293 """ 294 295 with open(dest, "+w", encoding="utf-8") as dest_file: 296 dest_file.write( 297 self.render(file_type=file_type, indent=indent, scopes=scopes, **kwargs) 298 ) 299 return self
A helper class that bundles the functionality of the parser and compiler together. Allows for loading source files, parsing strings and dicts, rendering to a different format, and finally writing the results of a render to a file.
List of paths from cwd to auto add to python path. This helps with importing inside of phml files.
179 def add( 180 self, 181 *components: dict[str, dict[str, list | All_Nodes] | AST] 182 | tuple[str, dict[str, list | All_Nodes] | AST] 183 | Path, 184 ): 185 """Add a component to the element replacement list. 186 187 Components passed in can be of a few types. The first type it can be is a 188 pathlib.Path type. This will allow for automatic parsing of the file at the 189 path and then the filename and parsed ast are passed to the compiler. It can 190 also be a dictionary of str being the name of the element to be replaced. 191 The name can be snake case, camel case, or pascal cased. The value can either 192 be the parsed result of the component from phml.utils.parse_component() or the 193 parsed ast of the component. Lastely, the component can be a tuple. The first 194 value is the name of the element to be replaced; with the second value being 195 either the parsed result of the component or the component's ast. 196 197 Note: 198 Any duplicate components will be replaced. 199 200 Args: 201 components: Any number values indicating 202 name of the component and the the component. The name is used 203 to replace a element with the tag==name. 204 """ 205 206 for component in components: 207 if isinstance(component, Path): 208 self.parser.load(component) 209 self.compiler.add((filename_from_path(component), parse_component(self.parser.ast))) 210 elif isinstance(component, dict): 211 self.compiler.add(*list(component.items())) 212 return self
Add a component to the element replacement list.
Components passed in can be of a few types. The first type it can be is a pathlib.Path type. This will allow for automatic parsing of the file at the path and then the filename and parsed ast are passed to the compiler. It can also be a dictionary of str being the name of the element to be replaced. The name can be snake case, camel case, or pascal cased. The value can either be the parsed result of the component from phml.utils.parse_component() or the parsed ast of the component. Lastely, the component can be a tuple. The first value is the name of the element to be replaced; with the second value being either the parsed result of the component or the component's ast.
Note:
Any duplicate components will be replaced.
Arguments:
- components: Any number values indicating
- name of the component and the the component. The name is used
- to replace a element with the tag==name.
214 def remove(self, *components: str | All_Nodes): 215 """Remove an element from the list of element replacements. 216 217 Takes any number of strings or node objects. If a string is passed 218 it is used as the key that will be removed. If a node object is passed 219 it will attempt to find a matching node and remove it. 220 """ 221 self.compiler.remove(*components) 222 return self
Remove an element from the list of element replacements.
Takes any number of strings or node objects. If a string is passed it is used as the key that will be removed. If a node object is passed it will attempt to find a matching node and remove it.
224 def load(self, file_path: str | Path, handler: Optional[Callable] = None): 225 """Load a source files data and parse it to phml. 226 227 Args: 228 file_path (str | Path): The file path to the source file. 229 """ 230 self.parser.load(file_path, handler) 231 return self
Load a source files data and parse it to phml.
Arguments:
- file_path (str | Path): The file path to the source file.
233 def parse(self, data: str | dict, handler: Optional[Callable] = None): 234 """Parse a str or dict object into phml. 235 236 Args: 237 data (str | dict): Object to parse to phml 238 """ 239 self.parser.parse(data, handler) 240 return self
Parse a str or dict object into phml.
Arguments:
- data (str | dict): Object to parse to phml
242 def render( 243 self, 244 file_type: str = file_types.HTML, 245 indent: Optional[int] = None, 246 scopes: Optional[list[str]] = None, 247 **kwargs, 248 ) -> str: 249 """Render the parsed ast to a different format. Defaults to rendering to html. 250 251 Args: 252 file_type (str): The format to render to. Currently support html, phml, and json. 253 indent (Optional[int], optional): The number of spaces per indent. By default it will 254 use the standard for the given format. HTML has 4 spaces, phml has 4 spaces, and json 255 has 2 spaces. 256 257 Returns: 258 str: The rendered content in the appropriate format. 259 """ 260 261 scopes = scopes or ["./"] 262 for scope in self.scopes: 263 if scope not in scopes: 264 scopes.append(scope) 265 266 return self.compiler.compile( 267 self.parser.ast, 268 to_format=file_type, 269 indent=indent, 270 scopes=scopes, 271 **kwargs, 272 )
Render the parsed ast to a different format. Defaults to rendering to html.
Arguments:
- file_type (str): The format to render to. Currently support html, phml, and json.
- indent (Optional[int], optional): The number of spaces per indent. By default it will
- use the standard for the given format. HTML has 4 spaces, phml has 4 spaces, and json
- has 2 spaces.
Returns:
str: The rendered content in the appropriate format.
274 def write( 275 self, 276 dest: str | Path, 277 file_type: str = file_types.HTML, 278 indent: Optional[int] = None, 279 scopes: Optional[list[str]] = None, 280 **kwargs, 281 ): 282 """Renders the parsed ast to a different format, then writes 283 it to a given file. Defaults to rendering and writing out as html. 284 285 Args: 286 dest (str | Path): The path to the file to be written to. 287 file_type (str): The format to render the ast as. 288 indent (Optional[int], optional): The number of spaces per indent. By default it will 289 use the standard for the given format. HTML has 4 spaces, phml has 4 spaces, and json 290 has 2 spaces. 291 kwargs: Any additional data to pass to the compiler that will be exposed to the 292 phml files. 293 """ 294 295 with open(dest, "+w", encoding="utf-8") as dest_file: 296 dest_file.write( 297 self.render(file_type=file_type, indent=indent, scopes=scopes, **kwargs) 298 ) 299 return self
Renders the parsed ast to a different format, then writes it to a given file. Defaults to rendering and writing out as html.
Arguments:
- dest (str | Path): The path to the file to be written to.
- file_type (str): The format to render the ast as.
- indent (Optional[int], optional): The number of spaces per indent. By default it will
- use the standard for the given format. HTML has 4 spaces, phml has 4 spaces, and json
- has 2 spaces.
- kwargs: Any additional data to pass to the compiler that will be exposed to the
- phml files.