File Coverage

include/eshu_pl.h
Criterion Covered Total %
statement 371 396 93.6
branch 306 412 74.2
condition n/a
subroutine n/a
pod n/a
total 677 808 83.7


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