File Coverage

include/litavis_parser.h
Criterion Covered Total %
statement 372 409 90.9
branch 210 278 75.5
condition n/a
subroutine n/a
pod n/a
total 582 687 84.7


line stmt bran cond sub pod time code
1             #ifndef LITAVIS_PARSER_H
2             #define LITAVIS_PARSER_H
3              
4             #include
5             #include
6             #include
7              
8             /* ── Parser context ───────────────────────────────────────── */
9              
10             typedef struct {
11             LitavisTokenList *tokens;
12             int pos; /* current token index */
13             const char *source_file; /* for error messages */
14             } LitavisParserCtx;
15              
16             /* ── Forward declarations ─────────────────────────────────── */
17              
18             static void litavis_parse_stylesheet(LitavisParserCtx *pctx, LitavisAST *ast, LitavisRule *parent);
19             static void litavis_parse_rule(LitavisParserCtx *pctx, LitavisAST *ast, LitavisRule *parent);
20             static void litavis_parse_at_rule(LitavisParserCtx *pctx, LitavisAST *ast, LitavisRule *parent);
21              
22             /* ── Token helpers ────────────────────────────────────────── */
23              
24 22796           static LitavisToken* litavis_parser_peek(LitavisParserCtx *pctx) {
25 22796 50         if (pctx->pos < pctx->tokens->count)
26 22796           return &pctx->tokens->tokens[pctx->pos];
27 0           return &pctx->tokens->tokens[pctx->tokens->count - 1]; /* EOF */
28             }
29              
30 6716           static LitavisToken* litavis_parser_advance(LitavisParserCtx *pctx) {
31 6716           LitavisToken *t = litavis_parser_peek(pctx);
32 6716 50         if (t->type != LITAVIS_T_EOF)
33 6716           pctx->pos++;
34 6716           return t;
35             }
36              
37 12519           static int litavis_parser_at(LitavisParserCtx *pctx, LitavisTokenType type) {
38 12519           return litavis_parser_peek(pctx)->type == type;
39             }
40              
41 3696           static int litavis_parser_match(LitavisParserCtx *pctx, LitavisTokenType type) {
42 3696 100         if (litavis_parser_at(pctx, type)) {
43 3690           litavis_parser_advance(pctx);
44 3690           return 1;
45             }
46 6           return 0;
47             }
48              
49             /* Extract a null-terminated string from a token (caller must free) */
50 2175           static char* litavis_token_str(LitavisToken *t) {
51 2175           char *s = (char*)malloc((size_t)t->length + 1);
52 2175 50         if (!s) LITAVIS_FATAL("out of memory");
53 2175           memcpy(s, t->start, (size_t)t->length);
54 2175           s[t->length] = '\0';
55 2175           return s;
56             }
57              
58             /* ── Parse a single declaration: property ':' value ';' ───── */
59              
60 975           static void litavis_parse_declaration(LitavisParserCtx *pctx, LitavisRule *rule) {
61 975           LitavisToken *prop_tok = litavis_parser_advance(pctx); /* PROPERTY or CSS_VAR_DEF */
62 975           char *key = litavis_token_str(prop_tok);
63              
64             /* Expect colon */
65 975           litavis_parser_match(pctx, LITAVIS_T_COLON);
66              
67             /* Expect value */
68 975           char *value = NULL;
69 975 100         if (litavis_parser_at(pctx, LITAVIS_T_VALUE)) {
70 974           LitavisToken *val_tok = litavis_parser_advance(pctx);
71 974           value = litavis_token_str(val_tok);
72             } else {
73 1           value = litavis_strdup("");
74             }
75              
76 975           litavis_rule_add_prop(rule, key, value);
77 975           free(key);
78 975           free(value);
79              
80             /* Consume optional semicolon */
81 975           litavis_parser_match(pctx, LITAVIS_T_SEMICOLON);
82 975           }
83              
84             /* ── Parse declarations inside a block ────────────────────── */
85              
86 823           static void litavis_parse_declarations(LitavisParserCtx *pctx, LitavisAST *ast, LitavisRule *rule) {
87 1862 100         while (!litavis_parser_at(pctx, LITAVIS_T_RBRACE) && !litavis_parser_at(pctx, LITAVIS_T_EOF)) {
    100          
88 1039           LitavisTokenType type = litavis_parser_peek(pctx)->type;
89              
90 1039 100         if (type == LITAVIS_T_PROPERTY || type == LITAVIS_T_CSS_VAR_DEF) {
    100          
91 975           litavis_parse_declaration(pctx, rule);
92 64 100         } else if (type == LITAVIS_T_SELECTOR || type == LITAVIS_T_AMPERSAND) {
    50          
93             /* Nested rule */
94 45           litavis_parse_rule(pctx, ast, rule);
95 19 50         } else if (type == LITAVIS_T_AT_KEYWORD) {
96 0           litavis_parse_at_rule(pctx, ast, rule);
97 19 50         } else if (type == LITAVIS_T_PREPROC_VAR_DEF) {
98             /* $var: value; inside a block — store as property */
99 0           LitavisToken *var_tok = litavis_parser_advance(pctx);
100 0           char *var_name = litavis_token_str(var_tok);
101 0           char *value = NULL;
102 0 0         if (litavis_parser_at(pctx, LITAVIS_T_VALUE)) {
103 0           LitavisToken *val_tok = litavis_parser_advance(pctx);
104 0           value = litavis_token_str(val_tok);
105             } else {
106 0           value = litavis_strdup("");
107             }
108 0           litavis_rule_add_prop(rule, var_name, value);
109 0           free(var_name);
110 0           free(value);
111 0           litavis_parser_match(pctx, LITAVIS_T_SEMICOLON);
112 19 100         } else if (type == LITAVIS_T_MIXIN_REF) {
113             /* %name; — store as a property for later resolution */
114 18           LitavisToken *mixin_tok = litavis_parser_advance(pctx);
115 18           char *mixin_name = litavis_token_str(mixin_tok);
116 18           litavis_rule_add_prop(rule, mixin_name, "");
117 18           free(mixin_name);
118 18           litavis_parser_match(pctx, LITAVIS_T_SEMICOLON);
119 1 50         } else if (type == LITAVIS_T_SEMICOLON) {
120 1           litavis_parser_advance(pctx); /* skip stray semicolons */
121 0 0         } else if (type == LITAVIS_T_COMMA) {
122 0           litavis_parser_advance(pctx); /* skip stray commas */
123             } else {
124             /* Unknown token inside declarations — skip */
125 0           litavis_parser_advance(pctx);
126             }
127             }
128 823           }
129              
130             /* ── Build selector string (may include & and commas) ─────── */
131              
132 813           static char* litavis_parse_selector_text(LitavisParserCtx *pctx) {
133             /* Collect selector tokens until LBRACE */
134             char buf[4096];
135 813           int buf_pos = 0;
136 813           buf[0] = '\0';
137              
138 1646 100         while (!litavis_parser_at(pctx, LITAVIS_T_LBRACE) && !litavis_parser_at(pctx, LITAVIS_T_EOF)) {
    100          
139 834           LitavisToken *t = litavis_parser_peek(pctx);
140              
141 1657 100         if (t->type == LITAVIS_T_SELECTOR || t->type == LITAVIS_T_AMPERSAND) {
    50          
142 823 100         if (buf_pos > 0 && buf[buf_pos - 1] != ' ' && buf[buf_pos - 1] != ',') {
    50          
143             /* add space between parts if needed */
144             }
145 823           int copy_len = t->length;
146 823 50         if (buf_pos + copy_len + 1 < 4096) {
147 823           memcpy(buf + buf_pos, t->start, (size_t)copy_len);
148 823           buf_pos += copy_len;
149 823           buf[buf_pos] = '\0';
150             }
151 823           litavis_parser_advance(pctx);
152 11 100         } else if (t->type == LITAVIS_T_COMMA) {
153 10 50         if (buf_pos + 2 < 4096) {
154 10           buf[buf_pos++] = ',';
155 10           buf[buf_pos++] = ' ';
156 10           buf[buf_pos] = '\0';
157             }
158 10           litavis_parser_advance(pctx);
159 1 50         } else if (t->type == LITAVIS_T_STRING) {
160 0           int copy_len = t->length;
161 0 0         if (buf_pos + copy_len + 1 < 4096) {
162 0           memcpy(buf + buf_pos, t->start, (size_t)copy_len);
163 0           buf_pos += copy_len;
164 0           buf[buf_pos] = '\0';
165             }
166 0           litavis_parser_advance(pctx);
167             } else {
168 1           break;
169             }
170             }
171              
172             /* Trim trailing whitespace */
173 813 50         while (buf_pos > 0 && (buf[buf_pos - 1] == ' ' || buf[buf_pos - 1] == '\t'))
    50          
    50          
174 0           buf_pos--;
175 813           buf[buf_pos] = '\0';
176              
177 813           return litavis_strdup(buf);
178             }
179              
180             /* ── Parse a rule: selector(s) '{' declarations '}' ──────── */
181              
182 813           static void litavis_parse_rule(LitavisParserCtx *pctx, LitavisAST *ast, LitavisRule *parent) {
183 813           char *selector = litavis_parse_selector_text(pctx);
184              
185 813 100         if (!litavis_parser_match(pctx, LITAVIS_T_LBRACE)) {
186 2           free(selector);
187 2           return; /* malformed — skip */
188             }
189              
190 811 100         if (parent) {
191             /* Nested rule — add as child of parent */
192 44           LitavisRule *child = litavis_rule_add_child(parent, selector);
193 44 50         child->source_file = pctx->source_file ? litavis_strdup(pctx->source_file) : NULL;
194 44           child->source_line = litavis_parser_peek(pctx)->line;
195 44           litavis_parse_declarations(pctx, ast, child);
196             } else {
197             /* Top-level rule */
198 767           LitavisRule *rule = litavis_ast_add_rule(ast, selector);
199 767 100         rule->source_file = pctx->source_file ? litavis_strdup(pctx->source_file) : NULL;
200 767           rule->source_line = litavis_parser_peek(pctx)->line;
201 767           litavis_parse_declarations(pctx, ast, rule);
202             }
203              
204 811           free(selector);
205 811           litavis_parser_match(pctx, LITAVIS_T_RBRACE);
206             }
207              
208             /* ── Parse @-rule ─────────────────────────────────────────── */
209              
210 16           static void litavis_parse_at_rule(LitavisParserCtx *pctx, LitavisAST *ast, LitavisRule *parent) {
211 16           LitavisToken *kw_tok = litavis_parser_advance(pctx); /* AT_KEYWORD */
212 16           char *keyword = litavis_token_str(kw_tok);
213              
214 16           char *prelude = NULL;
215 16 50         if (litavis_parser_at(pctx, LITAVIS_T_AT_PRELUDE)) {
216 16           LitavisToken *pl_tok = litavis_parser_advance(pctx);
217 16           prelude = litavis_token_str(pl_tok);
218             }
219              
220             /* Build selector for the at-rule: "@media (prelude)" */
221             char at_sel[4096];
222 16 50         if (prelude)
223 16           snprintf(at_sel, sizeof(at_sel), "%s %s", keyword, prelude);
224             else
225 0           snprintf(at_sel, sizeof(at_sel), "%s", keyword);
226              
227 16 100         if (litavis_parser_at(pctx, LITAVIS_T_LBRACE)) {
228             /* Block @-rule: @media { ... } */
229 12           litavis_parser_advance(pctx); /* consume { */
230              
231             LitavisRule *at_rule;
232 12 50         if (parent) {
233 0           at_rule = litavis_rule_add_child(parent, at_sel);
234             } else {
235 12           at_rule = litavis_ast_add_rule(ast, at_sel);
236             }
237 12           at_rule->is_at_rule = 1;
238 12 50         at_rule->at_prelude = prelude ? litavis_strdup(prelude) : NULL;
239 12 50         at_rule->source_file = pctx->source_file ? litavis_strdup(pctx->source_file) : NULL;
240 12           at_rule->source_line = kw_tok->line;
241              
242             /* Parse contents — can be rules or declarations (e.g. @font-face) */
243 12           litavis_parse_declarations(pctx, ast, at_rule);
244              
245 12           litavis_parser_match(pctx, LITAVIS_T_RBRACE);
246             } else {
247             /* Statement @-rule: @import ...; */
248             LitavisRule *at_rule;
249 4 50         if (parent) {
250 0           at_rule = litavis_rule_add_child(parent, at_sel);
251             } else {
252 4           at_rule = litavis_ast_add_rule(ast, at_sel);
253             }
254 4           at_rule->is_at_rule = 1;
255 4 50         at_rule->at_prelude = prelude ? litavis_strdup(prelude) : NULL;
256 4 50         at_rule->source_file = pctx->source_file ? litavis_strdup(pctx->source_file) : NULL;
257 4           at_rule->source_line = kw_tok->line;
258              
259 4           litavis_parser_match(pctx, LITAVIS_T_SEMICOLON);
260             }
261              
262 16           free(keyword);
263 16 50         if (prelude) free(prelude);
264 16           }
265              
266             /* ── Parse stylesheet (top-level or inside @-rule) ────────── */
267              
268 587           static void litavis_parse_stylesheet(LitavisParserCtx *pctx, LitavisAST *ast, LitavisRule *parent) {
269 1464 100         while (!litavis_parser_at(pctx, LITAVIS_T_EOF) && !litavis_parser_at(pctx, LITAVIS_T_RBRACE)) {
    100          
270 877           LitavisTokenType type = litavis_parser_peek(pctx)->type;
271              
272 877 100         if (type == LITAVIS_T_SELECTOR || type == LITAVIS_T_AMPERSAND) {
    50          
273 768           litavis_parse_rule(pctx, ast, parent);
274 109 100         } else if (type == LITAVIS_T_AT_KEYWORD) {
275 16           litavis_parse_at_rule(pctx, ast, parent);
276 93 100         } else if (type == LITAVIS_T_PREPROC_VAR_DEF) {
277             /* Top-level $var: value; — store as a rule */
278 65           LitavisToken *var_tok = litavis_parser_advance(pctx);
279 65           char *var_name = litavis_token_str(var_tok);
280 65           char *value = NULL;
281 65 50         if (litavis_parser_at(pctx, LITAVIS_T_VALUE)) {
282 65           LitavisToken *val_tok = litavis_parser_advance(pctx);
283 65           value = litavis_token_str(val_tok);
284             } else {
285 0           value = litavis_strdup("");
286             }
287             /* Store in AST as a special rule */
288 65           LitavisRule *vr = litavis_ast_add_rule(ast, var_name);
289 65           litavis_rule_add_prop(vr, var_name, value);
290 65           free(var_name);
291 65           free(value);
292 65           litavis_parser_match(pctx, LITAVIS_T_SEMICOLON);
293 28 100         } else if (type == LITAVIS_T_MIXIN_DEF) {
294             /* Top-level %name: (...); */
295 23           LitavisToken *mixin_tok = litavis_parser_advance(pctx);
296 23           char *mixin_name = litavis_token_str(mixin_tok);
297 23           char *body = NULL;
298 23 50         if (litavis_parser_at(pctx, LITAVIS_T_VALUE)) {
299 23           LitavisToken *val_tok = litavis_parser_advance(pctx);
300 23           body = litavis_token_str(val_tok);
301             } else {
302 0           body = litavis_strdup("");
303             }
304 23           LitavisRule *mr = litavis_ast_add_rule(ast, mixin_name);
305 23           mr->is_at_rule = 1; /* mark as non-output */
306 23           litavis_rule_add_prop(mr, mixin_name, body);
307 23           free(mixin_name);
308 23           free(body);
309 23           litavis_parser_match(pctx, LITAVIS_T_SEMICOLON);
310 5 100         } else if (type == LITAVIS_T_SEMICOLON || type == LITAVIS_T_COMMA) {
    50          
311 1           litavis_parser_advance(pctx); /* skip stray */
312             } else {
313 4           litavis_parser_advance(pctx); /* skip unknown */
314             }
315             }
316 587           }
317              
318             /* ── Selector flattening ──────────────────────────────────── */
319              
320             static void litavis_flatten_rule(LitavisRule *rule, const char *parent_sel, LitavisAST *flat);
321              
322             /* Combine parent + child selector, handling & */
323 34           static char* litavis_combine_selectors(const char *parent, const char *child) {
324 34 50         if (!parent || !*parent)
    50          
325 0           return litavis_strdup(child);
326              
327             /* Check if child contains '&' */
328 34           const char *amp = strchr(child, '&');
329 34 100         if (amp) {
330             /* Replace all & with parent selector */
331             char buf[8192];
332 11           int buf_pos = 0;
333 11           const char *p = child;
334 90 100         while (*p) {
335 79 100         if (*p == '&') {
336 11           int plen = (int)strlen(parent);
337 11 50         if (buf_pos + plen < 8192) {
338 11           memcpy(buf + buf_pos, parent, (size_t)plen);
339 11           buf_pos += plen;
340             }
341 11           p++;
342             } else {
343 68 50         if (buf_pos < 8191)
344 68           buf[buf_pos++] = *p;
345 68           p++;
346             }
347             }
348 11           buf[buf_pos] = '\0';
349 11           return litavis_strdup(buf);
350             }
351              
352             /* No & — descendant combinator (space) */
353 23           int plen = (int)strlen(parent);
354 23           int clen = (int)strlen(child);
355 23           char *combined = (char*)malloc((size_t)(plen + 1 + clen + 1));
356 23 50         if (!combined) LITAVIS_FATAL("out of memory");
357 23           memcpy(combined, parent, (size_t)plen);
358 23           combined[plen] = ' ';
359 23           memcpy(combined + plen + 1, child, (size_t)clen);
360 23           combined[plen + 1 + clen] = '\0';
361 23           return combined;
362             }
363              
364             /* Split a comma-separated selector list */
365 924           static int litavis_split_selectors(const char *sel, char **out, int max_out) {
366 924           int count = 0;
367 924           const char *p = sel;
368 1860 100         while (*p && count < max_out) {
    50          
369             /* Skip leading ws */
370 948 100         while (*p == ' ' || *p == '\t') p++;
    50          
371 936           const char *start = p;
372 936           int paren_d = 0;
373 6570 100         while (*p) {
374 5646 100         if (*p == '(') paren_d++;
375 5632 100         else if (*p == ')') paren_d--;
376 5618 100         else if (*p == ',' && paren_d == 0) break;
    50          
377 5634           p++;
378             }
379             /* Trim trailing ws */
380 936           const char *end = p;
381 936 50         while (end > start && (end[-1] == ' ' || end[-1] == '\t'))
    50          
    50          
382 0           end--;
383 936 50         if (end > start) {
384 936           int slen = (int)(end - start);
385 936           char *s = (char*)malloc((size_t)(slen + 1));
386 936 50         if (!s) LITAVIS_FATAL("out of memory");
387 936           memcpy(s, start, (size_t)slen);
388 936           s[slen] = '\0';
389 936           out[count++] = s;
390             }
391 936 100         if (*p == ',') p++;
392             }
393 924           return count;
394             }
395              
396 894           static void litavis_flatten_rule(LitavisRule *rule, const char *parent_sel, LitavisAST *flat) {
397             int i, j, k;
398              
399             /* Split this rule's selector on commas */
400             char *selectors[128];
401 894           int sel_count = litavis_split_selectors(rule->selector, selectors, 128);
402              
403             /* For each selector part, combine with parent */
404             char *combined[128];
405 894           int combined_count = 0;
406              
407 894 100         if (!parent_sel || !*parent_sel) {
    50          
408 1737 100         for (i = 0; i < sel_count; i++)
409 873           combined[combined_count++] = selectors[i];
410             } else {
411             /* Split parent too for combinatorial expansion */
412             char *parent_parts[128];
413 30           int parent_count = litavis_split_selectors(parent_sel, parent_parts, 128);
414              
415 62 100         for (i = 0; i < parent_count; i++) {
416 66 100         for (j = 0; j < sel_count; j++) {
417 34           combined[combined_count++] = litavis_combine_selectors(parent_parts[i], selectors[j]);
418             }
419             }
420 62 100         for (i = 0; i < parent_count; i++) free(parent_parts[i]);
421 61 100         for (i = 0; i < sel_count; i++) free(selectors[i]);
422             }
423              
424             /* Build the combined selector string */
425 894 100         if (rule->prop_count > 0 && !rule->is_at_rule) {
    100          
426             /* Rejoin combined selectors with ", " */
427             char joined[8192];
428 833           int jpos = 0;
429 1677 100         for (i = 0; i < combined_count && jpos < 8100; i++) {
    50          
430 844 100         if (i > 0) {
431 11           joined[jpos++] = ',';
432 11           joined[jpos++] = ' ';
433             }
434 844           int slen = (int)strlen(combined[i]);
435 844           memcpy(joined + jpos, combined[i], (size_t)slen);
436 844           jpos += slen;
437             }
438 833           joined[jpos] = '\0';
439              
440 833           LitavisRule *flat_rule = litavis_ast_add_rule(flat, joined);
441             /* Copy properties */
442 1883 100         for (i = 0; i < rule->prop_count; i++) {
443 1050           litavis_rule_add_prop(flat_rule, rule->props[i].key, rule->props[i].value);
444             }
445 833 100         flat_rule->source_file = rule->source_file ? litavis_strdup(rule->source_file) : NULL;
446 833           flat_rule->source_line = rule->source_line;
447             }
448              
449             /* Handle @-rules with children (e.g. @media) */
450 906 100         if (rule->is_at_rule && rule->child_count > 0) {
    100          
451 12           LitavisRule *flat_at = litavis_ast_add_rule(flat, rule->selector);
452 12           flat_at->is_at_rule = 1;
453 12 50         flat_at->at_prelude = rule->at_prelude ? litavis_strdup(rule->at_prelude) : NULL;
454 12 50         flat_at->source_file = rule->source_file ? litavis_strdup(rule->source_file) : NULL;
455 12           flat_at->source_line = rule->source_line;
456             /* Copy own props */
457 12 50         for (i = 0; i < rule->prop_count; i++) {
458 0           litavis_rule_add_prop(flat_at, rule->props[i].key, rule->props[i].value);
459             }
460             /* Flatten children into the @-rule's child list */
461 26 100         for (i = 0; i < rule->child_count; i++) {
462 14           LitavisAST *child_flat = litavis_ast_new(4);
463 14           litavis_flatten_rule(&rule->children[i], NULL, child_flat);
464             int ci;
465 29 100         for (ci = 0; ci < child_flat->count; ci++) {
466 15           LitavisRule *src = &child_flat->rules[ci];
467 15           LitavisRule *child = litavis_rule_add_child(flat_at, src->selector);
468 15           litavis_rule_merge_props(child, src);
469 15           child->is_at_rule = src->is_at_rule;
470 15 50         if (src->source_file)
471 0           child->source_file = litavis_strdup(src->source_file);
472 15           child->source_line = src->source_line;
473             }
474 14           litavis_ast_free(child_flat);
475             }
476 882 100         } else if (rule->is_at_rule) {
477             /* Statement @-rule or @-rule with only props (like @font-face) */
478 26           LitavisRule *flat_at = litavis_ast_add_rule(flat, rule->selector);
479 26           flat_at->is_at_rule = 1;
480 26 100         flat_at->at_prelude = rule->at_prelude ? litavis_strdup(rule->at_prelude) : NULL;
481 26 50         flat_at->source_file = rule->source_file ? litavis_strdup(rule->source_file) : NULL;
482 26           flat_at->source_line = rule->source_line;
483 48 100         for (i = 0; i < rule->prop_count; i++) {
484 22           litavis_rule_add_prop(flat_at, rule->props[i].key, rule->props[i].value);
485             }
486             }
487              
488             /* Recurse into children (non-at-rule) */
489 894 100         if (!rule->is_at_rule) {
490 886 100         for (i = 0; i < rule->child_count; i++) {
491             /* Build parent selector string for children */
492             char parent_joined[8192];
493 30           int pjpos = 0;
494 62 100         for (j = 0; j < combined_count && pjpos < 8100; j++) {
    50          
495 32 100         if (j > 0) {
496 2           parent_joined[pjpos++] = ',';
497 2           parent_joined[pjpos++] = ' ';
498             }
499 32           int slen = (int)strlen(combined[j]);
500 32           memcpy(parent_joined + pjpos, combined[j], (size_t)slen);
501 32           pjpos += slen;
502             }
503 30           parent_joined[pjpos] = '\0';
504 30           litavis_flatten_rule(&rule->children[i], parent_joined, flat);
505             }
506             }
507              
508             /* Free combined if we allocated them (parent_sel was non-null) */
509 894 100         if (parent_sel && *parent_sel) {
    50          
510 64 100         for (i = 0; i < combined_count; i++)
511 34           free(combined[i]);
512             }
513 894           }
514              
515 587           static LitavisAST* litavis_flatten(LitavisAST *nested) {
516             int i;
517 587           LitavisAST *flat = litavis_ast_new(nested->count * 2);
518 1437 100         for (i = 0; i < nested->count; i++) {
519 850           litavis_flatten_rule(&nested->rules[i], NULL, flat);
520             }
521 587           return flat;
522             }
523              
524             /* ── Public API ───────────────────────────────────────────── */
525              
526             /* Parse a token list into an AST. */
527 561           static void litavis_parse(LitavisAST *ast, LitavisTokenList *tokens, const char *source_file) {
528             LitavisParserCtx pctx;
529 561           pctx.tokens = tokens;
530 561           pctx.pos = 0;
531 561           pctx.source_file = source_file;
532 561           litavis_parse_stylesheet(&pctx, ast, NULL);
533 561           }
534              
535             /* Parse a raw CSS string into ctx->ast (tokenise + parse + flatten) */
536 561           static void litavis_parse_string(LitavisCtx *ctx, const char *input) {
537 561           int len = (int)strlen(input);
538 561           LitavisTokenList *tokens = litavis_tokenise(input, len);
539              
540             /* Parse into a temporary nested AST */
541 561           LitavisAST *nested = litavis_ast_new(16);
542 561           litavis_parse(nested, tokens, NULL);
543              
544             /* Flatten nested selectors */
545 561           LitavisAST *flat = litavis_flatten(nested);
546              
547             /* Merge flattened rules into ctx->ast */
548             int i;
549 1379 100         for (i = 0; i < flat->count; i++) {
550 818           LitavisRule *src = &flat->rules[i];
551 818           LitavisRule *dst = litavis_ast_add_rule(ctx->ast, src->selector);
552 818           litavis_rule_merge_props(dst, src);
553 818           dst->is_at_rule = src->is_at_rule;
554 818 100         if (src->at_prelude && !dst->at_prelude)
    50          
555 16           dst->at_prelude = litavis_strdup(src->at_prelude);
556 818 50         if (src->source_file && !dst->source_file)
    0          
557 0           dst->source_file = litavis_strdup(src->source_file);
558 818 100         if (src->source_line && !dst->source_line)
    100          
559 744           dst->source_line = src->source_line;
560             /* Copy children for @-rules */
561             int j;
562 833 100         for (j = 0; j < src->child_count; j++) {
563 15           LitavisRule *child = litavis_rule_add_child(dst, src->children[j].selector);
564 15           litavis_rule_merge_props(child, &src->children[j]);
565 15           child->is_at_rule = src->children[j].is_at_rule;
566             }
567             }
568              
569 561           litavis_ast_free(nested);
570 561           litavis_ast_free(flat);
571 561           litavis_token_list_free(tokens);
572 561           }
573              
574             /* Parse a file into ctx->ast */
575 27           static void litavis_parse_file(LitavisCtx *ctx, const char *filename) {
576 27           FILE *f = fopen(filename, "rb");
577 27 100         if (!f) {
578             char err[512];
579 1           snprintf(err, sizeof(err), "cannot open file: %s", filename);
580 1           LITAVIS_FATAL(err);
581             }
582              
583 26           fseek(f, 0, SEEK_END);
584 26           long size = ftell(f);
585 26           fseek(f, 0, SEEK_SET);
586              
587 26           char *buf = (char*)malloc((size_t)size + 1);
588 26 50         if (!buf) { fclose(f); LITAVIS_FATAL("out of memory"); }
589              
590 26           size_t read = fread(buf, 1, (size_t)size, f);
591 26           buf[read] = '\0';
592 26           fclose(f);
593              
594             /* Tokenise + parse */
595 26           LitavisTokenList *tokens = litavis_tokenise(buf, (int)read);
596 26           LitavisAST *nested = litavis_ast_new(16);
597             LitavisParserCtx pctx;
598 26           pctx.tokens = tokens;
599 26           pctx.pos = 0;
600 26           pctx.source_file = filename;
601 26           litavis_parse_stylesheet(&pctx, nested, NULL);
602              
603 26           LitavisAST *flat = litavis_flatten(nested);
604              
605             int i, j;
606 64 100         for (i = 0; i < flat->count; i++) {
607 38           LitavisRule *src = &flat->rules[i];
608 38           LitavisRule *dst = litavis_ast_add_rule(ctx->ast, src->selector);
609 38           litavis_rule_merge_props(dst, src);
610 38           dst->is_at_rule = src->is_at_rule;
611 38 50         if (src->at_prelude && !dst->at_prelude)
    0          
612 0           dst->at_prelude = litavis_strdup(src->at_prelude);
613 38 100         if (src->source_file && !dst->source_file)
    100          
614 21           dst->source_file = litavis_strdup(src->source_file);
615 38 100         if (src->source_line && !dst->source_line)
    100          
616 21           dst->source_line = src->source_line;
617 38 50         for (j = 0; j < src->child_count; j++) {
618 0           LitavisRule *child = litavis_rule_add_child(dst, src->children[j].selector);
619 0           litavis_rule_merge_props(child, &src->children[j]);
620 0           child->is_at_rule = src->children[j].is_at_rule;
621             }
622             }
623              
624 26           litavis_ast_free(nested);
625 26           litavis_ast_free(flat);
626 26           litavis_token_list_free(tokens);
627 26           free(buf);
628 26           }
629              
630             /* qsort comparator for directory entries */
631 13           static int litavis_dirent_cmp(const void *a, const void *b) {
632 13           return strcmp(*(const char**)a, *(const char**)b);
633             }
634              
635             /* Parse a directory of .css files (sorted, non-recursive) */
636 16           static void litavis_parse_dir(LitavisCtx *ctx, const char *dirname) {
637 16           DIR *d = opendir(dirname);
638 16 100         if (!d) {
639             char err[512];
640 1           snprintf(err, sizeof(err), "cannot open directory: %s", dirname);
641 1           LITAVIS_FATAL(err);
642             }
643              
644             /* Collect .css filenames */
645             char *files[1024];
646 15           int file_count = 0;
647             struct dirent *ent;
648              
649 75 100         while ((ent = readdir(d)) != NULL && file_count < 1024) {
    50          
650 60           int nlen = (int)strlen(ent->d_name);
651 60 100         if (nlen > 4 && strcmp(ent->d_name + nlen - 4, ".css") == 0) {
    100          
652 22           int dlen = (int)strlen(dirname);
653 22           char *path = (char*)malloc((size_t)(dlen + 1 + nlen + 1));
654 22 50         if (!path) LITAVIS_FATAL("out of memory");
655 22           memcpy(path, dirname, (size_t)dlen);
656 22           path[dlen] = '/';
657 22           memcpy(path + dlen + 1, ent->d_name, (size_t)(nlen + 1));
658 22           files[file_count++] = path;
659             }
660             }
661 15           closedir(d);
662              
663             /* Sort alphabetically */
664 15           qsort(files, (size_t)file_count, sizeof(char*), litavis_dirent_cmp);
665              
666             /* Parse each file */
667             int i;
668 37 100         for (i = 0; i < file_count; i++) {
669 22           litavis_parse_file(ctx, files[i]);
670 22           free(files[i]);
671             }
672 15           }
673              
674             #endif /* LITAVIS_PARSER_H */