Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔧 Improve typing for MarkdownIt.add_render_rule #287

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions markdown_it/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from collections.abc import Callable, Generator, Iterable, Mapping, MutableMapping
from contextlib import contextmanager
from typing import Any, Literal, overload
from typing import Any, Literal, Sequence, overload

from . import helpers, presets
from .common import normalize_url, utils
Expand Down Expand Up @@ -144,7 +144,7 @@
return self

def get_all_rules(self) -> dict[str, list[str]]:
"""Return the names of all active rules."""
"""Return the names of all rules."""
rules = {
chain: self[chain].ruler.get_all_rules()
for chain in ["core", "block", "inline"]
Expand Down Expand Up @@ -227,14 +227,23 @@
self.inline.ruler2.enableOnly(chain_rules["inline2"])

def add_render_rule(
self, name: str, function: Callable[..., Any], fmt: str = "html"
self,
name: str,
function: Callable[
[RendererProtocol, Sequence[Token], int, OptionsDict, EnvType], str
],
fmt: str = "html",
) -> None:
"""Add a rule for rendering a particular Token type.

Only applied when ``renderer.__output__ == fmt``

:param name: the name of the token type
:param function: the function to call to render the token;
it should have the signature ``function(renderer, tokens, idx, options, env)``
"""
if self.renderer.__output__ == fmt:
self.renderer.rules[name] = function.__get__(self.renderer) # type: ignore
self.renderer.rules[name] = function.__get__(self.renderer)

Check warning on line 246 in markdown_it/main.py

View check run for this annotation

Codecov / codecov/patch

markdown_it/main.py#L246

Added line #L246 was not covered by tests

def use(
self, plugin: Callable[..., None], *params: Any, **options: Any
Expand Down
2 changes: 1 addition & 1 deletion markdown_it/parser_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def __init__(self) -> None:

def tokenize(self, state: StateBlock, startLine: int, endLine: int) -> None:
"""Generate tokens for input range."""
rules = self.ruler.getRules("")
rules = self.ruler.getRules()
line = startLine
maxNesting = state.md.options.maxNesting
hasEmptyLines = False
Expand Down
2 changes: 1 addition & 1 deletion markdown_it/parser_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,5 @@ def __init__(self) -> None:

def process(self, state: StateCore) -> None:
"""Executes core chain rules."""
for rule in self.ruler.getRules(""):
for rule in self.ruler.getRules():
rule(state)
6 changes: 3 additions & 3 deletions markdown_it/parser_inline.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def skipToken(self, state: StateInline) -> None:
"""
ok = False
pos = state.pos
rules = self.ruler.getRules("")
rules = self.ruler.getRules()
maxNesting = state.md.options["maxNesting"]
cache = state.cache

Expand Down Expand Up @@ -106,7 +106,7 @@ def skipToken(self, state: StateInline) -> None:
def tokenize(self, state: StateInline) -> None:
"""Generate tokens for input range."""
ok = False
rules = self.ruler.getRules("")
rules = self.ruler.getRules()
end = state.posMax
maxNesting = state.md.options["maxNesting"]

Expand Down Expand Up @@ -141,7 +141,7 @@ def parse(
"""Process input string and push inline tokens into `tokens`"""
state = StateInline(src, md, env, tokens)
self.tokenize(state)
rules2 = self.ruler2.getRules("")
rules2 = self.ruler2.getRules()
for rule in rules2:
rule(state)
return state.tokens
14 changes: 12 additions & 2 deletions markdown_it/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,31 @@

from collections.abc import Sequence
import inspect
from typing import Any, ClassVar, Protocol
from typing import TYPE_CHECKING, Any, Callable, ClassVar, MutableMapping, Protocol

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

if TYPE_CHECKING:
from markdown_it import MarkdownIt

Check warning on line 19 in markdown_it/renderer.py

View check run for this annotation

Codecov / codecov/patch

markdown_it/renderer.py#L19

Added line #L19 was not covered by tests


class RendererProtocol(Protocol):
__output__: ClassVar[str]
rules: MutableMapping[
str,
Callable[[Sequence[Token], int, OptionsDict, EnvType], str],
]

def render(
self, tokens: Sequence[Token], options: OptionsDict, env: EnvType
) -> Any:
...

# note container and admon plugins also expect renderToken to be defined,
# but it is unclear if this should be a requirement for all renderers


class RendererHTML(RendererProtocol):
"""Contains render rules for tokens. Can be updated and extended.
Expand Down Expand Up @@ -57,7 +67,7 @@

__output__ = "html"

def __init__(self, parser: Any = None):
def __init__(self, parser: None | MarkdownIt = None):
self.rules = {
k: v
for k, v in inspect.getmembers(self, predicate=inspect.ismethod)
Expand Down
11 changes: 9 additions & 2 deletions markdown_it/ruler.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def srcCharCode(self) -> tuple[int, ...]:

class RuleOptionsType(TypedDict, total=False):
alt: list[str]
"""list of rules which can be terminated by this one."""


RuleFuncTv = TypeVar("RuleFuncTv")
Expand All @@ -71,9 +72,12 @@ class Rule(Generic[RuleFuncTv]):
enabled: bool
fn: RuleFuncTv = field(repr=False)
alt: list[str]
"""list of rules which can be terminated by this one."""


class Ruler(Generic[RuleFuncTv]):
"""Class to manage functions (rules) which identify syntax elements."""

def __init__(self) -> None:
# List of added rules.
self.__rules__: list[Rule[RuleFuncTv]] = []
Expand Down Expand Up @@ -255,10 +259,13 @@ def disable(

def getRules(self, chainName: str = "") -> list[RuleFuncTv]:
"""Return array of active functions (rules) for given chain name.

It analyzes rules configuration, compiles caches if not exists and returns result.

Default chain name is `''` (empty string). It can't be skipped.
That's done intentionally, to keep signature monomorphic for high speed.
:param chainName: name of chain to return rules for:
- The default `""` means all "top-level rules for this ruler.
- A specific name can be used to fetch only rules which can terminate
the named rule (used for block level rules like paragraph, list, etc.)

"""
if self.__cache__ is None:
Expand Down
78 changes: 53 additions & 25 deletions markdown_it/rules_block/state_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,40 +28,68 @@ def __init__(

self.tokens = tokens

self.bMarks: list[int] = [] # line begin offsets for fast jumps
self.eMarks: list[int] = [] # line end offsets for fast jumps
# offsets of the first non-space characters (tabs not expanded)
self.bMarks: list[int] = []
"""line begin offsets for fast jumps"""

self.eMarks: list[int] = []
"""line end offsets for fast jumps"""

self.tShift: list[int] = []
self.sCount: list[int] = [] # indents for each line (tabs expanded)
"""Offsets of the first non-space characters (tabs not expanded)"""

self.sCount: list[int] = []
"""indents for each line (tabs expanded)"""

# An amount of virtual spaces (tabs expanded) between beginning
# of each line (bMarks) and real beginning of that line.
#
# It exists only as a hack because blockquotes override bMarks
# losing information in the process.
#
# It's used only when expanding tabs, you can think about it as
# an initial tab length, e.g. bsCount=21 applied to string `\t123`
# means first tab should be expanded to 4-21%4 === 3 spaces.
#
self.bsCount: list[int] = []
"""
An amount of virtual spaces (tabs expanded) between beginning
of each line (bMarks) and real beginning of that line.

It exists only as a hack because blockquotes override bMarks
losing information in the process.

It's used only when expanding tabs, you can think about it as
an initial tab length, e.g. `bsCount=21` applied to string `\\t123`
means first tab should be expanded to `4-21 % 4 == 3` spaces.
"""

#
# block parser variables
self.blkIndent = 0 # required block content indent (for example, if we are
# inside a list, it would be positioned after list marker)
self.line = 0 # line index in src
self.lineMax = 0 # lines count
self.tight = False # loose/tight mode for lists
self.ddIndent = -1 # indent of the current dd block (-1 if there isn't any)
self.listIndent = -1 # indent of the current list block (-1 if there isn't any)

# can be 'blockquote', 'list', 'root', 'paragraph' or 'reference'
# used in lists to determine if they interrupt a paragraph
#

self.blkIndent = 0
"""required block content indent
(for example, if we are inside a list, it would be positioned after list marker)
"""

self.line = 0
"""line index in src"""
self.lineMax = 0
"""Total lines count"""

self.tight = False
"""loose/tight mode for lists"""

self.ddIndent = -1
"""indent of the current dd block (-1 if there isn't any),
used only by deflist plugin
"""

self.listIndent = -1
"""indent of the current list block (-1 if there isn't any)"""

self.parentType = "root"
"""
can be 'blockquote', 'list', 'root', 'paragraph' or 'reference'
used in lists to determine if they interrupt a paragraph
"""

self.level = 0
"""Current nesting level of tokens,
+1 when adding opening token, -1 when adding closing token
"""

# renderer
# renderer (does not appear to be used)
self.result = ""

# Create caches
Expand Down
28 changes: 21 additions & 7 deletions markdown_it/rules_inline/state_inline.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,28 +51,42 @@ def __init__(
self.tokens_meta: list[dict[str, Any] | None] = [None] * len(outTokens)

self.pos = 0
"""Current position in src string"""
self.posMax = len(self.src)
"""Length of the src string"""
self.level = 0
"""Current nesting level of tokens,
+1 when adding opening token, -1 when adding closing token
"""
self.pending = ""
"""Accumulated text not yet converted to a token.
This will be added as a `text` token when the next token is pushed (before it),
or when the parser finishes running (after all other tokens).
"""
self.pendingLevel = 0
"""The nesting level of the pending text"""

# Stores { start: end } pairs. Useful for backtrack
# optimization of pairs parse (emphasis, strikes).
self.cache: dict[int, int] = {}
"""
Stores { start: end } pairs.
Useful for backtrack optimization of pairs parse (emphasis, strikes).
"""

# List of emphasis-like delimiters for current tag
self.delimiters: list[Delimiter] = []
"""List of emphasis-like delimiters for current tag"""

# Stack of delimiter lists for upper level tags
self._prev_delimiters: list[list[Delimiter]] = []
"""Stack of delimiter lists for upper level tags"""

# backticklength => last seen position
self.backticks: dict[int, int] = {}
"""backticklength => last seen position"""
self.backticksScanned = False

# Counter used to disable inline linkify-it execution
# inside <a> and markdown links
self.linkLevel = 0
"""
Counter used to disable inline linkify-it execution
inside `<a>` and markdown links
"""

def __repr__(self) -> str:
return (
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ compare = [
linkify = ["linkify-it-py>=1,<3"]
plugins = ["mdit-py-plugins"]
rtd = [
"mdit-py-plugins @ git+https://github.com/executablebooks/mdit-py-plugins@master",
"mdit-py-plugins",
"myst-parser",
"pyyaml",
"sphinx",
Expand Down