Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/markdown_it/main.py: 64%
132 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:07 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:07 +0000
1from __future__ import annotations
3from collections.abc import Callable, Generator, Iterable, Mapping, MutableMapping
4from contextlib import contextmanager
5from typing import Any
7from . import helpers, presets # noqa F401
8from .common import normalize_url, utils # noqa F401
9from .parser_block import ParserBlock # noqa F401
10from .parser_core import ParserCore # noqa F401
11from .parser_inline import ParserInline # noqa F401
12from .renderer import RendererHTML, RendererProtocol
13from .rules_core.state_core import StateCore
14from .token import Token
15from .utils import OptionsDict
17try:
18 import linkify_it
19except ModuleNotFoundError:
20 linkify_it = None
23_PRESETS = {
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 | Mapping = "commonmark",
36 options_update: Mapping | 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: Any = 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 def __getitem__(self, name: str) -> Any:
71 return {
72 "inline": self.inline,
73 "block": self.block,
74 "core": self.core,
75 "renderer": self.renderer,
76 }[name]
78 def set(self, options: MutableMapping) -> None:
79 """Set parser options (in the same format as in constructor).
80 Probably, you will never need it, but you can change options after constructor call.
82 __Note:__ To achieve the best possible performance, don't modify a
83 `markdown-it` instance options on the fly. If you need multiple configurations
84 it's best to create multiple instances and initialize each with separate config.
85 """
86 self.options = OptionsDict(options)
88 def configure(
89 self, presets: str | Mapping, options_update: Mapping | None = None
90 ) -> MarkdownIt:
91 """Batch load of all options and component settings.
92 This is an internal method, and you probably will not need it.
93 But if you will - see available presets and data structure
94 [here](https://github.com/markdown-it/markdown-it/tree/master/lib/presets)
96 We strongly recommend to use presets instead of direct config loads.
97 That will give better compatibility with next versions.
98 """
99 if isinstance(presets, str):
100 if presets not in _PRESETS:
101 raise KeyError(f"Wrong `markdown-it` preset '{presets}', check name")
102 config = _PRESETS[presets]
103 else:
104 config = presets
106 if not config:
107 raise ValueError("Wrong `markdown-it` config, can't be empty")
109 options = config.get("options", {}) or {}
110 if options_update:
111 options = {**options, **options_update}
113 self.set(options)
115 if "components" in config:
116 for name, component in config["components"].items():
117 rules = component.get("rules", None)
118 if rules:
119 self[name].ruler.enableOnly(rules)
120 rules2 = component.get("rules2", None)
121 if rules2:
122 self[name].ruler2.enableOnly(rules2)
124 return self
126 def get_all_rules(self) -> dict[str, list[str]]:
127 """Return the names of all active rules."""
128 rules = {
129 chain: self[chain].ruler.get_all_rules()
130 for chain in ["core", "block", "inline"]
131 }
132 rules["inline2"] = self.inline.ruler2.get_all_rules()
133 return rules
135 def get_active_rules(self) -> dict[str, list[str]]:
136 """Return the names of all active rules."""
137 rules = {
138 chain: self[chain].ruler.get_active_rules()
139 for chain in ["core", "block", "inline"]
140 }
141 rules["inline2"] = self.inline.ruler2.get_active_rules()
142 return rules
144 def enable(
145 self, names: str | Iterable[str], ignoreInvalid: bool = False
146 ) -> MarkdownIt:
147 """Enable list or rules. (chainable)
149 :param names: rule name or list of rule names to enable.
150 :param ignoreInvalid: set `true` to ignore errors when rule not found.
152 It will automatically find appropriate components,
153 containing rules with given names. If rule not found, and `ignoreInvalid`
154 not set - throws exception.
156 Example::
158 md = MarkdownIt().enable(['sub', 'sup']).disable('smartquotes')
160 """
161 result = []
163 if isinstance(names, str):
164 names = [names]
166 for chain in ["core", "block", "inline"]:
167 result.extend(self[chain].ruler.enable(names, True))
168 result.extend(self.inline.ruler2.enable(names, True))
170 missed = [name for name in names if name not in result]
171 if missed and not ignoreInvalid:
172 raise ValueError(f"MarkdownIt. Failed to enable unknown rule(s): {missed}")
174 return self
176 def disable(
177 self, names: str | Iterable[str], ignoreInvalid: bool = False
178 ) -> MarkdownIt:
179 """The same as [[MarkdownIt.enable]], but turn specified rules off. (chainable)
181 :param names: rule name or list of rule names to disable.
182 :param ignoreInvalid: set `true` to ignore errors when rule not found.
184 """
185 result = []
187 if isinstance(names, str):
188 names = [names]
190 for chain in ["core", "block", "inline"]:
191 result.extend(self[chain].ruler.disable(names, True))
192 result.extend(self.inline.ruler2.disable(names, True))
194 missed = [name for name in names if name not in result]
195 if missed and not ignoreInvalid:
196 raise ValueError(f"MarkdownIt. Failed to disable unknown rule(s): {missed}")
197 return self
199 @contextmanager
200 def reset_rules(self) -> Generator[None, None, None]:
201 """A context manager, that will reset the current enabled rules on exit."""
202 chain_rules = self.get_active_rules()
203 yield
204 for chain, rules in chain_rules.items():
205 if chain != "inline2":
206 self[chain].ruler.enableOnly(rules)
207 self.inline.ruler2.enableOnly(chain_rules["inline2"])
209 def add_render_rule(self, name: str, function: Callable, fmt: str = "html") -> None:
210 """Add a rule for rendering a particular Token type.
212 Only applied when ``renderer.__output__ == fmt``
213 """
214 if self.renderer.__output__ == fmt:
215 self.renderer.rules[name] = function.__get__(self.renderer) # type: ignore
217 def use(self, plugin: Callable, *params, **options) -> MarkdownIt:
218 """Load specified plugin with given params into current parser instance. (chainable)
220 It's just a sugar to call `plugin(md, params)` with curring.
222 Example::
224 def func(tokens, idx):
225 tokens[idx].content = tokens[idx].content.replace('foo', 'bar')
226 md = MarkdownIt().use(plugin, 'foo_replace', 'text', func)
228 """
229 plugin(self, *params, **options)
230 return self
232 def parse(self, src: str, env: MutableMapping | None = None) -> list[Token]:
233 """Parse the source string to a token stream
235 :param src: source string
236 :param env: environment sandbox
238 Parse input string and return list of block tokens (special token type
239 "inline" will contain list of inline tokens).
241 `env` is used to pass data between "distributed" rules and return additional
242 metadata like reference info, needed for the renderer. It also can be used to
243 inject data in specific cases. Usually, you will be ok to pass `{}`,
244 and then pass updated object to renderer.
245 """
246 env = {} if env is None else env
247 if not isinstance(env, MutableMapping):
248 raise TypeError(f"Input data should be a MutableMapping, not {type(env)}")
249 if not isinstance(src, str):
250 raise TypeError(f"Input data should be a string, not {type(src)}")
251 state = StateCore(src, self, env)
252 self.core.process(state)
253 return state.tokens
255 def render(self, src: str, env: MutableMapping | None = None) -> Any:
256 """Render markdown string into html. It does all magic for you :).
258 :param src: source string
259 :param env: environment sandbox
260 :returns: The output of the loaded renderer
262 `env` can be used to inject additional metadata (`{}` by default).
263 But you will not need it with high probability. See also comment
264 in [[MarkdownIt.parse]].
265 """
266 env = {} if env is None else env
267 return self.renderer.render(self.parse(src, env), self.options, env)
269 def parseInline(self, src: str, env: MutableMapping | None = None) -> list[Token]:
270 """The same as [[MarkdownIt.parse]] but skip all block rules.
272 :param src: source string
273 :param env: environment sandbox
275 It returns the
276 block tokens list with the single `inline` element, containing parsed inline
277 tokens in `children` property. Also updates `env` object.
278 """
279 env = {} if env is None else env
280 if not isinstance(env, MutableMapping):
281 raise TypeError(f"Input data should be an MutableMapping, not {type(env)}")
282 if not isinstance(src, str):
283 raise TypeError(f"Input data should be a string, not {type(src)}")
284 state = StateCore(src, self, env)
285 state.inlineMode = True
286 self.core.process(state)
287 return state.tokens
289 def renderInline(self, src: str, env: MutableMapping | None = None) -> Any:
290 """Similar to [[MarkdownIt.render]] but for single paragraph content.
292 :param src: source string
293 :param env: environment sandbox
295 Similar to [[MarkdownIt.render]] but for single paragraph content. Result
296 will NOT be wrapped into `<p>` tags.
297 """
298 env = {} if env is None else env
299 return self.renderer.render(self.parseInline(src, env), self.options, env)
301 # link methods
303 def validateLink(self, url: str) -> bool:
304 """Validate if the URL link is allowed in output.
306 This validator can prohibit more than really needed to prevent XSS.
307 It's a tradeoff to keep code simple and to be secure by default.
309 Note: the url should be normalized at this point, and existing entities decoded.
310 """
311 return normalize_url.validateLink(url)
313 def normalizeLink(self, url: str) -> str:
314 """Normalize destination URLs in links
316 ::
318 [label]: destination 'title'
319 ^^^^^^^^^^^
320 """
321 return normalize_url.normalizeLink(url)
323 def normalizeLinkText(self, link: str) -> str:
324 """Normalize autolink content
326 ::
328 <destination>
329 ~~~~~~~~~~~
330 """
331 return normalize_url.normalizeLinkText(link)