File Coverage

include/litavis_vars.h
Criterion Covered Total %
statement 286 301 95.0
branch 187 260 71.9
condition n/a
subroutine n/a
pod n/a
total 473 561 84.3


line stmt bran cond sub pod time code
1             #ifndef LITAVIS_VARS_H
2             #define LITAVIS_VARS_H
3              
4             /* ── Variable scope ───────────────────────────────────────── */
5              
6             typedef struct LitavisVar {
7             char *name; /* variable name (without $) */
8             char *value; /* resolved string value */
9             } LitavisVar;
10              
11             typedef struct LitavisVarScope {
12             LitavisVar *vars;
13             int count;
14             int capacity;
15             struct LitavisVarScope *parent; /* lexical parent scope */
16             } LitavisVarScope;
17              
18             /* ── Map variable (named key-value store) ────────────────── */
19              
20             typedef struct LitavisMapEntry {
21             char *key;
22             char *value;
23             } LitavisMapEntry;
24              
25             typedef struct LitavisMapVar {
26             char *name; /* map name (without %) */
27             LitavisMapEntry *entries;
28             int count;
29             int capacity;
30             } LitavisMapVar;
31              
32             typedef struct LitavisMapStore {
33             LitavisMapVar *maps;
34             int count;
35             int capacity;
36             } LitavisMapStore;
37              
38             /* ── Mixin (named property set) ───────────────────────────── */
39              
40             typedef struct LitavisMixin {
41             char *name; /* mixin name (without %) */
42             LitavisProp *props; /* ordered properties */
43             int prop_count;
44             int prop_cap;
45             } LitavisMixin;
46              
47             typedef struct LitavisMixinStore {
48             LitavisMixin *mixins;
49             int count;
50             int capacity;
51             } LitavisMixinStore;
52              
53             /* ── Scope lifecycle ──────────────────────────────────────── */
54              
55 498           static LitavisVarScope* litavis_scope_new(LitavisVarScope *parent) {
56 498           LitavisVarScope *s = (LitavisVarScope*)malloc(sizeof(LitavisVarScope));
57 498 50         if (!s) LITAVIS_FATAL("out of memory");
58 498           s->vars = NULL;
59 498           s->count = 0;
60 498           s->capacity = 0;
61 498           s->parent = parent;
62 498           return s;
63             }
64              
65 498           static void litavis_scope_free(LitavisVarScope *scope) {
66             int i;
67 498 50         if (!scope) return;
68 562 100         for (i = 0; i < scope->count; i++) {
69 64           free(scope->vars[i].name);
70 64           free(scope->vars[i].value);
71             }
72 498 100         if (scope->vars) free(scope->vars);
73 498           free(scope);
74             }
75              
76             /* ── Scope set/get ────────────────────────────────────────── */
77              
78 64           static void litavis_scope_set(LitavisVarScope *scope, const char *name, const char *value) {
79             int i;
80             /* Update if already exists in this scope */
81 78 100         for (i = 0; i < scope->count; i++) {
82 14 50         if (strcmp(scope->vars[i].name, name) == 0) {
83 0           free(scope->vars[i].value);
84 0           scope->vars[i].value = litavis_strdup(value);
85 0           return;
86             }
87             }
88             /* Add new */
89 64 100         if (scope->count >= scope->capacity) {
90 52 50         int new_cap = scope->capacity < 8 ? 8 : scope->capacity * 2;
91 52           LitavisVar *nv = (LitavisVar*)realloc(scope->vars, sizeof(LitavisVar) * (size_t)new_cap);
92 52 50         if (!nv) LITAVIS_FATAL("out of memory");
93 52           scope->vars = nv;
94 52           scope->capacity = new_cap;
95             }
96 64           scope->vars[scope->count].name = litavis_strdup(name);
97 64           scope->vars[scope->count].value = litavis_strdup(value);
98 64           scope->count++;
99             }
100              
101             /* Walk parent chain to find variable */
102 76           static char* litavis_scope_get(LitavisVarScope *scope, const char *name) {
103             int i;
104 76           LitavisVarScope *s = scope;
105 80 100         while (s) {
106 89 100         for (i = 0; i < s->count; i++) {
107 85 100         if (strcmp(s->vars[i].name, name) == 0)
108 72           return s->vars[i].value;
109             }
110 4           s = s->parent;
111             }
112 4           return NULL;
113             }
114              
115             /* ── Mixin store ──────────────────────────────────────────── */
116              
117 498           static LitavisMixinStore* litavis_mixin_store_new(void) {
118 498           LitavisMixinStore *s = (LitavisMixinStore*)malloc(sizeof(LitavisMixinStore));
119 498 50         if (!s) LITAVIS_FATAL("out of memory");
120 498           s->mixins = NULL;
121 498           s->count = 0;
122 498           s->capacity = 0;
123 498           return s;
124             }
125              
126 498           static void litavis_mixin_store_free(LitavisMixinStore *store) {
127             int i, j;
128 498 50         if (!store) return;
129 519 100         for (i = 0; i < store->count; i++) {
130 21           free(store->mixins[i].name);
131 72 100         for (j = 0; j < store->mixins[i].prop_count; j++) {
132 51           free(store->mixins[i].props[j].key);
133 51           free(store->mixins[i].props[j].value);
134             }
135 21 50         if (store->mixins[i].props) free(store->mixins[i].props);
136             }
137 498 100         if (store->mixins) free(store->mixins);
138 498           free(store);
139             }
140              
141 21           static void litavis_mixin_define(LitavisMixinStore *store, const char *name,
142             LitavisProp *props, int prop_count) {
143             int i;
144 21 100         if (store->count >= store->capacity) {
145 19 50         int new_cap = store->capacity < 4 ? 4 : store->capacity * 2;
146 19           LitavisMixin *nm = (LitavisMixin*)realloc(store->mixins, sizeof(LitavisMixin) * (size_t)new_cap);
147 19 50         if (!nm) LITAVIS_FATAL("out of memory");
148 19           store->mixins = nm;
149 19           store->capacity = new_cap;
150             }
151 21           LitavisMixin *m = &store->mixins[store->count++];
152 21           m->name = litavis_strdup(name);
153 21           m->prop_count = prop_count;
154 21           m->prop_cap = prop_count;
155 21 50         m->props = (LitavisProp*)malloc(sizeof(LitavisProp) * (size_t)(prop_count > 0 ? prop_count : 1));
156 21 50         if (!m->props) LITAVIS_FATAL("out of memory");
157 72 100         for (i = 0; i < prop_count; i++) {
158 51           m->props[i].key = litavis_strdup(props[i].key);
159 51           m->props[i].value = litavis_strdup(props[i].value);
160             }
161 21           }
162              
163 19           static LitavisMixin* litavis_mixin_get(LitavisMixinStore *store, const char *name) {
164             int i;
165 21 100         for (i = 0; i < store->count; i++) {
166 20 100         if (strcmp(store->mixins[i].name, name) == 0)
167 18           return &store->mixins[i];
168             }
169 1           return NULL;
170             }
171              
172             /* ── Map store ────────────────────────────────────────────── */
173              
174 498           static LitavisMapStore* litavis_map_store_new(void) {
175 498           LitavisMapStore *s = (LitavisMapStore*)malloc(sizeof(LitavisMapStore));
176 498 50         if (!s) LITAVIS_FATAL("out of memory");
177 498           s->maps = NULL;
178 498           s->count = 0;
179 498           s->capacity = 0;
180 498           return s;
181             }
182              
183 498           static void litavis_map_store_free(LitavisMapStore *store) {
184             int i, j;
185 498 50         if (!store) return;
186 519 100         for (i = 0; i < store->count; i++) {
187 21           free(store->maps[i].name);
188 72 100         for (j = 0; j < store->maps[i].count; j++) {
189 51           free(store->maps[i].entries[j].key);
190 51           free(store->maps[i].entries[j].value);
191             }
192 21 50         if (store->maps[i].entries) free(store->maps[i].entries);
193             }
194 498 100         if (store->maps) free(store->maps);
195 498           free(store);
196             }
197              
198 21           static void litavis_map_define(LitavisMapStore *store, const char *name,
199             LitavisMapEntry *entries, int count) {
200             int i;
201 21 100         if (store->count >= store->capacity) {
202 19 50         int new_cap = store->capacity < 4 ? 4 : store->capacity * 2;
203 19           LitavisMapVar *nm = (LitavisMapVar*)realloc(store->maps, sizeof(LitavisMapVar) * (size_t)new_cap);
204 19 50         if (!nm) LITAVIS_FATAL("out of memory");
205 19           store->maps = nm;
206 19           store->capacity = new_cap;
207             }
208 21           LitavisMapVar *m = &store->maps[store->count++];
209 21           m->name = litavis_strdup(name);
210 21           m->count = count;
211 21           m->capacity = count;
212 21 50         m->entries = (LitavisMapEntry*)malloc(sizeof(LitavisMapEntry) * (size_t)(count > 0 ? count : 1));
213 21 50         if (!m->entries) LITAVIS_FATAL("out of memory");
214 72 100         for (i = 0; i < count; i++) {
215 51           m->entries[i].key = litavis_strdup(entries[i].key);
216 51           m->entries[i].value = litavis_strdup(entries[i].value);
217             }
218 21           }
219              
220 11           static char* litavis_map_get(LitavisMapStore *store, const char *name, const char *key) {
221             int i, j;
222 12 100         for (i = 0; i < store->count; i++) {
223 11 50         if (strcmp(store->maps[i].name, name) == 0) {
224 21 100         for (j = 0; j < store->maps[i].count; j++) {
225 20 100         if (strcmp(store->maps[i].entries[j].key, key) == 0)
226 10           return store->maps[i].entries[j].value;
227             }
228             }
229             }
230 1           return NULL;
231             }
232              
233             /* ── Value resolution ─────────────────────────────────────── */
234              
235             /*
236             * Resolve $var and $map{key} references within a value string.
237             * Returns a new allocated string. Caller must free.
238             */
239 84           static char* litavis_resolve_value(const char *value, LitavisVarScope *scope,
240             LitavisMapStore *maps) {
241             char buf[8192];
242 84           int bpos = 0;
243 84           const char *p = value;
244              
245 295 100         while (*p && bpos < 8100) {
    50          
246 211 100         if (*p == '$') {
247             /* Scan variable name */
248 86           const char *name_start = p + 1;
249 86           const char *name_end = name_start;
250 461 100         while (*name_end && (isalnum((unsigned char)*name_end) || *name_end == '-' || *name_end == '_'))
    100          
    100          
    50          
251 375           name_end++;
252              
253 86 50         if (name_end > name_start) {
254 86           int name_len = (int)(name_end - name_start);
255             char name[256];
256 86 50         if (name_len > 255) name_len = 255;
257 86           memcpy(name, name_start, (size_t)name_len);
258 86           name[name_len] = '\0';
259              
260             /* Check for map access: $name{key} */
261 86 100         if (*name_end == '{') {
262 11           const char *key_start = name_end + 1;
263 11           const char *key_end = key_start;
264 67 50         while (*key_end && *key_end != '}')
    100          
265 56           key_end++;
266 11 50         if (*key_end == '}') {
267 11           int key_len = (int)(key_end - key_start);
268             char key[256];
269 11 50         if (key_len > 255) key_len = 255;
270 11           memcpy(key, key_start, (size_t)key_len);
271 11           key[key_len] = '\0';
272              
273 11 50         char *map_val = maps ? litavis_map_get(maps, name, key) : NULL;
274 11 100         if (map_val) {
275 10           int vlen = (int)strlen(map_val);
276 10 50         if (bpos + vlen < 8192) {
277 10           memcpy(buf + bpos, map_val, (size_t)vlen);
278 10           bpos += vlen;
279             }
280 10           p = key_end + 1;
281 10           continue;
282             }
283             }
284             }
285              
286             /* Simple variable lookup */
287 76           char *var_val = litavis_scope_get(scope, name);
288 76 100         if (var_val) {
289 72           int vlen = (int)strlen(var_val);
290 72 50         if (bpos + vlen < 8192) {
291 72           memcpy(buf + bpos, var_val, (size_t)vlen);
292 72           bpos += vlen;
293             }
294 72           p = name_end;
295 72           continue;
296             }
297             }
298             /* Not a recognized variable — copy $ literally */
299 4           buf[bpos++] = *p++;
300             } else {
301 125           buf[bpos++] = *p++;
302             }
303             }
304 84           buf[bpos] = '\0';
305 84           return litavis_strdup(buf);
306             }
307              
308             /* ── Parse mixin body into properties ─────────────────────── */
309              
310             /*
311             * Parse a mixin body string like "border-top: dotted 1px black; border-bottom: solid 2px black"
312             * into LitavisProp array.
313             */
314 21           static int litavis_parse_mixin_body(const char *body, LitavisProp *out_props, int max_props) {
315 21           int count = 0;
316 21           const char *p = body;
317              
318 72 100         while (*p && count < max_props) {
    50          
319             /* Skip whitespace */
320 560 100         while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
    100          
    50          
    100          
    50          
321 64 100         if (!*p) break;
322              
323             /* Check for mixin reference: %name; (no colon) */
324 51 100         if (*p == '%') {
325 1           const char *ref_start = p;
326 1           p++; /* skip % */
327 6 50         while (*p && (isalnum((unsigned char)*p) || *p == '-' || *p == '_'))
    100          
    50          
    50          
328 5           p++;
329 1           int ref_len = (int)(p - ref_start);
330             /* Skip trailing whitespace and semicolon */
331 1 50         while (*p && (*p == ' ' || *p == '\t')) p++;
    50          
    50          
332 1 50         if (*p == ';') p++;
333              
334 1 50         if (ref_len > 1 && count < max_props) {
    50          
335 1           char *key = (char*)malloc((size_t)(ref_len + 1));
336 1 50         if (!key) LITAVIS_FATAL("out of memory");
337 1           memcpy(key, ref_start, (size_t)ref_len);
338 1           key[ref_len] = '\0';
339 1           out_props[count].key = key;
340 1           out_props[count].value = litavis_strdup("");
341 1           count++;
342             }
343 1           continue;
344             }
345              
346             /* Scan property name up to : */
347 50           const char *key_start = p;
348 379 50         while (*p && *p != ':' && *p != ';') p++;
    100          
    50          
349 50 50         if (*p != ':') { if (*p == ';') p++; continue; }
    0          
350              
351 50           int key_len = (int)(p - key_start);
352             /* Trim trailing ws from key */
353 50 50         while (key_len > 0 && (key_start[key_len - 1] == ' ' || key_start[key_len - 1] == '\t'))
    50          
    50          
354 0           key_len--;
355              
356 50           p++; /* skip : */
357             /* Skip whitespace */
358 100 50         while (*p && (*p == ' ' || *p == '\t')) p++;
    100          
    50          
359              
360             /* Scan value up to ; or end */
361 50           const char *val_start = p;
362 339 50         while (*p && *p != ';') p++;
    100          
363 50           int val_len = (int)(p - val_start);
364             /* Trim trailing ws from value */
365 50 50         while (val_len > 0 && (val_start[val_len - 1] == ' ' || val_start[val_len - 1] == '\t'))
    50          
    50          
366 0           val_len--;
367              
368 50 50         if (key_len > 0) {
369 50           char *key = (char*)malloc((size_t)(key_len + 1));
370 50           char *val = (char*)malloc((size_t)(val_len + 1));
371 50 50         if (!key || !val) LITAVIS_FATAL("out of memory");
    50          
372 50           memcpy(key, key_start, (size_t)key_len);
373 50           key[key_len] = '\0';
374 50           memcpy(val, val_start, (size_t)val_len);
375 50           val[val_len] = '\0';
376 50           out_props[count].key = key;
377 50           out_props[count].value = val;
378 50           count++;
379             }
380              
381 50 50         if (*p == ';') p++;
382             }
383 21           return count;
384             }
385              
386             /* ── Main resolution ──────────────────────────────────────── */
387              
388             /*
389             * Two-pass resolution:
390             * Pass 1: Collect all $var, %mixin, %map definitions (hoisted)
391             * Pass 2: Resolve references in property values and expand mixins
392             */
393 211           static void litavis_resolve_vars(LitavisAST *ast, LitavisVarScope *global_scope,
394             LitavisMixinStore *mixins, LitavisMapStore *maps) {
395             int i, j, k;
396              
397             /* ── Pass 1: Collect definitions ──────────────────────── */
398 211           i = 0;
399 865 100         while (i < ast->count) {
400 654           LitavisRule *rule = &ast->rules[i];
401              
402             /* Check if this rule is a $var definition (selector starts with $) */
403 654 100         if (rule->selector[0] == '$') {
404             /* $varname → value stored as prop */
405 64           const char *var_name = rule->selector + 1; /* skip $ */
406 64 50         if (rule->prop_count > 0) {
407 64           litavis_scope_set(global_scope, var_name, rule->props[0].value);
408             }
409 64           litavis_ast_remove_rule(ast, i);
410 64           continue; /* don't increment i */
411             }
412              
413             /* Check if this rule is a %mixin or %map definition */
414 590 100         if (rule->selector[0] == '%' && rule->is_at_rule) {
    50          
415 21           const char *name = rule->selector + 1; /* skip % */
416 21 50         if (rule->prop_count > 0) {
417             /* Check if the value looks like map entries (key: value; ...) */
418 21           char *body = rule->props[0].value;
419              
420             /* Parse body as properties for mixin */
421             LitavisProp body_props[64];
422 21           int body_count = litavis_parse_mixin_body(body, body_props, 64);
423              
424 21 50         if (body_count > 0) {
425             /* Could be a map or a mixin — if keys look like CSS properties, it's a mixin
426             For simplicity: store as both mixin (for %name; expansion) and map (for $name{key}) */
427             LitavisMapEntry entries[64];
428 72 100         for (j = 0; j < body_count; j++) {
429 51           entries[j].key = body_props[j].key;
430 51           entries[j].value = body_props[j].value;
431             }
432 21           litavis_map_define(maps, name, entries, body_count);
433 21           litavis_mixin_define(mixins, name, body_props, body_count);
434              
435             /* Free temp props */
436 72 100         for (j = 0; j < body_count; j++) {
437 51           free(body_props[j].key);
438 51           free(body_props[j].value);
439             }
440             }
441             }
442 21           litavis_ast_remove_rule(ast, i);
443 21           continue;
444             }
445              
446             /* Scan properties within rules for $var definitions */
447 569           j = 0;
448 1243 100         while (j < rule->prop_count) {
449 674 50         if (rule->props[j].key[0] == '$') {
450             /* $var: value inside a rule — collect but keep scope global for now */
451 0           const char *vname = rule->props[j].key + 1;
452 0           litavis_scope_set(global_scope, vname, rule->props[j].value);
453             /* Remove this property */
454 0           free(rule->props[j].key);
455 0           free(rule->props[j].value);
456 0 0         for (k = j; k < rule->prop_count - 1; k++) {
457 0           rule->props[k] = rule->props[k + 1];
458             }
459 0           rule->prop_count--;
460 0           continue; /* don't increment j */
461             }
462 674           j++;
463             }
464              
465 569           i++;
466             }
467              
468             /* ── Pass 1b: Resolve chained variable values ──────────── */
469             /* Variables may reference other variables ($primary: $base) */
470             {
471 211           int changed = 1;
472 211           int max_passes = 10; /* prevent infinite loops */
473 424 100         while (changed && max_passes-- > 0) {
    50          
474 213           changed = 0;
475 282 100         for (i = 0; i < global_scope->count; i++) {
476 69 100         if (strchr(global_scope->vars[i].value, '$')) {
477 3           char *resolved = litavis_resolve_value(global_scope->vars[i].value, global_scope, maps);
478 3 50         if (strcmp(resolved, global_scope->vars[i].value) != 0) {
479 3           free(global_scope->vars[i].value);
480 3           global_scope->vars[i].value = resolved;
481 3           changed = 1;
482             } else {
483 0           free(resolved);
484             }
485             }
486             }
487             }
488             }
489              
490             /* ── Pass 2: Resolve references ───────────────────────── */
491 780 100         for (i = 0; i < ast->count; i++) {
492 569           LitavisRule *rule = &ast->rules[i];
493              
494 569           j = 0;
495 1287 100         while (j < rule->prop_count) {
496             /* Check for mixin reference (%name) */
497 718 100         if (rule->props[j].key[0] == '%') {
498 19           const char *mixin_name = rule->props[j].key + 1;
499 19           LitavisMixin *mixin = litavis_mixin_get(mixins, mixin_name);
500 19 100         if (mixin && mixin->prop_count > 0) {
    50          
501             /* Remove the mixin ref property */
502 18           free(rule->props[j].key);
503 18           free(rule->props[j].value);
504 25 100         for (k = j; k < rule->prop_count - 1; k++) {
505 7           rule->props[k] = rule->props[k + 1];
506             }
507 18           rule->prop_count--;
508              
509             /* Insert mixin properties at position j */
510 18           int need = rule->prop_count + mixin->prop_count;
511 18 100         if (need > rule->prop_cap) {
512 10           int new_cap = need * 2;
513 10           LitavisProp *np = (LitavisProp*)realloc(rule->props, sizeof(LitavisProp) * (size_t)new_cap);
514 10 50         if (!np) LITAVIS_FATAL("out of memory");
515 10           rule->props = np;
516 10           rule->prop_cap = new_cap;
517             }
518             /* Shift existing props after j to make room */
519 25 100         for (k = rule->prop_count - 1; k >= j; k--) {
520 7           rule->props[k + mixin->prop_count] = rule->props[k];
521             }
522             /* Insert mixin props */
523 62 100         for (k = 0; k < mixin->prop_count; k++) {
524 44           rule->props[j + k].key = litavis_strdup(mixin->props[k].key);
525 44           rule->props[j + k].value = litavis_strdup(mixin->props[k].value);
526             }
527 18           rule->prop_count += mixin->prop_count;
528             /* Don't advance j — re-examine inserted props for
529             nested mixin refs and $var resolution */
530 18           continue;
531             } else {
532             /* Unknown mixin — remove the reference */
533 1           free(rule->props[j].key);
534 1           free(rule->props[j].value);
535 1 50         for (k = j; k < rule->prop_count - 1; k++) {
536 0           rule->props[k] = rule->props[k + 1];
537             }
538 1           rule->prop_count--;
539 1           continue;
540             }
541             }
542              
543             /* Resolve $var references in property values */
544 699 100         if (strchr(rule->props[j].value, '$')) {
545 81           char *resolved = litavis_resolve_value(rule->props[j].value, global_scope, maps);
546 81           free(rule->props[j].value);
547 81           rule->props[j].value = resolved;
548             }
549              
550             /* Also resolve $var in custom property values (--var: $something) */
551              
552 699           j++;
553             }
554             }
555 211           }
556              
557             #endif /* LITAVIS_VARS_H */