1# fences (``` lang, ~~~ lang)
2from __future__ import annotations
3
4from collections.abc import Callable
5import logging
6
7from .state_block import StateBlock
8
9LOGGER = logging.getLogger(__name__)
10
11
12def make_fence_rule(
13 *,
14 markers: tuple[str, ...] = ("~", "`"),
15 token_type: str = "fence",
16 exact_match: bool = False,
17 disallow_marker_in_info: tuple[str, ...] = ("`",),
18 min_markers: int = 3,
19) -> Callable[[StateBlock, int, int, bool], bool]:
20 """Create a fence parsing rule with configurable options.
21
22 :param markers: Tuple of single characters that can be used as fence markers.
23 :param token_type: The token type name to emit (e.g. "fence", "colon_fence").
24 :param exact_match: If True, the closing fence must have exactly the same
25 number of marker characters as the opening fence (not "at least as many").
26 This enables nesting of fences with different marker counts.
27 :param disallow_marker_in_info: Tuple of marker characters that are not allowed
28 to appear in the info string. The check only applies when the actual opening
29 marker is in this tuple (e.g. a tilde fence is unaffected by ``"`"`` being
30 listed). Per CommonMark, backtick fences cannot have backticks in the info
31 string. Use ``()`` to disable this restriction.
32 :param min_markers: Minimum number of marker characters to form a fence.
33 :return: A block rule function with signature
34 ``(state, startLine, endLine, silent) -> bool``.
35 """
36
37 closing_matcher: Callable[[int, int], bool]
38 if exact_match:
39 # closing code fence must have exactly the same number of markers as the opening one
40 closing_matcher = lambda opening_len, closing_len: closing_len == opening_len # noqa: E731
41 else:
42 # closing code fence must be at least as long as the opening one
43 closing_matcher = lambda opening_len, closing_len: closing_len >= opening_len # noqa: E731
44
45 def _fence_rule(
46 state: StateBlock, startLine: int, endLine: int, silent: bool
47 ) -> bool:
48 LOGGER.debug(
49 "entering fence: %s, %s, %s, %s", state, startLine, endLine, silent
50 )
51
52 haveEndMarker = False
53 pos = state.bMarks[startLine] + state.tShift[startLine]
54 maximum = state.eMarks[startLine]
55
56 if state.is_code_block(startLine):
57 return False
58
59 if pos + min_markers > maximum:
60 return False
61
62 marker = state.src[pos]
63
64 if marker not in markers:
65 return False
66
67 # scan marker length
68 mem = pos
69 pos = state.skipCharsStr(pos, marker)
70
71 length = pos - mem
72
73 if length < min_markers:
74 return False
75
76 markup = state.src[mem:pos]
77 params = state.src[pos:maximum]
78
79 if marker in disallow_marker_in_info and marker in params:
80 return False
81
82 # Since start is found, we can report success here in validation mode
83 if silent:
84 return True
85
86 # search end of block
87 nextLine = startLine
88
89 while True:
90 nextLine += 1
91 if nextLine >= endLine:
92 # unclosed block should be autoclosed by end of document.
93 # also block seems to be autoclosed by end of parent
94 break
95
96 pos = mem = state.bMarks[nextLine] + state.tShift[nextLine]
97 maximum = state.eMarks[nextLine]
98
99 if pos < maximum and state.sCount[nextLine] < state.blkIndent:
100 # non-empty line with negative indent should stop the list:
101 # - ```
102 # test
103 break
104
105 try:
106 if state.src[pos] != marker:
107 continue
108 except IndexError:
109 break
110
111 if state.is_code_block(nextLine):
112 continue
113
114 pos = state.skipCharsStr(pos, marker)
115
116 if not closing_matcher(length, pos - mem):
117 continue
118
119 # make sure tail has spaces only
120 pos = state.skipSpaces(pos)
121
122 if pos < maximum:
123 continue
124
125 haveEndMarker = True
126 # found!
127 break
128
129 # If a fence has heading spaces, they should be removed from its inner block
130 length = state.sCount[startLine]
131
132 state.line = nextLine + (1 if haveEndMarker else 0)
133
134 token = state.push(token_type, "code", 0)
135 token.info = params
136 token.content = state.getLines(startLine + 1, nextLine, length, True)
137 token.markup = markup
138 token.map = [startLine, state.line]
139
140 return True
141
142 return _fence_rule
143
144
145#: The default fence rule (backtick and tilde markers, CommonMark compliant).
146fence = make_fence_rule()