Coverage for fastblocks / adapters / fonts / google.py: 89%

100 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-26 03:30 -0800

1"""Google Fonts adapter implementation.""" 

2 

3from contextlib import suppress 

4from urllib.parse import quote_plus 

5from uuid import UUID 

6 

7from acb.depends import depends 

8 

9from ._base import FontsBase, FontsBaseSettings 

10 

11 

12class GoogleFontsSettings(FontsBaseSettings): 

13 """Google Fonts-specific settings.""" 

14 

15 api_key: str | None = None # Optional API key for advanced features 

16 families: list[str] = ["Roboto", "Open Sans"] 

17 weights: list[str] = ["400", "700"] 

18 subsets: list[str] = ["latin"] 

19 display: str = "swap" # font-display CSS property 

20 preconnect: bool = True # Add preconnect link for performance 

21 

22 

23class GoogleFonts(FontsBase): 

24 """Google Fonts adapter implementation.""" 

25 

26 # Required ACB 0.19.0+ metadata 

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

28 MODULE_STATUS = "stable" 

29 

30 # Common Google Fonts families with fallbacks 

31 FONT_FALLBACKS = { 

32 "Roboto": "Roboto, -apple-system, BlinkMacSystemFont, sans-serif", 

33 "Open Sans": "'Open Sans', -apple-system, BlinkMacSystemFont, sans-serif", 

34 "Lato": "Lato, -apple-system, BlinkMacSystemFont, sans-serif", 

35 "Montserrat": "Montserrat, -apple-system, BlinkMacSystemFont, sans-serif", 

36 "Source Sans Pro": "'Source Sans Pro', -apple-system, BlinkMacSystemFont, sans-serif", 

37 "Inter": "Inter, -apple-system, BlinkMacSystemFont, sans-serif", 

38 "Poppins": "Poppins, -apple-system, BlinkMacSystemFont, sans-serif", 

39 "Nunito": "Nunito, -apple-system, BlinkMacSystemFont, sans-serif", 

40 "Playfair Display": "'Playfair Display', Georgia, serif", 

41 "Merriweather": "Merriweather, Georgia, serif", 

42 "Lora": "Lora, Georgia, serif", 

43 "Source Serif Pro": "'Source Serif Pro', Georgia, serif", 

44 "Crimson Text": "'Crimson Text', Georgia, serif", 

45 "PT Serif": "'PT Serif', Georgia, serif", 

46 "Fira Code": "'Fira Code', 'Source Code Pro', monospace", 

47 "Source Code Pro": "'Source Code Pro', 'Courier New', monospace", 

48 "JetBrains Mono": "'JetBrains Mono', 'Source Code Pro', monospace", 

49 "Inconsolata": "Inconsolata, 'Courier New', monospace", 

50 } 

51 

52 def __init__(self) -> None: 

53 """Initialize Google Fonts adapter.""" 

54 super().__init__() 

55 self.settings = GoogleFontsSettings() 

56 

57 # Register with ACB dependency system 

58 with suppress(Exception): 

59 depends.set(self) 

60 

61 async def get_font_import(self) -> str: 

62 """Generate Google Fonts import statements.""" 

63 # Build font families parameter 

64 families_param = self._build_families_param() 

65 

66 # Build query parameters 

67 params = [f"family={families_param}"] 

68 

69 if self.settings.subsets: 

70 subsets = "&".join(self.settings.subsets) 

71 params.extend((f"subset={subsets}", f"display={self.settings.display}")) 

72 

73 query_string = "&".join(params) 

74 url = f"https://fonts.googleapis.com/css2?{query_string}" 

75 

76 # Generate link tags 

77 links = [] 

78 

79 # Add preconnect for performance 

80 if self.settings.preconnect: 

81 links.extend( 

82 ( 

83 '<link rel="preconnect" href="https://fonts.googleapis.com">', 

84 '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>', 

85 ) 

86 ) 

87 

88 # Add the main stylesheet link 

89 links.append(f'<link rel="stylesheet" href="{url}">') 

90 

91 return "\n".join(links) 

92 

93 def get_font_family(self, font_type: str) -> str: 

94 """Get font family CSS values with fallbacks.""" 

95 # Map font types to families 

96 if font_type == "primary" and self.settings.families: 

97 primary_font = self.settings.families[0] 

98 return self.FONT_FALLBACKS.get( 

99 primary_font, f"'{primary_font}', sans-serif" 

100 ) 

101 elif font_type == "secondary" and len(self.settings.families) > 1: 

102 secondary_font = self.settings.families[1] 

103 return self.FONT_FALLBACKS.get(secondary_font, f"'{secondary_font}', serif") 

104 if font_type in self.FONT_FALLBACKS: 

105 return self.FONT_FALLBACKS[font_type] 

106 # Default fallbacks 

107 return { 

108 "primary": "-apple-system, BlinkMacSystemFont, sans-serif", 

109 "secondary": "Georgia, serif", 

110 "monospace": "'Source Code Pro', monospace", 

111 "heading": "-apple-system, BlinkMacSystemFont, sans-serif", 

112 "body": "-apple-system, BlinkMacSystemFont, sans-serif", 

113 }.get(font_type, "inherit") 

114 

115 def _build_families_param(self) -> str: 

116 """Build the families parameter for Google Fonts URL.""" 

117 family_strings = [] 

118 

119 for family in self.settings.families: 

120 # Encode family name 

121 encoded_family = quote_plus(family) 

122 

123 # Add weights if specified 

124 if self.settings.weights: 

125 weights_str = ";".join( 

126 [f"wght@{weight}" for weight in self.settings.weights] 

127 ) 

128 family_strings.append(f"{encoded_family}:ital,{weights_str}") 

129 else: 

130 family_strings.append(encoded_family) 

131 

132 return "&family=".join(family_strings) 

133 

134 def get_css_variables(self) -> str: 

135 """Generate CSS custom properties for fonts.""" 

136 variables = [] 

137 

138 if self.settings.families: 

139 primary_font = self.get_font_family("primary") 

140 variables.append(f" --font-primary: {primary_font};") 

141 

142 if len(self.settings.families) > 1: 

143 secondary_font = self.get_font_family("secondary") 

144 variables.append(f" --font-secondary: {secondary_font};") 

145 

146 # Add weight variables 

147 if self.settings.weights: 

148 for weight in self.settings.weights: 

149 var_name = ( 

150 "normal" 

151 if weight == "400" 

152 else ("bold" if weight == "700" else f"weight-{weight}") 

153 ) 

154 variables.append(f" --font-weight-{var_name}: {weight};") 

155 

156 if variables: 

157 return ":root {\n" + "\n".join(variables) + "\n}" 

158 return "" 

159 

160 def get_font_preload(self, font_family: str, weight: str = "400") -> str: 

161 """Generate font preload link for critical fonts.""" 

162 # This would need actual font file URLs, which require API access 

163 # For now, return a basic structure 

164 encoded_family = quote_plus(font_family) 

165 return f'<link rel="preload" as="font" type="font/woff2" href="https://fonts.gstatic.com/s/{encoded_family.lower()}/..." crossorigin>' 

166 

167 def get_font_face_declarations(self) -> str: 

168 """Generate @font-face declarations for local hosting (if API key available).""" 

169 if not self.settings.api_key: 

170 return "<!-- API key required for local font hosting -->" 

171 

172 # This would integrate with Google Fonts API to get actual font file URLs 

173 # For now, return a placeholder 

174 declarations = [] 

175 for family in self.settings.families: 

176 for weight in self.settings.weights: 

177 declarations.append(f""" 

178@font-face {{ 

179 font-family: '{family}'; 

180 font-style: normal; 

181 font-weight: {weight}; 

182 font-display: {self.settings.display}; 

183 src: url('...') format('woff2'); 

184}}""") 

185 

186 return "\n".join(declarations) 

187 

188 def validate_font_availability(self, font_family: str) -> bool: 

189 """Check if a font family is available in Google Fonts.""" 

190 # This would require API integration 

191 # For now, return True for common fonts 

192 common_fonts = { 

193 "Roboto", 

194 "Open Sans", 

195 "Lato", 

196 "Montserrat", 

197 "Source Sans Pro", 

198 "Inter", 

199 "Poppins", 

200 "Nunito", 

201 "Playfair Display", 

202 "Merriweather", 

203 "Lora", 

204 "Source Serif Pro", 

205 "Fira Code", 

206 "Source Code Pro", 

207 } 

208 return font_family in common_fonts 

209 

210 async def get_optimized_import( 

211 self, critical_fonts: list[str] | None = None 

212 ) -> str: 

213 """Generate optimized font import with critical font prioritization.""" 

214 if critical_fonts: 

215 # Prioritize critical fonts in the import order 

216 prioritized_families = [] 

217 remaining_families = self.settings.families.copy() 

218 

219 for critical in critical_fonts: 

220 if critical in remaining_families: 

221 prioritized_families.append(critical) 

222 remaining_families.remove(critical) 

223 

224 # Add remaining fonts 

225 prioritized_families.extend(remaining_families) 

226 

227 # Temporarily override families for this import 

228 original_families = self.settings.families 

229 self.settings.families = prioritized_families 

230 

231 import_html = await self.get_font_import() 

232 

233 # Restore original families 

234 self.settings.families = original_families 

235 

236 return import_html 

237 

238 return await self.get_font_import() 

239 

240 

241FontsSettings = GoogleFontsSettings 

242Fonts = GoogleFonts 

243 

244depends.set(Fonts, "google")