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 Optional 132 133from . import builder, core, nodes, utils, virtual_python 134from .core import Compiler, Parser, file_types 135from .nodes import AST, All_Nodes 136 137__version__ = "0.1.0" 138__all__ = [ 139 "PHMLCore", 140 "Compiler", 141 "Parser", 142 "file_types", 143 "AST", 144 "core", 145 "nodes", 146 "utils", 147 "virtual_python", 148 "file_types", 149 "builder", 150] 151 152 153class PHMLCore: 154 """A helper class that bundles the functionality 155 of the parser and compiler together. Allows for loading source files, 156 parsing strings and dicts, rendering to a different format, and finally 157 writing the results of a render to a file. 158 """ 159 160 parser: Parser 161 """Instance of a [Parser][phml.parser.Parser].""" 162 compiler: Compiler 163 """Instance of a [Compiler][phml.compile.Compiler].""" 164 165 @property 166 def ast(self) -> AST: 167 """Reference to the parser attributes ast value.""" 168 return self.parser.ast 169 170 @ast.setter 171 def ast(self, _ast: AST): 172 self.parser.ast = _ast 173 174 def __init__( 175 self, 176 components: Optional[dict[str, dict[str, list | All_Nodes]]] = None, 177 ): 178 self.parser = Parser() 179 self.compiler = Compiler(components=components) 180 181 def add( 182 self, 183 *components: dict[str, dict[str, list | All_Nodes] | AST] 184 | tuple[str, dict[str, list | All_Nodes] | AST] 185 | Path, 186 ): 187 """Add a component to the element replacement list. 188 189 Components passed in can be of a few types. The first type it can be is a 190 pathlib.Path type. This will allow for automatic parsing of the file at the 191 path and then the filename and parsed ast are passed to the compiler. It can 192 also be a dictionary of str being the name of the element to be replaced. 193 The name can be snake case, camel case, or pascal cased. The value can either 194 be the parsed result of the component from phml.utils.parse_component() or the 195 parsed ast of the component. Lastely, the component can be a tuple. The first 196 value is the name of the element to be replaced; with the second value being 197 either the parsed result of the component or the component's ast. 198 199 Note: 200 Any duplicate components will be replaced. 201 202 Args: 203 components: Any number values indicating 204 name of the component and the the component. The name is used 205 to replace a element with the tag==name. 206 """ 207 from phml.utils import filename_from_path # pylint: disable=import-outside-toplevel 208 209 for component in components: 210 if isinstance(component, Path): 211 self.parser.load(component) 212 self.compiler.add((filename_from_path(component), self.parser.ast)) 213 else: 214 self.compiler.add(component) 215 return self 216 217 def remove(self, *components: str | All_Nodes): 218 """Remove an element from the list of element replacements. 219 220 Takes any number of strings or node objects. If a string is passed 221 it is used as the key that will be removed. If a node object is passed 222 it will attempt to find a matching node and remove it. 223 """ 224 self.compiler.remove(*components) 225 return self 226 227 def load(self, path: str | Path): 228 """Load a source files data and parse it to phml. 229 230 Args: 231 path (str | Path): The path to the source file. 232 """ 233 self.parser.load(path) 234 return self 235 236 def parse(self, data: str | dict): 237 """Parse a str or dict object into phml. 238 239 Args: 240 data (str | dict): Object to parse to phml 241 """ 242 self.parser.parse(data) 243 return self 244 245 def render( 246 self, file_type: str = file_types.HTML, indent: Optional[int] = None, **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 return self.compiler.compile(self.parser.ast, to_format=file_type, indent=indent, **kwargs) 260 261 def write( 262 self, 263 dest: str | Path, 264 file_type: str = file_types.HTML, 265 indent: Optional[int] = None, 266 **kwargs, 267 ): 268 """Renders the parsed ast to a different format, then writes 269 it to a given file. Defaults to rendering and writing out as html. 270 271 Args: 272 dest (str | Path): The path to the file to be written to. 273 file_type (str): The format to render the ast as. 274 indent (Optional[int], optional): The number of spaces per indent. By default it will 275 use the standard for the given format. HTML has 4 spaces, phml has 4 spaces, and json 276 has 2 spaces. 277 kwargs: Any additional data to pass to the compiler that will be exposed to the 278 phml files. 279 """ 280 with open(dest, "+w", encoding="utf-8") as dest_file: 281 dest_file.write(self.render(file_type=file_type, indent=indent, **kwargs)) 282 return self
154class PHMLCore: 155 """A helper class that bundles the functionality 156 of the parser and compiler together. Allows for loading source files, 157 parsing strings and dicts, rendering to a different format, and finally 158 writing the results of a render to a file. 159 """ 160 161 parser: Parser 162 """Instance of a [Parser][phml.parser.Parser].""" 163 compiler: Compiler 164 """Instance of a [Compiler][phml.compile.Compiler].""" 165 166 @property 167 def ast(self) -> AST: 168 """Reference to the parser attributes ast value.""" 169 return self.parser.ast 170 171 @ast.setter 172 def ast(self, _ast: AST): 173 self.parser.ast = _ast 174 175 def __init__( 176 self, 177 components: Optional[dict[str, dict[str, list | All_Nodes]]] = None, 178 ): 179 self.parser = Parser() 180 self.compiler = Compiler(components=components) 181 182 def add( 183 self, 184 *components: dict[str, dict[str, list | All_Nodes] | AST] 185 | tuple[str, dict[str, list | All_Nodes] | AST] 186 | Path, 187 ): 188 """Add a component to the element replacement list. 189 190 Components passed in can be of a few types. The first type it can be is a 191 pathlib.Path type. This will allow for automatic parsing of the file at the 192 path and then the filename and parsed ast are passed to the compiler. It can 193 also be a dictionary of str being the name of the element to be replaced. 194 The name can be snake case, camel case, or pascal cased. The value can either 195 be the parsed result of the component from phml.utils.parse_component() or the 196 parsed ast of the component. Lastely, the component can be a tuple. The first 197 value is the name of the element to be replaced; with the second value being 198 either the parsed result of the component or the component's ast. 199 200 Note: 201 Any duplicate components will be replaced. 202 203 Args: 204 components: Any number values indicating 205 name of the component and the the component. The name is used 206 to replace a element with the tag==name. 207 """ 208 from phml.utils import filename_from_path # pylint: disable=import-outside-toplevel 209 210 for component in components: 211 if isinstance(component, Path): 212 self.parser.load(component) 213 self.compiler.add((filename_from_path(component), self.parser.ast)) 214 else: 215 self.compiler.add(component) 216 return self 217 218 def remove(self, *components: str | All_Nodes): 219 """Remove an element from the list of element replacements. 220 221 Takes any number of strings or node objects. If a string is passed 222 it is used as the key that will be removed. If a node object is passed 223 it will attempt to find a matching node and remove it. 224 """ 225 self.compiler.remove(*components) 226 return self 227 228 def load(self, path: str | Path): 229 """Load a source files data and parse it to phml. 230 231 Args: 232 path (str | Path): The path to the source file. 233 """ 234 self.parser.load(path) 235 return self 236 237 def parse(self, data: str | dict): 238 """Parse a str or dict object into phml. 239 240 Args: 241 data (str | dict): Object to parse to phml 242 """ 243 self.parser.parse(data) 244 return self 245 246 def render( 247 self, file_type: str = file_types.HTML, indent: Optional[int] = None, **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 return self.compiler.compile(self.parser.ast, to_format=file_type, indent=indent, **kwargs) 261 262 def write( 263 self, 264 dest: str | Path, 265 file_type: str = file_types.HTML, 266 indent: Optional[int] = None, 267 **kwargs, 268 ): 269 """Renders the parsed ast to a different format, then writes 270 it to a given file. Defaults to rendering and writing out as html. 271 272 Args: 273 dest (str | Path): The path to the file to be written to. 274 file_type (str): The format to render the ast as. 275 indent (Optional[int], optional): The number of spaces per indent. By default it will 276 use the standard for the given format. HTML has 4 spaces, phml has 4 spaces, and json 277 has 2 spaces. 278 kwargs: Any additional data to pass to the compiler that will be exposed to the 279 phml files. 280 """ 281 with open(dest, "+w", encoding="utf-8") as dest_file: 282 dest_file.write(self.render(file_type=file_type, indent=indent, **kwargs)) 283 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.
182 def add( 183 self, 184 *components: dict[str, dict[str, list | All_Nodes] | AST] 185 | tuple[str, dict[str, list | All_Nodes] | AST] 186 | Path, 187 ): 188 """Add a component to the element replacement list. 189 190 Components passed in can be of a few types. The first type it can be is a 191 pathlib.Path type. This will allow for automatic parsing of the file at the 192 path and then the filename and parsed ast are passed to the compiler. It can 193 also be a dictionary of str being the name of the element to be replaced. 194 The name can be snake case, camel case, or pascal cased. The value can either 195 be the parsed result of the component from phml.utils.parse_component() or the 196 parsed ast of the component. Lastely, the component can be a tuple. The first 197 value is the name of the element to be replaced; with the second value being 198 either the parsed result of the component or the component's ast. 199 200 Note: 201 Any duplicate components will be replaced. 202 203 Args: 204 components: Any number values indicating 205 name of the component and the the component. The name is used 206 to replace a element with the tag==name. 207 """ 208 from phml.utils import filename_from_path # pylint: disable=import-outside-toplevel 209 210 for component in components: 211 if isinstance(component, Path): 212 self.parser.load(component) 213 self.compiler.add((filename_from_path(component), self.parser.ast)) 214 else: 215 self.compiler.add(component) 216 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.
Args
- 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.
218 def remove(self, *components: str | All_Nodes): 219 """Remove an element from the list of element replacements. 220 221 Takes any number of strings or node objects. If a string is passed 222 it is used as the key that will be removed. If a node object is passed 223 it will attempt to find a matching node and remove it. 224 """ 225 self.compiler.remove(*components) 226 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.
228 def load(self, path: str | Path): 229 """Load a source files data and parse it to phml. 230 231 Args: 232 path (str | Path): The path to the source file. 233 """ 234 self.parser.load(path) 235 return self
Load a source files data and parse it to phml.
Args
- path (str | Path): The path to the source file.
237 def parse(self, data: str | dict): 238 """Parse a str or dict object into phml. 239 240 Args: 241 data (str | dict): Object to parse to phml 242 """ 243 self.parser.parse(data) 244 return self
Parse a str or dict object into phml.
Args
- data (str | dict): Object to parse to phml
246 def render( 247 self, file_type: str = file_types.HTML, indent: Optional[int] = None, **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 return self.compiler.compile(self.parser.ast, to_format=file_type, indent=indent, **kwargs)
Render the parsed ast to a different format. Defaults to rendering to html.
Args
- 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.
262 def write( 263 self, 264 dest: str | Path, 265 file_type: str = file_types.HTML, 266 indent: Optional[int] = None, 267 **kwargs, 268 ): 269 """Renders the parsed ast to a different format, then writes 270 it to a given file. Defaults to rendering and writing out as html. 271 272 Args: 273 dest (str | Path): The path to the file to be written to. 274 file_type (str): The format to render the ast as. 275 indent (Optional[int], optional): The number of spaces per indent. By default it will 276 use the standard for the given format. HTML has 4 spaces, phml has 4 spaces, and json 277 has 2 spaces. 278 kwargs: Any additional data to pass to the compiler that will be exposed to the 279 phml files. 280 """ 281 with open(dest, "+w", encoding="utf-8") as dest_file: 282 dest_file.write(self.render(file_type=file_type, indent=indent, **kwargs)) 283 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.
Args
- 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.
18class Compiler: 19 """Used to compile phml into other formats. HTML, PHML, 20 JSON, Markdown, etc... 21 """ 22 23 ast: AST 24 """phml ast used by the compiler to generate a new format.""" 25 26 def __init__( 27 self, 28 ast: Optional[AST] = None, 29 components: Optional[dict[str, dict[str, list | All_Nodes]]] = None, 30 ): 31 self.ast = ast 32 self.components = components or {} 33 34 def add( 35 self, 36 *components: dict[str, dict[str, list | All_Nodes] | AST] 37 | tuple[str, dict[str, list | All_Nodes] | AST], 38 ): 39 """Add a component to the compilers component list. 40 41 Components passed in can be of a few types. It can also be a dictionary of str 42 being the name of the element to be replaced. The name can be snake case, camel 43 case, or pascal cased. The value can either be the parsed result of the component 44 from phml.utils.parse_component() or the parsed ast of the component. Lastely, 45 the component can be a tuple. The first value is the name of the element to be 46 replaced; with the second value being either the parsed result of the component 47 or the component's ast. 48 49 Note: 50 Any duplicate components will be replaced. 51 52 Args: 53 components: Any number values indicating 54 name of the component and the the component. The name is used 55 to replace a element with the tag==name. 56 """ 57 58 for component in components: 59 if isinstance(component, dict): 60 for key, value in component.items(): 61 if isinstance(value, AST): 62 self.components[tag_from_file(key)] = parse_component(value) 63 else: 64 self.components[tag_from_file(key)] = value 65 elif isinstance(component, tuple): 66 if isinstance(component[1], AST): 67 self.components[tag_from_file(component[0])] = parse_component(component[1]) 68 else: 69 self.components[tag_from_file(component[0])] = component[1] 70 71 return self 72 73 def remove(self, *components: str | All_Nodes): 74 """Takes either component names or components and removes them 75 from the dictionary. 76 77 Args: 78 components (str | All_Nodes): Any str name of components or 79 node value to remove from the components list in the compiler. 80 """ 81 for component in components: 82 if isinstance(component, str): 83 if component in self.components: 84 self.components.pop(component, None) 85 else: 86 raise KeyError(f"Invalid component name {component}") 87 elif isinstance(component, All_Nodes): 88 for key, value in self.components: 89 if isinstance(value, dict) and value["component"] == component: 90 self.components.pop(key, None) 91 break 92 93 if value == components: 94 self.components.pop(key, None) 95 break 96 97 return self 98 99 def compile( 100 self, 101 ast: Optional[AST] = None, 102 to_format: str = HTML, 103 indent: Optional[int] = None, 104 handler: Optional[Callable] = None, 105 **kwargs: Any, 106 ) -> str: 107 """Execute compilation to a different format.""" 108 109 ast = ast or self.ast 110 111 if ast is None: 112 raise Exception("Must provide an ast to compile.") 113 114 doctypes = [dt for dt in visit_children(ast.tree) if test(dt, "doctype")] 115 if len(doctypes) == 0: 116 ast.tree.children.insert(0, DocType(parent=ast.tree)) 117 118 if to_format == PHML: 119 return phml(ast, indent or 4) 120 121 if to_format == HTML: 122 return html(ast, self.components, indent or 4, **kwargs) 123 124 if to_format == JSON: 125 return json(ast, indent or 2) 126 127 if handler is None: 128 raise Exception(f"Unkown format < { to_format } >") 129 130 return handler(ast, indent)
Used to compile phml into other formats. HTML, PHML, JSON, Markdown, etc...
34 def add( 35 self, 36 *components: dict[str, dict[str, list | All_Nodes] | AST] 37 | tuple[str, dict[str, list | All_Nodes] | AST], 38 ): 39 """Add a component to the compilers component list. 40 41 Components passed in can be of a few types. It can also be a dictionary of str 42 being the name of the element to be replaced. The name can be snake case, camel 43 case, or pascal cased. The value can either be the parsed result of the component 44 from phml.utils.parse_component() or the parsed ast of the component. Lastely, 45 the component can be a tuple. The first value is the name of the element to be 46 replaced; with the second value being either the parsed result of the component 47 or the component's ast. 48 49 Note: 50 Any duplicate components will be replaced. 51 52 Args: 53 components: Any number values indicating 54 name of the component and the the component. The name is used 55 to replace a element with the tag==name. 56 """ 57 58 for component in components: 59 if isinstance(component, dict): 60 for key, value in component.items(): 61 if isinstance(value, AST): 62 self.components[tag_from_file(key)] = parse_component(value) 63 else: 64 self.components[tag_from_file(key)] = value 65 elif isinstance(component, tuple): 66 if isinstance(component[1], AST): 67 self.components[tag_from_file(component[0])] = parse_component(component[1]) 68 else: 69 self.components[tag_from_file(component[0])] = component[1] 70 71 return self
Add a component to the compilers component list.
Components passed in can be of a few types. 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.
Args
- 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.
73 def remove(self, *components: str | All_Nodes): 74 """Takes either component names or components and removes them 75 from the dictionary. 76 77 Args: 78 components (str | All_Nodes): Any str name of components or 79 node value to remove from the components list in the compiler. 80 """ 81 for component in components: 82 if isinstance(component, str): 83 if component in self.components: 84 self.components.pop(component, None) 85 else: 86 raise KeyError(f"Invalid component name {component}") 87 elif isinstance(component, All_Nodes): 88 for key, value in self.components: 89 if isinstance(value, dict) and value["component"] == component: 90 self.components.pop(key, None) 91 break 92 93 if value == components: 94 self.components.pop(key, None) 95 break 96 97 return self
Takes either component names or components and removes them from the dictionary.
Args
- components (str | All_Nodes): Any str name of components or
- node value to remove from the components list in the compiler.
99 def compile( 100 self, 101 ast: Optional[AST] = None, 102 to_format: str = HTML, 103 indent: Optional[int] = None, 104 handler: Optional[Callable] = None, 105 **kwargs: Any, 106 ) -> str: 107 """Execute compilation to a different format.""" 108 109 ast = ast or self.ast 110 111 if ast is None: 112 raise Exception("Must provide an ast to compile.") 113 114 doctypes = [dt for dt in visit_children(ast.tree) if test(dt, "doctype")] 115 if len(doctypes) == 0: 116 ast.tree.children.insert(0, DocType(parent=ast.tree)) 117 118 if to_format == PHML: 119 return phml(ast, indent or 4) 120 121 if to_format == HTML: 122 return html(ast, self.components, indent or 4, **kwargs) 123 124 if to_format == JSON: 125 return json(ast, indent or 2) 126 127 if handler is None: 128 raise Exception(f"Unkown format < { to_format } >") 129 130 return handler(ast, indent)
Execute compilation to a different format.
22class Parser: 23 """Primary logic to handle everything with a phml file. 24 25 This class can parse files as phml files and create an ast. 26 The ast and the nodes themselfs can translate themselves to; 27 html, phml, and json. The ast can recursively return itself as 28 an html string. However, only this class can process the python 29 blocks inside of the phml file. 30 31 Call Parser.convert() and pass any kwargs you wish to be exposed to 32 the process that processes the python. You may also use Parser.util to 33 pass extensions to convert and manipulate the html along with the python 34 processing. 35 """ 36 37 parser: HypertextMarkupParser 38 """The custom builtin `html.parser` class that builds phml ast.""" 39 40 ast: AST 41 """The recursive node tree of the phml ast.""" 42 43 def __init__(self): 44 self.phml_parser = HypertextMarkupParser() 45 self.ast = None 46 47 def load(self, path: str | Path, handler: Optional[Callable] = None): 48 """Parse a given phml file to AST following hast and unist. 49 50 When finished the PHML.ast variable will be populated with the 51 resulting ast. 52 53 Args: 54 path (str | Path): The path to the file that should be parsed. 55 handler (Callable | None): A function to call instead of the built in 56 parser to parse to a phml.AST. Must take a string and return a phml.AST. 57 """ 58 59 with open(path, "r", encoding="utf-8") as source: 60 src = source.read() 61 62 if handler is None: 63 path = Path(path) 64 65 if path.suffix == ".json": 66 self.ast = AST(json_to_ast(loads(src))) 67 else: 68 self.phml_parser.reset() 69 self.phml_parser.cur = Root() 70 71 try: 72 self.phml_parser.feed(src) 73 if len(self.phml_parser.cur_tags) > 0: 74 last = self.phml_parser.cur_tags[-1].position 75 raise Exception( 76 f"Unbalanced tags in source file '{path}' at \ 77[{last.start.line}:{last.start.column}]" 78 ) 79 self.ast = AST(self.phml_parser.cur) 80 except Exception as exception: 81 self.ast = None 82 raise Exception(f"'{path}': {exception}") from exception 83 else: 84 self.ast = handler(src) 85 86 return self 87 88 def parse(self, data: str | dict, handler: Optional[Callable] = None): 89 """Parse data from a phml/html string or from a dict to a phml ast. 90 91 Args: 92 data (str | dict): Data to parse in to a ast 93 data_type (str): Can be `HTML`, `PHML`, `MARKDOWN`, or `JSON` which 94 tells parser how to parse the data. Otherwise it will assume 95 str data to be html/phml and dict as `json`. 96 handler (Callable | None): A function to call instead of the built in 97 parser to parse to a phml.AST. Must take a string and return a phml.AST. 98 """ 99 if handler is None: 100 if isinstance(data, dict): 101 self.ast = AST(json_to_ast(data)) 102 elif isinstance(data, str): 103 self.phml_parser.reset() 104 self.phml_parser.cur = Root() 105 106 try: 107 self.phml_parser.feed(data) 108 if len(self.phml_parser.cur_tags) > 0: 109 last = self.phml_parser.cur_tags[-1].position 110 raise Exception( 111 f"Unbalanced tags in source at [{last.start.line}:{last.start.column}]" 112 ) 113 self.ast = AST(self.phml_parser.cur) 114 except Exception as exception: 115 self.ast = None 116 raise Exception( 117 f"{data[:6] + '...' if len(data) > 6 else data}\ 118: {exception}" 119 ) from exception 120 else: 121 self.ast = handler(data) 122 123 return self
Primary logic to handle everything with a phml file.
This class can parse files as phml files and create an ast. The ast and the nodes themselfs can translate themselves to; html, phml, and json. The ast can recursively return itself as an html string. However, only this class can process the python blocks inside of the phml file.
Call Parser.convert() and pass any kwargs you wish to be exposed to the process that processes the python. You may also use Parser.util to pass extensions to convert and manipulate the html along with the python processing.
The custom builtin html.parser
class that builds phml ast.
47 def load(self, path: str | Path, handler: Optional[Callable] = None): 48 """Parse a given phml file to AST following hast and unist. 49 50 When finished the PHML.ast variable will be populated with the 51 resulting ast. 52 53 Args: 54 path (str | Path): The path to the file that should be parsed. 55 handler (Callable | None): A function to call instead of the built in 56 parser to parse to a phml.AST. Must take a string and return a phml.AST. 57 """ 58 59 with open(path, "r", encoding="utf-8") as source: 60 src = source.read() 61 62 if handler is None: 63 path = Path(path) 64 65 if path.suffix == ".json": 66 self.ast = AST(json_to_ast(loads(src))) 67 else: 68 self.phml_parser.reset() 69 self.phml_parser.cur = Root() 70 71 try: 72 self.phml_parser.feed(src) 73 if len(self.phml_parser.cur_tags) > 0: 74 last = self.phml_parser.cur_tags[-1].position 75 raise Exception( 76 f"Unbalanced tags in source file '{path}' at \ 77[{last.start.line}:{last.start.column}]" 78 ) 79 self.ast = AST(self.phml_parser.cur) 80 except Exception as exception: 81 self.ast = None 82 raise Exception(f"'{path}': {exception}") from exception 83 else: 84 self.ast = handler(src) 85 86 return self
Parse a given phml file to AST following hast and unist.
When finished the PHML.ast variable will be populated with the resulting ast.
Args
88 def parse(self, data: str | dict, handler: Optional[Callable] = None): 89 """Parse data from a phml/html string or from a dict to a phml ast. 90 91 Args: 92 data (str | dict): Data to parse in to a ast 93 data_type (str): Can be `HTML`, `PHML`, `MARKDOWN`, or `JSON` which 94 tells parser how to parse the data. Otherwise it will assume 95 str data to be html/phml and dict as `json`. 96 handler (Callable | None): A function to call instead of the built in 97 parser to parse to a phml.AST. Must take a string and return a phml.AST. 98 """ 99 if handler is None: 100 if isinstance(data, dict): 101 self.ast = AST(json_to_ast(data)) 102 elif isinstance(data, str): 103 self.phml_parser.reset() 104 self.phml_parser.cur = Root() 105 106 try: 107 self.phml_parser.feed(data) 108 if len(self.phml_parser.cur_tags) > 0: 109 last = self.phml_parser.cur_tags[-1].position 110 raise Exception( 111 f"Unbalanced tags in source at [{last.start.line}:{last.start.column}]" 112 ) 113 self.ast = AST(self.phml_parser.cur) 114 except Exception as exception: 115 self.ast = None 116 raise Exception( 117 f"{data[:6] + '...' if len(data) > 6 else data}\ 118: {exception}" 119 ) from exception 120 else: 121 self.ast = handler(data) 122 123 return self
Parse data from a phml/html string or from a dict to a phml ast.
Args
- data (str | dict): Data to parse in to a ast
- data_type (str): Can be
HTML
,PHML
,MARKDOWN
, orJSON
which - tells parser how to parse the data. Otherwise it will assume
- str data to be html/phml and dict as
json
. - handler (Callable | None): A function to call instead of the built in
- parser to parse to a phml.AST. Must take a string and return a phml.AST.
17class AST: 18 """PHML ast. 19 20 Contains utility functions that can manipulate the ast. 21 """ 22 23 def __init__(self, tree): 24 if hasattr(tree, "type") and tree.type in ["root", "element"]: 25 self.tree = tree 26 else: 27 raise TypeError("The given tree/root node for AST must be of type `Root` or `Element`") 28 29 def __iter__(self) -> Iterator: 30 from phml.utils import walk # pylint: disable=import-outside-toplevel 31 32 return walk(self.tree) 33 34 def __eq__(self, obj) -> bool: 35 if isinstance(obj, self.__class__): 36 if self.tree == obj.tree: 37 return True 38 return False 39 40 @cached_property 41 def size(self) -> int: 42 """Get the number of nodes in the ast tree.""" 43 from phml.utils import size # pylint: disable=import-outside-toplevel 44 45 return size(self.tree) 46 47 @property 48 def children(self) -> list: 49 """Get access to the ast roots children. 50 Is none if there is no root. 51 """ 52 return self.tree.children if self.tree is not None else None
PHML ast.
Contains utility functions that can manipulate the ast.