|
- from __future__ import annotations
-
- from collections.abc import Callable, Generator, Iterable, Mapping, MutableMapping
- from contextlib import contextmanager
- from typing import Any, Literal, overload
-
- from . import helpers, presets
- from .common import normalize_url, utils
- from .parser_block import ParserBlock
- from .parser_core import ParserCore
- from .parser_inline import ParserInline
- from .renderer import RendererHTML, RendererProtocol
- from .rules_core.state_core import StateCore
- from .token import Token
- from .utils import EnvType, OptionsDict, OptionsType, PresetType
-
- try:
- import linkify_it
- except ModuleNotFoundError:
- linkify_it = None
-
-
- _PRESETS: dict[str, PresetType] = {
- "default": presets.default.make(),
- "js-default": presets.js_default.make(),
- "zero": presets.zero.make(),
- "commonmark": presets.commonmark.make(),
- "gfm-like": presets.gfm_like.make(),
- }
-
-
- class MarkdownIt:
- def __init__(
- self,
- config: str | PresetType = "commonmark",
- options_update: Mapping[str, Any] | None = None,
- *,
- renderer_cls: Callable[[MarkdownIt], RendererProtocol] = RendererHTML,
- ):
- """Main parser class
-
- :param config: name of configuration to load or a pre-defined dictionary
- :param options_update: dictionary that will be merged into ``config["options"]``
- :param renderer_cls: the class to load as the renderer:
- ``self.renderer = renderer_cls(self)
- """
- # add modules
- self.utils = utils
- self.helpers = helpers
-
- # initialise classes
- self.inline = ParserInline()
- self.block = ParserBlock()
- self.core = ParserCore()
- self.renderer = renderer_cls(self)
- self.linkify = linkify_it.LinkifyIt() if linkify_it else None
-
- # set the configuration
- if options_update and not isinstance(options_update, Mapping):
- # catch signature change where renderer_cls was not used as a key-word
- raise TypeError(
- f"options_update should be a mapping: {options_update}"
- "\n(Perhaps you intended this to be the renderer_cls?)"
- )
- self.configure(config, options_update=options_update)
-
- def __repr__(self) -> str:
- return f"{self.__class__.__module__}.{self.__class__.__name__}()"
-
- @overload
- def __getitem__(self, name: Literal["inline"]) -> ParserInline:
- ...
-
- @overload
- def __getitem__(self, name: Literal["block"]) -> ParserBlock:
- ...
-
- @overload
- def __getitem__(self, name: Literal["core"]) -> ParserCore:
- ...
-
- @overload
- def __getitem__(self, name: Literal["renderer"]) -> RendererProtocol:
- ...
-
- @overload
- def __getitem__(self, name: str) -> Any:
- ...
-
- def __getitem__(self, name: str) -> Any:
- return {
- "inline": self.inline,
- "block": self.block,
- "core": self.core,
- "renderer": self.renderer,
- }[name]
-
- def set(self, options: OptionsType) -> None:
- """Set parser options (in the same format as in constructor).
- Probably, you will never need it, but you can change options after constructor call.
-
- __Note:__ To achieve the best possible performance, don't modify a
- `markdown-it` instance options on the fly. If you need multiple configurations
- it's best to create multiple instances and initialize each with separate config.
- """
- self.options = OptionsDict(options)
-
- def configure(
- self, presets: str | PresetType, options_update: Mapping[str, Any] | None = None
- ) -> MarkdownIt:
- """Batch load of all options and component settings.
- This is an internal method, and you probably will not need it.
- But if you will - see available presets and data structure
- [here](https://github.com/markdown-it/markdown-it/tree/master/lib/presets)
-
- We strongly recommend to use presets instead of direct config loads.
- That will give better compatibility with next versions.
- """
- if isinstance(presets, str):
- if presets not in _PRESETS:
- raise KeyError(f"Wrong `markdown-it` preset '{presets}', check name")
- config = _PRESETS[presets]
- else:
- config = presets
-
- if not config:
- raise ValueError("Wrong `markdown-it` config, can't be empty")
-
- options = config.get("options", {}) or {}
- if options_update:
- options = {**options, **options_update} # type: ignore
-
- self.set(options) # type: ignore
-
- if "components" in config:
- for name, component in config["components"].items():
- rules = component.get("rules", None)
- if rules:
- self[name].ruler.enableOnly(rules)
- rules2 = component.get("rules2", None)
- if rules2:
- self[name].ruler2.enableOnly(rules2)
-
- return self
-
- def get_all_rules(self) -> dict[str, list[str]]:
- """Return the names of all active rules."""
- rules = {
- chain: self[chain].ruler.get_all_rules()
- for chain in ["core", "block", "inline"]
- }
- rules["inline2"] = self.inline.ruler2.get_all_rules()
- return rules
-
- def get_active_rules(self) -> dict[str, list[str]]:
- """Return the names of all active rules."""
- rules = {
- chain: self[chain].ruler.get_active_rules()
- for chain in ["core", "block", "inline"]
- }
- rules["inline2"] = self.inline.ruler2.get_active_rules()
- return rules
-
- def enable(
- self, names: str | Iterable[str], ignoreInvalid: bool = False
- ) -> MarkdownIt:
- """Enable list or rules. (chainable)
-
- :param names: rule name or list of rule names to enable.
- :param ignoreInvalid: set `true` to ignore errors when rule not found.
-
- It will automatically find appropriate components,
- containing rules with given names. If rule not found, and `ignoreInvalid`
- not set - throws exception.
-
- Example::
-
- md = MarkdownIt().enable(['sub', 'sup']).disable('smartquotes')
-
- """
- result = []
-
- if isinstance(names, str):
- names = [names]
-
- for chain in ["core", "block", "inline"]:
- result.extend(self[chain].ruler.enable(names, True))
- result.extend(self.inline.ruler2.enable(names, True))
-
- missed = [name for name in names if name not in result]
- if missed and not ignoreInvalid:
- raise ValueError(f"MarkdownIt. Failed to enable unknown rule(s): {missed}")
-
- return self
-
- def disable(
- self, names: str | Iterable[str], ignoreInvalid: bool = False
- ) -> MarkdownIt:
- """The same as [[MarkdownIt.enable]], but turn specified rules off. (chainable)
-
- :param names: rule name or list of rule names to disable.
- :param ignoreInvalid: set `true` to ignore errors when rule not found.
-
- """
- result = []
-
- if isinstance(names, str):
- names = [names]
-
- for chain in ["core", "block", "inline"]:
- result.extend(self[chain].ruler.disable(names, True))
- result.extend(self.inline.ruler2.disable(names, True))
-
- missed = [name for name in names if name not in result]
- if missed and not ignoreInvalid:
- raise ValueError(f"MarkdownIt. Failed to disable unknown rule(s): {missed}")
- return self
-
- @contextmanager
- def reset_rules(self) -> Generator[None, None, None]:
- """A context manager, that will reset the current enabled rules on exit."""
- chain_rules = self.get_active_rules()
- yield
- for chain, rules in chain_rules.items():
- if chain != "inline2":
- self[chain].ruler.enableOnly(rules)
- self.inline.ruler2.enableOnly(chain_rules["inline2"])
-
- def add_render_rule(
- self, name: str, function: Callable[..., Any], fmt: str = "html"
- ) -> None:
- """Add a rule for rendering a particular Token type.
-
- Only applied when ``renderer.__output__ == fmt``
- """
- if self.renderer.__output__ == fmt:
- self.renderer.rules[name] = function.__get__(self.renderer) # type: ignore
-
- def use(
- self, plugin: Callable[..., None], *params: Any, **options: Any
- ) -> MarkdownIt:
- """Load specified plugin with given params into current parser instance. (chainable)
-
- It's just a sugar to call `plugin(md, params)` with curring.
-
- Example::
-
- def func(tokens, idx):
- tokens[idx].content = tokens[idx].content.replace('foo', 'bar')
- md = MarkdownIt().use(plugin, 'foo_replace', 'text', func)
-
- """
- plugin(self, *params, **options)
- return self
-
- def parse(self, src: str, env: EnvType | None = None) -> list[Token]:
- """Parse the source string to a token stream
-
- :param src: source string
- :param env: environment sandbox
-
- Parse input string and return list of block tokens (special token type
- "inline" will contain list of inline tokens).
-
- `env` is used to pass data between "distributed" rules and return additional
- metadata like reference info, needed for the renderer. It also can be used to
- inject data in specific cases. Usually, you will be ok to pass `{}`,
- and then pass updated object to renderer.
- """
- env = {} if env is None else env
- if not isinstance(env, MutableMapping):
- raise TypeError(f"Input data should be a MutableMapping, not {type(env)}")
- if not isinstance(src, str):
- raise TypeError(f"Input data should be a string, not {type(src)}")
- state = StateCore(src, self, env)
- self.core.process(state)
- return state.tokens
-
- def render(self, src: str, env: EnvType | None = None) -> Any:
- """Render markdown string into html. It does all magic for you :).
-
- :param src: source string
- :param env: environment sandbox
- :returns: The output of the loaded renderer
-
- `env` can be used to inject additional metadata (`{}` by default).
- But you will not need it with high probability. See also comment
- in [[MarkdownIt.parse]].
- """
- env = {} if env is None else env
- return self.renderer.render(self.parse(src, env), self.options, env)
-
- def parseInline(self, src: str, env: EnvType | None = None) -> list[Token]:
- """The same as [[MarkdownIt.parse]] but skip all block rules.
-
- :param src: source string
- :param env: environment sandbox
-
- It returns the
- block tokens list with the single `inline` element, containing parsed inline
- tokens in `children` property. Also updates `env` object.
- """
- env = {} if env is None else env
- if not isinstance(env, MutableMapping):
- raise TypeError(f"Input data should be an MutableMapping, not {type(env)}")
- if not isinstance(src, str):
- raise TypeError(f"Input data should be a string, not {type(src)}")
- state = StateCore(src, self, env)
- state.inlineMode = True
- self.core.process(state)
- return state.tokens
-
- def renderInline(self, src: str, env: EnvType | None = None) -> Any:
- """Similar to [[MarkdownIt.render]] but for single paragraph content.
-
- :param src: source string
- :param env: environment sandbox
-
- Similar to [[MarkdownIt.render]] but for single paragraph content. Result
- will NOT be wrapped into `<p>` tags.
- """
- env = {} if env is None else env
- return self.renderer.render(self.parseInline(src, env), self.options, env)
-
- # link methods
-
- def validateLink(self, url: str) -> bool:
- """Validate if the URL link is allowed in output.
-
- This validator can prohibit more than really needed to prevent XSS.
- It's a tradeoff to keep code simple and to be secure by default.
-
- Note: the url should be normalized at this point, and existing entities decoded.
- """
- return normalize_url.validateLink(url)
-
- def normalizeLink(self, url: str) -> str:
- """Normalize destination URLs in links
-
- ::
-
- [label]: destination 'title'
- ^^^^^^^^^^^
- """
- return normalize_url.normalizeLink(url)
-
- def normalizeLinkText(self, link: str) -> str:
- """Normalize autolink content
-
- ::
-
- <destination>
- ~~~~~~~~~~~
- """
- return normalize_url.normalizeLinkText(link)
|