Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/markdown_it/renderer.py: 26%

118 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:07 +0000

1""" 

2class Renderer 

3 

4Generates HTML from parsed token stream. Each instance has independent 

5copy of rules. Those can be rewritten with ease. Also, you can add new 

6rules if you create plugin and adds new token types. 

7""" 

8from __future__ import annotations 

9 

10from collections.abc import MutableMapping, Sequence 

11import inspect 

12from typing import Any, ClassVar 

13 

14from .common.utils import escapeHtml, unescapeAll 

15from .token import Token 

16from .utils import OptionsDict 

17 

18try: 

19 from typing import Protocol 

20except ImportError: # Python <3.8 doesn't have `Protocol` in the stdlib 

21 from typing_extensions import Protocol # type: ignore 

22 

23 

24class RendererProtocol(Protocol): 

25 __output__: ClassVar[str] 

26 

27 def render( 

28 self, tokens: Sequence[Token], options: OptionsDict, env: MutableMapping 

29 ) -> Any: 

30 ... 

31 

32 

33class RendererHTML(RendererProtocol): 

34 """Contains render rules for tokens. Can be updated and extended. 

35 

36 Example: 

37 

38 Each rule is called as independent static function with fixed signature: 

39 

40 :: 

41 

42 class Renderer: 

43 def token_type_name(self, tokens, idx, options, env) { 

44 # ... 

45 return renderedHTML 

46 

47 :: 

48 

49 class CustomRenderer(RendererHTML): 

50 def strong_open(self, tokens, idx, options, env): 

51 return '<b>' 

52 def strong_close(self, tokens, idx, options, env): 

53 return '</b>' 

54 

55 md = MarkdownIt(renderer_cls=CustomRenderer) 

56 

57 result = md.render(...) 

58 

59 See https://github.com/markdown-it/markdown-it/blob/master/lib/renderer.js 

60 for more details and examples. 

61 """ 

62 

63 __output__ = "html" 

64 

65 def __init__(self, parser=None): 

66 self.rules = { 

67 k: v 

68 for k, v in inspect.getmembers(self, predicate=inspect.ismethod) 

69 if not (k.startswith("render") or k.startswith("_")) 

70 } 

71 

72 def render( 

73 self, tokens: Sequence[Token], options: OptionsDict, env: MutableMapping 

74 ) -> str: 

75 """Takes token stream and generates HTML. 

76 

77 :param tokens: list on block tokens to render 

78 :param options: params of parser instance 

79 :param env: additional data from parsed input 

80 

81 """ 

82 result = "" 

83 

84 for i, token in enumerate(tokens): 

85 if token.type == "inline": 

86 if token.children: 

87 result += self.renderInline(token.children, options, env) 

88 elif token.type in self.rules: 

89 result += self.rules[token.type](tokens, i, options, env) 

90 else: 

91 result += self.renderToken(tokens, i, options, env) 

92 

93 return result 

94 

95 def renderInline( 

96 self, tokens: Sequence[Token], options: OptionsDict, env: MutableMapping 

97 ) -> str: 

98 """The same as ``render``, but for single token of `inline` type. 

99 

100 :param tokens: list on block tokens to render 

101 :param options: params of parser instance 

102 :param env: additional data from parsed input (references, for example) 

103 """ 

104 result = "" 

105 

106 for i, token in enumerate(tokens): 

107 if token.type in self.rules: 

108 result += self.rules[token.type](tokens, i, options, env) 

109 else: 

110 result += self.renderToken(tokens, i, options, env) 

111 

112 return result 

113 

114 def renderToken( 

115 self, 

116 tokens: Sequence[Token], 

117 idx: int, 

118 options: OptionsDict, 

119 env: MutableMapping, 

120 ) -> str: 

121 """Default token renderer. 

122 

123 Can be overridden by custom function 

124 

125 :param idx: token index to render 

126 :param options: params of parser instance 

127 """ 

128 result = "" 

129 needLf = False 

130 token = tokens[idx] 

131 

132 # Tight list paragraphs 

133 if token.hidden: 

134 return "" 

135 

136 # Insert a newline between hidden paragraph and subsequent opening 

137 # block-level tag. 

138 # 

139 # For example, here we should insert a newline before blockquote: 

140 # - a 

141 # > 

142 # 

143 if token.block and token.nesting != -1 and idx and tokens[idx - 1].hidden: 

144 result += "\n" 

145 

146 # Add token name, e.g. `<img` 

147 result += ("</" if token.nesting == -1 else "<") + token.tag 

148 

149 # Encode attributes, e.g. `<img src="foo"` 

150 result += self.renderAttrs(token) 

151 

152 # Add a slash for self-closing tags, e.g. `<img src="foo" /` 

153 if token.nesting == 0 and options["xhtmlOut"]: 

154 result += " /" 

155 

156 # Check if we need to add a newline after this tag 

157 if token.block: 

158 needLf = True 

159 

160 if token.nesting == 1: 

161 if idx + 1 < len(tokens): 

162 nextToken = tokens[idx + 1] 

163 

164 if nextToken.type == "inline" or nextToken.hidden: 

165 # Block-level tag containing an inline tag. 

166 # 

167 needLf = False 

168 

169 elif nextToken.nesting == -1 and nextToken.tag == token.tag: 

170 # Opening tag + closing tag of the same type. E.g. `<li></li>`. 

171 # 

172 needLf = False 

173 

174 result += ">\n" if needLf else ">" 

175 

176 return result 

177 

178 @staticmethod 

179 def renderAttrs(token: Token) -> str: 

180 """Render token attributes to string.""" 

181 result = "" 

182 

183 for key, value in token.attrItems(): 

184 result += " " + escapeHtml(key) + '="' + escapeHtml(str(value)) + '"' 

185 

186 return result 

187 

188 def renderInlineAsText( 

189 self, 

190 tokens: Sequence[Token] | None, 

191 options: OptionsDict, 

192 env: MutableMapping, 

193 ) -> str: 

194 """Special kludge for image `alt` attributes to conform CommonMark spec. 

195 

196 Don't try to use it! Spec requires to show `alt` content with stripped markup, 

197 instead of simple escaping. 

198 

199 :param tokens: list on block tokens to render 

200 :param options: params of parser instance 

201 :param env: additional data from parsed input 

202 """ 

203 result = "" 

204 

205 for token in tokens or []: 

206 if token.type == "text": 

207 result += token.content 

208 elif token.type == "image": 

209 if token.children: 

210 result += self.renderInlineAsText(token.children, options, env) 

211 elif token.type == "softbreak": 

212 result += "\n" 

213 

214 return result 

215 

216 ################################################### 

217 

218 def code_inline(self, tokens: Sequence[Token], idx: int, options, env) -> str: 

219 token = tokens[idx] 

220 return ( 

221 "<code" 

222 + self.renderAttrs(token) 

223 + ">" 

224 + escapeHtml(tokens[idx].content) 

225 + "</code>" 

226 ) 

227 

228 def code_block( 

229 self, 

230 tokens: Sequence[Token], 

231 idx: int, 

232 options: OptionsDict, 

233 env: MutableMapping, 

234 ) -> str: 

235 token = tokens[idx] 

236 

237 return ( 

238 "<pre" 

239 + self.renderAttrs(token) 

240 + "><code>" 

241 + escapeHtml(tokens[idx].content) 

242 + "</code></pre>\n" 

243 ) 

244 

245 def fence( 

246 self, 

247 tokens: Sequence[Token], 

248 idx: int, 

249 options: OptionsDict, 

250 env: MutableMapping, 

251 ) -> str: 

252 token = tokens[idx] 

253 info = unescapeAll(token.info).strip() if token.info else "" 

254 langName = "" 

255 langAttrs = "" 

256 

257 if info: 

258 arr = info.split(maxsplit=1) 

259 langName = arr[0] 

260 if len(arr) == 2: 

261 langAttrs = arr[1] 

262 

263 if options.highlight: 

264 highlighted = options.highlight( 

265 token.content, langName, langAttrs 

266 ) or escapeHtml(token.content) 

267 else: 

268 highlighted = escapeHtml(token.content) 

269 

270 if highlighted.startswith("<pre"): 

271 return highlighted + "\n" 

272 

273 # If language exists, inject class gently, without modifying original token. 

274 # May be, one day we will add .deepClone() for token and simplify this part, but 

275 # now we prefer to keep things local. 

276 if info: 

277 # Fake token just to render attributes 

278 tmpToken = Token(type="", tag="", nesting=0, attrs=token.attrs.copy()) 

279 tmpToken.attrJoin("class", options.langPrefix + langName) 

280 

281 return ( 

282 "<pre><code" 

283 + self.renderAttrs(tmpToken) 

284 + ">" 

285 + highlighted 

286 + "</code></pre>\n" 

287 ) 

288 

289 return ( 

290 "<pre><code" 

291 + self.renderAttrs(token) 

292 + ">" 

293 + highlighted 

294 + "</code></pre>\n" 

295 ) 

296 

297 def image( 

298 self, 

299 tokens: Sequence[Token], 

300 idx: int, 

301 options: OptionsDict, 

302 env: MutableMapping, 

303 ) -> str: 

304 token = tokens[idx] 

305 

306 # "alt" attr MUST be set, even if empty. Because it's mandatory and 

307 # should be placed on proper position for tests. 

308 if token.children: 

309 token.attrSet("alt", self.renderInlineAsText(token.children, options, env)) 

310 else: 

311 token.attrSet("alt", "") 

312 

313 return self.renderToken(tokens, idx, options, env) 

314 

315 def hardbreak( 

316 self, tokens: Sequence[Token], idx: int, options: OptionsDict, *args 

317 ) -> str: 

318 return "<br />\n" if options.xhtmlOut else "<br>\n" 

319 

320 def softbreak( 

321 self, tokens: Sequence[Token], idx: int, options: OptionsDict, *args 

322 ) -> str: 

323 return ( 

324 ("<br />\n" if options.xhtmlOut else "<br>\n") if options.breaks else "\n" 

325 ) 

326 

327 def text(self, tokens: Sequence[Token], idx: int, *args) -> str: 

328 return escapeHtml(tokens[idx].content) 

329 

330 def html_block(self, tokens: Sequence[Token], idx: int, *args) -> str: 

331 return tokens[idx].content 

332 

333 def html_inline(self, tokens: Sequence[Token], idx: int, *args) -> str: 

334 return tokens[idx].content