This commit is contained in:
CreaRobotics committed 2026-03-02 11:34:40 +01:00
1 parent 6ccab0347b
commit 954a2fd6d6
18684 files changed
+601012 -190

No files matched your search

@@ -0,0 +1,19 @@
# Copyright (c) 2016-2025 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.
@@ -0,0 +1,97 @@
# Copyright (c) 2016-2025 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 functools
import material
import os
from glob import iglob
from inspect import getfile
from markdown import Markdown
from pymdownx import emoji, twemoji_db
from xml.etree.ElementTree import Element
# -----------------------------------------------------------------------------
# Functions
# -----------------------------------------------------------------------------
# Create twemoji index
def twemoji(options: object, md: Markdown):
paths = options.get("custom_icons", [])[:]
return _load_twemoji_index(tuple(paths))
# Create emoji or icon
def to_svg(
index: str, shortname: str, alias: str, uc: str | None, alt: str,
title: str, category: str, options: object, md: Markdown
):
if not uc:
icons = md.inlinePatterns["emoji"].emoji_index["emoji"]
# Create and return element to host icon
el = Element("span", { "class": options.get("classes", index) })
el.text = md.htmlStash.store(_load(icons[shortname]["path"]))
return el
# Delegate to `pymdownx.emoji` extension
return emoji.to_svg(
index, shortname, alias, uc, alt, title, category, options, md
)
# -----------------------------------------------------------------------------
# Helper functions
# -----------------------------------------------------------------------------
# Load icon
@functools.lru_cache(maxsize = None)
def _load(file: str):
with open(file, encoding = "utf-8") as f:
return f.read()
# Load twemoji index and add icons
@functools.lru_cache(maxsize = None)
def _load_twemoji_index(paths):
index = {
"name": "twemoji",
"emoji": twemoji_db.emoji,
"aliases": twemoji_db.aliases
}
# Compute path to theme root and traverse all icon directories
root = os.path.dirname(getfile(material))
root = os.path.join(root, "templates", ".icons")
for path in [*paths, root]:
base = os.path.normpath(path)
# Index icons provided by the theme and via custom icons
glob = os.path.join(base, "**", "*.svg")
glob = iglob(os.path.normpath(glob), recursive = True)
for file in glob:
icon = file[len(base) + 1:-4].replace(os.path.sep, "-")
# Add icon to index
name = f":{icon}:"
if not any(name in index[key] for key in ["emoji", "aliases"]):
index["emoji"][name] = { "name": name, "path": file }
# Return index
return index
@@ -0,0 +1,223 @@
# Copyright (c) 2016-2025 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
from material.utilities.filter import FileFilter, FilterConfig
from mkdocs.structure.pages import _RelativePathTreeprocessor
from markdown import Extension, Markdown
from markdown.treeprocessors import Treeprocessor
from mkdocs.exceptions import ConfigurationError
from urllib.parse import urlparse
from xml.etree.ElementTree import Element
# -----------------------------------------------------------------------------
# Classes
# -----------------------------------------------------------------------------
class PreviewProcessor(Treeprocessor):
"""
A Markdown treeprocessor to enable instant previews on links.
Note that this treeprocessor is dependent on the `relpath` treeprocessor
registered programmatically by MkDocs before rendering a page.
"""
def __init__(self, md: Markdown, config: dict):
"""
Initialize the treeprocessor.
Arguments:
md: The Markdown instance.
config: The configuration.
"""
super().__init__(md)
self.config = config
def run(self, root: Element):
"""
Run the treeprocessor.
Arguments:
root: The root element of the parsed Markdown document.
"""
at = self.md.treeprocessors.get_index_for_name("relpath")
# Hack: Python Markdown has no notion of where it is, i.e., which file
# is being processed. This seems to be a deliberate design decision, as
# it is not possible to access the file path of the current page, but
# it might also be an oversight that is now impossible to fix. However,
# since this extension is only useful in the context of Material for
# MkDocs, we can assume that the _RelativePathTreeprocessor is always
# present, telling us the file path of the current page. If that ever
# changes, we would need to wrap this extension in a plugin, but for
# the time being we are sneaky and will probably get away with it.
processor = self.md.treeprocessors[at]
if not isinstance(processor, _RelativePathTreeprocessor):
raise TypeError("Relative path processor not registered")
# Normalize configurations
configurations = self.config["configurations"]
configurations.append({
"sources": self.config.get("sources"),
"targets": self.config.get("targets")
})
# Walk through all configurations - @todo refactor so that we don't
# iterate multiple times over the same elements
for configuration in configurations:
# Skip, if the configuration defines nothing – we could also fix
# this in the file filter, but we first fix it here and check if
# it generalizes well enough to other inclusion/exclusion sites,
# because here, it would hinder the ability to automaticaly
# include all sources, while excluding specific targets.
if (
not configuration.get("sources") and
not configuration.get("targets")
):
continue
# Skip if page should not be considered
filter = get_filter(configuration, "sources")
if not filter(processor.file):
continue
# Walk through all links and add preview attributes
filter = get_filter(configuration, "targets")
for el in root.iter("a"):
href = el.get("href")
if not href:
continue
# Skip footnotes
if "footnote-ref" in el.get("class", ""):
continue
# Skip external links
url = urlparse(href)
if url.scheme or url.netloc:
continue
# Add preview attribute to internal links
for path in processor._possible_target_uris(
processor.file, url.path,
processor.config.use_directory_urls
):
target = processor.files.get_file_from_path(path)
if not target:
continue
# Include, if filter matches
if filter(target):
el.set("data-preview", "")
# -----------------------------------------------------------------------------
class PreviewExtension(Extension):
"""
A Markdown extension to enable instant previews on links.
This extensions allows to automatically add the `data-preview` attribute to
internal links matching specific criteria, so Material for MkDocs renders a
nice preview on hover as part of a tooltip. It is the recommended way to
add previews to links in a programmatic way.
"""
def __init__(self, *args, **kwargs):
"""
"""
self.config = {
"configurations": [[], "Filter configurations"],
"sources": [{}, "Link sources"],
"targets": [{}, "Link targets"]
}
super().__init__(*args, **kwargs)
def extendMarkdown(self, md: Markdown):
"""
Register Markdown extension.
Arguments:
md: The Markdown instance.
"""
md.registerExtension(self)
# Create and register treeprocessor - we use the same priority as the
# `relpath` treeprocessor, the latter of which is guaranteed to run
# after our treeprocessor, so we can check the original Markdown URIs
# before they are resolved to URLs.
processor = PreviewProcessor(md, self.getConfigs())
md.treeprocessors.register(processor, "preview", 0)
# -----------------------------------------------------------------------------
# Functions
# -----------------------------------------------------------------------------
def get_filter(settings: dict, key: str):
"""
Get file filter from settings.
Arguments:
settings: The settings.
key: The key in the settings.
Returns:
The file filter.
"""
config = FilterConfig()
config.load_dict(settings.get(key) or {})
# Validate filter configuration
errors, warnings = config.validate()
for _, w in warnings:
log.warning(
f"Error reading filter configuration in '{key}':\n"
f"{w}"
)
for _, e in errors:
raise ConfigurationError(
f"Error reading filter configuration in '{key}':\n"
f"{e}"
)
# Return file filter
return FileFilter(config = config) # type: ignore
def makeExtension(**kwargs):
"""
Register Markdown extension.
Arguments:
**kwargs: Configuration options.
Returns:
The Markdown extension.
"""
return PreviewExtension(**kwargs)
# -----------------------------------------------------------------------------
# Data
# -----------------------------------------------------------------------------
# Set up logging
log = logging.getLogger("mkdocs.material.extensions.preview")