added podman, json and yaml

This commit is contained in:
2022-11-27 19:11:46 +01:00
parent 01135dea09
commit 5226e858bb
790 changed files with 114578 additions and 16 deletions

View File

@ -0,0 +1,332 @@
from __future__ import annotations
import fnmatch
import logging
import os
import posixpath
import shutil
from pathlib import PurePath
from typing import (
TYPE_CHECKING,
Any,
Dict,
Iterable,
Iterator,
List,
Mapping,
Optional,
Sequence,
Union,
)
from urllib.parse import quote as urlquote
import jinja2.environment
from mkdocs import utils
if TYPE_CHECKING:
from mkdocs.config.defaults import MkDocsConfig
from mkdocs.structure.pages import Page
log = logging.getLogger(__name__)
class Files:
"""A collection of [File][mkdocs.structure.files.File] objects."""
def __init__(self, files: List[File]) -> None:
self._files = files
self._src_uris: Optional[Dict[str, File]] = None
def __iter__(self) -> Iterator[File]:
"""Iterate over the files within."""
return iter(self._files)
def __len__(self) -> int:
"""The number of files within."""
return len(self._files)
def __contains__(self, path: str) -> bool:
"""Whether the file with this `src_uri` is in the collection."""
return PurePath(path).as_posix() in self.src_uris
@property
def src_paths(self) -> Dict[str, File]:
"""Soft-deprecated, prefer `src_uris`."""
return {file.src_path: file for file in self._files}
@property
def src_uris(self) -> Dict[str, File]:
"""A mapping containing every file, with the keys being their
[`src_uri`][mkdocs.structure.files.File.src_uri]."""
if self._src_uris is None:
self._src_uris = {file.src_uri: file for file in self._files}
return self._src_uris
def get_file_from_path(self, path: str) -> Optional[File]:
"""Return a File instance with File.src_uri equal to path."""
return self.src_uris.get(PurePath(path).as_posix())
def append(self, file: File) -> None:
"""Append file to Files collection."""
self._src_uris = None
self._files.append(file)
def remove(self, file: File) -> None:
"""Remove file from Files collection."""
self._src_uris = None
self._files.remove(file)
def copy_static_files(self, dirty: bool = False) -> None:
"""Copy static files from source to destination."""
for file in self:
if not file.is_documentation_page():
file.copy_file(dirty)
def documentation_pages(self) -> Sequence[File]:
"""Return iterable of all Markdown page file objects."""
return [file for file in self if file.is_documentation_page()]
def static_pages(self) -> Sequence[File]:
"""Return iterable of all static page file objects."""
return [file for file in self if file.is_static_page()]
def media_files(self) -> Sequence[File]:
"""Return iterable of all file objects which are not documentation or static pages."""
return [file for file in self if file.is_media_file()]
def javascript_files(self) -> Sequence[File]:
"""Return iterable of all javascript file objects."""
return [file for file in self if file.is_javascript()]
def css_files(self) -> Sequence[File]:
"""Return iterable of all CSS file objects."""
return [file for file in self if file.is_css()]
def add_files_from_theme(self, env: jinja2.Environment, config: MkDocsConfig) -> None:
"""Retrieve static files from Jinja environment and add to collection."""
def filter(name):
# '.*' filters dot files/dirs at root level whereas '*/.*' filters nested levels
patterns = ['.*', '*/.*', '*.py', '*.pyc', '*.html', '*readme*', 'mkdocs_theme.yml']
# Exclude translation files
patterns.append("locales/*")
patterns.extend(f'*{x}' for x in utils.markdown_extensions)
patterns.extend(config.theme.static_templates)
for pattern in patterns:
if fnmatch.fnmatch(name.lower(), pattern):
return False
return True
for path in env.list_templates(filter_func=filter):
# Theme files do not override docs_dir files
path = PurePath(path).as_posix()
if path not in self.src_uris:
for dir in config.theme.dirs:
# Find the first theme dir which contains path
if os.path.isfile(os.path.join(dir, path)):
self.append(File(path, dir, config.site_dir, config.use_directory_urls))
break
class File:
"""
A MkDocs File object.
Points to the source and destination locations of a file.
The `path` argument must be a path that exists relative to `src_dir`.
The `src_dir` and `dest_dir` must be absolute paths on the local file system.
The `use_directory_urls` argument controls how destination paths are generated. If `False`, a Markdown file is
mapped to an HTML file of the same name (the file extension is changed to `.html`). If True, a Markdown file is
mapped to an HTML index file (`index.html`) nested in a directory using the "name" of the file in `path`. The
`use_directory_urls` argument has no effect on non-Markdown files.
File objects have the following properties, which are Unicode strings:
"""
src_uri: str
"""The pure path (always '/'-separated) of the source file relative to the source directory."""
abs_src_path: str
"""The absolute concrete path of the source file. Will use backslashes on Windows."""
dest_uri: str
"""The pure path (always '/'-separated) of the destination file relative to the destination directory."""
abs_dest_path: str
"""The absolute concrete path of the destination file. Will use backslashes on Windows."""
url: str
"""The URI of the destination file relative to the destination directory as a string."""
@property
def src_path(self) -> str:
"""Same as `src_uri` (and synchronized with it) but will use backslashes on Windows. Discouraged."""
return os.path.normpath(self.src_uri)
@src_path.setter
def src_path(self, value):
self.src_uri = PurePath(value).as_posix()
@property
def dest_path(self) -> str:
"""Same as `dest_uri` (and synchronized with it) but will use backslashes on Windows. Discouraged."""
return os.path.normpath(self.dest_uri)
@dest_path.setter
def dest_path(self, value):
self.dest_uri = PurePath(value).as_posix()
page: Optional[Page]
def __init__(self, path: str, src_dir: str, dest_dir: str, use_directory_urls: bool) -> None:
self.page = None
self.src_path = path
self.abs_src_path = os.path.normpath(os.path.join(src_dir, self.src_path))
self.name = self._get_stem()
self.dest_uri = self._get_dest_path(use_directory_urls)
self.abs_dest_path = os.path.normpath(os.path.join(dest_dir, self.dest_path))
self.url = self._get_url(use_directory_urls)
def __eq__(self, other) -> bool:
return (
isinstance(other, self.__class__)
and self.src_uri == other.src_uri
and self.abs_src_path == other.abs_src_path
and self.url == other.url
)
def __repr__(self):
return (
f"File(src_uri='{self.src_uri}', dest_uri='{self.dest_uri}',"
f" name='{self.name}', url='{self.url}')"
)
def _get_stem(self) -> str:
"""Return the name of the file without it's extension."""
filename = posixpath.basename(self.src_uri)
stem, ext = posixpath.splitext(filename)
return 'index' if stem in ('index', 'README') else stem
def _get_dest_path(self, use_directory_urls: bool) -> str:
"""Return destination path based on source path."""
if self.is_documentation_page():
parent, filename = posixpath.split(self.src_uri)
if not use_directory_urls or self.name == 'index':
# index.md or README.md => index.html
# foo.md => foo.html
return posixpath.join(parent, self.name + '.html')
else:
# foo.md => foo/index.html
return posixpath.join(parent, self.name, 'index.html')
return self.src_uri
def _get_url(self, use_directory_urls: bool) -> str:
"""Return url based in destination path."""
url = self.dest_uri
dirname, filename = posixpath.split(url)
if use_directory_urls and filename == 'index.html':
url = (dirname or '.') + '/'
return urlquote(url)
def url_relative_to(self, other: File) -> str:
"""Return url for file relative to other file."""
return utils.get_relative_url(self.url, other.url if isinstance(other, File) else other)
def copy_file(self, dirty: bool = False) -> None:
"""Copy source file to destination, ensuring parent directories exist."""
if dirty and not self.is_modified():
log.debug(f"Skip copying unmodified file: '{self.src_uri}'")
else:
log.debug(f"Copying media file: '{self.src_uri}'")
try:
utils.copy_file(self.abs_src_path, self.abs_dest_path)
except shutil.SameFileError:
pass # Let plugins write directly into site_dir.
def is_modified(self) -> bool:
if os.path.isfile(self.abs_dest_path):
return os.path.getmtime(self.abs_dest_path) < os.path.getmtime(self.abs_src_path)
return True
def is_documentation_page(self) -> bool:
"""Return True if file is a Markdown page."""
return utils.is_markdown_file(self.src_uri)
def is_static_page(self) -> bool:
"""Return True if file is a static page (HTML, XML, JSON)."""
return self.src_uri.endswith(('.html', '.htm', '.xml', '.json'))
def is_media_file(self) -> bool:
"""Return True if file is not a documentation or static page."""
return not (self.is_documentation_page() or self.is_static_page())
def is_javascript(self) -> bool:
"""Return True if file is a JavaScript file."""
return self.src_uri.endswith(('.js', '.javascript'))
def is_css(self) -> bool:
"""Return True if file is a CSS file."""
return self.src_uri.endswith('.css')
def get_files(config: Union[MkDocsConfig, Mapping[str, Any]]) -> Files:
"""Walk the `docs_dir` and return a Files collection."""
files = []
exclude = ['.*', '/templates']
for source_dir, dirnames, filenames in os.walk(config['docs_dir'], followlinks=True):
relative_dir = os.path.relpath(source_dir, config['docs_dir'])
for dirname in list(dirnames):
path = os.path.normpath(os.path.join(relative_dir, dirname))
# Skip any excluded directories
if _filter_paths(basename=dirname, path=path, is_dir=True, exclude=exclude):
dirnames.remove(dirname)
dirnames.sort()
for filename in _sort_files(filenames):
path = os.path.normpath(os.path.join(relative_dir, filename))
# Skip any excluded files
if _filter_paths(basename=filename, path=path, is_dir=False, exclude=exclude):
continue
# Skip README.md if an index file also exists in dir
if filename == 'README.md' and 'index.md' in filenames:
log.warning(
f"Both index.md and README.md found. Skipping README.md from {source_dir}"
)
continue
files.append(
File(path, config['docs_dir'], config['site_dir'], config['use_directory_urls'])
)
return Files(files)
def _sort_files(filenames: Iterable[str]) -> List[str]:
"""Always sort `index` or `README` as first filename in list."""
def key(f):
if os.path.splitext(f)[0] in ['index', 'README']:
return (0,)
return (1, f)
return sorted(filenames, key=key)
def _filter_paths(basename: str, path: str, is_dir: bool, exclude: Iterable[str]) -> bool:
""".gitignore style file filtering."""
for item in exclude:
# Items ending in '/' apply only to directories.
if item.endswith('/') and not is_dir:
continue
# Items starting with '/' apply to the whole path.
# In any other cases just the basename is used.
match = path if item.startswith('/') else basename
if fnmatch.fnmatch(match, item.strip('/')):
return True
return False

View File

@ -0,0 +1,242 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any, Iterator, List, Mapping, Optional, Type, TypeVar, Union
from urllib.parse import urlsplit
from mkdocs.structure.files import Files
from mkdocs.structure.pages import Page
from mkdocs.utils import nest_paths
if TYPE_CHECKING:
from mkdocs.config.defaults import MkDocsConfig
log = logging.getLogger(__name__)
class Navigation:
def __init__(self, items: List[Union[Page, Section, Link]], pages: List[Page]) -> None:
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
homepage: Optional[Page]
"""The [page][mkdocs.structure.pages.Page] object for the homepage of the site."""
pages: List[Page]
"""A flat list of all [page][mkdocs.structure.pages.Page] objects contained in the navigation."""
def __repr__(self):
return '\n'.join(item._indent_print() for item in self)
def __iter__(self) -> Iterator[Union[Page, Section, Link]]:
return iter(self.items)
def __len__(self) -> int:
return len(self.items)
class Section:
def __init__(self, title: str, children: List[Union[Page, Section, Link]]) -> None:
self.title = title
self.children = children
self.parent = None
self.active = False
def __repr__(self):
return f"Section(title='{self.title}')"
title: str
"""The title of the section."""
parent: Optional[Section]
"""The immediate parent of the section or `None` if the section is at the top level."""
children: List[Union[Page, Section, Link]]
"""An iterable of all child navigation objects. Children may include nested sections, pages and links."""
@property
def active(self) -> bool:
"""
When `True`, indicates that a child page of this section is the current page and
can be used to highlight the section as the currently viewed section. Defaults
to `False`.
"""
return self.__active
@active.setter
def active(self, value: bool):
"""Set active status of section and ancestors."""
self.__active = bool(value)
if self.parent is not None:
self.parent.active = bool(value)
is_section: bool = True
"""Indicates that the navigation object is a "section" object. Always `True` for section objects."""
is_page: bool = False
"""Indicates that the navigation object is a "page" object. Always `False` for section objects."""
is_link: bool = False
"""Indicates that the navigation object is a "link" object. Always `False` for section objects."""
@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: str, url: str):
self.title = title
self.url = url
self.parent = None
def __repr__(self):
title = f"'{self.title}'" if (self.title is not None) else '[blank]'
return f"Link(title={title}, url='{self.url}')"
title: str
"""The title of the link. This would generally be used as the label of the link."""
url: str
"""The URL that the link points to. The URL should always be an absolute URLs and
should not need to have `base_url` prepended."""
parent: Optional[Section]
"""The immediate parent of the link. `None` if the link is at the top level."""
children: None = None
"""Links do not contain children and the attribute is always `None`."""
active: bool = False
"""External links cannot be "active" and the attribute is always `False`."""
is_section: bool = False
"""Indicates that the navigation object is a "section" object. Always `False` for link objects."""
is_page: bool = False
"""Indicates that the navigation object is a "page" object. Always `False` for link objects."""
is_link: bool = True
"""Indicates that the navigation object is a "link" object. Always `True` for link objects."""
@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: Files, config: Union[MkDocsConfig, Mapping[str, Any]]) -> Navigation:
"""Build site navigation from config and files."""
nav_config = config['nav'] or nest_paths(f.src_uri 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 accessible).
for file in missing_from_config:
Page(None, file, config)
links = _get_by_type(items, Link)
for link in links:
scheme, netloc, path, query, fragment = urlsplit(link.url)
if scheme or netloc:
log.debug(f"An external link to '{link.url}' is included in the 'nav' configuration.")
elif link.url.startswith('/'):
log.debug(
f"An absolute path to '{link.url}' is included in the 'nav' "
"configuration, which presumably points to an external resource."
)
else:
msg = (
f"A relative path to '{link.url}' is included in the 'nav' "
"configuration, which is not found in the documentation files"
)
log.warning(msg)
return Navigation(items, pages)
def _data_to_navigation(data, files: Files, config: Union[MkDocsConfig, Mapping[str, Any]]):
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)
T = TypeVar('T')
def _get_by_type(nav, t: Type[T]) -> List[T]:
ret = []
for item in nav:
if isinstance(item, t):
ret.append(item)
if item.children:
ret.extend(_get_by_type(item.children, t))
return ret
def _add_parent_links(nav) -> None:
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: List[Page]) -> None:
bookended = [None, *pages, None]
zipped = zip(bookended[:-2], pages, bookended[2:])
for page0, page1, page2 in zipped:
page1.previous_page, page1.next_page = page0, page2

View File

@ -0,0 +1,347 @@
from __future__ import annotations
import logging
import os
import posixpath
from typing import TYPE_CHECKING, Any, Mapping, MutableMapping, Optional, Union
from urllib.parse import unquote as urlunquote
from urllib.parse import urljoin, urlsplit, urlunsplit
from xml.etree.ElementTree import Element
import markdown
from markdown.extensions import Extension
from markdown.treeprocessors import Treeprocessor
from markdown.util import AMP_SUBSTITUTE
from mkdocs.structure.files import File, Files
from mkdocs.structure.toc import get_toc
from mkdocs.utils import get_build_date, get_markdown_title, meta
if TYPE_CHECKING:
from mkdocs.config.defaults import MkDocsConfig
from mkdocs.structure.nav import Section
from mkdocs.structure.toc import TableOfContents
log = logging.getLogger(__name__)
class Page:
def __init__(
self, title: Optional[str], file: File, config: Union[MkDocsConfig, Mapping[str, Any]]
) -> None:
file.page = self
self.file = file
self.title = title
# Navigation attributes
self.parent = None
self.children = None
self.previous_page = None
self.next_page = None
self.active = False
self.update_date = get_build_date()
self._set_canonical_url(config.get('site_url', None))
self._set_edit_url(
config.get('repo_url', None), config.get('edit_uri'), config.get('edit_uri_template')
)
# Placeholders to be filled in later in the build process.
self.markdown = None
self.content = None
self.toc = [] # type: ignore
self.meta = {}
def __eq__(self, other) -> bool:
return (
isinstance(other, self.__class__)
and self.title == other.title
and self.file == other.file
)
def __repr__(self):
title = f"'{self.title}'" if (self.title is not None) else '[blank]'
url = self.abs_url or self.file.url
return f"Page(title={title}, url='{url}')"
def _indent_print(self, depth=0):
return '{}{}'.format(' ' * depth, repr(self))
title: Optional[str]
"""Contains the Title for the current page."""
markdown: Optional[str]
"""The original Markdown content from the file."""
content: Optional[str]
"""The rendered Markdown as HTML, this is the contents of the documentation."""
toc: TableOfContents
"""An iterable object representing the Table of contents for a page. Each item in
the `toc` is an [`AnchorLink`][mkdocs.structure.toc.AnchorLink]."""
meta: MutableMapping[str, Any]
"""A mapping of the metadata included at the top of the markdown page."""
@property
def url(self) -> str:
"""The URL of the page relative to the MkDocs `site_dir`."""
return '' if self.file.url in ('.', './') else self.file.url
file: File
"""The documentation [`File`][mkdocs.structure.files.File] that the page is being rendered from."""
abs_url: Optional[str]
"""The absolute URL of the page from the server root as determined by the value
assigned to the [site_url][] configuration setting. The value includes any
subdirectory included in the `site_url`, but not the domain. [base_url][] should
not be used with this variable."""
canonical_url: Optional[str]
"""The full, canonical URL to the current page as determined by the value assigned
to the [site_url][] configuration setting. The value includes the domain and any
subdirectory included in the `site_url`. [base_url][] should not be used with this
variable."""
@property
def active(self) -> bool:
"""When `True`, indicates that this page is the currently viewed page. Defaults to `False`."""
return self.__active
@active.setter
def active(self, value: bool):
"""Set active status of page and ancestors."""
self.__active = bool(value)
if self.parent is not None:
self.parent.active = bool(value)
@property
def is_index(self) -> bool:
return self.file.name == 'index'
@property
def is_top_level(self) -> bool:
return self.parent is None
edit_url: Optional[str]
"""The full URL to the source page in the source repository. Typically used to
provide a link to edit the source page. [base_url][] should not be used with this
variable."""
@property
def is_homepage(self) -> bool:
"""Evaluates to `True` for the homepage of the site and `False` for all other pages."""
return self.is_top_level and self.is_index and self.file.url in ('.', './', 'index.html')
previous_page: Optional[Page]
"""The [page][mkdocs.structure.pages.Page] object for the previous page or `None`.
The value will be `None` if the current page is the first item in the site navigation
or if the current page is not included in the navigation at all."""
next_page: Optional[Page]
"""The [page][mkdocs.structure.pages.Page] object for the next page or `None`.
The value will be `None` if the current page is the last item in the site navigation
or if the current page is not included in the navigation at all."""
parent: Optional[Section]
"""The immediate parent of the page in the site navigation. `None` if the
page is at the top level."""
children: None = None
"""Pages do not contain children and the attribute is always `None`."""
is_section: bool = False
"""Indicates that the navigation object is a "section" object. Always `False` for page objects."""
is_page: bool = True
"""Indicates that the navigation object is a "page" object. Always `True` for page objects."""
is_link: bool = False
"""Indicates that the navigation object is a "link" object. Always `False` for page objects."""
@property
def ancestors(self):
if self.parent is None:
return []
return [self.parent] + self.parent.ancestors
def _set_canonical_url(self, base: Optional[str]) -> None:
if base:
if not base.endswith('/'):
base += '/'
self.canonical_url = canonical_url = urljoin(base, self.url)
self.abs_url = urlsplit(canonical_url).path
else:
self.canonical_url = None
self.abs_url = None
def _set_edit_url(
self,
repo_url: Optional[str],
edit_uri: Optional[str] = None,
edit_uri_template: Optional[str] = None,
) -> None:
if edit_uri or edit_uri_template:
src_uri = self.file.src_uri
if edit_uri_template:
noext = posixpath.splitext(src_uri)[0]
edit_uri = edit_uri_template.format(path=src_uri, path_noext=noext)
else:
assert edit_uri is not None and edit_uri.endswith('/')
edit_uri += src_uri
if repo_url:
# Ensure urljoin behavior is correct
if not edit_uri.startswith(('?', '#')) and not repo_url.endswith('/'):
repo_url += '/'
else:
try:
parsed_url = urlsplit(edit_uri)
if not parsed_url.scheme or not parsed_url.netloc:
log.warning(
f"edit_uri: {edit_uri!r} is not a valid URL, it should include the http:// (scheme)"
)
except ValueError as e:
log.warning(f"edit_uri: {edit_uri!r} is not a valid URL: {e}")
self.edit_url = urljoin(repo_url or '', edit_uri)
else:
self.edit_url = None
def read_source(self, config: MkDocsConfig) -> None:
source = config['plugins'].run_event('page_read_source', page=self, config=config)
if source is None:
try:
with open(self.file.abs_src_path, encoding='utf-8-sig', errors='strict') as f:
source = f.read()
except OSError:
log.error(f'File not found: {self.file.src_path}')
raise
except ValueError:
log.error(f'Encoding error reading file: {self.file.src_path}')
raise
self.markdown, self.meta = meta.get_data(source)
self._set_title()
def _set_title(self) -> None:
"""
Set the title for a Markdown document.
Check these in order and use the first that returns a valid title:
- value provided on init (passed in from config)
- value of metadata 'title'
- content of the first H1 in Markdown content
- convert filename to title
"""
if self.title is not None:
return
if 'title' in self.meta:
self.title = self.meta['title']
return
assert self.markdown is not None
title = get_markdown_title(self.markdown)
if title is None:
if self.is_homepage:
title = 'Home'
else:
title = self.file.name.replace('-', ' ').replace('_', ' ')
# Capitalize if the filename was all lowercase, otherwise leave it as-is.
if title.lower() == title:
title = title.capitalize()
self.title = title
def render(self, config: MkDocsConfig, files: Files) -> None:
"""
Convert the Markdown source file to HTML as per the config.
"""
extensions = [_RelativePathExtension(self.file, files), *config['markdown_extensions']]
md = markdown.Markdown(
extensions=extensions,
extension_configs=config['mdx_configs'] or {},
)
assert self.markdown is not None
self.content = md.convert(self.markdown)
self.toc = get_toc(getattr(md, 'toc_tokens', []))
class _RelativePathTreeprocessor(Treeprocessor):
def __init__(self, file: File, files: Files) -> None:
self.file = file
self.files = files
def run(self, root: Element) -> Element:
"""
Update urls on anchors and images to make them relative
Iterates through the full document tree looking for specific
tags and then makes them relative based on the site navigation
"""
for element in root.iter():
if element.tag == 'a':
key = 'href'
elif element.tag == 'img':
key = 'src'
else:
continue
url = element.get(key)
assert url is not None
new_url = self.path_to_url(url)
element.set(key, new_url)
return root
def path_to_url(self, url: str) -> str:
scheme, netloc, path, query, fragment = urlsplit(url)
if (
scheme
or netloc
or not path
or url.startswith('/')
or url.startswith('\\')
or AMP_SUBSTITUTE in url
or '.' not in os.path.split(path)[-1]
):
# Ignore URLs unless they are a relative link to a source file.
# AMP_SUBSTITUTE is used internally by Markdown only for email.
# No '.' in the last part of a path indicates path does not point to a file.
return url
# Determine the filepath of the target.
target_uri = posixpath.join(posixpath.dirname(self.file.src_uri), urlunquote(path))
target_uri = posixpath.normpath(target_uri).lstrip('/')
# Validate that the target exists in files collection.
target_file = self.files.get_file_from_path(target_uri)
if target_file is None:
log.warning(
f"Documentation file '{self.file.src_uri}' contains a link to "
f"'{target_uri}' which is not found in the documentation files."
)
return url
path = target_file.url_relative_to(self.file)
components = (scheme, netloc, path, query, fragment)
return urlunsplit(components)
class _RelativePathExtension(Extension):
"""
The Extension class is what we pass to markdown, it then
registers the Treeprocessor.
"""
def __init__(self, file: File, files: Files) -> None:
self.file = file
self.files = files
def extendMarkdown(self, md: markdown.Markdown) -> None:
relpath = _RelativePathTreeprocessor(self.file, self.files)
md.treeprocessors.register(relpath, "relpath", 0)

View File

@ -0,0 +1,77 @@
"""
Deals with generating the per-page table of contents.
For the sake of simplicity we use the Python-Markdown `toc` extension to
generate a list of dicts for each toc item, and then store it as AnchorLinks to
maintain compatibility with older versions of MkDocs.
"""
from __future__ import annotations
from typing import Any, Dict, List
def get_toc(toc_tokens: list) -> TableOfContents:
toc = [_parse_toc_token(i) for i in toc_tokens]
# For the table of contents, always mark the first element as active
if len(toc):
toc[0].active = True # type: ignore[attr-defined]
return TableOfContents(toc)
class TableOfContents:
"""
Represents the table of contents for a given page.
"""
def __init__(self, items: list) -> None:
self.items = items
def __iter__(self):
return iter(self.items)
def __len__(self) -> int:
return len(self.items)
def __str__(self) -> str:
return ''.join(str(item) for item in self)
class AnchorLink:
"""
A single entry in the table of contents.
"""
def __init__(self, title: str, id: str, level: int) -> None:
self.title, self.id, self.level = title, id, level
self.children = []
title: str
"""The text of the item."""
@property
def url(self) -> str:
"""The hash fragment of a URL pointing to the item."""
return '#' + self.id
level: int
"""The zero-based level of the item."""
children: List[AnchorLink]
"""An iterable of any child items."""
def __str__(self):
return self.indent_print()
def indent_print(self, depth=0):
indent = ' ' * depth
ret = f'{indent}{self.title} - {self.url}\n'
for item in self.children:
ret += item.indent_print(depth + 1)
return ret
def _parse_toc_token(token: Dict[str, Any]) -> AnchorLink:
anchor = AnchorLink(token['name'], token['id'], token['level'])
for i in token['children']:
anchor.children.append(_parse_toc_token(i))
return anchor