phml.nodes

  1from __future__ import annotations
  2
  3from enum import StrEnum, unique
  4from types import NoneType
  5from typing import Any, Iterator, NoReturn, TypeAlias, overload
  6
  7from saimll import SAIML
  8
  9Attribute: TypeAlias = str | bool
 10
 11
 12class Missing:
 13    pass
 14
 15
 16MISSING = Missing()
 17
 18
 19def p_code(value) -> str:  # pragma: no cover
 20    """Get python code representation of phml nodes."""
 21    if value is None:
 22        return "None"
 23    return value.__p_code__()
 24
 25
 26@unique
 27class LiteralType(StrEnum):
 28    Text = "text"
 29    Comment = "comment"
 30
 31    @staticmethod
 32    def From(type: str) -> str:
 33        types = ["text", "comment"]
 34        if type in types:
 35            return type
 36        raise ValueError(f"Expected on of {', '.join(types)}")
 37
 38
 39@unique
 40class NodeType(StrEnum):
 41    AST = "ast"
 42    ELEMENT = "element"
 43    LITERAL = "literal"
 44
 45
 46class Point:
 47    """Represents one place in a source file.
 48
 49    The line field (1-indexed integer) represents a line in a source file. The column field
 50    (1-indexed integer) represents a column in a source file. The offset field (0-indexed integer)
 51    represents a character in a source file.
 52    """
 53
 54    def __init__(self, line: int, column: int) -> None:
 55        if line is None or line < 0:
 56            raise IndexError(f"Point.line must be >= 0 but was {line}")
 57
 58        self.line = line
 59
 60        if column is None or column < 0:
 61            raise IndexError(f"Point.column must be >= 0 but was {column}")
 62
 63        self.column = column
 64
 65    def __eq__(self, _o) -> bool:
 66        return (
 67            isinstance(_o, self.__class__)
 68            and _o.line == self.line
 69            and _o.column == self.column
 70        )
 71
 72    @staticmethod
 73    def from_dict(data: dict) -> Point:
 74        return Point(data["line"], data["column"])
 75
 76    def __p_code__(self) -> str:
 77        return f"Point({self.line}, {self.column})"
 78
 79    def __repr__(self) -> str:
 80        return f"{self.line}:{self.column}"
 81
 82    def __str__(self) -> str:
 83        return f"\x1b[38;5;244m{self.line}:{self.column}\x1b[39m"
 84
 85
 86class Position:
 87    """Position represents the location of a node in a source file.
 88
 89    The `start` field of `Position` represents the place of the first character
 90    of the parsed source region. The `end` field of Position represents the place
 91    of the first character after the parsed source region, whether it exists or not.
 92    The value of the `start` and `end` fields implement the `Point` interface.
 93
 94    The `indent` field of `Position` represents the start column at each index
 95    (plus start line) in the source region, for elements that span multiple lines.
 96
 97    If the syntactic unit represented by a node is not present in the source file at
 98    the time of parsing, the node is said to be `generated` and it must not have positional
 99    information.
100    """
101
102    @overload
103    def __init__(
104        self,
105        start: Point,
106        end: Point,
107    ) -> None:
108        """
109        Args:
110            start (tuple[int, int, int  |  None]): Tuple representing the line, column, and optional
111            offset of the start point.
112            end (tuple[int, int, int  |  None]): Tuple representing the line, column, and optional
113            offset of the end point.
114            indent (Optional[int], optional): The indent amount for the start of the position.
115        """
116        ...
117
118    @overload
119    def __init__(
120        self,
121        start: tuple[int, int],
122        end: tuple[int, int],
123    ) -> None:
124        """
125        Args:
126            start (tuple[int, int, int  |  None]): Tuple representing the line, column, and optional
127            offset of the start point.
128            end (tuple[int, int, int  |  None]): Tuple representing the line, column, and optional
129            offset of the end point.
130            indent (Optional[int], optional): The indent amount for the start of the position.
131        """
132        ...
133
134    def __init__(self, start: Point | tuple[int, int], end: Point | tuple[int, int]):
135        """
136        Args:
137            start (Point): Starting point of the position.
138            end (Point): End point of the position.
139            indent (int | None): The indent amount for the start of the position.
140        """
141
142        self.start = Point(start[0], start[1]) if isinstance(start, tuple) else start
143        self.end = Point(end[0], end[1]) if isinstance(end, tuple) else end
144
145    def __p_code__(self) -> str:
146        return f"Position({p_code(self.start)}, {p_code(self.end)})"
147
148    def __eq__(self, _o):
149        return (
150            isinstance(_o, Position) and _o.start == self.start and _o.end == self.end
151        )
152
153    @staticmethod
154    def from_pos(pos: Position) -> Position:
155        """Create a new position from another position object."""
156        return Position(
157            (pos.start.line, pos.start.column),
158            (pos.end.line, pos.end.column),
159        )
160
161    @staticmethod
162    def from_dict(data: dict) -> Position | None:
163        if data is None:
164            return None
165        return Position(Point.from_dict(data["start"]), Point.from_dict(data["end"]))
166
167    def as_dict(self) -> dict:
168        """Convert the position object to a dict."""
169        return {
170            "start": {
171                "line": self.start.line,
172                "column": self.start.column,
173            },
174            "end": {
175                "line": self.end.line,
176                "column": self.end.column,
177            },
178        }
179
180    def __repr__(self) -> str:
181        # indent = f" ~ {self.indent}" if self.indent is not None else ""
182        return f"<{self.start!r}-{self.end!r}>"
183
184    def __str__(self) -> str:
185        return f"\x1b[38;5;8m<\x1b[39m{self.start}\x1b[38;5;8m-\x1b[39m{self.end}\x1b[38;5;8m>\x1b[39m"
186
187
188class Node:
189    """Base phml node. Defines a type and basic interactions."""
190
191    def __init__(
192        self,
193        _type: NodeType,
194        position: Position | None = None,
195        parent: Parent | None = None,
196        in_pre: bool = False,
197    ) -> None:
198        self._position = position
199        self.parent = parent
200        self._type = _type
201        self.in_pre = in_pre
202
203        def __p_code__(self) -> str:
204            in_pre = f", in_pre={self.in_pre}" if self.in_pre else ""
205            return f"Node({self.type!r}, position={p_code(self.position)}{in_pre})"
206
207    def __eq__(self, _o):
208        return (
209            isinstance(_o, self.__class__)
210            and self.type == _o.type
211            and self.in_pre == _o.in_pre
212        )
213
214    def as_dict(self) -> dict:
215        return {
216            "type": str(self._type),
217        }
218
219    @staticmethod
220    def from_dict(data: dict, in_pre: bool = False):
221        if data["type"] == NodeType.AST:
222            ast = AST(
223                children=[] if data["children"] is not None else None,
224            )
225            if data["children"] is not None:
226                for child in data["children"]:
227                    ast.append(Node.from_dict(child, in_pre))
228            return ast
229        elif data["type"] == NodeType.ELEMENT:
230            return Element.from_dict(data)
231        elif data["type"] == NodeType.LITERAL:
232            return Literal(
233                LiteralType.From(data["name"]),
234                data["content"],
235            )
236        raise ValueError(
237            f"Phml ast dicts must have nodes with the following types: {NodeType.AST}, {NodeType.ELEMENT}, {NodeType.LITERAL}",
238        )
239
240    @property
241    def position(self) -> Position | None:
242        """The position of the node in the parsed phml text.
243        Is `None` if the node was generated.
244        """
245        return self._position
246
247    @property
248    def type(self) -> str:
249        """The node type. Either root, element, or litera."""
250        return self._type
251
252    def pos_as_str(self, color: bool = False) -> str:  # pragma: no cover
253        """Return the position formatted as a string."""
254
255        position = ""
256        if self.position is not None:
257            if color:
258                start = self.position.start
259                end = self.position.end
260                position = SAIML.parse(
261                    f"<[@F244]{start.line}[@F]-[@F244]{start.column}[@F]"
262                    f":[@F244]{end.line}[@F]-[@F244]{end.column}[@F]>",
263                )
264            else:
265                start = self.position.start
266                end = self.position.end
267                position = f"<{start.line}-{start.column}:{end.line}-{end.column}>"
268        return position
269
270    def __repr__(self) -> str:
271        return f"{self.type}()"
272
273    def __format__(self, indent: int = 0, color: bool = False, text: bool = False):
274        if color:
275            return (
276                SAIML.parse(f"{' '*indent}[@Fred]{self.type}[@F]")
277                + f" {self.pos_as_str(True)}"
278            )
279        return f"{' '*indent}{self.type} {self.pos_as_str()}"
280
281    def __str__(self) -> str:
282        return self.__format__()
283
284
285class Parent(Node):
286    def __init__(
287        self,
288        _type: NodeType,
289        children: list[Node] | None,
290        position: Position | None = None,
291        parent: Parent | None = None,
292        in_pre: bool = False,
293    ) -> None:
294        super().__init__(_type, position, parent, in_pre)
295        self.children = [] if children is not None else None
296
297        if children is not None:
298            self.extend(children)
299
300    def __p_code__(self) -> str:
301        children = (
302            "None"
303            if self.children is None
304            else f"[{', '.join([p_code(child) for child in self])}]"
305        )
306        in_pre = f", in_pre={self.in_pre}" if self.in_pre else ""
307        return f"Parent({self.type!r}, position={p_code(self.position)}{in_pre}, children={children})"
308
309    def __iter__(self) -> Iterator[Parent | Literal]:
310        if self.children is not None:
311            yield from self.children
312
313    @overload
314    def __setitem__(self, key: int, value: Node) -> NoReturn:
315        ...
316
317    @overload
318    def __setitem__(self, key: slice, value: list) -> NoReturn:
319        ...
320
321    def __setitem__(self, key: int | slice, value: Node | list):
322        if self.children is not None:
323            if isinstance(key, int):
324                if not isinstance(value, Node):
325                    raise ValueError(
326                        "Can not assign value that is not phml.Node to children",
327                    )
328                value.parent = self
329                self.children[key] = value
330            elif isinstance(key, slice):
331                if not isinstance(value, list):
332                    raise ValueError(
333                        "Can not assign value that is not list[phml.Node] to slice of children",
334                    )
335                for v in value:
336                    v.parent = self
337                self.children[key] = value
338        else:
339            raise ValueError("Invalid value type. Expected phml Node")
340
341    @overload
342    def __getitem__(self, _k: int) -> Parent | Literal:
343        ...
344
345    @overload
346    def __getitem__(self, _k: slice) -> list[Parent | Literal]:
347        ...
348
349    def __getitem__(
350        self,
351        key: int | slice,
352    ) -> Parent | Literal | list[Parent | Literal]:
353        if self.children is not None:
354            return self.children[key]
355        raise ValueError("A self closing element can not be indexed")
356
357    @overload
358    def __delitem__(self, key: int) -> NoReturn:
359        ...
360
361    @overload
362    def __delitem__(self, key: slice) -> NoReturn:
363        ...
364
365    def __delitem__(self, key: int | slice):
366        if self.children is not None:
367            del self.children[key]
368        else:
369            raise ValueError("Can not use del for a self closing elements children")
370
371    def pop(self, idx: int = 0) -> Node:
372        """Pop a node from the children. Defaults to index 0"""
373        if self.children is not None:
374            return self.children.pop(idx)
375        raise ValueError("A self closing element can not pop a child node")
376
377    def index(self, node: Node) -> int:
378        """Get the index of a node in the children."""
379        if self.children is not None:
380            return self.children.index(node)
381        raise ValueError("A self closing element can not be indexed")
382
383    def append(self, node: Node):
384        """Append a child node to the end of the children."""
385        if self.children is not None:
386            node.parent = self
387            self.children.append(node)
388        else:
389            raise ValueError(
390                "A child node can not be appended to a self closing element",
391            )
392
393    def extend(self, nodes: list):
394        """Extend the children with a list of nodes."""
395        if self.children is not None:
396            for child in nodes:
397                child.parent = self
398            self.children.extend(nodes)
399        else:
400            raise ValueError(
401                "A self closing element can not have it's children extended",
402            )
403
404    def insert(self, index: int, nodes: Node | list):
405        """Insert a child node or nodes into a specific index of the children."""
406        if self.children is not None:
407            if isinstance(nodes, list):
408                for n in nodes:
409                    n.parent = self
410                self.children[index:index] = nodes
411            else:
412                self.children.insert(index, nodes)
413        else:
414            raise ValueError(
415                "A child node can not be inserted into a self closing element",
416            )
417
418    def remove(self, node: Node):
419        """Remove a child node from the children."""
420        if self.children is None:
421            raise ValueError(
422                "A child node can not be removed from a self closing element.",
423            )
424        self.children.remove(node)
425
426    def len_as_str(self, color: bool = False) -> str:  # pragma: no cover
427        if color:
428            return SAIML.parse(
429                f"[@F66]{len(self) if self.children is not None else '/'}[@F]",
430            )
431        return f"{len(self) if self.children is not None else '/'}"
432
433    def __len__(self) -> int:
434        return len(self.children) if self.children is not None else 0
435
436    def __repr__(self) -> str:
437        return f"{self.type}(cldrn={self.len_as_str()})"
438
439    def __format__(self, indent: int = 0, color: bool = False, text: bool = False):
440        output = [f"{' '*indent}{self.type} [{self.len_as_str()}]{self.pos_as_str()}"]
441        if color:
442            output[0] = (
443                SAIML.parse(f"{' '*indent}[@Fred]{self.type}[@F]")
444                + f" [{self.len_as_str(True)}]"
445                + f" {self.pos_as_str(True)}"
446            )
447        for child in self.children or []:
448            output.extend(child.__format__(indent=indent + 2, color=color, text=text))
449        return output
450
451    def __str__(self) -> str:
452        return "\n".join(self.__format__())
453
454    def as_dict(self) -> dict:
455        return {
456            "children": [child.as_dict() for child in self.children]
457            if self.children is not None
458            else None,
459            **super().as_dict(),
460        }
461
462
463class AST(Parent):
464    def __init__(
465        self,
466        children: list[Node] | None = None,
467        position: Position | None = None,
468        in_pre: bool = False,
469    ) -> None:
470        super().__init__(NodeType.AST, children or [], position, None, in_pre)
471
472    def __eq__(self, _o):
473        return isinstance(_o, AST) and (
474            (_o.children is None and self.children is None)
475            or (len(_o) == len(self) and all(c1 == c2 for c1, c2 in zip(_o, self)))
476        )
477
478    def __p_code__(self) -> str:
479        children = (
480            "None"
481            if self.children is None
482            else f"[{', '.join([p_code(child) for child in self])}]"
483        )
484        in_pre = f", in_pre={self.in_pre}" if self.in_pre else ""
485        return f"AST(position={p_code(self.position)}, children={children}{in_pre})"
486
487
488class Element(Parent):
489    def __init__(
490        self,
491        tag: str,
492        attributes: dict[str, Attribute] | None = None,
493        children: list[Node] | None = None,
494        position: Position | None = None,
495        parent: Parent | None = None,
496        in_pre: bool = False,
497    ) -> None:
498        super().__init__(NodeType.ELEMENT, children, position, parent, in_pre)
499        self.tag = tag
500        self.attributes = attributes or {}
501        self.context = {}
502
503    def __p_code__(self) -> str:
504        children = (
505            "None"
506            if self.children is None
507            else f"[{', '.join([p_code(child) for child in self])}]"
508        )
509        in_pre = f", in_pre={self.in_pre}" if self.in_pre else ""
510        return f"Element({self.tag!r}, position={p_code(self.position)}, attributes={self.attributes}, children={children}{in_pre})"
511
512    def __eq__(self, _o) -> bool:
513        return (
514            isinstance(_o, Element)
515            and _o.tag == self.tag
516            and (
517                len(self.attributes) == len(_o.attributes)
518                and all(key in self.attributes for key in _o.attributes)
519                and all(_o.attributes[key] == value for key,value in self.attributes.items())
520            )
521            and (
522                (_o.children is None and self.children is None)
523                or (len(_o) == len(self) and all(c1 == c2 for c1, c2 in zip(_o, self)))
524            )
525        )
526
527    def as_dict(self) -> dict:
528        return {"tag": self.tag, "attributes": self.attributes, **super().as_dict()}
529
530    @staticmethod
531    def from_dict(data: dict, in_pre: bool = False) -> Element:
532        element = Element(
533            data["tag"],
534            attributes=data["attributes"],
535            children=[] if data["children"] is not None else None,
536        )
537        if data["children"] is not None:
538            element.children = [
539                Node.from_dict(child, in_pre or data["tag"] == "pre")
540                for child in data["children"]
541            ]
542        return element
543
544    @property
545    def tag_path(self) -> list[str]:
546        """Get the list of all the tags to the current element. Inclusive."""
547        path = [self.tag]
548        parent = self
549        while isinstance(parent.parent, Element):
550            path.append(parent.parent.tag)
551            parent = parent.parent
552
553        path.reverse()
554        return path
555
556    def __hash__(self) -> int:
557        return (
558            hash(self.tag)
559            + sum(hash(attr) for attr in self.attributes.values())
560            + hash(len(self))
561        )
562
563    def __contains__(self, _k: str) -> bool:
564        return _k in self.attributes
565
566    @overload
567    def __getitem__(self, _k: int) -> Parent | Literal:
568        ...
569
570    @overload
571    def __getitem__(self, _k: str) -> Attribute:
572        ...
573
574    @overload
575    def __getitem__(self, _k: slice) -> list[Parent | Literal]:
576        ...
577
578    def __getitem__(
579        self,
580        _k: str | int | slice,
581    ) -> Attribute | Parent | Literal | list[Parent | Literal]:
582        if isinstance(_k, str):
583            return self.attributes[_k]
584
585        if self.children is not None:
586            return self.children[_k]
587
588        raise ValueError("A self closing element can not have it's children indexed")
589
590    @overload
591    def __setitem__(self, key: int, value: Node) -> NoReturn:
592        ...
593
594    @overload
595    def __setitem__(self, key: slice, value: list) -> NoReturn:
596        ...
597
598    @overload
599    def __setitem__(self, key: str, value: Attribute) -> NoReturn:
600        ...
601
602    def __setitem__(self, key: str | int | slice, value: Attribute | Node | list):
603        if isinstance(key, str) and isinstance(value, Attribute):
604            self.attributes[key] = value
605        elif self.children is not None:
606            if isinstance(key, int) and isinstance(value, Node):
607                value.parent = self
608                self.children[key] = value
609            elif isinstance(key, slice) and isinstance(value, list):
610                for child in value:
611                    child.parent = self
612                self.children[key] = value
613        else:
614            raise ValueError(
615                "A self closing element can not have a subset of it's children assigned to",
616            )
617
618    @overload
619    def __delitem__(self, key: int) -> NoReturn:
620        ...
621
622    @overload
623    def __delitem__(self, key: slice) -> NoReturn:
624        ...
625
626    @overload
627    def __delitem__(self, key: str) -> NoReturn:
628        ...
629
630    def __delitem__(self, key: str | int | slice):
631        if isinstance(key, str):
632            del self.attributes[key]
633        elif self.children is not None:
634            del self.children[key]
635        else:
636            raise ValueError("Can not use del for a self closing elements children")
637
638    @overload
639    def pop(self, idx: int = 0) -> Node:
640        ...
641
642    @overload
643    def pop(self, idx: str, _default: Any = MISSING) -> Attribute:
644        ...
645
646    def pop(self, idx: str | int = 0, _default: Any = MISSING) -> Attribute | Node:
647        """Pop a specific attribute from the elements attributes. A default value
648        can be provided for when the value is not found, otherwise an error is thrown.
649        """
650        if isinstance(idx, str):
651            if _default != MISSING:
652                return self.attributes.pop(idx, _default)
653            return self.attributes.pop(idx)
654        if self.children is not None:
655            return self.children.pop(idx)
656
657        raise ValueError("A self closing element can not pop a child node")
658
659    def get(self, key: str, _default: Any = MISSING) -> Attribute:
660        """Get a specific element attribute. Returns `None` if not found
661        unless `_default` is defined.
662
663        Args:
664            key (str): The name of the attribute to retrieve.
665            _default (str|bool): The default value to return if the key
666                isn't an attribute.
667
668        Returns:
669            str|bool|None: str or bool if the attribute exists or a default
670                was provided, else None
671        """
672        if not isinstance(_default, (Attribute, NoneType)) and _default != MISSING:
673            raise TypeError("_default value must be str, bool, or MISSING")
674
675        if key in self:
676            return self[key]
677        if _default != MISSING:
678            return _default
679        raise ValueError(f"Attribute {key!r} not found")
680
681    def attrs_as_str(self, indent: int, color: bool = False) -> str:  # pragma: no cover
682        """Return a str representation of the attributes"""
683        if color:
684            attrs = (
685                (
686                    f"\n{' '*(indent)}▸ "
687                    + f"\n{' '*(indent)}▸ ".join(
688                        str(key)
689                        + ": "
690                        + (
691                            f"\x1b[32m{value!r}\x1b[39m"
692                            if isinstance(value, str)
693                            else f"\x1b[35m{value}\x1b[39m"
694                        )
695                        for key, value in self.attributes.items()
696                    )
697                )
698                if len(self.attributes) > 0
699                else ""
700            )
701        else:
702            attrs = (
703                (
704                    f"\n{' '*(indent)}▸ "
705                    + f"\n{' '*(indent)}▸ ".join(
706                        f"{key}: {value!r}" for key, value in self.attributes.items()
707                    )
708                )
709                if len(self.attributes) > 0
710                else ""
711            )
712
713        return attrs
714
715    def __repr__(self) -> str:
716        return f"{self.type}.{self.tag}(cldrn={self.len_as_str()}, attrs={self.attributes})"
717
718    def __format__(
719        self,
720        indent: int = 0,
721        color: bool = False,
722        text: bool = False,
723    ) -> list[str]:  # pragma: no cover
724        output: list[str] = []
725        if color:
726            output.append(
727                f"{' '*indent}"
728                + SAIML.parse(f"[@Fred]{self.type}[@F]" + f".[@Fblue]{self.tag}[@F]")
729                + f" [{self.len_as_str(True)}]"
730                + f" {self.pos_as_str(True)}"
731                + f"{self.attrs_as_str(indent+2, True)}",
732            )
733        else:
734            output.append(
735                f"{' '*indent}{self.type}.{self.tag}"
736                + f" [{self.len_as_str()}]{self.pos_as_str()}{self.attrs_as_str(indent+2)}",
737            )
738
739        for child in self.children or []:
740            output.extend(child.__format__(indent=indent + 2, color=color, text=text))
741        return output
742
743    def __str__(self) -> str:
744        return "\n".join(self.__format__())
745
746
747class Literal(Node):
748    def __init__(
749        self,
750        name: str,
751        content: str,
752        parent: Parent | None = None,
753        position: Position | None = None,
754        in_pre: bool = False,
755    ) -> None:
756        super().__init__(NodeType.LITERAL, position, parent, in_pre)
757        self.name = name
758        self.content = content
759
760    def __hash__(self) -> int:
761        return hash(self.content) + hash(str(self.name))
762
763    def __p_code__(self) -> str:
764        in_pre = ", in_pre=True" if self.in_pre else ""
765        return f"Literal({str(self.name)!r}, {self.content!r}{in_pre})"
766
767    def __eq__(self, _o) -> bool:
768        return (
769            isinstance(_o, Literal)
770            and _o.type == self.type
771            and self.name == _o.name
772            and self.content == _o.content
773        )
774
775    def as_dict(self) -> dict:
776        return {"name": str(self.name), "content": self.content, **super().as_dict()}
777
778    @staticmethod
779    def is_text(node: Node) -> bool:
780        """Check if a node is a literal and a text node."""
781        return isinstance(node, Literal) and node.name == LiteralType.Text
782
783    @staticmethod
784    def is_comment(node: Node) -> bool:
785        """Check if a node is a literal and a comment."""
786        return isinstance(node, Literal) and node.name == LiteralType.Comment
787
788    def __repr__(self) -> str:  # pragma: no cover
789        return f"{self.type}.{self.name}(len={len(self.content)})"
790
791    def __format__(
792        self,
793        indent: int = 0,
794        color: bool = False,
795        text: bool = False,
796    ):  # pragma: no cover
797        from .helpers import normalize_indent
798
799        content = ""
800        if text:
801            offset = " " * (indent + 2)
802            content = (
803                f'{offset}"""\n{normalize_indent(self.content, indent+4)}\n{offset}"""'
804            )
805        if color:
806            return [
807                SAIML.parse(
808                    f"{' '*indent}[@Fred]{self.type}[@F].[@Fblue]{self.name}[@F]"
809                    + (f"\n[@Fgreen]{SAIML.escape(content)}[@F]" if text else ""),
810                ),
811            ]
812        return [
813            f"{' '*indent}{self.type}.{self.name}" + (f"\n{content}" if text else ""),
814        ]
815
816    def __str__(self) -> str:  # pragma: no cover
817        return self.__format__()[0]
818
819
820def inspect(
821    node: Node,
822    color: bool = False,
823    text: bool = False,
824) -> str:  # pragma: no cover
825    """Inspected a given node recursively.
826
827    Args:
828        node (Node): Any type of node to inspect.
829        color (bool): Whether to return a string with ansi encoding. Default False.
830        text (bool): Whether to include the text from comment and text nodes. Default False.
831
832    Return:
833        A formatted multiline string representation of the node and it's children.
834    """
835    if isinstance(node, Node):
836        return "\n".join(node.__format__(color=color, text=text))
837    raise TypeError(f"Can only inspect phml Nodes was, {node!r}")
class Missing:
13class Missing:
14    pass
Missing()
def p_code(value) -> str:
20def p_code(value) -> str:  # pragma: no cover
21    """Get python code representation of phml nodes."""
22    if value is None:
23        return "None"
24    return value.__p_code__()

Get python code representation of phml nodes.

@unique
class LiteralType(enum.StrEnum):
27@unique
28class LiteralType(StrEnum):
29    Text = "text"
30    Comment = "comment"
31
32    @staticmethod
33    def From(type: str) -> str:
34        types = ["text", "comment"]
35        if type in types:
36            return type
37        raise ValueError(f"Expected on of {', '.join(types)}")

Enum where members are also (and must be) strings

@staticmethod
def From(type: str) -> str:
32    @staticmethod
33    def From(type: str) -> str:
34        types = ["text", "comment"]
35        if type in types:
36            return type
37        raise ValueError(f"Expected on of {', '.join(types)}")
Inherited Members
enum.Enum
name
value
builtins.str
encode
replace
split
rsplit
join
capitalize
casefold
title
center
count
expandtabs
find
partition
index
ljust
lower
lstrip
rfind
rindex
rjust
rstrip
rpartition
splitlines
strip
swapcase
translate
upper
startswith
endswith
removeprefix
removesuffix
isascii
islower
isupper
istitle
isspace
isdecimal
isdigit
isnumeric
isalpha
isalnum
isidentifier
isprintable
zfill
format
format_map
maketrans
@unique
class NodeType(enum.StrEnum):
40@unique
41class NodeType(StrEnum):
42    AST = "ast"
43    ELEMENT = "element"
44    LITERAL = "literal"

Enum where members are also (and must be) strings

Inherited Members
enum.Enum
name
value
builtins.str
encode
replace
split
rsplit
join
capitalize
casefold
title
center
count
expandtabs
find
partition
index
ljust
lower
lstrip
rfind
rindex
rjust
rstrip
rpartition
splitlines
strip
swapcase
translate
upper
startswith
endswith
removeprefix
removesuffix
isascii
islower
isupper
istitle
isspace
isdecimal
isdigit
isnumeric
isalpha
isalnum
isidentifier
isprintable
zfill
format
format_map
maketrans
class Point:
47class Point:
48    """Represents one place in a source file.
49
50    The line field (1-indexed integer) represents a line in a source file. The column field
51    (1-indexed integer) represents a column in a source file. The offset field (0-indexed integer)
52    represents a character in a source file.
53    """
54
55    def __init__(self, line: int, column: int) -> None:
56        if line is None or line < 0:
57            raise IndexError(f"Point.line must be >= 0 but was {line}")
58
59        self.line = line
60
61        if column is None or column < 0:
62            raise IndexError(f"Point.column must be >= 0 but was {column}")
63
64        self.column = column
65
66    def __eq__(self, _o) -> bool:
67        return (
68            isinstance(_o, self.__class__)
69            and _o.line == self.line
70            and _o.column == self.column
71        )
72
73    @staticmethod
74    def from_dict(data: dict) -> Point:
75        return Point(data["line"], data["column"])
76
77    def __p_code__(self) -> str:
78        return f"Point({self.line}, {self.column})"
79
80    def __repr__(self) -> str:
81        return f"{self.line}:{self.column}"
82
83    def __str__(self) -> str:
84        return f"\x1b[38;5;244m{self.line}:{self.column}\x1b[39m"

Represents one place in a source file.

The line field (1-indexed integer) represents a line in a source file. The column field (1-indexed integer) represents a column in a source file. The offset field (0-indexed integer) represents a character in a source file.

Point(line: int, column: int)
55    def __init__(self, line: int, column: int) -> None:
56        if line is None or line < 0:
57            raise IndexError(f"Point.line must be >= 0 but was {line}")
58
59        self.line = line
60
61        if column is None or column < 0:
62            raise IndexError(f"Point.column must be >= 0 but was {column}")
63
64        self.column = column
@staticmethod
def from_dict(data: dict) -> phml.nodes.Point:
73    @staticmethod
74    def from_dict(data: dict) -> Point:
75        return Point(data["line"], data["column"])
class Position:
 87class Position:
 88    """Position represents the location of a node in a source file.
 89
 90    The `start` field of `Position` represents the place of the first character
 91    of the parsed source region. The `end` field of Position represents the place
 92    of the first character after the parsed source region, whether it exists or not.
 93    The value of the `start` and `end` fields implement the `Point` interface.
 94
 95    The `indent` field of `Position` represents the start column at each index
 96    (plus start line) in the source region, for elements that span multiple lines.
 97
 98    If the syntactic unit represented by a node is not present in the source file at
 99    the time of parsing, the node is said to be `generated` and it must not have positional
100    information.
101    """
102
103    @overload
104    def __init__(
105        self,
106        start: Point,
107        end: Point,
108    ) -> None:
109        """
110        Args:
111            start (tuple[int, int, int  |  None]): Tuple representing the line, column, and optional
112            offset of the start point.
113            end (tuple[int, int, int  |  None]): Tuple representing the line, column, and optional
114            offset of the end point.
115            indent (Optional[int], optional): The indent amount for the start of the position.
116        """
117        ...
118
119    @overload
120    def __init__(
121        self,
122        start: tuple[int, int],
123        end: tuple[int, int],
124    ) -> None:
125        """
126        Args:
127            start (tuple[int, int, int  |  None]): Tuple representing the line, column, and optional
128            offset of the start point.
129            end (tuple[int, int, int  |  None]): Tuple representing the line, column, and optional
130            offset of the end point.
131            indent (Optional[int], optional): The indent amount for the start of the position.
132        """
133        ...
134
135    def __init__(self, start: Point | tuple[int, int], end: Point | tuple[int, int]):
136        """
137        Args:
138            start (Point): Starting point of the position.
139            end (Point): End point of the position.
140            indent (int | None): The indent amount for the start of the position.
141        """
142
143        self.start = Point(start[0], start[1]) if isinstance(start, tuple) else start
144        self.end = Point(end[0], end[1]) if isinstance(end, tuple) else end
145
146    def __p_code__(self) -> str:
147        return f"Position({p_code(self.start)}, {p_code(self.end)})"
148
149    def __eq__(self, _o):
150        return (
151            isinstance(_o, Position) and _o.start == self.start and _o.end == self.end
152        )
153
154    @staticmethod
155    def from_pos(pos: Position) -> Position:
156        """Create a new position from another position object."""
157        return Position(
158            (pos.start.line, pos.start.column),
159            (pos.end.line, pos.end.column),
160        )
161
162    @staticmethod
163    def from_dict(data: dict) -> Position | None:
164        if data is None:
165            return None
166        return Position(Point.from_dict(data["start"]), Point.from_dict(data["end"]))
167
168    def as_dict(self) -> dict:
169        """Convert the position object to a dict."""
170        return {
171            "start": {
172                "line": self.start.line,
173                "column": self.start.column,
174            },
175            "end": {
176                "line": self.end.line,
177                "column": self.end.column,
178            },
179        }
180
181    def __repr__(self) -> str:
182        # indent = f" ~ {self.indent}" if self.indent is not None else ""
183        return f"<{self.start!r}-{self.end!r}>"
184
185    def __str__(self) -> str:
186        return f"\x1b[38;5;8m<\x1b[39m{self.start}\x1b[38;5;8m-\x1b[39m{self.end}\x1b[38;5;8m>\x1b[39m"

Position represents the location of a node in a source file.

The start field of Position represents the place of the first character of the parsed source region. The end field of Position represents the place of the first character after the parsed source region, whether it exists or not. The value of the start and end fields implement the Point interface.

The indent field of Position represents the start column at each index (plus start line) in the source region, for elements that span multiple lines.

If the syntactic unit represented by a node is not present in the source file at the time of parsing, the node is said to be generated and it must not have positional information.

Position( start: phml.nodes.Point | tuple[int, int], end: phml.nodes.Point | tuple[int, int])
135    def __init__(self, start: Point | tuple[int, int], end: Point | tuple[int, int]):
136        """
137        Args:
138            start (Point): Starting point of the position.
139            end (Point): End point of the position.
140            indent (int | None): The indent amount for the start of the position.
141        """
142
143        self.start = Point(start[0], start[1]) if isinstance(start, tuple) else start
144        self.end = Point(end[0], end[1]) if isinstance(end, tuple) else end
Args
  • start (Point): Starting point of the position.
  • end (Point): End point of the position.
  • indent (int | None): The indent amount for the start of the position.
@staticmethod
def from_pos(pos: phml.nodes.Position) -> phml.nodes.Position:
154    @staticmethod
155    def from_pos(pos: Position) -> Position:
156        """Create a new position from another position object."""
157        return Position(
158            (pos.start.line, pos.start.column),
159            (pos.end.line, pos.end.column),
160        )

Create a new position from another position object.

@staticmethod
def from_dict(data: dict) -> phml.nodes.Position | None:
162    @staticmethod
163    def from_dict(data: dict) -> Position | None:
164        if data is None:
165            return None
166        return Position(Point.from_dict(data["start"]), Point.from_dict(data["end"]))
def as_dict(self) -> dict:
168    def as_dict(self) -> dict:
169        """Convert the position object to a dict."""
170        return {
171            "start": {
172                "line": self.start.line,
173                "column": self.start.column,
174            },
175            "end": {
176                "line": self.end.line,
177                "column": self.end.column,
178            },
179        }

Convert the position object to a dict.

class Node:
189class Node:
190    """Base phml node. Defines a type and basic interactions."""
191
192    def __init__(
193        self,
194        _type: NodeType,
195        position: Position | None = None,
196        parent: Parent | None = None,
197        in_pre: bool = False,
198    ) -> None:
199        self._position = position
200        self.parent = parent
201        self._type = _type
202        self.in_pre = in_pre
203
204        def __p_code__(self) -> str:
205            in_pre = f", in_pre={self.in_pre}" if self.in_pre else ""
206            return f"Node({self.type!r}, position={p_code(self.position)}{in_pre})"
207
208    def __eq__(self, _o):
209        return (
210            isinstance(_o, self.__class__)
211            and self.type == _o.type
212            and self.in_pre == _o.in_pre
213        )
214
215    def as_dict(self) -> dict:
216        return {
217            "type": str(self._type),
218        }
219
220    @staticmethod
221    def from_dict(data: dict, in_pre: bool = False):
222        if data["type"] == NodeType.AST:
223            ast = AST(
224                children=[] if data["children"] is not None else None,
225            )
226            if data["children"] is not None:
227                for child in data["children"]:
228                    ast.append(Node.from_dict(child, in_pre))
229            return ast
230        elif data["type"] == NodeType.ELEMENT:
231            return Element.from_dict(data)
232        elif data["type"] == NodeType.LITERAL:
233            return Literal(
234                LiteralType.From(data["name"]),
235                data["content"],
236            )
237        raise ValueError(
238            f"Phml ast dicts must have nodes with the following types: {NodeType.AST}, {NodeType.ELEMENT}, {NodeType.LITERAL}",
239        )
240
241    @property
242    def position(self) -> Position | None:
243        """The position of the node in the parsed phml text.
244        Is `None` if the node was generated.
245        """
246        return self._position
247
248    @property
249    def type(self) -> str:
250        """The node type. Either root, element, or litera."""
251        return self._type
252
253    def pos_as_str(self, color: bool = False) -> str:  # pragma: no cover
254        """Return the position formatted as a string."""
255
256        position = ""
257        if self.position is not None:
258            if color:
259                start = self.position.start
260                end = self.position.end
261                position = SAIML.parse(
262                    f"<[@F244]{start.line}[@F]-[@F244]{start.column}[@F]"
263                    f":[@F244]{end.line}[@F]-[@F244]{end.column}[@F]>",
264                )
265            else:
266                start = self.position.start
267                end = self.position.end
268                position = f"<{start.line}-{start.column}:{end.line}-{end.column}>"
269        return position
270
271    def __repr__(self) -> str:
272        return f"{self.type}()"
273
274    def __format__(self, indent: int = 0, color: bool = False, text: bool = False):
275        if color:
276            return (
277                SAIML.parse(f"{' '*indent}[@Fred]{self.type}[@F]")
278                + f" {self.pos_as_str(True)}"
279            )
280        return f"{' '*indent}{self.type} {self.pos_as_str()}"
281
282    def __str__(self) -> str:
283        return self.__format__()

Base phml node. Defines a type and basic interactions.

Node( _type: phml.nodes.NodeType, position: phml.nodes.Position | None = None, parent: phml.nodes.Parent | None = None, in_pre: bool = False)
192    def __init__(
193        self,
194        _type: NodeType,
195        position: Position | None = None,
196        parent: Parent | None = None,
197        in_pre: bool = False,
198    ) -> None:
199        self._position = position
200        self.parent = parent
201        self._type = _type
202        self.in_pre = in_pre
203
204        def __p_code__(self) -> str:
205            in_pre = f", in_pre={self.in_pre}" if self.in_pre else ""
206            return f"Node({self.type!r}, position={p_code(self.position)}{in_pre})"
def as_dict(self) -> dict:
215    def as_dict(self) -> dict:
216        return {
217            "type": str(self._type),
218        }
@staticmethod
def from_dict(data: dict, in_pre: bool = False):
220    @staticmethod
221    def from_dict(data: dict, in_pre: bool = False):
222        if data["type"] == NodeType.AST:
223            ast = AST(
224                children=[] if data["children"] is not None else None,
225            )
226            if data["children"] is not None:
227                for child in data["children"]:
228                    ast.append(Node.from_dict(child, in_pre))
229            return ast
230        elif data["type"] == NodeType.ELEMENT:
231            return Element.from_dict(data)
232        elif data["type"] == NodeType.LITERAL:
233            return Literal(
234                LiteralType.From(data["name"]),
235                data["content"],
236            )
237        raise ValueError(
238            f"Phml ast dicts must have nodes with the following types: {NodeType.AST}, {NodeType.ELEMENT}, {NodeType.LITERAL}",
239        )
position: phml.nodes.Position | None

The position of the node in the parsed phml text. Is None if the node was generated.

type: str

The node type. Either root, element, or litera.

def pos_as_str(self, color: bool = False) -> str:
253    def pos_as_str(self, color: bool = False) -> str:  # pragma: no cover
254        """Return the position formatted as a string."""
255
256        position = ""
257        if self.position is not None:
258            if color:
259                start = self.position.start
260                end = self.position.end
261                position = SAIML.parse(
262                    f"<[@F244]{start.line}[@F]-[@F244]{start.column}[@F]"
263                    f":[@F244]{end.line}[@F]-[@F244]{end.column}[@F]>",
264                )
265            else:
266                start = self.position.start
267                end = self.position.end
268                position = f"<{start.line}-{start.column}:{end.line}-{end.column}>"
269        return position

Return the position formatted as a string.

class Parent(Node):
286class Parent(Node):
287    def __init__(
288        self,
289        _type: NodeType,
290        children: list[Node] | None,
291        position: Position | None = None,
292        parent: Parent | None = None,
293        in_pre: bool = False,
294    ) -> None:
295        super().__init__(_type, position, parent, in_pre)
296        self.children = [] if children is not None else None
297
298        if children is not None:
299            self.extend(children)
300
301    def __p_code__(self) -> str:
302        children = (
303            "None"
304            if self.children is None
305            else f"[{', '.join([p_code(child) for child in self])}]"
306        )
307        in_pre = f", in_pre={self.in_pre}" if self.in_pre else ""
308        return f"Parent({self.type!r}, position={p_code(self.position)}{in_pre}, children={children})"
309
310    def __iter__(self) -> Iterator[Parent | Literal]:
311        if self.children is not None:
312            yield from self.children
313
314    @overload
315    def __setitem__(self, key: int, value: Node) -> NoReturn:
316        ...
317
318    @overload
319    def __setitem__(self, key: slice, value: list) -> NoReturn:
320        ...
321
322    def __setitem__(self, key: int | slice, value: Node | list):
323        if self.children is not None:
324            if isinstance(key, int):
325                if not isinstance(value, Node):
326                    raise ValueError(
327                        "Can not assign value that is not phml.Node to children",
328                    )
329                value.parent = self
330                self.children[key] = value
331            elif isinstance(key, slice):
332                if not isinstance(value, list):
333                    raise ValueError(
334                        "Can not assign value that is not list[phml.Node] to slice of children",
335                    )
336                for v in value:
337                    v.parent = self
338                self.children[key] = value
339        else:
340            raise ValueError("Invalid value type. Expected phml Node")
341
342    @overload
343    def __getitem__(self, _k: int) -> Parent | Literal:
344        ...
345
346    @overload
347    def __getitem__(self, _k: slice) -> list[Parent | Literal]:
348        ...
349
350    def __getitem__(
351        self,
352        key: int | slice,
353    ) -> Parent | Literal | list[Parent | Literal]:
354        if self.children is not None:
355            return self.children[key]
356        raise ValueError("A self closing element can not be indexed")
357
358    @overload
359    def __delitem__(self, key: int) -> NoReturn:
360        ...
361
362    @overload
363    def __delitem__(self, key: slice) -> NoReturn:
364        ...
365
366    def __delitem__(self, key: int | slice):
367        if self.children is not None:
368            del self.children[key]
369        else:
370            raise ValueError("Can not use del for a self closing elements children")
371
372    def pop(self, idx: int = 0) -> Node:
373        """Pop a node from the children. Defaults to index 0"""
374        if self.children is not None:
375            return self.children.pop(idx)
376        raise ValueError("A self closing element can not pop a child node")
377
378    def index(self, node: Node) -> int:
379        """Get the index of a node in the children."""
380        if self.children is not None:
381            return self.children.index(node)
382        raise ValueError("A self closing element can not be indexed")
383
384    def append(self, node: Node):
385        """Append a child node to the end of the children."""
386        if self.children is not None:
387            node.parent = self
388            self.children.append(node)
389        else:
390            raise ValueError(
391                "A child node can not be appended to a self closing element",
392            )
393
394    def extend(self, nodes: list):
395        """Extend the children with a list of nodes."""
396        if self.children is not None:
397            for child in nodes:
398                child.parent = self
399            self.children.extend(nodes)
400        else:
401            raise ValueError(
402                "A self closing element can not have it's children extended",
403            )
404
405    def insert(self, index: int, nodes: Node | list):
406        """Insert a child node or nodes into a specific index of the children."""
407        if self.children is not None:
408            if isinstance(nodes, list):
409                for n in nodes:
410                    n.parent = self
411                self.children[index:index] = nodes
412            else:
413                self.children.insert(index, nodes)
414        else:
415            raise ValueError(
416                "A child node can not be inserted into a self closing element",
417            )
418
419    def remove(self, node: Node):
420        """Remove a child node from the children."""
421        if self.children is None:
422            raise ValueError(
423                "A child node can not be removed from a self closing element.",
424            )
425        self.children.remove(node)
426
427    def len_as_str(self, color: bool = False) -> str:  # pragma: no cover
428        if color:
429            return SAIML.parse(
430                f"[@F66]{len(self) if self.children is not None else '/'}[@F]",
431            )
432        return f"{len(self) if self.children is not None else '/'}"
433
434    def __len__(self) -> int:
435        return len(self.children) if self.children is not None else 0
436
437    def __repr__(self) -> str:
438        return f"{self.type}(cldrn={self.len_as_str()})"
439
440    def __format__(self, indent: int = 0, color: bool = False, text: bool = False):
441        output = [f"{' '*indent}{self.type} [{self.len_as_str()}]{self.pos_as_str()}"]
442        if color:
443            output[0] = (
444                SAIML.parse(f"{' '*indent}[@Fred]{self.type}[@F]")
445                + f" [{self.len_as_str(True)}]"
446                + f" {self.pos_as_str(True)}"
447            )
448        for child in self.children or []:
449            output.extend(child.__format__(indent=indent + 2, color=color, text=text))
450        return output
451
452    def __str__(self) -> str:
453        return "\n".join(self.__format__())
454
455    def as_dict(self) -> dict:
456        return {
457            "children": [child.as_dict() for child in self.children]
458            if self.children is not None
459            else None,
460            **super().as_dict(),
461        }

Base phml node. Defines a type and basic interactions.

Parent( _type: phml.nodes.NodeType, children: list[phml.nodes.Node] | None, position: phml.nodes.Position | None = None, parent: phml.nodes.Parent | None = None, in_pre: bool = False)
287    def __init__(
288        self,
289        _type: NodeType,
290        children: list[Node] | None,
291        position: Position | None = None,
292        parent: Parent | None = None,
293        in_pre: bool = False,
294    ) -> None:
295        super().__init__(_type, position, parent, in_pre)
296        self.children = [] if children is not None else None
297
298        if children is not None:
299            self.extend(children)
def pop(self, idx: int = 0) -> phml.nodes.Node:
372    def pop(self, idx: int = 0) -> Node:
373        """Pop a node from the children. Defaults to index 0"""
374        if self.children is not None:
375            return self.children.pop(idx)
376        raise ValueError("A self closing element can not pop a child node")

Pop a node from the children. Defaults to index 0

def index(self, node: phml.nodes.Node) -> int:
378    def index(self, node: Node) -> int:
379        """Get the index of a node in the children."""
380        if self.children is not None:
381            return self.children.index(node)
382        raise ValueError("A self closing element can not be indexed")

Get the index of a node in the children.

def append(self, node: phml.nodes.Node):
384    def append(self, node: Node):
385        """Append a child node to the end of the children."""
386        if self.children is not None:
387            node.parent = self
388            self.children.append(node)
389        else:
390            raise ValueError(
391                "A child node can not be appended to a self closing element",
392            )

Append a child node to the end of the children.

def extend(self, nodes: list):
394    def extend(self, nodes: list):
395        """Extend the children with a list of nodes."""
396        if self.children is not None:
397            for child in nodes:
398                child.parent = self
399            self.children.extend(nodes)
400        else:
401            raise ValueError(
402                "A self closing element can not have it's children extended",
403            )

Extend the children with a list of nodes.

def insert(self, index: int, nodes: phml.nodes.Node | list):
405    def insert(self, index: int, nodes: Node | list):
406        """Insert a child node or nodes into a specific index of the children."""
407        if self.children is not None:
408            if isinstance(nodes, list):
409                for n in nodes:
410                    n.parent = self
411                self.children[index:index] = nodes
412            else:
413                self.children.insert(index, nodes)
414        else:
415            raise ValueError(
416                "A child node can not be inserted into a self closing element",
417            )

Insert a child node or nodes into a specific index of the children.

def remove(self, node: phml.nodes.Node):
419    def remove(self, node: Node):
420        """Remove a child node from the children."""
421        if self.children is None:
422            raise ValueError(
423                "A child node can not be removed from a self closing element.",
424            )
425        self.children.remove(node)

Remove a child node from the children.

def len_as_str(self, color: bool = False) -> str:
427    def len_as_str(self, color: bool = False) -> str:  # pragma: no cover
428        if color:
429            return SAIML.parse(
430                f"[@F66]{len(self) if self.children is not None else '/'}[@F]",
431            )
432        return f"{len(self) if self.children is not None else '/'}"
def as_dict(self) -> dict:
455    def as_dict(self) -> dict:
456        return {
457            "children": [child.as_dict() for child in self.children]
458            if self.children is not None
459            else None,
460            **super().as_dict(),
461        }
Inherited Members
Node
from_dict
position
type
pos_as_str
class AST(Parent):
464class AST(Parent):
465    def __init__(
466        self,
467        children: list[Node] | None = None,
468        position: Position | None = None,
469        in_pre: bool = False,
470    ) -> None:
471        super().__init__(NodeType.AST, children or [], position, None, in_pre)
472
473    def __eq__(self, _o):
474        return isinstance(_o, AST) and (
475            (_o.children is None and self.children is None)
476            or (len(_o) == len(self) and all(c1 == c2 for c1, c2 in zip(_o, self)))
477        )
478
479    def __p_code__(self) -> str:
480        children = (
481            "None"
482            if self.children is None
483            else f"[{', '.join([p_code(child) for child in self])}]"
484        )
485        in_pre = f", in_pre={self.in_pre}" if self.in_pre else ""
486        return f"AST(position={p_code(self.position)}, children={children}{in_pre})"

Base phml node. Defines a type and basic interactions.

AST( children: list[phml.nodes.Node] | None = None, position: phml.nodes.Position | None = None, in_pre: bool = False)
465    def __init__(
466        self,
467        children: list[Node] | None = None,
468        position: Position | None = None,
469        in_pre: bool = False,
470    ) -> None:
471        super().__init__(NodeType.AST, children or [], position, None, in_pre)
class Element(Parent):
489class Element(Parent):
490    def __init__(
491        self,
492        tag: str,
493        attributes: dict[str, Attribute] | None = None,
494        children: list[Node] | None = None,
495        position: Position | None = None,
496        parent: Parent | None = None,
497        in_pre: bool = False,
498    ) -> None:
499        super().__init__(NodeType.ELEMENT, children, position, parent, in_pre)
500        self.tag = tag
501        self.attributes = attributes or {}
502        self.context = {}
503
504    def __p_code__(self) -> str:
505        children = (
506            "None"
507            if self.children is None
508            else f"[{', '.join([p_code(child) for child in self])}]"
509        )
510        in_pre = f", in_pre={self.in_pre}" if self.in_pre else ""
511        return f"Element({self.tag!r}, position={p_code(self.position)}, attributes={self.attributes}, children={children}{in_pre})"
512
513    def __eq__(self, _o) -> bool:
514        return (
515            isinstance(_o, Element)
516            and _o.tag == self.tag
517            and (
518                len(self.attributes) == len(_o.attributes)
519                and all(key in self.attributes for key in _o.attributes)
520                and all(_o.attributes[key] == value for key,value in self.attributes.items())
521            )
522            and (
523                (_o.children is None and self.children is None)
524                or (len(_o) == len(self) and all(c1 == c2 for c1, c2 in zip(_o, self)))
525            )
526        )
527
528    def as_dict(self) -> dict:
529        return {"tag": self.tag, "attributes": self.attributes, **super().as_dict()}
530
531    @staticmethod
532    def from_dict(data: dict, in_pre: bool = False) -> Element:
533        element = Element(
534            data["tag"],
535            attributes=data["attributes"],
536            children=[] if data["children"] is not None else None,
537        )
538        if data["children"] is not None:
539            element.children = [
540                Node.from_dict(child, in_pre or data["tag"] == "pre")
541                for child in data["children"]
542            ]
543        return element
544
545    @property
546    def tag_path(self) -> list[str]:
547        """Get the list of all the tags to the current element. Inclusive."""
548        path = [self.tag]
549        parent = self
550        while isinstance(parent.parent, Element):
551            path.append(parent.parent.tag)
552            parent = parent.parent
553
554        path.reverse()
555        return path
556
557    def __hash__(self) -> int:
558        return (
559            hash(self.tag)
560            + sum(hash(attr) for attr in self.attributes.values())
561            + hash(len(self))
562        )
563
564    def __contains__(self, _k: str) -> bool:
565        return _k in self.attributes
566
567    @overload
568    def __getitem__(self, _k: int) -> Parent | Literal:
569        ...
570
571    @overload
572    def __getitem__(self, _k: str) -> Attribute:
573        ...
574
575    @overload
576    def __getitem__(self, _k: slice) -> list[Parent | Literal]:
577        ...
578
579    def __getitem__(
580        self,
581        _k: str | int | slice,
582    ) -> Attribute | Parent | Literal | list[Parent | Literal]:
583        if isinstance(_k, str):
584            return self.attributes[_k]
585
586        if self.children is not None:
587            return self.children[_k]
588
589        raise ValueError("A self closing element can not have it's children indexed")
590
591    @overload
592    def __setitem__(self, key: int, value: Node) -> NoReturn:
593        ...
594
595    @overload
596    def __setitem__(self, key: slice, value: list) -> NoReturn:
597        ...
598
599    @overload
600    def __setitem__(self, key: str, value: Attribute) -> NoReturn:
601        ...
602
603    def __setitem__(self, key: str | int | slice, value: Attribute | Node | list):
604        if isinstance(key, str) and isinstance(value, Attribute):
605            self.attributes[key] = value
606        elif self.children is not None:
607            if isinstance(key, int) and isinstance(value, Node):
608                value.parent = self
609                self.children[key] = value
610            elif isinstance(key, slice) and isinstance(value, list):
611                for child in value:
612                    child.parent = self
613                self.children[key] = value
614        else:
615            raise ValueError(
616                "A self closing element can not have a subset of it's children assigned to",
617            )
618
619    @overload
620    def __delitem__(self, key: int) -> NoReturn:
621        ...
622
623    @overload
624    def __delitem__(self, key: slice) -> NoReturn:
625        ...
626
627    @overload
628    def __delitem__(self, key: str) -> NoReturn:
629        ...
630
631    def __delitem__(self, key: str | int | slice):
632        if isinstance(key, str):
633            del self.attributes[key]
634        elif self.children is not None:
635            del self.children[key]
636        else:
637            raise ValueError("Can not use del for a self closing elements children")
638
639    @overload
640    def pop(self, idx: int = 0) -> Node:
641        ...
642
643    @overload
644    def pop(self, idx: str, _default: Any = MISSING) -> Attribute:
645        ...
646
647    def pop(self, idx: str | int = 0, _default: Any = MISSING) -> Attribute | Node:
648        """Pop a specific attribute from the elements attributes. A default value
649        can be provided for when the value is not found, otherwise an error is thrown.
650        """
651        if isinstance(idx, str):
652            if _default != MISSING:
653                return self.attributes.pop(idx, _default)
654            return self.attributes.pop(idx)
655        if self.children is not None:
656            return self.children.pop(idx)
657
658        raise ValueError("A self closing element can not pop a child node")
659
660    def get(self, key: str, _default: Any = MISSING) -> Attribute:
661        """Get a specific element attribute. Returns `None` if not found
662        unless `_default` is defined.
663
664        Args:
665            key (str): The name of the attribute to retrieve.
666            _default (str|bool): The default value to return if the key
667                isn't an attribute.
668
669        Returns:
670            str|bool|None: str or bool if the attribute exists or a default
671                was provided, else None
672        """
673        if not isinstance(_default, (Attribute, NoneType)) and _default != MISSING:
674            raise TypeError("_default value must be str, bool, or MISSING")
675
676        if key in self:
677            return self[key]
678        if _default != MISSING:
679            return _default
680        raise ValueError(f"Attribute {key!r} not found")
681
682    def attrs_as_str(self, indent: int, color: bool = False) -> str:  # pragma: no cover
683        """Return a str representation of the attributes"""
684        if color:
685            attrs = (
686                (
687                    f"\n{' '*(indent)}▸ "
688                    + f"\n{' '*(indent)}▸ ".join(
689                        str(key)
690                        + ": "
691                        + (
692                            f"\x1b[32m{value!r}\x1b[39m"
693                            if isinstance(value, str)
694                            else f"\x1b[35m{value}\x1b[39m"
695                        )
696                        for key, value in self.attributes.items()
697                    )
698                )
699                if len(self.attributes) > 0
700                else ""
701            )
702        else:
703            attrs = (
704                (
705                    f"\n{' '*(indent)}▸ "
706                    + f"\n{' '*(indent)}▸ ".join(
707                        f"{key}: {value!r}" for key, value in self.attributes.items()
708                    )
709                )
710                if len(self.attributes) > 0
711                else ""
712            )
713
714        return attrs
715
716    def __repr__(self) -> str:
717        return f"{self.type}.{self.tag}(cldrn={self.len_as_str()}, attrs={self.attributes})"
718
719    def __format__(
720        self,
721        indent: int = 0,
722        color: bool = False,
723        text: bool = False,
724    ) -> list[str]:  # pragma: no cover
725        output: list[str] = []
726        if color:
727            output.append(
728                f"{' '*indent}"
729                + SAIML.parse(f"[@Fred]{self.type}[@F]" + f".[@Fblue]{self.tag}[@F]")
730                + f" [{self.len_as_str(True)}]"
731                + f" {self.pos_as_str(True)}"
732                + f"{self.attrs_as_str(indent+2, True)}",
733            )
734        else:
735            output.append(
736                f"{' '*indent}{self.type}.{self.tag}"
737                + f" [{self.len_as_str()}]{self.pos_as_str()}{self.attrs_as_str(indent+2)}",
738            )
739
740        for child in self.children or []:
741            output.extend(child.__format__(indent=indent + 2, color=color, text=text))
742        return output
743
744    def __str__(self) -> str:
745        return "\n".join(self.__format__())

Base phml node. Defines a type and basic interactions.

Element( tag: str, attributes: dict[str, str | bool] | None = None, children: list[phml.nodes.Node] | None = None, position: phml.nodes.Position | None = None, parent: phml.nodes.Parent | None = None, in_pre: bool = False)
490    def __init__(
491        self,
492        tag: str,
493        attributes: dict[str, Attribute] | None = None,
494        children: list[Node] | None = None,
495        position: Position | None = None,
496        parent: Parent | None = None,
497        in_pre: bool = False,
498    ) -> None:
499        super().__init__(NodeType.ELEMENT, children, position, parent, in_pre)
500        self.tag = tag
501        self.attributes = attributes or {}
502        self.context = {}
def as_dict(self) -> dict:
528    def as_dict(self) -> dict:
529        return {"tag": self.tag, "attributes": self.attributes, **super().as_dict()}
@staticmethod
def from_dict(data: dict, in_pre: bool = False) -> phml.nodes.Element:
531    @staticmethod
532    def from_dict(data: dict, in_pre: bool = False) -> Element:
533        element = Element(
534            data["tag"],
535            attributes=data["attributes"],
536            children=[] if data["children"] is not None else None,
537        )
538        if data["children"] is not None:
539            element.children = [
540                Node.from_dict(child, in_pre or data["tag"] == "pre")
541                for child in data["children"]
542            ]
543        return element
tag_path: list[str]

Get the list of all the tags to the current element. Inclusive.

def pop( self, idx: str | int = 0, _default: Any = <phml.nodes.Missing object>) -> str | bool | phml.nodes.Node:
647    def pop(self, idx: str | int = 0, _default: Any = MISSING) -> Attribute | Node:
648        """Pop a specific attribute from the elements attributes. A default value
649        can be provided for when the value is not found, otherwise an error is thrown.
650        """
651        if isinstance(idx, str):
652            if _default != MISSING:
653                return self.attributes.pop(idx, _default)
654            return self.attributes.pop(idx)
655        if self.children is not None:
656            return self.children.pop(idx)
657
658        raise ValueError("A self closing element can not pop a child node")

Pop a specific attribute from the elements attributes. A default value can be provided for when the value is not found, otherwise an error is thrown.

def get( self, key: str, _default: Any = <phml.nodes.Missing object>) -> str | bool:
660    def get(self, key: str, _default: Any = MISSING) -> Attribute:
661        """Get a specific element attribute. Returns `None` if not found
662        unless `_default` is defined.
663
664        Args:
665            key (str): The name of the attribute to retrieve.
666            _default (str|bool): The default value to return if the key
667                isn't an attribute.
668
669        Returns:
670            str|bool|None: str or bool if the attribute exists or a default
671                was provided, else None
672        """
673        if not isinstance(_default, (Attribute, NoneType)) and _default != MISSING:
674            raise TypeError("_default value must be str, bool, or MISSING")
675
676        if key in self:
677            return self[key]
678        if _default != MISSING:
679            return _default
680        raise ValueError(f"Attribute {key!r} not found")

Get a specific element attribute. Returns None if not found unless _default is defined.

Args
  • key (str): The name of the attribute to retrieve.
  • _default (str|bool): The default value to return if the key isn't an attribute.
Returns

str|bool|None: str or bool if the attribute exists or a default was provided, else None

def attrs_as_str(self, indent: int, color: bool = False) -> str:
682    def attrs_as_str(self, indent: int, color: bool = False) -> str:  # pragma: no cover
683        """Return a str representation of the attributes"""
684        if color:
685            attrs = (
686                (
687                    f"\n{' '*(indent)}▸ "
688                    + f"\n{' '*(indent)}▸ ".join(
689                        str(key)
690                        + ": "
691                        + (
692                            f"\x1b[32m{value!r}\x1b[39m"
693                            if isinstance(value, str)
694                            else f"\x1b[35m{value}\x1b[39m"
695                        )
696                        for key, value in self.attributes.items()
697                    )
698                )
699                if len(self.attributes) > 0
700                else ""
701            )
702        else:
703            attrs = (
704                (
705                    f"\n{' '*(indent)}▸ "
706                    + f"\n{' '*(indent)}▸ ".join(
707                        f"{key}: {value!r}" for key, value in self.attributes.items()
708                    )
709                )
710                if len(self.attributes) > 0
711                else ""
712            )
713
714        return attrs

Return a str representation of the attributes

class Literal(Node):
748class Literal(Node):
749    def __init__(
750        self,
751        name: str,
752        content: str,
753        parent: Parent | None = None,
754        position: Position | None = None,
755        in_pre: bool = False,
756    ) -> None:
757        super().__init__(NodeType.LITERAL, position, parent, in_pre)
758        self.name = name
759        self.content = content
760
761    def __hash__(self) -> int:
762        return hash(self.content) + hash(str(self.name))
763
764    def __p_code__(self) -> str:
765        in_pre = ", in_pre=True" if self.in_pre else ""
766        return f"Literal({str(self.name)!r}, {self.content!r}{in_pre})"
767
768    def __eq__(self, _o) -> bool:
769        return (
770            isinstance(_o, Literal)
771            and _o.type == self.type
772            and self.name == _o.name
773            and self.content == _o.content
774        )
775
776    def as_dict(self) -> dict:
777        return {"name": str(self.name), "content": self.content, **super().as_dict()}
778
779    @staticmethod
780    def is_text(node: Node) -> bool:
781        """Check if a node is a literal and a text node."""
782        return isinstance(node, Literal) and node.name == LiteralType.Text
783
784    @staticmethod
785    def is_comment(node: Node) -> bool:
786        """Check if a node is a literal and a comment."""
787        return isinstance(node, Literal) and node.name == LiteralType.Comment
788
789    def __repr__(self) -> str:  # pragma: no cover
790        return f"{self.type}.{self.name}(len={len(self.content)})"
791
792    def __format__(
793        self,
794        indent: int = 0,
795        color: bool = False,
796        text: bool = False,
797    ):  # pragma: no cover
798        from .helpers import normalize_indent
799
800        content = ""
801        if text:
802            offset = " " * (indent + 2)
803            content = (
804                f'{offset}"""\n{normalize_indent(self.content, indent+4)}\n{offset}"""'
805            )
806        if color:
807            return [
808                SAIML.parse(
809                    f"{' '*indent}[@Fred]{self.type}[@F].[@Fblue]{self.name}[@F]"
810                    + (f"\n[@Fgreen]{SAIML.escape(content)}[@F]" if text else ""),
811                ),
812            ]
813        return [
814            f"{' '*indent}{self.type}.{self.name}" + (f"\n{content}" if text else ""),
815        ]
816
817    def __str__(self) -> str:  # pragma: no cover
818        return self.__format__()[0]

Base phml node. Defines a type and basic interactions.

Literal( name: str, content: str, parent: phml.nodes.Parent | None = None, position: phml.nodes.Position | None = None, in_pre: bool = False)
749    def __init__(
750        self,
751        name: str,
752        content: str,
753        parent: Parent | None = None,
754        position: Position | None = None,
755        in_pre: bool = False,
756    ) -> None:
757        super().__init__(NodeType.LITERAL, position, parent, in_pre)
758        self.name = name
759        self.content = content
def as_dict(self) -> dict:
776    def as_dict(self) -> dict:
777        return {"name": str(self.name), "content": self.content, **super().as_dict()}
@staticmethod
def is_text(node: phml.nodes.Node) -> bool:
779    @staticmethod
780    def is_text(node: Node) -> bool:
781        """Check if a node is a literal and a text node."""
782        return isinstance(node, Literal) and node.name == LiteralType.Text

Check if a node is a literal and a text node.

@staticmethod
def is_comment(node: phml.nodes.Node) -> bool:
784    @staticmethod
785    def is_comment(node: Node) -> bool:
786        """Check if a node is a literal and a comment."""
787        return isinstance(node, Literal) and node.name == LiteralType.Comment

Check if a node is a literal and a comment.

Inherited Members
Node
from_dict
position
type
pos_as_str
def inspect(node: phml.nodes.Node, color: bool = False, text: bool = False) -> str:
821def inspect(
822    node: Node,
823    color: bool = False,
824    text: bool = False,
825) -> str:  # pragma: no cover
826    """Inspected a given node recursively.
827
828    Args:
829        node (Node): Any type of node to inspect.
830        color (bool): Whether to return a string with ansi encoding. Default False.
831        text (bool): Whether to include the text from comment and text nodes. Default False.
832
833    Return:
834        A formatted multiline string representation of the node and it's children.
835    """
836    if isinstance(node, Node):
837        return "\n".join(node.__format__(color=color, text=text))
838    raise TypeError(f"Can only inspect phml Nodes was, {node!r}")

Inspected a given node recursively.

Args
  • node (Node): Any type of node to inspect.
  • color (bool): Whether to return a string with ansi encoding. Default False.
  • text (bool): Whether to include the text from comment and text nodes. Default False.
Return

A formatted multiline string representation of the node and it's children.