toc.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. """
  2. TOC directive
  3. ~~~~~~~~~~~~~
  4. The TOC directive syntax looks like::
  5. .. toc:: Title
  6. :min-level: 1
  7. :max-level: 3
  8. "Title", "min-level", and "max-level" option can be empty. "min-level"
  9. and "max-level" are integers >= 1 and <= 6, which define the allowed
  10. heading levels writers want to include in the table of contents.
  11. """
  12. from typing import TYPE_CHECKING, Any, Dict, Match
  13. from ..toc import normalize_toc_item, render_toc_ul
  14. from ._base import BaseDirective, DirectivePlugin
  15. if TYPE_CHECKING:
  16. from ..block_parser import BlockParser
  17. from ..core import BaseRenderer, BlockState
  18. from ..markdown import Markdown
  19. class TableOfContents(DirectivePlugin):
  20. def __init__(self, min_level: int = 1, max_level: int = 3) -> None:
  21. self.min_level = min_level
  22. self.max_level = max_level
  23. def generate_heading_id(self, token: Dict[str, Any], index: int) -> str:
  24. return "toc_" + str(index + 1)
  25. def parse(self, block: "BlockParser", m: Match[str], state: "BlockState") -> Dict[str, Any]:
  26. title = self.parse_title(m)
  27. options = self.parse_options(m)
  28. if options:
  29. d_options = dict(options)
  30. collapse = "collapse" in d_options
  31. min_level = _normalize_level(d_options, "min-level", self.min_level)
  32. max_level = _normalize_level(d_options, "max-level", self.max_level)
  33. if min_level < self.min_level:
  34. raise ValueError(f'"min-level" option MUST be >= {self.min_level}')
  35. if max_level > self.max_level:
  36. raise ValueError(f'"max-level" option MUST be <= {self.max_level}')
  37. if min_level > max_level:
  38. raise ValueError('"min-level" option MUST be less than "max-level" option')
  39. else:
  40. collapse = False
  41. min_level = self.min_level
  42. max_level = self.max_level
  43. attrs = {
  44. "min_level": min_level,
  45. "max_level": max_level,
  46. "collapse": collapse,
  47. }
  48. return {"type": "toc", "text": title or "", "attrs": attrs}
  49. def toc_hook(self, md: "Markdown", state: "BlockState") -> None:
  50. sections = []
  51. headings = []
  52. for tok in state.tokens:
  53. if tok["type"] == "toc":
  54. sections.append(tok)
  55. elif tok["type"] == "heading":
  56. headings.append(tok)
  57. if sections:
  58. toc_items = []
  59. # adding ID for each heading
  60. for i, tok in enumerate(headings):
  61. tok["attrs"]["id"] = self.generate_heading_id(tok, i)
  62. toc_items.append(normalize_toc_item(md, tok, parent=state))
  63. for sec in sections:
  64. _min = sec["attrs"]["min_level"]
  65. _max = sec["attrs"]["max_level"]
  66. toc = [item for item in toc_items if _min <= item[0] <= _max]
  67. sec["attrs"]["toc"] = toc
  68. def __call__(self, directive: BaseDirective, md: "Markdown") -> None:
  69. if md.renderer and md.renderer.NAME == "html":
  70. # only works with HTML renderer
  71. directive.register("toc", self.parse)
  72. md.before_render_hooks.append(self.toc_hook)
  73. md.renderer.register("toc", render_html_toc)
  74. def render_html_toc(renderer: "BaseRenderer", title: str, collapse: bool = False, **attrs: Any) -> str:
  75. if not title:
  76. title = "Table of Contents"
  77. content = render_toc_ul(attrs["toc"])
  78. html = '<details class="toc"'
  79. if not collapse:
  80. html += " open"
  81. html += ">\n<summary>" + title + "</summary>\n"
  82. return html + content + "</details>\n"
  83. def _normalize_level(options: Dict[str, Any], name: str, default: Any) -> Any:
  84. level = options.get(name)
  85. if not level:
  86. return default
  87. try:
  88. return int(level)
  89. except (ValueError, TypeError):
  90. raise ValueError(f'"{name}" option MUST be integer')