""" 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')