summaryrefslogtreecommitdiff
path: root/src/mistune/directives/toc.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/mistune/directives/toc.py')
-rw-r--r--src/mistune/directives/toc.py105
1 files changed, 105 insertions, 0 deletions
diff --git a/src/mistune/directives/toc.py b/src/mistune/directives/toc.py
new file mode 100644
index 0000000..4084f43
--- /dev/null
+++ b/src/mistune/directives/toc.py
@@ -0,0 +1,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')