"""
Utilities for constructing and handling badge assets (e.g. Shields.io badges).
"""
from pydantic import BaseModel
from .cleaning import canonicalize_shields_url, canonicalize_url, escape_shields_part, normalize_color
from .conversions import svg_to_base64
[docs]
class Badge(BaseModel):
"""
Representation of a README badge and its Markdown rendering.
Two `Badge` instances are considered equal if their *canonical* image URL
and link URL match, irrespective of superficial formatting differences in
the original Markdown.
Parameters
----------
alt_text : str
The alternative text for the badge image (used in the Markdown `alt`
field and as a textual fallback).
image_url : str
The URL of the badge image (e.g. a Shields.io badge endpoint).
link_url : str | None
The URL to link to when the badge is clicked. If ``None``, the badge
will be rendered as an image without a surrounding link.
full_match : str | None
The original Markdown string representing the badge, if this `Badge`
was created from a parsed README. When set, `as_markdown()` will return
this exact string, preserving original formatting.
"""
alt_text: str
image_url: str
link_url: str | None = None
full_match: str | None = None
[docs]
def as_markdown(self) -> str:
"""
Render the badge as a Markdown-formatted string.
If `full_match` is set (e.g. when this badge came from a parsed
README), that original Markdown string is returned verbatim. This
preserves existing formatting and parameter ordering, even if internal
fields were canonicalized.
Otherwise, a canonical Markdown representation is generated:
- If `link_url` is not ``None``:
``[](link_url)``
- If `link_url` is ``None``:
````
Returns
-------
str
The Markdown representation of the badge.
"""
if self.full_match is not None:
return self.full_match
if self.link_url is not None:
return f"[]({self.link_url})"
else:
return f""
def _signature(self) -> tuple[str, str | None]:
"""
Compute a canonical identity for the badge.
The signature is a tuple of:
- a canonicalized Shields.io image URL, and
- a canonicalized link URL (or ``None``).
Returns
-------
tuple[str, str | None]
The canonical `(image_url, link_url)` pair used for equality and
hashing.
"""
img = canonicalize_shields_url(str(self.image_url))
link = canonicalize_url(self.link_url) if self.link_url is not None else None
return img, link
def __eq__(self, other: object) -> bool:
"""
Compare two badges for semantic equality.
Badges are considered equal if their canonical image and link URLs
match, regardless of how they were originally written in Markdown.
Parameters
----------
other : object
The object to compare with.
Returns
-------
bool
``True`` if the badges are semantically equivalent, otherwise
``False``.
"""
if not isinstance(other, Badge):
return NotImplemented
return self._signature() == other._signature()
def __hash__(self) -> int:
"""
Compute a hash based on the badge's canonical identity.
Returns
-------
int
The hash value for the badge.
"""
return hash(self._signature())
def _make_shields_badge_url(
label: str,
message: str,
color: str, # right side
label_color: str, # left side
logo_b64: str | None = None,
) -> str:
"""
Construct a Shields.io badge URL, optionally embedding a base64 SVG logo.
This helper builds a URL for the static Shields.io endpoint:
``https://img.shields.io/badge/<label>-<message>-<color>.svg``
and attaches additional query parameters:
- ``labelColor`` to control the left-side (label) background.
- ``logo=<data URI>`` to embed a base64-encoded SVG logo, if provided.
The `label` and `message` are escaped to be safe in Shields.io URLs (e.g.
spaces, dashes, and special characters). Colors are normalized to a form
accepted by Shields (hex colors or named colors).
Parameters
----------
label : str
The text shown on the left-hand side of the badge.
message : str
The text shown on the right-hand side of the badge.
color : str
The color for the right-hand side of the badge (e.g. hex or named
color).
label_color : str
The color for the label side of the badge (left-hand side).
logo_b64 : str | None, optional
Base64-encoded SVG content to embed as the Shields.io logo. If
``None``, no logo parameter is added.
Returns
-------
str
The complete Shields.io badge URL.
"""
base = "https://img.shields.io/badge"
label_enc = escape_shields_part(label)
message_enc = escape_shields_part(message)
color_enc = normalize_color(color)
label_color_enc = normalize_color(label_color)
url = f"{base}/{label_enc}-{message_enc}-{color_enc}.svg?labelColor={label_color_enc}"
if logo_b64 is not None:
# Shields docs: data:image/svg%2bxml;base64,<BASE64>
mime_enc = "image/svg%2bxml"
logo_param = f"data:{mime_enc};base64,{logo_b64}"
url += f"&logo={logo_param}"
return url
[docs]
def compose_badge(
label: str,
message: str,
color: str,
label_color: str,
alt_text: str,
url: str | None = None,
svg_path: str | None = None,
) -> Badge:
"""
Construct a `Badge` with a Shields.io URL and optional embedded SVG logo.
This high-level helper:
1. Optionally reads an SVG file from `svg_path` and encodes it as base64.
2. Builds a Shields.io badge URL with `label`, `message`, `color`,
`label_color`, and the embedded logo (if any).
3. Instantiates a `Badge` with `alt_text`, the generated `image_url`,
and an optional `link_url`.
4. Pre-populates the `full_match` field with the Markdown representation
of the badge, so that `as_markdown()` returns a ready-to-use snippet.
Parameters
----------
label : str
The text shown on the left-hand side of the badge.
message : str
The text shown on the right-hand side of the badge.
color : str
The color for the right-hand side of the badge (e.g. hex or named).
label_color : str
The color for the label side (left-hand side) of the badge.
alt_text : str
The alternative text for the badge image (used as the `alt` attribute
in Markdown).
url : str | None, optional
The URL to link to when the badge is clicked. If ``None``, the badge
is rendered as a plain image without a link.
svg_path : str | None, optional
Filesystem path to an SVG file to embed as a logo in the badge. If
``None``, no logo is embedded.
Returns
-------
Badge
A fully constructed `Badge` instance with `image_url` pointing to a
Shields.io badge URL and `full_match` containing the Markdown snippet.
"""
logo_b64 = svg_to_base64(svg_path) if svg_path is not None else None
badge_url = _make_shields_badge_url(
label=label,
message=message,
color=color,
label_color=label_color,
logo_b64=logo_b64,
)
badge = Badge(
alt_text=alt_text,
image_url=badge_url,
link_url=url,
)
badge.full_match = badge.as_markdown()
return badge