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

1"""FontAwesome icons adapter implementation.""" 

2 

3from contextlib import suppress 

4from typing import Any 

5from uuid import UUID 

6 

7from acb.config import Settings 

8from acb.depends import depends 

9 

10from ._base import IconsBase 

11 

12 

13class FontAwesomeSettings(Settings): # type: ignore[misc] 

14 """FontAwesome-specific settings.""" 

15 

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 

22 

23 

24class FontAwesomeAdapter(IconsBase): 

25 """FontAwesome icons adapter implementation.""" 

26 

27 # Required ACB 0.19.0+ metadata 

28 MODULE_ID: UUID = UUID("01937d86-4f2a-7b3c-8d9e-f3b4d3c2d1a1") # Static UUID7 

29 MODULE_STATUS = "stable" 

30 

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 } 

89 

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 } 

107 

108 def __init__(self) -> None: 

109 """Initialize FontAwesome adapter.""" 

110 super().__init__() 

111 self.settings = FontAwesomeSettings() 

112 

113 # Register with ACB dependency system 

114 with suppress(Exception): 

115 depends.set(self) 

116 

117 def get_stylesheet_links(self) -> list[str]: 

118 """Generate FontAwesome stylesheet link tags.""" 

119 links = [] 

120 

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}">') 

129 

130 return links 

131 

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}" 

138 

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}" 

145 

146 # Determine style prefix 

147 style_prefix = self._get_style_prefix(self.settings.style) 

148 return f"{style_prefix} {fa_icon}" 

149 

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) 

153 

154 # Add any additional classes 

155 if "class" in attributes: 

156 icon_class = f"{icon_class} {attributes.pop('class')}" 

157 

158 # Build attributes string 

159 attr_parts = [f'class="{icon_class}"'] 

160 

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}"') 

167 

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"') 

171 

172 attrs_str = " ".join(attr_parts) 

173 return f"<i {attrs_str}></i>" 

174 

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") 

186 

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) 

192 

193 if position == "right": 

194 return f"{text} {icon_tag}" 

195 

196 return f"{icon_tag} {text}" 

197 

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) 

201 

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 } 

209 

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}"') 

214 

215 attrs_str = " ".join(attr_parts) 

216 return f"<button {attrs_str}>{icon_tag}</button>"