Skip to content
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
24 changes: 19 additions & 5 deletions src/mistune/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,22 @@ def create_markdown(
hard_wrap: bool = False,
renderer: Optional[RendererRef] = "html",
plugins: Optional[Iterable[PluginRef]] = None,
inject_tabs_assets: bool = False,
) -> Markdown:
"""Create a Markdown instance based on the given condition.

:param escape: Boolean. If using html renderer, escape html.
:param hard_wrap: Boolean. Break every new line into ``<br>``.
:param renderer: renderer instance, default is HTMLRenderer.
:param plugins: List of plugins.
:param inject_tabs_assets: Boolean. Inject CSS/JS for tabs support.

This method is used when you want to re-use a Markdown instance::

markdown = create_markdown(
escape=False,
hard_wrap=True,
inject_tabs_assets=True,
)
# re-use markdown function
markdown('.... your text ...')
Expand All @@ -46,35 +49,46 @@ def create_markdown(
# explicit and more similar to 2.x's API
renderer = None
elif renderer == "html":
renderer = HTMLRenderer(escape=escape)
renderer = HTMLRenderer(escape=escape, inject_tabs_assets=inject_tabs_assets)

inline = InlineParser(hard_wrap=hard_wrap)
real_plugins: Optional[Iterable[Plugin]] = None
if plugins is not None:
real_plugins = [import_plugin(n) for n in plugins]
return Markdown(renderer=renderer, inline=inline, plugins=real_plugins)
return Markdown(
renderer=renderer,
inline=inline,
plugins=real_plugins,
inject_tabs_assets=inject_tabs_assets
)


html: Markdown = create_markdown(escape=False, plugins=["strikethrough", "footnotes", "table", "speedup"])


__cached_parsers: Dict[Tuple[bool, Optional[RendererRef], Optional[Iterable[Any]]], Markdown] = {}
__cached_parsers: Dict[Tuple[bool, bool, Optional[RendererRef], Optional[Iterable[Any]]], Markdown] = {}


def markdown(
text: str,
escape: bool = True,
renderer: Optional[RendererRef] = "html",
plugins: Optional[Iterable[Any]] = None,
inject_tabs_assets: bool = False,
) -> Union[str, List[Dict[str, Any]]]:
if renderer == "ast":
# explicit and more similar to 2.x's API
renderer = None
key = (escape, renderer, plugins)
key = (escape, inject_tabs_assets, renderer, plugins)
if key in __cached_parsers:
return __cached_parsers[key](text)

md = create_markdown(escape=escape, renderer=renderer, plugins=plugins)
md = create_markdown(
escape=escape,
renderer=renderer,
plugins=plugins,
inject_tabs_assets=inject_tabs_assets
)
# improve the speed for markdown parser creation
__cached_parsers[key] = md
return md(text)
Expand Down
24 changes: 24 additions & 0 deletions src/mistune/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ def _md(args: argparse.Namespace) -> "Markdown":
else:
# default plugins
plugins = ["strikethrough", "footnotes", "table", "speedup"]

if args.tabs or args.tip:
plugins = list(plugins) if plugins else []
plugins.append("container")

if args.renderer == "rst":
renderer: "BaseRenderer" = RSTRenderer()
Expand All @@ -30,6 +34,7 @@ def _md(args: argparse.Namespace) -> "Markdown":
hard_wrap=args.hardwrap,
renderer=renderer,
plugins=plugins,
inject_tabs_assets=args.inject_assets,
)


Expand All @@ -53,6 +58,10 @@ def _output(text: str, args: argparse.Namespace) -> None:

$ cat README.md | python -m mistune
<p>...

Container syntax examples:
$ python -m mistune --tabs --inject-assets -m "::: tip\\nThis is a tip\\n:::"
$ python -m mistune --tip -f document.md
"""


Expand Down Expand Up @@ -101,6 +110,21 @@ def cli() -> None:
default="html",
help="specify the output renderer",
)
parser.add_argument(
"--tabs",
action="store_true",
help="enable tabs container support (::: tabs syntax)",
)
parser.add_argument(
"--tip",
action="store_true",
help="enable tip container support (::: tip, ::: warning, etc.)",
)
parser.add_argument(
"--inject-assets",
action="store_true",
help="inject CSS and JavaScript for interactive tabs/tips",
)
parser.add_argument("--version", action="version", version="mistune " + version)
args = parser.parse_args()

Expand Down
97 changes: 95 additions & 2 deletions src/mistune/block_parser.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import re
from typing import Optional, List, Tuple, Match, Pattern
from typing import Optional, List, Tuple, Match, Pattern, Callable, Any, Dict
import string
from .util import (
unikey,
Expand Down Expand Up @@ -34,6 +34,13 @@
_STRICT_BLOCK_QUOTE = re.compile(r"( {0,3}>[^\n]*(?:\n|$))+")


BeforeParseHook = Callable[["BlockParser", BlockState], None]
AfterParseHook = Callable[["BlockParser", BlockState], None]
BeforeTokenHook = Callable[["BlockParser", Match[str], BlockState, str], Optional[int]]
AfterTokenHook = Callable[["BlockParser", Dict[str, Any], BlockState, str], None]
ContainerHook = Callable[["BlockParser", str, Dict[str, Any], BlockState], None]


class BlockParser(Parser[BlockState]):
state_cls = BlockState

Expand Down Expand Up @@ -107,9 +114,91 @@ def __init__(
self.block_quote_rules = block_quote_rules
self.list_rules = list_rules
self.max_nested_level = max_nested_level
# register default parse methods

self._before_parse_hooks: List[BeforeParseHook] = []
self._after_parse_hooks: List[AfterParseHook] = []
self._before_token_hooks: Dict[str, List[BeforeTokenHook]] = {}
self._after_token_hooks: Dict[str, List[AfterTokenHook]] = {}
self._container_enter_hooks: Dict[str, List[ContainerHook]] = {}
self._container_exit_hooks: Dict[str, List[ContainerHook]] = {}

self._methods = {name: getattr(self, "parse_" + name) for name in self.SPECIFICATION}

def add_before_parse_hook(self, hook: BeforeParseHook) -> None:
"""Add a hook that runs before parsing starts."""
self._before_parse_hooks.append(hook)

def add_after_parse_hook(self, hook: AfterParseHook) -> None:
"""Add a hook that runs after parsing completes."""
self._after_parse_hooks.append(hook)

def add_before_token_hook(self, token_type: str, hook: BeforeTokenHook) -> None:
"""Add a hook that runs before a specific token type is parsed."""
if token_type not in self._before_token_hooks:
self._before_token_hooks[token_type] = []
self._before_token_hooks[token_type].append(hook)

def add_after_token_hook(self, token_type: str, hook: AfterTokenHook) -> None:
"""Add a hook that runs after a specific token type is parsed."""
if token_type not in self._after_token_hooks:
self._after_token_hooks[token_type] = []
self._after_token_hooks[token_type].append(hook)

def add_container_enter_hook(self, container_type: str, hook: ContainerHook) -> None:
"""Add a hook that runs when entering a container."""
if container_type not in self._container_enter_hooks:
self._container_enter_hooks[container_type] = []
self._container_enter_hooks[container_type].append(hook)

def add_container_exit_hook(self, container_type: str, hook: ContainerHook) -> None:
"""Add a hook that runs when exiting a container."""
if container_type not in self._container_exit_hooks:
self._container_exit_hooks[container_type] = []
self._container_exit_hooks[container_type].append(hook)

def _run_before_parse_hooks(self, state: BlockState) -> None:
"""Run all before-parse hooks."""
for hook in self._before_parse_hooks:
hook(self, state)

def _run_after_parse_hooks(self, state: BlockState) -> None:
"""Run all after-parse hooks."""
for hook in self._after_parse_hooks:
hook(self, state)

def _run_before_token_hooks(self, token_type: str, m: Match[str], state: BlockState) -> Optional[int]:
"""Run all before-token hooks for a specific token type."""
hooks = self._before_token_hooks.get(token_type, [])
for hook in hooks:
result = hook(self, m, state, token_type)
if result is not None:
return result
return None

def _run_after_token_hooks(self, token_type: str, token: Dict[str, Any], state: BlockState) -> None:
"""Run all after-token hooks for a specific token type."""
hooks = self._after_token_hooks.get(token_type, [])
for hook in hooks:
hook(self, token, state, token_type)

def _run_container_enter_hooks(self, container_type: str, attrs: Dict[str, Any], state: BlockState) -> None:
"""Run all container-enter hooks."""
hooks = self._container_enter_hooks.get(container_type, [])
for hook in hooks:
hook(self, container_type, attrs, state)
hooks = self._container_enter_hooks.get("*", [])
for hook in hooks:
hook(self, container_type, attrs, state)

def _run_container_exit_hooks(self, container_type: str, attrs: Dict[str, Any], state: BlockState) -> None:
"""Run all container-exit hooks."""
hooks = self._container_exit_hooks.get(container_type, [])
for hook in hooks:
hook(self, container_type, attrs, state)
hooks = self._container_exit_hooks.get("*", [])
for hook in hooks:
hook(self, container_type, attrs, state)

def parse_blank_line(self, m: Match[str], state: BlockState) -> int:
"""Parse token for blank lines."""
state.append_token({"type": "blank_line"})
Expand Down Expand Up @@ -442,6 +531,8 @@ def parse_raw_html(self, m: Match[str], state: BlockState) -> Optional[int]:
return None

def parse(self, state: BlockState, rules: Optional[List[str]] = None) -> None:
self._run_before_parse_hooks(state)

sc = self.compile_sc(rules)

while state.cursor < state.cursor_max:
Expand All @@ -468,6 +559,8 @@ def parse(self, state: BlockState, rules: Optional[List[str]] = None) -> None:
text = state.src[state.cursor :]
state.add_paragraph(text)
state.cursor = state.cursor_max

self._run_after_parse_hooks(state)


def _parse_html_to_end(state: BlockState, end_marker: str, start_pos: int) -> int:
Expand Down
24 changes: 24 additions & 0 deletions src/mistune/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class BlockState:
list_tight: bool
parent: Any
env: MutableMapping[str, Any]
container_stack: List[str]
container_depth: int

def __init__(self, parent: Optional[Any] = None) -> None:
self.src = ""
Expand All @@ -52,8 +54,12 @@ def __init__(self, parent: Optional[Any] = None) -> None:
# for saving def references
if parent:
self.env = parent.env
self.container_stack = parent.container_stack
self.container_depth = parent.container_depth
else:
self.env = {"ref_links": {}}
self.container_stack = []
self.container_depth = 0

def child_state(self, src: str) -> "BlockState":
child = self.__class__(self)
Expand Down Expand Up @@ -107,6 +113,24 @@ def depth(self) -> int:
parent = parent.parent
return d

def push_container(self, container_type: str) -> None:
self.container_stack.append(container_type)
self.container_depth = len(self.container_stack)

def pop_container(self) -> Optional[str]:
if self.container_stack:
self.container_depth = len(self.container_stack) - 1
return self.container_stack.pop()
return None

def current_container(self) -> Optional[str]:
if self.container_stack:
return self.container_stack[-1]
return None

def is_in_container(self, container_type: str) -> bool:
return container_type in self.container_stack


class InlineState:
"""The state to save inline parser's tokens."""
Expand Down
4 changes: 4 additions & 0 deletions src/mistune/directives/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from .image import Figure, Image
from .include import Include
from .toc import TableOfContents
from .tabs import Tabs
from .tip import Tip


class RstDirective(RSTDirective): # pragma: no cover
Expand All @@ -32,4 +34,6 @@ def __init__(self, plugins: List[DirectivePlugin]) -> None:
"Include",
"Image",
"Figure",
"Tabs",
"Tip",
]
Loading