Coverage for fastblocks/adapters/icons/fontawesome.py: 84%
68 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
1"""FontAwesome icons adapter implementation."""
3from contextlib import suppress
4from typing import Any
5from uuid import UUID
7from acb.config import Settings
8from acb.depends import depends
10from ._base import IconsBase
13class FontAwesomeSettings(Settings): # type: ignore[misc]
14 """FontAwesome-specific settings."""
16 version: str = "6.4.0"
17 style: str = "solid" # solid, regular, light, thin, brands
18 cdn_url: str = (
19 "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/{version}/css/all.min.css"
20 )
21 kit_url: str | None = None # For FontAwesome kit users
24class FontAwesomeAdapter(IconsBase):
25 """FontAwesome icons adapter implementation."""
27 # Required ACB 0.19.0+ metadata
28 MODULE_ID: UUID = UUID("01937d86-4f2a-7b3c-8d9e-f3b4d3c2d1a1") # Static UUID7
29 MODULE_STATUS = "stable"
31 # Icon mapping for common icons across different styles
32 ICON_MAPPINGS = {
33 "home": "fa-house",
34 "user": "fa-user",
35 "users": "fa-users",
36 "settings": "fa-gear",
37 "edit": "fa-pen-to-square",
38 "delete": "fa-trash",
39 "save": "fa-floppy-disk",
40 "search": "fa-magnifying-glass",
41 "add": "fa-plus",
42 "remove": "fa-minus",
43 "check": "fa-check",
44 "close": "fa-xmark",
45 "arrow_up": "fa-arrow-up",
46 "arrow_down": "fa-arrow-down",
47 "arrow_left": "fa-arrow-left",
48 "arrow_right": "fa-arrow-right",
49 "chevron_up": "fa-chevron-up",
50 "chevron_down": "fa-chevron-down",
51 "chevron_left": "fa-chevron-left",
52 "chevron_right": "fa-chevron-right",
53 "heart": "fa-heart",
54 "star": "fa-star",
55 "bookmark": "fa-bookmark",
56 "share": "fa-share",
57 "download": "fa-download",
58 "upload": "fa-upload",
59 "file": "fa-file",
60 "folder": "fa-folder",
61 "image": "fa-image",
62 "video": "fa-video",
63 "music": "fa-music",
64 "calendar": "fa-calendar",
65 "clock": "fa-clock",
66 "bell": "fa-bell",
67 "email": "fa-envelope",
68 "phone": "fa-phone",
69 "location": "fa-location-dot",
70 "link": "fa-link",
71 "external_link": "fa-external-link",
72 "info": "fa-circle-info",
73 "warning": "fa-triangle-exclamation",
74 "error": "fa-circle-exclamation",
75 "success": "fa-circle-check",
76 "menu": "fa-bars",
77 "grid": "fa-grid",
78 "list": "fa-list",
79 "lock": "fa-lock",
80 "unlock": "fa-unlock",
81 "eye": "fa-eye",
82 "eye_slash": "fa-eye-slash",
83 "shopping_cart": "fa-cart-shopping",
84 "credit_card": "fa-credit-card",
85 "print": "fa-print",
86 "question": "fa-circle-question",
87 "help": "fa-circle-question",
88 }
90 # Brand icons (always use fab prefix)
91 BRAND_ICONS = {
92 "github": "fa-github",
93 "twitter": "fa-twitter",
94 "facebook": "fa-facebook",
95 "instagram": "fa-instagram",
96 "linkedin": "fa-linkedin",
97 "youtube": "fa-youtube",
98 "google": "fa-google",
99 "apple": "fa-apple",
100 "microsoft": "fa-microsoft",
101 "amazon": "fa-amazon",
102 "discord": "fa-discord",
103 "slack": "fa-slack",
104 "telegram": "fa-telegram",
105 "whatsapp": "fa-whatsapp",
106 }
108 def __init__(self) -> None:
109 """Initialize FontAwesome adapter."""
110 super().__init__()
111 self.settings = FontAwesomeSettings()
113 # Register with ACB dependency system
114 with suppress(Exception):
115 depends.set(self)
117 def get_stylesheet_links(self) -> list[str]:
118 """Generate FontAwesome stylesheet link tags."""
119 links = []
121 # Use kit URL if provided (overrides CDN)
122 if self.settings.kit_url:
123 links.append(
124 f'<script src="{self.settings.kit_url}" crossorigin="anonymous"></script>'
125 )
126 else:
127 cdn_url = self.settings.cdn_url.format(version=self.settings.version)
128 links.append(f'<link rel="stylesheet" href="{cdn_url}">')
130 return links
132 def get_icon_class(self, icon_name: str) -> str:
133 """Get FontAwesome-specific class names for icons."""
134 # Check if it's a brand icon
135 if icon_name in self.BRAND_ICONS:
136 fa_icon = self.BRAND_ICONS[icon_name]
137 return f"fab {fa_icon}"
139 # Check if it's a mapped icon
140 if icon_name in self.ICON_MAPPINGS:
141 fa_icon = self.ICON_MAPPINGS[icon_name]
142 else:
143 # Use icon name as-is, adding fa- prefix if not present
144 fa_icon = icon_name if icon_name.startswith("fa-") else f"fa-{icon_name}"
146 # Determine style prefix
147 style_prefix = self._get_style_prefix(self.settings.style)
148 return f"{style_prefix} {fa_icon}"
150 def get_icon_tag(self, icon_name: str, **attributes: Any) -> str:
151 """Generate complete icon tags with FontAwesome classes."""
152 icon_class = self.get_icon_class(icon_name)
154 # Add any additional classes
155 if "class" in attributes:
156 icon_class = f"{icon_class} {attributes.pop('class')}"
158 # Build attributes string
159 attr_parts = [f'class="{icon_class}"']
161 # Handle common attributes
162 for key, value in attributes.items():
163 if key in ("id", "style", "title", "data-*"):
164 attr_parts.append(f'{key}="{value}"')
165 elif key.startswith("aria-"):
166 attr_parts.append(f'{key}="{value}"')
168 # Add accessibility attributes
169 if "title" not in attributes and "aria-label" not in attributes:
170 attr_parts.append(f'aria-label="{icon_name} icon"')
172 attrs_str = " ".join(attr_parts)
173 return f"<i {attrs_str}></i>"
175 def _get_style_prefix(self, style: str) -> str:
176 """Get FontAwesome style prefix."""
177 style_map = {
178 "solid": "fas",
179 "regular": "far",
180 "light": "fal",
181 "thin": "fat",
182 "duotone": "fad",
183 "brands": "fab",
184 }
185 return style_map.get(style, "fas")
187 def get_icon_with_text(
188 self, icon_name: str, text: str, position: str = "left", **attributes: Any
189 ) -> str:
190 """Generate icon with text combination."""
191 icon_tag = self.get_icon_tag(icon_name, **attributes)
193 if position == "right":
194 return f"{text} {icon_tag}"
196 return f"{icon_tag} {text}"
198 def get_icon_button(self, icon_name: str, **attributes: Any) -> str:
199 """Generate button with icon."""
200 icon_tag = self.get_icon_tag(icon_name)
202 # Extract button-specific attributes
203 button_class = attributes.pop("button_class", "btn")
204 button_attrs = {
205 k: v
206 for k, v in attributes.items()
207 if k in ("id", "style", "onclick", "type", "disabled")
208 }
210 # Build button attributes
211 attr_parts = [f'class="{button_class}"']
212 for key, value in button_attrs.items():
213 attr_parts.append(f'{key}="{value}"')
215 attrs_str = " ".join(attr_parts)
216 return f"<button {attrs_str}>{icon_tag}</button>"