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

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

147 statements  

1from __future__ import annotations 

2 

3from collections.abc import Callable, Generator, Iterable, Mapping, MutableMapping 

4from contextlib import contextmanager 

5from typing import Any, Literal, overload 

6 

7from . import helpers, presets 

8from .common import normalize_url, utils 

9from .parser_block import ParserBlock 

10from .parser_core import ParserCore 

11from .parser_inline import ParserInline 

12from .renderer import RendererHTML, RendererProtocol 

13from .rules_core.state_core import StateCore 

14from .token import Token 

15from .utils import EnvType, OptionsDict, OptionsType, PresetType 

16 

17try: 

18 import linkify_it 

19except ModuleNotFoundError: 

20 linkify_it = None 

21 

22 

23_PRESETS: dict[str, PresetType] = { 

24 "default": presets.default.make(), 

25 "js-default": presets.js_default.make(), 

26 "zero": presets.zero.make(), 

27 "commonmark": presets.commonmark.make(), 

28 "gfm-like": presets.gfm_like.make(), 

29} 

30 

31 

32class MarkdownIt: 

33 def __init__( 

34 self, 

35 config: str | PresetType = "commonmark", 

36 options_update: Mapping[str, Any] | None = None, 

37 *, 

38 renderer_cls: Callable[[MarkdownIt], RendererProtocol] = RendererHTML, 

39 ): 

40 """Main parser class 

41 

42 :param config: name of configuration to load or a pre-defined dictionary 

43 :param options_update: dictionary that will be merged into ``config["options"]`` 

44 :param renderer_cls: the class to load as the renderer: 

45 ``self.renderer = renderer_cls(self) 

46 """ 

47 # add modules 

48 self.utils = utils 

49 self.helpers = helpers 

50 

51 # initialise classes 

52 self.inline = ParserInline() 

53 self.block = ParserBlock() 

54 self.core = ParserCore() 

55 self.renderer = renderer_cls(self) 

56 self.linkify = linkify_it.LinkifyIt() if linkify_it else None 

57 

58 # set the configuration 

59 if options_update and not isinstance(options_update, Mapping): 

60 # catch signature change where renderer_cls was not used as a key-word 

61 raise TypeError( 

62 f"options_update should be a mapping: {options_update}" 

63 "\n(Perhaps you intended this to be the renderer_cls?)" 

64 ) 

65 self.configure(config, options_update=options_update) 

66 

67 def __repr__(self) -> str: 

68 return f"{self.__class__.__module__}.{self.__class__.__name__}()" 

69 

70 @overload 

71 def __getitem__(self, name: Literal["inline"]) -> ParserInline: 

72 ... 

73 

74 @overload 

75 def __getitem__(self, name: Literal["block"]) -> ParserBlock: 

76 ... 

77 

78 @overload 

79 def __getitem__(self, name: Literal["core"]) -> ParserCore: 

80 ... 

81 

82 @overload 

83 def __getitem__(self, name: Literal["renderer"]) -> RendererProtocol: 

84 ... 

85 

86 @overload 

87 def __getitem__(self, name: str) -> Any: 

88 ... 

89 

90 def __getitem__(self, name: str) -> Any: 

91 return { 

92 "inline": self.inline, 

93 "block": self.block, 

94 "core": self.core, 

95 "renderer": self.renderer, 

96 }[name] 

97 

98 def set(self, options: OptionsType) -> None: 

99 """Set parser options (in the same format as in constructor). 

100 Probably, you will never need it, but you can change options after constructor call. 

101 

102 __Note:__ To achieve the best possible performance, don't modify a 

103 `markdown-it` instance options on the fly. If you need multiple configurations 

104 it's best to create multiple instances and initialize each with separate config. 

105 """ 

106 self.options = OptionsDict(options) 

107 

108 def configure( 

109 self, presets: str | PresetType, options_update: Mapping[str, Any] | None = None 

110 ) -> MarkdownIt: 

111 """Batch load of all options and component settings. 

112 This is an internal method, and you probably will not need it. 

113 But if you will - see available presets and data structure 

114 [here](https://github.com/markdown-it/markdown-it/tree/master/lib/presets) 

115 

116 We strongly recommend to use presets instead of direct config loads. 

117 That will give better compatibility with next versions. 

118 """ 

119 if isinstance(presets, str): 

120 if presets not in _PRESETS: 

121 raise KeyError(f"Wrong `markdown-it` preset '{presets}', check name") 

122 config = _PRESETS[presets] 

123 else: 

124 config = presets 

125 

126 if not config: 

127 raise ValueError("Wrong `markdown-it` config, can't be empty") 

128 

129 options = config.get("options", {}) or {} 

130 if options_update: 

131 options = {**options, **options_update} # type: ignore 

132 

133 self.set(options) # type: ignore 

134 

135 if "components" in config: 

136 for name, component in config["components"].items(): 

137 rules = component.get("rules", None) 

138 if rules: 

139 self[name].ruler.enableOnly(rules) 

140 rules2 = component.get("rules2", None) 

141 if rules2: 

142 self[name].ruler2.enableOnly(rules2) 

143 

144 return self 

145 

146 def get_all_rules(self) -> dict[str, list[str]]: 

147 """Return the names of all active rules.""" 

148 rules = { 

149 chain: self[chain].ruler.get_all_rules() 

150 for chain in ["core", "block", "inline"] 

151 } 

152 rules["inline2"] = self.inline.ruler2.get_all_rules() 

153 return rules 

154 

155 def get_active_rules(self) -> dict[str, list[str]]: 

156 """Return the names of all active rules.""" 

157 rules = { 

158 chain: self[chain].ruler.get_active_rules() 

159 for chain in ["core", "block", "inline"] 

160 } 

161 rules["inline2"] = self.inline.ruler2.get_active_rules() 

162 return rules 

163 

164 def enable( 

165 self, names: str | Iterable[str], ignoreInvalid: bool = False 

166 ) -> MarkdownIt: 

167 """Enable list or rules. (chainable) 

168 

169 :param names: rule name or list of rule names to enable. 

170 :param ignoreInvalid: set `true` to ignore errors when rule not found. 

171 

172 It will automatically find appropriate components, 

173 containing rules with given names. If rule not found, and `ignoreInvalid` 

174 not set - throws exception. 

175 

176 Example:: 

177 

178 md = MarkdownIt().enable(['sub', 'sup']).disable('smartquotes') 

179 

180 """ 

181 result = [] 

182 

183 if isinstance(names, str): 

184 names = [names] 

185 

186 for chain in ["core", "block", "inline"]: 

187 result.extend(self[chain].ruler.enable(names, True)) 

188 result.extend(self.inline.ruler2.enable(names, True)) 

189 

190 missed = [name for name in names if name not in result] 

191 if missed and not ignoreInvalid: 

192 raise ValueError(f"MarkdownIt. Failed to enable unknown rule(s): {missed}") 

193 

194 return self 

195 

196 def disable( 

197 self, names: str | Iterable[str], ignoreInvalid: bool = False 

198 ) -> MarkdownIt: 

199 """The same as [[MarkdownIt.enable]], but turn specified rules off. (chainable) 

200 

201 :param names: rule name or list of rule names to disable. 

202 :param ignoreInvalid: set `true` to ignore errors when rule not found. 

203 

204 """ 

205 result = [] 

206 

207 if isinstance(names, str): 

208 names = [names] 

209 

210 for chain in ["core", "block", "inline"]: 

211 result.extend(self[chain].ruler.disable(names, True)) 

212 result.extend(self.inline.ruler2.disable(names, True)) 

213 

214 missed = [name for name in names if name not in result] 

215 if missed and not ignoreInvalid: 

216 raise ValueError(f"MarkdownIt. Failed to disable unknown rule(s): {missed}") 

217 return self 

218 

219 @contextmanager 

220 def reset_rules(self) -> Generator[None, None, None]: 

221 """A context manager, that will reset the current enabled rules on exit.""" 

222 chain_rules = self.get_active_rules() 

223 yield 

224 for chain, rules in chain_rules.items(): 

225 if chain != "inline2": 

226 self[chain].ruler.enableOnly(rules) 

227 self.inline.ruler2.enableOnly(chain_rules["inline2"]) 

228 

229 def add_render_rule( 

230 self, name: str, function: Callable[..., Any], fmt: str = "html" 

231 ) -> None: 

232 """Add a rule for rendering a particular Token type. 

233 

234 Only applied when ``renderer.__output__ == fmt`` 

235 """ 

236 if self.renderer.__output__ == fmt: 

237 self.renderer.rules[name] = function.__get__(self.renderer) # type: ignore 

238 

239 def use( 

240 self, plugin: Callable[..., None], *params: Any, **options: Any 

241 ) -> MarkdownIt: 

242 """Load specified plugin with given params into current parser instance. (chainable) 

243 

244 It's just a sugar to call `plugin(md, params)` with curring. 

245 

246 Example:: 

247 

248 def func(tokens, idx): 

249 tokens[idx].content = tokens[idx].content.replace('foo', 'bar') 

250 md = MarkdownIt().use(plugin, 'foo_replace', 'text', func) 

251 

252 """ 

253 plugin(self, *params, **options) 

254 return self 

255 

256 def parse(self, src: str, env: EnvType | None = None) -> list[Token]: 

257 """Parse the source string to a token stream 

258 

259 :param src: source string 

260 :param env: environment sandbox 

261 

262 Parse input string and return list of block tokens (special token type 

263 "inline" will contain list of inline tokens). 

264 

265 `env` is used to pass data between "distributed" rules and return additional 

266 metadata like reference info, needed for the renderer. It also can be used to 

267 inject data in specific cases. Usually, you will be ok to pass `{}`, 

268 and then pass updated object to renderer. 

269 """ 

270 env = {} if env is None else env 

271 if not isinstance(env, MutableMapping): 

272 raise TypeError(f"Input data should be a MutableMapping, not {type(env)}") 

273 if not isinstance(src, str): 

274 raise TypeError(f"Input data should be a string, not {type(src)}") 

275 state = StateCore(src, self, env) 

276 self.core.process(state) 

277 return state.tokens 

278 

279 def render(self, src: str, env: EnvType | None = None) -> Any: 

280 """Render markdown string into html. It does all magic for you :). 

281 

282 :param src: source string 

283 :param env: environment sandbox 

284 :returns: The output of the loaded renderer 

285 

286 `env` can be used to inject additional metadata (`{}` by default). 

287 But you will not need it with high probability. See also comment 

288 in [[MarkdownIt.parse]]. 

289 """ 

290 env = {} if env is None else env 

291 return self.renderer.render(self.parse(src, env), self.options, env) 

292 

293 def parseInline(self, src: str, env: EnvType | None = None) -> list[Token]: 

294 """The same as [[MarkdownIt.parse]] but skip all block rules. 

295 

296 :param src: source string 

297 :param env: environment sandbox 

298 

299 It returns the 

300 block tokens list with the single `inline` element, containing parsed inline 

301 tokens in `children` property. Also updates `env` object. 

302 """ 

303 env = {} if env is None else env 

304 if not isinstance(env, MutableMapping): 

305 raise TypeError(f"Input data should be an MutableMapping, not {type(env)}") 

306 if not isinstance(src, str): 

307 raise TypeError(f"Input data should be a string, not {type(src)}") 

308 state = StateCore(src, self, env) 

309 state.inlineMode = True 

310 self.core.process(state) 

311 return state.tokens 

312 

313 def renderInline(self, src: str, env: EnvType | None = None) -> Any: 

314 """Similar to [[MarkdownIt.render]] but for single paragraph content. 

315 

316 :param src: source string 

317 :param env: environment sandbox 

318 

319 Similar to [[MarkdownIt.render]] but for single paragraph content. Result 

320 will NOT be wrapped into `<p>` tags. 

321 """ 

322 env = {} if env is None else env 

323 return self.renderer.render(self.parseInline(src, env), self.options, env) 

324 

325 # link methods 

326 

327 def validateLink(self, url: str) -> bool: 

328 """Validate if the URL link is allowed in output. 

329 

330 This validator can prohibit more than really needed to prevent XSS. 

331 It's a tradeoff to keep code simple and to be secure by default. 

332 

333 Note: the url should be normalized at this point, and existing entities decoded. 

334 """ 

335 return normalize_url.validateLink(url) 

336 

337 def normalizeLink(self, url: str) -> str: 

338 """Normalize destination URLs in links 

339 

340 :: 

341 

342 [label]: destination 'title' 

343 ^^^^^^^^^^^ 

344 """ 

345 return normalize_url.normalizeLink(url) 

346 

347 def normalizeLinkText(self, link: str) -> str: 

348 """Normalize autolink content 

349 

350 :: 

351 

352 <destination> 

353 ~~~~~~~~~~~ 

354 """ 

355 return normalize_url.normalizeLinkText(link)