Skip to content

Commit

Permalink
[WIP] Use toc_tokens to generate the TOC
Browse files Browse the repository at this point in the history
  • Loading branch information
jimporter committed Jan 30, 2020
1 parent 535505b commit aeb5dac
Show file tree
Hide file tree
Showing 10 changed files with 58 additions and 123 deletions.
2 changes: 1 addition & 1 deletion mkdocs/contrib/search/search_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def _find_toc_by_id(self, toc, id_):
and return the matched item in the TOC.
"""
for toc_item in toc:
if toc_item.url[1:] == id_:
if toc_item.id == id_:
return toc_item
toc_item_r = self._find_toc_by_id(toc_item.children, id_)
if toc_item_r is not None:
Expand Down
2 changes: 1 addition & 1 deletion mkdocs/structure/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def render(self, config, files):
extension_configs=config['mdx_configs'] or {}
)
self.content = md.convert(self.markdown)
self.toc = get_toc(getattr(md, 'toc', ''))
self.toc = get_toc(getattr(md, 'toc_tokens', []))


class _RelativePathTreeprocessor(Treeprocessor):
Expand Down
84 changes: 23 additions & 61 deletions mkdocs/structure/toc.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
"""
Deals with generating the per-page table of contents.
For the sake of simplicity we use an existing markdown extension to generate
an HTML table of contents, and then parse that into the underlying data.
For the sake of simplicity we use the Python-Markdown `toc` extension to
generate a list of dicts for each toc item, and then store it as AnchorLinks to
maintain compatibility with older versions of MkDocs.
"""

from html.parser import HTMLParser


def get_toc(toc_html):
items = _parse_html_table_of_contents(toc_html)
return TableOfContents(items)
def get_toc(toc_tokens):
toc = [_parse_toc_token(i) for i in toc_tokens]
# For the table of contents, always mark the first element as active
if len(toc):
toc[0].active = True
return TableOfContents(toc)


class TableOfContents:
Expand All @@ -34,10 +38,14 @@ class AnchorLink:
"""
A single entry in the table of contents.
"""
def __init__(self, title, url, level):
self.title, self.url, self.level = title, url, level
def __init__(self, title, id, level):
self.title, self.id, self.level = title, id, level
self.children = []

@property
def url(self):
return '#' + self.id

def __str__(self):
return self.indent_print()

Expand All @@ -52,10 +60,6 @@ def indent_print(self, depth=0):
class _TOCParser(HTMLParser):
def __init__(self):
HTMLParser.__init__(self)
self.links = []

self.in_anchor = False
self.attrs = None
self.title = ''

# Prior to Python3.4 no convert_charrefs keyword existed.
Expand All @@ -65,19 +69,8 @@ def __init__(self):
if hasattr(self, 'convert_charrefs'): # pragma: no cover
self.convert_charrefs = False

def handle_starttag(self, tag, attrs):
if not self.in_anchor:
if tag == 'a':
self.in_anchor = True
self.attrs = dict(attrs)

def handle_endtag(self, tag):
if tag == 'a':
self.in_anchor = False

def handle_data(self, data):
if self.in_anchor:
self.title += data
self.title += data

def handle_charref(self, ref):
self.handle_entityref("#" + ref)
Expand All @@ -86,42 +79,11 @@ def handle_entityref(self, ref):
self.handle_data("&%s;" % ref)


def _parse_html_table_of_contents(html):
"""
Given a table of contents string that has been automatically generated by
the markdown library, parse it into a tree of AnchorLink instances.
Returns a list of all the parent AnchorLink instances.
"""
lines = html.splitlines()[2:-2]
ret, parents, level = [], [], 0
for line in lines:
parser = _TOCParser()
parser.feed(line)
if parser.title:
try:
href = parser.attrs['href']
except KeyError:
continue
title = parser.title
nav = AnchorLink(title, href, level)
# Add the item to its parent if required. If it is a topmost
# item then instead append it to our return value.
if parents:
parents[-1].children.append(nav)
else:
ret.append(nav)
# If this item has children, store it as the current parent
if line.endswith('<ul>'):
level += 1
parents.append(nav)
elif line.startswith('</ul>'):
level -= 1
if parents:
parents.pop()

# For the table of contents, always mark the first element as active
if ret:
ret[0].active = True
def _parse_toc_token(token):
parser = _TOCParser()
parser.feed(token['name'])
anchor = AnchorLink(parser.title, token['id'], token['level'])

return ret
for i in token['children']:
anchor.children.append(_parse_toc_token(i))
return anchor
2 changes: 1 addition & 1 deletion mkdocs/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def get_markdown_toc(markdown_source):
""" Return TOC generated by Markdown parser from Markdown source text. """
md = markdown.Markdown(extensions=['toc'])
md.convert(markdown_source)
return md.toc
return md.toc_tokens


def load_config(**cfg):
Expand Down
10 changes: 6 additions & 4 deletions mkdocs/tests/config/config_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,9 @@ def test_theme(self):
'highlightjs': True,
'hljs_style': 'github',
'hljs_languages': [],
'shortcuts': {'help': 191, 'next': 78, 'previous': 80, 'search': 83},
'nav_style': 'primary'
'navigation_depth': 2,
'nav_style': 'primary',
'shortcuts': {'help': 191, 'next': 78, 'previous': 80, 'search': 83}
}
}, {
'dirs': [os.path.join(theme_dir, 'readthedocs'), mkdocs_templates_dir],
Expand Down Expand Up @@ -182,8 +183,9 @@ def test_theme(self):
'highlightjs': True,
'hljs_style': 'github',
'hljs_languages': [],
'shortcuts': {'help': 191, 'next': 78, 'previous': 80, 'search': 83},
'nav_style': 'primary'
'navigation_depth': 2,
'nav_style': 'primary',
'shortcuts': {'help': 191, 'next': 78, 'previous': 80, 'search': 83}
}
}
)
Expand Down
33 changes: 1 addition & 32 deletions mkdocs/tests/structure/toc_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,6 @@

class TableOfContentsTests(unittest.TestCase):

def test_html_toc(self):
html = dedent("""
<div class="toc">
<ul>
<li><a href="#foo">Heading 1</a></li>
<li><a href="#bar">Heading 2</a></li>
</ul>
</div>
""")
expected = dedent("""
Heading 1 - #foo
Heading 2 - #bar
""")
toc = get_toc(html)
self.assertEqual(str(toc).strip(), expected)
self.assertEqual(len(toc), 2)

def test_indented_toc(self):
md = dedent("""
# Heading 1
Expand Down Expand Up @@ -163,20 +146,6 @@ def test_charref(self):
self.assertEqual(str(toc).strip(), expected)
self.assertEqual(len(toc), 1)

def test_skip_no_href(self):
html = dedent("""
<div class="toc">
<ul>
<li><a>Header 1</a></li>
<li><a href="#foo">Header 2</a></li>
</ul>
</div>
""")
expected = 'Header 2 - #foo'
toc = get_toc(html)
self.assertEqual(str(toc).strip(), expected)
self.assertEqual(len(toc), 1)

def test_level(self):
md = dedent("""
# Heading 1
Expand All @@ -192,4 +161,4 @@ def get_level_sequence(items):
yield item.level
yield from get_level_sequence(item.children)

self.assertEqual(tuple(get_level_sequence(toc)), (0, 1, 2, 2, 1))
self.assertEqual(tuple(get_level_sequence(toc)), (1, 2, 3, 3, 2))
6 changes: 3 additions & 3 deletions mkdocs/tests/theme_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ def test_simple_theme(self):
'highlightjs': True,
'hljs_style': 'github',
'hljs_languages': [],
'shortcuts': {'help': 191, 'next': 78, 'previous': 80, 'search': 83},
'nav_style': 'primary'
})
'navigation_depth': 2,
'nav_style': 'primary',
'shortcuts': {'help': 191, 'next': 78, 'previous': 80, 'search': 83} })

def test_custom_dir(self):
custom = tempfile.mkdtemp()
Expand Down
23 changes: 10 additions & 13 deletions mkdocs/themes/mkdocs/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,6 @@ body > .container {
min-height: 400px;
}

ul.nav .main {
font-weight: bold;
}

.source-links {
float: right;
}
Expand Down Expand Up @@ -186,16 +182,17 @@ footer {
border-right: 1px solid;
}

/* Nav: second level (shown on .active) */
.bs-sidebar .nav .nav {
display: none; /* Hide by default, but at >768px, show it */
margin-bottom: 8px;
.bs-sidebar .nav > li {
margin-left: 1em;
}
.bs-sidebar .nav .nav > li > a {
padding-top: 3px;
padding-bottom: 3px;
padding-left: 30px;
font-size: 90%;

.bs-sidebar .nav > li[data-level="0"] {
font-weight: bold;
margin-left: 0;
}

.bs-sidebar .nav > li[data-level="1"] {
margin-left: 0;
}

.headerlink {
Expand Down
2 changes: 2 additions & 0 deletions mkdocs/themes/mkdocs/mkdocs_theme.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ search_index_only: false
highlightjs: true
hljs_languages: []
hljs_style: github

navigation_depth: 2
nav_style: primary

shortcuts:
Expand Down
17 changes: 10 additions & 7 deletions mkdocs/themes/mkdocs/toc.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@
</button>
</div>

{% macro toc_item(item) %}
{%- if item.level <= config.theme.navigation_depth %}
<li class="nav-item" data-level="{{ item.level }}"><a href="{{ item.url }}" class="nav-link">{{ item.title }}</a></li>
{%- for child in item.children %}
{{- toc_item(child) }}
{%- endfor %}
{%- endif %}
{%- endmacro %}
<div id="toc-collapse" class="navbar-collapse collapse card bg-secondary">
<ul class="nav flex-column bs-sidenav">
{%- for toc_item in page.toc %}
<li class="nav-item main"><a href="{{ toc_item.url }}">{{ toc_item.title }}</a></li>
{%- for toc_item in toc_item.children %}
<li class="nav-item">
<a href="{{ toc_item.url }}" class="nav-link{% if toc_item.active %} active{% endif %}">{{ toc_item.title }}</a>
</li>
{%- endfor %}
{%- for item in page.toc %}
{{ toc_item(item) }}
{%- endfor %}
</ul>
</div>
Expand Down

0 comments on commit aeb5dac

Please sign in to comment.