adrienbrignon / mkdocs-exporter

⚡ The fastest and most configurable plugin for MkDocs, allowing seamless export of individual pages and/or entire documentation as PDF documents.
https://adrienbrignon.github.io/mkdocs-exporter/
MIT License
81 stars 8 forks source link

How to generate a full-site Table of Contents when using aggregator. #46

Open nbanyan opened 3 days ago

nbanyan commented 3 days ago

The Python Markdown TOC plugin works well for individual pages, but is there a good way to add a full TOC after the first cover page that would list the entire site when using the aggregator? All other PDF generators I've tried for Material have included TOCs by default, but yours is easier to use and install while also actual supporting Javascript.

Potential Alternative Solutions:

nbanyan commented 13 hours ago

I finally managed to create an MkDocs hook to create a table of contents and properly insert it into the site pages in a way that it is included in the aggregation. However, doing so exposed another missing feature. In the aggregate PDF, hyperlinks between pages link back to the served website instead of their associated destination in the PDF. Until that is implemented, the following code for generating an aggregate TOC is not useful for this plugin.

import markdown
from bs4 import BeautifulSoup
from mkdocs.structure.nav import get_navigation, Section
from mkdocs.config import Config
from mkdocs.structure.pages import Page
from mkdocs.structure.files import Files, File, InclusionLevel

def _get_page_toc(page: Page) -> str:
    '''
    Generate the HTML unordered list part of the Table of Contents for `page`.

    Args:
        page (Page): The page to create a Table of Contents from

    Returns:
        str: An HTML unordered list
    '''
    content = ''
    url = page.file.abs_src_path
    with open(url, 'r') as file:
        # render markdown file
        page_toc = markdown.Markdown(extensions=['toc', 'fenced_code', 'md_in_html', 'attr_list', 'meta'])
        page_toc.convert(file.read())
        content = page_toc.toc
        # add page path to anchor links
        content = content.replace('="#', '="' + page.file.dest_path + '#')
        # extract only the unordered list
        soup = BeautifulSoup(content, 'html.parser')
        content = str(soup.find('ul'))

    return content

def _nav_to_html(nav_items: list) -> str:
    '''
    Converts the MkDocs Navigation.items into an HTML string

    Args:
        nav_items (list): The contents of the `items` property of a Navigation object.

    Returns:
        str: A representation of the Navigation as an HTML string
    '''
    content = []
    for item in nav_items:
        if isinstance(item, Page):
            if item.markdown is None:
                item.markdown = ''
            title = item.title
            if title is not None:
                content.append('<li><a href="' + item.url + '">' + title + '</a>')
                content.append(_get_page_toc(item))
                content.append('</li>')
        if isinstance(item, Section):
            content.append('<li>')
            if isinstance(item.title, str):
                content.append(item.title.capitalize())
            content.append(_nav_to_html(item.children))
            content.append('</li>')

    content.insert(0, '<ul>')
    content.append('</ul>')
    return '\n'.join(content)

def on_files(files: Files, config: Config):
    '''
    Invoked when files are ready to be manipulated.

    Args:
        files (Files): The collection of files.
        config (Config): The site configuration.

    Returns:
        Files: The files collection
    '''
    nav = get_navigation(files, config)
    content = '<div class="toc"><span class="toctitle site_toc">Table of Contents</span>' + \
              _nav_to_html(nav.items) + \
              '</div>'
    # Create a virtual file from generated text
    file = File.generated(config, 'generated_site_toc.md', content=content, inclusion=InclusionLevel.INCLUDED)
    # Add the virtual file to the site files
    files.append(file)
    # Add the virtual file to the site navigation with empty text for the title to hide it when viewing the website.
    config.nav.insert(0, {'': 'generated_site_toc.md'})

    return files