Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/mdit_py_plugins/footnote/index.py: 88%

256 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:15 +0000

1"""Process footnotes""" 

2from __future__ import annotations 

3 

4from typing import TYPE_CHECKING, List, Optional, Sequence 

5 

6from markdown_it import MarkdownIt 

7from markdown_it.helpers import parseLinkLabel 

8from markdown_it.rules_block import StateBlock 

9from markdown_it.rules_core import StateCore 

10from markdown_it.rules_inline import StateInline 

11from markdown_it.token import Token 

12 

13from mdit_py_plugins.utils import is_code_block 

14 

15if TYPE_CHECKING: 

16 from markdown_it.renderer import RendererProtocol 

17 from markdown_it.utils import EnvType, OptionsDict 

18 

19 

20def footnote_plugin(md: MarkdownIt) -> None: 

21 """Plugin ported from 

22 `markdown-it-footnote <https://github.com/markdown-it/markdown-it-footnote>`__. 

23 

24 It is based on the 

25 `pandoc definition <http://johnmacfarlane.net/pandoc/README.html#footnotes>`__: 

26 

27 .. code-block:: md 

28 

29 Normal footnote: 

30 

31 Here is a footnote reference,[^1] and another.[^longnote] 

32 

33 [^1]: Here is the footnote. 

34 

35 [^longnote]: Here's one with multiple blocks. 

36 

37 Subsequent paragraphs are indented to show that they 

38 belong to the previous footnote. 

39 

40 """ 

41 md.block.ruler.before( 

42 "reference", "footnote_def", footnote_def, {"alt": ["paragraph", "reference"]} 

43 ) 

44 md.inline.ruler.after("image", "footnote_inline", footnote_inline) 

45 md.inline.ruler.after("footnote_inline", "footnote_ref", footnote_ref) 

46 md.core.ruler.after("inline", "footnote_tail", footnote_tail) 

47 

48 md.add_render_rule("footnote_ref", render_footnote_ref) 

49 md.add_render_rule("footnote_block_open", render_footnote_block_open) 

50 md.add_render_rule("footnote_block_close", render_footnote_block_close) 

51 md.add_render_rule("footnote_open", render_footnote_open) 

52 md.add_render_rule("footnote_close", render_footnote_close) 

53 md.add_render_rule("footnote_anchor", render_footnote_anchor) 

54 

55 # helpers (only used in other rules, no tokens are attached to those) 

56 md.add_render_rule("footnote_caption", render_footnote_caption) 

57 md.add_render_rule("footnote_anchor_name", render_footnote_anchor_name) 

58 

59 

60# ## RULES ## 

61 

62 

63def footnote_def(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool: 

64 """Process footnote block definition""" 

65 

66 if is_code_block(state, startLine): 

67 return False 

68 

69 start = state.bMarks[startLine] + state.tShift[startLine] 

70 maximum = state.eMarks[startLine] 

71 

72 # line should be at least 5 chars - "[^x]:" 

73 if start + 4 > maximum: 

74 return False 

75 

76 if state.src[start] != "[": 

77 return False 

78 if state.src[start + 1] != "^": 

79 return False 

80 

81 pos = start + 2 

82 while pos < maximum: 

83 if state.src[pos] == " ": 

84 return False 

85 if state.src[pos] == "]": 

86 break 

87 pos += 1 

88 

89 if pos == start + 2: # no empty footnote labels 

90 return False 

91 pos += 1 

92 if pos >= maximum or state.src[pos] != ":": 

93 return False 

94 if silent: 

95 return True 

96 pos += 1 

97 

98 label = state.src[start + 2 : pos - 2] 

99 state.env.setdefault("footnotes", {}).setdefault("refs", {})[":" + label] = -1 

100 

101 open_token = Token("footnote_reference_open", "", 1) 

102 open_token.meta = {"label": label} 

103 open_token.level = state.level 

104 state.level += 1 

105 state.tokens.append(open_token) 

106 

107 oldBMark = state.bMarks[startLine] 

108 oldTShift = state.tShift[startLine] 

109 oldSCount = state.sCount[startLine] 

110 oldParentType = state.parentType 

111 

112 posAfterColon = pos 

113 initial = offset = ( 

114 state.sCount[startLine] 

115 + pos 

116 - (state.bMarks[startLine] + state.tShift[startLine]) 

117 ) 

118 

119 while pos < maximum: 

120 ch = state.src[pos] 

121 

122 if ch == "\t": 

123 offset += 4 - offset % 4 

124 elif ch == " ": 

125 offset += 1 

126 

127 else: 

128 break 

129 

130 pos += 1 

131 

132 state.tShift[startLine] = pos - posAfterColon 

133 state.sCount[startLine] = offset - initial 

134 

135 state.bMarks[startLine] = posAfterColon 

136 state.blkIndent += 4 

137 state.parentType = "footnote" 

138 

139 if state.sCount[startLine] < state.blkIndent: 

140 state.sCount[startLine] += state.blkIndent 

141 

142 state.md.block.tokenize(state, startLine, endLine) 

143 

144 state.parentType = oldParentType 

145 state.blkIndent -= 4 

146 state.tShift[startLine] = oldTShift 

147 state.sCount[startLine] = oldSCount 

148 state.bMarks[startLine] = oldBMark 

149 

150 open_token.map = [startLine, state.line] 

151 

152 token = Token("footnote_reference_close", "", -1) 

153 state.level -= 1 

154 token.level = state.level 

155 state.tokens.append(token) 

156 

157 return True 

158 

159 

160def footnote_inline(state: StateInline, silent: bool) -> bool: 

161 """Process inline footnotes (^[...])""" 

162 

163 maximum = state.posMax 

164 start = state.pos 

165 

166 if start + 2 >= maximum: 

167 return False 

168 if state.src[start] != "^": 

169 return False 

170 if state.src[start + 1] != "[": 

171 return False 

172 

173 labelStart = start + 2 

174 labelEnd = parseLinkLabel(state, start + 1) 

175 

176 # parser failed to find ']', so it's not a valid note 

177 if labelEnd < 0: 

178 return False 

179 

180 # We found the end of the link, and know for a fact it's a valid link 

181 # so all that's left to do is to call tokenizer. 

182 # 

183 if not silent: 

184 refs = state.env.setdefault("footnotes", {}).setdefault("list", {}) 

185 footnoteId = len(refs) 

186 

187 tokens: List[Token] = [] 

188 state.md.inline.parse( 

189 state.src[labelStart:labelEnd], state.md, state.env, tokens 

190 ) 

191 

192 token = state.push("footnote_ref", "", 0) 

193 token.meta = {"id": footnoteId} 

194 

195 refs[footnoteId] = {"content": state.src[labelStart:labelEnd], "tokens": tokens} 

196 

197 state.pos = labelEnd + 1 

198 state.posMax = maximum 

199 return True 

200 

201 

202def footnote_ref(state: StateInline, silent: bool) -> bool: 

203 """Process footnote references ([^...])""" 

204 

205 maximum = state.posMax 

206 start = state.pos 

207 

208 # should be at least 4 chars - "[^x]" 

209 if start + 3 > maximum: 

210 return False 

211 

212 if "footnotes" not in state.env or "refs" not in state.env["footnotes"]: 

213 return False 

214 if state.src[start] != "[": 

215 return False 

216 if state.src[start + 1] != "^": 

217 return False 

218 

219 pos = start + 2 

220 while pos < maximum: 

221 if state.src[pos] == " ": 

222 return False 

223 if state.src[pos] == "\n": 

224 return False 

225 if state.src[pos] == "]": 

226 break 

227 pos += 1 

228 

229 if pos == start + 2: # no empty footnote labels 

230 return False 

231 if pos >= maximum: 

232 return False 

233 pos += 1 

234 

235 label = state.src[start + 2 : pos - 1] 

236 if (":" + label) not in state.env["footnotes"]["refs"]: 

237 return False 

238 

239 if not silent: 

240 if "list" not in state.env["footnotes"]: 

241 state.env["footnotes"]["list"] = {} 

242 

243 if state.env["footnotes"]["refs"][":" + label] < 0: 

244 footnoteId = len(state.env["footnotes"]["list"]) 

245 state.env["footnotes"]["list"][footnoteId] = {"label": label, "count": 0} 

246 state.env["footnotes"]["refs"][":" + label] = footnoteId 

247 else: 

248 footnoteId = state.env["footnotes"]["refs"][":" + label] 

249 

250 footnoteSubId = state.env["footnotes"]["list"][footnoteId]["count"] 

251 state.env["footnotes"]["list"][footnoteId]["count"] += 1 

252 

253 token = state.push("footnote_ref", "", 0) 

254 token.meta = {"id": footnoteId, "subId": footnoteSubId, "label": label} 

255 

256 state.pos = pos 

257 state.posMax = maximum 

258 return True 

259 

260 

261def footnote_tail(state: StateCore) -> None: 

262 """Post-processing step, to move footnote tokens to end of the token stream. 

263 

264 Also removes un-referenced tokens. 

265 """ 

266 

267 insideRef = False 

268 refTokens = {} 

269 

270 if "footnotes" not in state.env: 

271 return 

272 

273 current: List[Token] = [] 

274 tok_filter = [] 

275 for tok in state.tokens: 

276 if tok.type == "footnote_reference_open": 

277 insideRef = True 

278 current = [] 

279 currentLabel = tok.meta["label"] 

280 tok_filter.append(False) 

281 continue 

282 

283 if tok.type == "footnote_reference_close": 

284 insideRef = False 

285 # prepend ':' to avoid conflict with Object.prototype members 

286 refTokens[":" + currentLabel] = current 

287 tok_filter.append(False) 

288 continue 

289 

290 if insideRef: 

291 current.append(tok) 

292 

293 tok_filter.append((not insideRef)) 

294 

295 state.tokens = [t for t, f in zip(state.tokens, tok_filter) if f] 

296 

297 if "list" not in state.env.get("footnotes", {}): 

298 return 

299 foot_list = state.env["footnotes"]["list"] 

300 

301 token = Token("footnote_block_open", "", 1) 

302 state.tokens.append(token) 

303 

304 for i, foot_note in foot_list.items(): 

305 token = Token("footnote_open", "", 1) 

306 token.meta = {"id": i, "label": foot_note.get("label", None)} 

307 # TODO propagate line positions of original foot note 

308 # (but don't store in token.map, because this is used for scroll syncing) 

309 state.tokens.append(token) 

310 

311 if "tokens" in foot_note: 

312 tokens = [] 

313 

314 token = Token("paragraph_open", "p", 1) 

315 token.block = True 

316 tokens.append(token) 

317 

318 token = Token("inline", "", 0) 

319 token.children = foot_note["tokens"] 

320 token.content = foot_note["content"] 

321 tokens.append(token) 

322 

323 token = Token("paragraph_close", "p", -1) 

324 token.block = True 

325 tokens.append(token) 

326 

327 elif "label" in foot_note: 

328 tokens = refTokens[":" + foot_note["label"]] 

329 

330 state.tokens.extend(tokens) 

331 if state.tokens[len(state.tokens) - 1].type == "paragraph_close": 

332 lastParagraph: Optional[Token] = state.tokens.pop() 

333 else: 

334 lastParagraph = None 

335 

336 t = ( 

337 foot_note["count"] 

338 if (("count" in foot_note) and (foot_note["count"] > 0)) 

339 else 1 

340 ) 

341 j = 0 

342 while j < t: 

343 token = Token("footnote_anchor", "", 0) 

344 token.meta = {"id": i, "subId": j, "label": foot_note.get("label", None)} 

345 state.tokens.append(token) 

346 j += 1 

347 

348 if lastParagraph: 

349 state.tokens.append(lastParagraph) 

350 

351 token = Token("footnote_close", "", -1) 

352 state.tokens.append(token) 

353 

354 token = Token("footnote_block_close", "", -1) 

355 state.tokens.append(token) 

356 

357 

358######################################## 

359# Renderer partials 

360 

361 

362def render_footnote_anchor_name( 

363 self: RendererProtocol, 

364 tokens: Sequence[Token], 

365 idx: int, 

366 options: OptionsDict, 

367 env: EnvType, 

368) -> str: 

369 n = str(tokens[idx].meta["id"] + 1) 

370 prefix = "" 

371 

372 doc_id = env.get("docId", None) 

373 if isinstance(doc_id, str): 

374 prefix = f"-{doc_id}-" 

375 

376 return prefix + n 

377 

378 

379def render_footnote_caption( 

380 self: RendererProtocol, 

381 tokens: Sequence[Token], 

382 idx: int, 

383 options: OptionsDict, 

384 env: EnvType, 

385) -> str: 

386 n = str(tokens[idx].meta["id"] + 1) 

387 

388 if tokens[idx].meta.get("subId", -1) > 0: 

389 n += ":" + str(tokens[idx].meta["subId"]) 

390 

391 return "[" + n + "]" 

392 

393 

394def render_footnote_ref( 

395 self: RendererProtocol, 

396 tokens: Sequence[Token], 

397 idx: int, 

398 options: OptionsDict, 

399 env: EnvType, 

400) -> str: 

401 ident: str = self.rules["footnote_anchor_name"](tokens, idx, options, env) # type: ignore[attr-defined] 

402 caption: str = self.rules["footnote_caption"](tokens, idx, options, env) # type: ignore[attr-defined] 

403 refid = ident 

404 

405 if tokens[idx].meta.get("subId", -1) > 0: 

406 refid += ":" + str(tokens[idx].meta["subId"]) 

407 

408 return ( 

409 '<sup class="footnote-ref"><a href="#fn' 

410 + ident 

411 + '" id="fnref' 

412 + refid 

413 + '">' 

414 + caption 

415 + "</a></sup>" 

416 ) 

417 

418 

419def render_footnote_block_open( 

420 self: RendererProtocol, 

421 tokens: Sequence[Token], 

422 idx: int, 

423 options: OptionsDict, 

424 env: EnvType, 

425) -> str: 

426 return ( 

427 ( 

428 '<hr class="footnotes-sep" />\n' 

429 if options.xhtmlOut 

430 else '<hr class="footnotes-sep">\n' 

431 ) 

432 + '<section class="footnotes">\n' 

433 + '<ol class="footnotes-list">\n' 

434 ) 

435 

436 

437def render_footnote_block_close( 

438 self: RendererProtocol, 

439 tokens: Sequence[Token], 

440 idx: int, 

441 options: OptionsDict, 

442 env: EnvType, 

443) -> str: 

444 return "</ol>\n</section>\n" 

445 

446 

447def render_footnote_open( 

448 self: RendererProtocol, 

449 tokens: Sequence[Token], 

450 idx: int, 

451 options: OptionsDict, 

452 env: EnvType, 

453) -> str: 

454 ident: str = self.rules["footnote_anchor_name"](tokens, idx, options, env) # type: ignore[attr-defined] 

455 

456 if tokens[idx].meta.get("subId", -1) > 0: 

457 ident += ":" + tokens[idx].meta["subId"] 

458 

459 return '<li id="fn' + ident + '" class="footnote-item">' 

460 

461 

462def render_footnote_close( 

463 self: RendererProtocol, 

464 tokens: Sequence[Token], 

465 idx: int, 

466 options: OptionsDict, 

467 env: EnvType, 

468) -> str: 

469 return "</li>\n" 

470 

471 

472def render_footnote_anchor( 

473 self: RendererProtocol, 

474 tokens: Sequence[Token], 

475 idx: int, 

476 options: OptionsDict, 

477 env: EnvType, 

478) -> str: 

479 ident: str = self.rules["footnote_anchor_name"](tokens, idx, options, env) # type: ignore[attr-defined] 

480 

481 if tokens[idx].meta["subId"] > 0: 

482 ident += ":" + str(tokens[idx].meta["subId"]) 

483 

484 # ↩ with escape code to prevent display as Apple Emoji on iOS 

485 return ' <a href="#fnref' + ident + '" class="footnote-backref">\u21a9\uFE0E</a>'