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
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}
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
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
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
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)
67 def __repr__(self) -> str:
68 return f"{self.__class__.__module__}.{self.__class__.__name__}()"
70 @overload
71 def __getitem__(self, name: Literal["inline"]) -> ParserInline:
72 ...
74 @overload
75 def __getitem__(self, name: Literal["block"]) -> ParserBlock:
76 ...
78 @overload
79 def __getitem__(self, name: Literal["core"]) -> ParserCore:
80 ...
82 @overload
83 def __getitem__(self, name: Literal["renderer"]) -> RendererProtocol:
84 ...
86 @overload
87 def __getitem__(self, name: str) -> Any:
88 ...
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]
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.
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)
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)
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
126 if not config:
127 raise ValueError("Wrong `markdown-it` config, can't be empty")
129 options = config.get("options", {}) or {}
130 if options_update:
131 options = {**options, **options_update} # type: ignore
133 self.set(options) # type: ignore
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)
144 return self
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
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
164 def enable(
165 self, names: str | Iterable[str], ignoreInvalid: bool = False
166 ) -> MarkdownIt:
167 """Enable list or rules. (chainable)
169 :param names: rule name or list of rule names to enable.
170 :param ignoreInvalid: set `true` to ignore errors when rule not found.
172 It will automatically find appropriate components,
173 containing rules with given names. If rule not found, and `ignoreInvalid`
174 not set - throws exception.
176 Example::
178 md = MarkdownIt().enable(['sub', 'sup']).disable('smartquotes')
180 """
181 result = []
183 if isinstance(names, str):
184 names = [names]
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))
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}")
194 return self
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)
201 :param names: rule name or list of rule names to disable.
202 :param ignoreInvalid: set `true` to ignore errors when rule not found.
204 """
205 result = []
207 if isinstance(names, str):
208 names = [names]
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))
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
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"])
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.
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
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)
244 It's just a sugar to call `plugin(md, params)` with curring.
246 Example::
248 def func(tokens, idx):
249 tokens[idx].content = tokens[idx].content.replace('foo', 'bar')
250 md = MarkdownIt().use(plugin, 'foo_replace', 'text', func)
252 """
253 plugin(self, *params, **options)
254 return self
256 def parse(self, src: str, env: EnvType | None = None) -> list[Token]:
257 """Parse the source string to a token stream
259 :param src: source string
260 :param env: environment sandbox
262 Parse input string and return list of block tokens (special token type
263 "inline" will contain list of inline tokens).
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
279 def render(self, src: str, env: EnvType | None = None) -> Any:
280 """Render markdown string into html. It does all magic for you :).
282 :param src: source string
283 :param env: environment sandbox
284 :returns: The output of the loaded renderer
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)
293 def parseInline(self, src: str, env: EnvType | None = None) -> list[Token]:
294 """The same as [[MarkdownIt.parse]] but skip all block rules.
296 :param src: source string
297 :param env: environment sandbox
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
313 def renderInline(self, src: str, env: EnvType | None = None) -> Any:
314 """Similar to [[MarkdownIt.render]] but for single paragraph content.
316 :param src: source string
317 :param env: environment sandbox
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)
325 # link methods
327 def validateLink(self, url: str) -> bool:
328 """Validate if the URL link is allowed in output.
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.
333 Note: the url should be normalized at this point, and existing entities decoded.
334 """
335 return normalize_url.validateLink(url)
337 def normalizeLink(self, url: str) -> str:
338 """Normalize destination URLs in links
340 ::
342 [label]: destination 'title'
343 ^^^^^^^^^^^
344 """
345 return normalize_url.normalizeLink(url)
347 def normalizeLinkText(self, link: str) -> str:
348 """Normalize autolink content
350 ::
352 <destination>
353 ~~~~~~~~~~~
354 """
355 return normalize_url.normalizeLinkText(link)