aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/docs/src/plugins/blog
diff options
context:
space:
mode:
author苏向夜 <fu050409@163.com>2024-01-25 17:46:34 +0800
committer苏向夜 <fu050409@163.com>2024-01-25 17:46:34 +0800
commitc4eb3ae7a74c7c6881540d7d04502600197a8b2a (patch)
treec920c29c383da4d496a0f646e5d4d0f5de10a406 /docs/src/plugins/blog
parent3c3e121bc845573a2d2408c0db6be6f841fd8efc (diff)
downloadinfini-c4eb3ae7a74c7c6881540d7d04502600197a8b2a.tar.gz
infini-c4eb3ae7a74c7c6881540d7d04502600197a8b2a.zip
:fire: feat(docs): remove old documents
Diffstat (limited to 'docs/src/plugins/blog')
-rw-r--r--docs/src/plugins/blog/__init__.py19
-rw-r--r--docs/src/plugins/blog/author.py38
-rw-r--r--docs/src/plugins/blog/config.py88
-rw-r--r--docs/src/plugins/blog/plugin.py884
-rw-r--r--docs/src/plugins/blog/readtime/__init__.py51
-rw-r--r--docs/src/plugins/blog/readtime/parser.py45
-rw-r--r--docs/src/plugins/blog/structure/__init__.py292
-rw-r--r--docs/src/plugins/blog/structure/config.py37
-rw-r--r--docs/src/plugins/blog/structure/markdown.py58
-rw-r--r--docs/src/plugins/blog/structure/options.py87
-rw-r--r--docs/src/plugins/blog/templates/__init__.py42
11 files changed, 0 insertions, 1641 deletions
diff --git a/docs/src/plugins/blog/__init__.py b/docs/src/plugins/blog/__init__.py
deleted file mode 100644
index d1899378..00000000
--- a/docs/src/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/plugins/blog/author.py b/docs/src/plugins/blog/author.py
deleted file mode 100644
index 1dcfc2de..00000000
--- a/docs/src/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/plugins/blog/config.py b/docs/src/plugins/blog/config.py
deleted file mode 100644
index c7a85095..00000000
--- a/docs/src/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/plugins/blog/plugin.py b/docs/src/plugins/blog/plugin.py
deleted file mode 100644
index 375b8cfe..00000000
--- a/docs/src/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/plugins/blog/readtime/__init__.py b/docs/src/plugins/blog/readtime/__init__.py
deleted file mode 100644
index a0c149b9..00000000
--- a/docs/src/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/plugins/blog/readtime/parser.py b/docs/src/plugins/blog/readtime/parser.py
deleted file mode 100644
index b91a7b30..00000000
--- a/docs/src/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/plugins/blog/structure/__init__.py b/docs/src/plugins/blog/structure/__init__.py
deleted file mode 100644
index 2fc541fe..00000000
--- a/docs/src/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/plugins/blog/structure/config.py b/docs/src/plugins/blog/structure/config.py
deleted file mode 100644
index 129491b9..00000000
--- a/docs/src/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/plugins/blog/structure/markdown.py b/docs/src/plugins/blog/structure/markdown.py
deleted file mode 100644
index 64ade554..00000000
--- a/docs/src/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/plugins/blog/structure/options.py b/docs/src/plugins/blog/structure/options.py
deleted file mode 100644
index 281dec9f..00000000
--- a/docs/src/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/plugins/blog/templates/__init__.py b/docs/src/plugins/blog/templates/__init__.py
deleted file mode 100644
index 9f7d794b..00000000
--- a/docs/src/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)