File Coverage

include/litavis_emitter.h
Criterion Covered Total %
statement 147 169 86.9
branch 72 94 76.6
condition n/a
subroutine n/a
pod n/a
total 219 263 83.2


line stmt bran cond sub pod time code
1             #ifndef LITAVIS_EMITTER_H
2             #define LITAVIS_EMITTER_H
3              
4             /* ── Growable output buffer ──────────────────────────────── */
5              
6             typedef struct {
7             char *data;
8             int length;
9             int capacity;
10             } LitavisBuffer;
11              
12 177           static LitavisBuffer* litavis_buffer_new(int initial_capacity) {
13 177           LitavisBuffer *buf = (LitavisBuffer*)malloc(sizeof(LitavisBuffer));
14 177 50         if (!buf) LITAVIS_FATAL("out of memory");
15 177           buf->data = (char*)malloc((size_t)initial_capacity);
16 177 50         if (!buf->data) LITAVIS_FATAL("out of memory");
17 177           buf->length = 0;
18 177           buf->capacity = initial_capacity;
19 177           buf->data[0] = '\0';
20 177           return buf;
21             }
22              
23 4194           static void litavis_buffer_ensure(LitavisBuffer *buf, int extra) {
24 4194 50         if (buf->length + extra + 1 > buf->capacity) {
25 0           int new_cap = (buf->length + extra + 1) * 2;
26 0           char *nd = (char*)realloc(buf->data, (size_t)new_cap);
27 0 0         if (!nd) LITAVIS_FATAL("out of memory");
28 0           buf->data = nd;
29 0           buf->capacity = new_cap;
30             }
31 4194           }
32              
33 1934           static void litavis_buffer_append(LitavisBuffer *buf, const char *str, int len) {
34 1934 100         if (len <= 0) return;
35 1933           litavis_buffer_ensure(buf, len);
36 1933           memcpy(buf->data + buf->length, str, (size_t)len);
37 1933           buf->length += len;
38 1933           buf->data[buf->length] = '\0';
39             }
40              
41 2261           static void litavis_buffer_append_char(LitavisBuffer *buf, char c) {
42 2261           litavis_buffer_ensure(buf, 1);
43 2261           buf->data[buf->length++] = c;
44 2261           buf->data[buf->length] = '\0';
45 2261           }
46              
47 1304           static void litavis_buffer_append_str(LitavisBuffer *buf, const char *str) {
48 1304           litavis_buffer_append(buf, str, (int)strlen(str));
49 1304           }
50              
51             /* Returns owned string, frees the buffer struct (but not the data) */
52 177           static char* litavis_buffer_to_string(LitavisBuffer *buf) {
53 177           char *result = buf->data;
54 177           free(buf);
55 177           return result;
56             }
57              
58 0           static void litavis_buffer_free(LitavisBuffer *buf) {
59 0 0         if (!buf) return;
60 0 0         if (buf->data) free(buf->data);
61 0           free(buf);
62             }
63              
64             /* ── Emitter configuration ───────────────────────────────── */
65              
66             typedef struct {
67             int pretty; /* 0 = minified, 1 = pretty-printed */
68             char *indent; /* indent string (default: " ") */
69             int shorthand_hex; /* 1 = #fff not #ffffff */
70             int sort_props; /* 1 = alphabetise props */
71             } LitavisEmitConfig;
72              
73             /* ── Hex shorthand optimisation ──────────────────────────── */
74              
75 630           static void litavis_emit_hex_value(const char *value, LitavisBuffer *buf, int shorthand) {
76 630           int len = (int)strlen(value);
77 630 50         if (shorthand && value[0] == '#' && len == 7) {
    100          
    100          
78 30 100         if (value[1] == value[2] && value[3] == value[4] && value[5] == value[6]) {
    100          
    100          
79             char short_hex[5];
80 13           short_hex[0] = '#';
81 13           short_hex[1] = value[1];
82 13           short_hex[2] = value[3];
83 13           short_hex[3] = value[5];
84 13           short_hex[4] = '\0';
85 13           litavis_buffer_append(buf, short_hex, 4);
86 13           return;
87             }
88             }
89 617           litavis_buffer_append(buf, value, len);
90             }
91              
92             /* ── Property sorting ────────────────────────────────────── */
93              
94 30           static int litavis_prop_cmp(const void *a, const void *b) {
95 30           const LitavisProp *pa = (const LitavisProp*)a;
96 30           const LitavisProp *pb = (const LitavisProp*)b;
97 30           return strcmp(pa->key, pb->key);
98             }
99              
100 16           static void litavis_sort_props(LitavisProp *props, int count) {
101 16 100         if (count > 1)
102 12           qsort(props, (size_t)count, sizeof(LitavisProp), litavis_prop_cmp);
103 16           }
104              
105             /* ── Indent helper ───────────────────────────────────────── */
106              
107 96           static void litavis_emit_indent(LitavisBuffer *buf, const char *indent, int depth) {
108             int i;
109 144 100         for (i = 0; i < depth; i++)
110 48           litavis_buffer_append_str(buf, indent);
111 96           }
112              
113             /* ── Emit-first detection (@charset, @import) ───────────── */
114              
115 1036           static int litavis_is_emit_first(LitavisRule *rule) {
116 1036 100         if (!rule->is_at_rule) return 0;
117 22 100         if (strncmp(rule->selector, "@charset", 8) == 0) return 1;
118 20 100         if (strncmp(rule->selector, "@import", 7) == 0) return 1;
119 16           return 0;
120             }
121              
122             /* ── Forward declarations ────────────────────────────────── */
123              
124             static void litavis_emit_rule(LitavisRule *rule, LitavisEmitConfig *config,
125             LitavisBuffer *buf, int depth);
126             static void litavis_emit_at_rule(LitavisRule *rule, LitavisEmitConfig *config,
127             LitavisBuffer *buf, int depth);
128              
129             /* ── Emit declarations (properties) ──────────────────────── */
130              
131 517           static void litavis_emit_declarations(LitavisRule *rule, LitavisEmitConfig *config,
132             LitavisBuffer *buf, int depth) {
133             int i;
134 517 100         if (config->sort_props)
135 16           litavis_sort_props(rule->props, rule->prop_count);
136              
137 1148 100         for (i = 0; i < rule->prop_count; i++) {
138 631 100         if (config->pretty)
139 42           litavis_emit_indent(buf, config->indent, depth);
140              
141 631           litavis_buffer_append_str(buf, rule->props[i].key);
142              
143 631 100         if (config->pretty)
144 42           litavis_buffer_append_str(buf, ": ");
145             else
146 589           litavis_buffer_append_char(buf, ':');
147              
148 631 100         if (config->shorthand_hex)
149 630           litavis_emit_hex_value(rule->props[i].value, buf, 1);
150             else
151 1           litavis_buffer_append_str(buf, rule->props[i].value);
152              
153             /* Semicolon: always emit (including trailing) */
154 631           litavis_buffer_append_char(buf, ';');
155              
156 631 100         if (config->pretty)
157 42           litavis_buffer_append_char(buf, '\n');
158             }
159 517           }
160              
161             /* ── Emit a regular rule ─────────────────────────────────── */
162              
163 517           static void litavis_emit_rule(LitavisRule *rule, LitavisEmitConfig *config,
164             LitavisBuffer *buf, int depth) {
165             /* Skip empty rules */
166 517 50         if (rule->prop_count == 0 && rule->child_count == 0) return;
    0          
167              
168 517 100         if (config->pretty)
169 25           litavis_emit_indent(buf, config->indent, depth);
170              
171 517           litavis_buffer_append_str(buf, rule->selector);
172              
173 517 100         if (config->pretty) {
174 25           litavis_buffer_append_str(buf, " {\n");
175             } else {
176 492           litavis_buffer_append_char(buf, '{');
177             }
178              
179 517           litavis_emit_declarations(rule, config, buf, depth + 1);
180              
181 517 100         if (config->pretty) {
182 25           litavis_emit_indent(buf, config->indent, depth);
183 25           litavis_buffer_append_str(buf, "}\n");
184             } else {
185 492           litavis_buffer_append_char(buf, '}');
186             }
187             }
188              
189             /* ── Emit an @-rule ──────────────────────────────────────── */
190              
191 11           static void litavis_emit_at_rule(LitavisRule *rule, LitavisEmitConfig *config,
192             LitavisBuffer *buf, int depth) {
193 11 100         if (config->pretty)
194 2           litavis_emit_indent(buf, config->indent, depth);
195              
196             /* @keyword + prelude (selector already includes prelude, e.g. "@media (max-width: 768px)") */
197 11           litavis_buffer_append_str(buf, rule->selector);
198              
199 11 100         if (rule->child_count > 0) {
200             /* Block @-rule: @media (...) { ... } */
201 8 100         if (config->pretty) {
202 2           litavis_buffer_append_str(buf, " {\n");
203             } else {
204 6           litavis_buffer_append_char(buf, '{');
205             }
206              
207             /* Emit children (nested rules) */
208             int i;
209 18 100         for (i = 0; i < rule->child_count; i++) {
210 10 50         if (rule->children[i].is_at_rule)
211 0           litavis_emit_at_rule(&rule->children[i], config, buf, depth + 1);
212             else
213 10           litavis_emit_rule(&rule->children[i], config, buf, depth + 1);
214             }
215              
216 8 100         if (config->pretty) {
217 2           litavis_emit_indent(buf, config->indent, depth);
218 2           litavis_buffer_append_str(buf, "}\n");
219             } else {
220 6           litavis_buffer_append_char(buf, '}');
221             }
222 3 50         } else if (rule->prop_count > 0) {
223             /* @-rule with own properties (e.g. @font-face) */
224 0 0         if (config->pretty) {
225 0           litavis_buffer_append_str(buf, " {\n");
226             } else {
227 0           litavis_buffer_append_char(buf, '{');
228             }
229              
230 0           litavis_emit_declarations(rule, config, buf, depth + 1);
231              
232 0 0         if (config->pretty) {
233 0           litavis_emit_indent(buf, config->indent, depth);
234 0           litavis_buffer_append_str(buf, "}\n");
235             } else {
236 0           litavis_buffer_append_char(buf, '}');
237             }
238             } else {
239             /* Statement @-rule: @import url(...); */
240 3           litavis_buffer_append_char(buf, ';');
241 3 50         if (config->pretty)
242 0           litavis_buffer_append_char(buf, '\n');
243             }
244 11           }
245              
246             /* ── Emit full AST ───────────────────────────────────────── */
247              
248 177           static void litavis_emit_ast(LitavisAST *ast, LitavisEmitConfig *config, LitavisBuffer *buf) {
249             int i;
250              
251             /* Pass 1: emit @charset and @import first */
252 695 100         for (i = 0; i < ast->count; i++) {
253 518 100         if (litavis_is_emit_first(&ast->rules[i])) {
254 3           litavis_emit_at_rule(&ast->rules[i], config, buf, 0);
255             }
256             }
257              
258             /* Pass 2: emit everything else in order */
259 695 100         for (i = 0; i < ast->count; i++) {
260 518 100         if (litavis_is_emit_first(&ast->rules[i])) continue;
261              
262 515 100         if (ast->rules[i].is_at_rule) {
263 8           litavis_emit_at_rule(&ast->rules[i], config, buf, 0);
264             } else {
265 507           litavis_emit_rule(&ast->rules[i], config, buf, 0);
266             }
267             }
268 177           }
269              
270             /* ── Main entry point: compile and emit ──────────────────── */
271              
272 177           static char* litavis_emit(LitavisCtx *ctx) {
273             /* Clone the AST so compile() is non-destructive */
274 177           LitavisAST *work = litavis_ast_clone(ctx->ast);
275              
276             /* Create temporary stores for resolution */
277 177           LitavisVarScope *scope = litavis_scope_new(NULL);
278 177           LitavisMixinStore *mixins = litavis_mixin_store_new();
279 177           LitavisMapStore *maps = litavis_map_store_new();
280              
281             /* Pipeline: resolve vars → colours → merge → dedupe */
282 177           litavis_resolve_vars(work, scope, mixins, maps);
283 177           litavis_resolve_colours(work);
284 177           litavis_merge_same_selectors(work);
285 177 100         if (ctx->dedupe > 0)
286 143           litavis_dedupe(work, (LitavisDedupeStrategy)ctx->dedupe);
287              
288             /* Configure emitter */
289             LitavisEmitConfig config;
290 177           config.pretty = ctx->pretty;
291 177 50         config.indent = ctx->indent ? ctx->indent : " ";
292 177           config.shorthand_hex = ctx->shorthand_hex;
293 177           config.sort_props = ctx->sort_props;
294              
295             /* Emit */
296 177           LitavisBuffer *buf = litavis_buffer_new(4096);
297 177           litavis_emit_ast(work, &config, buf);
298              
299             /* Cleanup */
300 177           litavis_ast_free(work);
301 177           litavis_scope_free(scope);
302 177           litavis_mixin_store_free(mixins);
303 177           litavis_map_store_free(maps);
304              
305 177           return litavis_buffer_to_string(buf);
306             }
307              
308             /* ── Emit to file ────────────────────────────────────────── */
309              
310 2           static void litavis_emit_file(LitavisCtx *ctx, const char *filename) {
311 2           char *css = litavis_emit(ctx);
312 2           FILE *f = fopen(filename, "wb");
313 2 50         if (!f) {
314 0           free(css);
315             char err[512];
316 0           snprintf(err, sizeof(err), "cannot open file for writing: %s", filename);
317 0           LITAVIS_FATAL(err);
318             }
319 2           fwrite(css, 1, strlen(css), f);
320 2           fclose(f);
321 2           free(css);
322 2           }
323              
324             #endif /* LITAVIS_EMITTER_H */