summaryrefslogtreecommitdiff
path: root/src/mistune/directives/toc.py
blob: 4084f438e2ae6f9f592d3fc4b70d6ec9e032d503 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
"""
    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 ._base import DirectivePlugin
from ..toc import normalize_toc_item, render_toc_ul


class TableOfContents(DirectivePlugin):
    def __init__(self, min_level=1, max_level=3):
        self.min_level = min_level
        self.max_level = max_level

    def generate_heading_id(self, token, index):
        return 'toc_' + str(index + 1)

    def parse(self, block, m, state):
        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, state):
        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, md):
        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, title, collapse=False, **attrs):
    if not title:
        title = 'Table of Contents'
    toc = attrs['toc']
    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, name, default):
    level = options.get(name)
    if not level:
        return default
    try:
        return int(level)
    except (ValueError, TypeError):
        raise ValueError(f'"{name}" option MUST be integer')