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

237 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-09-29 00:51 -0700

1"""Enhanced Template Filters for Secondary Adapters Integration. 

2 

3This module provides comprehensive template filters for all FastBlocks secondary adapters: 

4- Cloudflare Images integration with transformations 

5- TwicPics integration with smart cropping 

6- WebAwesome icon integration 

7- KelpUI component integration 

8- Phosphor, Heroicons, Remix, Material Icons support 

9- Font loading and optimization 

10- Advanced HTMX integrations 

11 

12Requirements: 

13- All secondary adapter packages as available 

14 

15Author: lesleslie <les@wedgwoodwebworks.com> 

16Created: 2025-01-12 

17""" 

18 

19import typing as t 

20from uuid import UUID 

21 

22from acb.adapters import AdapterStatus 

23from acb.depends import depends 

24 

25 

26# Cloudflare Images Filters 

27def cf_image_url(image_id: str, **transformations: t.Any) -> str: 

28 """Generate Cloudflare Images URL with transformations. 

29 

30 Usage in templates: 

31 [[ cf_image_url('hero.jpg', width=800, quality=85, format='webp') ]] 

32 """ 

33 try: 

34 cloudflare = depends.get("cloudflare_images") 

35 if cloudflare: 

36 result = cloudflare.get_image_url(image_id, **transformations) 

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

38 except Exception: 

39 pass 

40 

41 # Fallback 

42 return image_id 

43 

44 

45def cf_responsive_image( 

46 image_id: str, alt: str, sizes: dict[str, dict[str, t.Any]], **attributes: t.Any 

47) -> str: 

48 """Generate responsive Cloudflare Images with srcset. 

49 

50 Usage in templates: 

51 [[ cf_responsive_image('hero.jpg', 'Hero Image', { 

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

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

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

55 }) ]] 

56 """ 

57 try: 

58 cloudflare = depends.get("cloudflare_images") 

59 if not cloudflare: 

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

61 

62 srcset_parts = [] 

63 src_url = image_id 

64 

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

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

67 size_url = cloudflare.get_image_url(image_id, **size_params) 

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

69 

70 # Use largest as default src 

71 if width > 800: 

72 src_url = size_url 

73 

74 attr_parts = [ 

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

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

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

78 ] 

79 

80 # Add default sizes if not provided 

81 if "sizes" not in attributes: 

82 attr_parts.append( 

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

84 ) 

85 

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

87 if key in ["width", "height", "class", "id", "style", "loading", "sizes"]: 

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

89 

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

91 

92 except Exception: 

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

94 

95 

96# TwicPics Filters 

97def twicpics_image(image_id: str, **transformations: t.Any) -> str: 

98 """Generate TwicPics image URL with smart transformations. 

99 

100 Usage in templates: 

101 [[ twicpics_image('product.jpg', resize='400x300', focus='auto') ]] 

102 """ 

103 try: 

104 twicpics = depends.get("twicpics") 

105 if twicpics: 

106 result = twicpics.get_image_url(image_id, **transformations) 

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

108 except Exception: 

109 pass 

110 

111 return image_id 

112 

113 

114def twicpics_smart_crop( 

115 image_id: str, width: int, height: int, focus: str = "auto", **attributes: t.Any 

116) -> str: 

117 """Generate TwicPics image with smart cropping. 

118 

119 Usage in templates: 

120 [[ twicpics_smart_crop('landscape.jpg', 400, 300, 'face', class='hero-img') ]] 

121 """ 

122 try: 

123 twicpics = depends.get("twicpics") 

124 if twicpics: 

125 transform_params = { 

126 "resize": f"{width}x{height}", 

127 "focus": focus, 

128 **attributes, 

129 } 

130 

131 # Extract img attributes from transform params 

132 img_attrs = {} 

133 transform_only = {} 

134 

135 for key, value in transform_params.items(): 

136 if key in ["class", "id", "style", "loading", "alt"]: 

137 img_attrs[key] = value 

138 else: 

139 transform_only[key] = value 

140 

141 image_url = twicpics.get_image_url(image_id, **transform_only) 

142 

143 attr_parts = [f'src="{image_url}"'] 

144 if "alt" not in img_attrs: 

145 attr_parts.append(f'alt="{image_id}"') 

146 

147 for key, value in img_attrs.items(): 

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

149 

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

151 

152 except Exception: 

153 pass 

154 

155 return f'<img src="{image_id}" alt="{image_id}" width="{width}" height="{height}">' 

156 

157 

158# WebAwesome Icon Filters 

159def wa_icon(icon_name: str, **attributes: t.Any) -> str: 

160 """Generate WebAwesome icon. 

161 

162 Usage in templates: 

163 [[ wa_icon('home', size='24', class='nav-icon') ]] 

164 """ 

165 try: 

166 webawesome = depends.get("webawesome") 

167 if webawesome: 

168 result = webawesome.get_icon_tag(icon_name, **attributes) 

169 return str(result) if result is not None else f"[{icon_name}]" 

170 except Exception: 

171 pass 

172 

173 # Fallback 

174 css_class = attributes.get("class", "") 

175 size = attributes.get("size", "16") 

176 return f'<i class="wa wa-{icon_name} {css_class}" style="font-size: {size}px;"></i>' 

177 

178 

179def wa_icon_with_text( 

180 icon_name: str, text: str, position: str = "left", **attributes: t.Any 

181) -> str: 

182 """Generate WebAwesome icon with text. 

183 

184 Usage in templates: 

185 [[ wa_icon_with_text('save', 'Save Changes', 'left', class='btn-icon') ]] 

186 """ 

187 try: 

188 webawesome = depends.get("webawesome") 

189 if webawesome and hasattr(webawesome, "get_icon_with_text"): 

190 result = webawesome.get_icon_with_text( 

191 icon_name, text, position, **attributes 

192 ) 

193 return ( 

194 str(result) 

195 if result is not None 

196 else f"{wa_icon(icon_name, **attributes)} {text}" 

197 ) 

198 except Exception: 

199 pass 

200 

201 # Fallback 

202 icon = wa_icon(icon_name, **attributes) 

203 if position == "right": 

204 return f"{text} {icon}" 

205 else: 

206 return f"{icon} {text}" 

207 

208 

209# KelpUI Component Filters 

210def kelp_component(component_type: str, content: str = "", **attributes: t.Any) -> str: 

211 """Generate KelpUI component. 

212 

213 Usage in templates: 

214 [[ kelp_component('button', 'Click Me', variant='primary', size='large') ]] 

215 """ 

216 try: 

217 kelpui = depends.get("kelpui") 

218 if kelpui: 

219 result = kelpui.build_component(component_type, content, **attributes) 

220 return ( 

221 str(result) 

222 if result is not None 

223 else f'<div class="kelp-{component_type}">{content}</div>' 

224 ) 

225 except Exception: 

226 pass 

227 

228 # Fallback 

229 css_class = f"kelp-{component_type}" 

230 variant = attributes.get("variant", "") 

231 size = attributes.get("size", "") 

232 

233 if variant: 

234 css_class += f" kelp-{component_type}--{variant}" 

235 if size: 

236 css_class += f" kelp-{component_type}--{size}" 

237 

238 if "class" in attributes: 

239 css_class += f" {attributes['class']}" 

240 

241 if component_type == "button": 

242 return f'<button class="{css_class}">{content}</button>' 

243 else: 

244 return f'<div class="{css_class}">{content}</div>' 

245 

246 

247def kelp_card(title: str = "", content: str = "", **attributes: t.Any) -> str: 

248 """Generate KelpUI card component. 

249 

250 Usage in templates: 

251 [[ kelp_card('Card Title', '<p>Card content here</p>', variant='elevated') ]] 

252 """ 

253 try: 

254 kelpui = depends.get("kelpui") 

255 if kelpui and hasattr(kelpui, "build_card"): 

256 result = kelpui.build_card(title, content, **attributes) 

257 return ( 

258 str(result) 

259 if result is not None 

260 else _build_fallback_card(title, content, **attributes) 

261 ) 

262 except Exception: 

263 pass 

264 

265 return _build_fallback_card(title, content, **attributes) 

266 

267 

268def _build_fallback_card(title: str, content: str, **attributes: t.Any) -> str: 

269 """Build fallback card HTML.""" 

270 css_class = "kelp-card" 

271 variant = attributes.get("variant", "") 

272 

273 if variant: 

274 css_class += f" kelp-card--{variant}" 

275 if "class" in attributes: 

276 css_class += f" {attributes['class']}" 

277 

278 card_html = [f'<div class="{css_class}">'] 

279 

280 if title: 

281 card_html.append( 

282 f'<div class="kelp-card__header"><h3 class="kelp-card__title">{title}</h3></div>' 

283 ) 

284 

285 if content: 

286 card_html.append(f'<div class="kelp-card__content">{content}</div>') 

287 

288 card_html.append("</div>") 

289 

290 return "".join(card_html) 

291 

292 

293# Phosphor Icons Filters 

294def phosphor_icon(icon_name: str, weight: str = "regular", **attributes: t.Any) -> str: 

295 """Generate Phosphor icon. 

296 

297 Usage in templates: 

298 [[ phosphor_icon('house', 'bold', size='24', class='nav-icon') ]] 

299 """ 

300 try: 

301 phosphor = depends.get("phosphor") 

302 if phosphor: 

303 result = phosphor.get_icon_tag(icon_name, weight=weight, **attributes) 

304 return str(result) if result is not None else f"[{icon_name}]" 

305 except Exception: 

306 pass 

307 

308 # Fallback 

309 css_class = f"ph ph-{icon_name}" 

310 if weight != "regular": 

311 css_class += f" ph-{weight}" 

312 

313 if "class" in attributes: 

314 css_class += f" {attributes['class']}" 

315 

316 size = attributes.get("size", "16") 

317 return f'<i class="{css_class}" style="font-size: {size}px;"></i>' 

318 

319 

320# Heroicons Filters 

321def heroicon(icon_name: str, style: str = "outline", **attributes: t.Any) -> str: 

322 """Generate Heroicon. 

323 

324 Usage in templates: 

325 [[ heroicon('home', 'solid', size='24', class='nav-icon') ]] 

326 """ 

327 try: 

328 heroicons = depends.get("heroicons") 

329 if heroicons: 

330 result = heroicons.get_icon_tag(icon_name, style=style, **attributes) 

331 return str(result) if result is not None else f"[{icon_name}]" 

332 except Exception: 

333 pass 

334 

335 # Fallback SVG approach 

336 css_class = attributes.get("class", "") 

337 size = attributes.get("size", "24") 

338 

339 return f'''<svg class="heroicon heroicon-{icon_name} {css_class}" 

340 width="{size}" height="{size}" 

341 fill="{style == "solid" and "currentColor" or "none"}" 

342 stroke="currentColor" stroke-width="1.5"> 

343 <use href="#heroicon-{icon_name}-{style}"></use> 

344 </svg>''' 

345 

346 

347# Remix Icons Filters 

348def remix_icon(icon_name: str, **attributes: t.Any) -> str: 

349 """Generate Remix icon. 

350 

351 Usage in templates: 

352 [[ remix_icon('home-line', size='24', class='nav-icon') ]] 

353 """ 

354 try: 

355 remix = depends.get("remix_icons") 

356 if remix: 

357 result = remix.get_icon_tag(icon_name, **attributes) 

358 return str(result) if result is not None else f"[{icon_name}]" 

359 except Exception: 

360 pass 

361 

362 # Fallback 

363 css_class = f"ri-{icon_name}" 

364 if "class" in attributes: 

365 css_class += f" {attributes['class']}" 

366 

367 size = attributes.get("size", "16") 

368 return f'<i class="{css_class}" style="font-size: {size}px;"></i>' 

369 

370 

371# Material Icons Filters 

372def material_icon(icon_name: str, variant: str = "filled", **attributes: t.Any) -> str: 

373 """Generate Material Design icon. 

374 

375 Usage in templates: 

376 [[ material_icon('home', 'outlined', size='24', class='nav-icon') ]] 

377 """ 

378 try: 

379 material = depends.get("material_icons") 

380 if material: 

381 result = material.get_icon_tag(icon_name, variant=variant, **attributes) 

382 return str(result) if result is not None else f"[{icon_name}]" 

383 except Exception: 

384 pass 

385 

386 # Fallback 

387 css_class = "material-icons" 

388 if variant != "filled": 

389 css_class += f"-{variant}" 

390 

391 if "class" in attributes: 

392 css_class += f" {attributes['class']}" 

393 

394 size = attributes.get("size", "24") 

395 return f'<span class="{css_class}" style="font-size: {size}px;">{icon_name}</span>' 

396 

397 

398# Advanced Font Filters 

399async def async_optimized_font_loading(fonts: list[str], critical: bool = True) -> str: 

400 """Generate optimized font loading with preload hints. 

401 

402 Usage in templates: 

403 [[ await async_optimized_font_loading(['Inter', 'Roboto Mono'], critical=True) ]] 

404 """ 

405 try: 

406 font_adapter = depends.get("fonts") 

407 if font_adapter and hasattr(font_adapter, "get_optimized_loading"): 

408 result = await font_adapter.get_optimized_loading(fonts, critical=critical) 

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

410 except Exception: 

411 pass 

412 

413 # Fallback 

414 html_parts = [] 

415 for font in fonts: 

416 font_family = font.replace(" ", "+") 

417 if critical: 

418 html_parts.append( 

419 f'<link rel="preload" href="https://fonts.googleapis.com/css2?family={font_family}&display=swap" as="style">' 

420 ) 

421 html_parts.append( 

422 f'<link href="https://fonts.googleapis.com/css2?family={font_family}&display=swap" rel="stylesheet">' 

423 ) 

424 

425 return "\n".join(html_parts) 

426 

427 

428def font_face_declaration( 

429 font_name: str, font_files: dict[str, str], **attributes: t.Any 

430) -> str: 

431 """Generate @font-face CSS declaration. 

432 

433 Usage in templates: 

434 [[ font_face_declaration('CustomFont', { 

435 'woff2': '/fonts/custom.woff2', 

436 'woff': '/fonts/custom.woff' 

437 }, weight='400', style='normal') ]] 

438 """ 

439 try: 

440 font_adapter = depends.get("fonts") 

441 if font_adapter and hasattr(font_adapter, "generate_font_face"): 

442 result = font_adapter.generate_font_face( 

443 font_name, font_files, **attributes 

444 ) 

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

446 except Exception: 

447 pass 

448 

449 # Fallback 

450 src_parts = [] 

451 format_map = { 

452 "woff2": "woff2", 

453 "woff": "woff", 

454 "ttf": "truetype", 

455 "otf": "opentype", 

456 } 

457 

458 for ext, url in font_files.items(): 

459 format_name = format_map.get(ext, ext) 

460 src_parts.append(f'url("{url}") format("{format_name}")') 

461 

462 css_parts = [ 

463 "@font-face {", 

464 f' font-family: "{font_name}";', 

465 f" src: {', '.join(src_parts)};", 

466 ] 

467 

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

469 if key in ["weight", "style", "display", "stretch"]: 

470 css_key = f"font-{key}" if key in ["weight", "style", "stretch"] else key 

471 css_parts.append(f" {css_key}: {value};") 

472 

473 css_parts.append("}") 

474 

475 return "\n".join(css_parts) 

476 

477 

478# Advanced HTMX Integration Filters 

479def htmx_progressive_enhancement( 

480 content: str, htmx_attrs: dict[str, str], fallback_action: str = "" 

481) -> str: 

482 """Create progressively enhanced element with HTMX. 

483 

484 Usage in templates: 

485 [[ htmx_progressive_enhancement('<button>Save</button>', { 

486 'hx-post': '/api/save', 

487 'hx-target': '#result' 

488 }, fallback_action='/save') ]] 

489 """ 

490 # Add fallback action if provided 

491 if fallback_action: 

492 if "<form" in content: 

493 content = content.replace( 

494 "<form", f'<form action="{fallback_action}" method="post"' 

495 ) 

496 elif "<button" in content and "onclick" not in content: 

497 content = content.replace( 

498 "<button", 

499 f"<button onclick=\"window.location.href='{fallback_action}'\"", 

500 ) 

501 

502 # Add HTMX attributes 

503 for attr_name, attr_value in htmx_attrs.items(): 

504 # Find the main element and add attributes 

505 if "<" in content: 

506 first_tag_end = content.find(">") 

507 if first_tag_end != -1: 

508 before_close = content[:first_tag_end] 

509 after_close = content[first_tag_end:] 

510 content = f'{before_close} {attr_name}="{attr_value}"{after_close}' 

511 

512 return content 

513 

514 

515def htmx_turbo_frame( 

516 frame_id: str, src: str = "", loading: str = "lazy", **attributes: t.Any 

517) -> str: 

518 """Create Turbo Frame-like behavior with HTMX. 

519 

520 Usage in templates: 

521 [[ htmx_turbo_frame('user-profile', '/users/123/profile', loading='eager') ]] 

522 """ 

523 attrs_list = [f'id="{frame_id}"'] 

524 

525 if src: 

526 attrs_list.extend( 

527 [ 

528 f'hx-get="{src}"', 

529 f'hx-trigger="{"load" if loading == "eager" else "revealed"}"', 

530 'hx-swap="innerHTML"', 

531 ] 

532 ) 

533 

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

535 if key.startswith("hx-") or key in ["class", "style"]: 

536 attrs_list.append(f'{key}="{value}"') 

537 

538 attrs_str = " ".join(attrs_list) 

539 

540 placeholder = "Loading..." if loading == "eager" else "Click to load" 

541 return f"<div {attrs_str}>{placeholder}</div>" 

542 

543 

544def htmx_infinite_scroll_sentinel( 

545 next_url: str, container: str = "#content", threshold: str = "0px" 

546) -> str: 

547 """Create intersection observer sentinel for infinite scroll. 

548 

549 Usage in templates: 

550 [[ htmx_infinite_scroll_sentinel('/api/posts?page=2', '#posts', '100px') ]] 

551 """ 

552 return f'''<div hx-get="{next_url}" 

553 hx-trigger="revealed" 

554 hx-target="{container}" 

555 hx-swap="beforeend" 

556 style="height: 1px; margin-bottom: {threshold};"> 

557 </div>''' 

558 

559 

560# Filter registration mapping 

561ENHANCED_FILTERS = { 

562 # Cloudflare Images 

563 "cf_image_url": cf_image_url, 

564 "cf_responsive_image": cf_responsive_image, 

565 # TwicPics 

566 "twicpics_image": twicpics_image, 

567 "twicpics_smart_crop": twicpics_smart_crop, 

568 # WebAwesome 

569 "wa_icon": wa_icon, 

570 "wa_icon_with_text": wa_icon_with_text, 

571 # KelpUI 

572 "kelp_component": kelp_component, 

573 "kelp_card": kelp_card, 

574 # Icon Libraries 

575 "phosphor_icon": phosphor_icon, 

576 "heroicon": heroicon, 

577 "remix_icon": remix_icon, 

578 "material_icon": material_icon, 

579 # Font Management 

580 "font_face_declaration": font_face_declaration, 

581 # HTMX Advanced 

582 "htmx_progressive_enhancement": htmx_progressive_enhancement, 

583 "htmx_turbo_frame": htmx_turbo_frame, 

584 "htmx_infinite_scroll_sentinel": htmx_infinite_scroll_sentinel, 

585} 

586 

587# Async filters 

588ENHANCED_ASYNC_FILTERS = { 

589 "async_optimized_font_loading": async_optimized_font_loading, 

590} 

591 

592 

593MODULE_ID = UUID("01937d8a-1234-7890-abcd-1234567890ab") 

594MODULE_STATUS = AdapterStatus.STABLE