diff options
| author | 2023-12-15 10:08:56 +0800 | |
|---|---|---|
| committer | 2023-12-15 10:08:56 +0800 | |
| commit | d697a1774ad8571e9314e3bd85aa141170587d19 (patch) | |
| tree | cd3c3d84e3288db1f8e574a0b4dfb85e2e40f764 /src/plugins/blog/structure | |
| parent | 4943d5eff52a75caaccda6a1d84183032f06be26 (diff) | |
| parent | 4dafb0f0a81255193f2a44df5d203239325e2236 (diff) | |
| download | infini-d697a1774ad8571e9314e3bd85aa141170587d19.tar.gz infini-d697a1774ad8571e9314e3bd85aa141170587d19.zip | |
Merge branch 'master' into master
Diffstat (limited to 'src/plugins/blog/structure')
| -rw-r--r-- | src/plugins/blog/structure/__init__.py | 292 | ||||
| -rw-r--r-- | src/plugins/blog/structure/config.py | 37 | ||||
| -rw-r--r-- | src/plugins/blog/structure/markdown.py | 58 | ||||
| -rw-r--r-- | src/plugins/blog/structure/options.py | 87 |
4 files changed, 0 insertions, 474 deletions
diff --git a/src/plugins/blog/structure/__init__.py b/src/plugins/blog/structure/__init__.py deleted file mode 100644 index 2fc541fe..00000000 --- a/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/src/plugins/blog/structure/config.py b/src/plugins/blog/structure/config.py deleted file mode 100644 index 129491b9..00000000 --- a/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/src/plugins/blog/structure/markdown.py b/src/plugins/blog/structure/markdown.py deleted file mode 100644 index 64ade554..00000000 --- a/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/src/plugins/blog/structure/options.py b/src/plugins/blog/structure/options.py deleted file mode 100644 index 281dec9f..00000000 --- a/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 |
