aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/docs/src/plugins/social/plugin.py
diff options
context:
space:
mode:
Diffstat (limited to 'docs/src/plugins/social/plugin.py')
-rw-r--r--docs/src/plugins/social/plugin.py516
1 files changed, 0 insertions, 516 deletions
diff --git a/docs/src/plugins/social/plugin.py b/docs/src/plugins/social/plugin.py
deleted file mode 100644
index 3cdfa3ce..00000000
--- a/docs/src/plugins/social/plugin.py
+++ /dev/null
@@ -1,516 +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.
-
-# -----------------------------------------------------------------------------
-# 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("<svg", f"<svg fill=\"{fill}\"")
-
- # Convert to PNG and return image
- svg2png(bytestring = data, write_to = file, scale = 10)
- return Image.open(file)
-
- # Retrieve font
- def _load_font(self, config):
- name = self.config.cards_layout_options.get("font_family")
- if not name:
-
- # Retrieve from theme (default: Roboto)
- theme = config.theme
- if isinstance(theme["font"], dict) and "text" in theme["font"]:
- name = theme["font"]["text"]
- else:
- name = "Roboto"
-
- # Google fonts can return varients like OpenSans_Condensed-Regular.ttf so
- # we only use the font requested e.g. OpenSans-Regular.ttf
- font_filename_base = name.replace(' ', '')
- filename_regex = re.escape(font_filename_base)+r"-(\w+)\.[ot]tf$"
-
- font = {}
- # Check for cached files - note these may be in subfolders
- for currentpath, folders, files in os.walk(self.cache):
- for file in files:
- # Map available font weights to file paths
- fname = os.path.join(currentpath, file)
- match = re.search(filename_regex, fname)
- if match:
- font[match.group(1)] = fname
-
- # If none found, fetch from Google and try again
- if len(font) == 0:
- self._load_font_from_google(name)
- for currentpath, folders, files in os.walk(self.cache):
- for file in files:
- # Map available font weights to file paths
- fname = os.path.join(currentpath, file)
- match = re.search(filename_regex, fname)
- if match:
- font[match.group(1)] = fname
-
- # Return available font weights with fallback
- return defaultdict(lambda: font["Regular"], font)
-
- # Retrieve font from Google Fonts
- def _load_font_from_google(self, name):
- url = "https://fonts.google.com/download?family={}"
- res = requests.get(url.format(name.replace(" ", "+")), stream = True)
-
- # Write archive to temporary file
- tmp = TemporaryFile()
- for chunk in res.iter_content(chunk_size = 32768):
- tmp.write(chunk)
-
- # Unzip fonts from temporary file
- zip = ZipFile(tmp)
- files = [file for file in zip.namelist() if file.endswith(".ttf") or file.endswith(".otf")]
- zip.extractall(self.cache, files)
-
- # Close and delete temporary file
- tmp.close()
- return files
-
-# -----------------------------------------------------------------------------
-# Data
-# -----------------------------------------------------------------------------
-
-# Set up logging
-log = logging.getLogger("mkdocs")
-log.addFilter(DuplicateFilter())
-
-# Color palette
-colors = dict({
- "red": { "fill": "#ef5552", "text": "#ffffff" },
- "pink": { "fill": "#e92063", "text": "#ffffff" },
- "purple": { "fill": "#ab47bd", "text": "#ffffff" },
- "deep-purple": { "fill": "#7e56c2", "text": "#ffffff" },
- "indigo": { "fill": "#4051b5", "text": "#ffffff" },
- "blue": { "fill": "#2094f3", "text": "#ffffff" },
- "light-blue": { "fill": "#02a6f2", "text": "#ffffff" },
- "cyan": { "fill": "#00bdd6", "text": "#ffffff" },
- "teal": { "fill": "#009485", "text": "#ffffff" },
- "green": { "fill": "#4cae4f", "text": "#ffffff" },
- "light-green": { "fill": "#8bc34b", "text": "#ffffff" },
- "lime": { "fill": "#cbdc38", "text": "#000000" },
- "yellow": { "fill": "#ffec3d", "text": "#000000" },
- "amber": { "fill": "#ffc105", "text": "#000000" },
- "orange": { "fill": "#ffa724", "text": "#000000" },
- "deep-orange": { "fill": "#ff6e42", "text": "#ffffff" },
- "brown": { "fill": "#795649", "text": "#ffffff" },
- "grey": { "fill": "#757575", "text": "#ffffff" },
- "blue-grey": { "fill": "#546d78", "text": "#ffffff" },
- "black": { "fill": "#000000", "text": "#ffffff" },
- "white": { "fill": "#ffffff", "text": "#000000" }
-})