1from typing import Any, ClassVar, Dict, Iterable, Optional, Tuple, Union, Literal
2from urllib.parse import unquote
3from ..core import BaseRenderer, BlockState
4from ..util import escape as escape_text
5from ..util import safe_entity, striptags
6
7
8class HTMLRenderer(BaseRenderer):
9 """A renderer for converting Markdown to HTML."""
10
11 _escape: bool
12 _allow_harmful_protocols: Optional[Union[bool, Iterable[str]]]
13 NAME: ClassVar[Literal["html"]] = "html"
14 SAFE_PROTOCOLS: ClassVar[Tuple[str, ...]] = (
15 "http:",
16 "https:",
17 "mailto:",
18 "tel:",
19 "ftp:",
20 "ftps:",
21 "irc:",
22 "ircs:",
23 )
24 GOOD_DATA_PROTOCOLS: ClassVar[Tuple[str, ...]] = (
25 "data:image/gif;",
26 "data:image/png;",
27 "data:image/jpeg;",
28 "data:image/webp;",
29 )
30
31 def __init__(
32 self,
33 escape: bool = True,
34 allow_harmful_protocols: Optional[Union[bool, Iterable[str]]] = None,
35 ) -> None:
36 super(HTMLRenderer, self).__init__()
37 self._allow_harmful_protocols = allow_harmful_protocols
38 self._escape = escape
39
40 def render_token(self, token: Dict[str, Any], state: BlockState) -> str:
41 # backward compitable with v2
42 func = self._get_method(token["type"])
43 attrs = token.get("attrs")
44
45 if "raw" in token:
46 text = token["raw"]
47 elif "children" in token:
48 text = self.render_tokens(token["children"], state)
49 else:
50 if attrs:
51 return func(**attrs)
52 else:
53 return func()
54 if attrs:
55 return func(text, **attrs)
56 else:
57 return func(text)
58
59 def safe_url(self, url: str) -> str:
60 """Ensure the given URL is safe. This method is used for rendering
61 links, images, and etc.
62 """
63 allow_harmful_protocols = self._allow_harmful_protocols
64 if allow_harmful_protocols is True:
65 return escape_text(url)
66
67 _url = _unquote_url(url).lower().lstrip()
68 if allow_harmful_protocols and _url.startswith(tuple(allow_harmful_protocols)):
69 return escape_text(url)
70
71 if _is_safe_url(_url, self.SAFE_PROTOCOLS, self.GOOD_DATA_PROTOCOLS):
72 return escape_text(url)
73 return "#harmful-link"
74
75 def text(self, text: str) -> str:
76 if self._escape:
77 return escape_text(text)
78 return safe_entity(text)
79
80 def emphasis(self, text: str) -> str:
81 return "<em>" + text + "</em>"
82
83 def strong(self, text: str) -> str:
84 return "<strong>" + text + "</strong>"
85
86 def link(self, text: str, url: str, title: Optional[str] = None) -> str:
87 s = '<a href="' + self.safe_url(url) + '"'
88 if title:
89 s += ' title="' + safe_entity(title) + '"'
90 return s + ">" + text + "</a>"
91
92 def image(self, text: str, url: str, title: Optional[str] = None) -> str:
93 src = self.safe_url(url)
94 alt = striptags(text)
95 s = '<img src="' + src + '" alt="' + alt + '"'
96 if title:
97 s += ' title="' + safe_entity(title) + '"'
98 return s + " />"
99
100 def codespan(self, text: str) -> str:
101 return "<code>" + escape_text(text) + "</code>"
102
103 def linebreak(self) -> str:
104 return "<br />\n"
105
106 def softbreak(self) -> str:
107 return "\n"
108
109 def inline_html(self, html: str) -> str:
110 if self._escape:
111 return escape_text(html)
112 return html
113
114 def paragraph(self, text: str) -> str:
115 return "<p>" + text + "</p>\n"
116
117 def heading(self, text: str, level: int, **attrs: Any) -> str:
118 tag = "h" + str(level)
119 html = "<" + tag
120 _id = attrs.get("id")
121 if _id:
122 html += ' id="' + escape_text(_id) + '"'
123 return html + ">" + text + "</" + tag + ">\n"
124
125 def blank_line(self) -> str:
126 return ""
127
128 def thematic_break(self) -> str:
129 return "<hr />\n"
130
131 def block_text(self, text: str) -> str:
132 return text
133
134 def block_code(self, code: str, info: Optional[str] = None) -> str:
135 html = "<pre><code"
136 if info is not None:
137 info = safe_entity(info.strip())
138 if info:
139 lang = info.split(None, 1)[0]
140 html += ' class="language-' + lang + '"'
141 return html + ">" + escape_text(code) + "</code></pre>\n"
142
143 def block_quote(self, text: str) -> str:
144 return "<blockquote>\n" + text + "</blockquote>\n"
145
146 def block_html(self, html: str) -> str:
147 if self._escape:
148 return "<p>" + escape_text(html.strip()) + "</p>\n"
149 return html + "\n"
150
151 def block_error(self, text: str) -> str:
152 return '<div class="error"><pre>' + escape_text(text) + "</pre></div>\n"
153
154 def list(self, text: str, ordered: bool, **attrs: Any) -> str:
155 if ordered:
156 html = "<ol"
157 start = attrs.get("start")
158 if start is not None:
159 html += ' start="' + str(start) + '"'
160 return html + ">\n" + text + "</ol>\n"
161 return "<ul>\n" + text + "</ul>\n"
162
163 def list_item(self, text: str) -> str:
164 return "<li>" + text + "</li>\n"
165
166
167def _unquote_url(url: str) -> str:
168 for _ in range(3):
169 decoded = unquote(url)
170 if decoded == url:
171 break
172 url = decoded
173 return url
174
175
176def _is_safe_url(url: str, safe_protocols: Tuple[str, ...], good_data_protocols: Tuple[str, ...]) -> bool:
177 if url.startswith(safe_protocols):
178 return True
179 if url.startswith(good_data_protocols):
180 return True
181 if url.startswith(("/", "#", "?")):
182 return True
183 return ":" not in url.split("/", 1)[0]