Table of contents#

Generate heading IDs, render tables of contents, and customize slug behavior for documentation pages.


Use the heading_ids plugin and register the built-in TableOfContents directive renderer when the table of contents should be declared inside the Markdown document.

from wenmode import Wenmode
from wenmode.directives import TableOfContents
from wenmode.plugins import heading_ids
from wenmode.rules import AtxHeading, LeafDirective

wen = Wenmode(
    [AtxHeading, LeafDirective],
    directives=[TableOfContents()],
    plugins=[heading_ids],
)
text = '''
::toc{min=2 max=3}

# Title

## Usage
'''

html = wen.render(text)

assert '<nav aria-label="Table of contents" class="toc">' in html
assert '<a href="#usage">Usage</a>' in html
assert '<h2 id="usage">Usage</h2>' in html

You can also build a table of contents manually when you want to place or style it outside the Markdown document.

from wenmode import HTMLRenderer, Wenmode
from wenmode.headings import Slugger, add_heading_ids
from wenmode.toc import collect_toc, render_toc_html

wen = Wenmode()
text = '''
# Title

## Usage
'''

root = wen.parse(text)
add_heading_ids(root, slugger=Slugger(), min_depth=2)

toc = collect_toc(root, min_depth=2, max_depth=3)
html = render_toc_html(toc) + HTMLRenderer().render(root)

assert '<a href="#usage">Usage</a>' in html

Generate heading IDs only#

Use the heading_ids plugin when you want Wenmode to add generated heading IDs during parsing without rendering a table of contents.

from wenmode import Wenmode
from wenmode.plugins import heading_ids
from wenmode.rules import AtxHeading

wen = Wenmode([AtxHeading], plugins=[heading_ids])
text = '# Hello World'
expected = '''
<h1 id="hello-world">Hello World</h1>
'''

html = wen.render(text)

assert html == expected.lstrip()

For already-parsed trees, use add_heading_ids().

Full-document parsing and add_heading_ids() reserve explicit IDs on headings, directives, and raw HTML before resolving collisions with generated heading IDs. Explicit IDs are preserved, including any duplicates you provide. Raw HTML IDs are conservatively reserved even when the renderer escapes raw HTML.

Incremental parsing cannot reserve IDs from content that has not arrived yet. Use a full-document parse when generated IDs must avoid later explicit IDs.

from wenmode import HTMLRenderer, Wenmode
from wenmode.headings import Slugger, add_heading_ids

text = '''
# Title

## Usage
'''

root = Wenmode().parse(text)
add_heading_ids(root, slugger=Slugger(), min_depth=2)

html = HTMLRenderer().render(root)
assert '<h1>Title</h1>' in html
assert '<h2 id="usage">Usage</h2>' in html

Customize heading slugs#

Create a Slugger subclass when your product needs a different heading ID format. Pass the slugger class to heading_ids.configure() for IDs generated during parsing.

from wenmode import Wenmode
from wenmode.headings import Slugger
from wenmode.plugins import heading_ids
from wenmode.rules import AtxHeading


class PrefixedSlugger(Slugger):
    name = 'prefixed'

    def slug(self, value: str) -> str:
        return 'section-' + super().slug(value)


wen = Wenmode([AtxHeading], plugins=[heading_ids.configure(PrefixedSlugger)])
html = wen.render('## Install\n\n## Install\n')

assert '<h2 id="section-install">Install</h2>' in html
assert '<h2 id="section-install-1">Install</h2>' in html

For already-parsed trees, pass an instance to add_heading_ids().

from wenmode import HTMLRenderer, Wenmode
from wenmode.headings import Slugger, add_heading_ids


class DocsSlugger(Slugger):
    name = 'docs'

    def slug(self, value: str) -> str:
        return 'docs-' + super().slug(value)


root = Wenmode().parse('## Usage\n')
add_heading_ids(root, slugger=DocsSlugger(), min_depth=2)

html = HTMLRenderer().render(root)

assert '<h2 id="docs-usage">Usage</h2>' in html