footnotes.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import re
  2. from typing import TYPE_CHECKING, Any, Dict, List, Match, Union
  3. from ..core import BlockState
  4. from ..helpers import LINK_LABEL
  5. from ..util import unikey
  6. if TYPE_CHECKING:
  7. from ..block_parser import BlockParser
  8. from ..core import BaseRenderer, InlineState
  9. from ..inline_parser import InlineParser
  10. from ..markdown import Markdown
  11. __all__ = ["footnotes"]
  12. _PARAGRAPH_SPLIT = re.compile(r"\n{2,}")
  13. # https://michelf.ca/projects/php-markdown/extra/#footnotes
  14. REF_FOOTNOTE = (
  15. r"^(?P<footnote_lead> {0,4})"
  16. r"\[\^(?P<footnote_key>" + LINK_LABEL + r")]:[ \t\n]"
  17. r"(?P<footnote_text>[^\n]*(?:\n+|$)"
  18. r"(?:(?P=footnote_lead) {1,4}(?! )[^\n]*\n+)*"
  19. r")"
  20. )
  21. INLINE_FOOTNOTE = r"\[\^(?P<footnote_key>" + LINK_LABEL + r")\]"
  22. def parse_inline_footnote(inline: "InlineParser", m: Match[str], state: "InlineState") -> int:
  23. key = unikey(m.group("footnote_key"))
  24. ref = state.env.get("ref_footnotes")
  25. if ref and key in ref:
  26. notes = state.env.get("footnotes")
  27. if not notes:
  28. notes = []
  29. if key not in notes:
  30. notes.append(key)
  31. state.env["footnotes"] = notes
  32. state.append_token({"type": "footnote_ref", "raw": key, "attrs": {"index": notes.index(key) + 1}})
  33. else:
  34. state.append_token({"type": "text", "raw": m.group(0)})
  35. return m.end()
  36. def parse_ref_footnote(block: "BlockParser", m: Match[str], state: BlockState) -> int:
  37. ref = state.env.get("ref_footnotes")
  38. if not ref:
  39. ref = {}
  40. key = unikey(m.group("footnote_key"))
  41. if key not in ref:
  42. ref[key] = m.group("footnote_text")
  43. state.env["ref_footnotes"] = ref
  44. return m.end()
  45. def parse_footnote_item(block: "BlockParser", key: str, index: int, state: BlockState) -> Dict[str, Any]:
  46. ref = state.env.get("ref_footnotes")
  47. if not ref:
  48. raise ValueError("Missing 'ref_footnotes'.")
  49. text = ref[key]
  50. lines = text.splitlines()
  51. second_line = None
  52. for second_line in lines[1:]:
  53. if second_line:
  54. break
  55. if second_line:
  56. spaces = len(second_line) - len(second_line.lstrip())
  57. pattern = re.compile(r"^ {" + str(spaces) + r",}", flags=re.M)
  58. text = pattern.sub("", text).strip()
  59. footer_state = BlockState()
  60. footer_state.process(text)
  61. block.parse(footer_state)
  62. children = footer_state.tokens
  63. else:
  64. text = text.strip()
  65. children = [{"type": "paragraph", "text": text}]
  66. return {"type": "footnote_item", "children": children, "attrs": {"key": key, "index": index}}
  67. def md_footnotes_hook(
  68. md: "Markdown", result: Union[str, List[Dict[str, Any]]], state: BlockState
  69. ) -> Union[str, List[Dict[str, Any]]]:
  70. notes = state.env.get("footnotes")
  71. if not notes:
  72. return result
  73. children = [parse_footnote_item(md.block, k, i + 1, state) for i, k in enumerate(notes)]
  74. state = BlockState(parent=state)
  75. state.tokens = [{"type": "footnotes", "children": children}]
  76. output = md.render_state(state)
  77. return result + output # type: ignore[operator]
  78. def render_footnote_ref(renderer: "BaseRenderer", key: str, index: int) -> str:
  79. i = str(index)
  80. html = '<sup class="footnote-ref" id="fnref-' + i + '">'
  81. return html + '<a href="#fn-' + i + '">' + i + "</a></sup>"
  82. def render_footnotes(renderer: "BaseRenderer", text: str) -> str:
  83. return '<section class="footnotes">\n<ol>\n' + text + "</ol>\n</section>\n"
  84. def render_footnote_item(renderer: "BaseRenderer", text: str, key: str, index: int) -> str:
  85. i = str(index)
  86. back = '<a href="#fnref-' + i + '" class="footnote">&#8617;</a>'
  87. text = text.rstrip()
  88. if text.endswith("</p>"):
  89. text = text[:-4] + back + "</p>"
  90. else:
  91. text = text + "\n" + back
  92. return '<li id="fn-' + i + '">' + text + "</li>\n"
  93. def footnotes(md: "Markdown") -> None:
  94. """A mistune plugin to support footnotes, spec defined at
  95. https://michelf.ca/projects/php-markdown/extra/#footnotes
  96. Here is an example:
  97. .. code-block:: text
  98. That's some text with a footnote.[^1]
  99. [^1]: And that's the footnote.
  100. It will be converted into HTML:
  101. .. code-block:: html
  102. <p>That's some text with a footnote.<sup class="footnote-ref" id="fnref-1"><a href="#fn-1">1</a></sup></p>
  103. <section class="footnotes">
  104. <ol>
  105. <li id="fn-1"><p>And that's the footnote.<a href="#fnref-1" class="footnote">&#8617;</a></p></li>
  106. </ol>
  107. </section>
  108. :param md: Markdown instance
  109. """
  110. md.inline.register(
  111. "footnote",
  112. INLINE_FOOTNOTE,
  113. parse_inline_footnote,
  114. before="link",
  115. )
  116. md.block.register(
  117. "ref_footnote",
  118. REF_FOOTNOTE,
  119. parse_ref_footnote,
  120. before="ref_link",
  121. )
  122. md.after_render_hooks.append(md_footnotes_hook)
  123. if md.renderer and md.renderer.NAME == "html":
  124. md.renderer.register("footnote_ref", render_footnote_ref)
  125. md.renderer.register("footnote_item", render_footnote_item)
  126. md.renderer.register("footnotes", render_footnotes)