From be8de118db913711eb72ae5187d26e54a0055727 Mon Sep 17 00:00:00 2001 From: 简律纯 Date: Fri, 15 Dec 2023 09:11:47 +0800 Subject: refactor(docs): optmst `docs` dir & `deps` --- src/plugins/social/__init__.py | 19 -- src/plugins/social/config.py | 48 ---- src/plugins/social/plugin.py | 516 ----------------------------------------- 3 files changed, 583 deletions(-) delete mode 100644 src/plugins/social/__init__.py delete mode 100644 src/plugins/social/config.py delete mode 100644 src/plugins/social/plugin.py (limited to 'src/plugins/social') diff --git a/src/plugins/social/__init__.py b/src/plugins/social/__init__.py deleted file mode 100644 index d1899378..00000000 --- a/src/plugins/social/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. diff --git a/src/plugins/social/config.py b/src/plugins/social/config.py deleted file mode 100644 index 2d87c25e..00000000 --- a/src/plugins/social/config.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -from mkdocs.config.base import Config -from mkdocs.config.config_options import Deprecated, Type - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Social plugin configuration -class SocialConfig(Config): - enabled = Type(bool, default = True) - cache_dir = Type(str, default = ".cache/plugin/social") - - # Settings for social cards - cards = Type(bool, default = True) - cards_dir = Type(str, default = "assets/images/social") - cards_layout_options = Type(dict, default = {}) - - # Deprecated settings - cards_color = Deprecated( - option_type = Type(dict, default = {}), - message = - "Deprecated, use 'cards_layout_options.background_color' " - "and 'cards_layout_options.color' with 'default' layout" - ) - cards_font = Deprecated( - option_type = Type(str), - message = "Deprecated, use 'cards_layout_options.font_family'" - ) diff --git a/src/plugins/social/plugin.py b/src/plugins/social/plugin.py deleted file mode 100644 index 3cdfa3ce..00000000 --- a/src/plugins/social/plugin.py +++ /dev/null @@ -1,516 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -# ----------------------------------------------------------------------------- -# Disclaimer -# ----------------------------------------------------------------------------- -# Please note: this version of the social plugin is not actively development -# anymore. Instead, Material for MkDocs Insiders ships a complete rewrite of -# the plugin which is much more powerful and addresses all shortcomings of -# this implementation. Additionally, the new social plugin allows to create -# entirely custom social cards. You can probably imagine, that this was a lot -# of work to pull off. If you run into problems, or want to have additional -# functionality, please consider sponsoring the project. You can then use the -# new version of the plugin immediately. -# ----------------------------------------------------------------------------- - -import concurrent.futures -import functools -import logging -import os -import posixpath -import re -import requests -import sys - -from collections import defaultdict -from hashlib import md5 -from io import BytesIO -from mkdocs.commands.build import DuplicateFilter -from mkdocs.exceptions import PluginError -from mkdocs.plugins import BasePlugin -from shutil import copyfile -from tempfile import TemporaryFile -from zipfile import ZipFile -try: - from cairosvg import svg2png - from PIL import Image, ImageDraw, ImageFont -except ImportError: - pass - -from .config import SocialConfig - - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Social plugin -class SocialPlugin(BasePlugin[SocialConfig]): - - def __init__(self): - self._executor = concurrent.futures.ThreadPoolExecutor(4) - - # Retrieve configuration - def on_config(self, config): - self.color = colors.get("indigo") - self.config.cards = self.config.enabled - if not self.config.cards: - return - - # Check dependencies - if "Image" not in globals(): - raise PluginError( - "Required dependencies of \"social\" plugin not found. " - "Install with: pip install \"mkdocs-material[imaging]\"" - ) - - # Move color options - if self.config.cards_color: - - # Move background color to new option - value = self.config.cards_color.get("fill") - if value: - self.config.cards_layout_options["background_color"] = value - - # Move color to new option - value = self.config.cards_color.get("text") - if value: - self.config.cards_layout_options["color"] = value - - # Move font family to new option - if self.config.cards_font: - value = self.config.cards_font - self.config.cards_layout_options["font_family"] = value - - # Check if site URL is defined - if not config.site_url: - log.warning( - "The \"site_url\" option is not set. The cards are generated, " - "but not linked, so they won't be visible on social media." - ) - - # Ensure presence of cache directory - self.cache = self.config.cache_dir - if not os.path.isdir(self.cache): - os.makedirs(self.cache) - - # Retrieve palette from theme configuration - theme = config.theme - if "palette" in theme: - palette = theme["palette"] - - # Use first palette, if multiple are defined - if isinstance(palette, list): - palette = palette[0] - - # Set colors according to palette - if "primary" in palette and palette["primary"]: - primary = palette["primary"].replace(" ", "-") - self.color = colors.get(primary, self.color) - - # Retrieve color overrides - options = self.config.cards_layout_options - self.color = { - "fill": options.get("background_color", self.color["fill"]), - "text": options.get("color", self.color["text"]) - } - - # Retrieve logo and font - self._resized_logo_promise = self._executor.submit(self._load_resized_logo, config) - self.font = self._load_font(config) - - self._image_promises = [] - - # Create social cards - def on_page_markdown(self, markdown, page, config, files): - if not self.config.cards: - return - - # Resolve image directory - directory = self.config.cards_dir - file, _ = os.path.splitext(page.file.src_path) - - # Resolve path of image - path = "{}.png".format(os.path.join( - config.site_dir, - directory, - file - )) - - # Resolve path of image directory - directory = os.path.dirname(path) - if not os.path.isdir(directory): - os.makedirs(directory) - - # Compute site name - site_name = config.site_name - - # Compute page title and description - title = page.meta.get("title", page.title) - description = config.site_description or "" - if "description" in page.meta: - description = page.meta["description"] - - # Check type of meta title - see https://t.ly/m1Us - if not isinstance(title, str): - log.error( - f"Page meta title of page '{page.file.src_uri}' must be a " - f"string, but is of type \"{type(title)}\"." - ) - sys.exit(1) - - # Check type of meta description - see https://t.ly/m1Us - if not isinstance(description, str): - log.error( - f"Page meta description of '{page.file.src_uri}' must be a " - f"string, but is of type \"{type(description)}\"." - ) - sys.exit(1) - - # Generate social card if not in cache - hash = md5("".join([ - site_name, - str(title), - description - ]).encode("utf-8")) - file = os.path.join(self.cache, f"{hash.hexdigest()}.png") - self._image_promises.append(self._executor.submit( - self._cache_image, - cache_path = file, dest_path = path, - render_function = lambda: self._render_card(site_name, title, description) - )) - - # Inject meta tags into page - meta = page.meta.get("meta", []) - page.meta["meta"] = meta + self._generate_meta(page, config) - - def on_post_build(self, config): - if not self.config.cards: - return - - # Check for exceptions - for promise in self._image_promises: - promise.result() - - # ------------------------------------------------------------------------- - - # Render image to cache (if not present), then copy from cache to site - def _cache_image(self, cache_path, dest_path, render_function): - if not os.path.isfile(cache_path): - image = render_function() - image.save(cache_path) - - # Copy file from cache - copyfile(cache_path, dest_path) - - @functools.lru_cache(maxsize=None) - def _get_font(self, kind, size): - return ImageFont.truetype(self.font[kind], size) - - # Render social card - def _render_card(self, site_name, title, description): - # Render background and logo - image = self._render_card_background((1200, 630), self.color["fill"]) - image.alpha_composite( - self._resized_logo_promise.result(), - (1200 - 228, 64 - 4) - ) - - # Render site name - font = self._get_font("Bold", 36) - image.alpha_composite( - self._render_text((826, 48), font, site_name, 1, 20), - (64 + 4, 64) - ) - - # Render page title - font = self._get_font("Bold", 92) - image.alpha_composite( - self._render_text((826, 328), font, title, 3, 30), - (64, 160) - ) - - # Render page description - font = self._get_font("Regular", 28) - image.alpha_composite( - self._render_text((826, 80), font, description, 2, 14), - (64 + 4, 512) - ) - - # Return social card image - return image - - # Render social card background - def _render_card_background(self, size, fill): - return Image.new(mode = "RGBA", size = size, color = fill) - - @functools.lru_cache(maxsize=None) - def _tmp_context(self): - image = Image.new(mode = "RGBA", size = (50, 50)) - return ImageDraw.Draw(image) - - @functools.lru_cache(maxsize=None) - def _text_bounding_box(self, text, font): - return self._tmp_context().textbbox((0, 0), text, font = font) - - # Render social card text - def _render_text(self, size, font, text, lmax, spacing = 0): - width = size[0] - lines, words = [], [] - - # Remove remnant HTML tags - text = re.sub(r"(<[^>]+>)", "", text) - - # Retrieve y-offset of textbox to correct for spacing - yoffset = 0 - - # Create drawing context and split text into lines - for word in text.split(" "): - combine = " ".join(words + [word]) - textbox = self._text_bounding_box(combine, font = font) - yoffset = textbox[1] - if not words or textbox[2] <= width: - words.append(word) - else: - lines.append(words) - words = [word] - - # Join words for each line and create image - lines.append(words) - lines = [" ".join(line) for line in lines] - image = Image.new(mode = "RGBA", size = size) - - # Create drawing context and split text into lines - context = ImageDraw.Draw(image) - context.text( - (0, spacing / 2 - yoffset), "\n".join(lines[:lmax]), - font = font, fill = self.color["text"], spacing = spacing - yoffset - ) - - # Return text image - return image - - # ------------------------------------------------------------------------- - - # Generate meta tags - def _generate_meta(self, page, config): - directory = self.config.cards_dir - file, _ = os.path.splitext(page.file.src_uri) - - # Compute page title - title = page.meta.get("title", page.title) - if not page.is_homepage: - title = f"{title} - {config.site_name}" - - # Compute page description - description = config.site_description - if "description" in page.meta: - description = page.meta["description"] - - # Resolve image URL - url = "{}.png".format(posixpath.join( - config.site_url or ".", - directory, - file - )) - - # Ensure forward slashes - url = url.replace(os.path.sep, "/") - - # Return meta tags - return [ - - # Meta tags for Open Graph - { "property": "og:type", "content": "website" }, - { "property": "og:title", "content": title }, - { "property": "og:description", "content": description }, - { "property": "og:image", "content": url }, - { "property": "og:image:type", "content": "image/png" }, - { "property": "og:image:width", "content": "1200" }, - { "property": "og:image:height", "content": "630" }, - { "property": "og:url", "content": page.canonical_url }, - - # Meta tags for Twitter - { "name": "twitter:card", "content": "summary_large_image" }, - # { "name": "twitter:site", "content": user }, - # { "name": "twitter:creator", "content": user }, - { "name": "twitter:title", "content": title }, - { "name": "twitter:description", "content": description }, - { "name": "twitter:image", "content": url } - ] - - def _load_resized_logo(self, config, width = 144): - logo = self._load_logo(config) - height = int(width * logo.height / logo.width) - return logo.resize((width, height)) - - # Retrieve logo image or icon - def _load_logo(self, config): - theme = config.theme - - # Handle images (precedence over icons) - if "logo" in theme: - _, extension = os.path.splitext(theme["logo"]) - - path = os.path.join(config.docs_dir, theme["logo"]) - - # Allow users to put the logo inside their custom_dir (theme["logo"] case) - if theme.custom_dir: - custom_dir_logo = os.path.join(theme.custom_dir, theme["logo"]) - if os.path.exists(custom_dir_logo): - path = custom_dir_logo - - # Load SVG and convert to PNG - if extension == ".svg": - return self._load_logo_svg(path) - - # Load PNG, JPEG, etc. - return Image.open(path).convert("RGBA") - - # Handle icons - icon = theme["icon"] or {} - if "logo" in icon and icon["logo"]: - logo = icon["logo"] - else: - logo = "material/library" - - # Resolve path of package - base = os.path.abspath(os.path.join( - os.path.dirname(__file__), - "../.." - )) - - path = f"{base}/templates/.icons/{logo}.svg" - - # Allow users to put the logo inside their custom_dir (theme["icon"]["logo"] case) - if theme.custom_dir: - custom_dir_logo = os.path.join(theme.custom_dir, ".icons", f"{logo}.svg") - if os.path.exists(custom_dir_logo): - path = custom_dir_logo - - # Load icon data and fill with color - return self._load_logo_svg(path, self.color["text"]) - - # Load SVG file and convert to PNG - def _load_logo_svg(self, path, fill = None): - file = BytesIO() - data = open(path).read() - - # Fill with color, if given - if fill: - data = data.replace("