diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 04230b02..820a1305 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,14 +24,14 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.12 + rev: v0.16.6 hooks: - id: ruff args: [--fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.20.2 + rev: v2.3.1 hooks: - id: mypy additional_dependencies: [mdurl, typing-extensions] diff --git a/AGENTS.md b/AGENTS.md index 7e49651b..5aadebec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -158,11 +158,9 @@ from __future__ import annotations from typing import Sequence + def parse_blocks( - state: StateBlock, - start_line: int, - end_line: int, - silent: bool = False + state: StateBlock, start_line: int, end_line: int, silent: bool = False ) -> bool: """Parse block-level content. @@ -293,18 +291,20 @@ HTML Output import pytest from markdown_it import MarkdownIt + def test_basic_parsing(): md = MarkdownIt() result = md.render("# Heading\n\nParagraph") assert "
Paragraph
" in result + @pytest.mark.parametrize( "input_text,expected", [ ("**bold**", "bold"), ("*italic*", "italic"), - ] + ], ) def test_emphasis(input_text, expected): md = MarkdownIt() @@ -389,7 +389,7 @@ for token in tokens: print(md.get_all_rules()) # Enable/disable specific rules -md.disable(['emphasis']) +md.disable(["emphasis"]) result = md.render("*text*") # Won't be emphasized ``` @@ -401,13 +401,13 @@ result = md.render("*text*") # Won't be emphasized 2. Create rule function in appropriate `rules_*/` directory 3. Rule signature for block rules: ```python - def rule_name(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool: - ... + def rule_name( + state: StateBlock, startLine: int, endLine: int, silent: bool + ) -> bool: ... ``` 4. Rule signature for inline rules: ```python - def rule_name(state: StateInline, silent: bool) -> bool: - ... + def rule_name(state: StateInline, silent: bool) -> bool: ... ``` 5. Register the rule in the appropriate parser's `__init__` method 6. Add tests for the new rule @@ -434,11 +434,13 @@ result = md.render("*text*") # Won't be emphasized ```python from markdown_it import MarkdownIt + def render_custom_link(self, tokens, idx, options, env): tokens[idx].attrSet("target", "_blank") tokens[idx].attrSet("rel", "noopener noreferrer") return self.renderToken(tokens, idx, options, env) + md = MarkdownIt() md.add_render_rule("link_open", render_custom_link) ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index e9bd6057..033cf4fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -267,6 +267,7 @@ It can then be activated by: ```python from markdown_it import MarkdownIt + md = MarkdownIt().enable("linkify") md.options["linkify"] = True ``` @@ -284,6 +285,7 @@ It can be activated by: ```python from markdown_it import MarkdownIt + md = MarkdownIt().enable("smartquotes") md.options["typographer"] = True ``` @@ -304,6 +306,7 @@ This plugin can be activated by: ```python from markdown_it import MarkdownIt from markdown_it.extensions.tasklists import tasklists_plugin + md = MarkdownIt().use(tasklists_plugin) ``` diff --git a/README.md b/README.md index 82d218b3..ce27254f 100644 --- a/README.md +++ b/README.md @@ -69,12 +69,12 @@ from mdit_py_plugins.front_matter import front_matter_plugin from mdit_py_plugins.footnote import footnote_plugin md = ( - MarkdownIt('commonmark', {'breaks':True,'html':True}) + MarkdownIt("commonmark", {"breaks": True, "html": True}) .use(front_matter_plugin) .use(footnote_plugin) - .enable('table') + .enable("table") ) -text = (""" +text = """ --- a: 1 --- @@ -86,7 +86,7 @@ a | b A footnote [^1] [^1]: some details -""") +""" tokens = md.parse(text) html_text = md.render(text) diff --git a/docs/architecture.md b/docs/architecture.md index 3100af7f..2ed00fac 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -105,7 +105,7 @@ with the same signature: ```python def function(renderer, tokens, idx, options, env): - return htmlResult + return htmlResult ``` In many cases that allows easy output change even without parser intrusion. @@ -113,23 +113,28 @@ For example, let's replace images with vimeo links to player's iframe: ```python import re + md = MarkdownIt("commonmark") -vimeoRE = re.compile(r'^https?:\/\/(www\.)?vimeo.com\/(\d+)($|\/)') +vimeoRE = re.compile(r"^https?:\/\/(www\.)?vimeo.com\/(\d+)($|\/)") + def render_vimeo(self, tokens, idx, options, env): token = tokens[idx] if vimeoRE.match(token.attrs["src"]): - ident = vimeoRE.match(token.attrs["src"])[2] - return ('\n') + return ( + '\n" + ) return self.image(tokens, idx, options, env) + md = MarkdownIt("commonmark") md.add_render_rule("image", render_vimeo) print(md.render("")) @@ -140,12 +145,14 @@ Here is another example, how to add `target="_blank"` to all links: ```python from markdown_it import MarkdownIt + def render_blank_link(self, tokens, idx, options, env): tokens[idx].attrSet("target", "_blank") # pass token to default renderer. return self.renderToken(tokens, idx, options, env) + md = MarkdownIt("commonmark") md.add_render_rule("link_open", render_blank_link) print(md.render("[a]\n\n[a]: b")) diff --git a/docs/conf.py b/docs/conf.py index e468b853..99b37b98 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -57,6 +57,7 @@ "Path", "Ellipsis", "NotRequired", + "Self", ) ] diff --git a/docs/plugins.md b/docs/plugins.md index 98d9600c..bfe66155 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -13,14 +13,16 @@ These can be enabled individually: ```python from markdown_it import MarkdownIt -md = MarkdownIt("commonmark").enable('table') + +md = MarkdownIt("commonmark").enable("table") ``` or as part of a configuration: ```python from markdown_it import MarkdownIt -md = MarkdownIt("gfm-like") # tables, strikethrough, linkify + +md = MarkdownIt("gfm-like") # tables, strikethrough, linkify md = MarkdownIt("gfm-like2") # + task lists, alerts, single-tilde strikethrough ``` @@ -48,6 +50,7 @@ They can be chained and loaded *via*: ```python from markdown_it import MarkdownIt from mdit_py_plugins import plugin1, plugin2 + md = MarkdownIt().use(plugin1, keyword=value).use(plugin2, keyword=value) html_string = md.render("some *Markdown*") ``` diff --git a/docs/using.md b/docs/using.md index 17f62d10..aae3e858 100644 --- a/docs/using.md +++ b/docs/using.md @@ -298,7 +298,7 @@ with the same signature: ```python def function(renderer, tokens, idx, options, env): - return htmlResult + return htmlResult ``` +++ diff --git a/markdown_it/cli/parse.py b/markdown_it/cli/parse.py index 5de738b2..16e7ac4c 100644 --- a/markdown_it/cli/parse.py +++ b/markdown_it/cli/parse.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """ CLI interface to markdown-it-py diff --git a/markdown_it/common/utils.py b/markdown_it/common/utils.py index 11bda644..500a590d 100644 --- a/markdown_it/common/utils.py +++ b/markdown_it/common/utils.py @@ -102,7 +102,7 @@ def replaceEntityPattern(match: str, name: str) -> str: if name in entities: return entities[name] - code: None | int = None + code: int | None = None if pat := DIGITAL_ENTITY_BASE10_RE.fullmatch(name): code = int(pat.group(1), 10) elif pat := DIGITAL_ENTITY_BASE16_RE.fullmatch(name): diff --git a/markdown_it/renderer.py b/markdown_it/renderer.py index 73dbde15..e37a04e1 100644 --- a/markdown_it/renderer.py +++ b/markdown_it/renderer.py @@ -61,7 +61,7 @@ def __init__(self, parser: Any = None): self.rules = { k: v for k, v in inspect.getmembers(self, predicate=inspect.ismethod) - if not (k.startswith("render") or k.startswith("_")) + if not k.startswith(("render", "_")) } def render( diff --git a/markdown_it/rules_block/fence.py b/markdown_it/rules_block/fence.py index 0d7e651e..621924b5 100644 --- a/markdown_it/rules_block/fence.py +++ b/markdown_it/rules_block/fence.py @@ -37,10 +37,10 @@ def make_fence_rule( closing_matcher: Callable[[int, int], bool] if exact_match: # closing code fence must have exactly the same number of markers as the opening one - closing_matcher = lambda opening_len, closing_len: closing_len == opening_len # noqa: E731 + closing_matcher = lambda opening_len, closing_len: closing_len == opening_len else: # closing code fence must be at least as long as the opening one - closing_matcher = lambda opening_len, closing_len: closing_len >= opening_len # noqa: E731 + closing_matcher = lambda opening_len, closing_len: closing_len >= opening_len def _fence_rule( state: StateBlock, startLine: int, endLine: int, silent: bool diff --git a/markdown_it/rules_block/reference.py b/markdown_it/rules_block/reference.py index ad94d409..175308f9 100644 --- a/markdown_it/rules_block/reference.py +++ b/markdown_it/rules_block/reference.py @@ -192,7 +192,7 @@ def reference(state: StateBlock, startLine: int, _endLine: int, silent: bool) -> return True -def getNextLine(state: StateBlock, nextLine: int) -> None | str: +def getNextLine(state: StateBlock, nextLine: int) -> str | None: endLine = state.lineMax if nextLine >= endLine or state.isEmpty(nextLine): diff --git a/markdown_it/rules_core/smartquotes.py b/markdown_it/rules_core/smartquotes.py index f9b8b457..3392dcb5 100644 --- a/markdown_it/rules_core/smartquotes.py +++ b/markdown_it/rules_core/smartquotes.py @@ -58,7 +58,7 @@ def process_inlines(tokens: list[Token], state: StateCore) -> None: # Find previous character, # default to space if it's the beginning of the line - lastChar: None | int = 0x20 + lastChar: int | None = 0x20 if t.start(0) + lastIndex - 1 >= 0: lastChar = charCodeAt(text, t.start(0) + lastIndex - 1) @@ -75,7 +75,7 @@ def process_inlines(tokens: list[Token], state: StateCore) -> None: # Find next character, # default to space if it's the end of the line - nextChar: None | int = 0x20 + nextChar: int | None = 0x20 if pos < maximum: nextChar = charCodeAt(text, pos) diff --git a/markdown_it/token.py b/markdown_it/token.py index d6d0b453..309bb96f 100644 --- a/markdown_it/token.py +++ b/markdown_it/token.py @@ -95,11 +95,11 @@ def attrPush(self, attrData: tuple[str, str | int | float]) -> None: name, value = attrData self.attrSet(name, value) - def attrSet(self, name: str, value: str | int | float) -> None: + def attrSet(self, name: str, value: str | float) -> None: """Set `name` attribute to `value`. Override old value if exists.""" self.attrs[name] = value - def attrGet(self, name: str) -> None | str | int | float: + def attrGet(self, name: str) -> str | int | float | None: """Get the value of attribute `name`, or null if it does not exist.""" return self.attrs.get(name, None) diff --git a/markdown_it/tree.py b/markdown_it/tree.py index 24bc2466..ebe00d23 100644 --- a/markdown_it/tree.py +++ b/markdown_it/tree.py @@ -7,10 +7,13 @@ from collections.abc import Generator, Sequence import textwrap -from typing import Any, NamedTuple, TypeVar, overload +from typing import TYPE_CHECKING, Any, NamedTuple, TypeVar, overload from .token import Token +if TYPE_CHECKING: + from typing_extensions import Self + class _NesterTokens(NamedTuple): opening: Token @@ -84,13 +87,13 @@ def __getitem__(self: _NodeType, item: int) -> _NodeType: ... @overload def __getitem__(self: _NodeType, item: slice) -> list[_NodeType]: ... - def __getitem__(self: _NodeType, item: int | slice) -> _NodeType | list[_NodeType]: + def __getitem__(self, item: int | slice) -> Self | list[Self]: return self.children[item] - def to_tokens(self: _NodeType) -> list[Token]: + def to_tokens(self) -> list[Token]: """Recover the linear token stream.""" - def recursive_collect_tokens(node: _NodeType, token_list: list[Token]) -> None: + def recursive_collect_tokens(node: Self, token_list: list[Token]) -> None: if node.type == "root": for child in node.children: recursive_collect_tokens(child, token_list) @@ -108,19 +111,19 @@ def recursive_collect_tokens(node: _NodeType, token_list: list[Token]) -> None: return tokens @property - def children(self: _NodeType) -> list[_NodeType]: + def children(self) -> list[Self]: return self._children @children.setter - def children(self: _NodeType, value: list[_NodeType]) -> None: + def children(self, value: list[Self]) -> None: self._children = value @property - def parent(self: _NodeType) -> _NodeType | None: + def parent(self) -> Self | None: return self._parent # type: ignore @parent.setter - def parent(self: _NodeType, value: _NodeType | None) -> None: + def parent(self, value: Self | None) -> None: self._parent = value @property @@ -139,7 +142,7 @@ def is_nested(self) -> bool: return bool(self.nester_tokens) @property - def siblings(self: _NodeType) -> Sequence[_NodeType]: + def siblings(self) -> Sequence[Self]: """Get siblings of the node. Gets the whole group of siblings, including self. @@ -165,7 +168,7 @@ def type(self) -> str: return self.nester_tokens.opening.type.removesuffix("_open") @property - def next_sibling(self: _NodeType) -> _NodeType | None: + def next_sibling(self) -> Self | None: """Get the next node in the sequence of siblings. Returns `None` if this is the last sibling. @@ -176,7 +179,7 @@ def next_sibling(self: _NodeType) -> _NodeType | None: return None @property - def previous_sibling(self: _NodeType) -> _NodeType | None: + def previous_sibling(self) -> Self | None: """Get the previous node in the sequence of siblings. Returns `None` if this is the first sibling. @@ -241,9 +244,7 @@ def pretty( ) return text - def walk( - self: _NodeType, *, include_self: bool = True - ) -> Generator[_NodeType, None, None]: + def walk(self, *, include_self: bool = True) -> Generator[Self, None, None]: """Recursively yield all descendant nodes in the tree starting at self. The order mimics the order of the underlying linear token @@ -282,7 +283,7 @@ def attrs(self) -> dict[str, str | int | float]: """Html attributes.""" return self._attribute_token().attrs - def attrGet(self, name: str) -> None | str | int | float: + def attrGet(self, name: str) -> str | int | float | None: """Get the value of attribute `name`, or null if it does not exist.""" return self._attribute_token().attrGet(name) diff --git a/markdown_it/utils.py b/markdown_it/utils.py index 09e60163..cc48fa5c 100644 --- a/markdown_it/utils.py +++ b/markdown_it/utils.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Callable, Iterable, MutableMapping +from collections.abc import Callable, Iterator, MutableMapping from collections.abc import MutableMapping as MutableMappingABC from pathlib import Path from typing import TYPE_CHECKING, Any, TypedDict, cast @@ -78,7 +78,7 @@ def __setitem__(self, key: str, value: Any) -> None: def __delitem__(self, key: str) -> None: del self._options[key] # type: ignore - def __iter__(self) -> Iterable[str]: # type: ignore + def __iter__(self) -> Iterator[str]: return iter(self._options) def __len__(self) -> int: diff --git a/tests/test_api/test_main.py b/tests/test_api/test_main.py index dca3e8b2..c84e33de 100644 --- a/tests/test_api/test_main.py +++ b/tests/test_api/test_main.py @@ -1,8 +1,13 @@ +from typing import TYPE_CHECKING + import pytest from markdown_it import MarkdownIt from markdown_it.token import Token +if TYPE_CHECKING: + from typing_extensions import Self + def test_get_rules(): md = MarkdownIt("zero") @@ -412,7 +417,7 @@ class _SliceCountingStr(str): slice_lengths: list[int] - def __new__(cls, value: str) -> "_SliceCountingStr": + def __new__(cls, value: str) -> "Self": self = super().__new__(cls, value) self.slice_lengths = [] return self diff --git a/tests/test_cmark_spec/get_cmark_spec.py b/tests/test_cmark_spec/get_cmark_spec.py index d59364f0..2883835e 100644 --- a/tests/test_cmark_spec/get_cmark_spec.py +++ b/tests/test_cmark_spec/get_cmark_spec.py @@ -55,7 +55,7 @@ def _json_to_fixture(data: list[dict[str, Any]]) -> str: if __name__ == "__main__": - import requests # type: ignore[import-untyped] + import requests args = create_argparser().parse_args() version: str = args.version