File Coverage

include/eshu_js.h
Criterion Covered Total %
statement 165 174 94.8
branch 137 174 78.7
condition n/a
subroutine n/a
pod n/a
total 302 348 86.7


line stmt bran cond sub pod time code
1             /*
2             * eshu_js.h — JavaScript indentation scanner
3             *
4             * Tracks {} () [] nesting depth while handling JS-specific constructs:
5             * double-quoted strings, single-quoted strings, template literals with
6             * ${} interpolation, regex literals, line comments, and block comments.
7             */
8              
9             #ifndef ESHU_JS_H
10             #define ESHU_JS_H
11              
12             #include "eshu.h"
13              
14             #define ESHU_JS_MAX_TMPL_DEPTH 16
15              
16             /* ══════════════════════════════════════════════════════════════════
17             * Scanner context — persists across lines
18             * ══════════════════════════════════════════════════════════════════ */
19              
20             typedef struct {
21             int depth;
22             enum eshu_state state;
23             int tmpl_depth;
24             int tmpl_brace_depth[ESHU_JS_MAX_TMPL_DEPTH];
25             int can_regex; /* 1 if next / starts a regex */
26             eshu_config_t cfg;
27             } eshu_js_ctx_t;
28              
29 51           static void eshu_js_ctx_init(eshu_js_ctx_t *ctx, const eshu_config_t *cfg) {
30 51           ctx->depth = 0;
31 51           ctx->state = ESHU_CODE;
32 51           ctx->tmpl_depth = 0;
33 51           ctx->can_regex = 1;
34 51           ctx->cfg = *cfg;
35 51           }
36              
37             /* ══════════════════════════════════════════════════════════════════
38             * Classify first non-ws char for pre-indent adjustment
39             * ══════════════════════════════════════════════════════════════════ */
40              
41 242           static int eshu_js_is_closing(char c) {
42 242 100         return c == '}' || c == ')' || c == ']';
    50          
    100          
43             }
44              
45             /* ══════════════════════════════════════════════════════════════════
46             * Scan a line for nesting changes
47             *
48             * Called AFTER the line has been emitted. Updates ctx->state
49             * and ctx->depth for the next line.
50             * ══════════════════════════════════════════════════════════════════ */
51              
52 253           static void eshu_js_scan_line(eshu_js_ctx_t *ctx,
53             const char *p, const char *end) {
54 2085 100         while (p < end) {
55 1838           char c = *p;
56              
57 1838           switch (ctx->state) {
58              
59 1240           case ESHU_CODE:
60 1240 100         if (c == '{' || c == '(' || c == '[') {
    100          
    100          
61 146           ctx->depth++;
62 146           ctx->can_regex = 1;
63 1094 100         } else if (c == '}') {
64 66           ctx->depth--;
65 66 50         if (ctx->depth < 0) ctx->depth = 0;
66             /* Check if this closes a template expression */
67 66 100         if (ctx->tmpl_depth > 0 &&
68 4 100         ctx->depth == ctx->tmpl_brace_depth[ctx->tmpl_depth - 1]) {
69 3           ctx->tmpl_depth--;
70 3           ctx->state = ESHU_JS_TEMPLATE;
71 3           ctx->can_regex = 0;
72 3           break;
73             }
74 63           ctx->can_regex = 0;
75 1028 100         } else if (c == ')' || c == ']') {
    100          
76 83           ctx->depth--;
77 83 50         if (ctx->depth < 0) ctx->depth = 0;
78 83           ctx->can_regex = 0;
79 945 100         } else if (c == '"') {
80 10           ctx->state = ESHU_STRING_DQ;
81 935 100         } else if (c == '\'') {
82 6           ctx->state = ESHU_STRING_SQ;
83 929 100         } else if (c == '`') {
84 9           ctx->state = ESHU_JS_TEMPLATE;
85 9           ctx->can_regex = 0;
86 920 100         } else if (c == '/' && p + 1 < end && *(p + 1) == '/') {
    50          
    100          
87             /* line comment — skip rest of line */
88 6           return;
89 914 100         } else if (c == '/' && p + 1 < end && *(p + 1) == '*') {
    50          
    100          
90 5           ctx->state = ESHU_COMMENT_BLOCK;
91 5           p++; /* skip '*' */
92 909 100         } else if (c == '/' && ctx->can_regex) {
    100          
93             /* regex literal */
94 6           ctx->state = ESHU_JS_REGEX;
95 903 100         } else if (c == '/') {
96             /* division operator */
97 1           ctx->can_regex = 1;
98 1276 100         } else if (isalnum((unsigned char)c) || c == '_' || c == '$') {
    50          
    50          
99 374           ctx->can_regex = 0;
100             /* skip rest of identifier/number */
101 1328 50         while (p + 1 < end &&
102 1328 100         (isalnum((unsigned char)*(p + 1)) ||
103 374 50         *(p + 1) == '_' || *(p + 1) == '$'))
    50          
104 954           p++;
105 528 100         } else if (c == '+' || c == '-') {
    100          
106 4 50         if (p + 1 < end && *(p + 1) == c) {
    100          
107 2           p++; /* ++ or -- */
108 2           ctx->can_regex = 0;
109             } else {
110 2           ctx->can_regex = 1;
111             }
112 524 100         } else if (c == '=' || c == ',' || c == ';' || c == '!' ||
    100          
    100          
    50          
    50          
113 353 100         c == '~' || c == '<' || c == '>' || c == '&' ||
    100          
    50          
    50          
114 345 50         c == '|' || c == '^' || c == '?' || c == ':' ||
    50          
    100          
    50          
115 338 50         c == '%' || c == '*') {
116 186           ctx->can_regex = 1;
117             }
118             /* whitespace does not change can_regex */
119 1231           break;
120              
121 177           case ESHU_STRING_DQ:
122 177 100         if (c == '\\' && p + 1 < end) {
    50          
123 5           p++; /* skip escaped char */
124 172 100         } else if (c == '"') {
125 10           ctx->state = ESHU_CODE;
126 10           ctx->can_regex = 0;
127             }
128 177           break;
129              
130 70           case ESHU_STRING_SQ:
131 70 100         if (c == '\\' && p + 1 < end) {
    50          
132 1           p++;
133 69 100         } else if (c == '\'') {
134 6           ctx->state = ESHU_CODE;
135 6           ctx->can_regex = 0;
136             }
137 70           break;
138              
139 163           case ESHU_JS_TEMPLATE:
140 163 50         if (c == '\\' && p + 1 < end) {
    0          
141 0           p++; /* skip escaped char */
142 163 100         } else if (c == '`') {
143 9           ctx->state = ESHU_CODE;
144 9           ctx->can_regex = 0;
145 154 100         } else if (c == '$' && p + 1 < end && *(p + 1) == '{') {
    50          
    50          
146 3           p++; /* skip '{' */
147 3 50         if (ctx->tmpl_depth < ESHU_JS_MAX_TMPL_DEPTH) {
148 3           ctx->tmpl_brace_depth[ctx->tmpl_depth] = ctx->depth;
149 3           ctx->tmpl_depth++;
150             }
151 3           ctx->depth++;
152 3           ctx->state = ESHU_CODE;
153 3           ctx->can_regex = 1;
154             }
155 163           break;
156              
157 38           case ESHU_JS_REGEX:
158 38 100         if (c == '\\' && p + 1 < end) {
    50          
159 2           p++;
160 36 100         } else if (c == '[') {
161 1           ctx->state = ESHU_JS_REGEX_CLASS;
162 35 100         } else if (c == '/') {
163             /* end of regex — skip flags */
164 10 50         while (p + 1 < end && isalpha((unsigned char)*(p + 1)))
    100          
165 4           p++;
166 6           ctx->state = ESHU_CODE;
167 6           ctx->can_regex = 0;
168             }
169 38           break;
170              
171 7           case ESHU_JS_REGEX_CLASS:
172 7 50         if (c == '\\' && p + 1 < end) {
    0          
173 0           p++;
174 7 100         } else if (c == ']') {
175 1           ctx->state = ESHU_JS_REGEX;
176             }
177 7           break;
178              
179 143           case ESHU_COMMENT_BLOCK:
180 143 100         if (c == '*' && p + 1 < end && *(p + 1) == '/') {
    100          
    100          
181 5           ctx->state = ESHU_CODE;
182 5           ctx->can_regex = 1;
183 5           p++;
184             }
185 143           break;
186              
187 0           default:
188 0           break;
189             }
190 1832           p++;
191             }
192             }
193              
194             /* ══════════════════════════════════════════════════════════════════
195             * Process a single line — decide indent, emit, scan
196             * ══════════════════════════════════════════════════════════════════ */
197              
198 256           static void eshu_js_process_line(eshu_js_ctx_t *ctx, eshu_buf_t *out,
199             const char *line_start, const char *eol) {
200 256           const char *content = eshu_skip_leading_ws(line_start);
201             int line_len;
202             int indent_depth;
203              
204             /* empty line — preserve it */
205 256 100         if (content >= eol) {
206 3           eshu_buf_putc(out, '\n');
207 3           return;
208             }
209              
210 253           line_len = (int)(eol - content);
211              
212             /* Template literal continuation: pass through verbatim
213             * (template literal whitespace is significant) */
214 253 100         if (ctx->state == ESHU_JS_TEMPLATE) {
215 3           eshu_buf_write(out, line_start, (size_t)(eol - line_start));
216 3 50         if (*eol == '\n') eshu_buf_putc(out, '\n');
217 3           eshu_js_scan_line(ctx, line_start, eol);
218 3           return;
219             }
220              
221             /* Block comment continuation */
222 250 100         if (ctx->state == ESHU_COMMENT_BLOCK) {
223 8           eshu_emit_indent(out, ctx->depth, &ctx->cfg);
224 8           eshu_buf_write_trimmed(out, content, line_len);
225 8           eshu_buf_putc(out, '\n');
226 8           eshu_js_scan_line(ctx, content, eol);
227 8           return;
228             }
229              
230             /* Regex spanning lines (rare but possible) */
231 242 50         if (ctx->state == ESHU_JS_REGEX ||
232 242 50         ctx->state == ESHU_JS_REGEX_CLASS) {
233 0           eshu_emit_indent(out, ctx->depth, &ctx->cfg);
234 0           eshu_buf_write_trimmed(out, content, line_len);
235 0           eshu_buf_putc(out, '\n');
236 0           eshu_js_scan_line(ctx, content, eol);
237 0           return;
238             }
239              
240             /* Normal code line */
241 242           indent_depth = ctx->depth;
242              
243             /* If line starts with closer, dedent this line */
244 242 100         if (eshu_js_is_closing(*content)) {
245 64           indent_depth--;
246 64 50         if (indent_depth < 0) indent_depth = 0;
247             }
248              
249 242           eshu_emit_indent(out, indent_depth, &ctx->cfg);
250 242           eshu_buf_write_trimmed(out, content, line_len);
251 242           eshu_buf_putc(out, '\n');
252              
253             /* Scan for nesting changes */
254 242           eshu_js_scan_line(ctx, content, eol);
255             }
256              
257             /* ══════════════════════════════════════════════════════════════════
258             * Public API — indent a JavaScript source string
259             * ══════════════════════════════════════════════════════════════════ */
260              
261 51           static char * eshu_indent_js(const char *src, size_t src_len,
262             const eshu_config_t *cfg, size_t *out_len) {
263             eshu_js_ctx_t ctx;
264             eshu_buf_t out;
265 51           const char *p = src;
266 51           const char *end = src + src_len;
267             char *result;
268              
269 51           eshu_js_ctx_init(&ctx, cfg);
270 51           eshu_buf_init(&out, src_len + 256);
271              
272             {
273 51           int line_num = 1;
274 307 100         while (p < end) {
275 256           const char *eol = eshu_find_eol(p);
276              
277 256 100         if (eshu_in_range(cfg, line_num)) {
278 254           eshu_js_process_line(&ctx, &out, p, eol);
279             } else {
280             /* Outside range: scan for state, emit verbatim */
281 2           size_t saved = out.len;
282 2           eshu_js_process_line(&ctx, &out, p, eol);
283 2           out.len = saved;
284 2           eshu_buf_write_trimmed(&out, p, (int)(eol - p));
285 2           eshu_buf_putc(&out, '\n');
286             }
287              
288 256           p = eol;
289 256 50         if (*p == '\n') p++;
290 256           line_num++;
291             }
292             }
293              
294             /* NUL-terminate */
295 51           eshu_buf_putc(&out, '\0');
296 51           out.len--;
297              
298 51           *out_len = out.len;
299 51           result = out.data;
300 51           return result;
301             }
302              
303             #endif /* ESHU_JS_H */