Source code for animaid.html_int

"""HTMLInt - An int subclass with HTML rendering capabilities."""

from __future__ import annotations

import html
import uuid
from typing import TYPE_CHECKING, Any, Self

from animaid.css_types import (
    BorderValue,
    ColorValue,
    CSSValue,
    SizeValue,
    SpacingValue,
)
from animaid.html_object import HTMLObject

if TYPE_CHECKING:
    from animaid.html_float import HTMLFloat


def _to_css(value: object) -> str:
    """Convert a value to its CSS string representation."""
    if hasattr(value, "to_css"):
        return str(value.to_css())
    return str(value)


[docs] class HTMLInt(HTMLObject, int): """An int subclass that renders as styled HTML. HTMLInt behaves like a regular Python int but includes methods for applying CSS styles, number formatting, and rendering to HTML. All styling methods modify the object in-place and return self for method chaining. Examples: >>> n = HTMLInt(42) >>> n.bold().red().render() '<span style="font-weight: bold; color: red">42</span>' >>> HTMLInt(1234567).comma().render() '<span>1,234,567</span>' >>> HTMLInt(1000).currency("$").bold().render() '<span style="font-weight: bold">$1,000</span>' >>> HTMLInt(1).ordinal().render() '<span>1st</span>' """ _styles: dict[str, str] _css_classes: list[str] _tag: str _display_format: str _format_options: dict[str, object] _obs_id: str def __new__(cls, value: int = 0, **styles: str | CSSValue) -> Self: """Create a new HTMLInt instance. Args: value: The integer value. **styles: Initial CSS styles (underscores converted to hyphens). Returns: A new HTMLInt instance. """ instance = super().__new__(cls, value) return instance def __init__(self, value: int = 0, **styles: str | CSSValue) -> None: """Initialize styles for the HTMLInt. Args: value: The integer value (handled by __new__). **styles: CSS property-value pairs. """ self._styles = {} self._css_classes = [] self._tag = "span" self._display_format = "default" self._format_options = {} self._obs_id = str(uuid.uuid4()) for key, val in styles.items(): css_key = key.replace("_", "-") self._styles[css_key] = _to_css(val) def _notify(self) -> None: """Publish change notification via pypubsub.""" try: from pubsub import pub pub.sendMessage("animaid.changed", obs_id=self._obs_id) except ImportError: pass # pypubsub not installed def _copy_with_settings( self, new_styles: dict[str, str] | None = None, new_classes: list[str] | None = None, new_tag: str | None = None, new_format: str | None = None, new_format_options: dict[str, object] | None = None, ) -> Self: """Create a copy of this HTMLInt with modified settings. This method is used internally for operations that must return a new object (like arithmetic operations). Args: new_styles: Styles to merge with existing styles. new_classes: Classes to add to existing classes. new_tag: New HTML tag to use. new_format: New display format. new_format_options: New format options. Returns: A new HTMLInt with combined settings. """ result = HTMLInt(int(self)) result._styles = self._styles.copy() result._css_classes = self._css_classes.copy() result._tag = self._tag result._display_format = self._display_format result._format_options = self._format_options.copy() result._obs_id = self._obs_id # Preserve ID so updates still work if new_styles: result._styles.update(new_styles) if new_classes: result._css_classes.extend(new_classes) if new_tag: result._tag = new_tag if new_format: result._display_format = new_format if new_format_options: result._format_options.update(new_format_options) return result # type: ignore[return-value]
[docs] def styled(self, **styles: str | CSSValue) -> Self: """Apply additional inline styles in-place. Args: **styles: CSS property-value pairs. Returns: Self for method chaining. """ for key, value in styles.items(): css_key = key.replace("_", "-") self._styles[css_key] = _to_css(value) self._notify() return self
[docs] def add_class(self, *class_names: str) -> Self: """Add CSS classes in-place. Args: *class_names: CSS class names to add. Returns: Self for method chaining. """ self._css_classes.extend(class_names) self._notify() return self
[docs] def tag(self, tag_name: str) -> Self: """Change the HTML tag in-place. Args: tag_name: The HTML tag to use (e.g., "div", "p", "strong"). Returns: Self for method chaining. """ self._tag = tag_name self._notify() return self
def _format_value(self) -> str: """Format the integer value based on display format settings.""" value = int(self) if self._display_format == "comma": return f"{value:,}" elif self._display_format == "currency": symbol = self._format_options.get("symbol", "$") return f"{symbol}{value:,}" elif self._display_format == "percent": return f"{value}%" elif self._display_format == "ordinal": return self._to_ordinal(value) elif self._display_format == "padded": width = self._format_options.get("width", 2) return f"{value:0{width}d}" else: return str(value) @staticmethod def _to_ordinal(n: int) -> str: """Convert an integer to its ordinal string (1st, 2nd, 3rd, etc.).""" if 11 <= abs(n) % 100 <= 13: suffix = "th" else: suffix = {1: "st", 2: "nd", 3: "rd"}.get(abs(n) % 10, "th") return f"{n}{suffix}"
[docs] def render(self) -> str: """Return HTML representation of this integer. Returns: A string containing valid HTML. """ content = html.escape(self._format_value()) attrs = self._build_attributes() if attrs: return f"<{self._tag} {attrs}>{content}</{self._tag}>" else: return f"<{self._tag}>{content}</{self._tag}>"
# ------------------------------------------------------------------------- # Number Formatting Methods # -------------------------------------------------------------------------
[docs] def comma(self) -> Self: """Apply thousand separator formatting in-place. Examples: >>> HTMLInt(1234567).comma().render() '<span>1,234,567</span>' """ self._display_format = "comma" self._notify() return self
[docs] def currency(self, symbol: str = "$") -> Self: """Apply currency formatting in-place. Args: symbol: Currency symbol (default "$") Examples: >>> HTMLInt(1000).currency().render() '<span>$1,000</span>' >>> HTMLInt(1000).currency("€").render() '<span>€1,000</span>' """ self._display_format = "currency" self._format_options["symbol"] = symbol self._notify() return self
[docs] def percent(self) -> Self: """Apply percentage formatting in-place. Examples: >>> HTMLInt(85).percent().render() '<span>85%</span>' """ self._display_format = "percent" self._notify() return self
[docs] def ordinal(self) -> Self: """Apply ordinal formatting (1st, 2nd, 3rd, etc.) in-place. Examples: >>> HTMLInt(1).ordinal().render() '<span>1st</span>' >>> HTMLInt(22).ordinal().render() '<span>22nd</span>' """ self._display_format = "ordinal" self._notify() return self
[docs] def padded(self, width: int = 2) -> Self: """Apply zero-padding formatting in-place. Args: width: Minimum width (default 2) Examples: >>> HTMLInt(7).padded(3).render() '<span>007</span>' """ self._display_format = "padded" self._format_options["width"] = width self._notify() return self
# ------------------------------------------------------------------------- # Style Methods (no-argument styles) # -------------------------------------------------------------------------
[docs] def bold(self) -> Self: """Apply bold text style in-place.""" self._styles["font-weight"] = "bold" self._notify() return self
[docs] def italic(self) -> Self: """Apply italic text style in-place.""" self._styles["font-style"] = "italic" self._notify() return self
[docs] def underline(self) -> Self: """Apply underline text style in-place.""" self._styles["text-decoration"] = "underline" self._notify() return self
[docs] def strikethrough(self) -> Self: """Apply strikethrough text style in-place.""" self._styles["text-decoration"] = "line-through" self._notify() return self
[docs] def monospace(self) -> Self: """Apply monospace font in-place.""" self._styles["font-family"] = "monospace" self._notify() return self
# ------------------------------------------------------------------------- # Color Shortcuts # -------------------------------------------------------------------------
[docs] def red(self) -> Self: """Apply red text color in-place.""" self._styles["color"] = "red" self._notify() return self
[docs] def blue(self) -> Self: """Apply blue text color in-place.""" self._styles["color"] = "blue" self._notify() return self
[docs] def green(self) -> Self: """Apply green text color in-place.""" self._styles["color"] = "green" self._notify() return self
[docs] def yellow(self) -> Self: """Apply yellow text color in-place.""" self._styles["color"] = "#b8860b" self._notify() return self
[docs] def orange(self) -> Self: """Apply orange text color in-place.""" self._styles["color"] = "orange" self._notify() return self
[docs] def purple(self) -> Self: """Apply purple text color in-place.""" self._styles["color"] = "purple" self._notify() return self
[docs] def gray(self) -> Self: """Apply gray text color in-place.""" self._styles["color"] = "gray" self._notify() return self
[docs] def white(self) -> Self: """Apply white text color in-place.""" self._styles["color"] = "white" self._notify() return self
[docs] def black(self) -> Self: """Apply black text color in-place.""" self._styles["color"] = "black" self._notify() return self
# ------------------------------------------------------------------------- # Size Shortcuts # -------------------------------------------------------------------------
[docs] def xs(self) -> Self: """Apply extra-small text size (12px) in-place.""" self._styles["font-size"] = "12px" self._notify() return self
[docs] def small(self) -> Self: """Apply small text size (14px) in-place.""" self._styles["font-size"] = "14px" self._notify() return self
[docs] def medium(self) -> Self: """Apply medium text size (16px) in-place.""" self._styles["font-size"] = "16px" self._notify() return self
[docs] def large(self) -> Self: """Apply large text size (20px) in-place.""" self._styles["font-size"] = "20px" self._notify() return self
[docs] def xl(self) -> Self: """Apply extra-large text size (24px) in-place.""" self._styles["font-size"] = "24px" self._notify() return self
[docs] def xxl(self) -> Self: """Apply 2x extra-large text size (32px) in-place.""" self._styles["font-size"] = "32px" self._notify() return self
# ------------------------------------------------------------------------- # Style Presets # -------------------------------------------------------------------------
[docs] def success(self) -> Self: """Apply success style (green) in-place.""" self._styles["color"] = "#2e7d32" self._styles["background-color"] = "#e8f5e9" self._styles["padding"] = "2px 6px" self._styles["border-radius"] = "4px" self._notify() return self
[docs] def warning(self) -> Self: """Apply warning style (orange) in-place.""" self._styles["color"] = "#e65100" self._styles["background-color"] = "#fff3e0" self._styles["padding"] = "2px 6px" self._styles["border-radius"] = "4px" self._notify() return self
[docs] def error(self) -> Self: """Apply error style (red) in-place.""" self._styles["color"] = "#c62828" self._styles["background-color"] = "#ffebee" self._styles["padding"] = "2px 6px" self._styles["border-radius"] = "4px" self._notify() return self
[docs] def info(self) -> Self: """Apply info style (blue) in-place.""" self._styles["color"] = "#1565c0" self._styles["background-color"] = "#e3f2fd" self._styles["padding"] = "2px 6px" self._styles["border-radius"] = "4px" self._notify() return self
[docs] def badge(self) -> Self: """Apply badge/pill style in-place.""" self._styles["background-color"] = "#e0e0e0" self._styles["padding"] = "4px 10px" self._styles["border-radius"] = "12px" self._styles["font-size"] = "0.85em" self._styles["font-weight"] = "500" self._notify() return self
# ------------------------------------------------------------------------- # Style Methods (require value arguments) # -------------------------------------------------------------------------
[docs] def color(self, value: ColorValue) -> Self: """Apply specified text color in-place.""" self._styles["color"] = _to_css(value) self._notify() return self
[docs] def background(self, value: ColorValue) -> Self: """Apply specified background color in-place.""" self._styles["background-color"] = _to_css(value) self._notify() return self
[docs] def font_size(self, value: SizeValue) -> Self: """Apply specified font size in-place.""" self._styles["font-size"] = _to_css(value) self._notify() return self
[docs] def padding(self, value: SpacingValue) -> Self: """Apply specified padding in-place.""" self._styles["padding"] = _to_css(value) self._notify() return self
[docs] def margin(self, value: SpacingValue) -> Self: """Apply specified margin in-place.""" self._styles["margin"] = _to_css(value) self._notify() return self
[docs] def border(self, value: BorderValue) -> Self: """Apply specified border in-place.""" self._styles["border"] = _to_css(value) self._notify() return self
[docs] def border_radius(self, value: SizeValue) -> Self: """Apply specified border radius in-place.""" self._styles["border-radius"] = _to_css(value) self._notify() return self
# ------------------------------------------------------------------------- # Arithmetic Operations (return HTMLInt or HTMLFloat) # ------------------------------------------------------------------------- def _preserve_settings(self, result: HTMLInt | HTMLFloat) -> HTMLInt | HTMLFloat: """Copy settings to a new HTMLInt or HTMLFloat.""" result._styles = self._styles.copy() result._css_classes = self._css_classes.copy() result._tag = self._tag result._display_format = self._display_format result._format_options = self._format_options.copy() result._obs_id = self._obs_id # Preserve ID so updates still work return result def __add__(self, other: Any) -> Any: # type: ignore[override] """Add: HTMLInt + number.""" from animaid.html_float import HTMLFloat result: Any if isinstance(other, float) and not isinstance(other, int): result = HTMLFloat(int(self) + other) return self._preserve_settings(result) elif isinstance(other, HTMLFloat): result = HTMLFloat(int(self) + float(other)) return self._preserve_settings(result) else: result = HTMLInt(int.__add__(self, int(other))) return self._preserve_settings(result) def __radd__(self, other: Any) -> Any: # type: ignore[override] """Reverse add: number + HTMLInt.""" return self.__add__(other) def __sub__(self, other: Any) -> Any: # type: ignore[override] """Subtract: HTMLInt - number.""" from animaid.html_float import HTMLFloat result: Any if isinstance(other, float) and not isinstance(other, int): result = HTMLFloat(int(self) - other) return self._preserve_settings(result) elif isinstance(other, HTMLFloat): result = HTMLFloat(int(self) - float(other)) return self._preserve_settings(result) else: result = HTMLInt(int.__sub__(self, int(other))) return self._preserve_settings(result) def __rsub__(self, other: Any) -> Any: # type: ignore[override] """Reverse subtract: number - HTMLInt.""" from animaid.html_float import HTMLFloat result: Any if isinstance(other, float): result = HTMLFloat(other - int(self)) return self._preserve_settings(result) else: result = HTMLInt(other - int(self)) return self._preserve_settings(result) def __mul__(self, other: Any) -> Any: # type: ignore[override] """Multiply: HTMLInt * number.""" from animaid.html_float import HTMLFloat result: Any if isinstance(other, float) and not isinstance(other, int): result = HTMLFloat(int(self) * other) return self._preserve_settings(result) elif isinstance(other, HTMLFloat): result = HTMLFloat(int(self) * float(other)) return self._preserve_settings(result) else: result = HTMLInt(int.__mul__(self, int(other))) return self._preserve_settings(result) def __rmul__(self, other: Any) -> Any: # type: ignore[override] """Reverse multiply: number * HTMLInt.""" return self.__mul__(other) def __truediv__(self, other: Any) -> Any: # type: ignore[override] """True divide: HTMLInt / number (always returns HTMLFloat).""" from animaid.html_float import HTMLFloat result = HTMLFloat(int(self) / other) return self._preserve_settings(result) def __rtruediv__(self, other: Any) -> Any: # type: ignore[override] """Reverse true divide: number / HTMLInt.""" from animaid.html_float import HTMLFloat result = HTMLFloat(other / int(self)) return self._preserve_settings(result) def __floordiv__(self, other: Any) -> Any: # type: ignore[override] """Floor divide: HTMLInt // number.""" result = HTMLInt(int(self) // int(other)) return self._preserve_settings(result) def __rfloordiv__(self, other: Any) -> Any: # type: ignore[override] """Reverse floor divide: number // HTMLInt.""" result = HTMLInt(int(other) // int(self)) return self._preserve_settings(result) def __mod__(self, other: Any) -> Any: # type: ignore[override] """Modulo: HTMLInt % number.""" result = HTMLInt(int.__mod__(self, other)) return self._preserve_settings(result) def __rmod__(self, other: Any) -> Any: # type: ignore[override] """Reverse modulo: number % HTMLInt.""" result = HTMLInt(other % int(self)) return self._preserve_settings(result) def __pow__(self, other: Any) -> Any: # type: ignore[override] """Power: HTMLInt ** number.""" result = HTMLInt(int.__pow__(self, other)) return self._preserve_settings(result) def __neg__(self) -> Any: """Negate: -HTMLInt.""" result = HTMLInt(-int(self)) return self._preserve_settings(result) def __pos__(self) -> Any: """Positive: +HTMLInt.""" result = HTMLInt(+int(self)) return self._preserve_settings(result) def __abs__(self) -> Any: """Absolute value: abs(HTMLInt).""" result = HTMLInt(abs(int(self))) return self._preserve_settings(result) def __repr__(self) -> str: """Return a detailed representation for debugging.""" format_info = "" if self._display_format != "default": format_info = f", format={self._display_format!r}" styles_repr = ", ".join(f"{k}={v!r}" for k, v in self._styles.items()) if styles_repr: return f"HTMLInt({int(self)}{format_info}, {styles_repr})" return f"HTMLInt({int(self)}{format_info})"