File Coverage

blib/lib/File/Stubb/Render.pm
Criterion Covered Total %
statement 231 266 86.8
branch 84 118 71.1
condition 33 40 82.5
subroutine 34 41 82.9
pod 17 17 100.0
total 399 482 82.7


line stmt bran cond sub pod time code
1             package File::Stubb::Render;
2 3     3   131789 use 5.016;
  3         12  
3             our $VERSION = '0.04';
4 3     3   30 use strict;
  3         12  
  3         90  
5 3     3   40 use warnings;
  3         6  
  3         210  
6              
7 3     3   19 use File::Basename;
  3         7  
  3         293  
8 3     3   35 use File::Spec;
  3         6  
  3         78  
9 3     3   1738 use JSON::PP;
  3         46188  
  3         14217  
10              
11             # TODO: '?' sigil for '#' and '$' restricted targets
12             # TODO: Set '$_' in Perl and shell substs to rendered file
13              
14             my $ILLEGAL_PATH_RX = $^O eq 'MSWin32'
15             ? qr/[<>:"\/\\|?*]/
16             : qr/\//;
17              
18             my $PATH_SEP = $^O eq 'MSWin32' ? '\\' : '/';
19              
20             our $SUBST_TARGET_NAME_RX = qr/[a-zA-Z0-9_]+/;
21              
22             my $STDOUT_PATH = '-';
23              
24             my $SUBST_TARGET_RX = qr{
25             (? \\? [\?\$!\#])?
26             \^\^
27             \s* (? .*?) \s*
28             \^\^
29             }x;
30              
31             # Path substitution targets only contain basic targets
32             my $SUBST_TARGET_PATH_RX = qr{
33             \^\^
34             \s* (? $SUBST_TARGET_NAME_RX) \s*
35             \^\^
36             }x;
37              
38             sub _dir {
39              
40 18     18   59 my ($dir, $hidden) = @_;
41 18   50     52 $hidden //= 0;
42              
43 18 50       919 opendir my $dh, $dir
44             or die "Failed to open $dir as a directory: $!\n";
45             my @files =
46             sort
47 18         470 grep { ! /^\.\.?$/ }
  72         430  
48             readdir $dh;
49 18         224 closedir $dh;
50              
51 18 100       92 unless ($hidden) {
52 12         28 @files = grep { ! /^\./ } @files;
  24         80  
53             }
54              
55 18         52 return map { File::Spec->catfile($dir, $_) } @files;
  24         406  
56              
57             }
58              
59             sub _read_json {
60              
61 4     4   16 my ($self, $file) = @_;
62              
63 4 50       212 open my $fh, '<', $file
64             or die "Failed to open $file for reading: $!\n";
65 4         12 my $json = do { local $/ = undef; <$fh> };
  4         22  
  4         145  
66 4         47 close $fh;
67              
68 4         40 my $ref = decode_json($json);
69              
70 4 50       7279 unless (ref $ref eq 'HASH') {
71 0         0 die "Invalid .stubb.json\n";
72             }
73              
74 4 100 66     31 if ($self->{ Defaults } and ref $ref->{ defaults } eq 'HASH') {
75 2         5 for my $k (keys %{ $ref->{ defaults } }) {
  2         12  
76 6         20 $self->{ Subst }{ $k } = $ref->{ defaults }{ $k };
77             }
78             }
79              
80 4 50       14 if (exists $ref->{ render_hidden }) {
81 4         97 $self->{ Hidden } = !! $ref->{ render_hidden };
82             }
83              
84 4 50       41 if (exists $ref->{ follow_symlinks }) {
85 4         12 $self->{ FollowLink } = !! $ref->{ follow_symlinks };
86             }
87              
88 4 50       96 if (exists $ref->{ copy_perms }) {
89 4         14 $self->{ CopyPerms } = !! $ref->{ copy_perms };
90             }
91              
92 4         50 return 1;
93              
94             }
95              
96             sub _pl_subst {
97              
98 4     4   10 my ($self, $pl) = @_;
99              
100 4         35 local %_ = %{ $self->{ Subst } };
  4         21  
101 4   100 1   348 return eval("{ no strict; no warnings; $pl }") // '';
  1     1   9  
  1     1   3  
  1     1   43  
  1     1   6  
  1     1   2  
  1     1   66  
  1     1   6  
  1         2  
  1         33  
  1         4  
  1         3  
  1         66  
  1         5  
  1         2  
  1         23  
  1         4  
  1         2  
  1         63  
  1         6  
  1         3  
  1         22  
  1         5  
  1         2  
  1         39  
102              
103             }
104              
105             sub _qx_subst {
106              
107 4     4   15 my ($self, $qx) = @_;
108              
109 4         392 local %ENV = %ENV;
110 4         70 for my $k (%{ $self->{ Subst } }) {
  4         41  
111 24         173 $ENV{ $k } = $self->{ Subst }{ $k };
112             }
113              
114 4         18938 my $rt = qx/$qx/;
115 4         47 chomp $rt;
116 4         648 return $rt;
117              
118             }
119              
120             sub _render_link {
121              
122 0     0   0 my ($self, $link, $out) = @_;
123              
124 0         0 my $to = $self->render_path(readlink $link);
125              
126 0 0       0 symlink $to, $out
127             or die "Failed to symlink $to to $out: $!\n";
128              
129 0         0 return $out;
130              
131             }
132              
133             sub _render_dir {
134              
135 14     14   50 my ($self, $template, $out) = @_;
136              
137 14 50       47 if ($out eq $STDOUT_PATH) {
138 0         0 die "Cannot render directory to stdout\n";
139             }
140              
141 14         26 my @created;
142              
143 14 50       1760 mkdir $out or die "Failed to mkdir $out: $!\n";
144 14 100       88 if ($self->{ CopyPerms }) {
145 6         282 chmod((stat($template))[2] & 0777, $out);
146             }
147 14         56 push @created, $out;
148              
149 14         66 for my $f (_dir($template, $self->{ Hidden })) {
150              
151 17         786 my $bn = basename($f);
152              
153 17 100       68 next if $bn eq '.stubb.json';
154              
155 15         59 my $o = File::Spec->catfile(
156             $out,
157             $self->render_path($bn)
158             );
159              
160 15         278 my @c;
161              
162 15         28 eval {
163 15 100 66     444 if (-d $f) {
    50          
164 7         56 @c = $self->_render_dir($f, $o);
165             } elsif (!$self->{ FollowLink } and -l $f) {
166 0         0 @c = $self->_render_link($f, $o);
167             } else {
168 8         41 @c = $self->_render_file($f, $o);
169             }
170             };
171              
172 15 50       46 if ($@ ne '') {
173 0         0 for my $del (reverse @created) {
174 0 0       0 if (-d $del) {
    0          
175 0 0       0 rmdir $del or next;
176             } elsif (-f $del) {
177 0 0       0 unlink $del or next;
178             }
179             }
180 0         0 die $@;
181             }
182              
183 15         48 push @created, @c;
184              
185             }
186              
187 14         53 return @created;
188              
189             }
190              
191             sub _render_file {
192              
193 23     23   72 my ($self, $template, $out) = @_;
194              
195 23 50       1013 open my $rh, '<', $template
196             or die "Failed to open $template for reading: $!\n";
197 23         71 binmode $rh;
198              
199 23         69 my $wh;
200 23 50       89 if ($out eq $STDOUT_PATH) {
201 0         0 $wh = *STDOUT;
202             } else {
203 23 50       3405 open $wh, '>', $out
204             or die "Failed to open $out for writing: $!\n";
205             }
206 23         90 binmode $wh;
207              
208 23         513 while (my $l = readline $rh) {
209 60         878 $l =~ s{($SUBST_TARGET_RX)}{
210 85         238 my $m = $1;
211 85         793 my %c = %+;
212 85         241 my $repl = '';
213             {
214 85 100 100     126 if (defined $c{ SIGIL } and $c{ SIGIL } =~ /^\\/) {
  85         397  
215             # Copy escaped sigil
216 12         31 $repl .= substr $c{ SIGIL }, 1, 1;
217 12         26 $m = substr $m, length $c{ SIGIL };
218             }
219 85 100 100     385 if (not defined $c{ SIGIL } or $c{ SIGIL } =~ /^\\/) {
    100          
    100          
    100          
    50          
220 54         170 my ($targ, $def) = split /\s*\/\/\s*/, $c{ TARGET };
221 54 50       293 if ($targ !~ /^$SUBST_TARGET_NAME_RX$/) {
222 0         0 $repl = $m;
223 0         0 last;
224             }
225             $repl .= exists $self->{ Subst }{ $targ }
226 54 100 66     198 ? $self->{ Subst }{ $targ }
227             : $def // $m;
228             } elsif ($c{ SIGIL } eq '?') {
229 6 50       55 if ($c{ TARGET } !~ /^$SUBST_TARGET_NAME_RX$/) {
230 0         0 $repl = $m;
231 0         0 last;
232             }
233             $repl = exists $self->{ Subst }{ $c{ TARGET } }
234             ? $self->{ Subst }{ $c{ TARGET } }
235 6 100       23 : '';
236             } elsif ($c{ SIGIL } eq '$') {
237             $repl = $self->{ Restricted }
238             ? $m
239 12 100       55 : $self->_pl_subst($c{ TARGET });
240             } elsif ($c{ SIGIL } eq '#') {
241             $repl = $self->{ Restricted }
242             ? $m
243 8 100       43 : $self->_qx_subst($c{ TARGET });
244             } elsif ($c{ SIGIL } eq '!') {
245 5         12 $repl = substr $m, 1;
246             }
247             }
248 85         435 $repl;
249             }ge;
250 60         127 print { $wh } $l;
  60         750  
251             }
252              
253 23         249 close $rh;
254 23 50       1221 close $wh unless $out eq $STDOUT_PATH;
255              
256 23 100 66     162 if ($self->{ CopyPerms } and $out ne $STDOUT_PATH) {
257 4         177 chmod((stat($template))[2] & 0777, $out);
258             }
259              
260 23         219 return $out;
261              
262             }
263              
264             sub _path_targets {
265              
266 6     6   15 my ($self, $path) = @_;
267              
268 6         10 my %targ;
269              
270 6         108 while (my $m = $path =~ m/($SUBST_TARGET_PATH_RX)/g) {
271 6         74 $targ{ $+{ TARGET } } = 1;
272             }
273              
274 6         33 return sort keys %targ;
275              
276             }
277              
278             sub _file_targets {
279              
280 13     13   45 my ($self, $template, $targets) = @_;
281              
282 13 50       623 open my $fh, '<', $template
283             or die "Failed to open $template for reading: $!\n";
284 13         73 binmode $fh;
285              
286 13         504 while (my $l = readline $fh) {
287 36         645 while (my $m = $l =~ m/($SUBST_TARGET_RX)/g) {
288 45         423 my %c = %+;
289 45 100 100     533 if (not defined $c{ SIGIL } or $c{ SIGIL } eq '?' or $c{ SIGIL } =~ /^\\/) {
    100 100        
    100 100        
      100        
290 24         72 my $t = $c{ TARGET } =~ s/\s*\/\/.*$//r;
291 24 50       142 next unless $t =~ /^$SUBST_TARGET_NAME_RX$/;
292 24         223 $targets->{ basic }{ $t } = 1;
293             } elsif ($c{ SIGIL } eq '$' and not $self->{ Restricted }) {
294 4         6 push @{ $targets->{ perl } }, $c{ TARGET };
  4         35  
295             } elsif ($c{ SIGIL } eq '#' and not $self->{ Restricted }) {
296 4         11 push @{ $targets->{ shell } }, $c{ TARGET };
  4         42  
297             }
298             }
299             }
300              
301 13         151 close $fh;
302              
303 13         80 return 1;
304              
305             }
306              
307             sub _dir_targets {
308              
309 4     4   10 my ($self, $template, $targets) = @_;
310              
311 4         26 for my $f (_dir($template, $self->{ Hidden })) {
312              
313 7         311 my $bn = basename($f);
314              
315 7 100       33 next if $bn eq '.stubb.json';
316              
317 6         23 for my $t ($self->_path_targets($bn)) {
318 6         17 $targets->{ basic }{ $t } = 1;
319             }
320              
321 6 50 33     189 if (-l $f and !$self->{ FollowLink }) {
    100          
322 0         0 for my $t ($self->_path_targets(readlink $f)) {
323 0         0 $targets->{ basic }{ $t } = 1;
324             }
325             } elsif (-d $f) {
326 2         17 $self->_dir_targets($f, $targets);
327             } else {
328 4         14 $self->_file_targets($f, $targets);
329             }
330              
331             }
332              
333 4         11 return 1;
334              
335             }
336              
337             sub new {
338              
339 23     23 1 280700 my ($class, %param) = @_;
340              
341             my $self = {
342             Template => $param{ template },
343 23         270 Subst => {},
344             Hidden => 0,
345             FollowLink => 1,
346             CopyPerms => 0,
347             Defaults => 1,
348             IgnoreConf => 0,
349             Restricted => 0,
350             };
351              
352 23         73 bless $self, $class;
353              
354 23         273 my $json = File::Spec->catfile($self->{ Template }, '.stubb.json');
355              
356 23 100       124 if (defined $param{ ignore_config }) {
357 2         7 $self->{ IgnoreConf } = !! $param{ ignore_config };
358             }
359              
360 23 100       78 if (defined $param{ defaults }) {
361 2         7 $self->{ Defaults } = !! $param{ defaults };
362             }
363              
364 23 100 100     572 if (-f $json and !$self->{ IgnoreConf }) {
365 4         29 $self->_read_json($json);
366             }
367              
368 23 100       93 if (defined $param{ subst }) {
369 11         36 $self->{ Subst } = $param{ subst };
370             }
371              
372 23 100       75 if (defined $param{ render_hidden }) {
373 2         10 $self->{ Hidden } = !! $param{ render_hidden };
374             }
375              
376 23 100       91 if (defined $param{ follow_symlinks }) {
377 1         3 $self->{ FollowLink } = !! $param{ follow_symlinks };
378             }
379              
380 23 100       76 if (defined $param{ copy_perms }) {
381 1         6 $self->{ CopyPerms } = !! $param{ copy_perms };
382             }
383              
384 23 100       77 if (defined $param{ restricted }) {
385 3         15 $self->{ Restricted } = !! $param{ restricted };
386             }
387              
388 23         39 for my $k (keys %{ $self->{ Subst } }) {
  23         176  
389 36 50       354 unless ($k =~ /^$SUBST_TARGET_NAME_RX$/) {
390 0         0 die "'$k' is not a valid substitution target\n";
391             }
392             }
393              
394 23         169 return $self;
395              
396             }
397              
398             sub render {
399              
400 22     22 1 12612 my ($self, $out) = @_;
401              
402 22         79 $out = $self->render_path($out);
403              
404             my @created = -d $self->{ Template }
405             ? $self->_render_dir($self->{ Template }, $out)
406 22 100       590 : $self->_render_file($self->{ Template }, $out);
407              
408 22         130 return @created;
409              
410             }
411              
412             sub targets {
413              
414 11     11 1 3895 my ($self) = @_;
415              
416 11         68 my $targets = {
417             basic => {},
418             perl => [],
419             shell => [],
420             };
421              
422 11 100       328 if (-d $self->{ Template }) {
423 2         11 $self->_dir_targets($self->{ Template }, $targets);
424             } else {
425 9         178 $self->_file_targets($self->{ Template }, $targets);
426             }
427              
428 11         29 $targets->{ basic } = [ sort keys %{ $targets->{ basic } } ];
  11         88  
429              
430 11         100 return $targets;
431              
432             }
433              
434             sub render_path {
435              
436 38     38 1 1514 my ($self, $path) = @_;
437              
438 38         303 my @parts = split /\Q$PATH_SEP\E/, $path;
439              
440 38         105 for (@parts) {
441 91         540 s{($SUBST_TARGET_PATH_RX)}{
442 18         53 my $m = $1;
443 18         210 my %c = %+;
444 18         62 my $repl = '';
445             {
446 18 100       32 if (not exists $self->{ Subst }{ $c{ TARGET } }) {
  18         114  
447 4         9 $repl = $m;
448 4         10 last;
449             }
450 14 50       123 if ($self->{ Subst }{ $c{ TARGET } } =~ $ILLEGAL_PATH_RX) {
451 0         0 die "'$c{ TARGET }' path substitution would contain illegal path characters\n";
452             }
453 14         35 $repl = $self->{ Subst }{ $c{ TARGET } };
454             }
455 18         96 $repl;
456             }ge;
457             }
458              
459 38         585 return File::Spec->catfile(@parts);
460              
461             }
462              
463             sub template {
464              
465 1     1 1 750 my ($self) = @_;
466              
467 1         19 return $self->{ Template };
468              
469             }
470              
471             sub subst {
472              
473 4     4 1 1873 my ($self) = @_;
474              
475 4         25 return $self->{ Subst };
476              
477             }
478              
479             sub set_subst {
480              
481 0     0 1 0 my ($self, $subst) = @_;
482              
483 0 0       0 unless (ref $subst eq 'HASH') {
484 0         0 die "\$subst must be a hash ref";
485             }
486              
487 0         0 $self->{ Subst } = $subst;
488              
489             }
490              
491             sub hidden {
492              
493 4     4 1 15 my ($self) = @_;
494              
495 4         34 return $self->{ Hidden };
496              
497             }
498              
499             sub set_hidden {
500              
501 0     0 1 0 my ($self, $hidden) = @_;
502              
503 0         0 $self->{ Hidden } = !! $hidden;
504              
505             }
506              
507             sub follow_symlinks {
508              
509 4     4 1 14 my ($self) = @_;
510              
511 4         22 return $self->{ FollowLink };
512              
513             }
514              
515             sub set_follow_symlinks {
516              
517 0     0 1 0 my ($self, $follow) = @_;
518              
519 0         0 $self->{ FollowLink } = !! $follow;
520              
521             }
522              
523             sub copy_perms {
524              
525 4     4 1 12 my ($self) = @_;
526              
527 4         42 return $self->{ CopyPerms };
528              
529             }
530              
531             sub set_copy_perms {
532              
533 0     0 1 0 my ($self, $copy) = @_;
534              
535 0         0 $self->{ CopyPerms } = !! $copy;
536              
537             }
538              
539             sub restricted {
540              
541 3     3 1 1485 my ($self) = @_;
542              
543 3         30 return $self->{ Restricted };
544              
545             }
546              
547             sub set_restricted {
548              
549 0     0 1 0 my ($self, $rest) = @_;
550              
551 0         0 $self->{ Restricted } = !! $rest;
552              
553             }
554              
555             sub defaults {
556              
557 1     1 1 2 my ($self) = @_;
558              
559 1         5 return $self->{ Defaults };
560              
561             }
562              
563             sub ignore_config {
564              
565 0     0 1 0 my ($self) = @_;
566              
567 0         0 return $self->{ IgnoreConf };
568              
569             }
570              
571             1;
572              
573             =head1 NAME
574              
575             File::Stubb::Render - Stubb template rendering class
576              
577             =head1 USAGE
578              
579             use File::Stubb::Render;
580              
581             my $render = File::Stubb::Render->new(
582             template => '/path/to/template',
583             subst => { one => 1, two => 2 },
584             );
585              
586             my @created = $render->render('/path/to/output');
587              
588             =head1 DESCRIPTION
589              
590             B is a module that provides an object-oriented interface
591             to L's template rendering routines. This is a private module for L.
592             For user documentation, consult the L manual.
593              
594             =head1 METHODS
595              
596             =over 4
597              
598             =item $render = File::Stubb::Render->new(%params)
599              
600             Creates a new B object from C<%params>. The following are
601             valid parameters:
602              
603             =over 4
604              
605             =item template
606              
607             Path to template file. Required
608              
609             =item subst
610              
611             Hash ref of substitution parameters.
612              
613             =item render_hidden
614              
615             Boolean determining whether to render hidden files from a template directory.
616             Defaults to false.
617              
618             =item follow_symlinks
619              
620             Boolean determining whether to render the file symlinks point to, or to copy
621             the symlinks themselves in template directories. Defaults to true.
622              
623             =item copy_perms
624              
625             Boolean determining whether to copy the permissions of a template file when
626             rendering. Defaults to false.
627              
628             =item restricted
629              
630             Boolean determining whether to render files in a "restricted" mode. When
631             restricted, Perl targets and shell targets will not be rendered and left as-is.
632              
633             =item defaults
634              
635             Boolean determining whether to use any default substitution parameters
636             provided by a template's F<.stubb.json>.
637              
638             =item ignore_config
639              
640             Boolean determining whether a template's config file should be ignored during
641             initialization or not. Defaults to false.
642              
643             =back
644              
645             =item @created = $render->render($output)
646              
647             Render template to C<$output>. Returns a list of created files.
648              
649             For documentation on how L renders files, consult the L manual.
650              
651             =item $targets = $render->targets()
652              
653             Returns a hash ref list of targets in the object's template. The hash ref will
654             look something like this:
655              
656             {
657             basic => [ ... ],
658             perl => [ ... ],
659             shell => [ ... ],
660             }
661              
662             =item $new = $render->render_path($path)
663              
664             Renders C<$path> as a path name.
665              
666             =item $template = $render->template()
667              
668             Get the path to C<$render>'s template file.
669              
670             =item $subst = $render->subst()
671              
672             =item $render->set_subst($subst)
673              
674             Getter/setter for C<$render>'s substitution parameter hash ref.
675              
676             =item $hidden = $render->hidden()
677              
678             =item $render->set_hidden($hidden)
679              
680             Getter/setter for C<$render>'s render hidden files flag.
681              
682             =item $follow = $render->follow_symlinks()
683              
684             =item $render->set_follow_symlinks($follow)
685              
686             Getter/setter for C<$render>'s follow symlinks flag.
687              
688             =item $copy = $render->copy_perms
689              
690             =item $render->set_copy_perms($copy)
691              
692             Getter/setter for C<$render>'s copy perms flag.
693              
694             =item $rest = $render->restricted
695              
696             =item $render->set_restricted($rest)
697              
698             Getter/setter for C<$render>'s restricted flag.
699              
700             =item $def = $render->defaults()
701              
702             Getter for C<$render>'s defaults flag.
703              
704             =item $ign = $render->ignore_config()
705              
706             Getter for C<$render>'s ignore_config flag.
707              
708             =back
709              
710             =head1 AUTHOR
711              
712             Written by Samuel Young, Esamyoung12788@gmail.comE.
713              
714             This project's source can be found on its
715             L. Comments and pull
716             requests are welcome!
717              
718             =head1 COPYRIGHT
719              
720             Copyright (C) 2025 Samuel Young
721              
722             This program is free software: you can redistribute it and/or modify
723             it under the terms of the GNU General Public License as published by
724             the Free Software Foundation, either version 3 of the License, or
725             (at your option) any later version.
726              
727             =head1 SEE ALSO
728              
729             L
730              
731             =cut
732              
733             # vim: expandtab shiftwidth=4