Skip to content

Commit

Permalink
feat: add some icon filters
Browse files Browse the repository at this point in the history
  • Loading branch information
phil65 committed Oct 29, 2024
1 parent ff3c6dc commit 6d58a00
Show file tree
Hide file tree
Showing 5 changed files with 296 additions and 0 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ license = "MIT"
cli = ["rich", "typer"]
extras = ["black", "pygments", "humanize"]
llm = ["litellm", "python-dotenv", "pillow"]
icons = ["pyconify"]

[tool.uv]
dev-dependencies = [
Expand Down
136 changes: 136 additions & 0 deletions src/jinjarope/iconfilters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
from __future__ import annotations

from typing import Literal

from jinjarope import icons


Rotation = Literal["90", "180", "270", 90, 180, 270, "-90", 1, 2, 3]
Flip = Literal["horizontal", "vertical", "horizontal,vertical"]


def get_favicon(
url: str,
provider: Literal[
"google", "duckduckgo", "iconhorse", "yandex", "favicon_io", "favicon_ninja"
] = "duckduckgo",
size: int = 32,
):
"""Return a favicon URL for the given URL.
Arguments:
url: The URL to get the favicon for.
provider: The provider to use for the favicon.
size: Size of the favicon in pixels (not supported by all providers)
"""
from urllib.parse import urlparse

# Parse the URL to get the domain
domain = urlparse(url).netloc or url

match provider:
case "google":
return f"https://www.google.com/s2/favicons?domain={domain}&sz={size}"
case "duckduckgo":
return f"https://icons.duckduckgo.com/ip3/{domain}.ico"
case "iconhorse":
return f"https://icon.horse/icon/{domain}?size={size}"
case "yandex":
# Yandex supports sizes: 16, 32, 76, 120, 180, 192, 256
valid_sizes = [16, 32, 76, 120, 180, 192, 256]
closest_size = min(valid_sizes, key=lambda x: abs(x - size))
return f"https://favicon.yandex.net/favicon/{domain}?size={closest_size}"
case "favicon_io":
return f"https://favicon.io/favicon/{domain}"
case "favicon_ninja":
return f"https://favicon.ninja/icon?url={domain}&size={size}"
case _:
msg = f"Invalid provider: {provider}"
raise ValueError(msg)


def get_icon_svg(
icon: str,
color: str | None = None,
height: str | int | None = None,
width: str | int | None = None,
flip: Flip | None = None,
rotate: Rotation | None = None,
box: bool | None = None,
) -> str:
"""Return svg for given pyconify icon key.
Key should look like "mdi:file"
For compatibility, this method also supports compatibility for
emoji-slugs (":material-file:") as well as material-paths ("material/file")
If no icon group is supplied as part of the string, mdi is assumed as group.
When passing a string with "|" delimiters, the returned string will contain multiple
icons.
Arguments:
icon: Pyconify icon name
color: Icon color. Replaces currentColor with specific color, resulting in icon
with hardcoded palette.
height: Icon height. If only one dimension is specified, such as height, other
dimension will be automatically set to match it.
width: Icon width. If only one dimension is specified, such as height, other
dimension will be automatically set to match it.
flip: Flip icon.
rotate: Rotate icon. If an integer is provided, it is assumed to be in degrees.
box: Adds an empty rectangle to SVG that matches the icon's viewBox. It is needed
when importing SVG to various UI design tools that ignore viewBox. Those
tools, such as Sketch, create layer groups that automatically resize to fit
content. Icons usually have empty pixels around icon, so such software crops
those empty pixels and icon's group ends up being smaller than actual icon,
making it harder to align it in design.
Example:
get_icon_svg("file") # implicit mdi group
get_icon_svg("mdi:file") # pyconify key
get_icon_svg("material/file") # Material-style path
get_icon_svg(":material-file:") # material-style emoji slug
get_icon_svg("mdi:file|:material-file:") # returns a string with two svgs
"""
label = ""
for splitted in icon.split("|"):
key = get_pyconify_key(splitted)
import pyconify

label += pyconify.svg(
key,
color=color,
height=height,
width=width,
flip=flip,
rotate=rotate,
box=box,
).decode()
return label


def get_pyconify_key(icon: str) -> str:
"""Convert given string to a pyconify key.
Converts the keys from MkDocs-Material ("material/sth" or ":material-sth:")
to their pyconify equivalent.
Arguments:
icon: The string which should be converted to a pyconify key.
"""
for k, v in icons.PYCONIFY_TO_PREFIXES.items():
path = f"{v.replace('-', '/')}/"
icon = icon.replace(path, f"{k}:")
icon = icon.replace(f":{v}-", f"{k}:")
icon = icon.strip(":")
mapping = {k: v[0] for k, v in icons._get_collection_map().items()}
for prefix in mapping:
if icon.startswith(f"{prefix}-"):
icon = icon.replace(f"{prefix}-", f"{prefix}:")
break
if (count := icon.count(":")) > 1:
icon = icon.replace(":", "-", count - 1)
if ":" not in icon:
icon = f"mdi:{icon}"
return icon
100 changes: 100 additions & 0 deletions src/jinjarope/icons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from __future__ import annotations

import functools
import pathlib


PYCONIFY_TO_PREFIXES = {
"mdi": "material",
"simple-icons": "simple",
"octicon": "octicons",
"fa6-regular": "fontawesome-regular",
"fa-brands": "fontawesome-brands",
"fa6-solid": "fontawesome-solid",
}


ROOT = pathlib.Path(__file__).parent
ICON_FILE = ROOT / "resources" / "icons.json.gzip"


@functools.cache
def _get_collection_map(*prefixes: str) -> dict[str, list[str]]:
"""Return a dictionary with a mapping from pyconify name to icon prefixes.
In order to provide compatibility with the materialx-icon-index,
we also add the prefixes used by that index, which is different from
the pyconify prefixes. (material vs mdi etc, see PYCONIFY_TO_PREFIXES)
Arguments:
prefixes: The collections to fetch
"""
import pyconify

mapping = {coll: [coll] for coll in pyconify.collections(*prefixes)}
for k, v in PYCONIFY_TO_PREFIXES.items():
if k in mapping:
mapping[k].append(v)
return mapping


@functools.cache
def _get_pyconify_icon_index(*collections: str) -> dict[str, dict[str, str]]:
"""Return a icon index for the pymdownx emoji extension containing pyconify icons.
The dictionaries contain three key-value pairs:
- "name": the emoji identifier
- "path": the pyconify key
- "set": the collection name
Arguments:
collections: Collections to fetch. If none given, fetch all
"""
import pyconify

index = {}
for coll, prefixes in _get_collection_map(*collections).items():
collection = pyconify.collection(coll)
for icon_name in collection.get("uncategorized", []):
for prefix in prefixes:
name = f":{prefix}-{icon_name}:"
index[name] = {
"name": name,
"path": f"{coll}:{icon_name}",
"set": coll,
}
for cat in pyconify.collection(coll).get("categories", {}).values():
for icon_name in cat:
for prefix in prefixes:
name = f":{prefix}-{icon_name}:"
index[name] = {
"name": name,
"path": f"{coll}:{icon_name}",
"set": coll,
}
return index


def write_icon_index():
"""Fetch the complete icon index and write it gzipped to disk."""
import gzip
import json

mapping = _get_pyconify_icon_index()
with gzip.open(ICON_FILE, "w") as file:
file.write(json.dumps(mapping).encode())


def load_icon_index() -> dict[str, dict[str, str]]:
"""Load the complete icon index from disk."""
import gzip
import json

with gzip.open(ICON_FILE, "r") as file:
return json.loads(file.read())


if __name__ == "__main__":
# idx = load_icon_index()
# print(idx)
write_icon_index()
59 changes: 59 additions & 0 deletions src/jinjarope/resources/filters.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1006,3 +1006,62 @@ icon = "mdi:file-find"
template = """
{{ filters.get_file | get_file }}
"""

# Icons

[filters.get_favicon]
fn = "jinjarope.iconfilters.get_favicon"
group = "icons"
required_packages = ["urllib3"]

[filters.get_favicon.examples.basic]
template = """
{{ 'example.com' | get_favicon }}
"""
icon = "mdi:favicon"

[filters.get_favicon.examples.google]
template = """
{{ 'example.com' | get_favicon(provider='google', size=64) }}
"""
icon = "mdi:google"

[filters.get_icon_svg]
fn = "jinjarope.iconfilters.get_icon_svg"
group = "icons"
required_packages = ["pyconify"]

[filters.get_icon_svg.examples.basic]
template = """
{{ 'mdi:file' | get_icon_svg }}
"""
icon = "mdi:svg"

[filters.get_icon_svg.examples.styled]
template = """
{{ 'mdi:file' | get_icon_svg(color='#ff0000', height=32, width=32) }}
"""
icon = "mdi:palette"

[filters.get_icon_svg.examples.transformed]
template = """
{{ 'mdi:file' | get_icon_svg(flip='horizontal', rotate=90) }}
"""
icon = "mdi:rotate-3d-variant"

[filters.get_pyconify_key]
fn = "jinjarope.iconfilters.get_pyconify_key"
group = "icons"
required_packages = ["pyconify"]

[filters.get_pyconify_key.examples.basic]
template = """
{{ 'material/file' | get_pyconify_key }}
"""
icon = "mdi:key"

[filters.get_pyconify_key.examples.emoji]
template = """
{{ ':material-file:' | get_pyconify_key }}
"""
icon = "mdi:emoticon"
Binary file added src/jinjarope/resources/icons.json.gzip
Binary file not shown.

0 comments on commit 6d58a00

Please sign in to comment.