aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/docs/src/material/plugins
diff options
context:
space:
mode:
author简律纯 <i@jyunko.cn>2023-12-15 09:22:24 +0800
committer简律纯 <i@jyunko.cn>2023-12-15 09:22:24 +0800
commitdaa378d6964841d9a5c4bc39815d75c672117dea (patch)
tree8cfc92f73bcbd80a472ecc5f1f65df5b72ff1892 /docs/src/material/plugins
parentefb0730e641f575368e7e80fec80be16a03c95f4 (diff)
downloadinfini-daa378d6964841d9a5c4bc39815d75c672117dea.tar.gz
infini-daa378d6964841d9a5c4bc39815d75c672117dea.zip
refactor(docs/src): delete `material` dir
Diffstat (limited to 'docs/src/material/plugins')
-rw-r--r--docs/src/material/plugins/__init__.py19
-rw-r--r--docs/src/material/plugins/blog/__init__.py19
-rw-r--r--docs/src/material/plugins/blog/author.py38
-rw-r--r--docs/src/material/plugins/blog/config.py88
-rw-r--r--docs/src/material/plugins/blog/plugin.py884
-rw-r--r--docs/src/material/plugins/blog/readtime/__init__.py51
-rw-r--r--docs/src/material/plugins/blog/readtime/parser.py45
-rw-r--r--docs/src/material/plugins/blog/structure/__init__.py292
-rw-r--r--docs/src/material/plugins/blog/structure/config.py37
-rw-r--r--docs/src/material/plugins/blog/structure/markdown.py58
-rw-r--r--docs/src/material/plugins/blog/structure/options.py87
-rw-r--r--docs/src/material/plugins/blog/templates/__init__.py42
-rw-r--r--docs/src/material/plugins/group/__init__.py19
-rw-r--r--docs/src/material/plugins/group/config.py33
-rw-r--r--docs/src/material/plugins/group/plugin.py151
-rw-r--r--docs/src/material/plugins/info/__init__.py19
-rw-r--r--docs/src/material/plugins/info/config.py35
-rw-r--r--docs/src/material/plugins/info/plugin.py245
-rw-r--r--docs/src/material/plugins/offline/__init__.py19
-rw-r--r--docs/src/material/plugins/offline/config.py30
-rw-r--r--docs/src/material/plugins/offline/plugin.py69
-rw-r--r--docs/src/material/plugins/search/__init__.py19
-rw-r--r--docs/src/material/plugins/search/config.py58
-rw-r--r--docs/src/material/plugins/search/plugin.py580
-rw-r--r--docs/src/material/plugins/social/__init__.py19
-rw-r--r--docs/src/material/plugins/social/config.py48
-rw-r--r--docs/src/material/plugins/social/plugin.py516
-rw-r--r--docs/src/material/plugins/tags/__init__.py27
-rw-r--r--docs/src/material/plugins/tags/config.py38
-rw-r--r--docs/src/material/plugins/tags/plugin.py182
30 files changed, 0 insertions, 3767 deletions
diff --git a/docs/src/material/plugins/__init__.py b/docs/src/material/plugins/__init__.py
deleted file mode 100644
index d1899378..00000000
--- a/docs/src/material/plugins/__init__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
diff --git a/docs/src/material/plugins/blog/__init__.py b/docs/src/material/plugins/blog/__init__.py
deleted file mode 100644
index d1899378..00000000
--- a/docs/src/material/plugins/blog/__init__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
diff --git a/docs/src/material/plugins/blog/author.py b/docs/src/material/plugins/blog/author.py
deleted file mode 100644
index 1dcfc2de..00000000
--- a/docs/src/material/plugins/blog/author.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-from mkdocs.config.base import Config
-from mkdocs.config.config_options import DictOfItems, SubConfig, Type
-
-# -----------------------------------------------------------------------------
-# Classes
-# -----------------------------------------------------------------------------
-
-# Author
-class Author(Config):
- name = Type(str)
- description = Type(str)
- avatar = Type(str)
-
-# -----------------------------------------------------------------------------
-
-# Authors
-class Authors(Config):
- authors = DictOfItems(SubConfig(Author), default = {})
diff --git a/docs/src/material/plugins/blog/config.py b/docs/src/material/plugins/blog/config.py
deleted file mode 100644
index c7a85095..00000000
--- a/docs/src/material/plugins/blog/config.py
+++ /dev/null
@@ -1,88 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-from functools import partial
-from markdown.extensions.toc import slugify
-from mkdocs.config.config_options import Choice, Deprecated, Optional, Type
-from mkdocs.config.base import Config
-
-# -----------------------------------------------------------------------------
-# Classes
-# -----------------------------------------------------------------------------
-
-# Blog plugin configuration
-class BlogConfig(Config):
- enabled = Type(bool, default = True)
-
- # Settings for blog
- blog_dir = Type(str, default = "blog")
- blog_toc = Type(bool, default = False)
-
- # Settings for posts
- post_dir = Type(str, default = "{blog}/posts")
- post_date_format = Type(str, default = "long")
- post_url_date_format = Type(str, default = "yyyy/MM/dd")
- post_url_format = Type(str, default = "{date}/{slug}")
- post_url_max_categories = Type(int, default = 1)
- post_slugify = Type((type(slugify), partial), default = slugify)
- post_slugify_separator = Type(str, default = "-")
- post_excerpt = Choice(["optional", "required"], default = "optional")
- post_excerpt_max_authors = Type(int, default = 1)
- post_excerpt_max_categories = Type(int, default = 5)
- post_excerpt_separator = Type(str, default = "<!-- more -->")
- post_readtime = Type(bool, default = True)
- post_readtime_words_per_minute = Type(int, default = 265)
-
- # Settings for archive
- archive = Type(bool, default = True)
- archive_name = Type(str, default = "blog.archive")
- archive_date_format = Type(str, default = "yyyy")
- archive_url_date_format = Type(str, default = "yyyy")
- archive_url_format = Type(str, default = "archive/{date}")
- archive_toc = Optional(Type(bool))
-
- # Settings for categories
- categories = Type(bool, default = True)
- categories_name = Type(str, default = "blog.categories")
- categories_url_format = Type(str, default = "category/{slug}")
- categories_slugify = Type((type(slugify), partial), default = slugify)
- categories_slugify_separator = Type(str, default = "-")
- categories_allowed = Type(list, default = [])
- categories_toc = Optional(Type(bool))
-
- # Settings for pagination
- pagination = Type(bool, default = True)
- pagination_per_page = Type(int, default = 10)
- pagination_url_format = Type(str, default = "page/{page}")
- pagination_format = Type(str, default = "~2~")
- pagination_if_single_page = Type(bool, default = False)
- pagination_keep_content = Type(bool, default = False)
-
- # Settings for authors
- authors = Type(bool, default = True)
- authors_file = Type(str, default = "{blog}/.authors.yml")
-
- # Settings for drafts
- draft = Type(bool, default = False)
- draft_on_serve = Type(bool, default = True)
- draft_if_future_date = Type(bool, default = False)
-
- # Deprecated settings
- pagination_template = Deprecated(moved_to = "pagination_format")
diff --git a/docs/src/material/plugins/blog/plugin.py b/docs/src/material/plugins/blog/plugin.py
deleted file mode 100644
index 375b8cfe..00000000
--- a/docs/src/material/plugins/blog/plugin.py
+++ /dev/null
@@ -1,884 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-from __future__ import annotations
-
-import logging
-import os
-import posixpath
-import yaml
-
-from babel.dates import format_date
-from datetime import datetime
-from mkdocs.config.defaults import MkDocsConfig
-from mkdocs.exceptions import PluginError
-from mkdocs.plugins import BasePlugin, event_priority
-from mkdocs.structure import StructureItem
-from mkdocs.structure.files import File, Files, InclusionLevel
-from mkdocs.structure.nav import Navigation, Section
-from mkdocs.structure.pages import Page
-from mkdocs.utils import copy_file, get_relative_url
-from paginate import Page as Pagination
-from shutil import rmtree
-from tempfile import mkdtemp
-from yaml import SafeLoader
-
-from .author import Authors
-from .config import BlogConfig
-from .readtime import readtime
-from .structure import Archive, Category, Excerpt, Post, View
-from .templates import url_filter
-
-# -----------------------------------------------------------------------------
-# Classes
-# -----------------------------------------------------------------------------
-
-# Blog plugin
-class BlogPlugin(BasePlugin[BlogConfig]):
- supports_multiple_instances = True
-
- # Initialize plugin
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- # Initialize incremental builds
- self.is_serve = False
- self.is_dirty = False
-
- # Initialize temporary directory
- self.temp_dir = mkdtemp()
-
- # Determine whether we're serving the site
- def on_startup(self, *, command, dirty):
- self.is_serve = command == "serve"
- self.is_dirty = dirty
-
- # Initialize authors and set defaults
- def on_config(self, config):
- if not self.config.enabled:
- return
-
- # Initialize entrypoint
- self.blog: View
-
- # Initialize and resolve authors, if enabled
- if self.config.authors:
- self.authors = self._resolve_authors(config)
-
- # Initialize table of contents settings
- if not isinstance(self.config.archive_toc, bool):
- self.config.archive_toc = self.config.blog_toc
- if not isinstance(self.config.categories_toc, bool):
- self.config.categories_toc = self.config.blog_toc
-
- # By default, drafts are rendered when the documentation is served,
- # but not when it is built, for a better user experience
- if self.is_serve and self.config.draft_on_serve:
- self.config.draft = True
-
- # Resolve and load posts and generate views (run later) - we want to allow
- # other plugins to add generated posts or views, so we run this plugin as
- # late as possible. We also need to remove the posts from the navigation
- # before navigation is constructed, as the entrypoint should be considered
- # to be the active page for each post. The URLs of posts are computed before
- # Markdown processing, so that when linking to and from posts, behavior is
- # exactly the same as with regular documentation pages. We create all pages
- # related to posts as part of this plugin, so we control the entire process.
- @event_priority(-50)
- def on_files(self, files, *, config):
- if not self.config.enabled:
- return
-
- # Resolve path to entrypoint and site directory
- root = posixpath.normpath(self.config.blog_dir)
- site = config.site_dir
-
- # Compute path to posts directory
- path = self.config.post_dir.format(blog = root)
- path = posixpath.normpath(path)
-
- # Adjust destination paths for media files
- for file in files.media_files():
- if not file.src_uri.startswith(path):
- continue
-
- # We need to adjust destination paths for assets to remove the
- # purely functional posts directory prefix when building
- file.dest_uri = file.dest_uri.replace(path, root)
- file.abs_dest_path = os.path.join(site, file.dest_path)
- file.url = file.url.replace(path, root)
-
- # Resolve entrypoint and posts sorted by descending date - if the posts
- # directory or entrypoint do not exist, they are automatically created
- self.blog = self._resolve(files, config)
- self.blog.posts = sorted(
- self._resolve_posts(files, config),
- key = lambda post: post.config.date.created,
- reverse = True
- )
-
- # Generate views for archive
- if self.config.archive:
- views = self._generate_archive(config, files)
- self.blog.views.extend(views)
-
- # Generate views for categories
- if self.config.categories:
- views = self._generate_categories(config, files)
- self.blog.views.extend(views)
-
- # Generate pages for views
- if self.config.pagination:
- for view in self._resolve_views(self.blog):
- for page in self._generate_pages(view, config, files):
- page.file.inclusion = InclusionLevel.EXCLUDED
- view.pages.append(page)
-
- # Ensure that entrypoint is always included in navigation
- self.blog.file.inclusion = InclusionLevel.INCLUDED
-
- # Attach posts and views to navigation (run later) - again, we allow other
- # plugins to alter the navigation before we start to attach posts and views
- # generated by this plugin at the correct locations in the navigation. Also,
- # we make sure to correct links to the parent and siblings of each page.
- @event_priority(-50)
- def on_nav(self, nav, *, config, files):
- if not self.config.enabled:
- return
-
- # If we're not building a standalone blog, the entrypoint will always
- # have a parent when it is included in the navigation. The parent is
- # essential to correctly resolve the location where the archive and
- # category views are attached. If the entrypoint doesn't have a parent,
- # we know that the author did not include it in the navigation, so we
- # explicitly mark it as not included.
- if not self.blog.parent and self.config.blog_dir != ".":
- self.blog.file.inclusion = InclusionLevel.NOT_IN_NAV
-
- # Attach posts to entrypoint without adding them to the navigation, so
- # that the entrypoint is considered to be the active page for each post
- self._attach(self.blog, [None, *reversed(self.blog.posts), None])
- for post in self.blog.posts:
- post.file.inclusion = InclusionLevel.NOT_IN_NAV
-
- # Revert temporary exclusion of views from navigation
- for view in self._resolve_views(self.blog):
- for page in view.pages:
- page.file.inclusion = self.blog.file.inclusion
-
- # Attach views for archive
- if self.config.archive:
- title = self._translate(self.config.archive_name, config)
- views = [_ for _ in self.blog.views if isinstance(_, Archive)]
-
- # Attach and link views for archive
- if self.blog.file.inclusion.is_in_nav():
- self._attach_to(self.blog, Section(title, views), nav)
-
- # Attach views for categories
- if self.config.categories:
- title = self._translate(self.config.categories_name, config)
- views = [_ for _ in self.blog.views if isinstance(_, Category)]
-
- # Attach and link views for categories, if any
- if self.blog.file.inclusion.is_in_nav() and views:
- self._attach_to(self.blog, Section(title, views), nav)
-
- # Attach pages for views
- if self.config.pagination:
- for view in self._resolve_views(self.blog):
- for at in range(1, len(view.pages)):
- self._attach_at(view.parent, view, view.pages[at])
-
- # Prepare post for rendering (run later) - allow other plugins to alter
- # the contents or metadata of a post before it is rendered and make sure
- # that the post includes a separator, which is essential for rendering
- # excerpts that should be included in views
- @event_priority(-50)
- def on_page_markdown(self, markdown, *, page, config, files):
- if not self.config.enabled:
- return
-
- # Skip if page is not a post managed by this instance - this plugin has
- # support for multiple instances, which is why this check is necessary
- if page not in self.blog.posts:
- if not self.config.pagination:
- return
-
- # We set the contents of the view to its title if pagination should
- # not keep the content of the original view on paginated views
- if not self.config.pagination_keep_content:
- view = self._resolve_original(page)
- if view in self._resolve_views(self.blog):
-
- # If the current view is paginated, use the rendered title
- # of the original view in case the author set the title in
- # the page's contents, or it would be overridden with the
- # one set in mkdocs.yml, leading to inconsistent headings
- assert isinstance(view, View)
- if view != page:
- name = view._title_from_render or view.title
- return f"# {name}"
-
- # Nothing more to be done for views
- return
-
- # Extract and assign authors to post, if enabled
- if self.config.authors:
- for name in page.config.authors:
- if name not in self.authors:
- raise PluginError(f"Couldn't find author '{name}'")
-
- # Append to list of authors
- page.authors.append(self.authors[name])
-
- # Extract settings for excerpts
- separator = self.config.post_excerpt_separator
- max_authors = self.config.post_excerpt_max_authors
- max_categories = self.config.post_excerpt_max_categories
-
- # Ensure presence of separator and throw, if its absent and required -
- # we append the separator to the end of the contents of the post, if it
- # is not already present, so we can remove footnotes or other content
- # from the excerpt without affecting the content of the excerpt
- if separator not in page.markdown:
- path = page.file.src_path
- if self.config.post_excerpt == "required":
- raise PluginError(
- f"Couldn't find '{separator}' separator in '{path}'"
- )
- else:
- page.markdown += f"\n\n{separator}"
-
- # Create excerpt for post and inherit authors and categories - excerpts
- # can contain a subset of the authors and categories of the post
- page.excerpt = Excerpt(page, config, files)
- page.excerpt.authors = page.authors[:max_authors]
- page.excerpt.categories = page.categories[:max_categories]
-
- # Process posts
- def on_page_content(self, html, *, page, config, files):
- if not self.config.enabled:
- return
-
- # Skip if page is not a post managed by this instance - this plugin has
- # support for multiple instances, which is why this check is necessary
- if page not in self.blog.posts:
- return
-
- # Compute readtime of post, if enabled and not explicitly set
- if self.config.post_readtime:
- words_per_minute = self.config.post_readtime_words_per_minute
- if not page.config.readtime:
- page.config.readtime = readtime(html, words_per_minute)
-
- # Register template filters for plugin
- def on_env(self, env, *, config, files):
- if not self.config.enabled:
- return
-
- # Filter for formatting dates related to posts
- def date_filter(date: datetime):
- return self._format_date_for_post(date, config)
-
- # Register custom template filters
- env.filters["date"] = date_filter
- env.filters["url"] = url_filter
-
- # Prepare view for rendering (run latest) - views are rendered last, as we
- # need to mutate the navigation to account for pagination. The main problem
- # is that we need to replace the view in the navigation, because otherwise
- # the view would not be considered active.
- @event_priority(-100)
- def on_page_context(self, context, *, page, config, nav):
- if not self.config.enabled:
- return
-
- # Skip if page is not a view managed by this instance - this plugin has
- # support for multiple instances, which is why this check is necessary
- view = self._resolve_original(page)
- if view not in self._resolve_views(self.blog):
- return
-
- # If the current view is paginated, replace and rewire it - the current
- # view temporarily becomes the main view, and is reset after rendering
- assert isinstance(view, View)
- if view != page:
- prev = view.pages[view.pages.index(page) - 1]
-
- # Replace previous page with current page
- items = self._resolve_siblings(view, nav)
- items[items.index(prev)] = page
-
- # Render excerpts and prepare pagination
- posts, pagination = self._render(page)
-
- # Render pagination links
- def pager(args: object):
- return pagination.pager(
- format = self.config.pagination_format,
- show_if_single_page = self.config.pagination_if_single_page,
- **args
- )
-
- # Assign posts and pagination to context
- context["posts"] = posts
- context["pagination"] = pager if pagination else None
-
- # After rendering a paginated view, replace the URL of the paginated view
- # with the URL of the original view - since we need to replace the original
- # view with a paginated view in `on_page_context` for correct resolution of
- # the active state, we must fix the paginated view URLs after rendering
- def on_post_page(self, output, *, page, config):
- if not self.config.enabled:
- return
-
- # Skip if page is not a view managed by this instance - this plugin has
- # support for multiple instances, which is why this check is necessary
- view = self._resolve_original(page)
- if view not in self._resolve_views(self.blog):
- return
-
- # If the current view is paginated, replace the URL of the paginated
- # view with the URL of the original view - see https://t.ly/Yeh-P
- assert isinstance(view, View)
- if view != page:
- page.file.url = view.file.url
-
- # Remove temporary directory on shutdown
- def on_shutdown(self):
- rmtree(self.temp_dir)
-
- # -------------------------------------------------------------------------
-
- # Check if the given post is excluded
- def _is_excluded(self, post: Post):
- if self.config.draft:
- return False
-
- # If a post was not explicitly marked or unmarked as draft, and the
- # date should be taken into account, we automatically mark it as draft
- # if the publishing date is in the future. This, of course, is opt-in
- # and must be explicitly enabled by the author.
- if not isinstance(post.config.draft, bool):
- if self.config.draft_if_future_date:
- return post.config.date.created > datetime.now()
-
- # Post might be a draft
- return bool(post.config.draft)
-
- # -------------------------------------------------------------------------
-
- # Resolve entrypoint - the entrypoint of the blog must have been created
- # if it did not exist before, and hosts all posts sorted by descending date
- def _resolve(self, files: Files, config: MkDocsConfig):
- path = os.path.join(self.config.blog_dir, "index.md")
- path = os.path.normpath(path)
-
- # Create entrypoint, if it does not exist - note that the entrypoint is
- # created in the docs directory, not in the temporary directory
- docs = os.path.relpath(config.docs_dir)
- name = os.path.join(docs, path)
- if not os.path.isfile(name):
- file = self._path_to_file(path, config, temp = False)
- files.append(file)
-
- # Create file in docs directory
- self._save_to_file(file.abs_src_path, "# Blog\n\n")
-
- # Create and return entrypoint
- file = files.get_file_from_path(path)
- return View(None, file, config)
-
- # Resolve post - the caller must make sure that the given file points to an
- # actual post (and not a page), or behavior might be unpredictable
- def _resolve_post(self, file: File, config: MkDocsConfig):
- post = Post(file, config)
-
- # Compute path and create a temporary file for path resolution
- path = self._format_path_for_post(post, config)
- temp = self._path_to_file(path, config, temp = False)
-
- # Replace destination file system path and URL
- file.dest_uri = temp.dest_uri
- file.abs_dest_path = temp.abs_dest_path
- file.url = temp.url
-
- # Replace canonical URL and return post
- post._set_canonical_url(config.site_url)
- return post
-
- # Resolve posts from directory - traverse all documentation pages and filter
- # and yield those that are located in the posts directory
- def _resolve_posts(self, files: Files, config: MkDocsConfig):
- path = self.config.post_dir.format(blog = self.config.blog_dir)
- path = os.path.normpath(path)
-
- # Create posts directory, if it does not exist
- docs = os.path.relpath(config.docs_dir)
- name = os.path.join(docs, path)
- if not os.path.isdir(name):
- os.makedirs(name, exist_ok = True)
-
- # Filter posts from pages
- for file in files.documentation_pages():
- if not file.src_path.startswith(path):
- continue
-
- # Temporarily remove post from navigation
- file.inclusion = InclusionLevel.EXCLUDED
-
- # Resolve post - in order to determine whether a post should be
- # excluded, we must load it and analyze its metadata. All posts
- # marked as drafts are excluded, except for when the author has
- # configured drafts to be included in the navigation.
- post = self._resolve_post(file, config)
- if not self._is_excluded(post):
- yield post
-
- # Resolve authors - check if there's an authors file at the configured
- # location, and if one was found, load and validate it
- def _resolve_authors(self, config: MkDocsConfig):
- path = self.config.authors_file.format(blog = self.config.blog_dir)
- path = os.path.normpath(path)
-
- # Resolve path relative to docs directory
- docs = os.path.relpath(config.docs_dir)
- file = os.path.join(docs, path)
-
- # If the authors file does not exist, return here
- config: Authors = Authors()
- if not os.path.isfile(file):
- return config.authors
-
- # Open file and parse as YAML
- with open(file, encoding = "utf-8") as f:
- config.config_file_path = os.path.abspath(file)
- try:
- config.load_dict(yaml.load(f, SafeLoader) or {})
-
- # The authors file could not be loaded because of a syntax error,
- # which we display to the author with a nice error message
- except Exception as e:
- raise PluginError(
- f"Error reading authors file '{path}' in '{docs}':\n"
- f"{e}"
- )
-
- # Validate authors and throw if errors occurred
- errors, warnings = config.validate()
- if not config.authors and warnings:
- log.warning(
- f"Action required: the format of the authors file changed.\n"
- f"All authors must now be located under the 'authors' key.\n"
- f"Please adjust '{file}' to match:\n"
- f"\n"
- f"authors:\n"
- f" squidfunk:\n"
- f" avatar: https://avatars.githubusercontent.com/u/932156\n"
- f" description: Creator\n"
- f" name: Martin Donath\n"
- f"\n"
- )
- for _, w in warnings:
- log.warning(w)
- for _, e in errors:
- raise PluginError(
- f"Error reading authors file '{path}' in '{docs}':\n"
- f"{e}"
- )
-
- # Return authors
- return config.authors
-
- # Resolve views of the given view in pre-order
- def _resolve_views(self, view: View):
- yield view
-
- # Resolve views recursively
- for page in view.views:
- for next in self._resolve_views(page):
- assert isinstance(next, View)
- yield next
-
- # Resolve siblings of a navigation item
- def _resolve_siblings(self, item: StructureItem, nav: Navigation):
- if isinstance(item.parent, Section):
- return item.parent.children
- else:
- return nav.items
-
- # Resolve original page or view (e.g. for paginated views)
- def _resolve_original(self, page: Page):
- if isinstance(page, View):
- return page.pages[0]
- else:
- return page
-
- # -------------------------------------------------------------------------
-
- # Generate views for archive - analyze posts and generate the necessary
- # views, taking the date format provided by the author into account
- def _generate_archive(self, config: MkDocsConfig, files: Files):
- for post in self.blog.posts:
- date = post.config.date.created
-
- # Compute name and path of archive view
- name = self._format_date_for_archive(date, config)
- path = self._format_path_for_archive(post, config)
-
- # Create file for view, if it does not exist
- file = files.get_file_from_path(path)
- if not file or self.temp_dir not in file.abs_src_path:
- file = self._path_to_file(path, config)
- files.append(file)
-
- # Create file in temporary directory
- self._save_to_file(file.abs_src_path, f"# {name}")
-
- # Create and yield view - we don't explicitly set the title of
- # the view, so authors can override them in the page's content
- if not isinstance(file.page, Archive):
- yield Archive(None, file, config)
-
- # Assign post to archive
- assert isinstance(file.page, Archive)
- file.page.posts.append(post)
-
- # Generate views for categories - analyze posts and generate the necessary
- # views, taking the allowed categories as set by the author into account
- def _generate_categories(self, config: MkDocsConfig, files: Files):
- for post in self.blog.posts:
- for name in post.config.categories:
- path = self._format_path_for_category(name)
-
- # Ensure category is in non-empty allow list
- categories = self.config.categories_allowed or [name]
- if name not in categories:
- docs = os.path.relpath(config.docs_dir)
- path = os.path.relpath(post.file.abs_src_path, docs)
- raise PluginError(
- f"Error reading categories of post '{path}' in "
- f"'{docs}': category '{name}' not in allow list"
- )
-
- # Create file for view, if it does not exist
- file = files.get_file_from_path(path)
- if not file or self.temp_dir not in file.abs_src_path:
- file = self._path_to_file(path, config)
- files.append(file)
-
- # Create file in temporary directory
- self._save_to_file(file.abs_src_path, f"# {name}")
-
- # Create and yield view - we don't explicitly set the title of
- # the view, so authors can override them in the page's content
- if not isinstance(file.page, Category):
- yield Category(None, file, config)
-
- # Assign post to category and vice versa
- assert isinstance(file.page, Category)
- file.page.posts.append(post)
- post.categories.append(file.page)
-
- # Generate pages for pagination - analyze view and generate the necessary
- # pages, creating a chain of views for simple rendering and replacement
- def _generate_pages(self, view: View, config: MkDocsConfig, files: Files):
- yield view
-
- # Compute pagination boundaries and create pages - pages are internally
- # handled as copies of a view, as they map to the same source location
- step = self.config.pagination_per_page
- for at in range(step, len(view.posts), step):
- path = self._format_path_for_pagination(view, 1 + at // step)
-
- # Create file for view, if it does not exist
- file = files.get_file_from_path(path)
- if not file or self.temp_dir not in file.abs_src_path:
- file = self._path_to_file(path, config)
- files.append(file)
-
- # Copy file to temporary directory
- copy_file(view.file.abs_src_path, file.abs_src_path)
-
- # Create view and attach to previous page
- if not isinstance(file.page, View):
- yield View(None, file, config)
-
- # Assign pages and posts to view
- assert isinstance(file.page, View)
- file.page.pages = view.pages
- file.page.posts = view.posts
-
- # -------------------------------------------------------------------------
-
- # Attach a list of pages to each other and to the given parent item without
- # explicitly adding them to the navigation, which can be done by the caller
- def _attach(self, parent: StructureItem, pages: list[Page]):
- for tail, page, head in zip(pages, pages[1:], pages[2:]):
-
- # Link page to parent and siblings
- page.parent = parent
- page.previous_page = tail
- page.next_page = head
-
- # If the page is a view, we know that we generated it and need to
- # link its siblings back to the view
- if isinstance(page, View):
- view = self._resolve_original(page)
- if tail: tail.next_page = view
- if head: head.previous_page = view
-
- # Attach a page to the given parent and link it to the previous and next
- # page of the given host - this is exclusively used for paginated views
- def _attach_at(self, parent: StructureItem, host: Page, page: Page):
- self._attach(parent, [host.previous_page, page, host.next_page])
-
- # Attach a section as a sibling to the given view, make sure its pages are
- # part of the navigation, and ensure all pages are linked correctly
- def _attach_to(self, view: View, section: Section, nav: Navigation):
- section.parent = view.parent
-
- # Resolve siblings, which are the children of the parent section, or
- # the top-level list of navigation items if the view is at the root of
- # the project, and append the given section to it. It's currently not
- # possible to chose the position of a section.
- items = self._resolve_siblings(view, nav)
- items.append(section)
-
- # Find last sibling that is a page, skipping sections, as we need to
- # append the given section after all other pages
- tail = next(item for item in reversed(items) if isinstance(item, Page))
- head = tail.next_page
-
- # Attach section to navigation and pages to each other
- nav.pages.extend(section.children)
- self._attach(section, [tail, *section.children, head])
-
- # -------------------------------------------------------------------------
-
- # Render excerpts and pagination for the given view
- def _render(self, view: View):
- posts, pagination = view.posts, None
-
- # Create pagination, if enabled
- if self.config.pagination:
- at = view.pages.index(view)
-
- # Compute pagination boundaries
- step = self.config.pagination_per_page
- p, q = at * step, at * step + step
-
- # Extract posts in pagination boundaries
- posts = view.posts[p:q]
- pagination = self._render_pagination(view, (p, q))
-
- # Render excerpts for selected posts
- posts = [
- self._render_post(post.excerpt, view)
- for post in posts if post.excerpt
- ]
-
- # Return posts and pagination
- return posts, pagination
-
- # Render excerpt in the context of the given view
- def _render_post(self, excerpt: Excerpt, view: View):
- excerpt.render(view, self.config.post_excerpt_separator)
-
- # Determine whether to add posts to the table of contents of the view -
- # note that those settings can be changed individually for each type of
- # view, which is why we need to check the type of view and the table of
- # contents setting for that type of view
- toc = self.config.blog_toc
- if isinstance(view, Archive):
- toc = self.config.archive_toc
- if isinstance(view, Category):
- toc = self.config.categories_toc
-
- # Attach top-level table of contents item to view if it should be added
- # and both, the view and excerpt contain table of contents items
- if toc and excerpt.toc.items and view.toc.items:
- view.toc.items[0].children.append(excerpt.toc.items[0])
-
- # Return excerpt
- return excerpt
-
- # Create pagination for the given view and range
- def _render_pagination(self, view: View, range: tuple[int, int]):
- p, q = range
-
- # Create URL from the given page to another page
- def url_maker(n: int):
- return get_relative_url(view.pages[n - 1].url, view.url)
-
- # Return pagination
- return Pagination(
- view.posts, page = q // (q - p),
- items_per_page = q - p,
- url_maker = url_maker
- )
-
- # -------------------------------------------------------------------------
-
- # Format path for post
- def _format_path_for_post(self, post: Post, config: MkDocsConfig):
- categories = post.config.categories[:self.config.post_url_max_categories]
- categories = [self._slugify_category(name) for name in categories]
-
- # Replace placeholders in format string
- date = post.config.date.created
- path = self.config.post_url_format.format(
- categories = "/".join(categories),
- date = self._format_date_for_post_url(date, config),
- file = post.file.name,
- slug = post.config.slug or self._slugify_post(post)
- )
-
- # Normalize path and strip slashes at the beginning and end
- path = posixpath.normpath(path.strip("/"))
- return posixpath.join(self.config.blog_dir, f"{path}.md")
-
- # Format path for archive
- def _format_path_for_archive(self, post: Post, config: MkDocsConfig):
- date = post.config.date.created
- path = self.config.archive_url_format.format(
- date = self._format_date_for_archive_url(date, config)
- )
-
- # Normalize path and strip slashes at the beginning and end
- path = posixpath.normpath(path.strip("/"))
- return posixpath.join(self.config.blog_dir, f"{path}.md")
-
- # Format path for category
- def _format_path_for_category(self, name: str):
- path = self.config.categories_url_format.format(
- slug = self._slugify_category(name)
- )
-
- # Normalize path and strip slashes at the beginning and end
- path = posixpath.normpath(path.strip("/"))
- return posixpath.join(self.config.blog_dir, f"{path}.md")
-
- # Format path for pagination
- def _format_path_for_pagination(self, view: View, page: int):
- path = self.config.pagination_url_format.format(
- page = page
- )
-
- # Compute base path for pagination - if the given view is an index file,
- # we need to pop the file name from the base so it's not part of the URL
- # and we need to append `index` to the path, so the paginated view is
- # also an index page - see https://t.ly/71MKF
- base, _ = posixpath.splitext(view.file.src_uri)
- if view.is_index:
- base = posixpath.dirname(base)
- path = posixpath.join(path, "index")
-
- # Normalize path and strip slashes at the beginning and end
- path = posixpath.normpath(path.strip("/"))
- return posixpath.join(base, f"{path}.md")
-
- # -------------------------------------------------------------------------
-
- # Format date
- def _format_date(self, date: datetime, format: str, config: MkDocsConfig):
- locale = config.theme["language"]
- return format_date(date, format = format, locale = locale)
-
- # Format date for post
- def _format_date_for_post(self, date: datetime, config: MkDocsConfig):
- format = self.config.post_date_format
- return self._format_date(date, format, config)
-
- # Format date for post URL
- def _format_date_for_post_url(self, date: datetime, config: MkDocsConfig):
- format = self.config.post_url_date_format
- return self._format_date(date, format, config)
-
- # Format date for archive
- def _format_date_for_archive(self, date: datetime, config: MkDocsConfig):
- format = self.config.archive_date_format
- return self._format_date(date, format, config)
-
- # Format date for archive URL
- def _format_date_for_archive_url(self, date: datetime, config: MkDocsConfig):
- format = self.config.archive_url_date_format
- return self._format_date(date, format, config)
-
- # -------------------------------------------------------------------------
-
- # Slugify post title
- def _slugify_post(self, post: Post):
- separator = self.config.post_slugify_separator
- return self.config.post_slugify(post.title, separator)
-
- # Slugify category
- def _slugify_category(self, name: str):
- separator = self.config.categories_slugify_separator
- return self.config.categories_slugify(name, separator)
-
- # -------------------------------------------------------------------------
-
- # Create a file for the given path, which must point to a valid source file,
- # either inside the temporary directory or the docs directory
- def _path_to_file(self, path: str, config: MkDocsConfig, *, temp = True):
- assert path.endswith(".md")
- file = File(
- path,
- config.docs_dir if not temp else self.temp_dir,
- config.site_dir,
- config.use_directory_urls
- )
-
- # Hack: mark file as generated, so other plugins don't think it's part
- # of the file system. This is more or less a new quasi-standard that
- # still needs to be adopted by MkDocs, and was introduced by the
- # git-revision-date-localized-plugin - see https://bit.ly/3ZUmdBx
- if temp:
- file.generated_by = "material/blog"
-
- # Return file
- return file
-
- # Create a file with the given content on disk
- def _save_to_file(self, path: str, content: str):
- os.makedirs(os.path.dirname(path), exist_ok = True)
- with open(path, "w", encoding = "utf-8") as f:
- f.write(content)
-
- # -------------------------------------------------------------------------
-
- # Translate the placeholder referenced by the given key
- def _translate(self, key: str, config: MkDocsConfig) -> str:
- env = config.theme.get_env()
- template = env.get_template(
- "partials/language.html", globals = { "config": config }
- )
-
- # Translate placeholder
- return template.module.t(key)
-
-# -----------------------------------------------------------------------------
-# Data
-# -----------------------------------------------------------------------------
-
-# Set up logging
-log = logging.getLogger("mkdocs.material.blog")
diff --git a/docs/src/material/plugins/blog/readtime/__init__.py b/docs/src/material/plugins/blog/readtime/__init__.py
deleted file mode 100644
index a0c149b9..00000000
--- a/docs/src/material/plugins/blog/readtime/__init__.py
+++ /dev/null
@@ -1,51 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-import re
-
-from math import ceil
-
-from .parser import ReadtimeParser
-
-# -----------------------------------------------------------------------------
-# Functions
-# -----------------------------------------------------------------------------
-
-# Compute readtime - we first used the original readtime library, but the list
-# of dependencies it brings with it increased the size of the Docker image by
-# 20 MB (packed), which is an increase of 50%. For this reason, we adapt the
-# original readtime algorithm to our needs - see https://t.ly/fPZ7L
-def readtime(html: str, words_per_minute: int):
- parser = ReadtimeParser()
- parser.feed(html)
- parser.close()
-
- # Extract words from text and compute readtime in seconds
- words = len(re.split(r"\W+", "".join(parser.text)))
- seconds = ceil(words / words_per_minute * 60)
-
- # Account for additional images
- delta = 12
- for _ in range(parser.images):
- seconds += delta
- if delta > 3: delta -= 1
-
- # Return readtime in minutes
- return ceil(seconds / 60)
diff --git a/docs/src/material/plugins/blog/readtime/parser.py b/docs/src/material/plugins/blog/readtime/parser.py
deleted file mode 100644
index b91a7b30..00000000
--- a/docs/src/material/plugins/blog/readtime/parser.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-from html.parser import HTMLParser
-
-# -----------------------------------------------------------------------------
-# Classes
-# -----------------------------------------------------------------------------
-
-# Readtime parser
-class ReadtimeParser(HTMLParser):
-
- # Initialize parser
- def __init__(self):
- super().__init__(convert_charrefs = True)
-
- # Keep track of text and images
- self.text = []
- self.images = 0
-
- # Collect images
- def handle_starttag(self, tag, attrs):
- if tag == "img":
- self.images += 1
-
- # Collect text
- def handle_data(self, data):
- self.text.append(data)
diff --git a/docs/src/material/plugins/blog/structure/__init__.py b/docs/src/material/plugins/blog/structure/__init__.py
deleted file mode 100644
index 2fc541fe..00000000
--- a/docs/src/material/plugins/blog/structure/__init__.py
+++ /dev/null
@@ -1,292 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-from __future__ import annotations
-
-import logging
-import os
-import yaml
-
-from copy import copy
-from markdown import Markdown
-from material.plugins.blog.author import Author
-from mkdocs.config.defaults import MkDocsConfig
-from mkdocs.exceptions import PluginError
-from mkdocs.structure.files import File, Files
-from mkdocs.structure.nav import Section
-from mkdocs.structure.pages import Page, _RelativePathTreeprocessor
-from mkdocs.structure.toc import get_toc
-from mkdocs.utils.meta import YAML_RE
-from re import Match
-from yaml import SafeLoader
-
-from .config import PostConfig
-from .markdown import ExcerptTreeprocessor
-
-# -----------------------------------------------------------------------------
-# Classes
-# -----------------------------------------------------------------------------
-
-# Post
-class Post(Page):
-
- # Initialize post - posts are never listed in the navigation, which is why
- # they will never include a title that was manually set, so we can omit it
- def __init__(self, file: File, config: MkDocsConfig):
- super().__init__(None, file, config)
-
- # Resolve path relative to docs directory
- docs = os.path.relpath(config.docs_dir)
- path = os.path.relpath(file.abs_src_path, docs)
-
- # Read contents and metadata immediately
- with open(file.abs_src_path, encoding = "utf-8") as f:
- self.markdown = f.read()
-
- # Sadly, MkDocs swallows any exceptions that occur during parsing.
- # As we want to provide the best possible authoring experience, we
- # need to catch errors early and display them nicely. We decided to
- # drop support for MkDocs' MultiMarkdown syntax, because it is not
- # correctly implemented anyway. When using MultiMarkdown syntax, all
- # date formats are returned as strings and list are not properly
- # supported. Thus, we just use the relevants parts of `get_data`.
- match: Match = YAML_RE.match(self.markdown)
- if not match:
- raise PluginError(
- f"Error reading metadata of post '{path}' in '{docs}':\n"
- f"Expected metadata to be defined but found nothing"
- )
-
- # Extract metadata and parse as YAML
- try:
- self.meta = yaml.load(match.group(1), SafeLoader) or {}
- self.markdown = self.markdown[match.end():].lstrip("\n")
-
- # The post's metadata could not be parsed because of a syntax error,
- # which we display to the user with a nice error message
- except Exception as e:
- raise PluginError(
- f"Error reading metadata of post '{path}' in '{docs}':\n"
- f"{e}"
- )
-
- # Initialize post configuration, but remove all keys that this plugin
- # doesn't care about, or they will be reported as invalid configuration
- self.config: PostConfig = PostConfig(file.abs_src_path)
- self.config.load_dict({
- key: self.meta[key] for key in (
- set(self.meta.keys()) &
- set(self.config.keys())
- )
- })
-
- # Validate configuration and throw if errors occurred
- errors, warnings = self.config.validate()
- for _, w in warnings:
- log.warning(w)
- for k, e in errors:
- raise PluginError(
- f"Error reading metadata '{k}' of post '{path}' in '{docs}':\n"
- f"{e}"
- )
-
- # Excerpts are subsets of posts that are used in pages like archive and
- # category views. They are not rendered as standalone pages, but are
- # rendered in the context of a view. Each post has a dedicated excerpt
- # instance which is reused when rendering views.
- self.excerpt: Excerpt = None
-
- # Initialize authors and actegories
- self.authors: list[Author] = []
- self.categories: list[Category] = []
-
- # Ensure template is set or use default
- self.meta.setdefault("template", "blog-post.html")
-
- # Ensure template hides navigation
- self.meta["hide"] = self.meta.get("hide", [])
- if "navigation" not in self.meta["hide"]:
- self.meta["hide"].append("navigation")
-
- # The contents and metadata were already read in the constructor (and not
- # in `read_source` as for pages), so this function must be set to a no-op
- def read_source(self, config: MkDocsConfig):
- pass
-
-# -----------------------------------------------------------------------------
-
-# Excerpt
-class Excerpt(Page):
-
- # Initialize an excerpt for the given post - we create the Markdown parser
- # when intitializing the excerpt in order to improve rendering performance
- # for excerpts, as they are reused across several different views, because
- # posts might be referenced from multiple different locations
- def __init__(self, post: Post, config: MkDocsConfig, files: Files):
- self.file = copy(post.file)
- self.post = post
-
- # Set canonical URL, or we can't print excerpts when debugging the
- # blog plugin, as the `abs_url` property would be missing
- self._set_canonical_url(config.site_url)
-
- # Initialize configuration and metadata
- self.config = post.config
- self.meta = post.meta
-
- # Initialize authors and categories - note that views usually contain
- # subsets of those lists, which is why we need to manage them here
- self.authors: list[Author] = []
- self.categories: list[Category] = []
-
- # Initialize parser - note that we need to patch the configuration,
- # more specifically the table of contents extension
- config = _patch(config)
- self.md = Markdown(
- extensions = config.markdown_extensions,
- extension_configs = config.mdx_configs,
- )
-
- # Register excerpt tree processor - this processor resolves anchors to
- # posts from within views, so they point to the correct location
- self.md.treeprocessors.register(
- ExcerptTreeprocessor(post),
- "excerpt",
- 0
- )
-
- # Register relative path tree processor - this processor resolves links
- # to other pages and assets, and is used by MkDocs itself
- self.md.treeprocessors.register(
- _RelativePathTreeprocessor(self.file, files, config),
- "relpath",
- 1
- )
-
- # Render an excerpt of the post on the given page - note that this is not
- # thread-safe because excerpts are shared across views, as it cuts down on
- # the cost of initialization. However, if in the future, we decide to render
- # posts and views concurrently, we must change this behavior.
- def render(self, page: Page, separator: str):
- self.file.url = page.url
-
- # Retrieve excerpt tree processor and set page as base
- at = self.md.treeprocessors.get_index_for_name("excerpt")
- processor: ExcerptTreeprocessor = self.md.treeprocessors[at]
- processor.base = page
-
- # Ensure that the excerpt includes a title in its content, since the
- # title is linked to the post when rendering - see https://t.ly/5Gg2F
- self.markdown = self.post.markdown
- if not self.post._title_from_render:
- self.markdown = "\n\n".join([f"# {self.post.title}", self.markdown])
-
- # Convert Markdown to HTML and extract excerpt
- self.content = self.md.convert(self.markdown)
- self.content, *_ = self.content.split(separator, 1)
-
- # Extract table of contents and reset post URL - if we wouldn't reset
- # the excerpt URL, linking to the excerpt from the view would not work
- self.toc = get_toc(getattr(self.md, "toc_tokens", []))
- self.file.url = self.post.url
-
-# -----------------------------------------------------------------------------
-
-# View
-class View(Page):
-
- # Initialize view
- def __init__(self, title: str | None, file: File, config: MkDocsConfig):
- super().__init__(title, file, config)
- self.parent: View | Section
-
- # Initialize posts and views
- self.posts: list[Post] = []
- self.views: list[View] = []
-
- # Initialize pages for pagination
- self.pages: list[View] = []
-
- # Set necessary metadata
- def read_source(self, config: MkDocsConfig):
- super().read_source(config)
-
- # Ensure template is set or use default
- self.meta.setdefault("template", "blog.html")
-
-# -----------------------------------------------------------------------------
-
-# Archive view
-class Archive(View):
- pass
-
-# -----------------------------------------------------------------------------
-
-# Category view
-class Category(View):
- pass
-
-# -----------------------------------------------------------------------------
-# Helper functions
-# -----------------------------------------------------------------------------
-
-# Patch configuration
-def _patch(config: MkDocsConfig):
- config = copy(config)
-
- # Copy parts of configuration that needs to be patched
- config.validation = copy(config.validation)
- config.validation.links = copy(config.validation.links)
- config.markdown_extensions = copy(config.markdown_extensions)
- config.mdx_configs = copy(config.mdx_configs)
-
- # Make sure that the author did not add another instance of the table of
- # contents extension to the configuration, as this leads to weird behavior
- if "markdown.extensions.toc" in config.markdown_extensions:
- config.markdown_extensions.remove("markdown.extensions.toc")
-
- # In order to render excerpts for posts, we need to make sure that the
- # table of contents extension is appropriately configured
- config.mdx_configs["toc"] = {
- **config.mdx_configs.get("toc", {}),
- **{
- "anchorlink": True, # Render headline as clickable
- "baselevel": 2, # Render h1 as h2 and so forth
- "permalink": False, # Remove permalinks
- "toc_depth": 2 # Remove everything below h2
- }
- }
-
- # Additionally, we disable link validation when rendering excerpts, because
- # invalid links have already been reported when rendering the page
- links = config.validation.links
- links.not_found = logging.DEBUG
- links.absolute_links = logging.DEBUG
- links.unrecognized_links = logging.DEBUG
-
- # Return patched configuration
- return config
-
-# -----------------------------------------------------------------------------
-# Data
-# -----------------------------------------------------------------------------
-
-# Set up logging
-log = logging.getLogger("mkdocs.material.blog")
diff --git a/docs/src/material/plugins/blog/structure/config.py b/docs/src/material/plugins/blog/structure/config.py
deleted file mode 100644
index 129491b9..00000000
--- a/docs/src/material/plugins/blog/structure/config.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-from mkdocs.config.base import Config
-from mkdocs.config.config_options import ListOfItems, Optional, Type
-
-from .options import PostDate
-
-# -----------------------------------------------------------------------------
-# Classes
-# -----------------------------------------------------------------------------
-
-# Post configuration
-class PostConfig(Config):
- authors = ListOfItems(Type(str), default = [])
- categories = ListOfItems(Type(str), default = [])
- date = PostDate()
- draft = Optional(Type(bool))
- readtime = Optional(Type(int))
- slug = Optional(Type(str))
diff --git a/docs/src/material/plugins/blog/structure/markdown.py b/docs/src/material/plugins/blog/structure/markdown.py
deleted file mode 100644
index 64ade554..00000000
--- a/docs/src/material/plugins/blog/structure/markdown.py
+++ /dev/null
@@ -1,58 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-from markdown.treeprocessors import Treeprocessor
-from mkdocs.structure.pages import Page
-from mkdocs.utils import get_relative_url
-from xml.etree.ElementTree import Element
-
-# -----------------------------------------------------------------------------
-# Classes
-# -----------------------------------------------------------------------------
-
-# Excerpt tree processor
-class ExcerptTreeprocessor(Treeprocessor):
-
- # Initialize excerpt tree processor
- def __init__(self, page: Page, base: Page = None):
- self.page = page
- self.base = base
-
- # Transform HTML after Markdown processing
- def run(self, root: Element):
- main = True
-
- # We're only interested in anchors, which is why we continue when the
- # link does not start with an anchor tag
- for el in root.iter("a"):
- anchor = el.get("href")
- if not anchor.startswith("#"):
- continue
-
- # The main headline should link to the post page, not to a specific
- # anchor, which is why we remove the anchor in that case
- path = get_relative_url(self.page.url, self.base.url)
- if main:
- el.set("href", path)
- else:
- el.set("href", path + anchor)
-
- # Main headline has been seen
- main = False
diff --git a/docs/src/material/plugins/blog/structure/options.py b/docs/src/material/plugins/blog/structure/options.py
deleted file mode 100644
index 281dec9f..00000000
--- a/docs/src/material/plugins/blog/structure/options.py
+++ /dev/null
@@ -1,87 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-from datetime import date, datetime, time
-from mkdocs.config.base import BaseConfigOption, Config, ValidationError
-from typing import Dict
-
-# -----------------------------------------------------------------------------
-# Classes
-# -----------------------------------------------------------------------------
-
-# Date dictionary
-class DateDict(Dict[str, datetime]):
-
- # Initialize date dictionary
- def __init__(self, data: dict):
- super().__init__(data)
-
- # Ensure presence of `date.created`
- self.created: datetime = data["created"]
-
- # Allow attribute access
- def __getattr__(self, name: str):
- if name in self:
- return self[name]
-
-# -----------------------------------------------------------------------------
-
-# Post date option
-class PostDate(BaseConfigOption[DateDict]):
-
- # Initialize post dates
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- # Normalize the supported types for post dates to datetime
- def pre_validation(self, config: Config, key_name: str):
-
- # If the date points to a scalar value, convert it to a dictionary,
- # since we want to allow the user to specify custom and arbitrary date
- # values for posts. Currently, only the `created` date is mandatory,
- # because it's needed to sort posts for views.
- if not isinstance(config[key_name], dict):
- config[key_name] = { "created": config[key_name] }
-
- # Convert all date values to datetime
- for key, value in config[key_name].items():
- if isinstance(value, date):
- config[key_name][key] = datetime.combine(value, time())
-
- # Initialize date dictionary
- config[key_name] = DateDict(config[key_name])
-
- # Ensure each date value is of type datetime
- def run_validation(self, value: DateDict):
- for key in value:
- if not isinstance(value[key], datetime):
- raise ValidationError(
- f"Expected type: {date} or {datetime} "
- f"but received: {type(value[key])}"
- )
-
- # Ensure presence of `date.created`
- if not value.created:
- raise ValidationError(
- "Expected 'created' date when using dictionary syntax"
- )
-
- # Return date dictionary
- return value
diff --git a/docs/src/material/plugins/blog/templates/__init__.py b/docs/src/material/plugins/blog/templates/__init__.py
deleted file mode 100644
index 9f7d794b..00000000
--- a/docs/src/material/plugins/blog/templates/__init__.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-from jinja2 import pass_context
-from jinja2.runtime import Context
-from material.plugins.blog.structure import View
-from mkdocs.utils.templates import url_filter as _url_filter
-
-# -----------------------------------------------------------------------------
-# Functions
-# -----------------------------------------------------------------------------
-
-# Filter for normalizing URLs with support for paginated views
-@pass_context
-def url_filter(context: Context, url: str):
- page = context["page"]
-
- # If the current page is a view, check if the URL links to the page
- # itself, and replace it with the URL of the main view
- if isinstance(page, View):
- if page.url == url:
- url = page.pages[0].url
-
- # Forward to original template filter
- return _url_filter(context, url)
diff --git a/docs/src/material/plugins/group/__init__.py b/docs/src/material/plugins/group/__init__.py
deleted file mode 100644
index d1899378..00000000
--- a/docs/src/material/plugins/group/__init__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
diff --git a/docs/src/material/plugins/group/config.py b/docs/src/material/plugins/group/config.py
deleted file mode 100644
index fb19222a..00000000
--- a/docs/src/material/plugins/group/config.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-from __future__ import annotations
-
-from mkdocs.config.config_options import Type
-from mkdocs.config.base import Config
-
-# -----------------------------------------------------------------------------
-# Classes
-# -----------------------------------------------------------------------------
-
-# Group plugin configuration
-class GroupConfig(Config):
- enabled = Type(bool, default = False)
- plugins = Type(list | dict)
diff --git a/docs/src/material/plugins/group/plugin.py b/docs/src/material/plugins/group/plugin.py
deleted file mode 100644
index 4ab13dbf..00000000
--- a/docs/src/material/plugins/group/plugin.py
+++ /dev/null
@@ -1,151 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-import logging
-
-from collections.abc import Callable
-from mkdocs.config.config_options import Plugins
-from mkdocs.config.defaults import MkDocsConfig
-from mkdocs.exceptions import PluginError
-from mkdocs.plugins import BasePlugin, event_priority
-
-from .config import GroupConfig
-
-# -----------------------------------------------------------------------------
-# Classes
-# -----------------------------------------------------------------------------
-
-# Group plugin
-class GroupPlugin(BasePlugin[GroupConfig]):
- supports_multiple_instances = True
-
- # Determine whether we're serving the site
- def on_startup(self, *, command, dirty):
- self.is_serve = command == "serve"
- self.is_dirty = dirty
-
- # If the group is enabled, conditionally load plugins - at first, this might
- # sound easier than it actually is, as we need to jump through some hoops to
- # ensure correct ordering among plugins. We're effectively initializing the
- # plugins that are part of the group after all MkDocs finished initializing
- # all other plugins, so we need to patch the order of the methods. Moreover,
- # we must use MkDocs existing plugin collection, or we might have collisions
- # with other plugins that are not part of the group. As so often, this is a
- # little hacky, but has huge potential making plugin configuration easier.
- # There's one little caveat: the `__init__` and `on_startup` methods of the
- # plugins that are part of the group are called after all other plugins, so
- # the `event_priority` decorator for `on_startup` events and is effectively
- # useless. However, the `on_startup` event is only intended to set up the
- # plugin and doesn't receive anything else than the invoked command and
- # whether we're running a dirty build, so there should be no problems.
- @event_priority(150)
- def on_config(self, config):
- if not self.config.enabled:
- return
-
- # Retrieve plugin collection from configuration
- option: Plugins = dict(config._schema)["plugins"]
- assert isinstance(option, Plugins)
-
- # Load all plugins in group
- self.plugins: dict[str, BasePlugin] = {}
- try:
- for name, plugin in self._load(option):
- self.plugins[name] = plugin
-
- # The plugin could not be loaded, likely because it's not installed or
- # misconfigured, so we raise a plugin error for a nicer error message
- except Exception as e:
- raise PluginError(str(e))
-
- # Patch order of plugin methods
- for events in option.plugins.events.values():
- self._patch(events, config)
-
- # Invoke `on_startup` event for plugins in group
- command = "serve" if self.is_serve else "build"
- for method in option.plugins.events["startup"]:
- plugin = self._get_plugin(method)
-
- # Ensure that we have a method bound to a plugin (and not a hook)
- if plugin and plugin in self.plugins.values():
- method(command = command, dirty = self.is_dirty)
-
- # -------------------------------------------------------------------------
-
- # Retrieve plugin instance for bound method or nothing
- def _get_plugin(self, method: Callable):
- return getattr(method, "__self__", None)
-
- # Retrieve priority of plugin method
- def _get_priority(self, method: Callable):
- return getattr(method, "mkdocs_priority", 0)
-
- # Retrieve position of plugin
- def _get_position(self, plugin: BasePlugin, config: MkDocsConfig) -> int:
- for at, (_, candidate) in enumerate(config.plugins.items()):
- if plugin == candidate:
- return at
-
- # -------------------------------------------------------------------------
-
- # Load plugins that are part of the group
- def _load(self, option: Plugins):
- for name, data in option._parse_configs(self.config.plugins):
- yield option.load_plugin_with_namespace(name, data)
-
- # -------------------------------------------------------------------------
-
- # Patch order of plugin methods - all other plugin methods are already in
- # the right order, so we only need to check those that are part of the group
- # and bubble them up into the right location. Some plugin methods may define
- # priorities, so we need to make sure to order correctly within those.
- def _patch(self, methods: list[Callable], config: MkDocsConfig):
- position = self._get_position(self, config)
- for at in reversed(range(1, len(methods))):
- tail = methods[at - 1]
- head = methods[at]
-
- # Skip if the plugin is not part of the group
- plugin = self._get_plugin(head)
- if not plugin or plugin not in self.plugins.values():
- continue
-
- # Skip if the previous method has a higher priority than the current
- # one, because we know we can't swap them anyway
- if self._get_priority(tail) > self._get_priority(head):
- continue
-
- # Ensure that we have a method bound to a plugin (and not a hook)
- plugin = self._get_plugin(tail)
- if not plugin:
- continue
-
- # Both methods have the same priority, so we check if the ordering
- # of both methods is violated, and if it is, swap them
- if (position < self._get_position(plugin, config)):
- methods[at], methods[at - 1] = tail, head
-
-# -----------------------------------------------------------------------------
-# Data
-# -----------------------------------------------------------------------------
-
-# Set up logging
-log = logging.getLogger("mkdocs.material.group")
diff --git a/docs/src/material/plugins/info/__init__.py b/docs/src/material/plugins/info/__init__.py
deleted file mode 100644
index d1899378..00000000
--- a/docs/src/material/plugins/info/__init__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
diff --git a/docs/src/material/plugins/info/config.py b/docs/src/material/plugins/info/config.py
deleted file mode 100644
index cbd64d4c..00000000
--- a/docs/src/material/plugins/info/config.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-from mkdocs.config.config_options import Type
-from mkdocs.config.base import Config
-
-# -----------------------------------------------------------------------------
-# Classes
-# -----------------------------------------------------------------------------
-
-# Info plugin configuration
-class InfoConfig(Config):
- enabled = Type(bool, default = True)
- enabled_on_serve = Type(bool, default = False)
-
- # Settings for archive
- archive = Type(bool, default = True)
- archive_stop_on_violation = Type(bool, default = True)
diff --git a/docs/src/material/plugins/info/plugin.py b/docs/src/material/plugins/info/plugin.py
deleted file mode 100644
index 7c6fdc17..00000000
--- a/docs/src/material/plugins/info/plugin.py
+++ /dev/null
@@ -1,245 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-import json
-import logging
-import os
-import platform
-import requests
-import sys
-
-from colorama import Fore, Style
-from importlib.metadata import distributions, version
-from io import BytesIO
-from markdown.extensions.toc import slugify
-from mkdocs.plugins import BasePlugin, event_priority
-from mkdocs.structure.files import get_files
-from mkdocs.utils import get_theme_dir
-from zipfile import ZipFile, ZIP_DEFLATED
-
-from .config import InfoConfig
-
-# -----------------------------------------------------------------------------
-# Classes
-# -----------------------------------------------------------------------------
-
-# Info plugin
-class InfoPlugin(BasePlugin[InfoConfig]):
-
- # Initialize plugin
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- # Initialize incremental builds
- self.is_serve = False
-
- # Determine whether we're serving the site
- def on_startup(self, *, command, dirty):
- self.is_serve = command == "serve"
-
- # Create a self-contained example (run earliest) - determine all files that
- # are visible to MkDocs and are used to build the site, create an archive
- # that contains all of them, and print a summary of the archive contents.
- # The user must attach this archive to the bug report.
- @event_priority(100)
- def on_config(self, config):
- if not self.config.enabled:
- return
-
- # By default, the plugin is disabled when the documentation is served,
- # but not when it is built. This should nicely align with the expected
- # user experience when creating reproductions.
- if not self.config.enabled_on_serve and self.is_serve:
- return
-
- # Resolve latest version
- url = "https://github.com/squidfunk/mkdocs-material/releases/latest"
- res = requests.get(url, allow_redirects = False)
-
- # Check if we're running the latest version
- _, current = res.headers.get("location").rsplit("/", 1)
- present = version("mkdocs-material")
- if not present.startswith(current):
- log.error("Please upgrade to the latest version.")
- self._help_on_versions_and_exit(present, current)
-
- # Exit if archive creation is disabled
- if not self.config.archive:
- sys.exit(1)
-
- # Print message that we're creating a bug report
- log.info("Started archive creation for bug report")
-
- # Check that there are no overrides in place - we need to use a little
- # hack to detect whether the custom_dir setting was used without parsing
- # mkdocs.yml again - we check at which position the directory provided
- # by the theme resides, and if it's not the first one, abort.
- if config.theme.dirs.index(get_theme_dir(config.theme.name)):
- log.error("Please remove 'custom_dir' setting.")
- self._help_on_customizations_and_exit()
-
- # Check that there are no hooks in place - hooks can alter the behavior
- # of MkDocs in unpredictable ways, which is why they must be considered
- # being customizations. Thus, we can't offer support for debugging and
- # must abort here.
- if config.hooks:
- log.error("Please remove 'hooks' setting.")
- self._help_on_customizations_and_exit()
-
- # Create in-memory archive and prompt user to enter a short descriptive
- # name for the archive, which is also used as the directory name. Note
- # that the name is slugified for better readability and stripped of any
- # file extension that the user might have entered.
- archive = BytesIO()
- example = input("\nPlease name your bug report (2-4 words): ")
- example, _ = os.path.splitext(example)
- example = "-".join([present, slugify(example, "-")])
-
- # Create self-contained example from project
- files: list[str] = []
- with ZipFile(archive, "a", ZIP_DEFLATED, False) as f:
- for path in ["mkdocs.yml", "requirements.txt"]:
- if os.path.isfile(path):
- f.write(path, os.path.join(example, path))
-
- # Append all files visible to MkDocs
- for file in get_files(config):
- path = os.path.relpath(file.abs_src_path, os.path.curdir)
- f.write(path, os.path.join(example, path))
-
- # Add information on installed packages
- f.writestr(
- os.path.join(example, "requirements.lock.txt"),
- "\n".join(sorted([
- "==".join([package.name, package.version])
- for package in distributions()
- ]))
- )
-
- # Add information on platform
- f.writestr(
- os.path.join(example, "platform.json"),
- json.dumps(
- {
- "system": platform.platform(),
- "python": platform.python_version()
- },
- default = str,
- indent = 2
- )
- )
-
- # Retrieve list of processed files
- for a in f.filelist:
- files.append("".join([
- Fore.LIGHTBLACK_EX, a.filename, " ",
- _size(a.compress_size)
- ]))
-
- # Finally, write archive to disk
- buffer = archive.getbuffer()
- with open(f"{example}.zip", "wb") as f:
- f.write(archive.getvalue())
-
- # Print summary
- log.info("Archive successfully created:")
- print(Style.NORMAL)
-
- # Print archive file names
- files.sort()
- for file in files:
- print(f" {file}")
-
- # Print archive name
- print(Style.RESET_ALL)
- print("".join([
- " ", f.name, " ",
- _size(buffer.nbytes, 10)
- ]))
-
- # Print warning when file size is excessively large
- print(Style.RESET_ALL)
- if buffer.nbytes > 1000000:
- log.warning("Archive exceeds recommended maximum size of 1 MB")
-
- # Aaaaaand done
- sys.exit(1)
-
- # -------------------------------------------------------------------------
-
- # Print help on versions and exit
- def _help_on_versions_and_exit(self, have, need):
- print(Fore.RED)
- print(" When reporting issues, please first upgrade to the latest")
- print(" version of Material for MkDocs, as the problem might already")
- print(" be fixed in the latest version. This helps reduce duplicate")
- print(" efforts and saves us maintainers time.")
- print(Style.NORMAL)
- print(f" Please update from {have} to {need}.")
- print(Style.RESET_ALL)
- print(f" pip install --upgrade --force-reinstall mkdocs-material")
- print(Style.NORMAL)
-
- # Exit, unless explicitly told not to
- if self.config.archive_stop_on_violation:
- sys.exit(1)
-
- # Print help on customizations and exit
- def _help_on_customizations_and_exit(self):
- print(Fore.RED)
- print(" When reporting issues, you must remove all customizations")
- print(" and check if the problem persists. If not, the problem is")
- print(" caused by your overrides. Please understand that we can't")
- print(" help you debug your customizations. Please remove:")
- print(Style.NORMAL)
- print(" - theme.custom_dir")
- print(" - hooks")
- print(Fore.YELLOW)
- print(" Additionally, please remove all third-party JavaScript or")
- print(" CSS not explicitly mentioned in our documentation:")
- print(Style.NORMAL)
- print(" - extra_css")
- print(" - extra_javascript")
- print(Style.RESET_ALL)
-
- # Exit, unless explicitly told not to
- if self.config.archive_stop_on_violation:
- sys.exit(1)
-
-# -----------------------------------------------------------------------------
-# Helper functions
-# -----------------------------------------------------------------------------
-
-# Print human-readable size
-def _size(value, factor = 1):
- color = Fore.GREEN
- if value > 100000 * factor: color = Fore.RED
- elif value > 25000 * factor: color = Fore.YELLOW
- for unit in ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB"]:
- if abs(value) < 1000.0:
- return f"{color}{value:3.1f} {unit}"
- value /= 1000.0
-
-# -----------------------------------------------------------------------------
-# Data
-# -----------------------------------------------------------------------------
-
-# Set up logging
-log = logging.getLogger("mkdocs.material.info")
diff --git a/docs/src/material/plugins/offline/__init__.py b/docs/src/material/plugins/offline/__init__.py
deleted file mode 100644
index d1899378..00000000
--- a/docs/src/material/plugins/offline/__init__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
diff --git a/docs/src/material/plugins/offline/config.py b/docs/src/material/plugins/offline/config.py
deleted file mode 100644
index 49f51a94..00000000
--- a/docs/src/material/plugins/offline/config.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-from mkdocs.config.config_options import Type
-from mkdocs.config.base import Config
-
-# -----------------------------------------------------------------------------
-# Classes
-# -----------------------------------------------------------------------------
-
-# Offline plugin configuration
-class OfflineConfig(Config):
- enabled = Type(bool, default = True)
diff --git a/docs/src/material/plugins/offline/plugin.py b/docs/src/material/plugins/offline/plugin.py
deleted file mode 100644
index abcb2598..00000000
--- a/docs/src/material/plugins/offline/plugin.py
+++ /dev/null
@@ -1,69 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-import os
-
-from mkdocs.plugins import BasePlugin, event_priority
-
-from .config import OfflineConfig
-
-# -----------------------------------------------------------------------------
-# Classes
-# -----------------------------------------------------------------------------
-
-# Offline plugin
-class OfflinePlugin(BasePlugin[OfflineConfig]):
-
- # Set configuration for offline build
- def on_config(self, config):
- if not self.config.enabled:
- return
-
- # Ensure correct resolution of links when viewing the site from the
- # file system by disabling directory URLs
- config.use_directory_urls = False
-
- # Append iframe-worker to polyfills/shims
- config.extra["polyfills"] = config.extra.get("polyfills", [])
- if not any("iframe-worker" in url for url in config.extra["polyfills"]):
- script = "https://unpkg.com/iframe-worker/shim"
- config.extra["polyfills"].append(script)
-
- # Add support for offline search (run latest) - the search index is copied
- # and inlined into a script, so that it can be used without a server
- @event_priority(-100)
- def on_post_build(self, *, config):
- if not self.config.enabled:
- return
-
- # Ensure presence of search index
- path = os.path.join(config.site_dir, "search")
- file = os.path.join(path, "search_index.json")
- if not os.path.isfile(file):
- return
-
- # Obtain search index contents
- with open(file, encoding = "utf-8") as f:
- data = f.read()
-
- # Inline search index contents into script
- file = os.path.join(path, "search_index.js")
- with open(file, "w", encoding = "utf-8") as f:
- f.write(f"var __index = {data}")
diff --git a/docs/src/material/plugins/search/__init__.py b/docs/src/material/plugins/search/__init__.py
deleted file mode 100644
index d1899378..00000000
--- a/docs/src/material/plugins/search/__init__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
diff --git a/docs/src/material/plugins/search/config.py b/docs/src/material/plugins/search/config.py
deleted file mode 100644
index e150fbb3..00000000
--- a/docs/src/material/plugins/search/config.py
+++ /dev/null
@@ -1,58 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-from mkdocs.config.config_options import (
- Choice,
- Deprecated,
- Optional,
- ListOfItems,
- Type
-)
-from mkdocs.config.base import Config
-from mkdocs.contrib.search import LangOption
-
-# -----------------------------------------------------------------------------
-# Options
-# -----------------------------------------------------------------------------
-
-# Options for search pipeline
-pipeline = ("stemmer", "stopWordFilter", "trimmer")
-
-# -----------------------------------------------------------------------------
-# Classes
-# -----------------------------------------------------------------------------
-
-# Search plugin configuration
-class SearchConfig(Config):
- enabled = Type(bool, default = True)
-
- # Settings for search
- lang = Optional(LangOption())
- separator = Optional(Type(str))
- pipeline = ListOfItems(Choice(pipeline), default = [])
-
- # Settings for text segmentation (Chinese)
- jieba_dict = Optional(Type(str))
- jieba_dict_user = Optional(Type(str))
-
- # Unsupported settings, originally implemented in MkDocs
- indexing = Deprecated(message = "Unsupported option")
- prebuild_index = Deprecated(message = "Unsupported option")
- min_search_length = Deprecated(message = "Unsupported option")
diff --git a/docs/src/material/plugins/search/plugin.py b/docs/src/material/plugins/search/plugin.py
deleted file mode 100644
index 5c254e3f..00000000
--- a/docs/src/material/plugins/search/plugin.py
+++ /dev/null
@@ -1,580 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-import json
-import logging
-import os
-import regex as re
-
-from html import escape
-from html.parser import HTMLParser
-from mkdocs import utils
-from mkdocs.plugins import BasePlugin
-
-from .config import SearchConfig
-
-try:
- import jieba
-except ImportError:
- jieba = None
-
-# -----------------------------------------------------------------------------
-# Classes
-# -----------------------------------------------------------------------------
-
-# Search plugin
-class SearchPlugin(BasePlugin[SearchConfig]):
-
- # Initialize plugin
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- # Initialize incremental builds
- self.is_dirtyreload = False
-
- # Initialize search index cache
- self.search_index_prev = None
-
- # Determine whether we're serving the site
- def on_startup(self, *, command, dirty):
- self.is_dirty = dirty
-
- # Initialize plugin
- def on_config(self, config):
- if not self.config.enabled:
- return
-
- # Retrieve default value for language
- if not self.config.lang:
- self.config.lang = [self._translate(
- config, "search.config.lang"
- )]
-
- # Retrieve default value for separator
- if not self.config.separator:
- self.config.separator = self._translate(
- config, "search.config.separator"
- )
-
- # Retrieve default value for pipeline
- if not self.config.pipeline:
- self.config.pipeline = list(filter(len, re.split(
- r"\s*,\s*", self._translate(config, "search.config.pipeline")
- )))
-
- # Initialize search index
- self.search_index = SearchIndex(**self.config)
-
- # Set jieba dictionary, if given
- if self.config.jieba_dict:
- path = os.path.normpath(self.config.jieba_dict)
- if os.path.isfile(path):
- jieba.set_dictionary(path)
- log.debug(f"Loading jieba dictionary: {path}")
- else:
- log.warning(
- f"Configuration error for 'search.jieba_dict': "
- f"'{self.config.jieba_dict}' does not exist."
- )
-
- # Set jieba user dictionary, if given
- if self.config.jieba_dict_user:
- path = os.path.normpath(self.config.jieba_dict_user)
- if os.path.isfile(path):
- jieba.load_userdict(path)
- log.debug(f"Loading jieba user dictionary: {path}")
- else:
- log.warning(
- f"Configuration error for 'search.jieba_dict_user': "
- f"'{self.config.jieba_dict_user}' does not exist."
- )
-
- # Add page to search index
- def on_page_context(self, context, *, page, config, nav):
- if not self.config.enabled:
- return
-
- # Index page
- self.search_index.add_entry_from_context(page)
- page.content = re.sub(
- r"\s?data-search-\w+=\"[^\"]+\"",
- "",
- page.content
- )
-
- # Generate search index
- def on_post_build(self, *, config):
- if not self.config.enabled:
- return
-
- # Write search index
- base = os.path.join(config.site_dir, "search")
- path = os.path.join(base, "search_index.json")
-
- # Generate and write search index to file
- data = self.search_index.generate_search_index(self.search_index_prev)
- utils.write_file(data.encode("utf-8"), path)
-
- # Persist search index for repeated invocation
- if self.is_dirty:
- self.search_index_prev = self.search_index
-
- # Determine whether we're running under dirty reload
- def on_serve(self, server, *, config, builder):
- self.is_dirtyreload = self.is_dirty
-
- # -------------------------------------------------------------------------
-
- # Translate the given placeholder value
- def _translate(self, config, value):
- env = config.theme.get_env()
-
- # Load language template and return translation for placeholder
- language = "partials/language.html"
- template = env.get_template(language, None, { "config": config })
- return template.module.t(value)
-
-# -----------------------------------------------------------------------------
-
-# Search index with support for additional fields
-class SearchIndex:
-
- # Initialize search index
- def __init__(self, **config):
- self.config = config
- self.entries = []
-
- # Add page to search index
- def add_entry_from_context(self, page):
- search = page.meta.get("search", {})
- if search.get("exclude"):
- return
-
- # Divide page content into sections
- parser = Parser()
- parser.feed(page.content)
- parser.close()
-
- # Add sections to index
- for section in parser.data:
- if not section.is_excluded():
- self.create_entry_for_section(section, page.toc, page.url, page)
-
- # Override: graceful indexing and additional fields
- def create_entry_for_section(self, section, toc, url, page):
- item = self._find_toc_by_id(toc, section.id)
- if item:
- url = url + item.url
- elif section.id:
- url = url + "#" + section.id
-
- # Set page title as section title if none was given, which happens when
- # the first headline in a Markdown document is not a h1 headline. Also,
- # if a page title was set via front matter, use that even though a h1
- # might be given or the page name was specified in nav in mkdocs.yml
- if not section.title:
- section.title = [str(page.meta.get("title", page.title))]
-
- # Compute title and text
- title = "".join(section.title).strip()
- text = "".join(section.text).strip()
-
- # Segment Chinese characters if jieba is available
- if jieba:
- title = self._segment_chinese(title)
- text = self._segment_chinese(text)
-
- # Create entry for section
- entry = {
- "location": url,
- "title": title,
- "text": text
- }
-
- # Set document tags
- tags = page.meta.get("tags")
- if isinstance(tags, list):
- entry["tags"] = []
- for name in tags:
- if name and isinstance(name, (str, int, float, bool)):
- entry["tags"].append(name)
-
- # Set document boost
- search = page.meta.get("search", {})
- if "boost" in search:
- entry["boost"] = search["boost"]
-
- # Add entry to index
- self.entries.append(entry)
-
- # Generate search index
- def generate_search_index(self, prev):
- config = {
- key: self.config[key]
- for key in ["lang", "separator", "pipeline"]
- }
-
- # Hack: if we're running under dirty reload, the search index will only
- # include the entries for the current page. However, MkDocs > 1.4 allows
- # us to persist plugin state across rebuilds, which is exactly what we
- # do by passing the previously built index to this method. Thus, we just
- # remove the previous entries for the current page, and append the new
- # entries to the end of the index, as order doesn't matter.
- if prev and self.entries:
- path = self.entries[0]["location"]
-
- # Since we're sure that we're running under dirty reload, the list
- # of entries will only contain sections for a single page. Thus, we
- # use the first entry to remove all entries from the previous run
- # that belong to the current page. The rationale behind this is that
- # authors might add or remove section headers, so we need to make
- # sure that sections are synchronized correctly.
- entries = [
- entry for entry in prev.entries
- if not entry["location"].startswith(path)
- ]
-
- # Merge previous with current entries
- self.entries = entries + self.entries
-
- # Otherwise just set previous entries
- if prev and not self.entries:
- self.entries = prev.entries
-
- # Return search index as JSON
- data = { "config": config, "docs": self.entries }
- return json.dumps(
- data,
- separators = (",", ":"),
- default = str
- )
-
- # -------------------------------------------------------------------------
-
- # Retrieve item for anchor
- def _find_toc_by_id(self, toc, id):
- for toc_item in toc:
- if toc_item.id == id:
- return toc_item
-
- # Recurse into children of item
- toc_item = self._find_toc_by_id(toc_item.children, id)
- if toc_item is not None:
- return toc_item
-
- # No item found
- return None
-
- # Find and segment Chinese characters in string
- def _segment_chinese(self, data):
- expr = re.compile(r"(\p{IsHan}+)", re.UNICODE)
-
- # Replace callback
- def replace(match):
- value = match.group(0)
-
- # Replace occurrence in original string with segmented version and
- # surround with zero-width whitespace for efficient indexing
- return "".join([
- "\u200b",
- "\u200b".join(jieba.cut(value.encode("utf-8"))),
- "\u200b",
- ])
-
- # Return string with segmented occurrences
- return expr.sub(replace, data).strip("\u200b")
-
-# -----------------------------------------------------------------------------
-
-# HTML element
-class Element:
- """
- An element with attributes, essentially a small wrapper object for the
- parser to access attributes in other callbacks than handle_starttag.
- """
-
- # Initialize HTML element
- def __init__(self, tag, attrs = {}):
- self.tag = tag
- self.attrs = attrs
-
- # String representation
- def __repr__(self):
- return self.tag
-
- # Support comparison (compare by tag only)
- def __eq__(self, other):
- if other is Element:
- return self.tag == other.tag
- else:
- return self.tag == other
-
- # Support set operations
- def __hash__(self):
- return hash(self.tag)
-
- # Check whether the element should be excluded
- def is_excluded(self):
- return "data-search-exclude" in self.attrs
-
-# -----------------------------------------------------------------------------
-
-# HTML section
-class Section:
- """
- A block of text with markup, preceded by a title (with markup), i.e., a
- headline with a certain level (h1-h6). Internally used by the parser.
- """
-
- # Initialize HTML section
- def __init__(self, el, depth = 0):
- self.el = el
- self.depth = depth
-
- # Initialize section data
- self.text = []
- self.title = []
- self.id = None
-
- # String representation
- def __repr__(self):
- if self.id:
- return "#".join([self.el.tag, self.id])
- else:
- return self.el.tag
-
- # Check whether the section should be excluded
- def is_excluded(self):
- return self.el.is_excluded()
-
-# -----------------------------------------------------------------------------
-
-# HTML parser
-class Parser(HTMLParser):
- """
- This parser divides the given string of HTML into a list of sections, each
- of which are preceded by a h1-h6 level heading. A white- and blacklist of
- tags dictates which tags should be preserved as part of the index, and
- which should be ignored in their entirety.
- """
-
- # Initialize HTML parser
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- # Tags to skip
- self.skip = set([
- "object", # Objects
- "script", # Scripts
- "style" # Styles
- ])
-
- # Tags to keep
- self.keep = set([
- "p", # Paragraphs
- "code", "pre", # Code blocks
- "li", "ol", "ul", # Lists
- "sub", "sup" # Sub- and superscripts
- ])
-
- # Current context and section
- self.context = []
- self.section = None
-
- # All parsed sections
- self.data = []
-
- # Called at the start of every HTML tag
- def handle_starttag(self, tag, attrs):
- attrs = dict(attrs)
-
- # Ignore self-closing tags
- el = Element(tag, attrs)
- if not tag in void:
- self.context.append(el)
- else:
- return
-
- # Handle heading
- if tag in ([f"h{x}" for x in range(1, 7)]):
- depth = len(self.context)
- if "id" in attrs:
-
- # Ensure top-level section
- if tag != "h1" and not self.data:
- self.section = Section(Element("hx"), depth)
- self.data.append(self.section)
-
- # Set identifier, if not first section
- self.section = Section(el, depth)
- if self.data:
- self.section.id = attrs["id"]
-
- # Append section to list
- self.data.append(self.section)
-
- # Handle preface - ensure top-level section
- if not self.section:
- self.section = Section(Element("hx"))
- self.data.append(self.section)
-
- # Handle special cases to skip
- for key, value in attrs.items():
-
- # Skip block if explicitly excluded from search
- if key == "data-search-exclude":
- self.skip.add(el)
- return
-
- # Skip line numbers - see https://bit.ly/3GvubZx
- if key == "class" and value == "linenodiv":
- self.skip.add(el)
- return
-
- # Render opening tag if kept
- if not self.skip.intersection(self.context):
- if tag in self.keep:
-
- # Check whether we're inside the section title
- data = self.section.text
- if self.section.el in self.context:
- data = self.section.title
-
- # Append to section title or text
- data.append(f"<{tag}>")
-
- # Called at the end of every HTML tag
- def handle_endtag(self, tag):
- if not self.context or self.context[-1] != tag:
- return
-
- # Check whether we're exiting the current context, which happens when
- # a headline is nested in another element. In that case, we close the
- # current section, continuing to append data to the previous section,
- # which could also be a nested section – see https://bit.ly/3IxxIJZ
- if self.section.depth > len(self.context):
- for section in reversed(self.data):
- if section.depth <= len(self.context):
-
- # Set depth to infinity in order to denote that the current
- # section is exited and must never be considered again.
- self.section.depth = float("inf")
- self.section = section
- break
-
- # Remove element from skip list
- el = self.context.pop()
- if el in self.skip:
- if el.tag not in ["script", "style", "object"]:
- self.skip.remove(el)
- return
-
- # Render closing tag if kept
- if not self.skip.intersection(self.context):
- if tag in self.keep:
-
- # Check whether we're inside the section title
- data = self.section.text
- if self.section.el in self.context:
- data = self.section.title
-
- # Search for corresponding opening tag
- index = data.index(f"<{tag}>")
- for i in range(index + 1, len(data)):
- if not data[i].isspace():
- index = len(data)
- break
-
- # Remove element if empty (or only whitespace)
- if len(data) > index:
- while len(data) > index:
- data.pop()
-
- # Append to section title or text
- else:
- data.append(f"</{tag}>")
-
- # Called for the text contents of each tag
- def handle_data(self, data):
- if self.skip.intersection(self.context):
- return
-
- # Collapse whitespace in non-pre contexts
- if not "pre" in self.context:
- if not data.isspace():
- data = data.replace("\n", " ")
- else:
- data = " "
-
- # Handle preface - ensure top-level section
- if not self.section:
- self.section = Section(Element("hx"))
- self.data.append(self.section)
-
- # Handle section headline
- if self.section.el in self.context:
- permalink = False
- for el in self.context:
- if el.tag == "a" and el.attrs.get("class") == "headerlink":
- permalink = True
-
- # Ignore permalinks
- if not permalink:
- self.section.title.append(
- escape(data, quote = False)
- )
-
- # Collapse adjacent whitespace
- elif data.isspace():
- if not self.section.text or not self.section.text[-1].isspace():
- self.section.text.append(data)
- elif "pre" in self.context:
- self.section.text.append(data)
-
- # Handle everything else
- else:
- self.section.text.append(
- escape(data, quote = False)
- )
-
-# -----------------------------------------------------------------------------
-# Data
-# -----------------------------------------------------------------------------
-
-# Set up logging
-log = logging.getLogger("mkdocs.material.search")
-
-# Tags that are self-closing
-void = set([
- "area", # Image map areas
- "base", # Document base
- "br", # Line breaks
- "col", # Table columns
- "embed", # External content
- "hr", # Horizontal rules
- "img", # Images
- "input", # Input fields
- "link", # Links
- "meta", # Metadata
- "param", # External parameters
- "source", # Image source sets
- "track", # Text track
- "wbr" # Line break opportunities
-])
diff --git a/docs/src/material/plugins/social/__init__.py b/docs/src/material/plugins/social/__init__.py
deleted file mode 100644
index d1899378..00000000
--- a/docs/src/material/plugins/social/__init__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
diff --git a/docs/src/material/plugins/social/config.py b/docs/src/material/plugins/social/config.py
deleted file mode 100644
index 2d87c25e..00000000
--- a/docs/src/material/plugins/social/config.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-from mkdocs.config.base import Config
-from mkdocs.config.config_options import Deprecated, Type
-
-# -----------------------------------------------------------------------------
-# Classes
-# -----------------------------------------------------------------------------
-
-# Social plugin configuration
-class SocialConfig(Config):
- enabled = Type(bool, default = True)
- cache_dir = Type(str, default = ".cache/plugin/social")
-
- # Settings for social cards
- cards = Type(bool, default = True)
- cards_dir = Type(str, default = "assets/images/social")
- cards_layout_options = Type(dict, default = {})
-
- # Deprecated settings
- cards_color = Deprecated(
- option_type = Type(dict, default = {}),
- message =
- "Deprecated, use 'cards_layout_options.background_color' "
- "and 'cards_layout_options.color' with 'default' layout"
- )
- cards_font = Deprecated(
- option_type = Type(str),
- message = "Deprecated, use 'cards_layout_options.font_family'"
- )
diff --git a/docs/src/material/plugins/social/plugin.py b/docs/src/material/plugins/social/plugin.py
deleted file mode 100644
index 3cdfa3ce..00000000
--- a/docs/src/material/plugins/social/plugin.py
+++ /dev/null
@@ -1,516 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-# -----------------------------------------------------------------------------
-# Disclaimer
-# -----------------------------------------------------------------------------
-# Please note: this version of the social plugin is not actively development
-# anymore. Instead, Material for MkDocs Insiders ships a complete rewrite of
-# the plugin which is much more powerful and addresses all shortcomings of
-# this implementation. Additionally, the new social plugin allows to create
-# entirely custom social cards. You can probably imagine, that this was a lot
-# of work to pull off. If you run into problems, or want to have additional
-# functionality, please consider sponsoring the project. You can then use the
-# new version of the plugin immediately.
-# -----------------------------------------------------------------------------
-
-import concurrent.futures
-import functools
-import logging
-import os
-import posixpath
-import re
-import requests
-import sys
-
-from collections import defaultdict
-from hashlib import md5
-from io import BytesIO
-from mkdocs.commands.build import DuplicateFilter
-from mkdocs.exceptions import PluginError
-from mkdocs.plugins import BasePlugin
-from shutil import copyfile
-from tempfile import TemporaryFile
-from zipfile import ZipFile
-try:
- from cairosvg import svg2png
- from PIL import Image, ImageDraw, ImageFont
-except ImportError:
- pass
-
-from .config import SocialConfig
-
-
-# -----------------------------------------------------------------------------
-# Classes
-# -----------------------------------------------------------------------------
-
-# Social plugin
-class SocialPlugin(BasePlugin[SocialConfig]):
-
- def __init__(self):
- self._executor = concurrent.futures.ThreadPoolExecutor(4)
-
- # Retrieve configuration
- def on_config(self, config):
- self.color = colors.get("indigo")
- self.config.cards = self.config.enabled
- if not self.config.cards:
- return
-
- # Check dependencies
- if "Image" not in globals():
- raise PluginError(
- "Required dependencies of \"social\" plugin not found. "
- "Install with: pip install \"mkdocs-material[imaging]\""
- )
-
- # Move color options
- if self.config.cards_color:
-
- # Move background color to new option
- value = self.config.cards_color.get("fill")
- if value:
- self.config.cards_layout_options["background_color"] = value
-
- # Move color to new option
- value = self.config.cards_color.get("text")
- if value:
- self.config.cards_layout_options["color"] = value
-
- # Move font family to new option
- if self.config.cards_font:
- value = self.config.cards_font
- self.config.cards_layout_options["font_family"] = value
-
- # Check if site URL is defined
- if not config.site_url:
- log.warning(
- "The \"site_url\" option is not set. The cards are generated, "
- "but not linked, so they won't be visible on social media."
- )
-
- # Ensure presence of cache directory
- self.cache = self.config.cache_dir
- if not os.path.isdir(self.cache):
- os.makedirs(self.cache)
-
- # Retrieve palette from theme configuration
- theme = config.theme
- if "palette" in theme:
- palette = theme["palette"]
-
- # Use first palette, if multiple are defined
- if isinstance(palette, list):
- palette = palette[0]
-
- # Set colors according to palette
- if "primary" in palette and palette["primary"]:
- primary = palette["primary"].replace(" ", "-")
- self.color = colors.get(primary, self.color)
-
- # Retrieve color overrides
- options = self.config.cards_layout_options
- self.color = {
- "fill": options.get("background_color", self.color["fill"]),
- "text": options.get("color", self.color["text"])
- }
-
- # Retrieve logo and font
- self._resized_logo_promise = self._executor.submit(self._load_resized_logo, config)
- self.font = self._load_font(config)
-
- self._image_promises = []
-
- # Create social cards
- def on_page_markdown(self, markdown, page, config, files):
- if not self.config.cards:
- return
-
- # Resolve image directory
- directory = self.config.cards_dir
- file, _ = os.path.splitext(page.file.src_path)
-
- # Resolve path of image
- path = "{}.png".format(os.path.join(
- config.site_dir,
- directory,
- file
- ))
-
- # Resolve path of image directory
- directory = os.path.dirname(path)
- if not os.path.isdir(directory):
- os.makedirs(directory)
-
- # Compute site name
- site_name = config.site_name
-
- # Compute page title and description
- title = page.meta.get("title", page.title)
- description = config.site_description or ""
- if "description" in page.meta:
- description = page.meta["description"]
-
- # Check type of meta title - see https://t.ly/m1Us
- if not isinstance(title, str):
- log.error(
- f"Page meta title of page '{page.file.src_uri}' must be a "
- f"string, but is of type \"{type(title)}\"."
- )
- sys.exit(1)
-
- # Check type of meta description - see https://t.ly/m1Us
- if not isinstance(description, str):
- log.error(
- f"Page meta description of '{page.file.src_uri}' must be a "
- f"string, but is of type \"{type(description)}\"."
- )
- sys.exit(1)
-
- # Generate social card if not in cache
- hash = md5("".join([
- site_name,
- str(title),
- description
- ]).encode("utf-8"))
- file = os.path.join(self.cache, f"{hash.hexdigest()}.png")
- self._image_promises.append(self._executor.submit(
- self._cache_image,
- cache_path = file, dest_path = path,
- render_function = lambda: self._render_card(site_name, title, description)
- ))
-
- # Inject meta tags into page
- meta = page.meta.get("meta", [])
- page.meta["meta"] = meta + self._generate_meta(page, config)
-
- def on_post_build(self, config):
- if not self.config.cards:
- return
-
- # Check for exceptions
- for promise in self._image_promises:
- promise.result()
-
- # -------------------------------------------------------------------------
-
- # Render image to cache (if not present), then copy from cache to site
- def _cache_image(self, cache_path, dest_path, render_function):
- if not os.path.isfile(cache_path):
- image = render_function()
- image.save(cache_path)
-
- # Copy file from cache
- copyfile(cache_path, dest_path)
-
- @functools.lru_cache(maxsize=None)
- def _get_font(self, kind, size):
- return ImageFont.truetype(self.font[kind], size)
-
- # Render social card
- def _render_card(self, site_name, title, description):
- # Render background and logo
- image = self._render_card_background((1200, 630), self.color["fill"])
- image.alpha_composite(
- self._resized_logo_promise.result(),
- (1200 - 228, 64 - 4)
- )
-
- # Render site name
- font = self._get_font("Bold", 36)
- image.alpha_composite(
- self._render_text((826, 48), font, site_name, 1, 20),
- (64 + 4, 64)
- )
-
- # Render page title
- font = self._get_font("Bold", 92)
- image.alpha_composite(
- self._render_text((826, 328), font, title, 3, 30),
- (64, 160)
- )
-
- # Render page description
- font = self._get_font("Regular", 28)
- image.alpha_composite(
- self._render_text((826, 80), font, description, 2, 14),
- (64 + 4, 512)
- )
-
- # Return social card image
- return image
-
- # Render social card background
- def _render_card_background(self, size, fill):
- return Image.new(mode = "RGBA", size = size, color = fill)
-
- @functools.lru_cache(maxsize=None)
- def _tmp_context(self):
- image = Image.new(mode = "RGBA", size = (50, 50))
- return ImageDraw.Draw(image)
-
- @functools.lru_cache(maxsize=None)
- def _text_bounding_box(self, text, font):
- return self._tmp_context().textbbox((0, 0), text, font = font)
-
- # Render social card text
- def _render_text(self, size, font, text, lmax, spacing = 0):
- width = size[0]
- lines, words = [], []
-
- # Remove remnant HTML tags
- text = re.sub(r"(<[^>]+>)", "", text)
-
- # Retrieve y-offset of textbox to correct for spacing
- yoffset = 0
-
- # Create drawing context and split text into lines
- for word in text.split(" "):
- combine = " ".join(words + [word])
- textbox = self._text_bounding_box(combine, font = font)
- yoffset = textbox[1]
- if not words or textbox[2] <= width:
- words.append(word)
- else:
- lines.append(words)
- words = [word]
-
- # Join words for each line and create image
- lines.append(words)
- lines = [" ".join(line) for line in lines]
- image = Image.new(mode = "RGBA", size = size)
-
- # Create drawing context and split text into lines
- context = ImageDraw.Draw(image)
- context.text(
- (0, spacing / 2 - yoffset), "\n".join(lines[:lmax]),
- font = font, fill = self.color["text"], spacing = spacing - yoffset
- )
-
- # Return text image
- return image
-
- # -------------------------------------------------------------------------
-
- # Generate meta tags
- def _generate_meta(self, page, config):
- directory = self.config.cards_dir
- file, _ = os.path.splitext(page.file.src_uri)
-
- # Compute page title
- title = page.meta.get("title", page.title)
- if not page.is_homepage:
- title = f"{title} - {config.site_name}"
-
- # Compute page description
- description = config.site_description
- if "description" in page.meta:
- description = page.meta["description"]
-
- # Resolve image URL
- url = "{}.png".format(posixpath.join(
- config.site_url or ".",
- directory,
- file
- ))
-
- # Ensure forward slashes
- url = url.replace(os.path.sep, "/")
-
- # Return meta tags
- return [
-
- # Meta tags for Open Graph
- { "property": "og:type", "content": "website" },
- { "property": "og:title", "content": title },
- { "property": "og:description", "content": description },
- { "property": "og:image", "content": url },
- { "property": "og:image:type", "content": "image/png" },
- { "property": "og:image:width", "content": "1200" },
- { "property": "og:image:height", "content": "630" },
- { "property": "og:url", "content": page.canonical_url },
-
- # Meta tags for Twitter
- { "name": "twitter:card", "content": "summary_large_image" },
- # { "name": "twitter:site", "content": user },
- # { "name": "twitter:creator", "content": user },
- { "name": "twitter:title", "content": title },
- { "name": "twitter:description", "content": description },
- { "name": "twitter:image", "content": url }
- ]
-
- def _load_resized_logo(self, config, width = 144):
- logo = self._load_logo(config)
- height = int(width * logo.height / logo.width)
- return logo.resize((width, height))
-
- # Retrieve logo image or icon
- def _load_logo(self, config):
- theme = config.theme
-
- # Handle images (precedence over icons)
- if "logo" in theme:
- _, extension = os.path.splitext(theme["logo"])
-
- path = os.path.join(config.docs_dir, theme["logo"])
-
- # Allow users to put the logo inside their custom_dir (theme["logo"] case)
- if theme.custom_dir:
- custom_dir_logo = os.path.join(theme.custom_dir, theme["logo"])
- if os.path.exists(custom_dir_logo):
- path = custom_dir_logo
-
- # Load SVG and convert to PNG
- if extension == ".svg":
- return self._load_logo_svg(path)
-
- # Load PNG, JPEG, etc.
- return Image.open(path).convert("RGBA")
-
- # Handle icons
- icon = theme["icon"] or {}
- if "logo" in icon and icon["logo"]:
- logo = icon["logo"]
- else:
- logo = "material/library"
-
- # Resolve path of package
- base = os.path.abspath(os.path.join(
- os.path.dirname(__file__),
- "../.."
- ))
-
- path = f"{base}/templates/.icons/{logo}.svg"
-
- # Allow users to put the logo inside their custom_dir (theme["icon"]["logo"] case)
- if theme.custom_dir:
- custom_dir_logo = os.path.join(theme.custom_dir, ".icons", f"{logo}.svg")
- if os.path.exists(custom_dir_logo):
- path = custom_dir_logo
-
- # Load icon data and fill with color
- return self._load_logo_svg(path, self.color["text"])
-
- # Load SVG file and convert to PNG
- def _load_logo_svg(self, path, fill = None):
- file = BytesIO()
- data = open(path).read()
-
- # Fill with color, if given
- if fill:
- data = data.replace("<svg", f"<svg fill=\"{fill}\"")
-
- # Convert to PNG and return image
- svg2png(bytestring = data, write_to = file, scale = 10)
- return Image.open(file)
-
- # Retrieve font
- def _load_font(self, config):
- name = self.config.cards_layout_options.get("font_family")
- if not name:
-
- # Retrieve from theme (default: Roboto)
- theme = config.theme
- if isinstance(theme["font"], dict) and "text" in theme["font"]:
- name = theme["font"]["text"]
- else:
- name = "Roboto"
-
- # Google fonts can return varients like OpenSans_Condensed-Regular.ttf so
- # we only use the font requested e.g. OpenSans-Regular.ttf
- font_filename_base = name.replace(' ', '')
- filename_regex = re.escape(font_filename_base)+r"-(\w+)\.[ot]tf$"
-
- font = {}
- # Check for cached files - note these may be in subfolders
- for currentpath, folders, files in os.walk(self.cache):
- for file in files:
- # Map available font weights to file paths
- fname = os.path.join(currentpath, file)
- match = re.search(filename_regex, fname)
- if match:
- font[match.group(1)] = fname
-
- # If none found, fetch from Google and try again
- if len(font) == 0:
- self._load_font_from_google(name)
- for currentpath, folders, files in os.walk(self.cache):
- for file in files:
- # Map available font weights to file paths
- fname = os.path.join(currentpath, file)
- match = re.search(filename_regex, fname)
- if match:
- font[match.group(1)] = fname
-
- # Return available font weights with fallback
- return defaultdict(lambda: font["Regular"], font)
-
- # Retrieve font from Google Fonts
- def _load_font_from_google(self, name):
- url = "https://fonts.google.com/download?family={}"
- res = requests.get(url.format(name.replace(" ", "+")), stream = True)
-
- # Write archive to temporary file
- tmp = TemporaryFile()
- for chunk in res.iter_content(chunk_size = 32768):
- tmp.write(chunk)
-
- # Unzip fonts from temporary file
- zip = ZipFile(tmp)
- files = [file for file in zip.namelist() if file.endswith(".ttf") or file.endswith(".otf")]
- zip.extractall(self.cache, files)
-
- # Close and delete temporary file
- tmp.close()
- return files
-
-# -----------------------------------------------------------------------------
-# Data
-# -----------------------------------------------------------------------------
-
-# Set up logging
-log = logging.getLogger("mkdocs")
-log.addFilter(DuplicateFilter())
-
-# Color palette
-colors = dict({
- "red": { "fill": "#ef5552", "text": "#ffffff" },
- "pink": { "fill": "#e92063", "text": "#ffffff" },
- "purple": { "fill": "#ab47bd", "text": "#ffffff" },
- "deep-purple": { "fill": "#7e56c2", "text": "#ffffff" },
- "indigo": { "fill": "#4051b5", "text": "#ffffff" },
- "blue": { "fill": "#2094f3", "text": "#ffffff" },
- "light-blue": { "fill": "#02a6f2", "text": "#ffffff" },
- "cyan": { "fill": "#00bdd6", "text": "#ffffff" },
- "teal": { "fill": "#009485", "text": "#ffffff" },
- "green": { "fill": "#4cae4f", "text": "#ffffff" },
- "light-green": { "fill": "#8bc34b", "text": "#ffffff" },
- "lime": { "fill": "#cbdc38", "text": "#000000" },
- "yellow": { "fill": "#ffec3d", "text": "#000000" },
- "amber": { "fill": "#ffc105", "text": "#000000" },
- "orange": { "fill": "#ffa724", "text": "#000000" },
- "deep-orange": { "fill": "#ff6e42", "text": "#ffffff" },
- "brown": { "fill": "#795649", "text": "#ffffff" },
- "grey": { "fill": "#757575", "text": "#ffffff" },
- "blue-grey": { "fill": "#546d78", "text": "#ffffff" },
- "black": { "fill": "#000000", "text": "#ffffff" },
- "white": { "fill": "#ffffff", "text": "#000000" }
-})
diff --git a/docs/src/material/plugins/tags/__init__.py b/docs/src/material/plugins/tags/__init__.py
deleted file mode 100644
index 19994c95..00000000
--- a/docs/src/material/plugins/tags/__init__.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-# -----------------------------------------------------------------------------
-# Functions
-# -----------------------------------------------------------------------------
-
-# Casefold a string for comparison when sorting
-def casefold(tag: str):
- return tag.casefold()
diff --git a/docs/src/material/plugins/tags/config.py b/docs/src/material/plugins/tags/config.py
deleted file mode 100644
index f2d95084..00000000
--- a/docs/src/material/plugins/tags/config.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-from functools import partial
-from markdown.extensions.toc import slugify
-from mkdocs.config.config_options import Optional, Type
-from mkdocs.config.base import Config
-
-from . import casefold
-
-# -----------------------------------------------------------------------------
-# Classes
-# -----------------------------------------------------------------------------
-
-# Tags plugin configuration
-class TagsConfig(Config):
- enabled = Type(bool, default = True)
-
- # Settings for tags
- tags = Type(bool, default = True)
- tags_file = Optional(Type(str))
diff --git a/docs/src/material/plugins/tags/plugin.py b/docs/src/material/plugins/tags/plugin.py
deleted file mode 100644
index e5ce6bde..00000000
--- a/docs/src/material/plugins/tags/plugin.py
+++ /dev/null
@@ -1,182 +0,0 @@
-# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-import logging
-import sys
-
-from collections import defaultdict
-from markdown.extensions.toc import slugify
-from mkdocs import utils
-from mkdocs.plugins import BasePlugin
-
-# deprecated, but kept for downward compatibility. Use 'material.plugins.tags'
-# as an import source instead. This import is removed in the next major version.
-from . import casefold
-from .config import TagsConfig
-
-# -----------------------------------------------------------------------------
-# Classes
-# -----------------------------------------------------------------------------
-
-# Tags plugin
-class TagsPlugin(BasePlugin[TagsConfig]):
- supports_multiple_instances = True
-
- # Initialize plugin
- def on_config(self, config):
- if not self.config.enabled:
- return
-
- # Skip if tags should not be built
- if not self.config.tags:
- return
-
- # Initialize tags
- self.tags = defaultdict(list)
- self.tags_file = None
-
- # Retrieve tags mapping from configuration
- self.tags_map = config.extra.get("tags")
-
- # Use override of slugify function
- toc = { "slugify": slugify, "separator": "-" }
- if "toc" in config.mdx_configs:
- toc = { **toc, **config.mdx_configs["toc"] }
-
- # Partially apply slugify function
- self.slugify = lambda value: (
- toc["slugify"](str(value), toc["separator"])
- )
-
- # Hack: 2nd pass for tags index page(s)
- def on_nav(self, nav, config, files):
- if not self.config.enabled:
- return
-
- # Skip if tags should not be built
- if not self.config.tags:
- return
-
- # Resolve tags index page
- file = self.config.tags_file
- if file:
- self.tags_file = self._get_tags_file(files, file)
-
- # Build and render tags index page
- def on_page_markdown(self, markdown, page, config, files):
- if not self.config.enabled:
- return
-
- # Skip if tags should not be built
- if not self.config.tags:
- return
-
- # Skip, if page is excluded
- if page.file.inclusion.is_excluded():
- return
-
- # Render tags index page
- if page.file == self.tags_file:
- return self._render_tag_index(markdown)
-
- # Add page to tags index
- for tag in page.meta.get("tags", []):
- self.tags[tag].append(page)
-
- # Inject tags into page (after search and before minification)
- def on_page_context(self, context, page, config, nav):
- if not self.config.enabled:
- return
-
- # Skip if tags should not be built
- if not self.config.tags:
- return
-
- # Provide tags for page
- if "tags" in page.meta:
- context["tags"] = [
- self._render_tag(tag)
- for tag in page.meta["tags"]
- ]
-
- # -------------------------------------------------------------------------
-
- # Obtain tags file
- def _get_tags_file(self, files, path):
- file = files.get_file_from_path(path)
- if not file:
- log.error(f"Tags file '{path}' does not exist.")
- sys.exit(1)
-
- # Add tags file to files
- files.append(file)
- return file
-
- # Render tags index
- def _render_tag_index(self, markdown):
- if not "[TAGS]" in markdown:
- markdown += "\n[TAGS]"
-
- # Replace placeholder in Markdown with rendered tags index
- return markdown.replace("[TAGS]", "\n".join([
- self._render_tag_links(*args)
- for args in sorted(self.tags.items())
- ]))
-
- # Render the given tag and links to all pages with occurrences
- def _render_tag_links(self, tag, pages):
- classes = ["md-tag"]
- if isinstance(self.tags_map, dict):
- classes.append("md-tag-icon")
- type = self.tags_map.get(tag)
- if type:
- classes.append(f"md-tag--{type}")
-
- # Render section for tag and a link to each page
- classes = " ".join(classes)
- content = [f"## <span class=\"{classes}\">{tag}</span>", ""]
- for page in pages:
- url = utils.get_relative_url(
- page.file.src_uri,
- self.tags_file.src_uri
- )
-
- # Render link to page
- title = page.meta.get("title", page.title)
- content.append(f"- [{title}]({url})")
-
- # Return rendered tag links
- return "\n".join(content)
-
- # Render the given tag, linking to the tags index (if enabled)
- def _render_tag(self, tag):
- type = self.tags_map.get(tag) if self.tags_map else None
- if not self.tags_file or not self.slugify:
- return dict(name = tag, type = type)
- else:
- url = f"{self.tags_file.url}#{self.slugify(tag)}"
- return dict(name = tag, type = type, url = url)
-
-# -----------------------------------------------------------------------------
-# Data
-# -----------------------------------------------------------------------------
-
-# Set up logging
-log = logging.getLogger("mkdocs.material.tags")