Coverage for fastblocks/adapters/templates/async_filters.py: 11%

102 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-09-28 18:13 -0700

1"""Async versions of Jinja2 custom filters for FastBlocks adapter integration.""" 

2 

3from typing import Any 

4 

5from acb.depends import depends 

6 

7 

8async def async_image_url(image_id: str, **transformations: Any) -> str: 

9 """Generate image URL with transformations using configured image adapter (async). 

10 

11 Usage in templates: 

12 [[ await async_image_url('product.jpg', width=300, height=200, crop='fill') ]] 

13 """ 

14 images = depends.get("images") 

15 if images and hasattr(images, "get_image_url"): 

16 result = await images.get_image_url(image_id, **transformations) 

17 return str(result) if result is not None else image_id 

18 

19 # Fallback to basic URL 

20 return image_id 

21 

22 

23async def async_font_import() -> str: 

24 """Generate font import statements using configured font adapter (async). 

25 

26 Usage in templates: 

27 [% block head %] 

28 [[ await async_font_import() ]] 

29 [% endblock %] 

30 """ 

31 fonts = depends.get("fonts") 

32 if fonts and hasattr(fonts, "get_font_import"): 

33 result = await fonts.get_font_import() 

34 return str(result) if result is not None else "" 

35 

36 # Fallback - no custom fonts 

37 return "" 

38 

39 

40async def async_image_with_transformations( 

41 image_id: str, 

42 alt: str, 

43 transformations: dict[str, Any] | None = None, 

44 **attributes: Any, 

45) -> str: 

46 """Generate image tag with on-demand transformations (async). 

47 

48 Usage in templates: 

49 [[ await async_image_with_transformations('hero.jpg', 'Hero Image', 

50 {'width': 1200, 'quality': 80}, 

51 class='hero-img') ]] 

52 """ 

53 images = depends.get("images") 

54 if images: 

55 # Get transformed URL first 

56 transform_params = transformations or {} 

57 image_url = await images.get_image_url(image_id, **transform_params) 

58 

59 # Build img tag with transformed URL 

60 attr_parts = [f'src="{image_url}"', f'alt="{alt}"'] 

61 for key, value in attributes.items(): 

62 if key in [ 

63 "width", 

64 "height", 

65 "class", 

66 "id", 

67 "style", 

68 "loading", 

69 "decoding", 

70 ]: 

71 attr_parts.append(f'{key}="{value}"') 

72 

73 return f"<img {' '.join(attr_parts)}>" 

74 

75 # Fallback to basic img tag 

76 attr_parts = [f'src="{image_id}"', f'alt="{alt}"'] 

77 for key, value in attributes.items(): 

78 if key in ["width", "height", "class", "id", "style", "loading", "decoding"]: 

79 attr_parts.append(f'{key}="{value}"') 

80 

81 return f"<img {' '.join(attr_parts)}>" 

82 

83 

84async def async_responsive_image( 

85 image_id: str, alt: str, sizes: dict[str, dict[str, Any]], **attributes: Any 

86) -> str: 

87 """Generate responsive image with multiple sizes using image adapter (async). 

88 

89 Usage in templates: 

90 [[ await async_responsive_image('article.jpg', 'Article Image', { 

91 'mobile': {'width': 400, 'quality': 75}, 

92 'tablet': {'width': 800, 'quality': 80}, 

93 'desktop': {'width': 1200, 'quality': 85} 

94 }) ]] 

95 """ 

96 images = depends.get("images") 

97 if not images: 

98 # Fallback to basic img tag 

99 return f'<img src="{image_id}" alt="{alt}">' 

100 

101 # Generate srcset with different sizes 

102 srcset_parts = [] 

103 src_url = image_id # Default fallback 

104 

105 for size_name, size_params in sizes.items(): 

106 width = size_params.get("width", 400) 

107 size_url = await images.get_image_url(image_id, **size_params) 

108 srcset_parts.append(f"{size_url} {width}w") 

109 

110 # Use the largest size as default src 

111 if width > int(src_url.split("w")[0] if "w" in str(src_url) else "0"): 

112 src_url = size_url 

113 

114 # Build img tag with srcset 

115 attr_parts = [ 

116 f'src="{src_url}"', 

117 f'alt="{alt}"', 

118 f'srcset="{", ".join(srcset_parts)}"', 

119 ] 

120 

121 # Add sizes attribute if not provided 

122 if "sizes" not in attributes: 

123 # Default responsive sizes 

124 attr_parts.append( 

125 'sizes="(max-width: 480px) 400px, (max-width: 768px) 800px, 1200px"' 

126 ) 

127 

128 for key, value in attributes.items(): 

129 if key in [ 

130 "width", 

131 "height", 

132 "class", 

133 "id", 

134 "style", 

135 "loading", 

136 "decoding", 

137 "sizes", 

138 ]: 

139 attr_parts.append(f'{key}="{value}"') 

140 

141 return f"<img {' '.join(attr_parts)}>" 

142 

143 

144async def async_optimized_font_stack() -> str: 

145 """Generate optimized font stack with preload and fallback support (async). 

146 

147 Usage in templates: 

148 [% block head %] 

149 [[ await async_optimized_font_stack() ]] 

150 [% endblock %] 

151 """ 

152 fonts = depends.get("fonts") 

153 if not fonts: 

154 return "" 

155 

156 font_html_parts = [] 

157 

158 # Get font imports 

159 if hasattr(fonts, "get_font_import"): 

160 font_imports = await fonts.get_font_import() 

161 if font_imports: 

162 font_html_parts.append(font_imports) 

163 

164 # Get preload links for critical fonts 

165 if hasattr(fonts, "get_preload_links"): 

166 preload_links = fonts.get_preload_links() 

167 if preload_links: 

168 font_html_parts.append(preload_links) 

169 

170 # Get CSS variables if available 

171 if hasattr(fonts, "get_css_variables"): 

172 css_vars = fonts.get_css_variables() 

173 if css_vars: 

174 font_html_parts.append(f"<style>\n{css_vars}\n</style>") 

175 

176 return "\n".join(font_html_parts) 

177 

178 

179async def async_critical_css_fonts(critical_fonts: list[str] | None = None) -> str: 

180 """Generate critical font CSS for above-the-fold content (async). 

181 

182 Usage in templates: 

183 [% block head %] 

184 [[ await async_critical_css_fonts(['Inter', 'Roboto']) ]] 

185 [% endblock %] 

186 """ 

187 fonts = depends.get("fonts") 

188 if not fonts: 

189 return "" 

190 

191 # If the adapter supports optimized imports with critical font prioritization 

192 if hasattr(fonts, "get_optimized_import"): 

193 result = await fonts.get_optimized_import(critical_fonts) 

194 return str(result) if result is not None else "" 

195 

196 # Fallback to regular import 

197 if hasattr(fonts, "get_font_import"): 

198 result = await fonts.get_font_import() 

199 return str(result) if result is not None else "" 

200 

201 return "" 

202 

203 

204async def async_image_placeholder( 

205 width: int, 

206 height: int, 

207 text: str = "", 

208 bg_color: str = "#f0f0f0", 

209 text_color: str = "#666666", 

210) -> str: 

211 """Generate placeholder image URL for loading states (async). 

212 

213 Usage in templates: 

214 [[ await async_image_placeholder(400, 300, 'Loading...') ]] 

215 """ 

216 images = depends.get("images") 

217 

218 # If the image adapter supports placeholder generation 

219 if images and hasattr(images, "get_placeholder_url"): 

220 result = await images.get_placeholder_url( 

221 width=width, 

222 height=height, 

223 text=text, 

224 bg_color=bg_color, 

225 text_color=text_color, 

226 ) 

227 return ( 

228 str(result) 

229 if result is not None 

230 else f"https://via.placeholder.com/{width}x{height}" 

231 ) 

232 

233 # Fallback to data URL or placeholder service 

234 if text: 

235 placeholder_text = text.replace(" ", "%20") 

236 return f"https://via.placeholder.com/{width}x{height}/{bg_color.lstrip('#')}/{text_color.lstrip('#')}?text={placeholder_text}" 

237 else: 

238 return f"https://via.placeholder.com/{width}x{height}/{bg_color.lstrip('#')}" 

239 

240 

241async def async_lazy_image( 

242 image_id: str, alt: str, placeholder_url: str | None = None, **attributes: Any 

243) -> str: 

244 """Generate lazy-loading image with proper placeholder (async). 

245 

246 Usage in templates: 

247 [[ await async_lazy_image('hero.jpg', 'Hero Image', loading='lazy') ]] 

248 """ 

249 images = depends.get("images") 

250 

251 # Get the actual image URL 

252 if images and hasattr(images, "get_image_url"): 

253 result = await images.get_image_url(image_id, **attributes) 

254 actual_url = str(result) if result is not None else image_id 

255 else: 

256 actual_url = image_id 

257 

258 # Generate placeholder if not provided 

259 if not placeholder_url: 

260 placeholder_url = await async_image_placeholder( 

261 width=attributes.get("width", 400), 

262 height=attributes.get("height", 300), 

263 text="Loading...", 

264 ) 

265 

266 # Build lazy-loading img tag 

267 attr_parts = [ 

268 f'src="{placeholder_url}"', 

269 f'data-src="{actual_url}"', 

270 f'alt="{alt}"', 

271 'loading="lazy"', 

272 ] 

273 

274 for key, value in attributes.items(): 

275 if key not in ["width", "height"] and key in [ 

276 "class", 

277 "id", 

278 "style", 

279 "decoding", 

280 ]: 

281 attr_parts.append(f'{key}="{value}"') 

282 elif key in ["width", "height"]: 

283 attr_parts.append(f'{key}="{value}"') 

284 

285 return f"<img {' '.join(attr_parts)}>" 

286 

287 

288# Async filter registration mapping 

289FASTBLOCKS_ASYNC_FILTERS = { 

290 "async_image_url": async_image_url, 

291 "async_font_import": async_font_import, 

292 "async_image_with_transformations": async_image_with_transformations, 

293 "async_responsive_image": async_responsive_image, 

294 "async_optimized_font_stack": async_optimized_font_stack, 

295 "async_critical_css_fonts": async_critical_css_fonts, 

296 "async_image_placeholder": async_image_placeholder, 

297 "async_lazy_image": async_lazy_image, 

298}