Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/markdown_it/main.py: 82%
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``
232 :param name: must equal the ``type`` of the token(s) this should render
233 (e.g. ``"heading_open"``, ``"fence"``); it is not an arbitrary label.
234 If it does not match any token type actually produced by the active
235 rules, ``function`` will silently never be called.
236 """
237 if self.renderer.__output__ == fmt:
238 self.renderer.rules[name] = function.__get__(self.renderer) # type: ignore
240 def use(
241 self, plugin: Callable[..., None], *params: Any, **options: Any
242 ) -> MarkdownIt:
243 """Load specified plugin with given params into current parser instance. (chainable)
245 It's just a sugar to call `plugin(md, params)` with curring.
247 Example::
249 def func(tokens, idx):
250 tokens[idx].content = tokens[idx].content.replace('foo', 'bar')
251 md = MarkdownIt().use(plugin, 'foo_replace', 'text', func)
253 """
254 plugin(self, *params, **options)
255 return self
257 def parse(self, src: str, env: EnvType | None = None) -> list[Token]:
258 """Parse the source string to a token stream
260 :param src: source string
261 :param env: environment sandbox
263 Parse input string and return list of block tokens (special token type
264 "inline" will contain list of inline tokens).
266 `env` is used to pass data between "distributed" rules and return additional
267 metadata like reference info, needed for the renderer. It also can be used to
268 inject data in specific cases. Usually, you will be ok to pass `{}`,
269 and then pass updated object to renderer.
270 """
271 env = {} if env is None else env
272 if not isinstance(env, MutableMapping):
273 raise TypeError(f"Input data should be a MutableMapping, not {type(env)}")
274 if not isinstance(src, str):
275 raise TypeError(f"Input data should be a string, not {type(src)}")
276 state = StateCore(src, self, env)
277 self.core.process(state)
278 return state.tokens
280 def render(self, src: str, env: EnvType | None = None) -> Any:
281 """Render markdown string into html. It does all magic for you :).
283 :param src: source string
284 :param env: environment sandbox
285 :returns: The output of the loaded renderer
287 `env` can be used to inject additional metadata (`{}` by default).
288 But you will not need it with high probability. See also comment
289 in [[MarkdownIt.parse]].
290 """
291 env = {} if env is None else env
292 return self.renderer.render(self.parse(src, env), self.options, env)
294 def parseInline(self, src: str, env: EnvType | None = None) -> list[Token]:
295 """The same as [[MarkdownIt.parse]] but skip all block rules.
297 :param src: source string
298 :param env: environment sandbox
300 It returns the
301 block tokens list with the single `inline` element, containing parsed inline
302 tokens in `children` property. Also updates `env` object.
303 """
304 env = {} if env is None else env
305 if not isinstance(env, MutableMapping):
306 raise TypeError(f"Input data should be an MutableMapping, not {type(env)}")
307 if not isinstance(src, str):
308 raise TypeError(f"Input data should be a string, not {type(src)}")
309 state = StateCore(src, self, env)
310 state.inlineMode = True
311 self.core.process(state)
312 return state.tokens
314 def renderInline(self, src: str, env: EnvType | None = None) -> Any:
315 """Similar to [[MarkdownIt.render]] but for single paragraph content.
317 :param src: source string
318 :param env: environment sandbox
320 Similar to [[MarkdownIt.render]] but for single paragraph content. Result
321 will NOT be wrapped into `<p>` tags.
322 """
323 env = {} if env is None else env
324 return self.renderer.render(self.parseInline(src, env), self.options, env)
326 # link methods
328 def validateLink(self, url: str) -> bool:
329 """Validate if the URL link is allowed in output.
331 This validator can prohibit more than really needed to prevent XSS.
332 It's a tradeoff to keep code simple and to be secure by default.
334 Note: the url should be normalized at this point, and existing entities decoded.
335 """
336 return normalize_url.validateLink(url)
338 def normalizeLink(self, url: str) -> str:
339 """Normalize destination URLs in links
341 ::
343 [label]: destination 'title'
344 ^^^^^^^^^^^
345 """
346 return normalize_url.normalizeLink(url)
348 def normalizeLinkText(self, link: str) -> str:
349 """Normalize autolink content
351 ::
353 <destination>
354 ~~~~~~~~~~~
355 """
356 return normalize_url.normalizeLinkText(link)