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
class PHMLCore:
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.

PHMLCore( components: Optional[dict[str, dict[str, list | phml.nodes.root.Root | phml.nodes.element.Element | phml.nodes.text.Text | phml.nodes.comment.Comment | phml.nodes.doctype.DocType | phml.nodes.parent.Parent | phml.nodes.node.Node | phml.nodes.literal.Literal]]] = None)
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)
parser: phml.Parser

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

compiler: phml.Compiler

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

ast: phml.AST

Reference to the parser attributes ast value.

def add( self, *components: dict[str, dict[str, list | phml.nodes.root.Root | phml.nodes.element.Element | phml.nodes.text.Text | phml.nodes.comment.Comment | phml.nodes.doctype.DocType | phml.nodes.parent.Parent | phml.nodes.node.Node | phml.nodes.literal.Literal] | phml.AST] | tuple[str, dict[str, list | phml.nodes.root.Root | phml.nodes.element.Element | phml.nodes.text.Text | phml.nodes.comment.Comment | phml.nodes.doctype.DocType | phml.nodes.parent.Parent | phml.nodes.node.Node | phml.nodes.literal.Literal] | phml.AST] | pathlib.Path):
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.
def remove( self, *components: str | phml.nodes.root.Root | phml.nodes.element.Element | phml.nodes.text.Text | phml.nodes.comment.Comment | phml.nodes.doctype.DocType | phml.nodes.parent.Parent | phml.nodes.node.Node | phml.nodes.literal.Literal):
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.

def load(self, path: str | pathlib.Path):
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.
def parse(self, data: str | dict):
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
def render( self, file_type: str = 'html', indent: Optional[int] = None, **kwargs) -> str:
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.

def write( self, dest: str | pathlib.Path, file_type: str = 'html', indent: Optional[int] = None, **kwargs):
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.
class Compiler:
 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...

Compiler( ast: Optional[phml.AST] = None, components: Optional[dict[str, dict[str, list | phml.nodes.root.Root | phml.nodes.element.Element | phml.nodes.text.Text | phml.nodes.comment.Comment | phml.nodes.doctype.DocType | phml.nodes.parent.Parent | phml.nodes.node.Node | phml.nodes.literal.Literal]]] = None)
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 {}
ast: phml.AST

phml ast used by the compiler to generate a new format.

def add( self, *components: dict[str, dict[str, list | phml.nodes.root.Root | phml.nodes.element.Element | phml.nodes.text.Text | phml.nodes.comment.Comment | phml.nodes.doctype.DocType | phml.nodes.parent.Parent | phml.nodes.node.Node | phml.nodes.literal.Literal] | phml.AST] | tuple[str, dict[str, list | phml.nodes.root.Root | phml.nodes.element.Element | phml.nodes.text.Text | phml.nodes.comment.Comment | phml.nodes.doctype.DocType | phml.nodes.parent.Parent | phml.nodes.node.Node | phml.nodes.literal.Literal] | phml.AST]):
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.
def remove( self, *components: str | phml.nodes.root.Root | phml.nodes.element.Element | phml.nodes.text.Text | phml.nodes.comment.Comment | phml.nodes.doctype.DocType | phml.nodes.parent.Parent | phml.nodes.node.Node | phml.nodes.literal.Literal):
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.
def compile( self, ast: Optional[phml.AST] = None, to_format: str = 'html', indent: Optional[int] = None, handler: Optional[Callable] = None, **kwargs: Any) -> str:
 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.

class Parser:
 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.

Parser()
43    def __init__(self):
44        self.phml_parser = HypertextMarkupParser()
45        self.ast = None
parser: phml.core.parser.hypertext_markup_parser.HypertextMarkupParser

The custom builtin html.parser class that builds phml ast.

ast: phml.AST

The recursive node tree of the phml ast.

def load(self, path: str | pathlib.Path, handler: Optional[Callable] = None):
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
  • path (str | Path): The path to the file that should be parsed.
  • 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.
def parse(self, data: str | dict, handler: Optional[Callable] = None):
 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, or JSON 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.
class 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.

AST(tree)
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`")
size: int

Get the number of nodes in the ast tree.

children: list

Get access to the ast roots children. Is none if there is no root.