diff --git a/markdown_it/main.py b/markdown_it/main.py index c95465e2..dfa18189 100644 --- a/markdown_it/main.py +++ b/markdown_it/main.py @@ -7,6 +7,7 @@ Iterable, List, Mapping, + MutableMapping, Optional, Union, ) @@ -19,7 +20,7 @@ from .parser_inline import ParserInline # noqa F401 from .rules_core.state_core import StateCore from .renderer import RendererHTML -from .utils import AttrDict +from .utils import OptionsDict try: import linkify_it @@ -69,7 +70,6 @@ def __init__( f"options_update should be a mapping: {options_update}" "\n(Perhaps you intended this to be the renderer_cls?)" ) - self.options = AttrDict() self.configure(config, options_update=options_update) def __repr__(self) -> str: @@ -83,7 +83,7 @@ def __getitem__(self, name: str) -> Any: "renderer": self.renderer, }[name] - def set(self, options: AttrDict) -> None: + def set(self, options: MutableMapping) -> 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. @@ -91,7 +91,7 @@ def set(self, options: AttrDict) -> None: `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 = options + self.options = OptionsDict(options) def configure( self, presets: Union[str, Mapping], options_update: Optional[Mapping] = None @@ -118,8 +118,7 @@ def configure( if options_update: options = {**options, **options_update} - if options: - self.set(AttrDict(options)) + self.set(options) if "components" in config: for name, component in config["components"].items(): @@ -238,7 +237,7 @@ def func(tokens, idx): plugin(self, *params, **options) return self - def parse(self, src: str, env: Optional[AttrDict] = None) -> List[Token]: + def parse(self, src: str, env: Optional[MutableMapping] = None) -> List[Token]: """Parse the source string to a token stream :param src: source string @@ -252,16 +251,16 @@ def parse(self, src: str, env: Optional[AttrDict] = None) -> List[Token]: inject data in specific cases. Usually, you will be ok to pass `{}`, and then pass updated object to renderer. """ - env = AttrDict() if env is None else env - if not isinstance(env, AttrDict): # type: ignore - raise TypeError(f"Input data should be an AttrDict, not {type(env)}") + 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: Optional[AttrDict] = None) -> Any: + def render(self, src: str, env: Optional[MutableMapping] = None) -> Any: """Render markdown string into html. It does all magic for you :). :param src: source string @@ -272,11 +271,12 @@ def render(self, src: str, env: Optional[AttrDict] = None) -> Any: But you will not need it with high probability. See also comment in [[MarkdownIt.parse]]. """ - if env is None: - env = AttrDict() + env = {} if env is None else env return self.renderer.render(self.parse(src, env), self.options, env) - def parseInline(self, src: str, env: Optional[AttrDict] = None) -> List[Token]: + def parseInline( + self, src: str, env: Optional[MutableMapping] = None + ) -> List[Token]: """The same as [[MarkdownIt.parse]] but skip all block rules. :param src: source string @@ -286,9 +286,9 @@ def parseInline(self, src: str, env: Optional[AttrDict] = None) -> List[Token]: block tokens list with the single `inline` element, containing parsed inline tokens in `children` property. Also updates `env` object. """ - env = AttrDict() if env is None else env - if not isinstance(env, AttrDict): # type: ignore - raise TypeError(f"Input data should be an AttrDict, not {type(env)}") + 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) @@ -296,7 +296,7 @@ def parseInline(self, src: str, env: Optional[AttrDict] = None) -> List[Token]: self.core.process(state) return state.tokens - def renderInline(self, src: str, env: Optional[AttrDict] = None) -> Any: + def renderInline(self, src: str, env: Optional[MutableMapping] = None) -> Any: """Similar to [[MarkdownIt.render]] but for single paragraph content. :param src: source string @@ -305,7 +305,7 @@ def renderInline(self, src: str, env: Optional[AttrDict] = None) -> Any: Similar to [[MarkdownIt.render]] but for single paragraph content. Result will NOT be wrapped into `

` tags. """ - env = AttrDict() if env is None else env + env = {} if env is None else env return self.renderer.render(self.parseInline(src, env), self.options, env) # link methods diff --git a/markdown_it/port.yaml b/markdown_it/port.yaml index 7309bd8e..565f0143 100644 --- a/markdown_it/port.yaml +++ b/markdown_it/port.yaml @@ -11,6 +11,10 @@ Convert JS `for` loops to `while` loops this is generally the main difference between the codes, because in python you can't do e.g. `for {i=1;i str: + def render( + self, tokens: Sequence[Token], options: OptionsDict, env: MutableMapping + ) -> str: """Takes token stream and generates HTML. :param tokens: list on block tokens to render @@ -73,7 +76,9 @@ def render(self, tokens: Sequence[Token], options, env) -> str: return result - def renderInline(self, tokens: Sequence[Token], options, env) -> str: + def renderInline( + self, tokens: Sequence[Token], options: OptionsDict, env: MutableMapping + ) -> str: """The same as ``render``, but for single token of `inline` type. :param tokens: list on block tokens to render @@ -91,7 +96,11 @@ def renderInline(self, tokens: Sequence[Token], options, env) -> str: return result def renderToken( - self, tokens: Sequence[Token], idx: int, options: dict, env: dict + self, + tokens: Sequence[Token], + idx: int, + options: OptionsDict, + env: MutableMapping, ) -> str: """Default token renderer. @@ -161,7 +170,10 @@ def renderAttrs(token: Token) -> str: return result def renderInlineAsText( - self, tokens: Optional[Sequence[Token]], options, env + self, + tokens: Optional[Sequence[Token]], + options: OptionsDict, + env: MutableMapping, ) -> str: """Special kludge for image `alt` attributes to conform CommonMark spec. @@ -195,7 +207,13 @@ def code_inline(self, tokens: Sequence[Token], idx: int, options, env) -> str: + "" ) - def code_block(self, tokens: Sequence[Token], idx: int, options, env) -> str: + def code_block( + self, + tokens: Sequence[Token], + idx: int, + options: OptionsDict, + env: MutableMapping, + ) -> str: token = tokens[idx] return ( @@ -206,7 +224,13 @@ def code_block(self, tokens: Sequence[Token], idx: int, options, env) -> str: + "\n" ) - def fence(self, tokens: Sequence[Token], idx: int, options, env) -> str: + def fence( + self, + tokens: Sequence[Token], + idx: int, + options: OptionsDict, + env: MutableMapping, + ) -> str: token = tokens[idx] info = unescapeAll(token.info).strip() if token.info else "" langName = "" @@ -252,7 +276,13 @@ def fence(self, tokens: Sequence[Token], idx: int, options, env) -> str: + "\n" ) - def image(self, tokens: Sequence[Token], idx: int, options, env) -> str: + def image( + self, + tokens: Sequence[Token], + idx: int, + options: OptionsDict, + env: MutableMapping, + ) -> str: token = tokens[idx] # "alt" attr MUST be set, even if empty. Because it's mandatory and @@ -268,10 +298,14 @@ def image(self, tokens: Sequence[Token], idx: int, options, env) -> str: return self.renderToken(tokens, idx, options, env) - def hardbreak(self, tokens: Sequence[Token], idx: int, options, *args) -> str: + def hardbreak( + self, tokens: Sequence[Token], idx: int, options: OptionsDict, *args + ) -> str: return "
\n" if options.xhtmlOut else "
\n" - def softbreak(self, tokens: Sequence[Token], idx: int, options, *args) -> str: + def softbreak( + self, tokens: Sequence[Token], idx: int, options: OptionsDict, *args + ) -> str: return ( ("
\n" if options.xhtmlOut else "
\n") if options.breaks else "\n" ) diff --git a/markdown_it/ruler.py b/markdown_it/ruler.py index d49c9d51..81189ded 100644 --- a/markdown_it/ruler.py +++ b/markdown_it/ruler.py @@ -20,6 +20,7 @@ class Ruler Dict, Iterable, List, + MutableMapping, Optional, Tuple, TYPE_CHECKING, @@ -27,8 +28,6 @@ class Ruler ) import attr -from markdown_it.utils import AttrDict - if TYPE_CHECKING: from markdown_it import MarkdownIt @@ -36,7 +35,7 @@ class Ruler class StateBase: srcCharCode: Tuple[int, ...] - def __init__(self, src: str, md: "MarkdownIt", env: AttrDict): + def __init__(self, src: str, md: "MarkdownIt", env: MutableMapping): self.src = src self.env = env self.md = md diff --git a/markdown_it/rules_block/reference.py b/markdown_it/rules_block/reference.py index 882118a1..bdd96ead 100644 --- a/markdown_it/rules_block/reference.py +++ b/markdown_it/rules_block/reference.py @@ -1,7 +1,6 @@ import logging from ..common.utils import isSpace, normalizeReference, charCodeAt -from ..utils import AttrDict from .state_block import StateBlock @@ -189,19 +188,19 @@ def reference(state: StateBlock, startLine, _endLine, silent): state.line = startLine + lines + 1 if label not in state.env["references"]: - state.env["references"][label] = AttrDict( - {"title": title, "href": href, "map": [startLine, state.line]} - ) + state.env["references"][label] = { + "title": title, + "href": href, + "map": [startLine, state.line], + } else: state.env.setdefault("duplicate_refs", []).append( - AttrDict( - { - "title": title, - "href": href, - "label": label, - "map": [startLine, state.line], - } - ) + { + "title": title, + "href": href, + "label": label, + "map": [startLine, state.line], + } ) state.parentType = oldParentType diff --git a/markdown_it/rules_core/state_core.py b/markdown_it/rules_core/state_core.py index 64378fb4..a560a283 100644 --- a/markdown_it/rules_core/state_core.py +++ b/markdown_it/rules_core/state_core.py @@ -1,6 +1,5 @@ -from typing import List, Optional, TYPE_CHECKING +from typing import List, MutableMapping, Optional, TYPE_CHECKING -from ..utils import AttrDict from ..token import Token from ..ruler import StateBase @@ -13,7 +12,7 @@ def __init__( self, src: str, md: "MarkdownIt", - env: AttrDict, + env: MutableMapping, tokens: Optional[List[Token]] = None, ): self.src = src diff --git a/markdown_it/rules_inline/image.py b/markdown_it/rules_inline/image.py index 6f8c38cc..d3813f77 100644 --- a/markdown_it/rules_inline/image.py +++ b/markdown_it/rules_inline/image.py @@ -117,13 +117,13 @@ def image(state: StateInline, silent: bool): label = normalizeReference(label) - ref = state.env.references.get(label, None) + ref = state.env["references"].get(label, None) if not ref: state.pos = oldPos return False - href = ref.href - title = ref.title + href = ref["href"] + title = ref["title"] # # We found the end of the link, and know for a fact it's a valid link diff --git a/markdown_it/rules_inline/link.py b/markdown_it/rules_inline/link.py index b4b7ae76..919ccf12 100644 --- a/markdown_it/rules_inline/link.py +++ b/markdown_it/rules_inline/link.py @@ -113,13 +113,15 @@ def link(state: StateInline, silent: bool): label = normalizeReference(label) - ref = state.env.references[label] if label in state.env.references else None + ref = ( + state.env["references"][label] if label in state.env["references"] else None + ) if not ref: state.pos = oldPos return False - href = ref.href - title = ref.title + href = ref["href"] + title = ref["title"] # # We found the end of the link, and know for a fact it's a valid link diff --git a/markdown_it/rules_inline/state_inline.py b/markdown_it/rules_inline/state_inline.py index 0164299e..54555411 100644 --- a/markdown_it/rules_inline/state_inline.py +++ b/markdown_it/rules_inline/state_inline.py @@ -1,9 +1,8 @@ from collections import namedtuple -from typing import Dict, List, Optional, TYPE_CHECKING +from typing import Dict, List, MutableMapping, Optional, TYPE_CHECKING import attr -from ..utils import AttrDict from ..token import Token from ..ruler import StateBase from ..common.utils import isWhiteSpace, isPunctChar, isMdAsciiPunct @@ -48,7 +47,7 @@ class Delimiter: class StateInline(StateBase): def __init__( - self, src: str, md: "MarkdownIt", env: AttrDict, outTokens: List[Token] + self, src: str, md: "MarkdownIt", env: MutableMapping, outTokens: List[Token] ): self.src = src self.env = env diff --git a/markdown_it/utils.py b/markdown_it/utils.py index 013f4db3..605a39d3 100644 --- a/markdown_it/utils.py +++ b/markdown_it/utils.py @@ -1,5 +1,90 @@ from pathlib import Path -from typing import TYPE_CHECKING, Any, List, Union +from typing import TYPE_CHECKING, Any, Callable, List, Optional, Union + + +class OptionsDict(dict): + """A dictionary, with attribute access to core markdownit configuration options.""" + + @property + def maxNesting(self) -> int: + """Internal protection, recursion limit.""" + return self["maxNesting"] + + @maxNesting.setter + def maxNesting(self, value: int): + self["maxNesting"] = value + + @property + def html(self) -> bool: + """Enable HTML tags in source.""" + return self["html"] + + @html.setter + def html(self, value: bool): + self["html"] = value + + @property + def linkify(self) -> bool: + """Enable autoconversion of URL-like texts to links.""" + return self["linkify"] + + @linkify.setter + def linkify(self, value: bool): + self["linkify"] = value + + @property + def typographer(self) -> bool: + """Enable smartquotes and replacements.""" + return self["typographer"] + + @typographer.setter + def typographer(self, value: bool): + self["typographer"] = value + + @property + def quotes(self) -> str: + """Quote characters.""" + return self["quotes"] + + @quotes.setter + def quotes(self, value: str): + self["quotes"] = value + + @property + def xhtmlOut(self) -> bool: + """Use '/' to close single tags (
).""" + return self["xhtmlOut"] + + @xhtmlOut.setter + def xhtmlOut(self, value: bool): + self["xhtmlOut"] = value + + @property + def breaks(self) -> bool: + """Convert newlines in paragraphs into
.""" + return self["breaks"] + + @breaks.setter + def breaks(self, value: bool): + self["breaks"] = value + + @property + def langPrefix(self) -> str: + """CSS language prefix for fenced blocks.""" + return self["langPrefix"] + + @langPrefix.setter + def langPrefix(self, value: str): + self["langPrefix"] = value + + @property + def highlight(self) -> Optional[Callable[[str, str, str], str]]: + """Highlighter function: (content, langName, langAttrs) -> escaped HTML.""" + return self["highlight"] + + @highlight.setter + def highlight(self, value: Optional[Callable[[str, str, str], str]]): + self["highlight"] = value if TYPE_CHECKING: diff --git a/tests/test_api/test_main.py b/tests/test_api/test_main.py index 6fcc36c5..64dcb506 100644 --- a/tests/test_api/test_main.py +++ b/tests/test_api/test_main.py @@ -1,7 +1,6 @@ from markdown_it import MarkdownIt from markdown_it.token import Token from markdown_it.rules_core import StateCore -from markdown_it.utils import AttrDict def test_get_rules(): @@ -271,11 +270,11 @@ def test_empty_env(): """Test that an empty `env` is mutated, not copied and mutated.""" md = MarkdownIt() - env = AttrDict() + env = {} md.render("[foo]: /url\n[foo]", env) assert "references" in env - env = AttrDict() + env = {} md.parse("[foo]: /url\n[foo]", env) assert "references" in env diff --git a/tests/test_port/test_references.py b/tests/test_port/test_references.py index 759c6ca1..eb669c12 100644 --- a/tests/test_port/test_references.py +++ b/tests/test_port/test_references.py @@ -1,12 +1,11 @@ from markdown_it import MarkdownIt -from markdown_it.utils import AttrDict def test_ref_definitions(): md = MarkdownIt() src = "[a]: abc\n\n[b]: xyz\n\n[b]: ijk" - env = AttrDict() + env = {} tokens = md.parse(src, env) assert tokens == [] assert env == { @@ -21,14 +20,12 @@ def test_ref_definitions(): def test_use_existing_env(data_regression): md = MarkdownIt() src = "[a]\n\n[c]: ijk" - env = AttrDict( - { - "references": { - "A": {"title": "", "href": "abc", "map": [0, 1]}, - "B": {"title": "", "href": "xyz", "map": [2, 3]}, - } + env = { + "references": { + "A": {"title": "", "href": "abc", "map": [0, 1]}, + "B": {"title": "", "href": "xyz", "map": [2, 3]}, } - ) + } tokens = md.parse(src, env) data_regression.check([token.as_dict() for token in tokens]) assert env == {