Refactored blog plugin (+ other plugins)

This commit is contained in:
squidfunk 2023-08-21 17:45:32 +02:00
parent 1fa81d05f4
commit 38b7282b20
No known key found for this signature in database
GPG Key ID: 5ED40BC4F9C436DF
64 changed files with 2726 additions and 1717 deletions

View File

@ -1,4 +1,5 @@
squidfunk:
name: Martin Donath
description: Creator
avatar: https://avatars.githubusercontent.com/u/932156
authors:
squidfunk:
name: Martin Donath
description: Creator
avatar: https://avatars.githubusercontent.com/u/932156

View File

@ -0,0 +1,19 @@
# 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.

View File

@ -52,14 +52,14 @@
{% include "partials/icons.html" %}
{% endblock %}
{% block libs %}
{% for path in config.extra.polyfills %}
<script src="{{ path | url }}"></script>
{% for script in config.extra.polyfills %}
{{ script | script_tag }}
{% endfor %}
{% endblock %}
{% block fonts %}
{% if config.theme.font != false %}
{% set text = config.theme.font.text | d("Roboto", true) %}
{% set code = config.theme.font.code | d("Roboto Mono", true) %}
{% set text = config.theme.font.get("text", "Roboto") %}
{% set code = config.theme.font.get("code", "Roboto Mono") %}
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family={{
text | replace(' ', '+') + ':300,300i,400,400i,700,700i%7C' +
@ -252,11 +252,7 @@
{% block scripts %}
<script src="{{ 'assets/javascripts/bundle.9e673e97.min.js' | url }}"></script>
{% for script in config.extra_javascript %}
{% if script.path %}
{{ script | script_tag }}
{% else %}
<script src="{{ script | url }}"></script>
{% endif %}
{{ script | script_tag }}
{% endfor %}
{% endblock %}
</body>

View File

@ -1,16 +0,0 @@
{#-
This file was automatically generated - do not edit
-#}
{% extends "main.html" %}
{% block container %}
<div class="md-content" data-md-component="content">
<div class="md-content__inner">
<header class="md-typeset">
{{ page.content }}
</header>
{% for post in posts %}
{% include "partials/post.html" %}
{% endfor %}
</div>
</div>
{% endblock %}

View File

@ -1,16 +0,0 @@
{#-
This file was automatically generated - do not edit
-#}
{% extends "main.html" %}
{% block container %}
<div class="md-content" data-md-component="content">
<div class="md-content__inner">
<header class="md-typeset">
{{ page.content }}
</header>
{% for post in posts %}
{% include "partials/post.html" %}
{% endfor %}
</div>
</div>
{% endblock %}

View File

@ -45,17 +45,17 @@
<li class="md-nav__item">
<div class="md-nav__link">
{% include ".icons/material/calendar.svg" %}
<time datetime="{{ page.meta.date }}" class="md-ellipsis">
{{- page.meta.date_format -}}
<time datetime="{{ page.config.date.created }}" class="md-ellipsis">
{{- page.config.date.created | date -}}
</time>
</div>
</li>
{% if page.meta.date_updated %}
{% if page.config.date.updated %}
<li class="md-nav__item">
<div class="md-nav__link">
{% include ".icons/material/calendar-clock.svg" %}
<time datetime="{{ page.meta.date_updated }}" class="md-ellipsis">
{{- page.meta.date_updated_format -}}
<time datetime="{{ page.config.date.updated }}" class="md-ellipsis">
{{- page.config.date.updated | date -}}
</time>
</div>
</li>
@ -76,8 +76,8 @@
</div>
</li>
{% endif %}
{% if page.meta.readtime %}
{% set time = page.meta.readtime %}
{% if page.config.readtime %}
{% set time = page.config.readtime %}
<li class="md-nav__item">
<div class="md-nav__link">
{% include ".icons/material/clock-outline.svg" %}

View File

@ -11,9 +11,11 @@
{% for post in posts %}
{% include "partials/post.html" %}
{% endfor %}
{% block pagination %}
{% include "partials/pagination.html" %}
{% endblock %}
{% if pagination %}
{% block pagination %}
{% include "partials/pagination.html" %}
{% endblock %}
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -15,8 +15,8 @@
<div class="md-post__meta md-meta">
<ul class="md-meta__list">
<li class="md-meta__item">
<time datetime="{{ post.meta.date }}">
{{- post.meta.date_format -}}
<time datetime="{{ post.config.date.created }}">
{{- post.config.date.created | date -}}
</time>
{#- Collapse whitespace -#}
</li>
@ -31,8 +31,8 @@
{% endfor -%}
</li>
{% endif %}
{% if post.meta.readtime %}
{% set time = post.meta.readtime %}
{% if post.config.readtime %}
{% set time = post.config.readtime %}
<li class="md-meta__item">
{% if time == 1 %}
{{ lang.t("readtime.one") }}
@ -42,7 +42,7 @@
</li>
{% endif %}
</ul>
{% if post.meta.draft %}
{% if post.config.draft %}
<span class="md-draft">
{{ lang.t("blog.draft") }}
</span>

View File

@ -0,0 +1,19 @@
# 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.

View File

@ -0,0 +1,19 @@
# 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.

View File

@ -0,0 +1,38 @@
# 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 = {})

View File

@ -24,10 +24,10 @@ from mkdocs.config.config_options import Choice, Deprecated, Optional, Type
from mkdocs.config.base import Config
# -----------------------------------------------------------------------------
# Class
# Classes
# -----------------------------------------------------------------------------
# Blog plugin configuration scheme
# Blog plugin configuration
class BlogConfig(Config):
enabled = Type(bool, default = True)
@ -36,6 +36,7 @@ class BlogConfig(Config):
blog_toc = Type(bool, default = False)
# Options 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}")
@ -70,7 +71,9 @@ class BlogConfig(Config):
pagination = Type(bool, default = True)
pagination_per_page = Type(int, default = 10)
pagination_url_format = Type(str, default = "page/{page}")
pagination_template = Type(str, default = "~2~")
pagination_format = Type(str, default = "~2~")
pagination_if_single_page = Type(bool, default = False)
pagination_keep_content = Type(bool, default = False)
# Options for authors
authors = Type(bool, default = True)
@ -80,3 +83,6 @@ class BlogConfig(Config):
draft = Type(bool, default = False)
draft_on_serve = Type(bool, default = True)
draft_if_future_date = Type(bool, default = False)
# Deprecated options
pagination_template = Deprecated(moved_to = "pagination_format")

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,277 @@
# 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 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 typing import Union
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 for error reporting
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 views like archive and
# category views. They are not rendered as standalone pages, but are
# included in the context of the parent post. 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
# Initialize configuration, contents and metadata
self.config = post.config
self.markdown = post.markdown
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
# 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: Union[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 configuration that needs to be patched
config.validation = copy(config.validation)
config.validation.links = copy(config.validation.links)
config.mdx_configs = copy(config.mdx_configs)
config.mdx_configs["toc"] = copy(config.mdx_configs["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["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")

View File

@ -0,0 +1,37 @@
# 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))

View File

@ -0,0 +1,58 @@
# 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

View File

@ -0,0 +1,85 @@
# 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 collections import UserDict
from datetime import date, datetime, time
from mkdocs.config.base import BaseConfigOption, Config, ValidationError
# -----------------------------------------------------------------------------
# Classes
# -----------------------------------------------------------------------------
# Date dictionary
class DateDict(UserDict[str, datetime]):
# Initialize date dictionary
def __init__(self, data: dict):
super().__init__(data)
# Initialize date of creation
if "created" in data:
self.created: datetime = data["created"]
def __getattr__(self, name: str):
if name in self.data:
return self.data[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] }
# Initialize date dictionary and convert all date values to datetime
config[key_name] = DateDict(config[key_name])
for key, value in config[key_name].items():
if isinstance(value, date):
config[key_name][key] = datetime.combine(value, time())
# 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

View File

@ -0,0 +1,42 @@
# 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 | None):
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)

View File

@ -0,0 +1,19 @@
# 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.

View File

@ -22,10 +22,10 @@ from mkdocs.config.config_options import Type
from mkdocs.config.base import Config
# -----------------------------------------------------------------------------
# Class
# Classes
# -----------------------------------------------------------------------------
# Info plugin configuration scheme
# Info plugin configuration
class InfoConfig(Config):
enabled = Type(bool, default = True)
enabled_on_serve = Type(bool, default = False)

View File

@ -29,15 +29,15 @@ from colorama import Fore, Style
from importlib.metadata import distributions, version
from io import BytesIO
from markdown.extensions.toc import slugify
from mkdocs import utils
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 material.plugins.info.config import InfoConfig
from .config import InfoConfig
# -----------------------------------------------------------------------------
# Class
# Classes
# -----------------------------------------------------------------------------
# Info plugin
@ -87,8 +87,8 @@ class InfoPlugin(BasePlugin[InfoConfig]):
# 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.
base = utils.get_theme_dir(config.theme.name)
if config.theme.dirs.index(base):
path = get_theme_dir(config.theme.name)
if config.theme.dirs.index(path):
log.error("Please remove 'custom_dir' setting.")
self._help_on_customizations_and_exit()

View File

@ -0,0 +1,19 @@
# 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.

View File

@ -22,9 +22,9 @@ from mkdocs.config.config_options import Type
from mkdocs.config.base import Config
# -----------------------------------------------------------------------------
# Class
# Classes
# -----------------------------------------------------------------------------
# Offline plugin configuration scheme
# Offline plugin configuration
class OfflineConfig(Config):
enabled = Type(bool, default = True)

View File

@ -20,13 +20,13 @@
import os
from mkdocs import utils
from mkdocs.plugins import BasePlugin, event_priority
from mkdocs.utils import write_file
from material.plugins.offline.config import OfflineConfig
from .config import OfflineConfig
# -----------------------------------------------------------------------------
# Class
# Classes
# -----------------------------------------------------------------------------
# Offline plugin
@ -55,14 +55,13 @@ class OfflinePlugin(BasePlugin[OfflineConfig]):
return
# Check for existence of search index
base = os.path.join(config.site_dir, "search")
path = os.path.join(base, "search_index.json")
path = os.path.join(config.site_dir, "search", "search_index.json")
if not os.path.isfile(path):
return
# Create script with inlined search index
with open(path, "r") as f:
utils.write_file(
with open(path, encoding = "utf-8") as f:
write_file(
f"var __index = {f.read()}".encode("utf-8"),
path.replace(".json", ".js"),
)

View File

@ -0,0 +1,19 @@
# 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.

View File

@ -29,13 +29,17 @@ from mkdocs.config.base import Config
from mkdocs.contrib.search import LangOption
# -----------------------------------------------------------------------------
# Class
# Options
# -----------------------------------------------------------------------------
# Search pipeline functions
# Options for search pipeline
pipeline = ("stemmer", "stopWordFilter", "trimmer")
# Search plugin configuration scheme
# -----------------------------------------------------------------------------
# Classes
# -----------------------------------------------------------------------------
# Search plugin configuration
class SearchConfig(Config):
lang = Optional(LangOption())
separator = Optional(Type(str))

View File

@ -28,7 +28,7 @@ from html.parser import HTMLParser
from mkdocs import utils
from mkdocs.plugins import BasePlugin
from material.plugins.search.config import SearchConfig
from .config import SearchConfig
try:
import jieba
@ -36,7 +36,7 @@ except ImportError:
jieba = None
# -----------------------------------------------------------------------------
# Class
# Classes
# -----------------------------------------------------------------------------
# Search plugin
@ -46,13 +46,13 @@ class SearchPlugin(BasePlugin[SearchConfig]):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Initialize variables for incremental builds
# Initialize incremental builds
self.is_dirtyreload = False
# Initialize search index cache
self.search_index_prev = None
# Determine whether we're serving
# Determine whether we're serving the site
def on_startup(self, *, command, dirty):
self.is_dirty = dirty
@ -81,7 +81,7 @@ class SearchPlugin(BasePlugin[SearchConfig]):
# Set jieba dictionary, if given
if self.config.jieba_dict:
path = os.path.normpath(self.config.jieba_dict)
if os.path.exists(path):
if os.path.isfile(path):
jieba.set_dictionary(path)
log.debug(f"Loading jieba dictionary: {path}")
else:
@ -93,7 +93,7 @@ class SearchPlugin(BasePlugin[SearchConfig]):
# Set jieba user dictionary, if given
if self.config.jieba_dict_user:
path = os.path.normpath(self.config.jieba_dict_user)
if os.path.exists(path):
if os.path.isfile(path):
jieba.load_userdict(path)
log.debug(f"Loading jieba user dictionary: {path}")
else:

View File

@ -21,6 +21,11 @@
import logging
import sys
# -----------------------------------------------------------------------------
# Checks
# -----------------------------------------------------------------------------
# Check for pillow and cairosvg
try:
import cairosvg as _
import PIL as _

View File

@ -22,10 +22,10 @@ from mkdocs.config.base import Config
from mkdocs.config.config_options import Deprecated, Type
# -----------------------------------------------------------------------------
# Class
# Classes
# -----------------------------------------------------------------------------
# Social plugin configuration scheme
# Social plugin configuration
class SocialConfig(Config):
enabled = Type(bool, default = True)
cache_dir = Type(str, default = ".cache/plugin/social")

View File

@ -25,6 +25,7 @@ import os
import posixpath
import re
import requests
import sys
from cairosvg import svg2png
from collections import defaultdict
@ -37,10 +38,10 @@ from shutil import copyfile
from tempfile import TemporaryFile
from zipfile import ZipFile
from material.plugins.social.config import SocialConfig
from .config import SocialConfig
# -----------------------------------------------------------------------------
# Class
# Classes
# -----------------------------------------------------------------------------
# Social plugin

View File

@ -23,13 +23,13 @@ from markdown.extensions.toc import slugify
from mkdocs.config.config_options import Optional, Type
from mkdocs.config.base import Config
from material.plugins.tags import casefold
from . import casefold
# -----------------------------------------------------------------------------
# Class
# Classes
# -----------------------------------------------------------------------------
# Tags plugin configuration scheme
# Tags plugin configuration
class TagsConfig(Config):
enabled = Type(bool, default = True)

View File

@ -28,11 +28,11 @@ 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 material.plugins.tags import casefold
from material.plugins.tags.config import TagsConfig
from . import casefold
from .config import TagsConfig
# -----------------------------------------------------------------------------
# Class
# Classes
# -----------------------------------------------------------------------------
# Tags plugin

View File

@ -31,6 +31,6 @@ babel>=2.10.3
colorama>=0.4
lxml>=4.6
paginate>=0.5.6
regex>=2022.4.24
readtime>=2.0
regex>=2022.4.24
requests>=2.26

View File

@ -0,0 +1,19 @@
# 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.

View File

@ -116,8 +116,8 @@
<!-- JavaScript libraries -->
{% block libs %}
{% for path in config.extra.polyfills %}
<script src="{{ path | url }}"></script>
{% for script in config.extra.polyfills %}
{{ script | script_tag }}
{% endfor %}
{% endblock %}
@ -126,8 +126,8 @@
<!-- Load fonts from Google -->
{% if config.theme.font != false %}
{% set text = config.theme.font.text | d("Roboto", true) %}
{% set code = config.theme.font.code | d("Roboto Mono", true) %}
{% set text = config.theme.font.get("text", "Roboto") %}
{% set code = config.theme.font.get("code", "Roboto Mono") %}
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
@ -442,13 +442,7 @@
<!-- Custom JavaScript -->
{% for script in config.extra_javascript %}
<!-- Detected MkDocs 1.5+ which has `script.path` and `script_tag` -->
{% if script.path %}
{{ script | script_tag }}
{% else %}
<script src="{{ script | url }}"></script>
{% endif %}
{{ script | script_tag }}
{% endfor %}
{% endblock %}
</body>

View File

@ -1,41 +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.
-->
{% extends "main.html" %}
<!-- Page content -->
{% block container %}
<div class="md-content" data-md-component="content">
<div class="md-content__inner">
<!-- Header -->
<header class="md-typeset">
{{ page.content }}
</header>
<!-- Posts -->
{% for post in posts %}
{% include "partials/post.html" %}
{% endfor %}
</div>
</div>
{% endblock %}

View File

@ -1,41 +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.
-->
{% extends "main.html" %}
<!-- Page content -->
{% block container %}
<div class="md-content" data-md-component="content">
<div class="md-content__inner">
<!-- Header -->
<header class="md-typeset">
{{ page.content }}
</header>
<!-- Posts -->
{% for post in posts %}
{% include "partials/post.html" %}
{% endfor %}
</div>
</div>
{% endblock %}

View File

@ -81,19 +81,19 @@
<li class="md-nav__item">
<div class="md-nav__link">
{% include ".icons/material/calendar.svg" %}
<time datetime="{{ page.meta.date }}" class="md-ellipsis">
{{- page.meta.date_format -}}
<time datetime="{{ page.config.date.created }}" class="md-ellipsis">
{{- page.config.date.created | date -}}
</time>
</div>
</li>
<!-- Page date updated -->
{% if page.meta.date_updated %}
{% if page.config.date.updated %}
<li class="md-nav__item">
<div class="md-nav__link">
{% include ".icons/material/calendar-clock.svg" %}
<time datetime="{{ page.meta.date_updated }}" class="md-ellipsis">
{{- page.meta.date_updated_format -}}
<time datetime="{{ page.config.date.updated }}" class="md-ellipsis">
{{- page.config.date.updated | date -}}
</time>
</div>
</li>
@ -118,8 +118,8 @@
{% endif %}
<!-- Page readtime -->
{% if page.meta.readtime %}
{% set time = page.meta.readtime %}
{% if page.config.readtime %}
{% set time = page.config.readtime %}
<li class="md-nav__item">
<div class="md-nav__link">
{% include ".icons/material/clock-outline.svg" %}

View File

@ -38,9 +38,11 @@
{% endfor %}
<!-- Pagination -->
{% block pagination %}
{% include "partials/pagination.html" %}
{% endblock %}
{% if pagination %}
{% block pagination %}
{% include "partials/pagination.html" %}
{% endblock %}
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -41,8 +41,8 @@
<!-- Post date -->
<li class="md-meta__item">
<time datetime="{{ post.meta.date }}">
{{- post.meta.date_format -}}
<time datetime="{{ post.config.date.created }}">
{{- post.config.date.created | date -}}
</time>
{#- Collapse whitespace -#}
</li>
@ -64,8 +64,8 @@
{% endif %}
<!-- Post readtime -->
{% if post.meta.readtime %}
{% set time = post.meta.readtime %}
{% if post.config.readtime %}
{% set time = post.config.readtime %}
<li class="md-meta__item">
{% if time == 1 %}
{{ lang.t("readtime.one") }}
@ -77,7 +77,7 @@
</ul>
<!-- Draft marker -->
{% if post.meta.draft %}
{% if post.config.draft %}
<span class="md-draft">
{{ lang.t("blog.draft") }}
</span>

View File

@ -0,0 +1,19 @@
# 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.

View File

@ -0,0 +1,19 @@
# 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.

View File

@ -0,0 +1,38 @@
# 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 = {})

View File

@ -24,10 +24,10 @@ from mkdocs.config.config_options import Choice, Deprecated, Optional, Type
from mkdocs.config.base import Config
# -----------------------------------------------------------------------------
# Class
# Classes
# -----------------------------------------------------------------------------
# Blog plugin configuration scheme
# Blog plugin configuration
class BlogConfig(Config):
enabled = Type(bool, default = True)
@ -36,6 +36,7 @@ class BlogConfig(Config):
blog_toc = Type(bool, default = False)
# Options 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}")
@ -70,7 +71,9 @@ class BlogConfig(Config):
pagination = Type(bool, default = True)
pagination_per_page = Type(int, default = 10)
pagination_url_format = Type(str, default = "page/{page}")
pagination_template = Type(str, default = "~2~")
pagination_format = Type(str, default = "~2~")
pagination_if_single_page = Type(bool, default = False)
pagination_keep_content = Type(bool, default = False)
# Options for authors
authors = Type(bool, default = True)
@ -80,3 +83,6 @@ class BlogConfig(Config):
draft = Type(bool, default = False)
draft_on_serve = Type(bool, default = True)
draft_if_future_date = Type(bool, default = False)
# Deprecated options
pagination_template = Deprecated(moved_to = "pagination_format")

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,277 @@
# 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 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 typing import Union
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 for error reporting
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 views like archive and
# category views. They are not rendered as standalone pages, but are
# included in the context of the parent post. 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
# Initialize configuration, contents and metadata
self.config = post.config
self.markdown = post.markdown
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
# 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: Union[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 configuration that needs to be patched
config.validation = copy(config.validation)
config.validation.links = copy(config.validation.links)
config.mdx_configs = copy(config.mdx_configs)
config.mdx_configs["toc"] = copy(config.mdx_configs["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["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")

View File

@ -0,0 +1,37 @@
# 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))

View File

@ -0,0 +1,58 @@
# 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

View File

@ -0,0 +1,85 @@
# 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 collections import UserDict
from datetime import date, datetime, time
from mkdocs.config.base import BaseConfigOption, Config, ValidationError
# -----------------------------------------------------------------------------
# Classes
# -----------------------------------------------------------------------------
# Date dictionary
class DateDict(UserDict[str, datetime]):
# Initialize date dictionary
def __init__(self, data: dict):
super().__init__(data)
# Initialize date of creation
if "created" in data:
self.created: datetime = data["created"]
def __getattr__(self, name: str):
if name in self.data:
return self.data[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] }
# Initialize date dictionary and convert all date values to datetime
config[key_name] = DateDict(config[key_name])
for key, value in config[key_name].items():
if isinstance(value, date):
config[key_name][key] = datetime.combine(value, time())
# 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

View File

@ -0,0 +1,42 @@
# 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 | None):
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)

View File

@ -0,0 +1,19 @@
# 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.

View File

@ -22,10 +22,10 @@ from mkdocs.config.config_options import Type
from mkdocs.config.base import Config
# -----------------------------------------------------------------------------
# Class
# Classes
# -----------------------------------------------------------------------------
# Info plugin configuration scheme
# Info plugin configuration
class InfoConfig(Config):
enabled = Type(bool, default = True)
enabled_on_serve = Type(bool, default = False)

View File

@ -29,15 +29,15 @@ from colorama import Fore, Style
from importlib.metadata import distributions, version
from io import BytesIO
from markdown.extensions.toc import slugify
from mkdocs import utils
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 material.plugins.info.config import InfoConfig
from .config import InfoConfig
# -----------------------------------------------------------------------------
# Class
# Classes
# -----------------------------------------------------------------------------
# Info plugin
@ -87,8 +87,8 @@ class InfoPlugin(BasePlugin[InfoConfig]):
# 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.
base = utils.get_theme_dir(config.theme.name)
if config.theme.dirs.index(base):
path = get_theme_dir(config.theme.name)
if config.theme.dirs.index(path):
log.error("Please remove 'custom_dir' setting.")
self._help_on_customizations_and_exit()

View File

@ -0,0 +1,19 @@
# 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.

View File

@ -22,9 +22,9 @@ from mkdocs.config.config_options import Type
from mkdocs.config.base import Config
# -----------------------------------------------------------------------------
# Class
# Classes
# -----------------------------------------------------------------------------
# Offline plugin configuration scheme
# Offline plugin configuration
class OfflineConfig(Config):
enabled = Type(bool, default = True)

View File

@ -20,13 +20,13 @@
import os
from mkdocs import utils
from mkdocs.plugins import BasePlugin, event_priority
from mkdocs.utils import write_file
from material.plugins.offline.config import OfflineConfig
from .config import OfflineConfig
# -----------------------------------------------------------------------------
# Class
# Classes
# -----------------------------------------------------------------------------
# Offline plugin
@ -55,14 +55,13 @@ class OfflinePlugin(BasePlugin[OfflineConfig]):
return
# Check for existence of search index
base = os.path.join(config.site_dir, "search")
path = os.path.join(base, "search_index.json")
path = os.path.join(config.site_dir, "search", "search_index.json")
if not os.path.isfile(path):
return
# Create script with inlined search index
with open(path, "r") as f:
utils.write_file(
with open(path, encoding = "utf-8") as f:
write_file(
f"var __index = {f.read()}".encode("utf-8"),
path.replace(".json", ".js"),
)

View File

@ -0,0 +1,19 @@
# 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.

View File

@ -29,13 +29,17 @@ from mkdocs.config.base import Config
from mkdocs.contrib.search import LangOption
# -----------------------------------------------------------------------------
# Class
# Options
# -----------------------------------------------------------------------------
# Search pipeline functions
# Options for search pipeline
pipeline = ("stemmer", "stopWordFilter", "trimmer")
# Search plugin configuration scheme
# -----------------------------------------------------------------------------
# Classes
# -----------------------------------------------------------------------------
# Search plugin configuration
class SearchConfig(Config):
lang = Optional(LangOption())
separator = Optional(Type(str))

View File

@ -28,7 +28,7 @@ from html.parser import HTMLParser
from mkdocs import utils
from mkdocs.plugins import BasePlugin
from material.plugins.search.config import SearchConfig
from .config import SearchConfig
try:
import jieba
@ -36,7 +36,7 @@ except ImportError:
jieba = None
# -----------------------------------------------------------------------------
# Class
# Classes
# -----------------------------------------------------------------------------
# Search plugin
@ -46,13 +46,13 @@ class SearchPlugin(BasePlugin[SearchConfig]):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Initialize variables for incremental builds
# Initialize incremental builds
self.is_dirtyreload = False
# Initialize search index cache
self.search_index_prev = None
# Determine whether we're serving
# Determine whether we're serving the site
def on_startup(self, *, command, dirty):
self.is_dirty = dirty
@ -81,7 +81,7 @@ class SearchPlugin(BasePlugin[SearchConfig]):
# Set jieba dictionary, if given
if self.config.jieba_dict:
path = os.path.normpath(self.config.jieba_dict)
if os.path.exists(path):
if os.path.isfile(path):
jieba.set_dictionary(path)
log.debug(f"Loading jieba dictionary: {path}")
else:
@ -93,7 +93,7 @@ class SearchPlugin(BasePlugin[SearchConfig]):
# Set jieba user dictionary, if given
if self.config.jieba_dict_user:
path = os.path.normpath(self.config.jieba_dict_user)
if os.path.exists(path):
if os.path.isfile(path):
jieba.load_userdict(path)
log.debug(f"Loading jieba user dictionary: {path}")
else:

View File

@ -21,6 +21,11 @@
import logging
import sys
# -----------------------------------------------------------------------------
# Checks
# -----------------------------------------------------------------------------
# Check for pillow and cairosvg
try:
import cairosvg as _
import PIL as _

View File

@ -22,10 +22,10 @@ from mkdocs.config.base import Config
from mkdocs.config.config_options import Deprecated, Type
# -----------------------------------------------------------------------------
# Class
# Classes
# -----------------------------------------------------------------------------
# Social plugin configuration scheme
# Social plugin configuration
class SocialConfig(Config):
enabled = Type(bool, default = True)
cache_dir = Type(str, default = ".cache/plugin/social")

View File

@ -25,6 +25,7 @@ import os
import posixpath
import re
import requests
import sys
from cairosvg import svg2png
from collections import defaultdict
@ -37,10 +38,10 @@ from shutil import copyfile
from tempfile import TemporaryFile
from zipfile import ZipFile
from material.plugins.social.config import SocialConfig
from .config import SocialConfig
# -----------------------------------------------------------------------------
# Class
# Classes
# -----------------------------------------------------------------------------
# Social plugin

View File

@ -23,13 +23,13 @@ from markdown.extensions.toc import slugify
from mkdocs.config.config_options import Optional, Type
from mkdocs.config.base import Config
from material.plugins.tags import casefold
from . import casefold
# -----------------------------------------------------------------------------
# Class
# Classes
# -----------------------------------------------------------------------------
# Tags plugin configuration scheme
# Tags plugin configuration
class TagsConfig(Config):
enabled = Type(bool, default = True)

View File

@ -28,11 +28,11 @@ 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 material.plugins.tags import casefold
from material.plugins.tags.config import TagsConfig
from . import casefold
from .config import TagsConfig
# -----------------------------------------------------------------------------
# Class
# Classes
# -----------------------------------------------------------------------------
# Tags plugin