Coverage for src/turbo_themes/css_variables.py: 89%

91 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-08-04 10:58 +0000

1"""CSS variable generation utilities. 

2 

3Provides focused helper functions for generating CSS custom properties 

4from theme tokens. Used by ThemeManager.apply_theme_to_css_variables(). 

5""" 

6 

7from __future__ import annotations 

8 

9from .mapping_config import ( 

10 MappingConfig, 

11 OptionalGroupConfig, 

12 get_mapping_config, 

13 resolve_token_path, 

14) 

15from .models import Tokens 

16 

17 

18def apply_core_mappings( 

19 tokens: Tokens, 

20 config: MappingConfig | None = None, 

21) -> dict[str, str]: 

22 """Apply core token mappings to generate CSS variables. 

23 

24 Args: 

25 tokens: The theme tokens to map. 

26 config: Optional mapping configuration. Uses default if not provided. 

27 

28 Returns: 

29 Dictionary of CSS variable names to values. 

30 """ 

31 if config is None: 

32 config = get_mapping_config() 

33 

34 variables: dict[str, str] = {} 

35 prefix = config.prefix 

36 

37 for mapping in config.core_mappings: 

38 try: 

39 value = resolve_token_path(tokens, mapping.token_path) 

40 # Try fallback if primary path didn't resolve 

41 if value is None and mapping.fallback: 

42 value = resolve_token_path(tokens, mapping.fallback) 

43 if value is not None: 43 ↛ 37line 43 didn't jump to line 37 because the condition on line 43 was always true

44 variables[f"--{prefix}-{mapping.css_var}"] = str(value) 

45 except (AttributeError, KeyError): 

46 pass 

47 

48 return variables 

49 

50 

51def apply_optional_spacing( 

52 tokens: Tokens, 

53 config: OptionalGroupConfig | None = None, 

54 prefix: str = "turbo", 

55) -> dict[str, str]: 

56 """Apply optional spacing tokens to generate CSS variables. 

57 

58 Args: 

59 tokens: The theme tokens containing spacing. 

60 config: Optional spacing group configuration. 

61 prefix: CSS variable prefix. 

62 

63 Returns: 

64 Dictionary of spacing CSS variable names to values. 

65 """ 

66 variables: dict[str, str] = {} 

67 

68 if not tokens.spacing: 

69 return variables 

70 

71 if config is None: 

72 mapping_config = get_mapping_config() 

73 config = mapping_config.optional_groups.get("spacing") 

74 prefix = mapping_config.prefix 

75 

76 if config is None: 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true

77 return variables 

78 

79 spacing = tokens.spacing 

80 for prop in config.properties: 

81 value = getattr(spacing, prop, None) 

82 if value is not None: 82 ↛ 80line 82 didn't jump to line 80 because the condition on line 82 was always true

83 variables[f"--{prefix}-{config.prefix}-{prop}"] = str(value) 

84 

85 return variables 

86 

87 

88def apply_optional_elevation( 

89 tokens: Tokens, 

90 config: OptionalGroupConfig | None = None, 

91 prefix: str = "turbo", 

92) -> dict[str, str]: 

93 """Apply optional elevation tokens to generate CSS variables. 

94 

95 Args: 

96 tokens: The theme tokens containing elevation. 

97 config: Optional elevation group configuration. 

98 prefix: CSS variable prefix. 

99 

100 Returns: 

101 Dictionary of elevation CSS variable names to values. 

102 """ 

103 variables: dict[str, str] = {} 

104 

105 if not tokens.elevation: 

106 return variables 

107 

108 if config is None: 

109 mapping_config = get_mapping_config() 

110 config = mapping_config.optional_groups.get("elevation") 

111 prefix = mapping_config.prefix 

112 

113 if config is None: 113 ↛ 114line 113 didn't jump to line 114 because the condition on line 113 was never true

114 return variables 

115 

116 elevation = tokens.elevation 

117 for prop in config.properties: 

118 value = getattr(elevation, prop, None) 

119 if value is not None: 119 ↛ 117line 119 didn't jump to line 117 because the condition on line 119 was always true

120 variables[f"--{prefix}-{config.prefix}-{prop}"] = str(value) 

121 

122 return variables 

123 

124 

125def apply_optional_animation( 

126 tokens: Tokens, 

127 config: OptionalGroupConfig | None = None, 

128 prefix: str = "turbo", 

129) -> dict[str, str]: 

130 """Apply optional animation tokens to generate CSS variables. 

131 

132 Args: 

133 tokens: The theme tokens containing animation. 

134 config: Optional animation group configuration. 

135 prefix: CSS variable prefix. 

136 

137 Returns: 

138 Dictionary of animation CSS variable names to values. 

139 """ 

140 variables: dict[str, str] = {} 

141 

142 if not tokens.animation: 

143 return variables 

144 

145 if config is None: 

146 mapping_config = get_mapping_config() 

147 config = mapping_config.optional_groups.get("animation") 

148 prefix = mapping_config.prefix 

149 

150 if config is None or not config.mappings: 150 ↛ 151line 150 didn't jump to line 151 because the condition on line 150 was never true

151 return variables 

152 

153 for mapping in config.mappings: 

154 value = resolve_token_path(tokens, mapping.token_path) 

155 if value is not None: 155 ↛ 153line 155 didn't jump to line 153 because the condition on line 155 was always true

156 variables[f"--{prefix}-{config.prefix}-{mapping.css_var}"] = str(value) 

157 

158 return variables 

159 

160 

161def apply_optional_opacity( 

162 tokens: Tokens, 

163 config: OptionalGroupConfig | None = None, 

164 prefix: str = "turbo", 

165) -> dict[str, str]: 

166 """Apply optional opacity tokens to generate CSS variables. 

167 

168 Args: 

169 tokens: The theme tokens containing opacity. 

170 config: Optional opacity group configuration. 

171 prefix: CSS variable prefix. 

172 

173 Returns: 

174 Dictionary of opacity CSS variable names to values. 

175 """ 

176 variables: dict[str, str] = {} 

177 

178 if not tokens.opacity: 

179 return variables 

180 

181 if config is None: 

182 mapping_config = get_mapping_config() 

183 config = mapping_config.optional_groups.get("opacity") 

184 prefix = mapping_config.prefix 

185 

186 if config is None: 186 ↛ 187line 186 didn't jump to line 187 because the condition on line 186 was never true

187 return variables 

188 

189 opacity = tokens.opacity 

190 for prop in config.properties: 

191 value = getattr(opacity, prop, None) 

192 if value is not None: 192 ↛ 190line 192 didn't jump to line 190 because the condition on line 192 was always true

193 variables[f"--{prefix}-{config.prefix}-{prop}"] = str(value) 

194 

195 return variables 

196 

197 

198def generate_css_variables(tokens: Tokens) -> dict[str, str]: 

199 """Generate all CSS variables from theme tokens. 

200 

201 This is a convenience function that combines all mapping categories: 

202 core mappings, spacing, elevation, animation, and opacity. 

203 

204 Args: 

205 tokens: The theme tokens to convert. 

206 

207 Returns: 

208 Complete dictionary of CSS variable names to values. 

209 """ 

210 config = get_mapping_config() 

211 prefix = config.prefix 

212 

213 variables: dict[str, str] = {} 

214 

215 # Apply all mapping categories 

216 variables.update(apply_core_mappings(tokens, config)) 

217 variables.update( 

218 apply_optional_spacing( 

219 tokens, 

220 config.optional_groups.get("spacing"), 

221 prefix, 

222 ) 

223 ) 

224 variables.update( 

225 apply_optional_elevation( 

226 tokens, 

227 config.optional_groups.get("elevation"), 

228 prefix, 

229 ) 

230 ) 

231 variables.update( 

232 apply_optional_animation( 

233 tokens, 

234 config.optional_groups.get("animation"), 

235 prefix, 

236 ) 

237 ) 

238 variables.update( 

239 apply_optional_opacity( 

240 tokens, 

241 config.optional_groups.get("opacity"), 

242 prefix, 

243 ) 

244 ) 

245 

246 return variables