Repository: mkdocs/mkdocs

Issue:
Adds a AnchorLink.level property
this allows themes to restrict the toc to a certain depth.

here's a proof-of-concept: https://github.com/squidfunk/mkdocs-material/pull/450


Discussion:
Hmm, so this adds a `level` attribute to the `AnchorLink` class to track the "level" of each item in a template. Is that really necessary? Why can't you track that with a template variable? In other words, convince me this is the right approach to the problem.

Regardless, this would need to have some tests. Also, it should be against the 1.0.dev branch. Finally, as this would be a 1.0 feature, the template specific setting (in the associated Material PR) may want to take advantage of the new template settings implemented in #1201,
> Is that really necessary? Why can't you track that with a template variable? In other words, convince me this is the right approach to the problem.

in general i tend to keep the logic in templates at a minimum and rather pass rich context objects. imo this is easier to read in the templates. also, all theme developers and users can easily access this information w/o implementing a logic for it. it's obviously simply implemented and maintainable on the Python side.

thanks for the other hints.
Add some tests and I'll give this another look.
a test is added. the fails are caused by missing interpreters.

Resolving patch:
diff --git a/mkdocs/structure/toc.py b/mkdocs/structure/toc.py
--- a/mkdocs/structure/toc.py
+++ b/mkdocs/structure/toc.py
@@ -39,8 +39,8 @@ class AnchorLink(object):
     """
     A single entry in the table of contents.
     """
-    def __init__(self, title, url):
-        self.title, self.url = title, url
+    def __init__(self, title, url, level):
+        self.title, self.url, self.level = title, url, level
         self.children = []
 
     def __str__(self):
@@ -99,8 +99,7 @@ def _parse_html_table_of_contents(html):
     Returns a list of all the parent AnchorLink instances.
     """
     lines = html.splitlines()[2:-2]
-    parents = []
-    ret = []
+    ret, parents, level = [], [], 0
     for line in lines:
         parser = _TOCParser()
         parser.feed(line)
@@ -110,7 +109,7 @@ def _parse_html_table_of_contents(html):
             except KeyError:
                 continue
             title = parser.title
-            nav = AnchorLink(title, href)
+            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:
@@ -119,8 +118,10 @@ def _parse_html_table_of_contents(html):
                 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()
 
diff --git a/mkdocs/toc.py b/mkdocs/toc.py
deleted file mode 100644
--- a/mkdocs/toc.py
+++ /dev/null
@@ -1,138 +0,0 @@
-# coding: utf-8
-
-"""
-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.
-
-The steps we take to generate a table of contents are:
-
-* Pre-process the markdown, injecting a [TOC] marker.
-* Generate HTML from markdown.
-* Post-process the HTML, spliting the content and the table of contents.
-* Parse table of contents HTML into the underlying data structure.
-"""
-
-from __future__ import unicode_literals
-
-try:                                    # pragma: no cover
-    from html.parser import HTMLParser  # noqa
-except ImportError:                     # pragma: no cover
-    from HTMLParser import HTMLParser   # noqa
-
-
-class TableOfContents(object):
-    """
-    Represents the table of contents for a given page.
-    """
-    def __init__(self, html):
-        self.items = _parse_html_table_of_contents(html)
-
-    def __iter__(self):
-        return iter(self.items)
-
-    def __len__(self):
-        return len(self.items)
-
-    def __str__(self):
-        return ''.join([str(item) for item in self])
-
-
-class AnchorLink(object):
-    """
-    A single entry in the table of contents.
-    """
-    def __init__(self, title, url):
-        self.title, self.url = title, url
-        self.children = []
-
-    def __str__(self):
-        return self.indent_print()
-
-    def indent_print(self, depth=0):
-        indent = '    ' * depth
-        ret = '%s%s - %s\n' % (indent, self.title, self.url)
-        for item in self.children:
-            ret += item.indent_print(depth + 1)
-        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'):
-            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]
-    parents = []
-    ret = []
-    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)
-            # 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>'):
-                parents.append(nav)
-        elif line.startswith('</ul>'):
-            if parents:
-                parents.pop()
-
-    # For the table of contents, always mark the first element as active
-    if ret:
-        ret[0].active = True
-
-    return ret


Test patch:
diff --git a/mkdocs/tests/structure/toc_tests.py b/mkdocs/tests/structure/toc_tests.py
--- a/mkdocs/tests/structure/toc_tests.py
+++ b/mkdocs/tests/structure/toc_tests.py
@@ -178,3 +178,21 @@ def test_skip_no_href(self):
         toc = get_toc(html)
         self.assertEqual(str(toc).strip(), expected)
         self.assertEqual(len(toc), 1)
+
+    def test_level(self):
+        md = dedent("""
+        # Heading 1
+        ## Heading 1.1
+        ### Heading 1.1.1
+        ### Heading 1.1.2
+        ## Heading 1.2
+        """)
+        toc = get_toc(get_markdown_toc(md))
+
+        def get_level_sequence(items):
+            for item in items:
+                yield item.level
+                for c in get_level_sequence(item.children):
+                    yield c
+
+        self.assertEqual(tuple(get_level_sequence(toc)), (0, 1, 2, 2, 1))