11from html import escape
22from types import GeneratorType
3- from typing import Tuple , Union , Dict , List , Generator , Optional , Iterable
3+ from typing import Tuple , Union , Dict , List , FrozenSet , Generator , Iterable
44
55
66class SafeString :
7- __slots__ = (' safe_str' ,)
7+ __slots__ = (" safe_str" ,)
88
99 def __init__ (self , safe_str : str ) -> None :
1010 self .safe_str = safe_str
1111
12+ def __hash__ (self ) -> int :
13+ return hash (f"SafeString__{ self .safe_str } " )
14+
1215
1316Node = Union [
1417 str ,
@@ -21,6 +24,69 @@ def __init__(self, safe_str: str) -> None:
2124
2225TagTuple = Tuple [str , Tuple [Node , ...], str ]
2326
27+ _common_safe_attribute_names : FrozenSet [str ] = frozenset (
28+ {
29+ "alt" ,
30+ "autoplay" ,
31+ "autoplay" ,
32+ "charset" ,
33+ "checked" ,
34+ "class" ,
35+ "colspan" ,
36+ "content" ,
37+ "contenteditable" ,
38+ "dir" ,
39+ "draggable" ,
40+ "enctype" ,
41+ "for" ,
42+ "height" ,
43+ "hidden" ,
44+ "href" ,
45+ "hreflang" ,
46+ "http-equiv" ,
47+ "id" ,
48+ "itemprop" ,
49+ "itemscope" ,
50+ "itemtype" ,
51+ "lang" ,
52+ "loadable" ,
53+ "method" ,
54+ "name" ,
55+ "onblur" ,
56+ "onclick" ,
57+ "onfocus" ,
58+ "onkeydown" ,
59+ "onkeyup" ,
60+ "onload" ,
61+ "onselect" ,
62+ "onsubmit" ,
63+ "placeholder" ,
64+ "poster" ,
65+ "property" ,
66+ "rel" ,
67+ "rowspan" ,
68+ "sizes" ,
69+ "spellcheck" ,
70+ "src" ,
71+ "style" ,
72+ "target" ,
73+ "title" ,
74+ "type" ,
75+ "value" ,
76+ "width" ,
77+ }
78+ )
79+
80+
81+ def escape_attribute_key (k : str ) -> str :
82+ return (
83+ escape (k )
84+ .replace ("=" , "=" )
85+ .replace ("\\ " , "\" )
86+ .replace ("`" , "`" )
87+ .replace (" " , " " )
88+ )
89+
2490
2591class Tag :
2692 __slots__ = ("tag_start" , "rendered" , "closing_tag" , "no_children_close" )
@@ -36,13 +102,30 @@ def __init__(self, name: str, self_closing: bool = False) -> None:
36102 self .rendered = f"{ self .tag_start } { self .no_children_close } "
37103
38104 def __call__ (
39- self , attributes : Dict [str , Optional [str ]], * children : Node
105+ self ,
106+ attributes : Dict [Union [SafeString , str ], Union [str , SafeString , None ]],
107+ * children : Node ,
40108 ) -> TagTuple :
41109 if attributes :
42110 # in this case this is faster than attrs = "".join([...])
43111 attrs = ""
44112 for key , val in attributes .items ():
45- attrs += f" { key } " if val is None else f' { key } ="{ val } "'
113+ # optimization: a large portion of attribute keys should be
114+ # covered by this check. It allows us to skip escaping
115+ # where it is not needed. Note this is for attribute names only;
116+ # attributes values are always escaped (when they are `str`s)
117+ if key not in _common_safe_attribute_names :
118+ key = (
119+ key .safe_str
120+ if isinstance (key , SafeString )
121+ else escape_attribute_key (key )
122+ )
123+ if isinstance (val , str ):
124+ attrs += f' { key } ="{ escape (val )} "'
125+ elif isinstance (val , SafeString ):
126+ attrs += f' { key } ="{ val .safe_str } "'
127+ elif val is None :
128+ attrs += f" { key } "
46129
47130 if children :
48131 return f"{ self .tag_start } { attrs } >" , children , self .closing_tag
0 commit comments