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
« 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."""
3from typing import Any
5from acb.depends import depends
8async def async_image_url(image_id: str, **transformations: Any) -> str:
9 """Generate image URL with transformations using configured image adapter (async).
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
19 # Fallback to basic URL
20 return image_id
23async def async_font_import() -> str:
24 """Generate font import statements using configured font adapter (async).
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 ""
36 # Fallback - no custom fonts
37 return ""
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).
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)
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}"')
73 return f"<img {' '.join(attr_parts)}>"
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}"')
81 return f"<img {' '.join(attr_parts)}>"
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).
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}">'
101 # Generate srcset with different sizes
102 srcset_parts = []
103 src_url = image_id # Default fallback
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")
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
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 ]
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 )
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}"')
141 return f"<img {' '.join(attr_parts)}>"
144async def async_optimized_font_stack() -> str:
145 """Generate optimized font stack with preload and fallback support (async).
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 ""
156 font_html_parts = []
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)
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)
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>")
176 return "\n".join(font_html_parts)
179async def async_critical_css_fonts(critical_fonts: list[str] | None = None) -> str:
180 """Generate critical font CSS for above-the-fold content (async).
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 ""
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 ""
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 ""
201 return ""
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).
213 Usage in templates:
214 [[ await async_image_placeholder(400, 300, 'Loading...') ]]
215 """
216 images = depends.get("images")
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 )
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('#')}"
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).
246 Usage in templates:
247 [[ await async_lazy_image('hero.jpg', 'Hero Image', loading='lazy') ]]
248 """
249 images = depends.get("images")
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
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 )
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 ]
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}"')
285 return f"<img {' '.join(attr_parts)}>"
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}