1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
|
import re
from typing import Callable, List, Optional, Set
from markdown_it import MarkdownIt
from markdown_it.rules_core import StateCore
from markdown_it.token import Token
def anchors_plugin(
md: MarkdownIt,
min_level: int = 1,
max_level: int = 2,
slug_func: Optional[Callable[[str], str]] = None,
permalink: bool = False,
permalinkSymbol: str = "¶",
permalinkBefore: bool = False,
permalinkSpace: bool = True,
) -> None:
"""Plugin for adding header anchors, based on
`markdown-it-anchor <https://github.com/valeriangalliat/markdown-it-anchor>`__
.. code-block:: md
# Title String
renders as:
.. code-block:: html
<h1 id="title-string">Title String <a class="header-anchor" href="#title-string">¶</a></h1>
:param min_level: minimum header level to apply anchors
:param max_level: maximum header level to apply anchors
:param slug_func: function to convert title text to id slug.
:param permalink: Add a permalink next to the title
:param permalinkSymbol: the symbol to show
:param permalinkBefore: Add the permalink before the title, otherwise after
:param permalinkSpace: Add a space between the permalink and the title
Note, the default slug function aims to mimic the GitHub Markdown format, see:
- https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb
- https://gist.github.com/asabaylus/3071099
"""
selected_levels = list(range(min_level, max_level + 1))
md.core.ruler.push(
"anchor",
_make_anchors_func(
selected_levels,
slug_func or slugify,
permalink,
permalinkSymbol,
permalinkBefore,
permalinkSpace,
),
)
def _make_anchors_func(
selected_levels: List[int],
slug_func: Callable[[str], str],
permalink: bool,
permalinkSymbol: str,
permalinkBefore: bool,
permalinkSpace: bool,
) -> Callable[[StateCore], None]:
def _anchor_func(state: StateCore) -> None:
slugs: Set[str] = set()
for idx, token in enumerate(state.tokens):
if token.type != "heading_open":
continue
level = int(token.tag[1])
if level not in selected_levels:
continue
inline_token = state.tokens[idx + 1]
assert inline_token.children is not None
title = "".join(
child.content
for child in inline_token.children
if child.type in ["text", "code_inline"]
)
slug = unique_slug(slug_func(title), slugs)
token.attrSet("id", slug)
if permalink:
link_open = Token(
"link_open",
"a",
1,
)
link_open.attrSet("class", "header-anchor")
link_open.attrSet("href", f"#{slug}")
link_tokens = [
link_open,
Token("html_block", "", 0, content=permalinkSymbol),
Token("link_close", "a", -1),
]
if permalinkBefore:
inline_token.children = (
link_tokens
+ (
[Token("text", "", 0, content=" ")]
if permalinkSpace
else []
)
+ inline_token.children
)
else:
inline_token.children.extend(
([Token("text", "", 0, content=" ")] if permalinkSpace else [])
+ link_tokens
)
return _anchor_func
def slugify(title: str) -> str:
return re.sub(r"[^\w\u4e00-\u9fff\- ]", "", title.strip().lower().replace(" ", "-"))
def unique_slug(slug: str, slugs: Set[str]) -> str:
uniq = slug
i = 1
while uniq in slugs:
uniq = f"{slug}-{i}"
i += 1
slugs.add(uniq)
return uniq
|