Coverage for src / lexigram / ui / layouts / mixins.py: 100%
55 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
1"""Manager classes for layout CSS and JavaScript assets.
3Provides composition-based asset management for layouts.
4"""
6from __future__ import annotations
8from markupsafe import escape
11class CSSManager:
12 """Manages CSS asset injection for layout rendering."""
14 def __init__(self) -> None:
15 """Initialize CSS collections."""
16 self._css_files: list[tuple[str, dict[str, str]]] = []
17 self._inline_styles: list[str] = []
19 def add_css(self, href: str, **attrs: str) -> None:
20 """Add a CSS file link.
22 Args:
23 href: URL to CSS file
24 **attrs: Additional attributes (media, crossorigin, etc.)
25 """
26 self._css_files.append((href, attrs))
28 def add_inline_style(self, css: str) -> None:
29 """Add inline CSS.
31 Args:
32 css: CSS rules
33 """
34 self._inline_styles.append(css)
36 def render_css(self) -> str:
37 """Render all CSS as HTML.
39 Returns:
40 HTML string with link and style tags
41 """
42 parts: list[str] = []
44 # External CSS
45 for href, attrs in self._css_files:
46 attr_str = " ".join(f'{k}="{escape(v)}"' for k, v in attrs.items())
47 if attr_str:
48 parts.append(
49 f'<link rel="stylesheet" href="{escape(href)}" {attr_str}>',
50 )
51 else:
52 parts.append(f'<link rel="stylesheet" href="{escape(href)}">')
54 # Inline styles
55 if self._inline_styles:
56 parts.append("<style>")
57 parts.extend(self._inline_styles)
58 parts.append("</style>")
60 return "\n".join(parts)
63class JSManager:
64 """Manages JavaScript asset injection for layout rendering."""
66 def __init__(self) -> None:
67 """Initialize JS collections."""
68 self._js_files: list[tuple[str, dict[str, str]]] = []
69 self._inline_scripts: list[str] = []
70 self._deferred_scripts: list[str] = []
72 def add_js(
73 self,
74 src: str,
75 defer: bool = False,
76 async_: bool = False,
77 **attrs: str,
78 ) -> None:
79 """Add a JavaScript file.
81 Args:
82 src: URL to JS file
83 defer: Add defer attribute
84 async_: Add async attribute
85 **attrs: Additional attributes
86 """
87 if defer:
88 attrs["defer"] = ""
89 if async_:
90 attrs["async"] = ""
91 self._js_files.append((src, attrs))
93 def add_inline_script(self, script: str, defer: bool = False) -> None:
94 """Add inline JavaScript.
96 Args:
97 script: JavaScript code
98 defer: If True, render at end of body
99 """
100 if defer:
101 self._deferred_scripts.append(script)
102 else:
103 self._inline_scripts.append(script)
105 def render_js_head(self) -> str:
106 """Render JS for head section.
108 Returns:
109 HTML string with script tags
110 """
111 parts: list[str] = []
113 # External JS
114 for src, attrs in self._js_files:
115 attr_str = " ".join(
116 f'{k}="{escape(v)}"' if v else k for k, v in attrs.items()
117 )
118 if attr_str:
119 parts.append(f'<script src="{escape(src)}" {attr_str}></script>')
120 else:
121 parts.append(f'<script src="{escape(src)}"></script>')
123 # Inline scripts (non-deferred)
124 for script in self._inline_scripts:
125 parts.append(f"<script>{script}</script>")
127 return "\n".join(parts)
129 def render_js_body_end(self) -> str:
130 """Render deferred JS for end of body.
132 Returns:
133 HTML string with script tags
134 """
135 if not self._deferred_scripts:
136 return ""
138 parts = ["<script>"]
139 parts.extend(self._deferred_scripts)
140 parts.append("</script>")
141 return "\n".join(parts)
144__all__ = ["CSSManager", "JSManager"]