1- import typing as t
21from dataclasses import dataclass , field
3- from functools import cached_property
4- from html import escape
2+
3+ from markupsafe import escape
54
65# See https://developer.mozilla.org/en-US/docs/Glossary/Void_element
76VOID_ELEMENTS = frozenset (
2827RCDATA_CONTENT_ELEMENTS = frozenset (["textarea" , "title" ])
2928CONTENT_ELEMENTS = CDATA_CONTENT_ELEMENTS | RCDATA_CONTENT_ELEMENTS
3029
31- # TODO: add a pretty-printer for nodes for debugging
32- # TODO: consider how significant whitespace is handled from t-string to nodes
33-
34-
35- @t .runtime_checkable
36- class HasHTMLDunder (t .Protocol ):
37- def __html__ (self ) -> str : ...
38-
39-
40- type HTMLDunder = t .Callable [[], str ]
30+ # FUTURE: add a pretty-printer to nodes for debugging
31+ # FUTURE: make nodes frozen (and have the parser work with mutable builders)
4132
4233
4334@dataclass (slots = True )
@@ -50,24 +41,11 @@ def __html__(self) -> str:
5041
5142@dataclass (slots = False )
5243class Text (Node ):
53- # Django's `SafeString` and Markupsafe/Jinja2's `Markup` both inherit
54- # from `str`, but that is not a requirement for the `__html__` dunder.
55- text : str | HasHTMLDunder
56-
57- @cached_property
58- def _cached_str (self ) -> str :
59- if isinstance (self .text , HasHTMLDunder ):
60- return self .text .__html__ ()
61- return escape (t .cast (str , self .text ), quote = False )
62-
63- def _as_unescaped (self ) -> str :
64- """Return the text as-is, without escaping. For internal use only."""
65- if isinstance (self .text , HasHTMLDunder ):
66- return self .text .__html__ ()
67- return self .text
44+ text : str
6845
6946 def __str__ (self ) -> str :
70- return self ._cached_str
47+ # Use markupsafe's escape to handle HTML escaping
48+ return escape (self .text )
7149
7250
7351@dataclass (slots = True )
@@ -113,20 +91,26 @@ def __post_init__(self):
11391 def is_void (self ) -> bool :
11492 return self .tag in VOID_ELEMENTS
11593
94+ @property
95+ def is_content (self ) -> bool :
96+ return self .tag in CONTENT_ELEMENTS
97+
11698 def __str__ (self ) -> str :
117- # TODO: CONSIDER: should values in attrs support the __html__ dunder?
99+ # We use markupsafe's escape to handle HTML escaping of attribute values
100+ # which means it's possible to mark them as safe if needed.
118101 attrs_str = "" .join (
119- f" { key } " if value is None else f' { key } ="{ escape (value , quote = True )} "'
102+ f" { key } " if value is None else f' { key } ="{ escape (value )} "'
120103 for key , value in self .attrs .items ()
121104 )
122105 if self .is_void :
123106 return f"<{ self .tag } { attrs_str } />"
124107 if not self .children :
125108 return f"<{ self .tag } { attrs_str } ></{ self .tag } >"
126- if self .tag in CONTENT_ELEMENTS :
127- # Content elements should not escape their content
109+ if self .is_content :
110+ # Content elements should *not* escape their content when
111+ # rendering to HTML. Sheesh, HTML is weird.
128112 children_str = "" .join (
129- child ._as_unescaped () if isinstance (child , Text ) else str (child )
113+ child .text if isinstance (child , Text ) else str (child )
130114 for child in self .children
131115 )
132116 else :
0 commit comments