Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/mistune/renderers/html.py: 80%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

143 statements  

1import re 

2from typing import Any, ClassVar, Dict, Iterable, Literal, Mapping, Optional, Tuple, Union 

3from urllib.parse import unquote 

4 

5from ..core import BaseRenderer, BlockState 

6from ..util import escape as escape_text 

7from ..util import safe_entity, striptags 

8 

9 

10class HTMLRenderer(BaseRenderer): 

11 """A renderer for converting Markdown to HTML.""" 

12 

13 _escape: bool 

14 _allow_harmful_protocols: Optional[Union[bool, Iterable[str]]] 

15 NAME: ClassVar[Literal["html"]] = "html" 

16 SAFE_PROTOCOLS: ClassVar[Tuple[str, ...]] = ( 

17 "http:", 

18 "https:", 

19 "mailto:", 

20 "tel:", 

21 "ftp:", 

22 "ftps:", 

23 "irc:", 

24 "ircs:", 

25 ) 

26 GOOD_DATA_PROTOCOLS: ClassVar[Tuple[str, ...]] = ( 

27 "data:image/gif;", 

28 "data:image/png;", 

29 "data:image/jpeg;", 

30 "data:image/webp;", 

31 ) 

32 _attribute_name_re = re.compile(r"^[A-Za-z_:][A-Za-z0-9_.:-]*$") 

33 

34 def __init__( 

35 self, 

36 escape: bool = True, 

37 allow_harmful_protocols: Optional[Union[bool, Iterable[str]]] = None, 

38 ) -> None: 

39 super(HTMLRenderer, self).__init__() 

40 self._allow_harmful_protocols = allow_harmful_protocols 

41 self._escape = escape 

42 

43 @property 

44 def escape_enabled(self) -> bool: 

45 """Whether raw HTML nodes are escaped.""" 

46 return self._escape 

47 

48 @escape_enabled.setter 

49 def escape_enabled(self, enabled: bool) -> None: 

50 self._escape = enabled 

51 

52 def escape_raw(self, value: str) -> str: 

53 """Escape raw HTML when raw HTML output is disabled.""" 

54 if self.escape_enabled: 

55 return escape_text(value) 

56 return value 

57 

58 def escape_attribute(self, value: object) -> str: 

59 """Escape a value for use in a generated HTML attribute.""" 

60 return safe_entity(str(value)) 

61 

62 def render_attrs(self, attrs: Mapping[str, object]) -> str: 

63 """Render generated HTML attributes with escaped values.""" 

64 rendered = [] 

65 for name, value in attrs.items(): 

66 if not self._attribute_name_re.fullmatch(name): 

67 raise ValueError("invalid HTML attribute name: " + name) 

68 if value is None or value is False: 

69 continue 

70 if value is True: 

71 rendered.append(" " + name) 

72 else: 

73 rendered.append(' ' + name + '="' + self.escape_attribute(value) + '"') 

74 return "".join(rendered) 

75 

76 def sanitize_url(self, url: str) -> str: 

77 """Return a URL allowed by this renderer's protocol policy.""" 

78 allow_harmful_protocols = self._allow_harmful_protocols 

79 if allow_harmful_protocols is True: 

80 return url 

81 

82 normalized_url = _unquote_url(url).lower().lstrip() 

83 if allow_harmful_protocols and normalized_url.startswith(tuple(allow_harmful_protocols)): 

84 return url 

85 

86 if _is_safe_url(normalized_url, self.SAFE_PROTOCOLS, self.GOOD_DATA_PROTOCOLS): 

87 return url 

88 return "#harmful-link" 

89 

90 def render_token(self, token: Dict[str, Any], state: BlockState) -> str: 

91 # backward compitable with v2 

92 func = self._get_method(token["type"]) 

93 attrs = token.get("attrs") 

94 

95 if "raw" in token: 

96 text = token["raw"] 

97 elif "children" in token: 

98 text = self.render_tokens(token["children"], state) 

99 else: 

100 if attrs: 

101 return func(**attrs) 

102 else: 

103 return func() 

104 if attrs: 

105 return func(text, **attrs) 

106 else: 

107 return func(text) 

108 

109 def safe_url(self, url: str) -> str: 

110 """Ensure the given URL is safe. This method is used for rendering 

111 links, images, and etc. 

112 """ 

113 return self.escape_attribute(self.sanitize_url(url)) 

114 

115 def text(self, text: str) -> str: 

116 if self.escape_enabled: 

117 return escape_text(text) 

118 return safe_entity(text) 

119 

120 def emphasis(self, text: str) -> str: 

121 return "<em>" + text + "</em>" 

122 

123 def strong(self, text: str) -> str: 

124 return "<strong>" + text + "</strong>" 

125 

126 def link(self, text: str, url: str, title: Optional[str] = None) -> str: 

127 attrs: Dict[str, object] = {"href": self.sanitize_url(url)} 

128 if title: 

129 attrs["title"] = title 

130 return "<a" + self.render_attrs(attrs) + ">" + text + "</a>" 

131 

132 def image(self, text: str, url: str, title: Optional[str] = None) -> str: 

133 alt = striptags(text) 

134 attrs: Dict[str, object] = {"src": self.sanitize_url(url), "alt": alt} 

135 if title: 

136 attrs["title"] = title 

137 return "<img" + self.render_attrs(attrs) + " />" 

138 

139 def codespan(self, text: str) -> str: 

140 return "<code>" + escape_text(text) + "</code>" 

141 

142 def linebreak(self) -> str: 

143 return "<br />\n" 

144 

145 def softbreak(self) -> str: 

146 return "\n" 

147 

148 def inline_html(self, html: str) -> str: 

149 return self.escape_raw(html) 

150 

151 def paragraph(self, text: str) -> str: 

152 return "<p>" + text + "</p>\n" 

153 

154 def heading(self, text: str, level: int, **attrs: Any) -> str: 

155 tag = "h" + str(level) 

156 _id = attrs.get("id") 

157 rendered_attrs = self.render_attrs({"id": _id}) if _id else "" 

158 return "<" + tag + rendered_attrs + ">" + text + "</" + tag + ">\n" 

159 

160 def blank_line(self) -> str: 

161 return "" 

162 

163 def thematic_break(self) -> str: 

164 return "<hr />\n" 

165 

166 def block_text(self, text: str) -> str: 

167 return text 

168 

169 def block_code(self, code: str, info: Optional[str] = None) -> str: 

170 attrs: Dict[str, object] = {} 

171 if info is not None: 

172 info = safe_entity(info.strip()) 

173 if info: 

174 lang = info.split(None, 1)[0] 

175 attrs["class"] = "language-" + lang 

176 return "<pre><code" + self.render_attrs(attrs) + ">" + escape_text(code) + "</code></pre>\n" 

177 

178 def block_quote(self, text: str) -> str: 

179 return "<blockquote>\n" + text + "</blockquote>\n" 

180 

181 def block_html(self, html: str) -> str: 

182 if self.escape_enabled: 

183 return "<p>" + self.escape_raw(html.strip()) + "</p>\n" 

184 return html + "\n" 

185 

186 def block_error(self, text: str) -> str: 

187 return '<div class="error"><pre>' + escape_text(text) + "</pre></div>\n" 

188 

189 def list(self, text: str, ordered: bool, **attrs: Any) -> str: 

190 if ordered: 

191 start = attrs.get("start") 

192 return "<ol" + self.render_attrs({"start": start}) + ">\n" + text + "</ol>\n" 

193 return "<ul>\n" + text + "</ul>\n" 

194 

195 def list_item(self, text: str) -> str: 

196 return "<li>" + text + "</li>\n" 

197 

198 

199def _unquote_url(url: str) -> str: 

200 for _ in range(3): 

201 decoded = unquote(url) 

202 if decoded == url: 

203 break 

204 url = decoded 

205 return url 

206 

207 

208def _is_safe_url(url: str, safe_protocols: Tuple[str, ...], good_data_protocols: Tuple[str, ...]) -> bool: 

209 if url.startswith(safe_protocols): 

210 return True 

211 if url.startswith(good_data_protocols): 

212 return True 

213 if url.startswith(("/", "#", "?")): 

214 return True 

215 return ":" not in url.split("/", 1)[0]