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
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
1from __future__ import annotations
3from collections.abc import Callable, Generator, Iterable, Mapping, MutableMapping
4from contextlib import contextmanager
5from typing import Any, Literal, overload
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
17try:
18 import linkify_it
19except ModuleNotFoundError:
20 linkify_it = None
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}
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
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
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
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)
68 def __repr__(self) -> str:
69 return f"{self.__class__.__module__}.{self.__class__.__name__}()"
71 @overload
72 def __getitem__(self, name: Literal["inline"]) -> ParserInline: ...
74 @overload
75 def __getitem__(self, name: Literal["block"]) -> ParserBlock: ...
77 @overload
78 def __getitem__(self, name: Literal["core"]) -> ParserCore: ...
80 @overload
81 def __getitem__(self, name: Literal["renderer"]) -> RendererProtocol: ...
83 @overload
84 def __getitem__(self, name: str) -> Any: ...
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]
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.
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)
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)
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
122 if not config:
123 raise ValueError("Wrong `markdown-it` config, can't be empty")
125 options = config.get("options", {}) or {}
126 if options_update:
127 options = {**options, **options_update} # type: ignore
129 self.set(options)
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)
140 return self
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
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
160 def enable(
161 self, names: str | Iterable[str], ignoreInvalid: bool = False
162 ) -> MarkdownIt:
163 """Enable list or rules. (chainable)
165 :param names: rule name or list of rule names to enable.
166 :param ignoreInvalid: set `true` to ignore errors when rule not found.
168 It will automatically find appropriate components,
169 containing rules with given names. If rule not found, and `ignoreInvalid`
170 not set - throws exception.
172 Example::
174 md = MarkdownIt().enable(['sub', 'sup']).disable('smartquotes')
176 """
177 result = []
179 if isinstance(names, str):
180 names = [names]
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))
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}")
190 return self
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)
197 :param names: rule name or list of rule names to disable.
198 :param ignoreInvalid: set `true` to ignore errors when rule not found.
200 """
201 result = []
203 if isinstance(names, str):
204 names = [names]
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))
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
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"])
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.
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
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)
240 It's just a sugar to call `plugin(md, params)` with curring.
242 Example::
244 def func(tokens, idx):
245 tokens[idx].content = tokens[idx].content.replace('foo', 'bar')
246 md = MarkdownIt().use(plugin, 'foo_replace', 'text', func)
248 """
249 plugin(self, *params, **options)
250 return self
252 def parse(self, src: str, env: EnvType | None = None) -> list[Token]:
253 """Parse the source string to a token stream
255 :param src: source string
256 :param env: environment sandbox
258 Parse input string and return list of block tokens (special token type
259 "inline" will contain list of inline tokens).
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
275 def render(self, src: str, env: EnvType | None = None) -> Any:
276 """Render markdown string into html. It does all magic for you :).
278 :param src: source string
279 :param env: environment sandbox
280 :returns: The output of the loaded renderer
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)
289 def parseInline(self, src: str, env: EnvType | None = None) -> list[Token]:
290 """The same as [[MarkdownIt.parse]] but skip all block rules.
292 :param src: source string
293 :param env: environment sandbox
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
309 def renderInline(self, src: str, env: EnvType | None = None) -> Any:
310 """Similar to [[MarkdownIt.render]] but for single paragraph content.
312 :param src: source string
313 :param env: environment sandbox
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)
321 # link methods
323 def validateLink(self, url: str) -> bool:
324 """Validate if the URL link is allowed in output.
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.
329 Note: the url should be normalized at this point, and existing entities decoded.
330 """
331 return normalize_url.validateLink(url)
333 def normalizeLink(self, url: str) -> str:
334 """Normalize destination URLs in links
336 ::
338 [label]: destination 'title'
339 ^^^^^^^^^^^
340 """
341 return normalize_url.normalizeLink(url)
343 def normalizeLinkText(self, link: str) -> str:
344 """Normalize autolink content
346 ::
348 <destination>
349 ~~~~~~~~~~~
350 """
351 return normalize_url.normalizeLinkText(link)