nav.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import logging
  2. from urllib.parse import urlparse
  3. from mkdocs.structure.pages import Page
  4. from mkdocs.utils import nest_paths, warning_filter
  5. log = logging.getLogger(__name__)
  6. log.addFilter(warning_filter)
  7. class Navigation:
  8. def __init__(self, items, pages):
  9. self.items = items # Nested List with full navigation of Sections, Pages, and Links.
  10. self.pages = pages # Flat List of subset of Pages in nav, in order.
  11. self.homepage = None
  12. for page in pages:
  13. if page.is_homepage:
  14. self.homepage = page
  15. break
  16. def __repr__(self):
  17. return '\n'.join([item._indent_print() for item in self])
  18. def __iter__(self):
  19. return iter(self.items)
  20. def __len__(self):
  21. return len(self.items)
  22. class Section:
  23. def __init__(self, title, children):
  24. self.title = title
  25. self.children = children
  26. self.parent = None
  27. self.active = False
  28. self.is_section = True
  29. self.is_page = False
  30. self.is_link = False
  31. def __repr__(self):
  32. return "Section(title='{}')".format(self.title)
  33. def _get_active(self):
  34. """ Return active status of section. """
  35. return self.__active
  36. def _set_active(self, value):
  37. """ Set active status of section and ancestors. """
  38. self.__active = bool(value)
  39. if self.parent is not None:
  40. self.parent.active = bool(value)
  41. active = property(_get_active, _set_active)
  42. @property
  43. def ancestors(self):
  44. if self.parent is None:
  45. return []
  46. return [self.parent] + self.parent.ancestors
  47. def _indent_print(self, depth=0):
  48. ret = ['{}{}'.format(' ' * depth, repr(self))]
  49. for item in self.children:
  50. ret.append(item._indent_print(depth + 1))
  51. return '\n'.join(ret)
  52. class Link:
  53. def __init__(self, title, url):
  54. self.title = title
  55. self.url = url
  56. self.parent = None
  57. # These should never change but are included for consistency with sections and pages.
  58. self.children = None
  59. self.active = False
  60. self.is_section = False
  61. self.is_page = False
  62. self.is_link = True
  63. def __repr__(self):
  64. title = "'{}'".format(self.title) if (self.title is not None) else '[blank]'
  65. return "Link(title={}, url='{}')".format(title, self.url)
  66. @property
  67. def ancestors(self):
  68. if self.parent is None:
  69. return []
  70. return [self.parent] + self.parent.ancestors
  71. def _indent_print(self, depth=0):
  72. return '{}{}'.format(' ' * depth, repr(self))
  73. def get_navigation(files, config):
  74. """ Build site navigation from config and files."""
  75. nav_config = config['nav'] or nest_paths(f.src_path for f in files.documentation_pages())
  76. items = _data_to_navigation(nav_config, files, config)
  77. if not isinstance(items, list):
  78. items = [items]
  79. # Get only the pages from the navigation, ignoring any sections and links.
  80. pages = _get_by_type(items, Page)
  81. # Include next, previous and parent links.
  82. _add_previous_and_next_links(pages)
  83. _add_parent_links(items)
  84. missing_from_config = [file for file in files.documentation_pages() if file.page is None]
  85. if missing_from_config:
  86. log.info(
  87. 'The following pages exist in the docs directory, but are not '
  88. 'included in the "nav" configuration:\n - {}'.format(
  89. '\n - '.join([file.src_path for file in missing_from_config]))
  90. )
  91. # Any documentation files not found in the nav should still have an associated page, so we
  92. # create them here. The Page object will automatically be assigned to `file.page` during
  93. # its creation (and this is the only way in which these page objects are accessable).
  94. for file in missing_from_config:
  95. Page(None, file, config)
  96. links = _get_by_type(items, Link)
  97. for link in links:
  98. scheme, netloc, path, params, query, fragment = urlparse(link.url)
  99. if scheme or netloc:
  100. log.debug(
  101. "An external link to '{}' is included in "
  102. "the 'nav' configuration.".format(link.url)
  103. )
  104. elif link.url.startswith('/'):
  105. log.debug(
  106. "An absolute path to '{}' is included in the 'nav' configuration, "
  107. "which presumably points to an external resource.".format(link.url)
  108. )
  109. else:
  110. msg = (
  111. "A relative path to '{}' is included in the 'nav' configuration, "
  112. "which is not found in the documentation files".format(link.url)
  113. )
  114. log.warning(msg)
  115. return Navigation(items, pages)
  116. def _data_to_navigation(data, files, config):
  117. if isinstance(data, dict):
  118. return [
  119. _data_to_navigation((key, value), files, config)
  120. if isinstance(value, str) else
  121. Section(title=key, children=_data_to_navigation(value, files, config))
  122. for key, value in data.items()
  123. ]
  124. elif isinstance(data, list):
  125. return [
  126. _data_to_navigation(item, files, config)[0]
  127. if isinstance(item, dict) and len(item) == 1 else
  128. _data_to_navigation(item, files, config)
  129. for item in data
  130. ]
  131. title, path = data if isinstance(data, tuple) else (None, data)
  132. file = files.get_file_from_path(path)
  133. if file:
  134. return Page(title, file, config)
  135. return Link(title, path)
  136. def _get_by_type(nav, T):
  137. ret = []
  138. for item in nav:
  139. if isinstance(item, T):
  140. ret.append(item)
  141. elif item.children:
  142. ret.extend(_get_by_type(item.children, T))
  143. return ret
  144. def _add_parent_links(nav):
  145. for item in nav:
  146. if item.is_section:
  147. for child in item.children:
  148. child.parent = item
  149. _add_parent_links(item.children)
  150. def _add_previous_and_next_links(pages):
  151. bookended = [None] + pages + [None]
  152. zipped = zip(bookended[:-2], bookended[1:-1], bookended[2:])
  153. for page0, page1, page2 in zipped:
  154. page1.previous_page, page1.next_page = page0, page2