Coverage for phml\nodes.py: 99%

163 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-04-12 14:26 -0500

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( 

520 _o.attributes[key] == value 

521 for key, value in self.attributes.items() 

522 ) 

523 ) 

524 and ( 

525 (_o.children is None and self.children is None) 

526 or (len(_o) == len(self) and all(c1 == c2 for c1, c2 in zip(_o, self))) 

527 ) 

528 ) 

529 

530 def as_dict(self) -> dict: 

531 return {"tag": self.tag, "attributes": self.attributes, **super().as_dict()} 

532 

533 @staticmethod 

534 def from_dict(data: dict, in_pre: bool = False) -> Element: 

535 element = Element( 

536 data["tag"], 

537 attributes=data["attributes"], 

538 children=[] if data["children"] is not None else None, 

539 ) 

540 if data["children"] is not None: 

541 element.children = [ 

542 Node.from_dict(child, in_pre or data["tag"] == "pre") 

543 for child in data["children"] 

544 ] 

545 return element 

546 

547 @property 

548 def tag_path(self) -> list[str]: 

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

550 path = [self.tag] 

551 parent = self 

552 while isinstance(parent.parent, Element): 

553 path.append(parent.parent.tag) 

554 parent = parent.parent 

555 

556 path.reverse() 

557 return path 

558 

559 def __hash__(self) -> int: 

560 return ( 

561 hash(self.tag) 

562 + sum(hash(attr) for attr in self.attributes.values()) 

563 + hash(len(self)) 

564 ) 

565 

566 def __contains__(self, _k: str) -> bool: 

567 return _k in self.attributes 

568 

569 @overload 

570 def __getitem__(self, _k: int) -> Parent | Literal: 

571 ... 

572 

573 @overload 

574 def __getitem__(self, _k: str) -> Attribute: 

575 ... 

576 

577 @overload 

578 def __getitem__(self, _k: slice) -> list[Parent | Literal]: 

579 ... 

580 

581 def __getitem__( 

582 self, 

583 _k: str | int | slice, 

584 ) -> Attribute | Parent | Literal | list[Parent | Literal]: 

585 if isinstance(_k, str): 

586 return self.attributes[_k] 

587 

588 if self.children is not None: 

589 return self.children[_k] 

590 

591 raise ValueError("A self closing element can not have it's children indexed") 

592 

593 @overload 

594 def __setitem__(self, key: int, value: Node) -> NoReturn: 

595 ... 

596 

597 @overload 

598 def __setitem__(self, key: slice, value: list) -> NoReturn: 

599 ... 

600 

601 @overload 

602 def __setitem__(self, key: str, value: Attribute) -> NoReturn: 

603 ... 

604 

605 def __setitem__(self, key: str | int | slice, value: Attribute | Node | list): 

606 if isinstance(key, str): 

607 if not isinstance(value, Attribute): 

608 raise TypeError(f"Expected bool or str to be assigned to attribute {key!r} but was; {value!r}") 

609 self.attributes[key] = value 

610 else: 

611 if self.children is None: 

612 raise ValueError( 

613 "A self closing element can not have a subset of it's children assigned to", 

614 ) 

615 if isinstance(key, int) and isinstance(value, Node): 

616 value.parent = self 

617 self.children[key] = value 

618 elif isinstance(key, slice) and isinstance(value, list): 

619 for child in value: 

620 child.parent = self 

621 self.children[key] = value 

622 

623 @overload 

624 def __delitem__(self, key: int) -> NoReturn: 

625 ... 

626 

627 @overload 

628 def __delitem__(self, key: slice) -> NoReturn: 

629 ... 

630 

631 @overload 

632 def __delitem__(self, key: str) -> NoReturn: 

633 ... 

634 

635 def __delitem__(self, key: str | int | slice): 

636 if isinstance(key, str): 

637 del self.attributes[key] 

638 elif self.children is not None: 

639 del self.children[key] 

640 else: 

641 raise ValueError("Can not use del for a self closing elements children") 

642 

643 @overload 

644 def pop(self, idx: int = 0) -> Node: 

645 ... 

646 

647 @overload 

648 def pop(self, idx: str, _default: Any = MISSING) -> Attribute: 

649 ... 

650 

651 def pop(self, idx: str | int = 0, _default: Any = MISSING) -> Attribute | Node: 

652 """Pop a specific attribute from the elements attributes. A default value 

653 can be provided for when the value is not found, otherwise an error is thrown. 

654 """ 

655 if isinstance(idx, str): 

656 if _default != MISSING: 

657 return self.attributes.pop(idx, _default) 

658 return self.attributes.pop(idx) 

659 if self.children is not None: 

660 return self.children.pop(idx) 

661 

662 raise ValueError("A self closing element can not pop a child node") 

663 

664 def get(self, key: str, _default: Any = MISSING) -> Attribute: 

665 """Get a specific element attribute. Returns `None` if not found 

666 unless `_default` is defined. 

667 

668 Args: 

669 key (str): The name of the attribute to retrieve. 

670 _default (str|bool): The default value to return if the key 

671 isn't an attribute. 

672 

673 Returns: 

674 str|bool|None: str or bool if the attribute exists or a default 

675 was provided, else None 

676 """ 

677 if not isinstance(_default, (Attribute, NoneType)) and _default != MISSING: 

678 raise TypeError("_default value must be str, bool, or MISSING") 

679 

680 if key in self: 

681 return self[key] 

682 if _default != MISSING: 

683 return _default 

684 raise ValueError(f"Attribute {key!r} not found") 

685 

686 def attrs_as_str(self, indent: int, color: bool = False) -> str: # pragma: no cover 

687 """Return a str representation of the attributes""" 

688 if color: 

689 attrs = ( 

690 ( 

691 f"\n{' '*(indent)}▸ " 

692 + f"\n{' '*(indent)}▸ ".join( 

693 str(key) 

694 + ": " 

695 + ( 

696 f"\x1b[32m{value!r}\x1b[39m" 

697 if isinstance(value, str) 

698 else f"\x1b[35m{value}\x1b[39m" 

699 ) 

700 for key, value in self.attributes.items() 

701 ) 

702 ) 

703 if len(self.attributes) > 0 

704 else "" 

705 ) 

706 else: 

707 attrs = ( 

708 ( 

709 f"\n{' '*(indent)}▸ " 

710 + f"\n{' '*(indent)}▸ ".join( 

711 f"{key}: {value!r}" for key, value in self.attributes.items() 

712 ) 

713 ) 

714 if len(self.attributes) > 0 

715 else "" 

716 ) 

717 

718 return attrs 

719 

720 def __repr__(self) -> str: 

721 return f"{self.type}.{self.tag}(cldrn={self.len_as_str()}, attrs={self.attributes})" 

722 

723 def __format__( 

724 self, 

725 indent: int = 0, 

726 color: bool = False, 

727 text: bool = False, 

728 ) -> list[str]: # pragma: no cover 

729 output: list[str] = [] 

730 if color: 

731 output.append( 

732 f"{' '*indent}" 

733 + SAIML.parse(f"[@Fred]{self.type}[@F]" + f".[@Fblue]{self.tag}[@F]") 

734 + f" [{self.len_as_str(True)}]" 

735 + f" {self.pos_as_str(True)}" 

736 + f"{self.attrs_as_str(indent+2, True)}", 

737 ) 

738 else: 

739 output.append( 

740 f"{' '*indent}{self.type}.{self.tag}" 

741 + f" [{self.len_as_str()}]{self.pos_as_str()}{self.attrs_as_str(indent+2)}", 

742 ) 

743 

744 for child in self.children or []: 

745 output.extend(child.__format__(indent=indent + 2, color=color, text=text)) 

746 return output 

747 

748 def __str__(self) -> str: 

749 return "\n".join(self.__format__()) 

750 

751 

752class Literal(Node): 

753 def __init__( 

754 self, 

755 name: str, 

756 content: str, 

757 parent: Parent | None = None, 

758 position: Position | None = None, 

759 in_pre: bool = False, 

760 ) -> None: 

761 super().__init__(NodeType.LITERAL, position, parent, in_pre) 

762 self.name = name 

763 self.content = content 

764 

765 def __hash__(self) -> int: 

766 return hash(self.content) + hash(str(self.name)) 

767 

768 def __p_code__(self) -> str: 

769 in_pre = ", in_pre=True" if self.in_pre else "" 

770 return f"Literal({str(self.name)!r}, {self.content!r}{in_pre})" 

771 

772 def __eq__(self, _o) -> bool: 

773 return ( 

774 isinstance(_o, Literal) 

775 and _o.type == self.type 

776 and self.name == _o.name 

777 and self.content == _o.content 

778 ) 

779 

780 def as_dict(self) -> dict: 

781 return {"name": str(self.name), "content": self.content, **super().as_dict()} 

782 

783 @staticmethod 

784 def is_text(node: Node) -> bool: 

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

786 return isinstance(node, Literal) and node.name == LiteralType.Text 

787 

788 @staticmethod 

789 def is_comment(node: Node) -> bool: 

790 """Check if a node is a literal and a comment.""" 

791 return isinstance(node, Literal) and node.name == LiteralType.Comment 

792 

793 def __repr__(self) -> str: # pragma: no cover 

794 return f"{self.type}.{self.name}(len={len(self.content)})" 

795 

796 def __format__( 

797 self, 

798 indent: int = 0, 

799 color: bool = False, 

800 text: bool = False, 

801 ): # pragma: no cover 

802 from .helpers import normalize_indent 

803 

804 content = "" 

805 if text: 

806 offset = " " * (indent + 2) 

807 content = ( 

808 f'{offset}"""\n{normalize_indent(self.content, indent+4)}\n{offset}"""' 

809 ) 

810 if color: 

811 return [ 

812 SAIML.parse( 

813 f"{' '*indent}[@Fred]{self.type}[@F].[@Fblue]{self.name}[@F]" 

814 + (f"\n[@Fgreen]{SAIML.escape(content)}[@F]" if text else ""), 

815 ), 

816 ] 

817 return [ 

818 f"{' '*indent}{self.type}.{self.name}" + (f"\n{content}" if text else ""), 

819 ] 

820 

821 def __str__(self) -> str: # pragma: no cover 

822 return self.__format__()[0] 

823 

824 

825def inspect( 

826 node: Node, 

827 color: bool = False, 

828 text: bool = False, 

829) -> str: # pragma: no cover 

830 """Inspected a given node recursively. 

831 

832 Args: 

833 node (Node): Any type of node to inspect. 

834 color (bool): Whether to return a string with ansi encoding. Default False. 

835 text (bool): Whether to include the text from comment and text nodes. Default False. 

836 

837 Return: 

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

839 """ 

840 if isinstance(node, Node): 

841 return "\n".join(node.__format__(color=color, text=text)) 

842 raise TypeError(f"Can only inspect phml Nodes was, {node!r}")