Skip to content

Commit

Permalink
Use toc_tokens to generate the TOC
Browse files Browse the repository at this point in the history
This patch improves the consistency of TOC levels, so now the level is always
equal to the N in the `<hN>` tag. It also allows users of the MkDocs theme to
set the navigation depth to show in the TOC panel (defaulting to 2).
  • Loading branch information
jimporter committed Feb 13, 2020
1 parent 2fca717 commit 71db354
Show file tree
Hide file tree
Showing 15 changed files with 67 additions and 150 deletions.
3 changes: 3 additions & 0 deletions docs/about/release-notes.md
Expand Up @@ -95,6 +95,9 @@ do, adding `--strict`, `--theme`, `--theme-dir`, and `--site-dir`.
theme (#1234).
* Bugfix: Multi-row nav headers in the `mkdocs` theme no longer obscure the
document content (#716).
* Add support for `navigation_depth` theme option for the `mkdocs` theme (#1970).
* `level` attribute in `page.toc` items is now 1-indexed to match the level in
`<hN>` tags (#1970).

## Version 1.0.4 (2018-09-07)

Expand Down
3 changes: 3 additions & 0 deletions docs/user-guide/styling-your-docs.md
Expand Up @@ -73,6 +73,9 @@ supports the following options:

* __`search`__: Display the search modal. Default: `83` (s)

* __`navigation_depth`__: The maximum depth of the navigation tree in the
sidebar. Default: `2`.

* __`nav_style`__: This adjusts the visual style for the top navigation bar; by
default, this is set to `primary` (the default), but it can also be set to
`dark` or `light`.
Expand Down
2 changes: 1 addition & 1 deletion mkdocs/contrib/search/search_index.py
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
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
105 changes: 20 additions & 85 deletions mkdocs/structure/toc.py
@@ -1,16 +1,18 @@
"""
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 +36,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 @@ -49,79 +55,8 @@ def indent_print(self, depth=0):
return ret


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.
# However, in Python3.5 the default was changed to True.
# We need the False behavior in all versions but can only
# set it if it exists.
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

def handle_charref(self, ref):
self.handle_entityref("#" + ref)

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

return ret
def _parse_toc_token(token):
anchor = AnchorLink(token['name'], token['id'], token['level'])
for i in token['children']:
anchor.children.append(_parse_toc_token(i))
return anchor
2 changes: 1 addition & 1 deletion mkdocs/tests/base.py
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
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
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))
5 changes: 3 additions & 2 deletions mkdocs/tests/theme_tests.py
Expand Up @@ -32,8 +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):
Expand Down
22 changes: 9 additions & 13 deletions mkdocs/themes/mkdocs/css/base.css
Expand Up @@ -33,10 +33,6 @@ body > .container {
/* csslint ignore:end */
}

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

.source-links {
float: right;
}
Expand Down Expand Up @@ -168,7 +164,7 @@ footer {
}

/* First level of nav */
.bs-sidenav {
.bs-sidebar > .navbar-collapse > .nav {
padding-top: 10px;
padding-bottom: 10px;
border-radius: 5px;
Expand All @@ -194,16 +190,16 @@ 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 .nav .nav {
margin-left: 1em;
}

.bs-sidebar .nav > li > a {
font-weight: bold;
}

.bs-sidebar .nav .nav > li > a {
padding-top: 3px;
padding-bottom: 3px;
padding-left: 30px;
font-size: 90%;
font-weight: normal;
}

.headerlink {
Expand Down
2 changes: 2 additions & 0 deletions mkdocs/themes/mkdocs/mkdocs_theme.yml
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
22 changes: 14 additions & 8 deletions mkdocs/themes/mkdocs/toc.html
Expand Up @@ -5,15 +5,21 @@
</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>
<ul class="nav flex-column">
{%- for child in item.children %}
{{- toc_item(child) }}
{%- endfor %}
</ul>
</li>
{%- 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 %}
<ul class="nav flex-column">
{%- for item in page.toc %}
{{ toc_item(item) }}
{%- endfor %}
</ul>
</div>
Expand Down
2 changes: 1 addition & 1 deletion requirements/project-min.txt
@@ -1,7 +1,7 @@
click==3.3
Jinja2==2.10.1
livereload==2.5.1
Markdown==3.0.1
Markdown==3.2.1
PyYAML==3.13
tornado==4.1
mdx_gh_links>=0.2
2 changes: 1 addition & 1 deletion requirements/project.txt
@@ -1,7 +1,7 @@
click>=7.0
Jinja2>=2.10.3
livereload>=2.6.1
Markdown>=3.1.1
Markdown>=3.2.1
PyYAML>=5.2
tornado>=5.1.1
mdx_gh_links>=0.2
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -58,7 +58,7 @@ def get_packages(package):
'Jinja2>=2.10.1',
'livereload>=2.5.1',
'lunr[languages]>=0.5.2',
'Markdown>=3.0.1',
'Markdown>=3.2.1',
'PyYAML>=3.10',
'tornado>=5.0'
],
Expand Down

0 comments on commit 71db354

Please sign in to comment.