| """ |
| TOC directive |
| ~~~~~~~~~~~~~ |
| |
| The TOC directive syntax looks like:: |
| |
| .. toc:: Title |
| :min-level: 1 |
| :max-level: 3 |
| |
| "Title", "min-level", and "max-level" option can be empty. "min-level" |
| and "max-level" are integers >= 1 and <= 6, which define the allowed |
| heading levels writers want to include in the table of contents. |
| """ |
|
|
| from typing import TYPE_CHECKING, Any, Dict, Match |
|
|
| from ..toc import normalize_toc_item, render_toc_ul |
| from ._base import BaseDirective, DirectivePlugin |
|
|
| if TYPE_CHECKING: |
| from ..block_parser import BlockParser |
| from ..core import BaseRenderer, BlockState |
| from ..markdown import Markdown |
|
|
|
|
| class TableOfContents(DirectivePlugin): |
| def __init__(self, min_level: int = 1, max_level: int = 3) -> None: |
| self.min_level = min_level |
| self.max_level = max_level |
|
|
| def generate_heading_id(self, token: Dict[str, Any], index: int) -> str: |
| return "toc_" + str(index + 1) |
|
|
| def parse(self, block: "BlockParser", m: Match[str], state: "BlockState") -> Dict[str, Any]: |
| title = self.parse_title(m) |
| options = self.parse_options(m) |
| if options: |
| d_options = dict(options) |
| collapse = "collapse" in d_options |
| min_level = _normalize_level(d_options, "min-level", self.min_level) |
| max_level = _normalize_level(d_options, "max-level", self.max_level) |
| if min_level < self.min_level: |
| raise ValueError(f'"min-level" option MUST be >= {self.min_level}') |
| if max_level > self.max_level: |
| raise ValueError(f'"max-level" option MUST be <= {self.max_level}') |
| if min_level > max_level: |
| raise ValueError('"min-level" option MUST be less than "max-level" option') |
| else: |
| collapse = False |
| min_level = self.min_level |
| max_level = self.max_level |
|
|
| attrs = { |
| "min_level": min_level, |
| "max_level": max_level, |
| "collapse": collapse, |
| } |
| return {"type": "toc", "text": title or "", "attrs": attrs} |
|
|
| def toc_hook(self, md: "Markdown", state: "BlockState") -> None: |
| sections = [] |
| headings = [] |
|
|
| for tok in state.tokens: |
| if tok["type"] == "toc": |
| sections.append(tok) |
| elif tok["type"] == "heading": |
| headings.append(tok) |
|
|
| if sections: |
| toc_items = [] |
| |
| for i, tok in enumerate(headings): |
| tok["attrs"]["id"] = self.generate_heading_id(tok, i) |
| toc_items.append(normalize_toc_item(md, tok)) |
|
|
| for sec in sections: |
| _min = sec["attrs"]["min_level"] |
| _max = sec["attrs"]["max_level"] |
| toc = [item for item in toc_items if _min <= item[0] <= _max] |
| sec["attrs"]["toc"] = toc |
|
|
| def __call__(self, directive: BaseDirective, md: "Markdown") -> None: |
| if md.renderer and md.renderer.NAME == "html": |
| |
| directive.register("toc", self.parse) |
| md.before_render_hooks.append(self.toc_hook) |
| md.renderer.register("toc", render_html_toc) |
|
|
|
|
| def render_html_toc(renderer: "BaseRenderer", title: str, collapse: bool = False, **attrs: Any) -> str: |
| if not title: |
| title = "Table of Contents" |
| content = render_toc_ul(attrs["toc"]) |
|
|
| html = '<details class="toc"' |
| if not collapse: |
| html += " open" |
| html += ">\n<summary>" + title + "</summary>\n" |
| return html + content + "</details>\n" |
|
|
|
|
| def _normalize_level(options: Dict[str, Any], name: str, default: Any) -> Any: |
| level = options.get(name) |
| if not level: |
| return default |
| try: |
| return int(level) |
| except (ValueError, TypeError): |
| raise ValueError(f'"{name}" option MUST be integer') |
|
|