Coverage for fastblocks/adapters/fonts/google.py: 89%
98 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"""Google Fonts adapter implementation."""
3from contextlib import suppress
4from urllib.parse import quote_plus
5from uuid import UUID
7from acb.config import Settings # type: ignore[attr-defined]
8from acb.depends import depends
10from ._base import FontsBase
13class GoogleFontsSettings(Settings): # type: ignore[misc]
14 """Google Fonts-specific settings."""
16 api_key: str | None = None # Optional API key for advanced features
17 families: list[str] = ["Roboto", "Open Sans"]
18 weights: list[str] = ["400", "700"]
19 subsets: list[str] = ["latin"]
20 display: str = "swap" # font-display CSS property
21 preconnect: bool = True # Add preconnect link for performance
24class GoogleFontsAdapter(FontsBase):
25 """Google Fonts adapter implementation."""
27 # Required ACB 0.19.0+ metadata
28 MODULE_ID: UUID = UUID("01937d86-4f2a-7b3c-8d9e-f3b4d3c2e1a1") # Static UUID7
29 MODULE_STATUS = "stable"
31 # Common Google Fonts families with fallbacks
32 FONT_FALLBACKS = {
33 "Roboto": "Roboto, -apple-system, BlinkMacSystemFont, sans-serif",
34 "Open Sans": "'Open Sans', -apple-system, BlinkMacSystemFont, sans-serif",
35 "Lato": "Lato, -apple-system, BlinkMacSystemFont, sans-serif",
36 "Montserrat": "Montserrat, -apple-system, BlinkMacSystemFont, sans-serif",
37 "Source Sans Pro": "'Source Sans Pro', -apple-system, BlinkMacSystemFont, sans-serif",
38 "Inter": "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
39 "Poppins": "Poppins, -apple-system, BlinkMacSystemFont, sans-serif",
40 "Nunito": "Nunito, -apple-system, BlinkMacSystemFont, sans-serif",
41 "Playfair Display": "'Playfair Display', Georgia, serif",
42 "Merriweather": "Merriweather, Georgia, serif",
43 "Lora": "Lora, Georgia, serif",
44 "Source Serif Pro": "'Source Serif Pro', Georgia, serif",
45 "Crimson Text": "'Crimson Text', Georgia, serif",
46 "PT Serif": "'PT Serif', Georgia, serif",
47 "Fira Code": "'Fira Code', 'Source Code Pro', monospace",
48 "Source Code Pro": "'Source Code Pro', 'Courier New', monospace",
49 "JetBrains Mono": "'JetBrains Mono', 'Source Code Pro', monospace",
50 "Inconsolata": "Inconsolata, 'Courier New', monospace",
51 }
53 def __init__(self) -> None:
54 """Initialize Google Fonts adapter."""
55 super().__init__()
56 self.settings = GoogleFontsSettings()
58 # Register with ACB dependency system
59 with suppress(Exception):
60 depends.set(self)
62 async def get_font_import(self) -> str:
63 """Generate Google Fonts import statements."""
64 # Build font families parameter
65 families_param = self._build_families_param()
67 # Build query parameters
68 params = [f"family={families_param}"]
70 if self.settings.subsets:
71 subsets = "&".join(self.settings.subsets)
72 params.extend((f"subset={subsets}", f"display={self.settings.display}"))
74 query_string = "&".join(params)
75 url = f"https://fonts.googleapis.com/css2?{query_string}"
77 # Generate link tags
78 links = []
80 # Add preconnect for performance
81 if self.settings.preconnect:
82 links.extend(
83 (
84 '<link rel="preconnect" href="https://fonts.googleapis.com">',
85 '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>',
86 )
87 )
89 # Add the main stylesheet link
90 links.append(f'<link rel="stylesheet" href="{url}">')
92 return "\n".join(links)
94 def get_font_family(self, font_type: str) -> str:
95 """Get font family CSS values with fallbacks."""
96 # Map font types to families
97 if font_type == "primary" and self.settings.families:
98 primary_font = self.settings.families[0]
99 return self.FONT_FALLBACKS.get(
100 primary_font, f"'{primary_font}', sans-serif"
101 )
102 elif font_type == "secondary" and len(self.settings.families) > 1:
103 secondary_font = self.settings.families[1]
104 return self.FONT_FALLBACKS.get(secondary_font, f"'{secondary_font}', serif")
105 if font_type in self.FONT_FALLBACKS:
106 return self.FONT_FALLBACKS[font_type]
107 # Default fallbacks
108 return {
109 "primary": "-apple-system, BlinkMacSystemFont, sans-serif",
110 "secondary": "Georgia, serif",
111 "monospace": "'Source Code Pro', monospace",
112 "heading": "-apple-system, BlinkMacSystemFont, sans-serif",
113 "body": "-apple-system, BlinkMacSystemFont, sans-serif",
114 }.get(font_type, "inherit")
116 def _build_families_param(self) -> str:
117 """Build the families parameter for Google Fonts URL."""
118 family_strings = []
120 for family in self.settings.families:
121 # Encode family name
122 encoded_family = quote_plus(family)
124 # Add weights if specified
125 if self.settings.weights:
126 weights_str = ";".join(
127 [f"wght@{weight}" for weight in self.settings.weights]
128 )
129 family_strings.append(f"{encoded_family}:ital,{weights_str}")
130 else:
131 family_strings.append(encoded_family)
133 return "&family=".join(family_strings)
135 def get_css_variables(self) -> str:
136 """Generate CSS custom properties for fonts."""
137 variables = []
139 if self.settings.families:
140 primary_font = self.get_font_family("primary")
141 variables.append(f" --font-primary: {primary_font};")
143 if len(self.settings.families) > 1:
144 secondary_font = self.get_font_family("secondary")
145 variables.append(f" --font-secondary: {secondary_font};")
147 # Add weight variables
148 if self.settings.weights:
149 for weight in self.settings.weights:
150 var_name = (
151 "normal"
152 if weight == "400"
153 else ("bold" if weight == "700" else f"weight-{weight}")
154 )
155 variables.append(f" --font-weight-{var_name}: {weight};")
157 if variables:
158 return ":root {\n" + "\n".join(variables) + "\n}"
159 return ""
161 def get_font_preload(self, font_family: str, weight: str = "400") -> str:
162 """Generate font preload link for critical fonts."""
163 # This would need actual font file URLs, which require API access
164 # For now, return a basic structure
165 encoded_family = quote_plus(font_family)
166 return f'<link rel="preload" as="font" type="font/woff2" href="https://fonts.gstatic.com/s/{encoded_family.lower()}/..." crossorigin>'
168 def get_font_face_declarations(self) -> str:
169 """Generate @font-face declarations for local hosting (if API key available)."""
170 if not self.settings.api_key:
171 return "<!-- API key required for local font hosting -->"
173 # This would integrate with Google Fonts API to get actual font file URLs
174 # For now, return a placeholder
175 declarations = []
176 for family in self.settings.families:
177 for weight in self.settings.weights:
178 declarations.append(f"""
179@font-face {{
180 font-family: '{family}';
181 font-style: normal;
182 font-weight: {weight};
183 font-display: {self.settings.display};
184 src: url('...') format('woff2');
185}}""")
187 return "\n".join(declarations)
189 def validate_font_availability(self, font_family: str) -> bool:
190 """Check if a font family is available in Google Fonts."""
191 # This would require API integration
192 # For now, return True for common fonts
193 common_fonts = {
194 "Roboto",
195 "Open Sans",
196 "Lato",
197 "Montserrat",
198 "Source Sans Pro",
199 "Inter",
200 "Poppins",
201 "Nunito",
202 "Playfair Display",
203 "Merriweather",
204 "Lora",
205 "Source Serif Pro",
206 "Fira Code",
207 "Source Code Pro",
208 }
209 return font_family in common_fonts
211 async def get_optimized_import(
212 self, critical_fonts: list[str] | None = None
213 ) -> str:
214 """Generate optimized font import with critical font prioritization."""
215 if critical_fonts:
216 # Prioritize critical fonts in the import order
217 prioritized_families = []
218 remaining_families = self.settings.families.copy()
220 for critical in critical_fonts:
221 if critical in remaining_families:
222 prioritized_families.append(critical)
223 remaining_families.remove(critical)
225 # Add remaining fonts
226 prioritized_families.extend(remaining_families)
228 # Temporarily override families for this import
229 original_families = self.settings.families
230 self.settings.families = prioritized_families
232 import_html = await self.get_font_import()
234 # Restore original families
235 self.settings.families = original_families
237 return import_html
239 return await self.get_font_import()