File Coverage

blib/lib/App/Basis.pm
Criterion Covered Total %
statement 175 314 55.7
branch 65 162 40.1
condition 10 17 58.8
subroutine 27 46 58.7
pod 15 17 88.2
total 292 556 52.5


line stmt bran cond sub pod time code
1             # ABSTRACT: Simple way to create applications
2              
3              
4             package App::Basis ;
5             $App::Basis::VERSION = '1.2';
6 2     2   29902 use 5.014 ;
  2         5  
7 2     2   6 use warnings ;
  2         2  
  2         55  
8 2     2   6 use strict ;
  2         6  
  2         29  
9 2     2   1435 use Getopt::Long ;
  2         14174  
  2         6  
10 2     2   263 use Exporter ;
  2         3  
  2         50  
11 2     2   491 use File::HomeDir ;
  2         3654  
  2         76  
12 2     2   8 use Path::Tiny ;
  2         2  
  2         70  
13 2     2   1851 use IPC::Cmd qw(run run_forked) ;
  2         85561  
  2         123  
14 2     2   18 use List::Util qw(max) ;
  2         3  
  2         152  
15 2     2   9 use POSIX qw(strftime) ;
  2         2  
  2         10  
16 2     2   1796 use utf8::all ;
  2         71607  
  2         10  
17 2     2   2556 use Digest::MD5 qw(md5_base64) ;
  2         4  
  2         86  
18 2     2   861 use YAML::Tiny::Color ;
  2         22636  
  2         142  
19              
20 2     2   28 use vars qw( @EXPORT @ISA) ;
  2         3  
  2         11302  
21              
22             @ISA = qw(Exporter) ;
23              
24             # this is the list of things that will get imported into the loading packages
25             # namespace
26             @EXPORT = qw(
27             init_app
28             show_usage
29             msg_exit
30             get_program
31             debug set_debug
32             daemonise
33             execute_cmd run_cmd
34             set_log_file
35             fix_filename
36             set_test_mode
37             saymd
38             set_verbose
39             verbose
40             verbose_data
41             ) ;
42              
43             # ----------------------------------------------------------------------------
44              
45             my $PROGRAM = path($0)->basename ;
46             my $LOG_FILE = fix_filename("~/$PROGRAM.log") ;
47              
48             # these variables are held available throughout the life of the app
49             my $_app_simple_ctrlc_count = 0 ;
50             my $_app_simple_ctrlc_handler ;
51             my $_app_simple_help_text = 'Application has not defined help_text yet.' ;
52             my $_app_simple_help_options = '' ;
53             my $_app_simple_cleanup_func ;
54             my $_app_simple_help_cmdline = '' ;
55              
56             my %_app_simple_objects = () ;
57             my %_cmd_line_options = () ;
58              
59             # we may want to die rather than exiting, helps with testing!
60             my $_test_mode = 0 ;
61              
62              
63             # ----------------------------------------------------------------------------
64             # control how we output things to help with testing
65             sub _output
66             {
67 3     3   5 my ( $where, $msg ) = @_ ;
68              
69 3 50       11 if ( !$_test_mode ) {
70 0 0       0 if ( $where =~ /stderr/i ) {
71 0         0 say STDERR $msg ;
72             } else {
73 0         0 say $msg ;
74             }
75             }
76             }
77              
78             # ----------------------------------------------------------------------------
79              
80              
81             sub set_log_file
82             {
83 1     1 1 5 my ($file) = @_ ;
84 1         2 $LOG_FILE = $file ;
85             }
86              
87             # ----------------------------------------------------------------------------
88              
89              
90             sub debug
91             {
92 0     0 1 0 my ( $level, @debug ) = @_ ;
93              
94             # we may want to undef the debug object, so no debug comes out
95              
96 0 0       0 if ( exists $_app_simple_objects{logger} ) {
97              
98             # run the coderef for the logger
99             $_app_simple_objects{logger}->( $level, @debug )
100 0 0       0 if ( defined $_app_simple_objects{logger} ) ;
101             } else {
102 0         0 path($LOG_FILE)
103             ->append_utf8( strftime( '%Y-%m-%d %H:%M:%S', gmtime( time() ) )
104             . " [$level] "
105             . join( ' ', @debug )
106             . "\n" ) ;
107             }
108             }
109              
110             # ----------------------------------------------------------------------------
111              
112              
113             sub set_debug
114             {
115 0     0 1 0 my $func = shift ;
116 0 0 0     0 if ( !$func || ref($func) ne "CODE" ) {
117 0         0 warn "set_debug function expects a CODE, got a " . ref($func) ;
118             } else {
119 0         0 $_app_simple_objects{logger} = $func ;
120             }
121             }
122              
123             # -----------------------------------------------------------------------------
124             my $verbose = 1 ;
125              
126              
127             sub set_verbose
128             {
129 6     6 1 8 $verbose = shift ;
130             }
131              
132              
133             sub verbose
134             {
135 0     0 1 0 my ($msg) = @_ ;
136 0 0       0 say STDERR $msg if ($verbose) ;
137             }
138              
139              
140             sub verbose_data
141             {
142 0 0   0 0 0 if ( @_ % 2 ) {
143 0 0       0 say STDERR Dump(@_) if ($verbose) ;
144              
145             } else {
146 0         0 my ($data) = @_ ;
147 0 0       0 say STDERR Dump($data) if ($verbose) ;
148             }
149             }
150              
151             # ----------------------------------------------------------------------------
152             # check that the option structure does not have repeated things in it
153             # returns string of any issue
154              
155             sub _validate_options
156             {
157 12     12   14 my ($options) = @_ ;
158 12         14 my %seen ;
159 12         14 my $result = "" ;
160              
161 12         8 foreach my $opt ( keys %{$options} ) {
  12         30  
162             # options are long|short=, or thing=, or flags long|sort, or long
163 28         18 my ( $long, $short ) ;
164 28 50       161 if ( $opt =~ /^(.*?)\|(.*?)=/ ) {
    100          
    100          
    50          
    0          
165 0         0 $long = $1 ;
166 0         0 $short = $2 ;
167 0 0       0 if ( $seen{$long} ) {
    0          
168 0         0 $result
169             = "Long option '$long' has already been used. option line '$opt' is at fault"
170             ;
171 0         0 last ;
172             } elsif ( $seen{$short} ) {
173 0         0 $result
174             = "Short option '$short' has already been used. option line '$opt' is at fault"
175             ;
176 0         0 last ;
177             }
178 0         0 $seen{$short} = 1 ;
179 0         0 $seen{$long} = 1 ;
180             } elsif ( $opt =~ /^(.*?)\|(.*?)$/ ) {
181 13         29 $long = $1 ;
182 13         18 $short = $2 ;
183 13 100       33 if ( $seen{$long} ) {
184 1         4 $result
185             = "Long flag '$long' has already been used. option line '$opt' is at fault"
186             ;
187 1         3 last ;
188             }
189              
190 12 50       21 if ( $seen{$short} ) {
191 0         0 $result
192             = "short flag '$short' has already been used. option line '$opt' is at fault"
193             ;
194 0         0 last ;
195             }
196 12         20 $seen{$short} = 1 ;
197 12         24 $seen{$long} = 1 ;
198             } elsif ( $opt =~ /^(.*?)=/ ) {
199 4         6 $long = $1 ;
200 4 50       8 if ( $seen{$long} ) {
201 0         0 $result
202             = "Option '$long' has already been used. option line '$opt' is at fault"
203             ;
204              
205 0         0 last ;
206             }
207 4         8 $seen{$long} = 1 ;
208             } elsif ( $opt =~ /^(.*?)$/ ) {
209 11         20 $long = $1 ;
210 11 50       30 if ( $seen{$long} ) {
211 0         0 $result
212             = "flag '$long' has already been used. option line '$opt' is at fault"
213             ;
214 0         0 last ;
215             }
216 11         26 $seen{$long} = 1 ;
217             } elsif ( $opt =~ /^(.*?)\|(.*?)\|(.*?)\$/ ) {
218 0         0 $long = $1 ;
219 0         0 $short = $2 ;
220 0         0 my $extra = $3 ;
221 0 0       0 if ( $seen{$long} ) {
222 0         0 $result
223             = "flag '$long' has already been used. option line '$opt' is at fault"
224             ;
225 0         0 last ;
226             }
227 0 0       0 if ( $seen{$short} ) {
228 0         0 $result
229             = "flag '$short' has already been used. option line '$opt' is at fault"
230             ;
231 0         0 last ;
232             }
233 0 0       0 if ( $seen{$extra} ) {
234 0         0 $result
235             = "flag '$extra' has already been used. option line '$opt' is at fault"
236             ;
237 0         0 last ;
238             }
239 0         0 $seen{$long} = 1 ;
240 0         0 $seen{$short} = 1 ;
241 0         0 $seen{$extra} = 1 ;
242             }
243             }
244 12         32 return $result ;
245             }
246              
247             # ----------------------------------------------------------------------------
248              
249              
250             sub init_app
251             {
252 12 50   12 1 3672 my %args
253             = @_ % 2
254             ? die("Odd number of values passed where even is expected.")
255             : @_ ;
256 12         34 my @options ;
257 12         13 my $has_required = 0 ;
258 12         11 my %full_options ;
259              
260 12 50       30 if ( $args{log_file} ) {
261 0         0 $LOG_FILE = fix_filename( $args{log_file} ) ;
262             }
263              
264 12 50       24 if ( $args{debug} ) {
265 0         0 set_debug( $args{debug} ) ;
266             }
267              
268             # get program description
269 12 50       37 $_app_simple_help_text = $args{help_text} if ( $args{help_text} ) ;
270             $_app_simple_help_cmdline = $args{help_cmdline}
271 12 50       20 if ( $args{help_cmdline} ) ;
272              
273 12 50       34 die "options must be a hashref" if ( ref( $args{options} ) ne 'HASH' ) ;
274              
275 12         21 $args{options}->{'help|h|?'} = 'Show help' ;
276              
277 12         10 my @keys = sort keys %{ $args{options} } ;
  12         67  
278 12         29 my %dnames = _desc_names(@keys) ;
279 12         55 my $max_desc_len = max( map length, values %dnames ) + 1 ;
280 12         24 my $help_fmt = " %-${max_desc_len}s %s\n" ;
281              
282             # add help text for 'help' first.
283 12         52 $_app_simple_help_options .= sprintf $help_fmt, $dnames{'help|h|?'},
284             'Show help' ;
285              
286             #
287 12         24 my $msg = _validate_options( $args{options} ) ;
288 12 100       23 if ($msg) {
289 1         10 die "$msg" ;
290             }
291              
292             # get options and their descriptions
293 11         15 foreach my $o (@keys) {
294              
295             # save the option
296 25         22 push @options, $o ;
297              
298 25         23 my $name = $o ;
299              
300             # we want the long version of the name if its provided
301 25         75 $name =~ s/.*?(\w+).*/$1/ ;
302              
303             # remove any type data
304 25         24 $name =~ s/=(.*)// ;
305              
306 25 100       44 if ( ref( $args{options}->{$o} ) eq 'HASH' ) {
307             die "parameterised option '$name' require a desc option"
308 15 100       42 if ( !$args{options}->{$o}->{desc} ) ;
309 14         19 $full_options{$name} = $args{options}->{$o} ;
310 14 100       30 $has_required++ if ( $full_options{$name}->{required} ) ;
311             } else {
312             $full_options{$name} = {
313 10         22 desc => $args{options}->{$o},
314              
315             # possible options that can be passed
316             # depends => '',
317             # default => '',
318             # required => 0,
319             # validate => sub {}
320             } ;
321             }
322              
323             # save the option string too
324 24         28 $full_options{$name}->{options} = $o ;
325              
326             # build the entry for the help text
327 24         23 my $desc = $full_options{$name}->{desc} ;
328 24 100       46 if ( $name ne 'help' ) {
329 14         15 my $desc = $full_options{$name}->{desc} ;
330              
331             # show the right way to use the options
332 14         15 my $dname = $dnames{$o} ;
333 14 100       22 $dname .= '*' if ( $full_options{$name}->{required} ) ;
334              
335             $desc .= " [DEFAULT: $full_options{$name}->{default}]"
336 14 100       27 if ( $full_options{$name}->{default} ) ;
337 14         45 $_app_simple_help_options .= sprintf $help_fmt, $dname, $desc ;
338             }
339             }
340              
341             # show required options
342 10 100       14 if ($has_required) {
343 8 50       20 $_app_simple_help_options
344             .= "* required option" . ( $has_required > 1 ? 's' : '' ) . "\n" ;
345             }
346              
347             # catch control-c, user provided or our default
348             $_app_simple_ctrlc_handler
349 10 50       19 = $args{ctrl_c} ? $args{ctrl_c} : \&_app_simple_ctrlc_func ;
350 10         57 $SIG{'INT'} = $_app_simple_ctrlc_handler ;
351              
352             # get an cleanup function handler
353 10 50       16 $_app_simple_cleanup_func = $args{cleanup} if ( $args{cleanup} ) ;
354              
355             # check command line args
356 10         35 GetOptions( \%_cmd_line_options, @options ) ;
357              
358             # help is a built in
359 10 50       1938 show_usage() if ( $_cmd_line_options{help} ) ;
360              
361             # now if we have the extended version we can do some checking
362 10         29 foreach my $name ( sort keys %full_options ) {
363             warn "Missing desc field for $name"
364 20 50       35 if ( !$full_options{$name}->{desc} ) ;
365 20 100       25 if ( $full_options{$name}->{required} ) {
366             show_usage( "Required option '$name' is missing", 1 )
367             if (
368             !( $_cmd_line_options{$name}
369             || $full_options{$name}->{default}
370             )
371 8 50 66     25 ) ;
372             }
373 19 100       24 if ( $full_options{$name}->{depends} ) {
374 6 100       15 if ( !$_cmd_line_options{ $full_options{$name}->{depends} } ) {
375 1         11 show_usage(
376             "Option '$name' depends on option '$full_options{$name}->{depends}' but it is missing",
377             1
378             ) ;
379             }
380             }
381              
382             # set a default if there is no value
383 18 100       21 if ( $full_options{$name}->{default} ) {
384             $_cmd_line_options{$name} = $full_options{$name}->{default}
385 4 50       7 if ( !$_cmd_line_options{$name} ) ;
386             }
387              
388             # call the validation routine if we have one
389 18 100 66     42 if ( $_cmd_line_options{$name} && $full_options{$name}->{validate} ) {
390             die "need to pass a coderef to validate for option '$name'"
391 3 50       8 if ( !ref( $full_options{$name}->{validate} ) eq 'CODE' ) ;
392             die
393             "Option '$name' has validate and should either also have a default or be required"
394             if (
395             !( $full_options{$name}->{required}
396             || $full_options{$name}->{default}
397             )
398 3 0 33     7 ) ;
399 3         4 my $coderef = $full_options{$name}->{validate} ;
400 3         15 my $result = $coderef->( $_cmd_line_options{$name} ) ;
401 2 100       13 show_usage("Option '$name' does not pass validation")
402             if ( !$result ) ;
403             }
404             }
405              
406             # auto set verbose if it has been used
407 6         19 set_verbose( $_cmd_line_options{verbose} ) ;
408              
409 6         35 return %_cmd_line_options ;
410             }
411              
412             # ----------------------------------------------------------------------------
413              
414              
415             sub get_program
416             {
417 1     1 1 519 return $PROGRAM ;
418             }
419              
420             # ----------------------------------------------------------------------------
421              
422              
423             sub get_options
424             {
425 0     0 1 0 return %_cmd_line_options ;
426             }
427              
428             # ----------------------------------------------------------------------------
429             # handle the ctrl-c presses
430              
431             sub _app_simple_ctrlc_func
432             {
433              
434             # exit if we are already in ctrlC
435 0 0   0   0 exit(2) if ( $_app_simple_ctrlc_count++ ) ;
436 0         0 _output( 'STDERR', "\nCaught Ctrl-C. press again to exit immediately" ) ;
437              
438             # re-init the handler
439 0         0 $SIG{'INT'} = $_app_simple_ctrlc_handler ;
440             }
441              
442             # ----------------------------------------------------------------------------
443              
444             # to help with testing we may want to die, which can be caught rather than
445             # exiting, so lets find out
446              
447             sub _exit_or_die
448             {
449 3   100 3   11 my $state = shift || 1 ;
450              
451 3 50       8 if ($_test_mode) {
452 3         25 STDERR->flush() ;
453 3         8 STDOUT->flush() ;
454 3         32 die "exit state $state" ;
455             }
456 0         0 exit($state) ;
457             }
458              
459             # ----------------------------------------------------------------------------
460              
461              
462             sub show_usage
463             {
464 3     3 1 5 my ( $msg, $state ) = @_ ;
465              
466 3         13 my $help = qq{
467             Syntax: $PROGRAM [options] $_app_simple_help_cmdline
468              
469             About: $_app_simple_help_text
470              
471             [options]
472             $_app_simple_help_options} ;
473 3 50       6 if ($msg) {
474              
475             # if we have an error message it MUST go to STDERR
476             # to make sure that any program the output is piped to
477             # does not get the message to process
478 3         22 _output( 'STDERR', "$help\nError: $msg\n" ) ;
479             } else {
480 0         0 _output( 'STDOUT', $help ) ;
481             }
482              
483 3         8 _exit_or_die($state) ;
484             }
485              
486             # ----------------------------------------------------------------------------
487              
488              
489             sub msg_exit
490             {
491 0     0 1 0 my ( $msg, $state ) = @_ ;
492              
493 0 0       0 _output( 'STDERR', $msg ) if ($msg) ;
494 0         0 _exit_or_die($state) ;
495             }
496              
497             # -----------------------------------------------------------------------------
498              
499              
500             sub daemonise
501             {
502 0     0 1 0 my $rootdir = shift ;
503              
504 0 0       0 if ($rootdir) {
505 0 0       0 chroot($rootdir)
506             or die
507             "Could not chroot to $rootdir, only the root user can do this." ;
508             }
509              
510             # fork once and let the parent exit
511 0         0 my $pid = fork() ;
512              
513             # exit if $pid ;
514             # parent to return 0, as it is logical
515 0 0       0 if ($pid) {
516 0         0 return 0 ;
517             }
518 0 0       0 die "Couldn't fork: $!" unless defined $pid ;
519              
520             # disassociate from controlling terminal, leave the
521             # process group behind
522              
523 0 0       0 POSIX::setsid() or die "Can't start a new session" ;
524              
525             # show that we have started a daemon process
526 0         0 return 1 ;
527             }
528              
529             # ----------------------------------------------------------------------------
530              
531              
532             sub execute_cmd
533             {
534 0     0 1 0 my %args = @_ ;
535              
536 0 0       0 my $command = $args{command} or die "command required" ;
537             # pass everything thought encode incase there is utf8 there
538 0         0 utf8::encode($command) ;
539              
540 0         0 my $r = IPC::Cmd::run_forked( $command, \%args ) ;
541              
542 0         0 return $r ;
543             }
544              
545             # ----------------------------------------------------------------------------
546              
547              
548             sub run_cmd
549             {
550 2     2 1 389 my ( $cmd, $timeout ) = @_ ;
551              
552             # use our local version of path so that it can pass taint checks
553 2         18 local $ENV{PATH} = $ENV{PATH} ;
554              
555             # pass everything thought encode incase there is utf8 there
556 2         8 utf8::encode($cmd) ;
557              
558 2         7 my %data = ( command => $cmd ) ;
559 2 50       8 $data{timeout} = $timeout if ($timeout) ;
560 2         14 my ( $ret, $err, $full_buff, $stdout_buff, $stderr_buff ) = run(%data) ;
561              
562 2         16279 my $stdout = join( "\n", @{$stdout_buff} ) ;
  2         8  
563 2         3 my $stderr = join( "\n", @{$stderr_buff} ) ;
  2         5  
564              
565 2         31 return ( !$ret, $stdout, $stderr ) ;
566             }
567              
568             # -----------------------------------------------------------------------------
569              
570              
571             sub fix_filename
572             {
573 9     9 1 1101 my $file = shift ;
574 9 50       47 return if ( !$file ) ;
575              
576 9         59 my $home = File::HomeDir->my_home ;
577 9         326 $file =~ s/^~/$home/ ;
578 9 100       30 if ( $file =~ m|^\.\./| ) {
579 1         11 my $parent = path( Path::Tiny->cwd )->dirname ;
580 1         108 $file =~ s|^(\.{2})/|$parent/| ;
581             }
582 9 100 100     64 if ( $file =~ m|^\./| || $file eq '.' ) {
583 2         17 my $cwd = Path::Tiny->cwd ;
584 2         74 $file =~ s|^(\.)/?|$cwd| ;
585             }
586              
587             # replace multiple separators
588 9         29 $file =~ s|//|/|g ;
589              
590             # get the OS specific path
591 9         30 return path($file)->canonpath ;
592             }
593              
594             # ----------------------------------------------------------------------------
595             # Returns a hash containing a formatted name for each option. For example:
596             # ( 'help|h|?' ) -> { 'help|h|?' => '-h, -?, --help' }
597             sub _desc_names
598             {
599 12     12   17 my %descs ;
600 12         19 foreach my $o (@_) {
601 29         31 $_ = $o ; # Keep a copy of key in $o.
602 29         53 s/=.*$// ;
603              
604             # Sort by length so single letter options are shown first.
605 29         89 my @parts = sort { length $a <=> length $b } split /\|/ ;
  38         62  
606              
607             # Single chars get - prefix, names get -- prefix.
608 29 100       46 my $s = join ", ", map { ( length > 1 ? '--' : '-' ) . $_ } @parts ;
  55         150  
609              
610 29         71 $descs{$o} = $s ;
611             }
612 12         48 return %descs ;
613             }
614              
615             # ----------------------------------------------------------------------------
616             # special function to help us test this module, as it flags that we can die
617             # rather than exiting when doing some operations
618             # also test mode will not output to STDERR/STDOUT
619              
620             sub set_test_mode
621             {
622 1     1 0 12 $_test_mode = shift ;
623             }
624              
625             # ----------------------------------------------------------------------------
626              
627              
628              
629              
630             # saymd function taken and modied from
631             # echomd -- An md like conversion tool for shell terminals
632             # https://raw.githubusercontent.com/WebReflection/echomd/master/perl/echomd
633             # some mod's of my own
634              
635             #
636             # Fully inspired by the work of John Gruber
637             #
638             #
639             # -----------------------------------------------------------------------------
640             # The MIT License (MIT)
641             # Copyright (c) 2016 Andrea Giammarchi - @WebReflection
642             #
643             # Permission is hereby granted, free of charge, to any person obtaining a
644             # copy of this software and associated documentation files (the "Software"),
645             # to deal in the Software without restriction, including without limitation
646             # the rights to use, copy, modify, merge, publish, distribute, sublicense,
647             # and/or sell copies of the Software, and to permit persons to whom
648             # the Software is furnished to do so, subject to the following conditions:
649             #
650             # The above copyright notice and this permission notice shall be included
651             # in all copies or substantial portions of the Software.
652             #
653             # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
654             # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
655             # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
656             # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
657             # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
658             # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
659             # THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
660             # -----------------------------------------------------------------------------
661              
662             # for *bold* _underline_ ~strike~ (strike on Linux only)
663             # it works with both double **__~~ or just single *_~
664             sub _bold_underline_strike
665             {
666 0     0     my ($txt) = @_ ;
667 0           $txt =~ s/(\*{1,2})(?=\S)(.+?)(?<=\S)\1/\x1B[1m$2\x1B[22m/gs ;
668 0           $txt =~ s/(\_{1,2})(?=\S)(.+?)(?<=\S)\1/\x1B[4m$2\x1B[24m/gs ;
669 0           $txt =~ s/(\~{1,2})(?=\S)(.+?)(?<=\S)\1/\x1B[9m$2\x1B[29m/gs ;
670 0           return $txt ;
671             }
672              
673             # for #color(text) or bg#bgcolor(text)
674             # virtually compatible with #RGBA(text)
675             # or for background via bg#RGBA(text)
676             sub _color
677             {
678 0     0     my ($txt) = @_ ;
679 0           $txt =~ s{(bg)?#([a-zA-Z0-9]{3,8})\((.+?)\)(?!\))}
680 0           {_get_color($1,$2,$3)}egs ;
681 0           return $txt ;
682             }
683              
684             # for very important # Headers
685             # and for less important ## One
686             sub _header
687             {
688 0     0     my ($txt) = @_ ;
689 0           $txt =~ s{^(\#{1,6})[ \t]+(.+?)[ \t]*\#*([\r\n]+|$)}
690 0           {_get_header($1,$2).$3}egm ;
691 0           return $txt ;
692             }
693              
694             # for horizontal lines
695             # --- or - - - or ___ or * * *
696             sub _horizontal
697             {
698 0     0     my ($txt) = @_ ;
699 0           my $line = "─" x 72 ;
700 0           $txt =~ s{^[ ]{0,2}([ ]?[\*_-][ ]?){3,}[ \t]*$}
701             {\x1B[1m$line\x1B[22m}gm ;
702 0           return $txt ;
703             }
704              
705             # for lists such:
706             # * list 1
707             # etc, etc
708             # * list 2
709             # * list 3
710             sub _list
711             {
712 0     0     my ($txt) = @_ ;
713 0           $txt =~ s/^([ \t]{2,})[*+-]([ \t]{1,})/$1•$2/gm ;
714 0           return $txt ;
715             }
716              
717             # for quoted text such:
718             # > this is quote
719             # > this is the rest of the quote
720             sub _quote
721             {
722 0     0     my ($txt) = @_ ;
723 0           $txt =~ s/^[ \t]*>([ \t]?)/\x1B[7m$1\x1B[27m$1/gm ;
724 0           return $txt ;
725             }
726              
727             # HELPERS
728              
729             # used to grab colors by name
730             sub _get_color
731             {
732 0     0     my $bg = $1 ;
733 0           my $rgb = $2 ;
734 0           my $out = "" ;
735             # one day, when it won't show experimental warnings
736             # given($rgb){
737             # when("black") { $out = "\x1B[30m" }
738             # when("red") { $out = "\x1B[31m" }
739             # when("green") { $out = "\x1B[32m" }
740             # when("blue") { $out = "\x1B[34m" }
741             # when("magenta") { $out = "\x1B[35m" }
742             # when("cyan") { $out = "\x1B[36m" }
743             # when("white") { $out = "\x1B[37m" }
744             # when("yellow") { $out = "\x1B[39m" }
745             # when("grey") { $out = "\x1B[90m" }
746             # }
747 0 0         if ( $rgb eq "black" ) {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
748 0           $out = "\x1B[30m" ;
749             } elsif ( $rgb eq "red" ) {
750 0           $out = "\x1B[31m" ;
751             } elsif ( $rgb eq "green" ) {
752 0           $out = "\x1B[32m" ;
753             } elsif ( $rgb eq "blue" ) {
754 0           $out = "\x1B[34m" ;
755             } elsif ( $rgb eq "magenta" ) {
756 0           $out = "\x1B[35m" ;
757             } elsif ( $rgb eq "cyan" ) {
758 0           $out = "\x1B[36m" ;
759             } elsif ( $rgb eq "white" ) {
760 0           $out = "\x1B[37m" ;
761             } elsif ( $rgb eq "yellow" ) {
762 0           $out = "\x1B[39m" ;
763             } elsif ( $rgb eq "grey" ) {
764 0           $out = "\x1B[90m" ;
765             }
766 0 0         $out .= ( $out eq "" ) ? $3 : "$3\x1B[39m" ;
767 0 0         return ( !defined $bg ) ? $out : "\x1B[7m$out\x1B[27m" ;
768             }
769              
770             sub _get_header
771             {
772 0     0     my ( $hash, $txt ) = @_ ;
773 0 0         if ( length($hash) eq 1 ) {
774 0           $txt = "\x1B[1m$txt\x1B[22m" ;
775             }
776 0           return "\x1B[7m $txt \x1B[27m" ;
777             }
778              
779             # used to place parsed code back
780             sub _get_source
781             {
782 0     0     my ($hash) = @_ ;
783 0           my %code = %{ $_[1] } ;
  0            
784 0           for my $source ( keys %code ) {
785 0 0         if ( $code{$source} eq $hash ) {
786 0           return $source ;
787             }
788             }
789             }
790              
791             # main transformer
792             # takes care of code blocks too
793             # without modifying their content
794             # inline `code blocks` as well as
795             # ```
796             # multiline code blocks
797             # ```
798             sub saymd
799             {
800 0     0 1   my ($txt) = @_ ;
801 0           my %code ;
802             # preserve code blocks
803 0           $txt =~ s{(`{2,})(.+?)(?
804 0           {$1.($code{$2}=md5_base64($2)).$1}egs ;
805             # preserve inline blocks too
806 0           $txt =~ s{(`)(.+?)\1}{$1.($code{$2}=md5_base64($2)).$1}egm ;
  0            
807             # converter everything else
808 0           $txt = _horizontal($txt) ;
809 0           $txt = _header($txt) ;
810 0           $txt = _bold_underline_strike($txt) ;
811 0           $txt = _list($txt) ;
812 0           $txt = _quote($txt) ;
813 0           $txt = _color($txt) ;
814             # put back inline blocks
815 0           $txt =~ s{(`)(.+?)\1}{$1._get_source($2,\%code).$1}egm ;
  0            
816             # put back code blocks too
817 0           $txt =~ s{(`{3})(.+?)(?
818 0           {$1._get_source($2,\%code).$1}egs ;
819 0           say $txt;
820             }
821              
822             # ----------------------------------------------------------------------------
823             # make sure we do any cleanup required
824              
825             END {
826              
827             # call any user supplied cleanup
828 2 50   2   366 if ($_app_simple_cleanup_func) {
829 0         0 $_app_simple_cleanup_func->() ;
830 0         0 $_app_simple_cleanup_func = undef ;
831             }
832             }
833              
834              
835             # ----------------------------------------------------------------------------
836              
837             1 ;
838              
839             __END__