phml.core

phml.core

All core parsing, compiling, and valid file_types.

 1"""phml.core
 2
 3All core parsing, compiling, and valid file_types.
 4"""
 5
 6from . import file_types
 7from .compile import Compiler
 8from .parser import Parser
 9
10__all__ = ["Compiler", "Parser", "file_types"]
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                    elif isinstance(value, dict) and valid_component_dict(value):
 64                        self.components[tag_from_file(key)] = value
 65            elif isinstance(component, tuple):
 66                if isinstance(component[0], str) and isinstance(component[1], AST):
 67                    self.components[tag_from_file(component[0])] = parse_component(component[1])
 68                elif isinstance(component[0], str) and valid_component_dict(component[1]):
 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.items():
 89                    if isinstance(value, dict) and value["component"] == component:
 90                        self.components.pop(key, None)
 91                        break
 92
 93        return self
 94
 95    def compile(
 96        self,
 97        ast: Optional[AST] = None,
 98        to_format: str = HTML,
 99        indent: Optional[int] = None,
100        handler: Optional[Callable] = None,
101        scopes: Optional[list[str]] = None,
102        **kwargs: Any,
103    ) -> str:
104        """Execute compilation to a different format."""
105
106        ast = ast or self.ast
107
108        if ast is None:
109            raise Exception("Must provide an ast to compile.")
110
111        doctypes = [dt for dt in visit_children(ast.tree) if check(dt, "doctype")]
112        if len(doctypes) == 0:
113            ast.tree.children.insert(0, DocType(parent=ast.tree))
114
115        scopes = scopes or ["./"]
116        if scopes is not None:
117            from sys import path  # pylint: disable=import-outside-toplevel
118
119            for scope in scopes:
120                path.insert(0, scope)
121
122        if to_format == PHML:
123            return phml(ast, indent or 4)
124
125        if to_format == HTML:
126            return html(ast, self.components, indent or 4, **kwargs)
127
128        if to_format == JSON:
129            return json(ast, indent or 2)
130
131        if handler is None:
132            raise Exception(f"Unkown format < { to_format } >")
133
134        return handler(ast, indent)

Used to compile phml into other formats. HTML, PHML, JSON, Markdown, etc...

Compiler( ast: Optional[phml.nodes.AST.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 {}

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.nodes.AST.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.nodes.AST.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                    elif isinstance(value, dict) and valid_component_dict(value):
64                        self.components[tag_from_file(key)] = value
65            elif isinstance(component, tuple):
66                if isinstance(component[0], str) and isinstance(component[1], AST):
67                    self.components[tag_from_file(component[0])] = parse_component(component[1])
68                elif isinstance(component[0], str) and valid_component_dict(component[1]):
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.

Arguments:
  • components: Any number values indicating
  • name of the component and the the component. The name is used
  • to replace a element with the tag==name.
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.items():
89                    if isinstance(value, dict) and value["component"] == component:
90                        self.components.pop(key, None)
91                        break
92
93        return self

Takes either component names or components and removes them from the dictionary.

Arguments:
  • 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.nodes.AST.AST] = None, to_format: str = 'html', indent: Optional[int] = None, handler: Optional[Callable] = None, scopes: Optional[list[str]] = None, **kwargs: Any) -> str:
 95    def compile(
 96        self,
 97        ast: Optional[AST] = None,
 98        to_format: str = HTML,
 99        indent: Optional[int] = None,
100        handler: Optional[Callable] = None,
101        scopes: Optional[list[str]] = None,
102        **kwargs: Any,
103    ) -> str:
104        """Execute compilation to a different format."""
105
106        ast = ast or self.ast
107
108        if ast is None:
109            raise Exception("Must provide an ast to compile.")
110
111        doctypes = [dt for dt in visit_children(ast.tree) if check(dt, "doctype")]
112        if len(doctypes) == 0:
113            ast.tree.children.insert(0, DocType(parent=ast.tree))
114
115        scopes = scopes or ["./"]
116        if scopes is not None:
117            from sys import path  # pylint: disable=import-outside-toplevel
118
119            for scope in scopes:
120                path.insert(0, scope)
121
122        if to_format == PHML:
123            return phml(ast, indent or 4)
124
125        if to_format == HTML:
126            return html(ast, self.components, indent or 4, **kwargs)
127
128        if to_format == JSON:
129            return json(ast, indent or 2)
130
131        if handler is None:
132            raise Exception(f"Unkown format < { to_format } >")
133
134        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        self.phml_parser.cur_tags = []
 60
 61        with open(path, "r", encoding="utf-8") as source:
 62            src = source.read()
 63
 64        if handler is None:
 65            path = Path(path)
 66
 67            if path.suffix == ".json":
 68                self.ast = AST(json_to_ast(loads(src)))
 69            else:
 70                self.phml_parser.reset()
 71                self.phml_parser.cur = Root()
 72
 73                try:
 74                    self.phml_parser.feed(src)
 75                    if len(self.phml_parser.cur_tags) > 0:
 76                        last = self.phml_parser.cur_tags[-1].position
 77                        raise Exception(
 78                            f"Unbalanced tags in source file '{path.as_posix()}' at \
 79[{last.start.line}:{last.start.column}]"
 80                        )
 81                    self.ast = AST(self.phml_parser.cur)
 82                except Exception as exception:
 83                    self.ast = None
 84                    raise Exception(f"'{path.as_posix()}': {exception}") from exception
 85        else:
 86            self.ast = handler(src)
 87
 88        return self
 89
 90    def parse(self, data: str | dict, handler: Optional[Callable] = None):
 91        """Parse data from a phml/html string or from a dict to a phml ast.
 92
 93        Args:
 94            data (str | dict): Data to parse in to a ast
 95            data_type (str): Can be `HTML`, `PHML`, `MARKDOWN`, or `JSON` which
 96            tells parser how to parse the data. Otherwise it will assume
 97            str data to be html/phml and dict as `json`.
 98            handler (Callable | None): A function to call instead of the built in
 99            parser to parse to a phml.AST. Must take a string and return a phml.AST.
100        """
101
102        self.phml_parser.cur_tags = []
103
104        if handler is None:
105            if isinstance(data, dict):
106                self.ast = AST(json_to_ast(data))
107            elif isinstance(data, str):
108                self.phml_parser.reset()
109                self.phml_parser.cur = Root()
110
111                try:
112                    self.phml_parser.feed(data)
113                    if len(self.phml_parser.cur_tags) > 0:
114                        last = self.phml_parser.cur_tags[-1].position
115                        raise Exception(
116                            f"Unbalanced tags in source at [{last.start.line}:{last.start.column}]"
117                        )
118                    self.ast = AST(self.phml_parser.cur)
119                except Exception as exception:
120                    self.ast = None
121                    raise Exception(
122                        f"{data[:6] + '...' if len(data) > 6 else data}\
123: {exception}"
124                    ) from exception
125        else:
126            self.ast = handler(data)
127
128        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.

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        self.phml_parser.cur_tags = []
60
61        with open(path, "r", encoding="utf-8") as source:
62            src = source.read()
63
64        if handler is None:
65            path = Path(path)
66
67            if path.suffix == ".json":
68                self.ast = AST(json_to_ast(loads(src)))
69            else:
70                self.phml_parser.reset()
71                self.phml_parser.cur = Root()
72
73                try:
74                    self.phml_parser.feed(src)
75                    if len(self.phml_parser.cur_tags) > 0:
76                        last = self.phml_parser.cur_tags[-1].position
77                        raise Exception(
78                            f"Unbalanced tags in source file '{path.as_posix()}' at \
79[{last.start.line}:{last.start.column}]"
80                        )
81                    self.ast = AST(self.phml_parser.cur)
82                except Exception as exception:
83                    self.ast = None
84                    raise Exception(f"'{path.as_posix()}': {exception}") from exception
85        else:
86            self.ast = handler(src)
87
88        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.

Arguments:
  • 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):
 90    def parse(self, data: str | dict, handler: Optional[Callable] = None):
 91        """Parse data from a phml/html string or from a dict to a phml ast.
 92
 93        Args:
 94            data (str | dict): Data to parse in to a ast
 95            data_type (str): Can be `HTML`, `PHML`, `MARKDOWN`, or `JSON` which
 96            tells parser how to parse the data. Otherwise it will assume
 97            str data to be html/phml and dict as `json`.
 98            handler (Callable | None): A function to call instead of the built in
 99            parser to parse to a phml.AST. Must take a string and return a phml.AST.
100        """
101
102        self.phml_parser.cur_tags = []
103
104        if handler is None:
105            if isinstance(data, dict):
106                self.ast = AST(json_to_ast(data))
107            elif isinstance(data, str):
108                self.phml_parser.reset()
109                self.phml_parser.cur = Root()
110
111                try:
112                    self.phml_parser.feed(data)
113                    if len(self.phml_parser.cur_tags) > 0:
114                        last = self.phml_parser.cur_tags[-1].position
115                        raise Exception(
116                            f"Unbalanced tags in source at [{last.start.line}:{last.start.column}]"
117                        )
118                    self.ast = AST(self.phml_parser.cur)
119                except Exception as exception:
120                    self.ast = None
121                    raise Exception(
122                        f"{data[:6] + '...' if len(data) > 6 else data}\
123: {exception}"
124                    ) from exception
125        else:
126            self.ast = handler(data)
127
128        return self

Parse data from a phml/html string or from a dict to a phml ast.

Arguments:
  • 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.