1from __future__ import annotations
2
3from collections.abc import Sequence
4import itertools
5from typing import TYPE_CHECKING
6
7from markdown_it import MarkdownIt
8from markdown_it.common.utils import escapeHtml
9from markdown_it.rules_block import StateBlock
10
11from mdit_py_plugins.utils import is_code_block
12
13if TYPE_CHECKING:
14 from markdown_it.renderer import RendererProtocol
15 from markdown_it.token import Token
16 from markdown_it.utils import EnvType, OptionsDict
17
18
19def myst_block_plugin(md: MarkdownIt) -> None:
20 """Parse MyST targets (``(name)=``), blockquotes (``% comment``) and block breaks (``+++``)."""
21 md.block.ruler.before(
22 "blockquote",
23 "myst_line_comment",
24 line_comment,
25 {"alt": ["paragraph", "reference", "blockquote", "list", "footnote_def"]},
26 )
27 md.block.ruler.before(
28 "hr",
29 "myst_block_break",
30 block_break,
31 {"alt": ["paragraph", "reference", "blockquote", "list", "footnote_def"]},
32 )
33 md.block.ruler.before(
34 "hr",
35 "myst_target",
36 target,
37 {"alt": ["paragraph", "reference", "blockquote", "list", "footnote_def"]},
38 )
39 md.add_render_rule("myst_target", render_myst_target)
40 md.add_render_rule("myst_line_comment", render_myst_line_comment)
41
42
43def line_comment(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
44 if is_code_block(state, startLine):
45 return False
46
47 pos = state.bMarks[startLine] + state.tShift[startLine]
48 maximum = state.eMarks[startLine]
49
50 if state.src[pos] != "%":
51 return False
52
53 if silent:
54 return True
55
56 token = state.push("myst_line_comment", "", 0)
57 token.attrSet("class", "myst-line-comment")
58 token.content = state.src[pos + 1 : maximum].rstrip()
59 token.markup = "%"
60
61 # search end of block while appending lines to `token.content`
62 for nextLine in itertools.count(startLine + 1):
63 if nextLine >= endLine:
64 break
65 pos = state.bMarks[nextLine] + state.tShift[nextLine]
66 maximum = state.eMarks[nextLine]
67
68 if state.src[pos] != "%":
69 break
70 token.content += "\n" + state.src[pos + 1 : maximum].rstrip()
71
72 state.line = nextLine
73 token.map = [startLine, nextLine]
74
75 return True
76
77
78def block_break(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
79 if is_code_block(state, startLine):
80 return False
81
82 pos = state.bMarks[startLine] + state.tShift[startLine]
83 maximum = state.eMarks[startLine]
84
85 marker = state.src[pos]
86 pos += 1
87
88 # Check block marker
89 if marker != "+":
90 return False
91
92 # markers can be mixed with spaces, but there should be at least 3 of them
93
94 cnt = 1
95 while pos < maximum:
96 ch = state.src[pos]
97 if ch != marker and ch not in ("\t", " "):
98 break
99 if ch == marker:
100 cnt += 1
101 pos += 1
102
103 if cnt < 3:
104 return False
105
106 if silent:
107 return True
108
109 state.line = startLine + 1
110
111 token = state.push("myst_block_break", "hr", 0)
112 token.attrSet("class", "myst-block")
113 token.content = state.src[pos:maximum].strip()
114 token.map = [startLine, state.line]
115 token.markup = marker * cnt
116
117 return True
118
119
120def target(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
121 if is_code_block(state, startLine):
122 return False
123
124 pos = state.bMarks[startLine] + state.tShift[startLine]
125 maximum = state.eMarks[startLine]
126
127 text = state.src[pos:maximum].strip()
128 if not text.startswith("("):
129 return False
130 if not text.endswith(")="):
131 return False
132 if not text[1:-2]:
133 return False
134
135 if silent:
136 return True
137
138 state.line = startLine + 1
139
140 token = state.push("myst_target", "", 0)
141 token.attrSet("class", "myst-target")
142 token.content = text[1:-2]
143 token.map = [startLine, state.line]
144
145 return True
146
147
148def render_myst_target(
149 self: RendererProtocol,
150 tokens: Sequence[Token],
151 idx: int,
152 options: OptionsDict,
153 env: EnvType,
154) -> str:
155 label = tokens[idx].content
156 class_name = "myst-target"
157 target = f'<a href="#{label}">({label})=</a>'
158 return f'<div class="{class_name}">{target}</div>'
159
160
161def render_myst_line_comment(
162 self: RendererProtocol,
163 tokens: Sequence[Token],
164 idx: int,
165 options: OptionsDict,
166 env: EnvType,
167) -> str:
168 # Strip leading whitespace from all lines
169 content = "\n".join(line.lstrip() for line in tokens[idx].content.split("\n"))
170 return f"<!-- {escapeHtml(content)} -->"