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
\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 == {