1"""Process definition lists."""
2
3from markdown_it import MarkdownIt
4from markdown_it.rules_block import StateBlock
5
6from mdit_py_plugins.utils import is_code_block
7
8
9def deflist_plugin(md: MarkdownIt) -> None:
10 """Plugin ported from
11 `markdown-it-deflist <https://github.com/markdown-it/markdown-it-deflist>`__.
12
13 The syntax is based on
14 `pandoc definition lists <http://johnmacfarlane.net/pandoc/README.html#definition-lists>`__:
15
16 .. code-block:: md
17
18 Term 1
19 : Definition 1 long form
20
21 second paragraph
22
23 Term 2 with *inline markup*
24 ~ Definition 2a compact style
25 ~ Definition 2b
26
27 """
28
29 def skipMarker(state: StateBlock, line: int) -> int:
30 """Search `[:~][\n ]`, returns next pos after marker on success or -1 on fail."""
31 start = state.bMarks[line] + state.tShift[line]
32 maximum = state.eMarks[line]
33
34 if start >= maximum:
35 return -1
36
37 # Check bullet
38 marker = state.src[start]
39 start += 1
40 if marker != "~" and marker != ":":
41 return -1
42
43 pos = state.skipSpaces(start)
44
45 # require space after ":"
46 if start == pos:
47 return -1
48
49 # no empty definitions, e.g. " : "
50 if pos >= maximum:
51 return -1
52
53 return start
54
55 def markTightParagraphs(state: StateBlock, idx: int) -> None:
56 level = state.level + 2
57
58 i = idx + 2
59 l2 = len(state.tokens) - 2
60 while i < l2:
61 if (
62 state.tokens[i].level == level
63 and state.tokens[i].type == "paragraph_open"
64 ):
65 state.tokens[i + 2].hidden = True
66 state.tokens[i].hidden = True
67 i += 2
68 i += 1
69
70 def deflist(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
71 if is_code_block(state, startLine):
72 return False
73
74 if silent:
75 # quirk: validation mode validates a dd block only, not a whole deflist
76 if state.ddIndent < 0:
77 return False
78 return skipMarker(state, startLine) >= 0
79
80 nextLine = startLine + 1
81 if nextLine >= endLine:
82 return False
83
84 if state.isEmpty(nextLine):
85 nextLine += 1
86 if nextLine >= endLine:
87 return False
88
89 if state.sCount[nextLine] < state.blkIndent:
90 return False
91 contentStart = skipMarker(state, nextLine)
92 if contentStart < 0:
93 return False
94
95 # Start list
96 listTokIdx = len(state.tokens)
97 tight = True
98
99 token = state.push("dl_open", "dl", 1)
100 token.map = listLines = [startLine, 0]
101
102 # Iterate list items
103 dtLine = startLine
104 ddLine = nextLine
105
106 # One definition list can contain multiple DTs,
107 # and one DT can be followed by multiple DDs.
108 #
109 # Thus, there is two loops here, and label is
110 # needed to break out of the second one
111 #
112 break_outer = False
113
114 while True:
115 prevEmptyEnd = False
116
117 token = state.push("dt_open", "dt", 1)
118 token.map = [dtLine, dtLine]
119
120 token = state.push("inline", "", 0)
121 token.map = [dtLine, dtLine]
122 token.content = state.getLines(
123 dtLine, dtLine + 1, state.blkIndent, False
124 ).strip()
125 token.children = []
126
127 token = state.push("dt_close", "dt", -1)
128
129 while True:
130 token = state.push("dd_open", "dd", 1)
131 token.map = itemLines = [nextLine, 0]
132
133 pos = contentStart
134 maximum = state.eMarks[ddLine]
135 offset = (
136 state.sCount[ddLine]
137 + contentStart
138 - (state.bMarks[ddLine] + state.tShift[ddLine])
139 )
140
141 while pos < maximum:
142 if state.src[pos] == "\t":
143 offset += 4 - offset % 4
144 elif state.src[pos] == " ":
145 offset += 1
146 else:
147 break
148
149 pos += 1
150
151 contentStart = pos
152
153 oldTight = state.tight
154 oldDDIndent = state.ddIndent
155 oldIndent = state.blkIndent
156 oldTShift = state.tShift[ddLine]
157 oldSCount = state.sCount[ddLine]
158 oldParentType = state.parentType
159 state.blkIndent = state.ddIndent = state.sCount[ddLine] + 2
160 state.tShift[ddLine] = contentStart - state.bMarks[ddLine]
161 state.sCount[ddLine] = offset
162 state.tight = True
163 state.parentType = "deflist"
164
165 state.md.block.tokenize(state, ddLine, endLine)
166
167 # If any of list item is tight, mark list as tight
168 if not state.tight or prevEmptyEnd:
169 tight = False
170
171 # Item become loose if finish with empty line,
172 # but we should filter last element, because it means list finish
173 prevEmptyEnd = (state.line - ddLine) > 1 and state.isEmpty(
174 state.line - 1
175 )
176
177 state.tShift[ddLine] = oldTShift
178 state.sCount[ddLine] = oldSCount
179 state.tight = oldTight
180 state.parentType = oldParentType
181 state.blkIndent = oldIndent
182 state.ddIndent = oldDDIndent
183
184 token = state.push("dd_close", "dd", -1)
185
186 itemLines[1] = nextLine = state.line
187
188 if nextLine >= endLine:
189 break_outer = True
190 break
191
192 if state.sCount[nextLine] < state.blkIndent:
193 break_outer = True
194 break
195
196 contentStart = skipMarker(state, nextLine)
197 if contentStart < 0:
198 break
199
200 ddLine = nextLine
201
202 # go to the next loop iteration:
203 # insert DD tag and repeat checking
204
205 if break_outer:
206 break_outer = False
207 break
208
209 if nextLine >= endLine:
210 break
211 dtLine = nextLine
212
213 if state.isEmpty(dtLine):
214 break
215 if state.sCount[dtLine] < state.blkIndent:
216 break
217
218 ddLine = dtLine + 1
219 if ddLine >= endLine:
220 break
221 if state.isEmpty(ddLine):
222 ddLine += 1
223 if ddLine >= endLine:
224 break
225
226 if state.sCount[ddLine] < state.blkIndent:
227 break
228 contentStart = skipMarker(state, ddLine)
229 if contentStart < 0:
230 break
231
232 # go to the next loop iteration:
233 # insert DT and DD tags and repeat checking
234
235 # Finalise list
236 token = state.push("dl_close", "dl", -1)
237
238 listLines[1] = nextLine
239
240 state.line = nextLine
241
242 # mark paragraphs tight if needed
243 if tight:
244 markTightParagraphs(state, listTokIdx)
245
246 return True
247
248 md.block.ruler.before(
249 "paragraph",
250 "deflist",
251 deflist,
252 {"alt": ["paragraph", "reference", "blockquote"]},
253 )