Coverage for fastblocks/adapters/templates/filters.py: 37%
164 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"""Jinja2 custom filters for FastBlocks adapter integration."""
3from typing import Any
5from acb.depends import depends
8def img_tag(image_id: str, alt: str, **attributes: Any) -> str:
9 """Generate image tag using configured image adapter.
11 Usage in templates:
12 [[ img_tag('product.jpg', 'Product Image', width=300, class='responsive') ]]
13 """
14 images = depends.get("images")
15 if images:
16 result = images.get_img_tag(image_id, alt, **attributes)
17 return (
18 str(result) if result is not None else f'<img src="{image_id}" alt="{alt}">'
19 )
21 # Fallback to basic img tag
22 attr_parts = [f'src="{image_id}"', f'alt="{alt}"']
23 for key, value in attributes.items():
24 if key in ["width", "height", "class", "id", "style"]:
25 attr_parts.append(f'{key}="{value}"')
27 return f"<img {' '.join(attr_parts)}>"
30def image_url(image_id: str, **transformations: Any) -> str:
31 """Generate image URL with transformations using configured image adapter.
33 Note: For full functionality with transformations, use async_image_url in async templates.
35 Usage in templates:
36 [[ image_url('product.jpg', width=300, height=200, crop='fill') ]]
37 [[ await async_image_url('product.jpg', width=300, height=200, crop='fill') ]] # async version
38 """
39 images = depends.get("images")
40 if images and hasattr(images, "get_sync_image_url"):
41 # Some adapters may provide sync methods for simple URLs
42 result = images.get_sync_image_url(image_id, **transformations)
43 return str(result) if result is not None else image_id
44 elif images:
45 # Return base URL with query parameters as fallback
46 if transformations:
47 params = "&".join([f"{k}={v}" for k, v in transformations.items()])
48 return f"{image_id}?{params}"
49 return image_id
51 # Fallback to basic URL
52 return image_id
55def style_class(component: str, **modifiers: Any) -> str:
56 """Get style framework class for component.
58 Usage in templates:
59 [[ style_class('button', variant='primary', size='large') ]]
60 """
61 styles = depends.get("styles")
62 if styles:
63 base_class = styles.get_component_class(component)
64 base_class = (
65 str(base_class) if base_class is not None else component.replace("_", "-")
66 )
68 # Apply modifiers if the adapter supports it
69 if hasattr(styles, "get_utility_classes"):
70 utilities = styles.get_utility_classes()
71 if utilities:
72 for modifier, value in modifiers.items():
73 utility_key = f"{modifier}_{value}"
74 if utility_key in utilities:
75 utility_class = utilities[utility_key]
76 if utility_class:
77 base_class = f"{base_class} {utility_class}"
79 return base_class
81 # Fallback to semantic class name
82 return component.replace("_", "-")
85def icon_tag(icon_name: str, **attributes: Any) -> str:
86 """Generate icon tag using configured icon adapter.
88 Usage in templates:
89 [[ icon_tag('home', class='nav-icon', size='24') ]]
90 """
91 icons = depends.get("icons")
92 if icons:
93 result = icons.get_icon_tag(icon_name, **attributes)
94 return str(result) if result is not None else f"[{icon_name}]"
96 # Fallback to text placeholder
97 return f"[{icon_name}]"
100def icon_with_text(
101 icon_name: str, text: str, position: str = "left", **attributes: Any
102) -> str:
103 """Generate icon with text using configured icon adapter.
105 Usage in templates:
106 [[ icon_with_text('save', 'Save Changes', position='left') ]]
107 """
108 icons = depends.get("icons")
109 if icons and hasattr(icons, "get_icon_with_text"):
110 result = icons.get_icon_with_text(icon_name, text, position, **attributes)
111 return (
112 str(result)
113 if result is not None
114 else f"{icon_tag(icon_name, **attributes)} {text}"
115 )
117 # Fallback implementation
118 icon = icon_tag(icon_name, **attributes)
119 if position == "right":
120 return f"{text} {icon}"
121 else:
122 return f"{icon} {text}"
125def font_import() -> str:
126 """Generate font import statements using configured font adapter.
128 Note: For full functionality, use async_font_import in async templates.
130 Usage in templates:
131 [% block head %]
132 [[ font_import() ]]
133 [[ await async_font_import() ]] # async version for full functionality
134 [% endblock %]
135 """
136 fonts = depends.get("fonts")
137 if fonts and hasattr(fonts, "get_sync_font_import"):
138 # Some adapters may provide sync methods for basic imports
139 result = fonts.get_sync_font_import()
140 return str(result) if result is not None else ""
141 elif fonts:
142 # Return basic stylesheet links if available
143 if hasattr(fonts, "get_stylesheet_links"):
144 links = fonts.get_stylesheet_links()
145 return "\n".join(links) if links else ""
147 # Fallback - no custom fonts
148 return ""
151def font_family(font_type: str = "primary") -> str:
152 """Get font family CSS value using configured font adapter.
154 Usage in templates:
155 <style>
156 body { font-family: [[ font_family('primary') ]]; }
157 h1 { font-family: [[ font_family('heading') ]]; }
158 </style>
159 """
160 fonts = depends.get("fonts")
161 if fonts:
162 result = fonts.get_font_family(font_type)
163 return str(result) if result is not None else "inherit"
165 # Fallback fonts
166 fallbacks = {
167 "primary": "-apple-system, BlinkMacSystemFont, sans-serif",
168 "secondary": "Georgia, serif",
169 "heading": "-apple-system, BlinkMacSystemFont, sans-serif",
170 "body": "-apple-system, BlinkMacSystemFont, sans-serif",
171 "monospace": "'Courier New', monospace",
172 }
173 return fallbacks.get(font_type, "inherit")
176def stylesheet_links() -> str:
177 """Generate all stylesheet links for configured adapters.
179 Usage in templates:
180 [% block head %]
181 [[ stylesheet_links() ]]
182 [% endblock %]
183 """
184 links = []
186 # Get style framework links
187 styles = depends.get("styles")
188 if styles:
189 links.extend(styles.get_stylesheet_links())
191 # Get icon framework links
192 icons = depends.get("icons")
193 if icons and hasattr(icons, "get_stylesheet_links"):
194 links.extend(icons.get_stylesheet_links())
196 return "\n".join(links)
199def component_html(component: str, content: str = "", **attributes: Any) -> str:
200 """Generate complete HTML component using style adapter.
202 Usage in templates:
203 [[ component_html('button', 'Click Me', variant='primary', class='my-btn') ]]
204 """
205 styles = depends.get("styles")
206 if styles and hasattr(styles, "build_component_html"):
207 result = styles.build_component_html(component, content, **attributes)
208 return (
209 str(result)
210 if result is not None
211 else f'<div class="{component}">{content}</div>'
212 )
214 # Fallback to basic HTML
215 css_class = style_class(component)
216 if "class" in attributes:
217 css_class = f"{css_class} {attributes.pop('class')}"
219 attr_parts = [f'class="{css_class}"']
220 for key, value in attributes.items():
221 attr_parts.append(f'{key}="{value}"')
223 attrs_str = " ".join(attr_parts)
225 if component.startswith("button"):
226 return f"<button {attrs_str}>{content}</button>"
227 else:
228 return f"<div {attrs_str}>{content}</div>"
231def htmx_attrs(**htmx_attributes: Any) -> str:
232 """Generate HTMX attributes for enhanced interactivity.
234 Usage in templates:
235 <button [[ htmx_attrs(get='/api/data', target='#content', swap='innerHTML') ]]>
236 Load Data
237 </button>
238 """
239 attr_parts = []
241 # Map common HTMX attributes with enhanced support
242 attr_mapping = {
243 "get": "hx-get",
244 "post": "hx-post",
245 "put": "hx-put",
246 "delete": "hx-delete",
247 "patch": "hx-patch",
248 "target": "hx-target",
249 "swap": "hx-swap",
250 "trigger": "hx-trigger",
251 "indicator": "hx-indicator",
252 "confirm": "hx-confirm",
253 "vals": "hx-vals",
254 "headers": "hx-headers",
255 "include": "hx-include",
256 "params": "hx-params",
257 "boost": "hx-boost",
258 "push_url": "hx-push-url",
259 "replace_url": "hx-replace-url",
260 "ext": "hx-ext",
261 "select": "hx-select",
262 "select_oob": "hx-select-oob",
263 "sync": "hx-sync",
264 "history": "hx-history",
265 "disabled_elt": "hx-disabled-elt",
266 "encoding": "hx-encoding",
267 "preserve": "hx-preserve",
268 }
270 for key, value in htmx_attributes.items():
271 htmx_attr = attr_mapping.get(key, f"hx-{key.replace('_', '-')}")
272 attr_parts.append(f'{htmx_attr}="{value}"')
274 return " ".join(attr_parts)
277def htmx_component(component_type: str, **attributes: Any) -> str:
278 """Generate HTMX-enabled components with adapter integration.
280 Usage in templates:
281 <div [[ htmx_component('card', get='/api/details/{id}', target='#details') ]]>
282 [[ component_html('card-header', 'Title') ]]
283 <div id="details"></div>
284 </div>
285 """
286 # Extract HTMX attributes
287 htmx_attrs_dict = {}
288 component_attrs = {}
290 for key, value in attributes.items():
291 if key in [
292 "get",
293 "post",
294 "put",
295 "delete",
296 "patch",
297 "target",
298 "swap",
299 "trigger",
300 "indicator",
301 "confirm",
302 "vals",
303 "headers",
304 "include",
305 "params",
306 "boost",
307 "push_url",
308 "replace_url",
309 "ext",
310 "select",
311 "select_oob",
312 "sync",
313 "history",
314 "disabled_elt",
315 "encoding",
316 "preserve",
317 ]:
318 htmx_attrs_dict[key] = value
319 else:
320 component_attrs[key] = value
322 # Get component styling from style adapter
323 css_class = style_class(component_type, **component_attrs)
325 # Add HTMX attributes if any
326 htmx_str = htmx_attrs(**htmx_attrs_dict) if htmx_attrs_dict else ""
328 # Build complete attribute string
329 attr_parts = [f'class="{css_class}"']
330 if htmx_str:
331 attr_parts.append(htmx_str)
333 return " ".join(attr_parts)
336def htmx_form(action: str, **attributes: Any) -> str:
337 """Generate HTMX-enabled forms with validation and feedback.
339 Usage in templates:
340 <form [[ htmx_form('/users/create', target='#form-container',
341 validation_target='#form-errors') ]]>
342 <!-- form fields -->
343 </form>
344 """
345 # Set default HTMX behavior for forms
346 form_attrs = {
347 "post": action,
348 "swap": "outerHTML",
349 "indicator": "#form-loading",
350 **attributes,
351 }
353 # Handle validation target if specified
354 if "validation_target" in form_attrs:
355 validation_target = form_attrs.pop("validation_target")
356 form_attrs["headers"] = f'{{"HX-Error-Target": "{validation_target}"}}'
358 return htmx_attrs(**form_attrs)
361def htmx_lazy_load(url: str, placeholder: str = "Loading...", **attributes: Any) -> str:
362 """Create lazy-loading containers with intersection observers.
364 Usage in templates:
365 <div [[ htmx_lazy_load('/api/content', 'Loading content...',
366 trigger='revealed once') ]]>
367 </div>
368 """
369 lazy_attrs = {
370 "get": url,
371 "trigger": "revealed once",
372 "indicator": "this",
373 **attributes,
374 }
376 attrs_str = htmx_attrs(**lazy_attrs)
377 return f'{attrs_str} data-placeholder="{placeholder}"'
380def htmx_infinite_scroll(
381 next_url: str, container: str = "#infinite-container", **attributes: Any
382) -> str:
383 """Generate infinite scroll triggers.
385 Usage in templates:
386 <div [[ htmx_infinite_scroll('/api/posts?page=2', '#posts-container') ]]>
387 Loading more posts...
388 </div>
389 """
390 scroll_attrs = {
391 "get": next_url,
392 "trigger": "revealed",
393 "target": container,
394 "swap": "afterend",
395 **attributes,
396 }
398 return htmx_attrs(**scroll_attrs)
401def htmx_search(endpoint: str, debounce: int = 300, **attributes: Any) -> str:
402 """Generate debounced search inputs.
404 Usage in templates:
405 <input type="text" name="q"
406 [[ htmx_search('/api/search', 500, target='#results') ]]>
407 """
408 search_attrs = {
409 "get": endpoint,
410 "trigger": f"keyup changed delay:{debounce}ms",
411 "target": "#search-results",
412 "indicator": "#search-loading",
413 **attributes,
414 }
416 return htmx_attrs(**search_attrs)
419def htmx_modal(content_url: str, **attributes: Any) -> str:
420 """Create modal dialog triggers.
422 Usage in templates:
423 <button [[ htmx_modal('/modal/user/{id}', target='#modal-container') ]]>
424 View Details
425 </button>
426 """
427 modal_attrs = {
428 "get": content_url,
429 "target": "#modal-container",
430 "swap": "innerHTML",
431 **attributes,
432 }
434 return htmx_attrs(**modal_attrs)
437def htmx_img_swap(
438 image_id: str, transformations: dict[str, Any] | None = None, **attributes: Any
439) -> str:
440 """Dynamic image swapping with transformations using image adapter.
442 Usage in templates:
443 <img [[ htmx_img_swap('product.jpg', {'width': 300},
444 trigger='mouseenter once', target='this') ]]>
445 """
446 images = depends.get("images")
447 if not images:
448 return htmx_attrs(**attributes)
450 # Build transformation URL
451 if transformations:
452 # This would typically be handled by the image adapter
453 transform_url = f"/api/images/{image_id}/transform"
454 swap_attrs = {
455 "get": transform_url,
456 "vals": str(transformations),
457 "target": "this",
458 "swap": "outerHTML",
459 **attributes,
460 }
461 else:
462 swap_attrs = {
463 "get": f"/api/images/{image_id}",
464 "target": "this",
465 "swap": "outerHTML",
466 **attributes,
467 }
469 return htmx_attrs(**swap_attrs)
472def htmx_icon_toggle(icon_on: str, icon_off: str, **attributes: Any) -> str:
473 """Icon state toggles for interactive elements.
475 Usage in templates:
476 <button [[ htmx_icon_toggle('heart-filled', 'heart-outline',
477 post='/favorites/toggle/{id}') ]]>
478 [[ icon_tag('heart-outline') ]]
479 </button>
480 """
481 toggle_attrs = {"swap": "outerHTML", "target": "this", **attributes}
483 # Add data attributes for icon states
484 attrs_str = htmx_attrs(**toggle_attrs)
485 return f'{attrs_str} data-icon-on="{icon_on}" data-icon-off="{icon_off}"'
488def htmx_ws_connect(endpoint: str, **attributes: Any) -> str:
489 """Generate WebSocket connection attributes for real-time features.
491 Usage in templates:
492 <div [[ htmx_ws_connect('/ws/notifications',
493 listen='notification-received') ]]>
494 </div>
495 """
496 ws_attrs = {"ext": "ws", **attributes}
498 # Handle WebSocket-specific attributes
499 if "listen" in ws_attrs:
500 listen_event = ws_attrs.pop("listen")
501 attrs_str = htmx_attrs(**ws_attrs)
502 return f'{attrs_str} ws-connect="{endpoint}" sse-listen="{listen_event}"'
503 else:
504 attrs_str = htmx_attrs(**ws_attrs)
505 return f'{attrs_str} ws-connect="{endpoint}"'
508def htmx_validation_feedback(field_name: str, **attributes: Any) -> str:
509 """Generate real-time validation feedback containers.
511 Usage in templates:
512 <input name="email"
513 [[ htmx_validation_feedback('email',
514 validate_url='/validate/email') ]]>
515 """
516 validate_url = attributes.pop("validate_url", f"/validate/{field_name}")
518 validation_attrs = {
519 "get": validate_url,
520 "trigger": "blur, keyup changed delay:500ms",
521 "target": f"#{field_name}-feedback",
522 "include": "this",
523 **attributes,
524 }
526 return htmx_attrs(**validation_attrs)
529def htmx_error_container(container_id: str = "htmx-errors") -> str:
530 """Generate error display containers for HTMX responses.
532 Usage in templates:
533 <div [[ htmx_error_container('form-errors') ]]></div>
534 """
535 return f'id="{container_id}" class="htmx-error-container" role="alert"'
538def htmx_retry_trigger(max_retries: int = 3, backoff: str = "exponential") -> str:
539 """Generate retry mechanisms for failed HTMX requests.
541 Usage in templates:
542 <div [[ htmx_retry_trigger(3, 'exponential') ]]>
543 """
544 return f'data-max-retries="{max_retries}" data-backoff="{backoff}"'
547# Filter registration mapping for template engines
548FASTBLOCKS_FILTERS = {
549 "img_tag": img_tag,
550 "image_url": image_url,
551 "style_class": style_class,
552 "icon_tag": icon_tag,
553 "icon_with_text": icon_with_text,
554 "font_import": font_import,
555 "font_family": font_family,
556 "stylesheet_links": stylesheet_links,
557 "component_html": component_html,
558 "htmx_attrs": htmx_attrs,
559 "htmx_component": htmx_component,
560 "htmx_form": htmx_form,
561 "htmx_lazy_load": htmx_lazy_load,
562 "htmx_infinite_scroll": htmx_infinite_scroll,
563 "htmx_search": htmx_search,
564 "htmx_modal": htmx_modal,
565 "htmx_img_swap": htmx_img_swap,
566 "htmx_icon_toggle": htmx_icon_toggle,
567 "htmx_ws_connect": htmx_ws_connect,
568 "htmx_validation_feedback": htmx_validation_feedback,
569 "htmx_error_container": htmx_error_container,
570 "htmx_retry_trigger": htmx_retry_trigger,
571}