Skip to content

Commit

Permalink
compute size automatically if not set (#26)
Browse files Browse the repository at this point in the history
* get is already defaulting to None

* accept any type of parameters

* compute size automatically

* compute size automatically

* fix mypy issues

* fix the tests

* fix mypy

* remove test notebook

* Fix typos

* gitignore .vscode

* Change size to sizes

* Update existing test for computed "sizes"

---------

Co-authored-by: tcmetzger <[email protected]>
  • Loading branch information
12rambau and tcmetzger authored Jan 28, 2023
1 parent 70c8aa6 commit dec53da
Show file tree
Hide file tree
Showing 12 changed files with 153 additions and 67 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ dmypy.json

# Pyre type checker
.pyre/

# VSCode
.vscode/
17 changes: 9 additions & 8 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,21 @@
favicons = [
# generic icons compatible with most browsers
{
# "rel": "icon", # automatically set to "icon" if ommitted
"size": "32x32",
# "rel": "icon", # automatically set to "icon" if omitted
# "sizes": "32x32", # automatically set if the file can be read
# "type": "image/png", # autogenerated from file type
"href": "favicon-32x32.png",
},
{"size": "16x16", "href": "favicon-16x16.png"},
{"rel": "shortcut icon", "size": "any", "href": "favicon.ico"},
"favicon-16x16.png",
{"rel": "shortcut icon", "sizes": "any", "href": "favicon.ico"},
# chrome specific
{"size": "192x192", "href": "android-chrome-192x192.png"},
"android-chrome-192x192.png",
# apple icons
{"rel": "mask-icon", "color": "#2d89ef", "href": "safari-pinned-tab.svg"},
{"rel": "apple-touch-icon", "size": "180x180", "href": "apple-touch-icon.png"},
{"rel": "apple-touch-icon", "href": "apple-touch-icon.png"},
# msapplications
{"name": "msapplication-TileColor", "content": "#2d89ef"},
{"name": "theme-color", "size": "#ffffff"},
{"href": "mstile-150x150.png"},
{"name": "theme-color", "content": "#ffffff"},
"https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/mstile-150x150.png"
# to show it works as well with absolute urls
]
7 changes: 5 additions & 2 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
def test(session):
"""Apply the tests on the lib."""
session.install(".[test]")
session.run("pytest", "--color=yes", "tests")
test_files = session.posargs or ["tests"]
session.run("pytest", "--color=yes", *test_files)


@nox.session(name="mypy", reuse_venv=True)
Expand All @@ -29,7 +30,9 @@ def mypy(session):
def docs(session):
"""Build the docs."""
session.install(".[doc]")
session.run("sphinx-build", "-b=html", "docs/source", "docs/build/html")
session.run(
"sphinx-build", "-b=html", *session.posargs, "docs/source", "docs/build/html"
)


@nox.session(reuse_venv=True)
Expand Down
127 changes: 105 additions & 22 deletions sphinx_favicon/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,29 @@
The sphinx-favicon extension gives you more flexibility than the standard favicon.ico supported by Sphinx. It provides a quick and easy way to add the most important favicon formats for different browsers and devices.
"""

from io import BytesIO
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Union
from urllib.parse import urlparse

import docutils.nodes as nodes
import imagesize
import requests
from requests.exceptions import RequestException
from sphinx.application import Sphinx
from sphinx.util import logging

logger = logging.getLogger(__name__)

FaviconsDef = Union[Dict[str, str], List[Dict[str, str]]]

OUTPUT_STATIC_DIR = "_static"
FILE_FIELD = "static-file"
"""field in the ``FaviconsDef`` pointing to file in the ``html_static_path``"""
OUTPUT_STATIC_DIR: str = "_static"
"output folder for static items in the html builder"

SUPPORTED_MIME_TYPES = {
FILE_FIELD: str = "static-file"
"field in the ``FaviconsDef`` pointing to file in the ``html_static_path``"

SUPPORTED_MIME_TYPES: Dict[str, str] = {
"bmp": "image/x-ms-bmp",
"gif": "image/gif",
"ico": "image/x-icon",
Expand All @@ -31,14 +38,19 @@
"png": "image/png",
"svg": "image/svg+xml",
}
"supported mime types of the link tag"


SUPPORTED_SIZE_TYPES: List[str] = ["bmp", "gif", "jpeg", "jpg", "png"]
"list of file type that can be used to compute size"


def generate_meta(favicon: Dict[str, str]) -> str:
"""Generate metatag based on favicon data.
Default behavior:
- If favicon data contains no ``rel`` attribute, sets ``rel="icon"``
- If no ``size`` attribute is provided, ``size`` will be omitted
- If no ``sizes`` attribute is provided, ``sizes`` will be computed from the file
- If no favicon MIME type is provided, the value for ``type`` will be
based on the favicon's file name extension (for BMP, GIF, ICO, JPG, JPEG,
SVG, or PNG files)
Expand All @@ -52,29 +64,89 @@ def generate_meta(favicon: Dict[str, str]) -> str:
# get the tag of the output
tag = "meta" if "name" in favicon else "link"

# prepare all the tag parameters and leave them in the favicon dict

# default to "icon" for link elements
if tag == "link":
favicon.setdefault("rel", "icon")
favicon["href"] # to raise an error if not set
extension = favicon["href"].split(".")[-1]

# set the type for link elements.
# if type is not set try to guess it from the file extention
# if type is not set, try to guess it from the file extension
type_ = favicon.get("type")
if not type_ and tag == "link":
extention = favicon["href"].split(".")[-1]
if extention in SUPPORTED_MIME_TYPES.keys():
type_ = SUPPORTED_MIME_TYPES[extention]
if type_ is not None:
if not type_ and tag == "link" and extension in SUPPORTED_MIME_TYPES:
type_ = SUPPORTED_MIME_TYPES[extension]
favicon["type"] = type_

# build the html element
parameters = [f'{k}="{v}"' for k, v in favicon.items()]
parameters = [f'{k}="{v}"' for k, v in favicon.items() if v is not None]
html_element = f" <{tag} {' '.join(parameters)}>"

return html_element


def _sizes(
favicon: Dict[str, str], static_path: List[str], confdir: str
) -> Dict[str, str]:
"""Compute the size of the favicon if its size is not explicitly defined.
If the file is a SUPPORTED_MIME_TYPES, then the size is computed on the fly and added
to the favicon attributes. Don't do anything if the favicon is not a link tag.
Args:
favicon: The favicon description as set in the conf.py file
static_path: The static_path registered in the application
confdir: The source directory of the documentation
Returns:
The favicon with a fully qualified size
"""
# exit if the favicon tag has no href (like meta)
if not (FILE_FIELD in favicon or "href" in favicon):
return favicon

# init the parameters
link: Optional[str] = favicon.get("href") or favicon.get(FILE_FIELD)
extension: Optional[str] = link.split(".")[-1] if link else None
sizes: Optional[str] = favicon.get("sizes")

# get the size automatically if not supplied
if link and sizes is None and extension in SUPPORTED_SIZE_TYPES:
file: Optional[Union[BytesIO, Path]] = None
if bool(urlparse(link).netloc):
try:
response = requests.get(link)
except RequestException:
response = requests.Response()
response.status_code = -1

if response.status_code == 200:
file = BytesIO(response.content)
else:
logger.warning(
f"The provided link ({link}) cannot be read. "
"Size will not be computed."
)
else:
for folder in static_path:
path = Path(confdir) / folder / link
if path.is_file():
file = path
break
if file is None:
logger.warning(
f"The provided path ({link}) is not part of any of the static path. "
"Size will not be computed."
)

# compute the image size if image file is found
if file is not None:
w, h = imagesize.get(file)
size = f"{int(w)}x{int(h)}"
favicon["sizes"] = size

return favicon


def _static_to_href(pathto: Callable, favicon: Dict[str, str]) -> Dict[str, str]:
"""Replace static ref to fully qualified href.
Expand Down Expand Up @@ -109,12 +181,16 @@ def _static_to_href(pathto: Callable, favicon: Dict[str, str]) -> Dict[str, str]
return favicon


def create_favicons_meta(pathto: Callable, favicons: FaviconsDef) -> Optional[str]:
def create_favicons_meta(
pathto: Callable, favicons: FaviconsDef, static_path: List[str], confdir: str
) -> Optional[str]:
"""Create ``<link>`` elements for favicons defined in configuration.
Args:
pathto: Sphinx helper_ function to handle relative URLs
favicons: Favicon data from configuration. Can be a single dict or a list of dicts.
static_path: the static_path registered in the application
confdir: the source directory of the documentation
Returns:
``<link>`` elements for all favicons.
Expand All @@ -138,7 +214,7 @@ def create_favicons_meta(pathto: Callable, favicons: FaviconsDef) -> Optional[st
"Custom favicons will not be included in build."
)
continue

favicon = _sizes(favicon, static_path, confdir)
tag = generate_meta(_static_to_href(pathto, favicon))
meta_favicons.append(tag)

Expand All @@ -158,17 +234,24 @@ def html_page_context(
app: The sphinx application
pagename: the name of the page as string
templatename: the name of the template as string
context: the html context dictionnary
context: the html context dictionary
doctree: the docutils document tree
"""
if doctree and app.config["favicons"]:
pathto: Callable = context["pathto"] # should exist in a HTML context
favicons_meta = create_favicons_meta(pathto, app.config["favicons"])
context["metatags"] += favicons_meta
# extract parameters from app
favicons: Optional[Dict[str, str]] = app.config["favicons"]
pathto: Callable = context["pathto"]
static_path: List[str] = app.config["html_static_path"]
confdir: str = app.confdir

if not (doctree and favicons):
return

favicons_meta = create_favicons_meta(pathto, favicons, static_path, confdir)
context["metatags"] += favicons_meta


def setup(app: Sphinx) -> Dict[str, Any]:
"""Add custom configuration to shinx app.
"""Add custom configuration to sphinx app.
Args:
app: the Sphinx application
Expand Down
4 changes: 2 additions & 2 deletions tests/roots/test-href_and_static/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
{
"sizes": "32x32",
"static-file": "square.svg",
"href": "https://secure.example.com/favicon/favicon-32x32.png",
"href": "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/favicon-32x32.png",
},
{
"sizes": "128x128",
"static-file": "nested/triangle.svg",
"href": "https://secure.example.com/favicon/apple-touch-icon-180x180.png",
"href": "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/apple-touch-icon.png",
},
]
6 changes: 3 additions & 3 deletions tests/roots/test-list_of_three_dicts/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@
{
"rel": "icon",
"sizes": "16x16",
"href": "https://secure.example.com/favicon/favicon-16x16.png",
"href": "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/favicon-16x16.png",
"type": "image/png",
},
{
"rel": "icon",
"sizes": "32x32",
"href": "https://secure.example.com/favicon/favicon-32x32.png",
"href": "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/favicon-32x32.png",
"type": "image/png",
},
{
"rel": "apple-touch-icon",
"sizes": "180x180",
"href": "https://secure.example.com/favicon/apple-touch-icon-180x180.png",
"href": "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/apple-touch-icon.png",
"type": "image/png",
},
]
22 changes: 0 additions & 22 deletions tests/roots/test-list_of_three_dicts_automated_values/conf.py

This file was deleted.

18 changes: 18 additions & 0 deletions tests/roots/test-list_of_three_icons_automated_values/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
extensions = ["sphinx_favicon"]

master_doc = "index"
exclude_patterns = ["_build"]

html_theme = "basic"

favicons = [
"https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/favicon-16x16.png",
{
"href": "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/favicon-32x32.png",
},
{
"rel": "apple-touch-icon",
"sizes": "180x180",
"href": "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/apple-touch-icon.png",
},
]
4 changes: 2 additions & 2 deletions tests/roots/test-list_of_urls/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@

favicons = [
"https://secure.example.com/favicon/favicon-16x16.gif",
"https://secure.example.com/favicon/favicon-32x32.png",
"https://secure.example.com/favicon/apple-touch-icon-180x180.png",
"https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/favicon-32x32.png",
"https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/apple-touch-icon.png",
]
2 changes: 1 addition & 1 deletion tests/roots/test-single_dict/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
favicons = {
"rel": "apple-touch-icon",
"sizes": "180x180",
"href": "https://secure.example.com/favicon/apple-touch-icon-180x180.png",
"href": "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/apple-touch-icon.png",
}
Loading

0 comments on commit dec53da

Please sign in to comment.