Skip to content

Commit

Permalink
‼️ BREAKING: Replace use of AttrDict for env/options (#151)
Browse files Browse the repository at this point in the history
For `env` any Python mutable mapping is now allowed,
and so attribute access to keys is not allowed.
For `MarkdownIt.options` it is now set as an `OptionsDict`,
which is a dictionary sub-class, with attribute access
only for core markdownit configuration keys.
  • Loading branch information
chrisjsewell committed Mar 31, 2021
1 parent 7a1df9c commit 9ecda04
Show file tree
Hide file tree
Showing 15 changed files with 189 additions and 72 deletions.
38 changes: 19 additions & 19 deletions markdown_it/main.py
Expand Up @@ -7,6 +7,7 @@
Iterable,
List,
Mapping,
MutableMapping,
Optional,
Union,
)
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -83,15 +83,15 @@ 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.
__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 = options
self.options = OptionsDict(options)

def configure(
self, presets: Union[str, Mapping], options_update: Optional[Mapping] = None
Expand All @@ -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():
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -286,17 +286,17 @@ 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)
state.inlineMode = True
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
Expand All @@ -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 `<p>` 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
Expand Down
4 changes: 4 additions & 0 deletions markdown_it/port.yaml
Expand Up @@ -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<x;i++} {}`
- |
`env` is a common Python dictionary, and so does not have attribute access to keys,
as with JavaScript dictionaries.
`options` have attribute access only to core markdownit configuration options
- |
`Token.attrs` is a dictionary, instead of a list of lists.
Upstream the list format is only used to guarantee order: https://github.com/markdown-it/markdown-it/issues/142,
Expand Down
2 changes: 1 addition & 1 deletion markdown_it/presets/commonmark.py
Expand Up @@ -34,7 +34,7 @@ def make():
# or '' if the source string is not changed and should be escaped externally.
# If result starts with <pre... internal wrapper is skipped.
#
# function (/*str, lang*/) { return ''; }
# function (/*str, lang, attrs*/) { return ''; }
#
"highlight": None,
},
Expand Down
2 changes: 1 addition & 1 deletion markdown_it/presets/default.py
Expand Up @@ -26,7 +26,7 @@ def make():
# or '' if the source string is not changed and should be escaped externaly.
# If result starts with <pre... internal wrapper is skipped.
#
# function (/*str, lang*/) { return ''; }
# function (/*str, lang, attrs*/) { return ''; }
#
"highlight": None,
},
Expand Down
2 changes: 1 addition & 1 deletion markdown_it/presets/zero.py
Expand Up @@ -28,7 +28,7 @@ def make():
# Highlighter function. Should return escaped HTML,
# or '' if the source string is not changed and should be escaped externaly.
# If result starts with <pre... internal wrapper is skipped.
# function (/*str, lang*/) { return ''; }
# function (/*str, lang, attrs*/) { return ''; }
"highlight": None,
},
"components": {
Expand Down
54 changes: 44 additions & 10 deletions markdown_it/renderer.py
Expand Up @@ -6,10 +6,11 @@ class Renderer
rules if you create plugin and adds new token types.
"""
import inspect
from typing import Optional, Sequence
from typing import MutableMapping, Optional, Sequence

from .common.utils import unescapeAll, escapeHtml
from .token import Token
from .utils import OptionsDict


class RendererHTML:
Expand Down Expand Up @@ -51,7 +52,9 @@ def __init__(self, parser=None):
if not (k.startswith("render") or k.startswith("_"))
}

def render(self, tokens: Sequence[Token], options, env) -> 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
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -195,7 +207,13 @@ def code_inline(self, tokens: Sequence[Token], idx: int, options, env) -> str:
+ "</code>"
)

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 (
Expand All @@ -206,7 +224,13 @@ def code_block(self, tokens: Sequence[Token], idx: int, options, env) -> str:
+ "</code></pre>\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 = ""
Expand Down Expand Up @@ -252,7 +276,13 @@ def fence(self, tokens: Sequence[Token], idx: int, options, env) -> str:
+ "</code></pre>\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
Expand All @@ -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 "<br />\n" if options.xhtmlOut else "<br>\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 (
("<br />\n" if options.xhtmlOut else "<br>\n") if options.breaks else "\n"
)
Expand Down
5 changes: 2 additions & 3 deletions markdown_it/ruler.py
Expand Up @@ -20,23 +20,22 @@ class Ruler
Dict,
Iterable,
List,
MutableMapping,
Optional,
Tuple,
TYPE_CHECKING,
Union,
)
import attr

from markdown_it.utils import AttrDict

if TYPE_CHECKING:
from markdown_it import MarkdownIt


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
Expand Down
23 changes: 11 additions & 12 deletions 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


Expand Down Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions 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

Expand All @@ -13,7 +12,7 @@ def __init__(
self,
src: str,
md: "MarkdownIt",
env: AttrDict,
env: MutableMapping,
tokens: Optional[List[Token]] = None,
):
self.src = src
Expand Down
6 changes: 3 additions & 3 deletions markdown_it/rules_inline/image.py
Expand Up @@ -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
Expand Down

0 comments on commit 9ecda04

Please sign in to comment.