""" 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 = [] # adding ID for each heading 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": # only works with HTML renderer 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 = '
\n" return html + content + "
\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')