216 lines
5.8 KiB
Python
216 lines
5.8 KiB
Python
"""
|
|
TOC directive
|
|
~~~~~~~~~~~~~
|
|
|
|
The TOC directive syntax looks like::
|
|
|
|
.. toc:: Title
|
|
:depth: 3
|
|
|
|
"Title" and "depth" option can be empty. "depth" is an integer less
|
|
than 6, which defines the max heading level writers want to include
|
|
in TOC.
|
|
"""
|
|
|
|
from .base import Directive
|
|
|
|
|
|
class DirectiveToc(Directive):
|
|
def __init__(self, depth=3):
|
|
self.depth = depth
|
|
|
|
def parse(self, block, m, state):
|
|
title = m.group('value')
|
|
depth = None
|
|
options = self.parse_options(m)
|
|
if options:
|
|
depth = dict(options).get('depth')
|
|
if depth:
|
|
try:
|
|
depth = int(depth)
|
|
except (ValueError, TypeError):
|
|
return {
|
|
'type': 'block_error',
|
|
'raw': 'TOC depth MUST be integer',
|
|
}
|
|
|
|
return {'type': 'toc', 'raw': None, 'params': (title, depth)}
|
|
|
|
def reset_toc_state(self, md, s, state):
|
|
state['toc_depth'] = self.depth
|
|
state['toc_headings'] = []
|
|
return s, state
|
|
|
|
def register_plugin(self, md):
|
|
md.block.tokenize_heading = record_toc_heading
|
|
md.before_parse_hooks.append(self.reset_toc_state)
|
|
md.before_render_hooks.append(md_toc_hook)
|
|
|
|
if md.renderer.NAME == 'html':
|
|
md.renderer.register('theading', render_html_theading)
|
|
elif md.renderer.NAME == 'ast':
|
|
md.renderer.register('theading', render_ast_theading)
|
|
|
|
def __call__(self, md):
|
|
self.register_directive(md, 'toc')
|
|
self.register_plugin(md)
|
|
|
|
if md.renderer.NAME == 'html':
|
|
md.renderer.register('toc', render_html_toc)
|
|
elif md.renderer.NAME == 'ast':
|
|
md.renderer.register('toc', render_ast_toc)
|
|
|
|
|
|
def record_toc_heading(text, level, state):
|
|
# we will use this method to replace tokenize_heading
|
|
tid = 'toc_' + str(len(state['toc_headings']) + 1)
|
|
state['toc_headings'].append((tid, text, level))
|
|
return {'type': 'theading', 'text': text, 'params': (level, tid)}
|
|
|
|
|
|
def md_toc_hook(md, tokens, state):
|
|
headings = state.get('toc_headings')
|
|
if not headings:
|
|
return tokens
|
|
|
|
# add TOC items into the given location
|
|
default_depth = state.get('toc_depth', 3)
|
|
headings = list(_cleanup_headings_text(md.inline, headings, state))
|
|
for tok in tokens:
|
|
if tok['type'] == 'toc':
|
|
params = tok['params']
|
|
depth = params[1] or default_depth
|
|
items = [d for d in headings if d[2] <= depth]
|
|
tok['raw'] = items
|
|
return tokens
|
|
|
|
|
|
def render_ast_toc(items, title, depth):
|
|
return {
|
|
'type': 'toc',
|
|
'items': [list(d) for d in items],
|
|
'title': title,
|
|
'depth': depth,
|
|
}
|
|
|
|
|
|
def render_ast_theading(children, level, tid):
|
|
return {
|
|
'type': 'heading', 'children': children,
|
|
'level': level, 'id': tid,
|
|
}
|
|
|
|
|
|
def render_html_toc(items, title, depth):
|
|
html = '<section class="toc">\n'
|
|
if title:
|
|
html += '<h1>' + title + '</h1>\n'
|
|
|
|
return html + render_toc_ul(items) + '</section>\n'
|
|
|
|
|
|
def render_html_theading(text, level, tid):
|
|
tag = 'h' + str(level)
|
|
return '<' + tag + ' id="' + tid + '">' + text + '</' + tag + '>\n'
|
|
|
|
|
|
def extract_toc_items(md, s):
|
|
"""Extract TOC headings into list structure of::
|
|
|
|
[
|
|
('toc_1', 'Introduction', 1),
|
|
('toc_2', 'Install', 2),
|
|
('toc_3', 'Upgrade', 2),
|
|
('toc_4', 'License', 1),
|
|
]
|
|
|
|
:param md: Markdown Instance with TOC plugin.
|
|
:param s: text string.
|
|
"""
|
|
s, state = md.before_parse(s, {})
|
|
md.block.parse(s, state)
|
|
headings = state.get('toc_headings')
|
|
if not headings:
|
|
return []
|
|
return list(_cleanup_headings_text(md.inline, headings, state))
|
|
|
|
|
|
def render_toc_ul(toc):
|
|
"""Render a <ul> table of content HTML. The param "toc" should
|
|
be formatted into this structure::
|
|
|
|
[
|
|
(toc_id, text, level),
|
|
]
|
|
|
|
For example::
|
|
|
|
[
|
|
('toc-intro', 'Introduction', 1),
|
|
('toc-install', 'Install', 2),
|
|
('toc-upgrade', 'Upgrade', 2),
|
|
('toc-license', 'License', 1),
|
|
]
|
|
"""
|
|
if not toc:
|
|
return ''
|
|
|
|
s = '<ul>\n'
|
|
levels = []
|
|
for k, text, level in toc:
|
|
item = '<a href="#{}">{}</a>'.format(k, text)
|
|
if not levels:
|
|
s += '<li>' + item
|
|
levels.append(level)
|
|
elif level == levels[-1]:
|
|
s += '</li>\n<li>' + item
|
|
elif level > levels[-1]:
|
|
s += '\n<ul>\n<li>' + item
|
|
levels.append(level)
|
|
else:
|
|
last_level = levels.pop()
|
|
while levels:
|
|
last_level = levels.pop()
|
|
if level == last_level:
|
|
s += '</li>\n</ul>\n</li>\n<li>' + item
|
|
levels.append(level)
|
|
break
|
|
elif level > last_level:
|
|
s += '</li>\n<li>' + item
|
|
levels.append(last_level)
|
|
levels.append(level)
|
|
break
|
|
else:
|
|
s += '</li>\n</ul>\n'
|
|
else:
|
|
levels.append(level)
|
|
s += '</li>\n<li>' + item
|
|
|
|
while len(levels) > 1:
|
|
s += '</li>\n</ul>\n'
|
|
levels.pop()
|
|
|
|
return s + '</li>\n</ul>\n'
|
|
|
|
|
|
def _cleanup_headings_text(inline, items, state):
|
|
for item in items:
|
|
text = item[1]
|
|
tokens = inline._scan(text, state, inline.rules)
|
|
text = ''.join(_inline_token_text(tok) for tok in tokens)
|
|
yield item[0], text, item[2]
|
|
|
|
|
|
def _inline_token_text(token):
|
|
tok_type = token[0]
|
|
if tok_type == 'inline_html':
|
|
return ''
|
|
|
|
if len(token) == 2:
|
|
return token[1]
|
|
|
|
if tok_type in {'image', 'link'}:
|
|
return token[2]
|
|
|
|
return ''
|