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