| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192 |
- import logging
- from urllib.parse import urlparse
- from mkdocs.structure.pages import Page
- from mkdocs.utils import nest_paths, warning_filter
- log = logging.getLogger(__name__)
- log.addFilter(warning_filter)
- class Navigation:
- def __init__(self, items, pages):
- self.items = items # Nested List with full navigation of Sections, Pages, and Links.
- self.pages = pages # Flat List of subset of Pages in nav, in order.
- self.homepage = None
- for page in pages:
- if page.is_homepage:
- self.homepage = page
- break
- def __repr__(self):
- return '\n'.join([item._indent_print() for item in self])
- def __iter__(self):
- return iter(self.items)
- def __len__(self):
- return len(self.items)
- class Section:
- def __init__(self, title, children):
- self.title = title
- self.children = children
- self.parent = None
- self.active = False
- self.is_section = True
- self.is_page = False
- self.is_link = False
- def __repr__(self):
- return "Section(title='{}')".format(self.title)
- def _get_active(self):
- """ Return active status of section. """
- return self.__active
- def _set_active(self, value):
- """ Set active status of section and ancestors. """
- self.__active = bool(value)
- if self.parent is not None:
- self.parent.active = bool(value)
- active = property(_get_active, _set_active)
- @property
- def ancestors(self):
- if self.parent is None:
- return []
- return [self.parent] + self.parent.ancestors
- def _indent_print(self, depth=0):
- ret = ['{}{}'.format(' ' * depth, repr(self))]
- for item in self.children:
- ret.append(item._indent_print(depth + 1))
- return '\n'.join(ret)
- class Link:
- def __init__(self, title, url):
- self.title = title
- self.url = url
- self.parent = None
- # These should never change but are included for consistency with sections and pages.
- self.children = None
- self.active = False
- self.is_section = False
- self.is_page = False
- self.is_link = True
- def __repr__(self):
- title = "'{}'".format(self.title) if (self.title is not None) else '[blank]'
- return "Link(title={}, url='{}')".format(title, self.url)
- @property
- def ancestors(self):
- if self.parent is None:
- return []
- return [self.parent] + self.parent.ancestors
- def _indent_print(self, depth=0):
- return '{}{}'.format(' ' * depth, repr(self))
- def get_navigation(files, config):
- """ Build site navigation from config and files."""
- nav_config = config['nav'] or nest_paths(f.src_path for f in files.documentation_pages())
- items = _data_to_navigation(nav_config, files, config)
- if not isinstance(items, list):
- items = [items]
- # Get only the pages from the navigation, ignoring any sections and links.
- pages = _get_by_type(items, Page)
- # Include next, previous and parent links.
- _add_previous_and_next_links(pages)
- _add_parent_links(items)
- missing_from_config = [file for file in files.documentation_pages() if file.page is None]
- if missing_from_config:
- log.info(
- 'The following pages exist in the docs directory, but are not '
- 'included in the "nav" configuration:\n - {}'.format(
- '\n - '.join([file.src_path for file in missing_from_config]))
- )
- # Any documentation files not found in the nav should still have an associated page, so we
- # create them here. The Page object will automatically be assigned to `file.page` during
- # its creation (and this is the only way in which these page objects are accessable).
- for file in missing_from_config:
- Page(None, file, config)
- links = _get_by_type(items, Link)
- for link in links:
- scheme, netloc, path, params, query, fragment = urlparse(link.url)
- if scheme or netloc:
- log.debug(
- "An external link to '{}' is included in "
- "the 'nav' configuration.".format(link.url)
- )
- elif link.url.startswith('/'):
- log.debug(
- "An absolute path to '{}' is included in the 'nav' configuration, "
- "which presumably points to an external resource.".format(link.url)
- )
- else:
- msg = (
- "A relative path to '{}' is included in the 'nav' configuration, "
- "which is not found in the documentation files".format(link.url)
- )
- log.warning(msg)
- return Navigation(items, pages)
- def _data_to_navigation(data, files, config):
- if isinstance(data, dict):
- return [
- _data_to_navigation((key, value), files, config)
- if isinstance(value, str) else
- Section(title=key, children=_data_to_navigation(value, files, config))
- for key, value in data.items()
- ]
- elif isinstance(data, list):
- return [
- _data_to_navigation(item, files, config)[0]
- if isinstance(item, dict) and len(item) == 1 else
- _data_to_navigation(item, files, config)
- for item in data
- ]
- title, path = data if isinstance(data, tuple) else (None, data)
- file = files.get_file_from_path(path)
- if file:
- return Page(title, file, config)
- return Link(title, path)
- def _get_by_type(nav, T):
- ret = []
- for item in nav:
- if isinstance(item, T):
- ret.append(item)
- elif item.children:
- ret.extend(_get_by_type(item.children, T))
- return ret
- def _add_parent_links(nav):
- for item in nav:
- if item.is_section:
- for child in item.children:
- child.parent = item
- _add_parent_links(item.children)
- def _add_previous_and_next_links(pages):
- bookended = [None] + pages + [None]
- zipped = zip(bookended[:-2], bookended[1:-1], bookended[2:])
- for page0, page1, page2 in zipped:
- page1.previous_page, page1.next_page = page0, page2
|