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

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

142 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 "gfm-like2": presets.gfm_like2.make(), 

30} 

31 

32 

33class MarkdownIt: 

34 def __init__( 

35 self, 

36 config: str | PresetType = "commonmark", 

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

38 *, 

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

40 ): 

41 """Main parser class 

42 

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

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

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

46 ``self.renderer = renderer_cls(self) 

47 """ 

48 # add modules 

49 self.utils = utils 

50 self.helpers = helpers 

51 

52 # initialise classes 

53 self.inline = ParserInline() 

54 self.block = ParserBlock() 

55 self.core = ParserCore() 

56 self.renderer = renderer_cls(self) 

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

58 

59 # set the configuration 

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

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

62 raise TypeError( 

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

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

65 ) 

66 self.configure(config, options_update=options_update) 

67 

68 def __repr__(self) -> str: 

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

70 

71 @overload 

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

73 

74 @overload 

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

76 

77 @overload 

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

79 

80 @overload 

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

82 

83 @overload 

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

85 

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

87 return { 

88 "inline": self.inline, 

89 "block": self.block, 

90 "core": self.core, 

91 "renderer": self.renderer, 

92 }[name] 

93 

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

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

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

97 

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

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

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

101 """ 

102 self.options = OptionsDict(options) 

103 

104 def configure( 

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

106 ) -> MarkdownIt: 

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

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

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

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

111 

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

113 That will give better compatibility with next versions. 

114 """ 

115 if isinstance(presets, str): 

116 if presets not in _PRESETS: 

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

118 config = _PRESETS[presets] 

119 else: 

120 config = presets 

121 

122 if not config: 

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

124 

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

126 if options_update: 

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

128 

129 self.set(options) 

130 

131 if "components" in config: 

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

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

134 if rules: 

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

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

137 if rules2: 

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

139 

140 return self 

141 

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

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

144 rules = { 

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

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

147 } 

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

149 return rules 

150 

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

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

153 rules = { 

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

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

156 } 

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

158 return rules 

159 

160 def enable( 

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

162 ) -> MarkdownIt: 

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

164 

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

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

167 

168 It will automatically find appropriate components, 

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

170 not set - throws exception. 

171 

172 Example:: 

173 

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

175 

176 """ 

177 result = [] 

178 

179 if isinstance(names, str): 

180 names = [names] 

181 

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

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

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

185 

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

187 if missed and not ignoreInvalid: 

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

189 

190 return self 

191 

192 def disable( 

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

194 ) -> MarkdownIt: 

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

196 

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

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

199 

200 """ 

201 result = [] 

202 

203 if isinstance(names, str): 

204 names = [names] 

205 

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

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

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

209 

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

211 if missed and not ignoreInvalid: 

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

213 return self 

214 

215 @contextmanager 

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

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

218 chain_rules = self.get_active_rules() 

219 yield 

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

221 if chain != "inline2": 

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

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

224 

225 def add_render_rule( 

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

227 ) -> None: 

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

229 

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

231 """ 

232 if self.renderer.__output__ == fmt: 

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

234 

235 def use( 

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

237 ) -> MarkdownIt: 

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

239 

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

241 

242 Example:: 

243 

244 def func(tokens, idx): 

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

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

247 

248 """ 

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

250 return self 

251 

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

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

254 

255 :param src: source string 

256 :param env: environment sandbox 

257 

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

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

260 

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

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

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

264 and then pass updated object to renderer. 

265 """ 

266 env = {} if env is None else env 

267 if not isinstance(env, MutableMapping): 

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

269 if not isinstance(src, str): 

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

271 state = StateCore(src, self, env) 

272 self.core.process(state) 

273 return state.tokens 

274 

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

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

277 

278 :param src: source string 

279 :param env: environment sandbox 

280 :returns: The output of the loaded renderer 

281 

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

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

284 in [[MarkdownIt.parse]]. 

285 """ 

286 env = {} if env is None else env 

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

288 

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

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

291 

292 :param src: source string 

293 :param env: environment sandbox 

294 

295 It returns the 

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

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

298 """ 

299 env = {} if env is None else env 

300 if not isinstance(env, MutableMapping): 

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

302 if not isinstance(src, str): 

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

304 state = StateCore(src, self, env) 

305 state.inlineMode = True 

306 self.core.process(state) 

307 return state.tokens 

308 

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

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

311 

312 :param src: source string 

313 :param env: environment sandbox 

314 

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

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

317 """ 

318 env = {} if env is None else env 

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

320 

321 # link methods 

322 

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

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

325 

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

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

328 

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

330 """ 

331 return normalize_url.validateLink(url) 

332 

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

334 """Normalize destination URLs in links 

335 

336 :: 

337 

338 [label]: destination 'title' 

339 ^^^^^^^^^^^ 

340 """ 

341 return normalize_url.normalizeLink(url) 

342 

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

344 """Normalize autolink content 

345 

346 :: 

347 

348 <destination> 

349 ~~~~~~~~~~~ 

350 """ 

351 return normalize_url.normalizeLinkText(link)