cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
css_composer.c
Go to the documentation of this file.
1
8#include <stdint.h>
9#include <stdio.h>
10#include <stdlib.h>
11#include <string.h>
12#include <math.h>
13#include <ctype.h>
14
15#define MIN3(a, b, c) ((a) < (b) ? ((a) < (c) ? (a) : (c)) : ((b) < (c) ? (b) : (c)))
16#define MAX3(a, b, c) ((a) > (b) ? ((a) > (c) ? (a) : (c)) : ((b) > (c) ? (b) : (c)))
17
25 cwist_color_hsl hsl = {0.0f, 0.0f, 0.0f};
26 float r = rgb.r / 255.0f;
27 float g = rgb.g / 255.0f;
28 float b = rgb.b / 255.0f;
29
30 float max = MAX3(r, g, b);
31 float min = MIN3(r, g, b);
32 hsl.l = (max + min) / 2.0f;
33
34 if (max == min) {
35 hsl.h = 0.0f;
36 hsl.s = 0.0f;
37 } else {
38 float d = max - min;
39 hsl.s = hsl.l > 0.5f ? d / (2.0f - max - min) : d / (max + min);
40
41 if (max == r) {
42 hsl.h = (g - b) / d + (g < b ? 6.0f : 0.0f);
43 } else if (max == g) {
44 hsl.h = (b - r) / d + 2.0f;
45 } else {
46 hsl.h = (r - g) / d + 4.0f;
47 }
48 hsl.h /= 6.0f;
49 }
50 hsl.h *= 360.0f;
51 return hsl;
52}
53
61static float hue2rgb(float p, float q, float t) {
62 if (t < 0.0f) t += 1.0f;
63 if (t > 1.0f) t -= 1.0f;
64 if (t < 1.0f / 6.0f) return p + (q - p) * 6.0f * t;
65 if (t < 1.0f / 2.0f) return q;
66 if (t < 2.0f / 3.0f) return p + (q - p) * (2.0f / 3.0f - t) * 6.0f;
67 return p;
68}
69
77 cwist_color_rgb rgb = {0, 0, 0};
78 /* Wrap the hue into [0, 360): hue2rgb() only folds one turn. */
79 float h = fmodf(hsl.h, 360.0f) / 360.0f;
80 if (h < 0.0f) h += 1.0f;
81 float s = hsl.s;
82 float l = hsl.l;
83
84 /* Clamp first: converting a float outside 0..255 to unsigned char is
85 * undefined. Gray is rounded like the chromatic channels below. */
86 if (s < 0.0f) s = 0.0f;
87 if (s > 1.0f) s = 1.0f;
88 if (l < 0.0f) l = 0.0f;
89 if (l > 1.0f) l = 1.0f;
90 if (s == 0.0f) {
91 rgb.r = rgb.g = rgb.b = (unsigned char)roundf(l * 255.0f);
92 } else {
93 float q = l < 0.5f ? l * (1.0f + s) : l + s - l * s;
94 float p = 2.0f * l - q;
95 rgb.r = (unsigned char)(roundf(hue2rgb(p, q, h + 1.0f / 3.0f) * 255.0f));
96 rgb.g = (unsigned char)(roundf(hue2rgb(p, q, h) * 255.0f));
97 rgb.b = (unsigned char)(roundf(hue2rgb(p, q, h - 1.0f / 3.0f) * 255.0f));
98 }
99 return rgb;
100}
101
107static inline int hex_char_to_val(char c) {
108 if (c >= '0' && c <= '9') return c - '0';
109 if (c >= 'a' && c <= 'f') return c - 'a' + 10;
110 if (c >= 'A' && c <= 'F') return c - 'A' + 10;
111 return -1;
112}
113
122 cwist_color_rgb rgb = {0, 0, 0};
123 if (!hex) return rgb;
124 if (hex[0] == '#') hex++;
125 size_t len = strlen(hex);
126 if (len == 6) {
127 int r1 = hex_char_to_val(hex[0]);
128 int r2 = hex_char_to_val(hex[1]);
129 int g1 = hex_char_to_val(hex[2]);
130 int g2 = hex_char_to_val(hex[3]);
131 int b1 = hex_char_to_val(hex[4]);
132 int b2 = hex_char_to_val(hex[5]);
133 if (r1 >= 0 && r2 >= 0 && g1 >= 0 && g2 >= 0 && b1 >= 0 && b2 >= 0) {
134 rgb.r = (unsigned char)((r1 << 4) | r2);
135 rgb.g = (unsigned char)((g1 << 4) | g2);
136 rgb.b = (unsigned char)((b1 << 4) | b2);
137 }
138 } else if (len == 3) {
139 int r = hex_char_to_val(hex[0]);
140 int g = hex_char_to_val(hex[1]);
141 int b = hex_char_to_val(hex[2]);
142 if (r >= 0 && g >= 0 && b >= 0) {
143 rgb.r = (unsigned char)((r << 4) | r);
144 rgb.g = (unsigned char)((g << 4) | g);
145 rgb.b = (unsigned char)((b << 4) | b);
146 }
147 }
148 return rgb;
149}
150
156void cwist_color_rgb_to_hex(cwist_color_rgb rgb, char out_hex[8]) {
157 if (out_hex) {
158 snprintf(out_hex, 8, "#%02x%02x%02x", rgb.r, rgb.g, rgb.b);
159 }
160}
161
169 if (!cfg) return;
170 cfg->primary_color = cwist_color_hex_to_rgb("#3B82F6"); // Default Tailwind Blue
171 cfg->secondary_color = cwist_color_hex_to_rgb("#10B981"); // Default Tailwind Green
172 cfg->roundness_px = 8.0f;
173 cfg->spacing_base_px = 4.0f;
174 cfg->is_dark_mode = false;
175}
176
180static void get_shifted_hex(cwist_color_rgb base, float light_shift, char out[8]) {
182 hsl.l += light_shift;
183 if (hsl.l > 1.0f) hsl.l = 1.0f;
184 if (hsl.l < 0.0f) hsl.l = 0.0f;
186 cwist_color_rgb_to_hex(shifted, out);
187}
188
196 if (!cfg) return NULL;
198
199 char p_hex[8], p_hover[8], p_active[8];
200 char s_hex[8], s_hover[8], s_active[8];
201
203 get_shifted_hex(cfg->primary_color, cfg->is_dark_mode ? 0.1f : -0.1f, p_hover);
204 get_shifted_hex(cfg->primary_color, cfg->is_dark_mode ? 0.15f : -0.15f, p_active);
205
207 get_shifted_hex(cfg->secondary_color, cfg->is_dark_mode ? 0.1f : -0.1f, s_hover);
208 get_shifted_hex(cfg->secondary_color, cfg->is_dark_mode ? 0.15f : -0.15f, s_active);
209
210 char buf[1024];
211 snprintf(buf, sizeof(buf),
212 ":root {\n"
213 " --color-primary: %s;\n"
214 " --color-primary-hover: %s;\n"
215 " --color-primary-active: %s;\n"
216 " --color-secondary: %s;\n"
217 " --color-secondary-hover: %s;\n"
218 " --color-secondary-active: %s;\n"
219 " --bg-body: %s;\n"
220 " --bg-surface: %s;\n"
221 " --text-main: %s;\n"
222 " --text-muted: %s;\n"
223 " --radius-sm: %.1fpx;\n"
224 " --radius-md: %.1fpx;\n"
225 " --radius-lg: %.1fpx;\n"
226 " --space-1: %.1fpx;\n"
227 " --space-2: %.1fpx;\n"
228 " --space-4: %.1fpx;\n"
229 " --space-8: %.1fpx;\n"
230 "}\n",
231 p_hex, p_hover, p_active, s_hex, s_hover, s_active,
232 cfg->is_dark_mode ? "#121212" : "#FFFFFF", cfg->is_dark_mode ? "#1E1E1E" : "#F3F4F6",
233 cfg->is_dark_mode ? "#F9FAFB" : "#111827", cfg->is_dark_mode ? "#9CA3AF" : "#6B7280",
234 cfg->roundness_px * 0.5f, cfg->roundness_px, cfg->roundness_px * 1.5f,
235 cfg->spacing_base_px, cfg->spacing_base_px * 2.0f, cfg->spacing_base_px * 4.0f,
236 cfg->spacing_base_px * 8.0f);
237
238 cwist_sstring_append(css, buf);
239 return css;
240}
241
249 (void)cfg; // Currently relying on generated variables
251
252 const char *utils = "/* Typography */\n"
253 ".text-primary { color: var(--color-primary); }\n"
254 ".text-main { color: var(--text-main); }\n"
255 ".text-muted { color: var(--text-muted); }\n"
256 "\n"
257 "/* Backgrounds & Surfaces */\n"
258 ".bg-body { background-color: var(--bg-body); }\n"
259 ".bg-surface { background-color: var(--bg-surface); }\n"
260 ".bg-primary { background-color: var(--color-primary); color: #fff; }\n"
261 ".bg-primary:hover { background-color: var(--color-primary-hover); }\n"
262 ".bg-primary:active { background-color: var(--color-primary-active); }\n"
263 "\n"
264 "/* Components */\n"
265 ".btn {\n"
266 " display: inline-flex;\n"
267 " align-items: center;\n"
268 " justify-content: center;\n"
269 " padding: var(--space-2) var(--space-4);\n"
270 " border-radius: var(--radius-md);\n"
271 " font-weight: 500;\n"
272 " transition: background-color 0.2s, color 0.2s;\n"
273 " cursor: pointer;\n"
274 " border: none;\n"
275 "}\n"
276 "\n"
277 ".card {\n"
278 " background-color: var(--bg-surface);\n"
279 " border-radius: var(--radius-lg);\n"
280 " padding: var(--space-4);\n"
281 " box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);\n"
282 "}\n";
283
284 cwist_sstring_append(css, utils);
285 return css;
286}
287
295 if (!cfg) return NULL;
296 cwist_sstring *final_css = cwist_sstring_create();
297
300
301 if (vars) cwist_sstring_append(final_css, vars->data);
302 if (utils) cwist_sstring_append(final_css, utils->data);
303
304 if (vars) cwist_sstring_destroy(vars);
305 if (utils) cwist_sstring_destroy(utils);
306
307 return final_css;
308}
309
310struct cwist_css_scope_entry {
311 char *base_class;
312 cwist_sstring *scoped_class;
313 char *declarations;
314 bool used;
315};
316
321static bool append_ok(cwist_sstring *s, const char *text) {
322 cwist_error_t err = cwist_sstring_append(s, text);
323 bool ok = cwist_error_is_ok(&err);
325 return ok;
326}
327
334static bool is_css_ident(const char *s) {
335 if (!s || !*s) return false;
336 const char *p = s;
337 if (*p == '-') p++;
338 if (!(isalpha((unsigned char)*p) || *p == '_')) return false;
339 for (p++; *p; p++) {
340 if (!(isalnum((unsigned char)*p) || *p == '_' || *p == '-')) return false;
341 }
342 return true;
343}
344
351static struct cwist_css_scope_entry *scope_find(const cwist_css_scope *scope,
352 const char *base_class) {
353 for (size_t i = 0; i < scope->count; i++) {
354 if (strcmp(scope->entries[i].base_class, base_class) == 0) return &scope->entries[i];
355 }
356 return NULL;
357}
358
363static struct cwist_css_scope_entry *scope_get_or_add(cwist_css_scope *scope,
364 const char *base_class) {
365 struct cwist_css_scope_entry *entry = scope_find(scope, base_class);
366 if (entry) return entry;
367
368 if (scope->count == scope->capacity) {
369 size_t new_capacity = scope->capacity ? scope->capacity * 2 : 8;
370 struct cwist_css_scope_entry *grown = (struct cwist_css_scope_entry *)cwist_realloc(
371 scope->entries, new_capacity * sizeof(*grown));
372 if (!grown) return NULL;
373 scope->entries = grown;
374 scope->capacity = new_capacity;
375 }
376
377 char *base_copy = cwist_strdup(base_class);
379 if (!base_copy || !scoped || !append_ok(scoped, base_class) || !append_ok(scoped, "-") ||
380 !append_ok(scoped, scope->suffix)) {
381 cwist_free(base_copy);
382 cwist_sstring_destroy(scoped);
383 return NULL;
384 }
385
386 entry = &scope->entries[scope->count++];
387 entry->base_class = base_copy;
388 entry->scoped_class = scoped;
389 entry->declarations = NULL;
390 entry->used = false;
391 return entry;
392}
393
402void cwist_css_scope_init(cwist_css_scope *scope, const char *component_name) {
403 if (!scope) return;
404 const char *name = component_name ? component_name : "";
405
406 /* FNV-1a, 32-bit. Deliberately unseeded: markup and stylesheet may be
407 * produced by different worker processes and must agree on the suffix. */
408 uint32_t hash = 2166136261u;
409 for (const unsigned char *p = (const unsigned char *)name; *p; p++) {
410 hash ^= *p;
411 hash *= 16777619u;
412 }
413
414 snprintf(scope->suffix, sizeof(scope->suffix), "%08x", (unsigned int)hash);
415 scope->entries = NULL;
416 scope->count = 0;
417 scope->capacity = 0;
418}
419
427const char *cwist_css_scope_class(cwist_css_scope *scope, const char *base_class) {
428 if (!scope || !scope->suffix[0] || !is_css_ident(base_class)) return NULL;
429 struct cwist_css_scope_entry *entry = scope_get_or_add(scope, base_class);
430 if (!entry) return NULL;
431 entry->used = true;
432 return entry->scoped_class->data;
433}
434
444int cwist_css_scope_add_rule(cwist_css_scope *scope, const char *base_class,
445 const char *declarations) {
446 if (!scope || !scope->suffix[0] || !is_css_ident(base_class) || !declarations) return -1;
447 if (strpbrk(declarations, "{}<")) return -1;
448
449 char *copy = cwist_strdup(declarations);
450 if (!copy) return -1;
451 struct cwist_css_scope_entry *entry = scope_get_or_add(scope, base_class);
452 if (!entry) {
453 cwist_free(copy);
454 return -1;
455 }
456 cwist_free(entry->declarations);
457 entry->declarations = copy;
458 return 0;
459}
460
469 if (!scope) return NULL;
471 if (!css) return NULL;
472
473 cwist_error_t err = cwist_sstring_assign(css, "");
474 bool ok = cwist_error_is_ok(&err);
476
477 for (size_t i = 0; ok && i < scope->count; i++) {
478 const struct cwist_css_scope_entry *entry = &scope->entries[i];
479 if (!entry->used || !entry->declarations) continue;
480 ok = append_ok(css, ".") && append_ok(css, entry->scoped_class->data) &&
481 append_ok(css, " { ") && append_ok(css, entry->declarations) && append_ok(css, " }\n");
482 }
483 if (!ok) {
485 return NULL;
486 }
487 return css;
488}
489
497 if (!scope) return;
498 for (size_t i = 0; i < scope->count; i++) {
499 cwist_free(scope->entries[i].base_class);
500 cwist_sstring_destroy(scope->entries[i].scoped_class);
501 cwist_free(scope->entries[i].declarations);
502 }
503 cwist_free(scope->entries);
504 memset(scope, 0, sizeof(*scope));
505}
506
512static bool css_is_space(char c) {
513 return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f';
514}
515
521static bool css_is_word(char c) {
522 return isalnum((unsigned char)c) || c == '-' || c == '_' || c == '%' || c == '.' || c == '#' ||
523 c == '\\' || (unsigned char)c >= 0x80;
524}
525
532static bool css_starts_url(const char *p) {
533 return (p[0] == 'u' || p[0] == 'U') && (p[1] == 'r' || p[1] == 'R') &&
534 (p[2] == 'l' || p[2] == 'L') && p[3] == '(';
535}
536
548 if (!css) return NULL;
549 size_t n = strlen(css);
550 /* Every step copies or drops input bytes, so the output never grows. */
551 char *out = (char *)cwist_alloc(n + 1);
552 if (!out) return NULL;
553
554 size_t o = 0;
555 size_t verbatim_end = 0; /* out[0..verbatim_end) ends in copied, non-structural bytes */
556 bool pending_space = false;
557 size_t i = 0;
558 while (i < n) {
559 char c = css[i];
560
561 if (c == '/' && css[i + 1] == '*') {
562 const char *close = strstr(css + i + 2, "*/");
563 size_t stop = close ? (size_t)(close - css) + 2 : n;
564 if (css[i + 2] == '!') {
565 if (pending_space && o > 0) out[o++] = ' ';
566 pending_space = false;
567 memcpy(out + o, css + i, stop - i);
568 o += stop - i;
569 verbatim_end = o;
570 } else if (o > 0 && stop < n && css_is_word(out[o - 1]) && css_is_word(css[stop])) {
571 /* Removing the comment must not glue two tokens together. */
572 pending_space = true;
573 }
574 i = stop;
575 continue;
576 }
577 if (css_is_space(c)) {
578 pending_space = true;
579 i++;
580 continue;
581 }
582
583 if (pending_space) {
584 bool after_structural = o > 0 && o != verbatim_end && strchr("{};,:>(", out[o - 1]);
585 bool before_structural = strchr("{};,>)", c) != NULL;
586 if (o > 0 && !after_structural && !before_structural) out[o++] = ' ';
587 pending_space = false;
588 }
589
590 if (c == '"' || c == '\'') {
591 size_t j = i + 1;
592 while (j < n && css[j] != c) {
593 j += (css[j] == '\\' && j + 1 < n) ? 2 : 1;
594 }
595 size_t stop = j < n ? j + 1 : n;
596 memcpy(out + o, css + i, stop - i);
597 o += stop - i;
598 verbatim_end = o;
599 i = stop;
600 continue;
601 }
602 if (c == '\\') {
603 size_t stop = i + 1 < n ? i + 2 : n;
604 memcpy(out + o, css + i, stop - i);
605 o += stop - i;
606 verbatim_end = o;
607 i = stop;
608 continue;
609 }
610 if (css_starts_url(css + i) && (o == 0 || !css_is_word(out[o - 1]))) {
611 size_t j = i + 4;
612 while (j < n && css_is_space(css[j])) j++;
613 if (j < n && css[j] != '"' && css[j] != '\'') {
614 /* Unquoted url(): copy the whole argument as written. */
615 const char *close = strchr(css + j, ')');
616 size_t stop = close ? (size_t)(close - css) + 1 : n;
617 memcpy(out + o, css + i, stop - i);
618 o += stop - i;
619 verbatim_end = o;
620 i = stop;
621 continue;
622 }
623 }
624 if (c == '}' && o > 0 && o != verbatim_end && out[o - 1] == ';') o--;
625
626 out[o++] = c;
627 i++;
628}
629out[o] = '\0';
630
632bool ok = result != NULL;
633if (ok) {
634 cwist_error_t err = cwist_sstring_assign_len(result, out, o);
635 ok = cwist_error_is_ok(&err);
637}
638cwist_free(out);
639if (!ok) {
640 if (result) cwist_sstring_destroy(result);
641 return NULL;
642}
643return result;
644}
645
654cwist_sstring *cwist_css_bundle(const char *const *parts, size_t count, bool minify) {
655 if (!parts && count > 0) return NULL;
657 if (!bundle) return NULL;
658
659 cwist_error_t err = cwist_sstring_assign(bundle, "");
660 bool ok = cwist_error_is_ok(&err);
662 for (size_t i = 0; ok && i < count; i++) {
663 if (!parts[i]) continue;
664 ok = append_ok(bundle, parts[i]) && append_ok(bundle, "\n");
665 }
666 if (!ok) {
667 cwist_sstring_destroy(bundle);
668 return NULL;
669 }
670 if (!minify) return bundle;
671
672 cwist_sstring *minified = cwist_css_minify(bundle->data ? bundle->data : "");
673 cwist_sstring_destroy(bundle);
674 return minified;
675}
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
void * cwist_realloc(void *ptr, size_t new_size)
Resize an existing allocation (libttak tracked).
Definition alloc.c:27
char * cwist_strdup(const char *src)
Duplicate a string using libttak-backed storage.
Definition alloc.c:32
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
Definition alloc.c:17
cwist_sstring * cwist_sstring_create(void)
Create a new sstring instance.
Definition sstring.c:802
cwist_error_t cwist_sstring_append(cwist_sstring *str, const char *data)
Append a C string.
Definition sstring.c:559
cwist_error_t cwist_sstring_assign_len(cwist_sstring *str, const char *data, size_t len)
Assign raw bytes with length.
Definition sstring.c:88
cwist_error_t cwist_sstring_assign(cwist_sstring *str, const char *data)
Assign a C string.
Definition sstring.c:504
void cwist_sstring_destroy(cwist_sstring *str)
Destroy an sstring instance.
Definition sstring.c:823
cwist_sstring * cwist_css_bundle(const char *const *parts, size_t count, bool minify)
Concatenate CSS parts into one bundle, optionally minified.
#define MAX3(a, b, c)
#define MIN3(a, b, c)
cwist_sstring * cwist_css_generate_stylesheet(const cwist_css_config *cfg)
Generate a complete stylesheet (variables plus utility classes).
const char * cwist_css_scope_class(cwist_css_scope *scope, const char *base_class)
Map a base class to its scoped class name, marking it as used.
int cwist_css_scope_add_rule(cwist_css_scope *scope, const char *base_class, const char *declarations)
Attach CSS declarations to a scoped class.
cwist_sstring * cwist_css_generate_variables(const cwist_css_config *cfg)
Generate the CSS custom-property block (:root variables) for a config.
void cwist_css_config_init(cwist_css_config *cfg)
Initialize a CSS configuration with default values.
cwist_color_rgb cwist_color_hex_to_rgb(const char *hex)
Parse a hexadecimal color string into an RGB color.
cwist_sstring * cwist_css_scope_generate_stylesheet(const cwist_css_scope *scope)
Emit the stylesheet for all used scope entries that have declarations.
cwist_color_rgb cwist_color_hsl_to_rgb(cwist_color_hsl hsl)
Convert an HSL color to the RGB color space.
cwist_sstring * cwist_css_minify(const char *css)
Minify CSS source by stripping comments and collapsing whitespace.
void cwist_color_rgb_to_hex(cwist_color_rgb rgb, char out_hex[8])
Format an RGB color as a lowercase "#rrggbb" hex string.
void cwist_css_scope_init(cwist_css_scope *scope, const char *component_name)
Initialize a CSS scoping context, deriving a deterministic class suffix.
cwist_sstring * cwist_css_generate_utility_classes(const cwist_css_config *cfg)
Generate the utility/component CSS classes that reference the theme variables.
cwist_color_hsl cwist_color_rgb_to_hsl(cwist_color_rgb rgb)
Convert an RGB color to the HSL color space.
void cwist_css_scope_destroy(cwist_css_scope *scope)
Destroy a CSS scoping context and free all owned resources.
Dynamic CSS Synthesis and Color Mathematics.
size_t count
Definition otel.c:58
HSL Color representation for dynamic manipulation.
float s
Saturation percentage (0.0 to 1.0)
float h
Hue angle in degrees (0.0 to 360.0)
float l
Lightness percentage (0.0 to 1.0)
RGB Color representation.
unsigned char g
Green channel (0-255)
unsigned char b
Blue channel (0-255)
unsigned char r
Red channel (0-255)
Dynamic CSS configuration parameters. Contains base metrics used to extrapolate an entire design syst...
float spacing_base_px
Base spacing unit in pixels (e.g. 4.0)
cwist_color_rgb secondary_color
Secondary/Accent color.
float roundness_px
Base border radius in pixels (e.g. 8.0)
bool is_dark_mode
Flag to trigger dark mode palette generation.
cwist_color_rgb primary_color
Base primary brand color.
Component-scoped class names and the rules attached to them.
size_t capacity
Allocated entries.
char suffix[9]
8 lowercase hex digits + NUL
size_t count
Number of known base classes.
struct cwist_css_scope_entry * entries
Per-class state, private.
Integrated error structure used throughout the system.
Definition cwist_err.h:72
char * data
Access directly only when raw handling is necessary.
Definition sstring.h:15
void cwist_error_dispose(cwist_error_t *err)
Release resources owned by an error value. Currently only CWIST_ERR_JSON owns memory (the cJSON paylo...
Definition error.c:43