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
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-26 03:30 -0800
1"""Google Fonts adapter implementation."""
3from contextlib import suppress
4from urllib.parse import quote_plus
5from uuid import UUID
7from acb.depends import depends
9from ._base import FontsBase, FontsBaseSettings
12class GoogleFontsSettings(FontsBaseSettings):
13 """Google Fonts-specific settings."""
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
23class GoogleFonts(FontsBase):
24 """Google Fonts adapter implementation."""
26 # Required ACB 0.19.0+ metadata
27 MODULE_ID: UUID = UUID("01937d86-4f2a-7b3c-8d9e-f3b4d3c2e1a1") # Static UUID7
28 MODULE_STATUS = "stable"
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 }
52 def __init__(self) -> None:
53 """Initialize Google Fonts adapter."""
54 super().__init__()
55 self.settings = GoogleFontsSettings()
57 # Register with ACB dependency system
58 with suppress(Exception):
59 depends.set(self)
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()
66 # Build query parameters
67 params = [f"family={families_param}"]
69 if self.settings.subsets:
70 subsets = "&".join(self.settings.subsets)
71 params.extend((f"subset={subsets}", f"display={self.settings.display}"))
73 query_string = "&".join(params)
74 url = f"https://fonts.googleapis.com/css2?{query_string}"
76 # Generate link tags
77 links = []
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 )
88 # Add the main stylesheet link
89 links.append(f'<link rel="stylesheet" href="{url}">')
91 return "\n".join(links)
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")
115 def _build_families_param(self) -> str:
116 """Build the families parameter for Google Fonts URL."""
117 family_strings = []
119 for family in self.settings.families:
120 # Encode family name
121 encoded_family = quote_plus(family)
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)
132 return "&family=".join(family_strings)
134 def get_css_variables(self) -> str:
135 """Generate CSS custom properties for fonts."""
136 variables = []
138 if self.settings.families:
139 primary_font = self.get_font_family("primary")
140 variables.append(f" --font-primary: {primary_font};")
142 if len(self.settings.families) > 1:
143 secondary_font = self.get_font_family("secondary")
144 variables.append(f" --font-secondary: {secondary_font};")
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};")
156 if variables:
157 return ":root {\n" + "\n".join(variables) + "\n}"
158 return ""
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>'
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 -->"
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}}""")
186 return "\n".join(declarations)
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
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()
219 for critical in critical_fonts:
220 if critical in remaining_families:
221 prioritized_families.append(critical)
222 remaining_families.remove(critical)
224 # Add remaining fonts
225 prioritized_families.extend(remaining_families)
227 # Temporarily override families for this import
228 original_families = self.settings.families
229 self.settings.families = prioritized_families
231 import_html = await self.get_font_import()
233 # Restore original families
234 self.settings.families = original_families
236 return import_html
238 return await self.get_font_import()
241FontsSettings = GoogleFontsSettings
242Fonts = GoogleFonts
244depends.set(Fonts, "google")