From 9b00ace7b9436c14e70566c2409cb52668fcd11e Mon Sep 17 00:00:00 2001
From: "Michael R. Crusoe" ' + text + '
`` tag in HTML."""
state.append_token({'type': 'thematic_break'})
# $ does not count '\n'
return m.end() + 1
- def parse_indent_code(self, m: Match, state: BlockState) -> int:
+ def parse_indent_code(self, m: Match[str], state: BlockState) -> int:
"""Parse token for code block which is indented by 4 spaces."""
# it is a part of the paragraph
end_pos = state.append_paragraph()
@@ -134,7 +136,7 @@ def parse_indent_code(self, m: Match, state: BlockState) -> int:
state.append_token({'type': 'block_code', 'raw': code, 'style': 'indent'})
return m.end()
- def parse_fenced_code(self, m: Match, state: BlockState) -> Optional[int]:
+ def parse_fenced_code(self, m: Match[str], state: BlockState) -> Optional[int]:
"""Parse token for fenced code block. A fenced code block is started with
3 or more backtick(`) or tilde(~).
@@ -156,7 +158,7 @@ def markdown(text):
# CommonMark Example 145
# Info strings for backtick code blocks cannot contain backticks
if info.find(c) != -1:
- return
+ return None
_end = re.compile(
r'^ {0,3}' + c + '{' + str(len(marker)) + r',}[ \t]*(?:\n|$)', re.M)
@@ -182,7 +184,7 @@ def markdown(text):
state.append_token(token)
return end_pos
- def parse_atx_heading(self, m: Match, state: BlockState) -> int:
+ def parse_atx_heading(self, m: Match[str], state: BlockState) -> int:
"""Parse token for ATX heading. An ATX heading is started with 1 to 6
symbol of ``#``."""
level = len(m.group('atx_1'))
@@ -195,7 +197,7 @@ def parse_atx_heading(self, m: Match, state: BlockState) -> int:
state.append_token(token)
return m.end() + 1
- def parse_setex_heading(self, m: Match, state: BlockState) -> Optional[int]:
+ def parse_setex_heading(self, m: Match[str], state: BlockState) -> Optional[int]:
"""Parse token for setex style heading. A setex heading syntax looks like:
.. code-block:: markdown
@@ -212,11 +214,12 @@ def parse_setex_heading(self, m: Match, state: BlockState) -> Optional[int]:
return m.end() + 1
sc = self.compile_sc(['thematic_break', 'list'])
- m = sc.match(state.src, state.cursor)
- if m:
- return self.parse_method(m, state)
+ m2 = sc.match(state.src, state.cursor)
+ if m2:
+ return self.parse_method(m2, state)
+ return None
- def parse_ref_link(self, m: Match, state: BlockState) -> Optional[int]:
+ def parse_ref_link(self, m: Match[str], state: BlockState) -> Optional[int]:
"""Parse link references and save the link information into ``state.env``.
Here is an example of a link reference:
@@ -241,11 +244,13 @@ def parse_ref_link(self, m: Match, state: BlockState) -> Optional[int]:
label = m.group('reflink_1')
key = unikey(label)
if not key:
- return
+ return None
href, href_pos = parse_link_href(state.src, m.end(), block=True)
if href is None:
- return
+ return None
+
+ assert href_pos is not None
_blank = self.BLANK_LINE.search(state.src, href_pos)
if _blank:
@@ -255,26 +260,27 @@ def parse_ref_link(self, m: Match, state: BlockState) -> Optional[int]:
title, title_pos = parse_link_title(state.src, href_pos, max_pos)
if title_pos:
- m = _BLANK_TO_LINE.match(state.src, title_pos)
- if m:
- title_pos = m.end()
+ m2 = _BLANK_TO_LINE.match(state.src, title_pos)
+ if m2:
+ title_pos = m2.end()
else:
title_pos = None
title = None
if title_pos is None:
- m = _BLANK_TO_LINE.match(state.src, href_pos)
- if m:
- href_pos = m.end()
+ m3 = _BLANK_TO_LINE.match(state.src, href_pos)
+ if m3:
+ href_pos = m3.end()
else:
href_pos = None
href = None
end_pos = title_pos or href_pos
if not end_pos:
- return
+ return None
if key not in state.env['ref_links']:
+ assert href is not None
href = unescape_char(href)
data = {'url': escape_url(href), 'label': label}
if title:
@@ -282,7 +288,9 @@ def parse_ref_link(self, m: Match, state: BlockState) -> Optional[int]:
state.env['ref_links'][key] = data
return end_pos
- def extract_block_quote(self, m: Match, state: BlockState) -> Tuple[str, int]:
+ def extract_block_quote(
+ self, m: Match[str], state: BlockState
+ ) -> Tuple[str, Optional[int]]:
"""Extract text and cursor end position of a block quote."""
# cleanup at first to detect if it is code block
@@ -295,16 +303,16 @@ def extract_block_quote(self, m: Match, state: BlockState) -> Tuple[str, int]:
state.cursor = m.end() + 1
- end_pos = None
+ end_pos: Optional[int] = None
if require_marker:
- m = _STRICT_BLOCK_QUOTE.match(state.src, state.cursor)
- if m:
- quote = m.group(0)
+ m2 = _STRICT_BLOCK_QUOTE.match(state.src, state.cursor)
+ if m2:
+ quote = m2.group(0)
quote = _BLOCK_QUOTE_LEADING.sub('', quote)
quote = expand_leading_tab(quote, 3)
quote = _BLOCK_QUOTE_TRIM.sub('', quote)
text += quote
- state.cursor = m.end()
+ state.cursor = m2.end()
else:
prev_blank_line = False
break_sc = self.compile_sc([
@@ -312,14 +320,14 @@ def extract_block_quote(self, m: Match, state: BlockState) -> Tuple[str, int]:
'list', 'block_html',
])
while state.cursor < state.cursor_max:
- m = _STRICT_BLOCK_QUOTE.match(state.src, state.cursor)
- if m:
- quote = m.group(0)
+ m3 = _STRICT_BLOCK_QUOTE.match(state.src, state.cursor)
+ if m3:
+ quote = m3.group(0)
quote = _BLOCK_QUOTE_LEADING.sub('', quote)
quote = expand_leading_tab(quote, 3)
quote = _BLOCK_QUOTE_TRIM.sub('', quote)
text += quote
- state.cursor = m.end()
+ state.cursor = m3.end()
if not quote.strip():
prev_blank_line = True
else:
@@ -332,9 +340,9 @@ def extract_block_quote(self, m: Match, state: BlockState) -> Tuple[str, int]:
# a block quote and a following paragraph
break
- m = break_sc.match(state.src, state.cursor)
- if m:
- end_pos = self.parse_method(m, state)
+ m4 = break_sc.match(state.src, state.cursor)
+ if m4:
+ end_pos = self.parse_method(m4, state)
if end_pos:
break
@@ -349,7 +357,7 @@ def extract_block_quote(self, m: Match, state: BlockState) -> Tuple[str, int]:
# treated as 4 spaces
return expand_tab(text), end_pos
- def parse_block_quote(self, m: Match, state: BlockState) -> int:
+ def parse_block_quote(self, m: Match[str], state: BlockState) -> int:
"""Parse token for block quote. Here is an example of the syntax:
.. code-block:: markdown
@@ -374,14 +382,14 @@ def parse_block_quote(self, m: Match, state: BlockState) -> int:
state.append_token(token)
return state.cursor
- def parse_list(self, m: Match, state: BlockState) -> int:
+ def parse_list(self, m: Match[str], state: BlockState) -> int:
"""Parse tokens for ordered and unordered list."""
return parse_list(self, m, state)
- def parse_block_html(self, m: Match, state: BlockState) -> Optional[int]:
+ def parse_block_html(self, m: Match[str], state: BlockState) -> Optional[int]:
return self.parse_raw_html(m, state)
- def parse_raw_html(self, m: Match, state: BlockState) -> Optional[int]:
+ def parse_raw_html(self, m: Match[str], state: BlockState) -> Optional[int]:
marker = m.group(0).strip()
# rule 2
@@ -429,6 +437,8 @@ def parse_raw_html(self, m: Match, state: BlockState) -> Optional[int]:
(close_tag and _CLOSE_TAG_END.match(state.src, start_pos, end_pos)):
return _parse_html_to_newline(state, self.BLANK_LINE)
+ return None
+
def parse(self, state: BlockState, rules: Optional[List[str]]=None) -> None:
sc = self.compile_sc(rules)
@@ -443,14 +453,14 @@ def parse(self, state: BlockState, rules: Optional[List[str]]=None) -> None:
state.add_paragraph(text)
state.cursor = end_pos
- end_pos = self.parse_method(m, state)
- if end_pos:
- state.cursor = end_pos
+ end_pos2 = self.parse_method(m, state)
+ if end_pos2:
+ state.cursor = end_pos2
else:
- end_pos = state.find_line_end()
- text = state.get_text(end_pos)
+ end_pos3 = state.find_line_end()
+ text = state.get_text(end_pos3)
state.add_paragraph(text)
- state.cursor = end_pos
+ state.cursor = end_pos3
if state.cursor < state.cursor_max:
text = state.src[state.cursor:]
@@ -458,7 +468,7 @@ def parse(self, state: BlockState, rules: Optional[List[str]]=None) -> None:
state.cursor = state.cursor_max
-def _parse_html_to_end(state, end_marker, start_pos):
+def _parse_html_to_end(state: BlockState, end_marker: str, start_pos: int) -> int:
marker_pos = state.src.find(end_marker, start_pos)
if marker_pos == -1:
text = state.src[state.cursor:]
@@ -473,7 +483,7 @@ def _parse_html_to_end(state, end_marker, start_pos):
return end_pos
-def _parse_html_to_newline(state, newline):
+def _parse_html_to_newline(state: BlockState, newline: Pattern[str]) -> int:
m = newline.search(state.src, state.cursor)
if m:
end_pos = m.start()
diff --git a/src/mistune/core.py b/src/mistune/core.py
index 2bbe92b..75715fa 100644
--- a/src/mistune/core.py
+++ b/src/mistune/core.py
@@ -1,12 +1,39 @@
import re
-from typing import Dict, Any
+from collections.abc import Generator
+from typing import (
+ Any,
+ Callable,
+ ClassVar,
+ Dict,
+ Generic,
+ Iterable,
+ List,
+ Match,
+ MutableMapping,
+ Optional,
+ Pattern,
+ Set,
+ Type,
+ TypeVar,
+ Union,
+ cast,
+)
+from typing_extensions import Self
_LINE_END = re.compile(r'\n|$')
-
class BlockState:
"""The state to save block parser's cursor and tokens."""
- def __init__(self, parent=None):
+
+ src: str
+ tokens: List[Dict[str, Any]]
+ cursor: int
+ cursor_max: int
+ list_tight: bool
+ parent: Any
+ env: MutableMapping[str, Any]
+
+ def __init__(self, parent: Optional[Any] = None) -> None:
self.src = ''
self.tokens = []
@@ -24,49 +51,51 @@ def __init__(self, parent=None):
else:
self.env = {'ref_links': {}}
- def child_state(self, src: str):
+ def child_state(self, src: str) -> 'BlockState':
child = self.__class__(self)
child.process(src)
return child
- def process(self, src: str):
+ def process(self, src: str) -> None:
self.src = src
self.cursor_max = len(src)
- def find_line_end(self):
+ def find_line_end(self) -> int:
m = _LINE_END.search(self.src, self.cursor)
+ assert m is not None
return m.end()
- def get_text(self, end_pos: int):
+ def get_text(self, end_pos: int) -> str:
return self.src[self.cursor:end_pos]
- def last_token(self):
+ def last_token(self) -> Any:
if self.tokens:
return self.tokens[-1]
- def prepend_token(self, token: Dict[str, Any]):
+ def prepend_token(self, token: Dict[str, Any]) -> None:
"""Insert token before the last token."""
self.tokens.insert(len(self.tokens) - 1, token)
- def append_token(self, token: Dict[str, Any]):
+ def append_token(self, token: Dict[str, Any]) -> None:
"""Add token to the end of token list."""
self.tokens.append(token)
- def add_paragraph(self, text: str):
+ def add_paragraph(self, text: str) -> None:
last_token = self.last_token()
if last_token and last_token['type'] == 'paragraph':
last_token['text'] += text
else:
self.tokens.append({'type': 'paragraph', 'text': text})
- def append_paragraph(self):
+ def append_paragraph(self) -> Optional[int]:
last_token = self.last_token()
if last_token and last_token['type'] == 'paragraph':
pos = self.find_line_end()
last_token['text'] += self.get_text(pos)
return pos
+ return None
- def depth(self):
+ def depth(self) -> int:
d = 0
parent = self.parent
while parent:
@@ -77,24 +106,25 @@ def depth(self):
class InlineState:
"""The state to save inline parser's tokens."""
- def __init__(self, env: Dict[str, Any]):
+
+ def __init__(self, env: MutableMapping[str, Any]):
self.env = env
self.src = ''
- self.tokens = []
+ self.tokens: List[Dict[str, Any]] = []
self.in_image = False
self.in_link = False
self.in_emphasis = False
self.in_strong = False
- def prepend_token(self, token: Dict[str, Any]):
+ def prepend_token(self, token: Dict[str, Any]) -> None:
"""Insert token before the last token."""
self.tokens.insert(len(self.tokens) - 1, token)
- def append_token(self, token: Dict[str, Any]):
+ def append_token(self, token: Dict[str, Any]) -> None:
"""Add token to the end of token list."""
self.tokens.append(token)
- def copy(self):
+ def copy(self) -> "InlineState":
"""Create a copy of current state."""
state = self.__class__(self.env)
state.in_image = self.in_image
@@ -104,21 +134,26 @@ def copy(self):
return state
-class Parser:
- sc_flag = re.M
- state_cls = BlockState
+ST = TypeVar("ST", InlineState, BlockState)
+
+class Parser(Generic[ST]):
+ sc_flag: "re._FlagsType" = re.M
+ state_cls: Type[ST]
- SPECIFICATION = {}
- DEFAULT_RULES = []
+ SPECIFICATION: ClassVar[Dict[str, str]] = {}
+ DEFAULT_RULES: ClassVar[Iterable[str]] = []
- def __init__(self):
+ def __init__(self) -> None:
self.specification = self.SPECIFICATION.copy()
self.rules = list(self.DEFAULT_RULES)
- self._methods = {}
+ self._methods: Dict[
+ str,
+ Callable[[Match[str], ST], Optional[int]],
+ ] = {}
- self.__sc = {}
+ self.__sc: Dict[str, Pattern[str]] = {}
- def compile_sc(self, rules=None):
+ def compile_sc(self, rules: Optional[List[str]] = None) -> Pattern[str]:
if rules is None:
key = '$'
rules = self.rules
@@ -134,7 +169,13 @@ def compile_sc(self, rules=None):
self.__sc[key] = sc
return sc
- def register(self, name: str, pattern, func, before=None):
+ def register(
+ self,
+ name: str,
+ pattern: Union[str, None],
+ func: Callable[[Self, Match[str], ST], Optional[int]],
+ before: Optional[str] = None,
+ ) -> None:
"""Register a new rule to parse the token. This method is usually used to
create a new plugin.
@@ -149,11 +190,11 @@ def register(self, name: str, pattern, func, before=None):
if name not in self.rules:
self.insert_rule(self.rules, name, before=before)
- def register_rule(self, name, pattern, func):
+ def register_rule(self, name: str, pattern: str, func: Any) -> None:
raise DeprecationWarning('This plugin is not compatible with mistune v3.')
@staticmethod
- def insert_rule(rules, name, before=None):
+ def insert_rule(rules: List[str], name: str, before: Optional[str] = None) -> None:
if before:
try:
index = rules.index(before)
@@ -163,18 +204,20 @@ def insert_rule(rules, name, before=None):
else:
rules.append(name)
- def parse_method(self, m, state):
- func = self._methods[m.lastgroup]
+ def parse_method(self, m: Match[str], state: ST) -> Optional[int]:
+ lastgroup = m.lastgroup
+ assert lastgroup
+ func = self._methods[lastgroup]
return func(m, state)
class BaseRenderer(object):
- NAME = 'base'
+ NAME: ClassVar[str] = "base"
- def __init__(self):
- self.__methods = {}
+ def __init__(self) -> None:
+ self.__methods: Dict[str, Callable[..., str]] = {}
- def register(self, name: str, method):
+ def register(self, name: str, method: Callable[..., str]) -> None:
"""Register a render method for the named token. For example::
def render_wiki(renderer, key, title):
@@ -185,25 +228,27 @@ def render_wiki(renderer, key, title):
# bind self into renderer method
self.__methods[name] = lambda *arg, **kwargs: method(self, *arg, **kwargs)
- def _get_method(self, name):
+ def _get_method(self, name: str) -> Callable[..., str]:
try:
- return object.__getattribute__(self, name)
+ return cast(Callable[..., str], object.__getattribute__(self, name))
except AttributeError:
method = self.__methods.get(name)
if not method:
raise AttributeError('No renderer "{!r}"'.format(name))
return method
- def render_token(self, token, state):
+ def render_token(self, token: Dict[str, Any], state: BlockState) -> str:
func = self._get_method(token['type'])
return func(token, state)
- def iter_tokens(self, tokens, state):
+ def iter_tokens(
+ self, tokens: Iterable[Dict[str, Any]], state: BlockState
+ ) -> Iterable[str]:
for tok in tokens:
yield self.render_token(tok, state)
- def render_tokens(self, tokens, state):
+ def render_tokens(self, tokens: Iterable[Dict[str, Any]], state: BlockState) -> str:
return ''.join(self.iter_tokens(tokens, state))
- def __call__(self, tokens, state):
+ def __call__(self, tokens: Iterable[Dict[str, Any]], state: BlockState) -> str:
return self.render_tokens(tokens, state)
diff --git a/src/mistune/directives/__init__.py b/src/mistune/directives/__init__.py
index 660c4c8..521f174 100644
--- a/src/mistune/directives/__init__.py
+++ b/src/mistune/directives/__init__.py
@@ -1,14 +1,16 @@
-from ._base import DirectiveParser, BaseDirective, DirectivePlugin
-from ._rst import RSTDirective
+from typing import List
+
+from ._base import BaseDirective, DirectiveParser, DirectivePlugin
from ._fenced import FencedDirective
+from ._rst import RSTDirective
from .admonition import Admonition
-from .toc import TableOfContents
+from .image import Figure, Image
from .include import Include
-from .image import Image, Figure
+from .toc import TableOfContents
+
-
class RstDirective(RSTDirective): # pragma: no cover
- def __init__(self, plugins):
+ def __init__(self, plugins: List[DirectivePlugin]) -> None:
super(RstDirective, self).__init__(plugins)
import warnings
warnings.warn(
diff --git a/src/mistune/directives/_base.py b/src/mistune/directives/_base.py
index ad326c6..49769de 100644
--- a/src/mistune/directives/_base.py
+++ b/src/mistune/directives/_base.py
@@ -1,23 +1,47 @@
import re
-
-
-class DirectiveParser:
+from abc import ABCMeta, abstractmethod
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Callable,
+ Dict,
+ Iterable,
+ List,
+ Match,
+ Optional,
+ Tuple,
+ Type,
+ Union,
+)
+
+if TYPE_CHECKING:
+ from ..block_parser import BlockParser
+ from ..core import BlockState
+ from ..markdown import Markdown
+
+
+class DirectiveParser(ABCMeta):
name = 'directive'
@staticmethod
- def parse_type(m: re.Match):
+ @abstractmethod
+ def parse_type(m: Match[str]) -> str:
raise NotImplementedError()
@staticmethod
- def parse_title(m: re.Match):
+ @abstractmethod
+ def parse_title(m: Match[str]) -> str:
raise NotImplementedError()
@staticmethod
- def parse_content(m: re.Match):
+ @abstractmethod
+ def parse_content(m: Match[str]) -> str:
raise NotImplementedError()
@classmethod
- def parse_tokens(cls, block, text, state):
+ def parse_tokens(
+ cls, block: "BlockParser", text: str, state: "BlockState"
+ ) -> Iterable[Dict[str, Any]]:
if state.depth() >= block.max_nested_level - 1 and cls.name in block.rules:
rules = list(block.rules)
rules.remove(cls.name)
@@ -28,7 +52,7 @@ def parse_tokens(cls, block, text, state):
return child.tokens
@staticmethod
- def parse_options(m: re.Match):
+ def parse_options(m: Match[str]) -> List[Tuple[str, str]]:
text = m.group('options')
if not text.strip():
return []
@@ -45,18 +69,33 @@ def parse_options(m: re.Match):
return options
-class BaseDirective:
- parser = DirectiveParser
- directive_pattern = None
+class BaseDirective(metaclass=ABCMeta):
+ parser: Type[DirectiveParser]
+ directive_pattern: Optional[str] = None
- def __init__(self, plugins):
- self._methods = {}
+ def __init__(self, plugins: List["DirectivePlugin"]):
+ self._methods: Dict[
+ str,
+ Callable[
+ ["BlockParser", Match[str], "BlockState"],
+ Union[Dict[str, Any], List[Dict[str, Any]]],
+ ],
+ ] = {}
self.__plugins = plugins
- def register(self, name, fn):
+ def register(
+ self,
+ name: str,
+ fn: Callable[
+ ["BlockParser", Match[str], "BlockState"],
+ Union[Dict[str, Any], List[Dict[str, Any]]],
+ ],
+ ) -> None:
self._methods[name] = fn
- def parse_method(self, block, m, state):
+ def parse_method(
+ self, block: "BlockParser", m: Match[str], state: "BlockState"
+ ) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
_type = self.parser.parse_type(m)
method = self._methods.get(_type)
if method:
@@ -78,10 +117,15 @@ def parse_method(self, block, m, state):
state.append_token(token)
return token
- def parse_directive(self, block, m, state):
+ @abstractmethod
+ def parse_directive(
+ self, block: "BlockParser", m: Match[str], state: "BlockState"
+ ) -> Optional[int]:
raise NotImplementedError()
- def register_block_parser(self, md, before=None):
+ def register_block_parser(
+ self, md: "Markdown", before: Optional[str] = None
+ ) -> None:
md.block.register(
self.parser.name,
self.directive_pattern,
@@ -89,33 +133,38 @@ def register_block_parser(self, md, before=None):
before=before,
)
- def __call__(self, md):
+ def __call__(self, markdown: "Markdown") -> None:
for plugin in self.__plugins:
plugin.parser = self.parser
- plugin(self, md)
+ plugin(self, markdown)
class DirectivePlugin:
- def __init__(self):
- self.parser = None
+ parser: Type[DirectiveParser]
+
+ def __init__(self) -> None: ...
- def parse_options(self, m: re.Match):
+ def parse_options(self, m: Match[str]) -> List[Tuple[str, str]]:
return self.parser.parse_options(m)
- def parse_type(self, m: re.Match):
+ def parse_type(self, m: Match[str]) -> str:
return self.parser.parse_type(m)
- def parse_title(self, m: re.Match):
+ def parse_title(self, m: Match[str]) -> str:
return self.parser.parse_title(m)
- def parse_content(self, m: re.Match):
+ def parse_content(self, m: Match[str]) -> str:
return self.parser.parse_content(m)
- def parse_tokens(self, block, text, state):
+ def parse_tokens(
+ self, block: "BlockParser", text: str, state: "BlockState"
+ ) -> Iterable[Dict[str, Any]]:
return self.parser.parse_tokens(block, text, state)
- def parse(self, block, m, state):
+ def parse(
+ self, block: "BlockParser", m: Match[str], state: "BlockState"
+ ) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
raise NotImplementedError()
- def __call__(self, md):
+ def __call__(self, directive: BaseDirective, md: "Markdown") -> None:
raise NotImplementedError()
diff --git a/src/mistune/directives/_fenced.py b/src/mistune/directives/_fenced.py
index 818f130..b52b0aa 100644
--- a/src/mistune/directives/_fenced.py
+++ b/src/mistune/directives/_fenced.py
@@ -1,5 +1,13 @@
import re
-from ._base import DirectiveParser, BaseDirective
+from typing import TYPE_CHECKING, List, Match, Optional
+
+from ._base import BaseDirective, DirectiveParser, DirectivePlugin
+
+if TYPE_CHECKING:
+ from ..block_parser import BlockParser
+ from ..core import BlockState
+ from ..markdown import Markdown
+
__all__ = ['FencedDirective']
@@ -16,15 +24,15 @@ class FencedParser(DirectiveParser):
name = 'fenced_directive'
@staticmethod
- def parse_type(m: re.Match):
+ def parse_type(m: Match[str]) -> str:
return m.group('type')
@staticmethod
- def parse_title(m: re.Match):
+ def parse_title(m: Match[str]) -> str:
return m.group('title')
@staticmethod
- def parse_content(m: re.Match):
+ def parse_content(m: Match[str]) -> str:
return m.group('text')
@@ -85,7 +93,7 @@ class FencedDirective(BaseDirective):
"""
parser = FencedParser
- def __init__(self, plugins, markers='`~'):
+ def __init__(self, plugins: List[DirectivePlugin], markers: str = "`~") -> None:
super(FencedDirective, self).__init__(plugins)
self.markers = markers
_marker_pattern = '|'.join(re.escape(c) for c in markers)
@@ -94,7 +102,9 @@ def __init__(self, plugins, markers='`~'):
r'\{[a-zA-Z0-9_-]+\}'
)
- def _process_directive(self, block, marker, start, state):
+ def _process_directive(
+ self, block: "BlockParser", marker: str, start: int, state: "BlockState"
+ ) -> Optional[int]:
mlen = len(marker)
cursor_start = start + len(marker)
@@ -114,17 +124,21 @@ def _process_directive(self, block, marker, start, state):
m = _directive_re.match(text)
if not m:
- return
+ return None
self.parse_method(block, m, state)
return end_pos
- def parse_directive(self, block, m, state):
- marker = m.group('fenced_directive_mark')
+ def parse_directive(
+ self, block: "BlockParser", m: Match[str], state: "BlockState"
+ ) -> Optional[int]:
+ marker = m.group("fenced_directive_mark")
return self._process_directive(block, marker, m.start(), state)
- def parse_fenced_code(self, block, m, state):
- info = m.group('fenced_3')
+ def parse_fenced_code(
+ self, block: "BlockParser", m: Match[str], state: "BlockState"
+ ) -> Optional[int]:
+ info = m.group("fenced_3")
if not info or not _type_re.match(info):
return block.parse_fenced_code(m, state)
@@ -134,7 +148,7 @@ def parse_fenced_code(self, block, m, state):
marker = m.group('fenced_2')
return self._process_directive(block, marker, m.start(), state)
- def __call__(self, md):
+ def __call__(self, md: "Markdown") -> None:
super(FencedDirective, self).__call__(md)
if self.markers == '`~':
md.block.register('fenced_code', None, self.parse_fenced_code)
diff --git a/src/mistune/directives/_rst.py b/src/mistune/directives/_rst.py
index 6e054cf..bbd0a8d 100644
--- a/src/mistune/directives/_rst.py
+++ b/src/mistune/directives/_rst.py
@@ -1,5 +1,12 @@
import re
-from ._base import DirectiveParser, BaseDirective
+from typing import TYPE_CHECKING, Match, Optional
+
+from ._base import BaseDirective, DirectiveParser
+
+if TYPE_CHECKING:
+ from ..block_parser import BlockParser
+ from ..core import BlockState
+ from ..markdown import Markdown
__all__ = ['RSTDirective']
@@ -15,15 +22,15 @@ class RSTParser(DirectiveParser):
name = 'rst_directive'
@staticmethod
- def parse_type(m: re.Match):
+ def parse_type(m: Match[str]) -> str:
return m.group('type')
@staticmethod
- def parse_title(m: re.Match):
+ def parse_title(m: Match[str]) -> str:
return m.group('title')
@staticmethod
- def parse_content(m: re.Match):
+ def parse_content(m: Match[str]) -> str:
full_content = m.group(0)
text = m.group('text')
pretext = full_content[:-len(text)]
@@ -60,14 +67,16 @@ class RSTDirective(BaseDirective):
parser = RSTParser
directive_pattern = r'^\.\. +[a-zA-Z0-9_-]+\:\:'
- def parse_directive(self, block, m, state):
- m = _directive_re.match(state.src, state.cursor)
- if not m:
- return
+ def parse_directive(
+ self, block: "BlockParser", m: Match[str], state: "BlockState"
+ ) -> Optional[int]:
+ m2 = _directive_re.match(state.src, state.cursor)
+ if not m2:
+ return None
- self.parse_method(block, m, state)
- return m.end()
+ self.parse_method(block, m2, state)
+ return m2.end()
- def __call__(self, md):
- super(RSTDirective, self).__call__(md)
- self.register_block_parser(md)
+ def __call__(self, markdown: "Markdown") -> None:
+ super(RSTDirective, self).__call__(markdown)
+ self.register_block_parser(markdown)
diff --git a/src/mistune/directives/admonition.py b/src/mistune/directives/admonition.py
index b380611..f0d65e3 100644
--- a/src/mistune/directives/admonition.py
+++ b/src/mistune/directives/admonition.py
@@ -1,4 +1,11 @@
-from ._base import DirectivePlugin
+from typing import TYPE_CHECKING, Any, Dict, List, Match, Optional
+
+from ._base import BaseDirective, DirectivePlugin
+
+if TYPE_CHECKING:
+ from ..block_parser import BlockParser
+ from ..core import BlockState
+ from ..markdown import Markdown
class Admonition(DirectivePlugin):
@@ -7,7 +14,9 @@ class Admonition(DirectivePlugin):
"important", "note", "tip", "warning",
}
- def parse(self, block, m, state):
+ def parse(
+ self, block: "BlockParser", m: Match[str], state: "BlockState"
+ ) -> Dict[str, Any]:
name = self.parse_type(m)
attrs = {'name': name}
options = dict(self.parse_options(m))
@@ -35,17 +44,18 @@ def parse(self, block, m, state):
'attrs': attrs,
}
- def __call__(self, directive, md):
+ def __call__(self, directive: "BaseDirective", md: "Markdown") -> None:
for name in self.SUPPORTED_NAMES:
directive.register(name, self.parse)
+ assert md.renderer is not None
if md.renderer.NAME == 'html':
md.renderer.register('admonition', render_admonition)
md.renderer.register('admonition_title', render_admonition_title)
md.renderer.register('admonition_content', render_admonition_content)
-def render_admonition(self, text, name, **attrs):
+def render_admonition(self: Any, text: str, name: str, **attrs: Any) -> str:
html = ' Optional[List[Dict[str, Any]]]:
content = self.parse_content(m)
if not content:
- return
+ return None
- tokens = self.parse_tokens(block, content, state)
+ tokens = list(self.parse_tokens(block, content, state))
caption = tokens[0]
if caption['type'] == 'paragraph':
caption['type'] = 'figcaption'
@@ -97,8 +118,11 @@ def parse_directive_content(self, block, m, state):
'children': tokens[1:]
})
return children
+ return None
- def parse(self, block, m, state):
+ def parse(
+ self, block: "BlockParser", m: Match[str], state: "BlockState"
+ ) -> Dict[str, Any]:
options = dict(self.parse_options(m))
image_attrs = _parse_attrs(options)
image_attrs['src'] = self.parse_title(m)
@@ -121,9 +145,10 @@ def parse(self, block, m, state):
'children': children,
}
- def __call__(self, directive, md):
+ def __call__(self, directive: "BaseDirective", md: "Markdown") -> None:
directive.register(self.NAME, self.parse)
+ assert md.renderer is not None
if md.renderer.NAME == 'html':
md.renderer.register('figure', render_figure)
md.renderer.register('block_image', render_block_image)
@@ -131,8 +156,14 @@ def __call__(self, directive, md):
md.renderer.register('legend', render_legend)
-def render_figure(self, text, align=None, figwidth=None, figclass=None):
- _cls = 'figure'
+def render_figure(
+ self: Any,
+ text: str,
+ align: Optional[str] = None,
+ figwidth: Optional[str] = None,
+ figclass: Optional[str] = None,
+) -> str:
+ _cls = "figure"
if align:
_cls += ' align-' + align
if figclass:
@@ -144,9 +175,9 @@ def render_figure(self, text, align=None, figwidth=None, figclass=None):
return html + '>\n' + text + '\n'
-def render_figcaption(self, text):
+def render_figcaption(self: Any, text: str) -> str:
return '
\n' + text + '
\n'
+def render_html_include(renderer: "BaseRenderer", text: str, **attrs: Any) -> str:
+ return '\n' + text + "
\n"
diff --git a/src/mistune/directives/toc.py b/src/mistune/directives/toc.py
index 4084f43..7945dbc 100644
--- a/src/mistune/directives/toc.py
+++ b/src/mistune/directives/toc.py
@@ -13,19 +13,28 @@
heading levels writers want to include in the table of contents.
"""
-from ._base import DirectivePlugin
+from typing import TYPE_CHECKING, Any, Dict, Match
+
from ..toc import normalize_toc_item, render_toc_ul
+from ._base import BaseDirective, DirectivePlugin
+
+if TYPE_CHECKING:
+ from ..block_parser import BlockParser
+ from ..core import BaseRenderer, BlockState
+ from ..markdown import Markdown
class TableOfContents(DirectivePlugin):
- def __init__(self, min_level=1, max_level=3):
+ def __init__(self, min_level: int = 1, max_level: int = 3) -> None:
self.min_level = min_level
self.max_level = max_level
- def generate_heading_id(self, token, index):
+ def generate_heading_id(self, token: Dict[str, Any], index: int) -> str:
return 'toc_' + str(index + 1)
- def parse(self, block, m, state):
+ def parse(
+ self, block: "BlockParser", m: Match[str], state: "BlockState"
+ ) -> Dict[str, Any]:
title = self.parse_title(m)
options = self.parse_options(m)
if options:
@@ -51,7 +60,7 @@ def parse(self, block, m, state):
}
return {'type': 'toc', 'text': title or '', 'attrs': attrs}
- def toc_hook(self, md, state):
+ def toc_hook(self, md: "Markdown", state: "BlockState") -> None:
sections = []
headings = []
@@ -74,15 +83,17 @@ def toc_hook(self, md, state):
toc = [item for item in toc_items if _min <= item[0] <= _max]
sec['attrs']['toc'] = toc
- def __call__(self, directive, md):
- if md.renderer and md.renderer.NAME == 'html':
+ def __call__(self, directive: BaseDirective, md: "Markdown") -> None:
+ if md.renderer and md.renderer.NAME == "html":
# only works with HTML renderer
directive.register('toc', self.parse)
md.before_render_hooks.append(self.toc_hook)
md.renderer.register('toc', render_html_toc)
-def render_html_toc(renderer, title, collapse=False, **attrs):
+def render_html_toc(
+ renderer: "BaseRenderer", title: str, collapse: bool = False, **attrs: Any
+) -> str:
if not title:
title = 'Table of Contents'
toc = attrs['toc']
@@ -95,7 +106,7 @@ def render_html_toc(renderer, title, collapse=False, **attrs):
return html + content + '\n'
-def _normalize_level(options, name, default):
+def _normalize_level(options: Dict[str, Any], name: str, default: Any) -> Any:
level = options.get(name)
if not level:
return default
diff --git a/src/mistune/helpers.py b/src/mistune/helpers.py
index 04c1df1..be73c70 100644
--- a/src/mistune/helpers.py
+++ b/src/mistune/helpers.py
@@ -1,5 +1,7 @@
import re
import string
+from typing import Any, Dict, Tuple, Union
+
from .util import escape_url
PREVENT_BACKSLASH = r'(? str:
return _ESCAPE_CHAR_RE.sub(r'\1', text)
-def parse_link_text(src, pos):
+def parse_link_text(src: str, pos: int) -> Union[Tuple[str, int], Tuple[None, None]]:
level = 1
found = False
start_pos = pos
@@ -77,7 +79,9 @@ def parse_link_text(src, pos):
return None, None
-def parse_link_label(src, start_pos):
+def parse_link_label(
+ src: str, start_pos: int
+) -> Union[Tuple[str, int], Tuple[None, None]]:
m = _INLINE_LINK_LABEL_RE.match(src, start_pos)
if m:
label = m.group(0)[:-1]
@@ -85,7 +89,9 @@ def parse_link_label(src, start_pos):
return None, None
-def parse_link_href(src, start_pos, block=False):
+def parse_link_href(
+ src: str, start_pos: int, block: bool = False
+) -> Union[Tuple[str, int], Tuple[None, None]]:
m = LINK_BRACKET_START.match(src, start_pos)
if m:
start_pos = m.end() - 1
@@ -110,7 +116,9 @@ def parse_link_href(src, start_pos, block=False):
return href, end_pos - 1
-def parse_link_title(src, start_pos, max_pos):
+def parse_link_title(
+ src: str, start_pos: int, max_pos: int
+) -> Union[Tuple[str, int], Tuple[None, None]]:
m = LINK_TITLE_RE.match(src, start_pos, max_pos)
if m:
title = m.group(1)[1:-1]
@@ -119,11 +127,13 @@ def parse_link_title(src, start_pos, max_pos):
return None, None
-def parse_link(src, pos):
+def parse_link(
+ src: str, pos: int
+) -> Union[Tuple[Dict[str, Any], int], Tuple[None, None]]:
href, href_pos = parse_link_href(src, pos)
if href is None:
return None, None
-
+ assert href_pos is not None
title, title_pos = parse_link_title(src, href_pos, len(src))
next_pos = title_pos or href_pos
m = PAREN_END_RE.match(src, next_pos)
diff --git a/src/mistune/inline_parser.py b/src/mistune/inline_parser.py
index 1fd961d..e14fb6d 100644
--- a/src/mistune/inline_parser.py
+++ b/src/mistune/inline_parser.py
@@ -1,21 +1,34 @@
import re
-from typing import Optional, List, Dict, Any, Match
-from .core import Parser, InlineState
-from .util import (
- escape,
- escape_url,
- unikey,
+from typing import (
+ Any,
+ ClassVar,
+ Dict,
+ Generic,
+ Iterator,
+ List,
+ Match,
+ MutableMapping,
+ Optional,
+ Pattern,
+ Tuple,
+ TypeVar,
+ Union,
)
+
+from typing_extensions import Literal
+
+from .core import InlineState, Parser
from .helpers import (
+ HTML_ATTRIBUTES,
+ HTML_TAGNAME,
PREVENT_BACKSLASH,
PUNCTUATION,
- HTML_TAGNAME,
- HTML_ATTRIBUTES,
- unescape_char,
parse_link,
parse_link_label,
parse_link_text,
+ unescape_char,
)
+from .util import escape, escape_url, unikey
PAREN_END_RE = re.compile(r'\s*\)')
@@ -46,7 +59,7 @@
}
-class InlineParser(Parser):
+class InlineParser(Parser[InlineState]):
sc_flag = 0
state_cls = InlineState
@@ -93,7 +106,7 @@ class InlineParser(Parser):
'linebreak',
)
- def __init__(self, hard_wrap: bool=False):
+ def __init__(self, hard_wrap: bool = False) -> None:
super(InlineParser, self).__init__()
self.hard_wrap = hard_wrap
@@ -107,7 +120,7 @@ def __init__(self, hard_wrap: bool=False):
name: getattr(self, 'parse_' + name) for name in self.rules
}
- def parse_escape(self, m: Match, state: InlineState) -> int:
+ def parse_escape(self, m: Match[str], state: InlineState) -> int:
text = m.group(0)
text = unescape_char(text)
state.append_token({
@@ -116,7 +129,7 @@ def parse_escape(self, m: Match, state: InlineState) -> int:
})
return m.end()
- def parse_link(self, m: Match, state: InlineState) -> Optional[int]:
+ def parse_link(self, m: Match[str], state: InlineState) -> Optional[int]:
pos = m.end()
marker = m.group(0)
@@ -133,13 +146,17 @@ def parse_link(self, m: Match, state: InlineState) -> Optional[int]:
if label is None:
text, end_pos = parse_link_text(state.src, pos)
if text is None:
- return
+ return None
+
+ assert end_pos is not None
if text is None:
text = label
+ assert text is not None
+
if end_pos >= len(state.src) and label is None:
- return
+ return None
rules = ['codespan', 'prec_auto_link', 'prec_inline_html']
prec_pos = self.precedence_scan(m, state, end_pos, rules)
@@ -165,11 +182,11 @@ def parse_link(self, m: Match, state: InlineState) -> Optional[int]:
label = label2
if label is None:
- return
+ return None
ref_links = state.env.get('ref_links')
if not ref_links:
- return
+ return None
key = unikey(label)
env = ref_links.get(key)
@@ -180,8 +197,15 @@ def parse_link(self, m: Match, state: InlineState) -> Optional[int]:
token['label'] = label
state.append_token(token)
return end_pos
-
- def __parse_link_token(self, is_image, text, attrs, state):
+ return None
+
+ def __parse_link_token(
+ self,
+ is_image: bool,
+ text: str,
+ attrs: Optional[Dict[str, Any]],
+ state: InlineState,
+ ) -> Dict[str, Any]:
new_state = state.copy()
new_state.src = text
if is_image:
@@ -200,7 +224,7 @@ def __parse_link_token(self, is_image, text, attrs, state):
}
return token
- def parse_auto_link(self, m: Match, state: InlineState) -> int:
+ def parse_auto_link(self, m: Match[str], state: InlineState) -> int:
text = m.group(0)
pos = m.end()
if state.in_link:
@@ -211,7 +235,7 @@ def parse_auto_link(self, m: Match, state: InlineState) -> int:
self._add_auto_link(text, text, state)
return pos
- def parse_auto_email(self, m: Match, state: InlineState) -> int:
+ def parse_auto_email(self, m: Match[str], state: InlineState) -> int:
text = m.group(0)
pos = m.end()
if state.in_link:
@@ -223,14 +247,14 @@ def parse_auto_email(self, m: Match, state: InlineState) -> int:
self._add_auto_link(url, text, state)
return pos
- def _add_auto_link(self, url, text, state):
+ def _add_auto_link(self, url: str, text: str, state: InlineState) -> None:
state.append_token({
'type': 'link',
'children': [{'type': 'text', 'raw': text}],
'attrs': {'url': escape_url(url)},
})
- def parse_emphasis(self, m: Match, state: InlineState) -> int:
+ def parse_emphasis(self, m: Match[str], state: InlineState) -> int:
pos = m.end()
marker = m.group(0)
@@ -279,17 +303,17 @@ def parse_emphasis(self, m: Match, state: InlineState) -> int:
})
return end_pos
- def parse_codespan(self, m: Match, state: InlineState) -> int:
+ def parse_codespan(self, m: Match[str], state: InlineState) -> int:
marker = m.group(0)
# require same marker with same length at end
pattern = re.compile(r'(.*?[^`])' + marker + r'(?!`)', re.S)
pos = m.end()
- m = pattern.match(state.src, pos)
- if m:
- end_pos = m.end()
- code = m.group(1)
+ m2 = pattern.match(state.src, pos)
+ if m2:
+ end_pos = m2.end()
+ code = m2.group(1)
# Line endings are treated like spaces
code = code.replace('\n', ' ')
if len(code.strip()):
@@ -301,15 +325,15 @@ def parse_codespan(self, m: Match, state: InlineState) -> int:
state.append_token({'type': 'text', 'raw': marker})
return pos
- def parse_linebreak(self, m: Match, state: InlineState) -> int:
+ def parse_linebreak(self, m: Match[str], state: InlineState) -> int:
state.append_token({'type': 'linebreak'})
return m.end()
- def parse_softbreak(self, m: Match, state: InlineState) -> int:
+ def parse_softbreak(self, m: Match[str], state: InlineState) -> int:
state.append_token({'type': 'softbreak'})
return m.end()
- def parse_inline_html(self, m: Match, state: InlineState) -> int:
+ def parse_inline_html(self, m: Match[str], state: InlineState) -> int:
end_pos = m.end()
html = m.group(0)
state.append_token({'type': 'inline_html', 'raw': html})
@@ -319,7 +343,7 @@ def parse_inline_html(self, m: Match, state: InlineState) -> int:
state.in_link = False
return end_pos
- def process_text(self, text: str, state: InlineState):
+ def process_text(self, text: str, state: InlineState) -> None:
state.append_token({'type': 'text', 'raw': text})
def parse(self, state: InlineState) -> List[Dict[str, Any]]:
@@ -351,7 +375,13 @@ def parse(self, state: InlineState) -> List[Dict[str, Any]]:
self.process_text(state.src[pos:], state)
return state.tokens
- def precedence_scan(self, m: Match, state: InlineState, end_pos: int, rules=None):
+ def precedence_scan(
+ self,
+ m: Match[str],
+ state: InlineState,
+ end_pos: int,
+ rules: Optional[List[str]] = None,
+ ) -> Optional[int]:
if rules is None:
rules = ['codespan', 'link', 'prec_auto_link', 'prec_inline_html']
@@ -359,20 +389,23 @@ def precedence_scan(self, m: Match, state: InlineState, end_pos: int, rules=None
sc = self.compile_sc(rules)
m1 = sc.search(state.src, mark_pos, end_pos)
if not m1:
- return
+ return None
- rule_name = m1.lastgroup.replace('prec_', '')
+ lastgroup = m1.lastgroup
+ if not lastgroup:
+ return None
+ rule_name = lastgroup.replace("prec_", "")
sc = self.compile_sc([rule_name])
m2 = sc.match(state.src, m1.start())
if not m2:
- return
+ return None
func = self._methods[rule_name]
new_state = state.copy()
new_state.src = state.src
m2_pos = func(m2, new_state)
if not m2_pos or m2_pos < end_pos:
- return
+ return None
raw_text = state.src[m.start():m2.start()]
state.append_token({'type': 'text', 'raw': raw_text})
@@ -380,11 +413,11 @@ def precedence_scan(self, m: Match, state: InlineState, end_pos: int, rules=None
state.append_token(token)
return m2_pos
- def render(self, state: InlineState):
+ def render(self, state: InlineState) -> List[Dict[str, Any]]:
self.parse(state)
return state.tokens
- def __call__(self, s, env):
+ def __call__(self, s: str, env: MutableMapping[str, Any]) -> List[Dict[str, Any]]:
state = self.state_cls(env)
state.src = s
return self.render(state)
diff --git a/src/mistune/list_parser.py b/src/mistune/list_parser.py
index a78e1a5..4bb2875 100644
--- a/src/mistune/list_parser.py
+++ b/src/mistune/list_parser.py
@@ -1,11 +1,15 @@
+"""because list is complex, split list parser in a new file"""
+
import re
-from .core import BlockState
-from .util import (
- strip_end,
- expand_tab,
- expand_leading_tab,
-)
-# because list is complex, split list parser in a new file
+from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple, Match
+
+from typing_extensions import Literal
+
+from .util import expand_leading_tab, expand_tab, strip_end
+
+if TYPE_CHECKING:
+ from .block_parser import BlockParser
+ from .core import BlockState
LIST_PATTERN = (
r'^(?P\n' + text + '
\n'
+def render_def_list(renderer: "BaseRenderer", text: str) -> str:
+ return "\n" + text + "
\n"
-def render_def_list_head(renderer, text):
- return '\n' + text + '
\n\n' + text + "
\n
' + text + '
\n' - def heading(self, text: str, level: int, **attrs) -> str: + def heading(self, text: str, level: int, **attrs: Any) -> str: tag = 'h' + str(level) html = '<' + tag _id = attrs.get('id') @@ -118,7 +127,7 @@ def thematic_break(self) -> str: def block_text(self, text: str) -> str: return text - def block_code(self, code: str, info=None) -> str: + def block_code(self, code: str, info: Optional[str] = None) -> str: html = ' str:
def block_error(self, text: str) -> str:
return '' + text + '
\n'
- def list(self, text: str, ordered: bool, **attrs) -> str:
+ def list(self, text: str, ordered: bool, **attrs: Any) -> str:
if ordered:
html = ' str:
out = self.render_tokens(tokens, state)
# special handle for line breaks
out += '\n\n'.join(self.render_referrences(state)) + '\n'
return strip_end(out)
- def render_referrences(self, state: BlockState):
+ def render_referrences(self, state: BlockState) -> Iterable[str]:
ref_links = state.env['ref_links']
for key in ref_links:
attrs = ref_links[key]
@@ -28,12 +29,12 @@ def render_referrences(self, state: BlockState):
text += ' "' + title + '"'
yield text
- def render_children(self, token, state: BlockState):
+ def render_children(self, token: Dict[str, Any], state: BlockState) -> str:
children = token['children']
return self.render_tokens(children, state)
def text(self, token: Dict[str, Any], state: BlockState) -> str:
- return token['raw']
+ return cast(str, token["raw"])
def emphasis(self, token: Dict[str, Any], state: BlockState) -> str:
return '*' + self.render_children(token, state) + '*'
@@ -42,7 +43,7 @@ def strong(self, token: Dict[str, Any], state: BlockState) -> str:
return '**' + self.render_children(token, state) + '**'
def link(self, token: Dict[str, Any], state: BlockState) -> str:
- label = token.get('label')
+ label = cast(str, token.get("label"))
text = self.render_children(token, state)
out = '[' + text + ']'
if label:
@@ -69,7 +70,7 @@ def image(self, token: Dict[str, Any], state: BlockState) -> str:
return '!' + self.link(token, state)
def codespan(self, token: Dict[str, Any], state: BlockState) -> str:
- return '`' + token['raw'] + '`'
+ return "`" + cast(str, token["raw"]) + "`"
def linebreak(self, token: Dict[str, Any], state: BlockState) -> str:
return ' \n'
@@ -81,15 +82,15 @@ def blank_line(self, token: Dict[str, Any], state: BlockState) -> str:
return ''
def inline_html(self, token: Dict[str, Any], state: BlockState) -> str:
- return token['raw']
+ return cast(str, token["raw"])
def paragraph(self, token: Dict[str, Any], state: BlockState) -> str:
text = self.render_children(token, state)
return text + '\n\n'
def heading(self, token: Dict[str, Any], state: BlockState) -> str:
- level = token['attrs']['level']
- marker = '#' * level
+ level = cast(int, token["attrs"]["level"])
+ marker = "#" * level
text = self.render_children(token, state)
return marker + ' ' + text + '\n\n'
@@ -100,23 +101,24 @@ def block_text(self, token: Dict[str, Any], state: BlockState) -> str:
return self.render_children(token, state) + '\n'
def block_code(self, token: Dict[str, Any], state: BlockState) -> str:
- attrs = token.get('attrs', {})
- info = attrs.get('info', '')
- code = token['raw']
- if code and code[-1] != '\n':
- code += '\n'
+ attrs = token.get("attrs", {})
+ info = cast(str, attrs.get("info", ""))
+ code = cast(str, token["raw"])
+ if code and code[-1] != "\n":
+ code += "\n"
marker = token.get('marker')
if not marker:
marker = _get_fenced_marker(code)
- return marker + info + '\n' + code + marker + '\n\n'
+ marker2 = cast(str, marker)
+ return marker2 + info + "\n" + code + marker2 + "\n\n"
def block_quote(self, token: Dict[str, Any], state: BlockState) -> str:
text = indent(self.render_children(token, state), '> ')
return text + '\n\n'
def block_html(self, token: Dict[str, Any], state: BlockState) -> str:
- return token['raw'] + '\n\n'
+ return cast(str, token["raw"]) + "\n\n"
def block_error(self, token: Dict[str, Any], state: BlockState) -> str:
return ''
@@ -125,7 +127,7 @@ def list(self, token: Dict[str, Any], state: BlockState) -> str:
return render_list(self, token, state)
-def _get_fenced_marker(code):
+def _get_fenced_marker(code: str) -> str:
found = fenced_re.findall(code)
if not found:
return '```'
diff --git a/src/mistune/renderers/rst.py b/src/mistune/renderers/rst.py
index fa12c21..7022d15 100644
--- a/src/mistune/renderers/rst.py
+++ b/src/mistune/renderers/rst.py
@@ -1,8 +1,9 @@
-from typing import Dict, Any
from textwrap import indent
-from ._list import render_list
+from typing import Any, Dict, Iterable, List, cast
+
from ..core import BaseRenderer, BlockState
from ..util import strip_end
+from ._list import render_list
class RSTRenderer(BaseRenderer):
@@ -20,7 +21,9 @@ class RSTRenderer(BaseRenderer):
}
INLINE_IMAGE_PREFIX = 'img-'
- def iter_tokens(self, tokens, state):
+ def iter_tokens(
+ self, tokens: Iterable[Dict[str, Any]], state: BlockState
+ ) -> Iterable[str]:
prev = None
for tok in tokens:
# ignore blank line
@@ -30,14 +33,14 @@ def iter_tokens(self, tokens, state):
prev = tok
yield self.render_token(tok, state)
- def __call__(self, tokens, state: BlockState):
+ def __call__(self, tokens: Iterable[Dict[str, Any]], state: BlockState) -> str:
state.env['inline_images'] = []
out = self.render_tokens(tokens, state)
# special handle for line breaks
out += '\n\n'.join(self.render_referrences(state)) + '\n'
return strip_end(out)
- def render_referrences(self, state: BlockState):
+ def render_referrences(self, state: BlockState) -> Iterable[str]:
images = state.env['inline_images']
for index, token in enumerate(images):
attrs = token['attrs']
@@ -45,13 +48,13 @@ def render_referrences(self, state: BlockState):
ident = self.INLINE_IMAGE_PREFIX + str(index)
yield '.. |' + ident + '| image:: ' + attrs['url'] + '\n :alt: ' + alt
- def render_children(self, token, state: BlockState):
+ def render_children(self, token: Dict[str, Any], state: BlockState) -> str:
children = token['children']
return self.render_tokens(children, state)
def text(self, token: Dict[str, Any], state: BlockState) -> str:
- text = token['raw']
- return text.replace('|', r'\|')
+ text = cast(str, token["raw"])
+ return text.replace("|", r"\|")
def emphasis(self, token: Dict[str, Any], state: BlockState) -> str:
return '*' + self.render_children(token, state) + '*'
@@ -62,16 +65,16 @@ def strong(self, token: Dict[str, Any], state: BlockState) -> str:
def link(self, token: Dict[str, Any], state: BlockState) -> str:
attrs = token['attrs']
text = self.render_children(token, state)
- return '`' + text + ' <' + attrs['url'] + '>`__'
+ return "`" + text + " <" + cast(str, attrs["url"]) + ">`__"
def image(self, token: Dict[str, Any], state: BlockState) -> str:
- refs: list = state.env['inline_images']
+ refs: List[Dict[str, Any]] = state.env["inline_images"]
index = len(refs)
refs.append(token)
return '|' + self.INLINE_IMAGE_PREFIX + str(index) + '|'
def codespan(self, token: Dict[str, Any], state: BlockState) -> str:
- return '``' + token['raw'] + '``'
+ return "``" + cast(str, token["raw"]) + "``"
def linebreak(self, token: Dict[str, Any], state: BlockState) -> str:
return ''
@@ -87,10 +90,10 @@ def paragraph(self, token: Dict[str, Any], state: BlockState) -> str:
children = token['children']
if len(children) == 1 and children[0]['type'] == 'image':
image = children[0]
- attrs = image['attrs']
- title = attrs.get('title')
+ attrs = image["attrs"]
+ title = cast(str, attrs.get("title"))
alt = self.render_children(image, state)
- text = '.. figure:: ' + attrs['url']
+ text = ".. figure:: " + cast(str, attrs["url"])
if title:
text += '\n :alt: ' + title
text += '\n\n' + indent(alt, ' ')
@@ -114,9 +117,9 @@ def block_text(self, token: Dict[str, Any], state: BlockState) -> str:
return self.render_children(token, state) + '\n'
def block_code(self, token: Dict[str, Any], state: BlockState) -> str:
- attrs = token.get('attrs', {})
- info = attrs.get('info')
- code = indent(token['raw'], ' ')
+ attrs = token.get("attrs", {})
+ info = cast(str, attrs.get("info"))
+ code = indent(cast(str, token["raw"]), " ")
if info:
lang = info.split()[0]
return '.. code:: ' + lang + '\n\n' + code + '\n'
diff --git a/src/mistune/toc.py b/src/mistune/toc.py
index 0c5787f..be4b8b3 100644
--- a/src/mistune/toc.py
+++ b/src/mistune/toc.py
@@ -1,7 +1,18 @@
+from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Tuple
+
+from .core import BlockState
from .util import striptags
+if TYPE_CHECKING:
+ from .markdown import Markdown
+
-def add_toc_hook(md, min_level=1, max_level=3, heading_id=None):
+def add_toc_hook(
+ md: "Markdown",
+ min_level: int = 1,
+ max_level: int = 3,
+ heading_id: Optional[Callable[[Dict[str, Any], int], str]] = None,
+) -> None:
"""Add a hook to save toc items into ``state.env``. This is
usually helpful for doc generator::
@@ -21,10 +32,11 @@ def add_toc_hook(md, min_level=1, max_level=3, heading_id=None):
:param heading_id: a function to generate heading_id
"""
if heading_id is None:
- def heading_id(token, index):
+
+ def heading_id(token: Dict[str, Any], index: int) -> str:
return 'toc_' + str(index + 1)
- def toc_hook(md, state):
+ def toc_hook(md: "Markdown", state: "BlockState") -> None:
headings = []
for tok in state.tokens:
@@ -44,16 +56,17 @@ def toc_hook(md, state):
md.before_render_hooks.append(toc_hook)
-def normalize_toc_item(md, token):
- text = token['text']
+def normalize_toc_item(md: "Markdown", token: Dict[str, Any]) -> Tuple[int, str, str]:
+ text = token["text"]
tokens = md.inline(text, {})
- html = md.renderer(tokens, {})
+ assert md.renderer is not None
+ html = md.renderer(tokens, BlockState())
text = striptags(html)
attrs = token['attrs']
return attrs['level'], attrs['id'], text
-def render_toc_ul(toc):
+def render_toc_ul(toc: Iterable[Tuple[int, str, str]]) -> str:
"""Render a table of content HTML. The param "toc" should
be formatted into this structure::
@@ -74,7 +87,7 @@ def render_toc_ul(toc):
return ''
s = '\n'
- levels = []
+ levels: List[int] = []
for level, k, text in toc:
item = '{}'.format(k, text)
if not levels:
diff --git a/src/mistune/util.py b/src/mistune/util.py
index 5e1b9ed..80275ff 100644
--- a/src/mistune/util.py
+++ b/src/mistune/util.py
@@ -1,24 +1,24 @@
import re
+from html import _replace_charref # type: ignore[attr-defined]
+from typing import Match
from urllib.parse import quote
-from html import _replace_charref
-
_expand_tab_re = re.compile(r'^( {0,3})\t', flags=re.M)
-def expand_leading_tab(text: str, width=4):
- def repl(m):
+def expand_leading_tab(text: str, width: int = 4) -> str:
+ def repl(m: Match[str]) -> str:
s = m.group(1)
return s + ' ' * (width - len(s))
return _expand_tab_re.sub(repl, text)
-def expand_tab(text: str, space: str=' '):
- repl = r'\1' + space
+def expand_tab(text: str, space: str = " ") -> str:
+ repl = r"\1" + space
return _expand_tab_re.sub(repl, text)
-def escape(s: str, quote: bool=True):
+def escape(s: str, quote: bool = True) -> str:
"""Escape characters of ``&<>``. If quote=True, ``"`` will be
converted to ``"e;``."""
s = s.replace("&", "&")
@@ -29,7 +29,7 @@ def escape(s: str, quote: bool=True):
return s
-def escape_url(link: str):
+def escape_url(link: str) -> str:
"""Escape URL for safety."""
safe = (
':/?#@' # gen-delims - '[]' (rfc3986)
@@ -39,12 +39,12 @@ def escape_url(link: str):
return escape(quote(unescape(link), safe=safe))
-def safe_entity(s: str):
+def safe_entity(s: str) -> str:
"""Escape characters for safety."""
return escape(unescape(s))
-def unikey(s: str):
+def unikey(s: str) -> str:
"""Generate a unique key for links and footnotes."""
key = ' '.join(s.split()).strip()
return key.lower().upper()
@@ -57,7 +57,7 @@ def unikey(s: str):
)
-def unescape(s: str):
+def unescape(s: str) -> str:
"""
Copy from `html.unescape`, but `_charref` is different. CommonMark
does not accept entity references without a trailing semicolon
@@ -70,12 +70,12 @@ def unescape(s: str):
_striptags_re = re.compile(r'(|<[^>]*>)')
-def striptags(s: str):
+def striptags(s: str) -> str:
return _striptags_re.sub('', s)
_strip_end_re = re.compile(r'\n\s+$')
-def strip_end(src: str):
+def strip_end(src: str) -> str:
return _strip_end_re.sub('\n', src)
diff --git a/tests/__init__.py b/tests/__init__.py
index 3e4e980..b715104 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,13 +1,15 @@
import re
-from tests import fixtures
+from abc import abstractmethod
from unittest import TestCase
+from tests import fixtures
+
class BaseTestCase(TestCase):
@classmethod
- def load_fixtures(cls, case_file):
- def attach_case(n, text, html):
- def method(self):
+ def load_fixtures(cls, case_file: str) -> None:
+ def attach_case(n: str, text: str, html: str) -> None:
+ def method(self: 'BaseTestCase') -> None:
self.assert_case(n, text, html)
name = 'test_{}'.format(n)
@@ -21,15 +23,18 @@ def method(self):
attach_case(n, text, html)
@classmethod
- def ignore_case(cls, name):
+ def ignore_case(cls, name: str) -> bool:
return False
+
+ @abstractmethod
+ def parse(self, text: str) -> str: ...
- def assert_case(self, name, text, html):
+ def assert_case(self, name: str, text: str, html: str) -> None:
result = self.parse(text)
self.assertEqual(result, html)
-def normalize_html(html):
+def normalize_html(html: str) -> str:
html = re.sub(r'>\n+', '>', html)
html = re.sub(r'\n+<', '<', html)
return html.strip()
diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py
index c1ad3c1..9a1abe2 100644
--- a/tests/fixtures/__init__.py
+++ b/tests/fixtures/__init__.py
@@ -1,6 +1,7 @@
import os
import re
import json
+from typing import Any, Iterable, Tuple
ROOT = os.path.join(os.path.dirname(__file__))
@@ -12,17 +13,17 @@
)
-def load_ast(filename):
+def load_ast(filename: str) -> Any:
with open(os.path.join(ROOT, 'ast', filename)) as f:
return json.load(f)
-def load_json(filename):
+def load_json(filename: str) -> Any:
with open(os.path.join(ROOT, filename)) as f:
return json.load(f)
-def load_examples(filename):
+def load_examples(filename: str) -> Iterable[Tuple[str, str, str]]:
if filename.endswith('.json'):
data = load_json(filename)
for item in data:
@@ -37,7 +38,7 @@ def load_examples(filename):
-def parse_examples(text):
+def parse_examples(text: str) -> Iterable[Tuple[str, str, str]]:
data = EXAMPLE_PATTERN.findall(text)
section = None
diff --git a/tests/test_directives.py b/tests/test_directives.py
index cd3bd01..388963a 100644
--- a/tests/test_directives.py
+++ b/tests/test_directives.py
@@ -77,7 +77,7 @@ def test_colon_fenced_toc(self):
class TestDirectiveInclude(BaseTestCase):
- md = create_markdown(escape=False, plugins=[RSTDirective([Include()])])
+ md = create_markdown(escape=False, plugins=[RSTDirective([Include()])]) # type: ignore[list-item]
def test_html_include(self):
html = self.md.read(os.path.join(ROOT, 'include/text.md'))[0]