File Coverage

include/eshu_pl.h
Criterion Covered Total %
statement 346 366 94.5
branch 281 364 77.2
condition n/a
subroutine n/a
pod n/a
total 627 730 85.8


line stmt bran cond sub pod time code
1             /*
2             * eshu_pl.h — Perl language indentation scanner
3             *
4             * Tracks {} () [] nesting depth while skipping strings, heredocs,
5             * regex, qw/qq/q constructs, pod sections, and comments.
6             * Rewrites leading whitespace only.
7             */
8              
9             #ifndef ESHU_PL_H
10             #define ESHU_PL_H
11              
12             #include "eshu.h"
13              
14             #define ESHU_PL_HEREDOC_MAX 64 /* max length of heredoc terminator */
15             #define ESHU_PL_QDEPTH_MAX 8 /* max nesting depth for paired delims */
16              
17             /* ══════════════════════════════════════════════════════════════════
18             * Scanner context — persists across lines
19             * ══════════════════════════════════════════════════════════════════ */
20              
21             typedef struct {
22             int depth; /* brace/paren/bracket nesting */
23             enum eshu_state state; /* current scanner state */
24             eshu_config_t cfg;
25              
26             /* Heredoc tracking */
27             char heredoc_tag[ESHU_PL_HEREDOC_MAX];
28             int heredoc_tag_len;
29             int heredoc_indented; /* <<~ variant */
30             int heredoc_pending; /* heredoc detected, body starts next line */
31              
32             /* Quoted construct tracking (qw, qq, q, s, tr, y, m) */
33             char q_open; /* opening delimiter */
34             char q_close; /* closing delimiter (0 for non-paired) */
35             int q_depth; /* nesting depth for paired delimiters */
36             int q_sections; /* remaining sections (2 for s///, 1 for others) */
37              
38             /* Regex tracking */
39             char rx_delim; /* regex delimiter char */
40              
41             /* Track whether last significant token could precede division */
42             int last_was_value; /* 1 if last token was var/number/)/] */
43              
44             /* POD buffering */
45             eshu_buf_t pod_buf;
46             int pod_active;
47             } eshu_pl_ctx_t;
48              
49 113           static void eshu_pl_ctx_init(eshu_pl_ctx_t *ctx, const eshu_config_t *cfg) {
50 113           ctx->depth = 0;
51 113           ctx->state = ESHU_CODE;
52 113           ctx->cfg = *cfg;
53 113           ctx->heredoc_tag[0] = '\0';
54 113           ctx->heredoc_tag_len = 0;
55 113           ctx->heredoc_indented = 0;
56 113           ctx->heredoc_pending = 0;
57 113           ctx->q_open = 0;
58 113           ctx->q_close = 0;
59 113           ctx->q_depth = 0;
60 113           ctx->q_sections = 0;
61 113           ctx->rx_delim = 0;
62 113           ctx->last_was_value = 0;
63 113           eshu_buf_init(&ctx->pod_buf, 256);
64 113           ctx->pod_active = 0;
65 113           }
66              
67             /* ══════════════════════════════════════════════════════════════════
68             * Delimiter helpers
69             * ══════════════════════════════════════════════════════════════════ */
70              
71 23           static char eshu_pl_matching_close(char open) {
72 23           switch (open) {
73 6           case '(': return ')';
74 15           case '{': return '}';
75 1           case '[': return ']';
76 1           case '<': return '>';
77 0           default: return 0; /* non-paired: same char closes */
78             }
79             }
80              
81 28           static int eshu_pl_is_paired(char c) {
82 28 100         return c == '(' || c == '{' || c == '[' || c == '<';
    100          
    100          
    100          
83             }
84              
85             /* ══════════════════════════════════════════════════════════════════
86             * Heredoc detection
87             *
88             * Recognises: <
89             * Sets heredoc_pending=1 so the NEXT line enters heredoc state.
90             * ══════════════════════════════════════════════════════════════════ */
91              
92 17           static int eshu_pl_detect_heredoc(eshu_pl_ctx_t *ctx,
93             const char *p, const char *end) {
94             const char *start;
95 17           int indented = 0;
96 17           char quote = 0;
97             int len;
98              
99             /* p points to the first '<' — we need "<<" */
100 17 50         if (p + 1 >= end || *(p + 1) != '<')
    100          
101 5           return 0;
102 12           p += 2;
103              
104             /* optional ~ for indented heredoc */
105 12 50         if (p < end && *p == '~') {
    100          
106 1           indented = 1;
107 1           p++;
108             }
109              
110             /* optional quote */
111 12 50         if (p < end && (*p == '\'' || *p == '"' || *p == '`')) {
    100          
    100          
    50          
112 6           quote = *p;
113 6           p++;
114             }
115              
116             /* identifier */
117 12           start = p;
118 60 50         while (p < end && (isalnum((unsigned char)*p) || *p == '_'))
    100          
    50          
119 48           p++;
120 12           len = (int)(p - start);
121 12 50         if (len == 0 || len >= ESHU_PL_HEREDOC_MAX)
    50          
122 0           return 0;
123              
124             /* closing quote must match */
125 12 100         if (quote && (p >= end || *p != quote))
    50          
    50          
126 0           return 0;
127              
128 12           memcpy(ctx->heredoc_tag, start, len);
129 12           ctx->heredoc_tag[len] = '\0';
130 12           ctx->heredoc_tag_len = len;
131 12           ctx->heredoc_indented = indented;
132 12           ctx->heredoc_pending = 1;
133              
134 12           return 1;
135             }
136              
137             /* ══════════════════════════════════════════════════════════════════
138             * Check if line is a heredoc terminator
139             * ══════════════════════════════════════════════════════════════════ */
140              
141 45           static int eshu_pl_is_heredoc_end(const eshu_pl_ctx_t *ctx,
142             const char *line, const char *eol) {
143 45           const char *p = line;
144             int len;
145              
146             /* for <<~ the terminator may be indented */
147 45 100         if (ctx->heredoc_indented) {
148 15 50         while (p < eol && (*p == ' ' || *p == '\t'))
    100          
    50          
149 12           p++;
150             }
151              
152 45           len = (int)(eol - p);
153             /* terminator may have trailing ; or whitespace but we'll be strict:
154             the line content (trimmed) must exactly match the tag */
155 45 50         if (len < ctx->heredoc_tag_len)
156 0           return 0;
157              
158 45 100         if (memcmp(p, ctx->heredoc_tag, ctx->heredoc_tag_len) != 0)
159 33           return 0;
160              
161             /* rest of line must be empty or just whitespace/semicolons */
162 12           p += ctx->heredoc_tag_len;
163 12 50         while (p < eol) {
164 0 0         if (*p != ' ' && *p != '\t' && *p != ';')
    0          
    0          
165 0           return 0;
166 0           p++;
167             }
168 12           return 1;
169             }
170              
171             /* ══════════════════════════════════════════════════════════════════
172             * Pod detection — "=word" at start of line
173             * ══════════════════════════════════════════════════════════════════ */
174              
175 665           static int eshu_pl_is_pod_start(const char *content, const char *eol) {
176 665 100         if (*content != '=')
177 641           return 0;
178             /* must be followed by a letter */
179 24 50         if (content + 1 >= eol || !isalpha((unsigned char)content[1]))
    50          
180 0           return 0;
181             /* must NOT be =cut */
182 24 50         if (eol - content >= 4 && memcmp(content, "=cut", 4) == 0)
    50          
183 0           return 0;
184 24           return 1;
185             }
186              
187 558           static int eshu_pl_is_pod_end(const char *content, const char *eol) {
188 558           int len = (int)(eol - content);
189 558 100         if (len < 4) return 0;
190 545 100         if (memcmp(content, "=cut", 4) != 0) return 0;
191             /* rest should be whitespace or EOL */
192 22 50         if (len > 4 && content[4] != ' ' && content[4] != '\t')
    0          
    0          
193 0           return 0;
194 22           return 1;
195             }
196              
197             /* ══════════════════════════════════════════════════════════════════
198             * Classify whether a preceding context expects regex or division
199             *
200             * Returns 1 if the next '/' should be treated as regex opening.
201             * ══════════════════════════════════════════════════════════════════ */
202              
203 9           static int eshu_pl_expects_regex(const eshu_pl_ctx_t *ctx) {
204             /*
205             * If last token was a "value" (variable, number, closing bracket/paren)
206             * then / is division. Otherwise it's regex.
207             */
208 9           return !ctx->last_was_value;
209             }
210              
211             /* ══════════════════════════════════════════════════════════════════
212             * Enter a quoted construct (q/qq/qw/qx/s/tr/y/m)
213             *
214             * p points to the character AFTER the keyword letter(s).
215             * Returns the number of chars consumed for the delimiter.
216             * ══════════════════════════════════════════════════════════════════ */
217              
218 28           static int eshu_pl_enter_quoted(eshu_pl_ctx_t *ctx, char delim,
219             int sections, enum eshu_state state) {
220 28           ctx->q_open = delim;
221 28           ctx->q_sections = sections;
222 28 100         if (eshu_pl_is_paired(delim)) {
223 23           ctx->q_close = eshu_pl_matching_close(delim);
224 23           ctx->q_depth = 1;
225             } else {
226 5           ctx->q_close = delim;
227 5           ctx->q_depth = 0; /* non-paired don't nest */
228             }
229 28           ctx->state = state;
230 28           return 1; /* consumed the delimiter char */
231             }
232              
233             /* ══════════════════════════════════════════════════════════════════
234             * Try to detect q/qq/qw/qx/s/tr/y/m constructs
235             *
236             * p points to a char that may be q, s, t, m, y.
237             * Returns chars consumed (including keyword + delimiter) or 0.
238             * ══════════════════════════════════════════════════════════════════ */
239              
240 479           static int eshu_pl_try_q_construct(eshu_pl_ctx_t *ctx,
241             const char *p, const char *end) {
242 479           const char *start = p;
243 479           char c = *p;
244 479           int sections = 1;
245 479           enum eshu_state st = ESHU_Q;
246              
247 479 100         if (c == 'q') {
248 13           p++;
249 13 50         if (p < end && *(p) == 'w') {
    100          
250 9           p++; st = ESHU_QW;
251 4 50         } else if (p < end && *(p) == 'q') {
    100          
252 1           p++; st = ESHU_QQ;
253 3 50         } else if (p < end && *(p) == 'x') {
    50          
254 0           p++; st = ESHU_QQ; /* qx behaves like qq */
255 3 50         } else if (p < end && *(p) == 'r') {
    100          
256 2           p++; st = ESHU_QQ; /* qr// behaves like qq for scanning */
257             } else {
258 1           st = ESHU_Q;
259             }
260 466 100         } else if (c == 's') {
261 224           p++;
262 224           sections = 2;
263 224           st = ESHU_QQ; /* s/// — two sections, interpolates */
264 242 100         } else if (c == 't' && p + 1 < end && *(p + 1) == 'r') {
    50          
    50          
265 1           p += 2;
266 1           sections = 2;
267 1           st = ESHU_QQ;
268 241 100         } else if (c == 'y') {
269 3           p++;
270 3           sections = 2;
271 3           st = ESHU_QQ;
272 238 50         } else if (c == 'm') {
273 238           p++;
274 238           st = ESHU_QQ; /* m// uses same delimiter tracking as qq */
275             } else {
276 0           return 0;
277             }
278              
279             /* Must be followed by a non-alnum delimiter */
280 479 50         if (p >= end)
281 0           return 0;
282 479 100         if (isalnum((unsigned char)*p) || *p == '_')
    50          
283 454           return 0;
284 25 100         if (*p == ' ' || *p == '\t' || *p == '\n')
    50          
    50          
285 2           return 0;
286              
287 23           eshu_pl_enter_quoted(ctx, *p, sections, st);
288 23           p++;
289 23           return (int)(p - start);
290             }
291              
292             /* Check if the character before position p is a word character
293             * (to prevent matching 'eq' as 'q' construct, etc.) */
294 692           static int eshu_pl_preceded_by_word(const char *line_start, const char *p) {
295 692 100         if (p <= line_start)
296 335           return 0;
297 357 100         return isalnum((unsigned char)*(p - 1)) || *(p - 1) == '_';
    50          
298             }
299              
300             /* ══════════════════════════════════════════════════════════════════
301             * Scan a line for nesting changes (Perl-aware)
302             *
303             * Called AFTER the line has been emitted. Updates ctx->state
304             * and ctx->depth for the next line.
305             * ══════════════════════════════════════════════════════════════════ */
306              
307 976           static void eshu_pl_scan_line(eshu_pl_ctx_t *ctx,
308             const char *p, const char *end) {
309 976           const char *line_start = p;
310              
311 9005 100         while (p < end) {
312 8053           char c = *p;
313              
314 8053           switch (ctx->state) {
315 7143           case ESHU_CODE:
316 7143 100         if (c == '{' || c == '(' || c == '[') {
    100          
    100          
317 623           ctx->depth++;
318 623           ctx->last_was_value = 0;
319 6520 100         } else if (c == '}' || c == ')' || c == ']') {
    100          
    100          
320 623           ctx->depth--;
321 623 50         if (ctx->depth < 0) ctx->depth = 0;
322 623           ctx->last_was_value = 1;
323 5897 100         } else if (c == '"') {
324 22           ctx->state = ESHU_STRING_DQ;
325 22           ctx->last_was_value = 0;
326 5875 100         } else if (c == '\'') {
327 47           ctx->state = ESHU_STRING_SQ;
328 47           ctx->last_was_value = 0;
329 5828 100         } else if (c == '#') {
330             /* line comment — skip rest */
331 24           ctx->last_was_value = 0;
332 24           return;
333 5804 100         } else if (c == '<' && eshu_pl_detect_heredoc(ctx, p, end)) {
    100          
334             /* heredoc_pending is set; continue scanning rest of line */
335             /* skip past the heredoc operator to avoid re-matching */
336 12           p += 2; /* skip << */
337 12 50         if (p < end && *p == '~') p++;
    100          
338 18 50         if (p < end && (*p == '\'' || *p == '"' || *p == '`')) {
    100          
    100          
    50          
339 6           char hq = *p; p++;
340 28 50         while (p < end && *p != hq) p++;
    100          
341 6 50         if (p < end) p++;
342             } else {
343 32 50         while (p < end && (isalnum((unsigned char)*p) || *p == '_'))
    100          
    50          
344 26           p++;
345             }
346 12           ctx->last_was_value = 1;
347 12           continue;
348 5792 100         } else if (c == '/' && eshu_pl_expects_regex(ctx)) {
    100          
349             /* regex literal */
350 3           ctx->rx_delim = '/';
351 3           ctx->state = ESHU_REGEX;
352 3           ctx->last_was_value = 0;
353 5789 100         } else if ((c == 'q' || c == 'm' || c == 's' || c == 'y' ||
    100          
    100          
    100          
    100          
354 739 50         (c == 't' && p + 1 < end && *(p + 1) == 'r')) &&
355 1148           !eshu_pl_preceded_by_word(line_start, p)) {
356 479           int consumed = eshu_pl_try_q_construct(ctx, p, end);
357 479 100         if (consumed > 0) {
358 23           p += consumed;
359 23           ctx->last_was_value = 0;
360 23           continue;
361             }
362             /* not a q-construct, fall through */
363 456 50         if (isalnum((unsigned char)c))
364 456           ctx->last_was_value = 0;
365 5310 100         } else if (c == '/' && !eshu_pl_expects_regex(ctx)) {
    50          
366             /* division operator */
367 3           ctx->last_was_value = 0;
368 5307 100         } else if (c == '$' || c == '@' || c == '%') {
    100          
    100          
369             /* variable sigil — skip the variable name */
370 698           ctx->last_was_value = 1;
371 698           p++;
372 3088 50         while (p < end && (isalnum((unsigned char)*p) || *p == '_' || *p == ':'))
    100          
    100          
    50          
373 2390           p++;
374 698           continue;
375 4609 100         } else if (isdigit((unsigned char)c)) {
376 151           ctx->last_was_value = 1;
377 343 50         while (p < end && (isalnum((unsigned char)*p) || *p == '.' || *p == '_'))
    100          
    100          
    50          
378 192           p++;
379 151           continue;
380 4458 100         } else if (c == '=' && p + 1 < end && *(p + 1) == '~') {
    50          
    100          
381             /* =~ forces next / to be regex */
382 14           ctx->last_was_value = 0;
383 14           p += 2;
384 14           continue;
385 4444 50         } else if (c == '!' && p + 1 < end && *(p + 1) == '~') {
    0          
    0          
386 0           ctx->last_was_value = 0;
387 0           p += 2;
388 0           continue;
389 4444 100         } else if (c == ' ' || c == '\t') {
    50          
390             /* whitespace — don't change last_was_value */
391 2523 100         } else if (isalpha((unsigned char)c) || c == '_') {
    100          
392             /* keyword or bareword — skip it */
393 1122           const char *ws = p;
394 5406 100         while (p < end && (isalnum((unsigned char)*p) || *p == '_'))
    100          
    100          
395 4284           p++;
396             /* barewords ending in a value context: could be function call */
397 1122           ctx->last_was_value = 0;
398 1122           continue;
399             } else {
400             /* operator chars: = + - * etc */
401 1401           ctx->last_was_value = 0;
402             }
403 5099           break;
404              
405 231           case ESHU_STRING_DQ:
406 231 100         if (c == '\\' && p + 1 < end) {
    50          
407 16           p++; /* skip escaped char */
408 215 100         } else if (c == '"') {
409 22           ctx->state = ESHU_CODE;
410 22           ctx->last_was_value = 1;
411             }
412 231           break;
413              
414 283           case ESHU_STRING_SQ:
415 283 100         if (c == '\\' && p + 1 < end) {
    50          
416 1           p++; /* skip escaped char */
417 282 100         } else if (c == '\'') {
418 47           ctx->state = ESHU_CODE;
419 47           ctx->last_was_value = 1;
420             }
421 283           break;
422              
423 23           case ESHU_REGEX:
424 23 100         if (c == '\\' && p + 1 < end) {
    50          
425 3           p++; /* skip escaped char */
426 20 100         } else if (c == ctx->rx_delim) {
427 3           ctx->state = ESHU_CODE;
428 3           ctx->last_was_value = 1;
429             /* skip optional flags */
430 3           p++;
431 3 50         while (p < end && isalpha((unsigned char)*p))
    50          
432 0           p++;
433 3           continue;
434             }
435 20           break;
436              
437 373           case ESHU_QW:
438             case ESHU_QQ:
439             case ESHU_Q:
440 373 100         if (c == '\\' && p + 1 < end && ctx->state != ESHU_QW) {
    50          
    50          
441 4           p++; /* skip escaped char (not in qw) */
442 369 100         } else if (ctx->q_close != ctx->q_open) {
443             /* paired delimiters — track nesting */
444 331 100         if (c == ctx->q_open) {
445 4           ctx->q_depth++;
446 327 100         } else if (c == ctx->q_close) {
447 27           ctx->q_depth--;
448 27 100         if (ctx->q_depth == 0) {
449 23           ctx->q_sections--;
450 23 100         if (ctx->q_sections <= 0) {
451 18           ctx->state = ESHU_CODE;
452 18           ctx->last_was_value = 1;
453             /* skip optional flags */
454 18           p++;
455 27 50         while (p < end && isalpha((unsigned char)*p))
    100          
456 9           p++;
457 18           continue;
458             } else {
459             /* next section: find new delimiter */
460 5           p++;
461             /* skip whitespace between sections */
462 5 50         while (p < end && (*p == ' ' || *p == '\t'))
    50          
    50          
463 0           p++;
464 5 50         if (p < end) {
465 5           eshu_pl_enter_quoted(ctx, *p,
466             ctx->q_sections, ctx->state);
467 5           p++; /* skip past opening delimiter */
468             }
469 5           continue;
470             }
471             }
472             }
473             } else {
474             /* non-paired: same char opens and closes */
475 38 100         if (c == ctx->q_close) {
476 9           ctx->q_sections--;
477 9 100         if (ctx->q_sections <= 0) {
478 5           ctx->state = ESHU_CODE;
479 5           ctx->last_was_value = 1;
480             /* skip optional flags */
481 5           p++;
482 8 50         while (p < end && isalpha((unsigned char)*p))
    100          
483 3           p++;
484 5           continue;
485             }
486             /* next section uses same delimiter, just continue */
487             }
488             }
489 345           break;
490              
491             /* These states are handled at the line level, not char level */
492 0           case ESHU_HEREDOC:
493             case ESHU_HEREDOC_INDENT:
494             case ESHU_POD:
495             case ESHU_CHAR_LIT:
496             case ESHU_COMMENT_LINE:
497             case ESHU_COMMENT_BLOCK:
498             case ESHU_PREPROCESSOR:
499 0           return;
500             }
501 5978           p++;
502             }
503             }
504              
505             /* ══════════════════════════════════════════════════════════════════
506             * Process a single Perl line — decide indent, emit, scan
507             * ══════════════════════════════════════════════════════════════════ */
508              
509 2082           static void eshu_pl_process_line(eshu_pl_ctx_t *ctx, eshu_buf_t *out,
510             const char *line_start, const char *eol) {
511 2082           const char *content = eshu_skip_leading_ws(line_start);
512             int line_len;
513             int indent_depth;
514              
515             /* ── Pod section: buffer and indent (before blank check!) ── */
516 2082 100         if (ctx->state == ESHU_POD) {
517 896 100         if (content < eol && eshu_pl_is_pod_end(content, eol)) {
    100          
518             /* Run buffered POD through the POD indenter */
519 22 50         if (ctx->pod_buf.len > 0) {
520             char *pod_result;
521             size_t pod_out_len;
522 22           eshu_buf_putc(&ctx->pod_buf, '\0');
523 22           ctx->pod_buf.len--;
524 22           pod_result = eshu_indent_pod(ctx->pod_buf.data, ctx->pod_buf.len, &ctx->cfg, &pod_out_len);
525 22           eshu_buf_write(out, pod_result, (int)pod_out_len);
526 22           free(pod_result);
527             }
528 22           ctx->pod_buf.len = 0;
529 22           ctx->pod_active = 0;
530             /* Emit =cut at column 0 */
531 22           eshu_buf_write_trimmed(out, content, (int)(eol - content));
532 22           eshu_buf_putc(out, '\n');
533 22           ctx->state = ESHU_CODE;
534             } else {
535             /* Buffer this POD line (including blanks) */
536 874           eshu_buf_write(&ctx->pod_buf, line_start, (int)(eol - line_start));
537 874           eshu_buf_putc(&ctx->pod_buf, '\n');
538             }
539 896           return;
540             }
541              
542             /* ── Heredoc body: pass through verbatim (before blank check!) ── */
543 1186 100         if (ctx->state == ESHU_HEREDOC || ctx->state == ESHU_HEREDOC_INDENT) {
    100          
544             /* emit line exactly as-is */
545 45           eshu_buf_write_trimmed(out, line_start, (int)(eol - line_start));
546 45           eshu_buf_putc(out, '\n');
547              
548             /* check for terminator */
549 45 100         if (eshu_pl_is_heredoc_end(ctx, line_start, eol)) {
550 12           ctx->state = ESHU_CODE;
551 12           ctx->heredoc_tag[0] = '\0';
552 12           ctx->heredoc_tag_len = 0;
553             }
554 45           return;
555             }
556              
557             /* empty line — preserve it */
558 1141 100         if (content >= eol) {
559 141           eshu_buf_putc(out, '\n');
560 141           return;
561             }
562              
563 1000           line_len = (int)(eol - content);
564              
565             /* ── Pod start detection (= at column 0) ── */
566 1000 100         if (content == line_start && eshu_pl_is_pod_start(content, eol)) {
    100          
567 24           ctx->state = ESHU_POD;
568 24           ctx->pod_active = 1;
569             /* Buffer the opening directive line */
570 24           eshu_buf_write(&ctx->pod_buf, content, (int)(eol - content));
571 24           eshu_buf_putc(&ctx->pod_buf, '\n');
572 24           return;
573             }
574              
575             /* ── Inside multi-line q/qq/qw/regex: indent at current depth+1 ── */
576 976 100         if (ctx->state == ESHU_QW || ctx->state == ESHU_QQ ||
    100          
577 954 50         ctx->state == ESHU_Q || ctx->state == ESHU_REGEX) {
    50          
578 22           int qdepth = ctx->depth + 1;
579             /* closing delimiter line gets same depth as opening line */
580 22 50         if (ctx->q_close && *content == ctx->q_close)
    100          
581 7           qdepth = ctx->depth;
582 22           eshu_emit_indent(out, qdepth, &ctx->cfg);
583 22           eshu_buf_write_trimmed(out, content, line_len);
584 22           eshu_buf_putc(out, '\n');
585 22           eshu_pl_scan_line(ctx, content, eol);
586 22           return;
587             }
588              
589             /* ── Normal Perl code ── */
590 954           indent_depth = ctx->depth;
591              
592             /* If line starts with closer, dedent this line */
593 954 100         if (*content == '}' || *content == ')' || *content == ']') {
    100          
    100          
594 220           indent_depth--;
595 220 50         if (indent_depth < 0) indent_depth = 0;
596             }
597              
598 954           eshu_emit_indent(out, indent_depth, &ctx->cfg);
599 954           eshu_buf_write_trimmed(out, content, line_len);
600 954           eshu_buf_putc(out, '\n');
601              
602             /* scan for nesting changes */
603 954           eshu_pl_scan_line(ctx, content, eol);
604              
605             /* If a heredoc was detected on this line, enter heredoc state now */
606 954 100         if (ctx->heredoc_pending) {
607 12           ctx->heredoc_pending = 0;
608 12 100         ctx->state = ctx->heredoc_indented ? ESHU_HEREDOC_INDENT : ESHU_HEREDOC;
609             }
610             }
611              
612             /* ══════════════════════════════════════════════════════════════════
613             * Public API — indent a Perl source string
614             * ══════════════════════════════════════════════════════════════════ */
615              
616 113           static char * eshu_indent_pl(const char *src, size_t src_len,
617             const eshu_config_t *cfg, size_t *out_len) {
618             eshu_pl_ctx_t ctx;
619             eshu_buf_t out;
620 113           const char *p = src;
621 113           const char *end = src + src_len;
622             char *result;
623              
624 113           eshu_pl_ctx_init(&ctx, cfg);
625 113           eshu_buf_init(&out, src_len + 256);
626              
627             {
628 113           int line_num = 1;
629 2195 100         while (p < end) {
630 2082           const char *eol = eshu_find_eol(p);
631              
632 2082 100         if (eshu_in_range(cfg, line_num)) {
633 2079           eshu_pl_process_line(&ctx, &out, p, eol);
634             } else {
635 3           size_t saved = out.len;
636 3           eshu_pl_process_line(&ctx, &out, p, eol);
637 3           out.len = saved;
638 3           eshu_buf_write_trimmed(&out, p, (int)(eol - p));
639 3           eshu_buf_putc(&out, '\n');
640             }
641              
642 2082           p = eol;
643 2082 50         if (*p == '\n') p++;
644 2082           line_num++;
645             }
646             }
647              
648             /* Flush any remaining buffered POD (no =cut at EOF) */
649 113 100         if (ctx.pod_buf.len > 0) {
650             char *pod_result;
651             size_t pod_out_len;
652 2           eshu_buf_putc(&ctx.pod_buf, '\0');
653 2           ctx.pod_buf.len--;
654 2           pod_result = eshu_indent_pod(ctx.pod_buf.data, ctx.pod_buf.len, &ctx.cfg, &pod_out_len);
655 2           eshu_buf_write(&out, pod_result, (int)pod_out_len);
656 2           free(pod_result);
657             }
658 113           eshu_buf_free(&ctx.pod_buf);
659              
660             /* NUL-terminate */
661 113           eshu_buf_putc(&out, '\0');
662 113           out.len--;
663              
664 113           *out_len = out.len;
665 113           result = out.data;
666 113           return result;
667             }
668              
669             #endif /* ESHU_PL_H */