toc.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Tuple
  2. from .core import BlockState
  3. from .util import striptags
  4. if TYPE_CHECKING:
  5. from .markdown import Markdown
  6. def add_toc_hook(
  7. md: "Markdown",
  8. min_level: int = 1,
  9. max_level: int = 3,
  10. heading_id: Optional[Callable[[Dict[str, Any], int], str]] = None,
  11. ) -> None:
  12. """Add a hook to save toc items into ``state.env``. This is
  13. usually helpful for doc generator::
  14. import mistune
  15. from mistune.toc import add_toc_hook, render_toc_ul
  16. md = mistune.create_markdown(...)
  17. add_toc_hook(md)
  18. html, state = md.parse(text)
  19. toc_items = state.env['toc_items']
  20. toc_html = render_toc_ul(toc_items)
  21. :param md: Markdown instance
  22. :param min_level: min heading level
  23. :param max_level: max heading level
  24. :param heading_id: a function to generate heading_id
  25. """
  26. if heading_id is None:
  27. def heading_id(token: Dict[str, Any], index: int) -> str:
  28. return "toc_" + str(index + 1)
  29. def toc_hook(md: "Markdown", state: "BlockState") -> None:
  30. headings = []
  31. for tok in state.tokens:
  32. if tok["type"] == "heading":
  33. level = tok["attrs"]["level"]
  34. if min_level <= level <= max_level:
  35. headings.append(tok)
  36. toc_items = []
  37. for i, tok in enumerate(headings):
  38. tok["attrs"]["id"] = heading_id(tok, i)
  39. toc_items.append(normalize_toc_item(md, tok, parent=state))
  40. # save items into state
  41. state.env["toc_items"] = toc_items
  42. md.before_render_hooks.append(toc_hook)
  43. def normalize_toc_item(md: "Markdown", token: Dict[str, Any], parent: Optional[Any] = None) -> Tuple[int, str, str]:
  44. text = token["text"]
  45. tokens = md.inline(text, parent.env if parent else {})
  46. assert md.renderer is not None
  47. html = md.renderer(tokens, BlockState())
  48. text = striptags(html)
  49. attrs = token["attrs"]
  50. return attrs["level"], attrs["id"], text
  51. def render_toc_ul(toc: Iterable[Tuple[int, str, str]]) -> str:
  52. """Render a <ul> table of content HTML. The param "toc" should
  53. be formatted into this structure::
  54. [
  55. (level, id, text),
  56. ]
  57. For example::
  58. [
  59. (1, 'toc-intro', 'Introduction'),
  60. (2, 'toc-install', 'Install'),
  61. (2, 'toc-upgrade', 'Upgrade'),
  62. (1, 'toc-license', 'License'),
  63. ]
  64. """
  65. if not toc:
  66. return ""
  67. s = ""
  68. levels: List[int] = []
  69. for level, k, text in toc:
  70. item = '<a href="#{}">{}</a>'.format(k, text)
  71. if not levels:
  72. s += "<li>" + item
  73. levels.append(level)
  74. elif level == levels[-1]:
  75. s += "</li>\n<li>" + item
  76. elif level > levels[-1]:
  77. s += "\n<ul>\n<li>" + item
  78. levels.append(level)
  79. else:
  80. levels.pop()
  81. while levels:
  82. last_level = levels.pop()
  83. if level == last_level:
  84. s += "</li>\n</ul>\n</li>\n<li>" + item
  85. levels.append(level)
  86. break
  87. elif level > last_level:
  88. s += "</li>\n<li>" + item
  89. levels.append(last_level)
  90. levels.append(level)
  91. break
  92. else:
  93. s += "</li>\n</ul>\n"
  94. else:
  95. levels.append(level)
  96. s += "</li>\n<li>" + item
  97. while len(levels) > 1:
  98. s += "</li>\n</ul>\n"
  99. levels.pop()
  100. if not s:
  101. return ""
  102. return "<ul>\n" + s + "</li>\n</ul>\n"