File Coverage

lib/ChordPro/Utils.pm
Criterion Covered Total %
statement 232 401 57.8
branch 67 206 32.5
condition 15 49 30.6
subroutine 36 45 80.0
pod 0 28 0.0
total 350 729 48.0


line stmt bran cond sub pod time code
1             #! perl
2              
3             package ChordPro::Utils;
4              
5 90     90   1298 use v5.26;
  90         428  
6 90     90   561 use utf8;
  90         174  
  90         605  
7 90     90   3180 use Carp;
  90         230  
  90         8151  
8 90     90   669 use feature qw( signatures );
  90         299  
  90         14256  
9 90     90   701 no warnings "experimental::signatures";
  90         252  
  90         5199  
10 90     90   572 use Ref::Util qw( is_arrayref is_hashref );
  90         224  
  90         6933  
11              
12 90     90   582 use Exporter 'import';
  90         226  
  90         7644  
13             our @EXPORT;
14             our @EXPORT_OK;
15              
16 90     90   1613 use ChordPro::Files;
  90         675  
  90         16374  
17              
18             ################ Filenames ################
19              
20 90     90   777 use File::Glob ( ":bsd_glob" );
  90         302  
  90         35458  
21              
22             # Derived from Path::ExpandTilde.
23              
24 90 50       14693 use constant BSD_GLOB_FLAGS => GLOB_NOCHECK | GLOB_QUOTE | GLOB_TILDE | GLOB_ERR
25             # add GLOB_NOCASE as in File::Glob
26 90     90   823 | ($^O =~ m/\A(?:MSWin32|VMS|os2|dos|riscos)\z/ ? GLOB_NOCASE : 0);
  90         211  
27              
28             # File::Glob did not try %USERPROFILE% (set in Windows NT derivatives) for ~ before 5.16
29 90   33 90   712 use constant WINDOWS_USERPROFILE => is_msw && $] < 5.016;
  90         240  
  90         621  
30              
31 218     218 0 590 sub expand_tilde ( $dir ) {
  218         599  
  218         437  
32              
33 218 50       975 return undef unless defined $dir;
34 218 50       2222 return fn_canonpath($dir) unless $dir =~ m/^~/;
35              
36             # Parse path into segments.
37 0         0 my ( $volume, $directories, $file ) = fn_splitpath( $dir, 1 );
38 0         0 my @parts = fn_splitdir($directories);
39 0         0 my $first = shift( @parts );
40 0 0       0 return fn_canonpath($dir) unless defined $first;
41              
42             # Expand first segment.
43 0         0 my $expanded;
44 0         0 if ( WINDOWS_USERPROFILE and $first eq '~' ) {
45             $expanded = $ENV{HOME} || $ENV{USERPROFILE};
46             }
47             else {
48 0         0 ( my $pattern = $first ) =~ s/([\\*?{[])/\\$1/g;
49 0         0 ($expanded) = bsd_glob( $pattern, BSD_GLOB_FLAGS );
50 0 0       0 croak( "Failed to expand $first: $!") if GLOB_ERROR;
51             }
52 0 0 0     0 return fn_canonpath($dir)
53             if !defined $expanded or $expanded eq $first;
54              
55             # Replace first segment with new path.
56 0         0 ( $volume, $directories ) = fn_splitpath( $expanded, 1 );
57 0         0 $directories = fn_catdir( $directories, @parts );
58 0         0 return fn_catpath($volume, $directories, $file);
59             }
60              
61             push( @EXPORT, 'expand_tilde' );
62              
63 0     0 0 0 sub sys ( @cmd ) {
  0         0  
  0         0  
64 0 0       0 warn("+ @cmd\n") if $::options->{trace};
65             # Use outer defined subroutine, depends on Wx or not.
66 0         0 my $res = ::sys(@cmd);
67 0 0       0 warn( sprintf("=%02x=> @cmd", $res), "\n" ) if $res;
68 0         0 return $res;
69             }
70              
71             push( @EXPORT, 'sys' );
72              
73             ################ (Pre)Processing ################
74              
75 214     214 0 497 sub make_preprocessor ( $prp ) {
  214         457  
  214         395  
76 214 50       970 return unless $prp;
77              
78 214         492 my $prep;
79 214         492 foreach my $linetype ( keys %{ $prp } ) {
  214         973  
80 642         1557 my @targets;
81 642         4384 my $code = "";
82 642         3009 foreach ( @{ $prp->{$linetype} } ) {
  642         2713  
83 0   0     0 my $flags = $_->{flags} // "g";
84             $code .= "m\0" . $_->{select} . "\0 && "
85 0 0       0 if $_->{select};
86 0 0       0 if ( $_->{pattern} ) {
87             $code .= "s\0" . $_->{pattern} . "\0"
88 0         0 . $_->{replace} . "\0$flags;\n";
89             }
90             else {
91             $code .= "s\0" . quotemeta($_->{target}) . "\0"
92 0         0 . quotemeta($_->{replace}) . "\0$flags;\n";
93             }
94             }
95 642 50       2469 if ( $code ) {
96 0         0 my $t = "sub { for (\$_[0]) {\n" . $code . "}}";
97 0         0 $prep->{$linetype} = eval $t;
98 0 0       0 die( "CODE : $t\n$@" ) if $@;
99             }
100             }
101 214         1442 $prep;
102             }
103              
104             push( @EXPORT, 'make_preprocessor' );
105              
106             ################ Utilities ################
107              
108             # Split (pseudo) command line into key/value pairs.
109              
110             # Similar to JavaScript, we do not distinguish single- and double
111             # quoted strings.
112             # \\ \' \" yield \ ' " (JS)
113             # \n yields a newline (convenience)
114             # Everything else yields the character following the backslash (JS)
115              
116             my %esc = ( n => "\n", '\\' => '\\', '"' => '"', "'" => "'" );
117              
118 77     77 0 274947 sub parse_kv ( $line, $kdef = undef ) {
  77         210  
  77         189  
  77         184  
119              
120 77         174 my @words;
121 77 100       278 if ( is_arrayref($line) ) {
122 8         26 @words = @$line;
123             }
124             else {
125             # Strip.
126 69         207 $line =~ s/^\s+//;
127 69         202 $line =~ s/\s+$//;
128              
129             # If it doesn't look like key=value, use the default key (if any).
130 69 100 66     407 if ( $kdef && $line !~ /^\w+=(?:['"]|[-+]?\d|\w)/ ) {
131 30         193 return { $kdef => $line };
132             }
133              
134 90     90   55512 use Text::ParseWords qw(quotewords);
  90         180967  
  90         73408  
135 39         268 @words = quotewords( '\s+', 1, $line );
136             }
137              
138 47         2163 my $res = {};
139 47         144 foreach ( @words ) {
140              
141             # Quoted values.
142 41 100       245 if ( /^(.*?)=(["'])(.*)\2$/ ) {
    100          
    100          
143 10         29 my ( $k, $v ) = ( $1, $3 );
144 10   0     38 $res->{$k} = $v =~ s;\\(.);$esc{$1}//$1;segr;
  0         0  
145             }
146              
147             # Unquoted values.
148             elsif ( /^(.*?)=(.+)$/ ) {
149 25         105 $res->{$1} = $2;
150             }
151              
152             # Negated keywords.
153             elsif ( /^no[-_]?(.+)/ ) {
154 2         8 $res->{$1} = 0;
155             }
156              
157             # Standalone keywords.
158             else {
159 4         12 $res->{$_}++;
160             }
161             }
162              
163 47         255 return $res;
164             }
165              
166             push( @EXPORT, 'parse_kv' );
167              
168             # Split (pseudo) command lines into key/value pairs.
169              
170             #### LEGACY -- WILL BE REMOVED ####
171              
172 2     2 0 4 sub parse_kvm ( @lines ) {
  2         6  
  2         4  
173              
174 2 50       13 if ( is_macos() ) {
175             # MacOS has the nasty habit to smartify quotes.
176 0         0 @lines = map { s/“/"/g; s/”/"/g; s/‘/'/g; s/’/'/gr;} @lines;
  0         0  
  0         0  
  0         0  
  0         0  
177             }
178              
179 90     90   850 use Text::ParseWords qw(quotewords);
  90         443  
  90         90714  
180 2         10 my @words = quotewords( '\s+', 1, @lines );
181 2         183 parse_kv( \@words );
182             }
183              
184             push( @EXPORT, 'parse_kvm' );
185              
186             # Odd/even.
187              
188 150     150 0 448 sub is_odd( $arg ) {
  150         345  
  150         297  
189 150         1006 ( $arg % 2 ) != 0;
190             }
191 1     1 0 4 sub is_even( $arg ) {
  1         3  
  1         2  
192 1         8 ( $arg % 2 ) == 0;
193             }
194              
195             push( @EXPORT, qw( is_odd is_even ) );
196              
197             # Map true/false etc to true / false.
198              
199 640     640 0 1188 sub is_true ( $arg ) {
  640         1145  
  640         913  
200 640 100 66     2792 return 0 if !defined($arg) || $arg eq '';
201 632 100       3110 return 0 if $arg =~ /^(false|null|no|none|off|\s+|0)$/i;
202 583         2220 return !!$arg;
203             }
204              
205             push( @EXPORT, 'is_true' );
206              
207             # Stricter form of true.
208 9     9 0 17 sub is_ttrue ( $arg ) {
  9         21  
  9         17  
209 9 50       30 return 0 if !defined($arg);
210 9         69 $arg =~ /^(on|true|1)$/i;
211             }
212              
213             push( @EXPORT, 'is_ttrue' );
214              
215             # Fix apos -> quote.
216              
217 1137     1137 0 1541 sub fq ( $arg ) {
  1137         1775  
  1137         1404  
218 1137         2164 $arg =~ s/'/\x{2019}/g;
219 1137         3733 $arg;
220             }
221              
222             push( @EXPORT, 'fq' );
223              
224             # Quote a string if needed unless forced.
225              
226 11     11 0 24 sub qquote ( $arg, $force = 0 ) {
  11         25  
  11         22  
  11         20  
227 11         27 for ( $arg ) {
228 11         38 s/([\\\"])/\\$1/g;
229 11         35 s/([[:^print:]])/sprintf("\\u%04x", ord($1))/ge;
  0         0  
230 11 100 100     81 return $_ unless /[\\\s]/ || $force;
231 9         66 return qq("$_");
232             }
233             }
234              
235             push( @EXPORT, 'qquote' );
236              
237             # Safely print values.
238              
239 90     90   880 use Scalar::Util qw(looks_like_number);
  90         184  
  90         368254  
240              
241             # We want overload:
242             # sub pv( $val )
243             # sub pv( $label, $val )
244              
245             sub pv {
246 0     0 0 0 my $val = pop;
247 0   0     0 my $label = pop // "";
248              
249 0         0 my $suppressundef;
250 0 0       0 if ( $label =~ /\?$/ ) {
251 0         0 $suppressundef++;
252 0         0 $label = $';
253             }
254 0 0       0 if ( defined $val ) {
255 0 0       0 if ( looks_like_number($val) ) {
256 0         0 $val = sprintf("%.3f", $val);
257 0         0 $val =~ s/0+$//;
258 0         0 $val =~ s/\.$//;
259             }
260             else {
261 0         0 $val = qquote( $val, 1 );
262             }
263             }
264             else {
265 0 0       0 return "" if $suppressundef;
266 0         0 $val = ""
267             }
268 0 0       0 defined wantarray ? $label.$val : warn($label.$val."\n");
269             }
270              
271             push( @EXPORT, 'pv' );
272              
273             # Processing JSON.
274              
275 133     133 0 423 sub json_load( $json, $source = "" ) {
  133         332  
  133         313  
  133         276  
276 133         556 my $info = json_parser();
277 133 50       651 if ( $info->{parser} eq "JSON::Relaxed" ) {
278 133         1061 state $pp = JSON::Relaxed::Parser->new( croak_on_error => 0,
279             strict => 0,
280             prp => 1 );
281 133         3849 my $data = $pp->decode($json."\n");
282 133 50       1158 return $data unless $pp->is_error;
283 0 0       0 $source .= ": " if $source;
284 0         0 die("${source}JSON error: " . $pp->err_msg . "\n");
285             }
286             else {
287 0         0 state $pp = JSON::PP->new;
288              
289             # Glue lines, so we have at lease some relaxation.
290 0         0 $json =~ s/"\s*\\\n\s*"//g;
291              
292 0 0       0 $pp->relaxed if $info->{relaxed};
293 0         0 $pp->decode($json."\n");
294             }
295             }
296              
297             # JSON parser, what and how (also used by runtimeinfo().
298 142     142 0 338 sub json_parser() {
  142         234  
299 142   50     1150 my $relax = $ENV{CHORDPRO_JSON_RELAXED} // 2;
300 142 50       601 if ( $relax > 1 ) {
301 142         15965 require JSON::Relaxed;
302 142         1122 return { parser => "JSON::Relaxed",
303             version => $JSON::Relaxed::VERSION }
304             }
305             else {
306 0         0 require JSON::PP;
307 0         0 return { parser => "JSON::PP",
308             relaxed => $relax,
309             version => $JSON::PP::VERSION }
310             }
311             }
312              
313             push( @EXPORT, qw(json_parser json_load) );
314              
315             # Like prp2cfg, but updates.
316             # Also allows array pre/append and JSON data.
317             # Useful error messages are signalled with exceptions.
318              
319             push( @EXPORT, 'prpadd2cfg' );
320              
321 160     160 0 381259 sub prpadd2cfg ( $cfg, @defs ) {
  160         407  
  160         447  
  160         336  
322 160   50     634 $cfg //= {};
323 160         560 state $specials = { false => 0, true => 1, null => undef };
324              
325 160         645 while ( @defs ) {
326 56         146 my $key = shift(@defs);
327 56         134 my $value = shift(@defs);
328             # warn("K:$key V:$value\n");
329              
330             # Check and process the value, if needed.
331 56 100 100     900 if ( exists $specials->{$value} ) {
    100          
332 3         9 $value = $specials->{$value};
333             # warn("Value => $value\n");
334             }
335             elsif ( !( ref($value)
336             || $value !~ /[\[\{\]\}]/ ) ) {
337             # Not simple, assume JSON struct.
338 1         6 $value = json_load( $value, $value );
339             # use DDP; p($value, as => "Value ->");
340             }
341              
342             # Note that ':' is not oficially supported by RRJson.
343 56         316 my @keys = split( /[:.]/, $key );
344 56         190 my $lastkey = pop(@keys);
345              
346             # Handle pdf.fonts.xxx shortcuts.
347 56 50       275 if ( join( ".", @keys ) eq "pdf.fonts" ) {
348 0         0 my $s = { pdf => { fonts => { $lastkey => $value } } };
349 0         0 ChordPro::Config::expand_font_shortcuts($s);
350 0         0 $value = $s->{pdf}{fonts}{$lastkey};
351             }
352              
353 56         130 my $cur = \$cfg; # current pointer in struct
354 56         125 my $errkey = ""; # error trail
355 56 50       232 if ( $keys[0] eq "chords" ) {
356             # Chords are not in the config, but elsewhere.
357 0         0 $cur = \ChordPro::Chords::config_chords();
358 0         0 $errkey = "chords.";
359 0         0 shift(@keys);
360             }
361              
362             # Step through the keys.
363 56         170 foreach ( @keys ) {
364 74 100       950 if ( is_arrayref($$cur) ) {
    50          
365 19         29 my $ok;
366 19 100       115 if ( /^[<>]?[-+]?\d+$/ ) {
    50          
367 18         55 $cur = \($$cur->[$_]);
368 18         36 $ok++;
369             }
370             elsif ( ! exists( $$cur->[0]->{name} ) ) {
371 0         0 die("Array ", substr($errkey,0,-1),
372             " requires integer index (got \"$_\")\n");
373             }
374             else {
375 1         4 for my $i ( 0..@{$$cur} ) {
  1         5  
376 1 50       26 if ( $$cur->[$i]->{name} eq $_ ) {
377 1         4 $cur = \($$cur->[$i]);
378 1         4 $ok++;
379 1         3 last;
380             }
381             }
382             }
383 19 50       62 unless ( $ok ) {
384 0         0 die("Array ", substr($errkey,0,-1),
385             " has no matching element with name \"$_\"\n");
386             }
387             }
388             elsif ( is_hashref($$cur) ) {
389 55         174 $cur = \($$cur->{$_});
390             }
391             else {
392 0         0 die("Key ", substr($errkey,0,-1),
393             " ", ref($$cur),
394             " does not refer to an array or hash\n");
395             }
396 74         263 $errkey .= "$_."
397              
398             }
399              
400             # Final key.
401 56 100       498 if ( is_arrayref($$cur) ) {
    100          
402 24 100       310 if ( $lastkey =~ />([-+]?\d+)?$/ ) { # append
    100          
    100          
403 9 100       38 if ( defined $1 ) {
404 5         45 splice( @{$$cur},
405 5 100       13 $1 >= 0 ? 1+$1 : 1+@{$$cur}+$1, 0, $value );
  1         10  
406             }
407             else {
408 4         7 push( @{$$cur}, $value );
  4         30  
409             }
410             }
411             elsif ( $lastkey =~ /<([-+]?\d+)?$/ ) { # prepend
412 6 100       51 if ( defined $1 ) {
413 4         8 splice( @{$$cur}, $1, 0, $value );
  4         30  
414             }
415             else {
416 2         5 unshift( @{$$cur}, $value );
  2         14  
417             }
418             }
419             elsif ( $lastkey =~ /\/([-+]?\d+)?$/ ) { # remove
420 3 100       13 if ( defined $1 ) {
421 2         5 splice( @{$$cur}, $1, 1 );
  2         13  
422             }
423             else {
424 1         4 pop( @{$$cur} );
  1         7  
425             }
426             }
427             else { # replace
428 6 50       33 die("Array $errkey requires integer index (got \"$lastkey\")\n")
429             unless $lastkey =~ /^[-+]?\d+$/;
430 6         35 $$cur->[$lastkey] = $value;
431             }
432             }
433             elsif ( is_hashref($$cur) ) {
434 30 50       129 if ( $errkey =~ /^chords\./ ) {
435             # Chords must be defined.
436 0         0 ChordPro::Chords::add_config_chord( { name => $lastkey,
437             %$value } );
438             }
439             else {
440 30         255 $$cur->{$lastkey} = $value;
441             }
442             }
443             else {
444 2 100       22 die("Key ", substr($errkey,0,-1),
445             " is scalar, not ",
446             $lastkey =~ /^(?:[-+]?\d+|[<>])$/ ? "array" : "hash",
447             "\n");
448             }
449             }
450              
451             # The structure has been modified, but also return for covenience.
452 158         641 return $cfg;
453             }
454              
455             push( @EXPORT, 'prpadd2cfg' );
456              
457             # Remove markup.
458 2385     2385 0 23616 sub demarkup ( $t ) {
  2385         4110  
  2385         3370  
459 2385         5350 return join( '', grep { ! /^\
  2406         12133  
460             }
461             push( @EXPORT, 'demarkup' );
462              
463             # Split into markup/nonmarkup segments.
464 2407     2407 0 3551 sub splitmarkup ( $t ) {
  2407         3705  
  2407         3448  
465 2407         15990 my @t = split( qr;();, $t );
466 2407         8335 return @t;
467             }
468             push( @EXPORT, 'splitmarkup' );
469              
470             # For conditional filling of hashes.
471 46     46 0 87 sub maybe ( $key, $value, @rest ) {
  46         124  
  46         78  
  46         84  
  46         71  
472 46 50 33     215 if (defined $key and defined $value) {
473 0         0 return ( $key, $value, @rest );
474             }
475             else {
476 46 50 33     529 ( defined($key) || @rest ) ? @rest : ();
477             }
478             }
479             push( @EXPORT, "maybe" );
480              
481             # Min/Max.
482 90     90   931 use List::Util ();
  90         179  
  90         116070  
483             *min = \&List::Util::min;
484             *max = \&List::Util::max;
485              
486             push( @EXPORT, "min", "max" );
487              
488             # Plural
489 0     0 0 0 sub plural( $n, $tag, $plural=undef ) {
  0         0  
  0         0  
  0         0  
  0         0  
490 0   0     0 $plural //= $tag . "s";
491 0 0 0     0 ( $n || "no" ) . ( $n == 1 ? $tag : $plural );
492             }
493              
494             push( @EXPORT, "plural" );
495              
496             # Dimensions.
497             # Fontsize allows typical font units, and defaults to ref 12.
498 0     0 0 0 sub fontsize( $size, $ref=12 ) {
  0         0  
  0         0  
  0         0  
499 0 0 0     0 if ( $size && $size =~ /^([.\d]+)(%|e[mx]|p[tx])$/ ) {
500 0 0       0 return $ref/100 * $1 if $2 eq '%';
501 0 0       0 return $ref * $1 if $2 eq 'em';
502 0 0       0 return $ref/2 * $1 if $2 eq 'ex';
503 0 0       0 return $1 if $2 eq 'pt';
504 0 0       0 return $1 * 0.75 if $2 eq 'px';
505             }
506 0 0       0 $size || $ref;
507             }
508              
509             push( @EXPORT, "fontsize" );
510              
511             # Dimension allows arbitrary units, and defaults to ref 12.
512 0     0 0 0 sub dimension( $size, %sz ) {
  0         0  
  0         0  
  0         0  
513 0 0       0 return unless defined $size;
514 0         0 my $ref;
515 0 0 0     0 if ( ( $ref = $sz{fsize} )
516             && $size =~ /^([.\d]+)(%|e[mx])$/ ) {
517 0 0       0 return $ref/100 * $1 if $2 eq '%';
518 0 0       0 return $ref * $1 if $2 eq 'em';
519 0 0       0 return $ref/2 * $1 if $2 eq 'ex';
520             }
521 0 0 0     0 if ( ( $ref = $sz{width} )
522             && $size =~ /^([.\d]+)(%)$/ ) {
523 0 0       0 return $ref/100 * $1 if $2 eq '%';
524             }
525 0 0       0 if ( $size =~ /^([.\d]+)(p[tx]|[cm]m|in|)$/ ) {
526 0 0       0 return $1 if $2 eq 'pt';
527 0 0       0 return $1 * 0.75 if $2 eq 'px';
528 0 0       0 return $1 * 72 / 2.54 if $2 eq 'cm';
529 0 0       0 return $1 * 72 / 25.4 if $2 eq 'mm';
530 0 0       0 return $1 * 72 if $2 eq 'in';
531 0 0       0 return $1 if $2 eq '';
532             }
533 0         0 $size; # let someone else croak
534             }
535              
536             push( @EXPORT, "dimension" );
537              
538             # Checking font names against the PDF corefonts.
539              
540             my %corefonts =
541             (
542             ( map { lc($_) => $_ }
543             "Times-Roman",
544             "Times-Bold",
545             "Times-Italic",
546             "Times-BoldItalic",
547             "Helvetica",
548             "Helvetica-Bold",
549             "Helvetica-Oblique",
550             "Helvetica-BoldOblique",
551             "Courier",
552             "Courier-Bold",
553             "Courier-Oblique",
554             "Courier-BoldOblique",
555             "Symbol",
556             "ZapfDingbats" ),
557             );
558              
559             sub is_corefont {
560 5341     5341 0 19030 $corefonts{lc $_[0]};
561             }
562              
563             push( @EXPORT, "is_corefont" );
564              
565             # Progress reporting.
566              
567 90     90   833 use Ref::Util qw(is_coderef);
  90         205  
  90         165178  
568              
569             # Progress can return a false result to allow caller to stop.
570              
571 62     62 0 190 sub progress(%args) {
  62         320  
  62         116  
572 62         128 state $callback;
573 62         180 state $phase = "";
574 62         146 state $index = 0;
575 62         142 state $total = '';
576 62 50       229 unless ( %args ) { # reset
577 0         0 undef $callback;
578 0         0 $phase = "";
579 0         0 $index = 0;
580 0         0 return;
581             }
582              
583 62 50       292 $callback = $args{callback} if exists $args{callback};
584 62 50       413 return 1 unless $callback;
585              
586 0 0       0 if ( exists $args{phase} ) {
587 0 0       0 $index = 0 if $phase ne $args{phase};
588 0         0 $phase = $args{phase};
589             }
590 0 0       0 if ( exists $args{index} ) {
591 0         0 $index = $args{index};
592              
593             # Use index<0 to only set callback/phase.
594 0 0       0 $index = 0, $total = '', return if $index < 0;
595             }
596 0 0       0 if ( exists $args{total} ) {
597 0         0 $total = $args{total};
598             }
599              
600 0         0 my $args = { phase => $phase, index => $index, total => $total, %args };
601              
602 0         0 my $ret = ++$index;
603 0 0       0 if ( is_coderef($callback) ) {
604 0         0 $ret = eval { $callback->(%$args) };
  0         0  
605 0 0       0 if ( $@ ) {
606 0         0 warn($@);
607 0         0 undef $callback;
608             }
609             }
610             else {
611 0 0       0 if ( $callback eq "warn" ) {
612             # Simple progress message. Suppress if $index = 0 or total = 1.
613 0         0 $callback =
614             '%{index=0||' .
615             '%{total=1||Progress[%{phase}]: %{index}%{total|/%{}}%{msg| - %{}}}' .
616             '}';
617             }
618 0         0 my $msg = ChordPro::Output::Common::fmt_subst
619             ( { meta => $args }, $callback );
620 0         0 $msg =~ s/\n+$//;
621 0 0       0 warn( $msg, "\n" ) if $msg;
622             }
623              
624 0         0 return $ret;
625             }
626              
627             push( @EXPORT, "progress" );
628              
629             # Common items for property directives ({textsize} etc.).
630              
631 100     100 0 276 sub propitems() {
  100         213  
632 100         1105 qw( chord chorus diagrams footer grid label tab text title toc );
633             }
634              
635 100     100 0 311 sub propitems_re() {
  100         231  
636 100         584 my $re = join( '|', propitems() );
637 100         47369 qr/(?:$re)/;
638             }
639              
640             push( @EXPORT, "propitems_re" );
641             push( @EXPORT_OK, "propitems" );
642              
643             # For debugging encoding problems.
644              
645 0     0 0   sub as( $s ) {
  0            
  0            
646 0 0         return "" unless defined $s;
647 0           $s =~ s{ ( [^\x{20}-\x{7f}] ) }
648 0           { join( '', map { sprintf '\x{%02x}', ord $_ } split //, $1) }gex;
  0            
649 0           return $s;
650             }
651              
652             push( @EXPORT_OK, "as" );
653              
654 0     0 0   sub enumerated( @s ) {
  0            
  0            
655 0 0         return "" unless @s;
656 0           my $last = pop(@s);
657 0           my $ret = "";
658 0 0         $ret .= join(", ", @s) . " and " if @s;
659 0           $ret .= $last;
660 0           return $ret;
661             }
662              
663             push( @EXPORT_OK, "enumerated" );
664              
665             # Determine image type.
666              
667 0     0     sub _detect_image_format( $test ) {
  0            
  0            
668              
669 0 0         for ( ref($test) ? $$test : $test ) {
670 0 0         /^GIF\d\d[a-z]/ and return 'gif';
671 0 0         /^\xFF\xD8\xFF/ and return 'jpeg';
672 0 0         /^\x89PNG\x0D\x0A\x1A\x0A/ and return 'png';
673 0 0         /^\s*P[1-6]/ and return 'pnm';
674 0 0         /^II\x2A\x00/ and return 'tiff';
675 0 0         /^MM\x00\x2A/ and return 'tiff';
676 0 0         /^
677             }
678              
679             # Not recognized.
680 0           return;
681             }
682              
683 0     0 0   sub detect_image_format( $test ) {
  0            
  0            
684 0           my $format = _detect_image_format($test);
685              
686 0 0         if ( $format ) {
687 0           return { file_ext => $format, error => "" };
688             }
689 0           return { file_ext => "", error => "Unrecognized image type." };
690             }
691              
692             push( @EXPORT_OK, "detect_image_format" );
693              
694             =cut
695              
696             1;