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}")
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.
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
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
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
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.
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
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.
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.
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.
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.
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.
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})"
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 )
The position of the node in the parsed phml text.
Is None
if the node was generated.
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.
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.
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)
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
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.
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.
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.
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.
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.
Inherited Members
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.
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.
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 = {}
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
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.
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
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
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.
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.
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
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.