Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
nbanyan opened this issue Nov 12, 2024 · 1 comment
Open

Comments

@nbanyan
Copy link

nbanyan commented Nov 12, 2024

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:

  • Create a MkDocs hook to manually generate and concatenate the TOCs, write to a Page object and inject it into MkDocs. I don't know if there's a way to do this while excluding the TOC page from the served site or having MkDocs throw a warning about the page not being in the Nav.
  • Use another plugin to concatenate the entire site into one page instead of using the aggregator option. This seems part overkill and makes several nice features of your solution unusable (like page specific cover pages). The plugin I was considering isn't compatible with exporter and so would also need to be a custom hook...
@nbanyan
Copy link
Author

nbanyan commented Nov 15, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant