File Coverage

include/litavis_cascade.h
Criterion Covered Total %
statement 80 84 95.2
branch 53 58 91.3
condition n/a
subroutine n/a
pod n/a
total 133 142 93.6


line stmt bran cond sub pod time code
1             #ifndef LITAVIS_CASCADE_H
2             #define LITAVIS_CASCADE_H
3              
4             /* ── Dedup strategy enum ──────────────────────────────────── */
5              
6             typedef enum {
7             LITAVIS_DEDUPE_OFF = 0, /* no deduplication */
8             LITAVIS_DEDUPE_CONSERVATIVE = 1, /* merge only when provably safe */
9             LITAVIS_DEDUPE_AGGRESSIVE = 2 /* merge all identical, ignore cascade */
10             } LitavisDedupeStrategy;
11              
12             /* ── Comparison helpers ───────────────────────────────────── */
13              
14             /*
15             * Deep compare two rules' properties.
16             * Returns 1 if same keys and values in same order, 0 otherwise.
17             */
18 156           static int litavis_props_equal(LitavisRule *a, LitavisRule *b) {
19             int i;
20 156 100         if (a->prop_count != b->prop_count) return 0;
21 295 100         for (i = 0; i < a->prop_count; i++) {
22 205 100         if (strcmp(a->props[i].key, b->props[i].key) != 0) return 0;
23 190 100         if (strcmp(a->props[i].value, b->props[i].value) != 0) return 0;
24             }
25 90           return 1;
26             }
27              
28             /*
29             * Check if rule 'between' defines any property that also appears in 'target'.
30             * Returns 1 if conflict found (NOT safe to merge across), 0 if safe.
31             */
32 15           static int litavis_has_prop_conflict(LitavisRule *target, LitavisRule *between) {
33             int i, j;
34 29 100         for (i = 0; i < target->prop_count; i++) {
35 31 100         for (j = 0; j < between->prop_count; j++) {
36 17 100         if (strcmp(target->props[i].key, between->props[j].key) == 0) {
37 5           return 1; /* conflict found */
38             }
39             }
40             }
41 10           return 0;
42             }
43              
44             /*
45             * Check if it is safe to merge rules at indices i and j.
46             * Examines all rules between i and j for property conflicts.
47             */
48 85           static int litavis_safe_to_merge(LitavisAST *ast, int i, int j) {
49             int k;
50 95 100         for (k = i + 1; k < j; k++) {
51 15 100         if (litavis_has_prop_conflict(&ast->rules[i], &ast->rules[k])) {
52 5           return 0;
53             }
54             }
55 80           return 1;
56             }
57              
58             /* ── Join two selectors with ", " ─────────────────────────── */
59              
60 85           static char* litavis_join_selectors(const char *a, const char *b) {
61 85           int alen = (int)strlen(a);
62 85           int blen = (int)strlen(b);
63 85           char *combined = (char*)malloc((size_t)(alen + 2 + blen + 1));
64 85 50         if (!combined) LITAVIS_FATAL("out of memory");
65 85           memcpy(combined, a, (size_t)alen);
66 85           combined[alen] = ',';
67 85           combined[alen + 1] = ' ';
68 85           memcpy(combined + alen + 2, b, (size_t)blen);
69 85           combined[alen + 2 + blen] = '\0';
70 85           return combined;
71             }
72              
73             /* ── Same-selector merging ────────────────────────────────── */
74              
75             /*
76             * Merge rules that share the same selector.
77             * Later properties overwrite earlier ones (matches browser behaviour).
78             * Always runs regardless of strategy — this is never wrong.
79             */
80 350           static void litavis_merge_same_selectors(LitavisAST *ast) {
81             int i, j;
82 1170 100         for (i = 0; i < ast->count; i++) {
83 27170 100         for (j = i + 1; j < ast->count; ) {
84 26350 50         if (strcmp(ast->rules[i].selector, ast->rules[j].selector) == 0) {
85             /* Merge j's properties into i. For conflicts, j wins
86             * (later in cascade). Then remove j. */
87 0           litavis_rule_merge_props(&ast->rules[i], &ast->rules[j]);
88 0           litavis_ast_remove_rule(ast, j);
89             /* Don't increment j — re-check this index */
90             } else {
91 26350           j++;
92             }
93             }
94             }
95 350           }
96              
97             /* ── Conservative dedup ───────────────────────────────────── */
98              
99             /*
100             * Merge rules at positions i and j (i < j) with identical properties
101             * ONLY when no intervening rule k (i < k < j) shares any property name.
102             */
103 168           static void litavis_dedupe_conservative(LitavisAST *ast) {
104 168           int i = 0;
105 442 100         while (i < ast->count) {
106             int j, merged;
107              
108             /* Skip @-rules — dedupe recursively within, never across */
109 274 100         if (ast->rules[i].is_at_rule) {
110 6           i++;
111 6           continue;
112             }
113              
114 268           merged = 0;
115 333 100         for (j = i + 1; j < ast->count; j++) {
116 145 100         if (ast->rules[j].is_at_rule) continue;
117              
118 141 100         if (litavis_props_equal(&ast->rules[i], &ast->rules[j])) {
119 85 100         if (litavis_safe_to_merge(ast, i, j)) {
120             /* Combine selectors: ".a" + ".b" → ".a, .b" */
121 80           char *combined = litavis_join_selectors(
122 80           ast->rules[i].selector,
123 80           ast->rules[j].selector
124             );
125 80           litavis_ast_rename_rule(ast, i, combined);
126 80           litavis_ast_remove_rule(ast, j);
127 80           free(combined);
128 80           merged = 1;
129 80           break; /* re-check from same i */
130             }
131             }
132             }
133              
134 268 100         if (!merged) i++;
135             }
136 168           }
137              
138             /* ── Aggressive dedup ─────────────────────────────────────── */
139              
140             /*
141             * Merge ALL rules with identical properties regardless of position.
142             * User accepts cascade reordering. Good for atomic/utility CSS.
143             */
144 5           static void litavis_dedupe_aggressive(LitavisAST *ast) {
145 5           int i = 0;
146 20 100         while (i < ast->count) {
147             int j, merged;
148              
149 15 50         if (ast->rules[i].is_at_rule) {
150 0           i++;
151 0           continue;
152             }
153              
154 15           merged = 0;
155 25 100         for (j = i + 1; j < ast->count; j++) {
156 15 50         if (ast->rules[j].is_at_rule) continue;
157              
158 15 100         if (litavis_props_equal(&ast->rules[i], &ast->rules[j])) {
159 5           char *combined = litavis_join_selectors(
160 5           ast->rules[i].selector,
161 5           ast->rules[j].selector
162             );
163 5           litavis_ast_rename_rule(ast, i, combined);
164 5           litavis_ast_remove_rule(ast, j);
165 5           free(combined);
166 5           merged = 1;
167 5           break; /* re-check from same i */
168             }
169             }
170              
171 15 100         if (!merged) i++;
172             }
173 5           }
174              
175             /* ── Main entry point ─────────────────────────────────────── */
176              
177             /*
178             * Run deduplication on a flat AST.
179             * Modifies ast in-place. Operates in insertion order.
180             *
181             * Phase 1: merge same-selector rules (always safe)
182             * Phase 2: merge different-selector rules with identical properties
183             */
184 175           static void litavis_dedupe(LitavisAST *ast, LitavisDedupeStrategy strategy) {
185 175 100         if (strategy == LITAVIS_DEDUPE_OFF) return;
186              
187             /* Always merge same selectors first */
188 173           litavis_merge_same_selectors(ast);
189              
190             /* Then apply cross-selector dedup based on strategy */
191 173 100         if (strategy == LITAVIS_DEDUPE_CONSERVATIVE) {
192 168           litavis_dedupe_conservative(ast);
193 5 50         } else if (strategy == LITAVIS_DEDUPE_AGGRESSIVE) {
194 5           litavis_dedupe_aggressive(ast);
195             }
196             }
197              
198             #endif /* LITAVIS_CASCADE_H */