Coverage for tdom/nodes.py: 100%

53 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-31 17:14 +0000

1from dataclasses import dataclass, field 

2 

3from markupsafe import escape 

4 

5# See https://developer.mozilla.org/en-US/docs/Glossary/Void_element 

6VOID_ELEMENTS = frozenset( 

7 [ 

8 "area", 

9 "base", 

10 "br", 

11 "col", 

12 "embed", 

13 "hr", 

14 "img", 

15 "input", 

16 "link", 

17 "meta", 

18 "param", 

19 "source", 

20 "track", 

21 "wbr", 

22 ] 

23) 

24 

25 

26CDATA_CONTENT_ELEMENTS = frozenset(["script", "style"]) 

27RCDATA_CONTENT_ELEMENTS = frozenset(["textarea", "title"]) 

28CONTENT_ELEMENTS = CDATA_CONTENT_ELEMENTS | RCDATA_CONTENT_ELEMENTS 

29 

30# FUTURE: add a pretty-printer to nodes for debugging 

31# FUTURE: make nodes frozen (and have the parser work with mutable builders) 

32 

33 

34@dataclass(slots=True) 

35class Node: 

36 def __html__(self) -> str: 

37 """Return the HTML representation of the node.""" 

38 # By default, just return the string representation 

39 return str(self) 

40 

41 

42@dataclass(slots=True) 

43class Text(Node): 

44 text: str # which may be markupsafe.Markup in practice. 

45 

46 def __str__(self) -> str: 

47 # Use markupsafe's escape to handle HTML escaping 

48 return escape(self.text) 

49 

50 def __eq__(self, other: object) -> bool: 

51 # This is primarily of use for testing purposes. We only consider 

52 # two Text nodes equal if their string representations match. 

53 return isinstance(other, Text) and str(self) == str(other) 

54 

55 

56@dataclass(slots=True) 

57class Fragment(Node): 

58 children: list[Node] = field(default_factory=list) 

59 

60 def __str__(self) -> str: 

61 return "".join(str(child) for child in self.children) 

62 

63 

64@dataclass(slots=True) 

65class Comment(Node): 

66 text: str 

67 

68 def __str__(self) -> str: 

69 return f"<!--{self.text}-->" 

70 

71 

72@dataclass(slots=True) 

73class DocumentType(Node): 

74 text: str = "html" 

75 

76 def __str__(self) -> str: 

77 return f"<!DOCTYPE {self.text}>" 

78 

79 

80@dataclass(slots=True) 

81class Element(Node): 

82 tag: str 

83 attrs: dict[str, str | None] = field(default_factory=dict) 

84 children: list[Node] = field(default_factory=list) 

85 

86 def __post_init__(self): 

87 """Ensure all preconditions are met.""" 

88 if not self.tag: 

89 raise ValueError("Element tag cannot be empty.") 

90 

91 # Void elements cannot have children 

92 if self.is_void and self.children: 

93 raise ValueError(f"Void element <{self.tag}> cannot have children.") 

94 

95 @property 

96 def is_void(self) -> bool: 

97 return self.tag in VOID_ELEMENTS 

98 

99 @property 

100 def is_content(self) -> bool: 

101 return self.tag in CONTENT_ELEMENTS 

102 

103 def __str__(self) -> str: 

104 # We use markupsafe's escape to handle HTML escaping of attribute values 

105 # which means it's possible to mark them as safe if needed. 

106 attrs_str = "".join( 

107 f" {key}" if value is None else f' {key}="{escape(value)}"' 

108 for key, value in self.attrs.items() 

109 ) 

110 if self.is_void: 

111 return f"<{self.tag}{attrs_str} />" 

112 if not self.children: 

113 return f"<{self.tag}{attrs_str}></{self.tag}>" 

114 children_str = "".join(str(child) for child in self.children) 

115 return f"<{self.tag}{attrs_str}>{children_str}</{self.tag}>"