File Coverage

lib/ChordPro/Output/ChordPro.pm
Criterion Covered Total %
statement 247 368 67.1
branch 127 230 55.2
condition 15 38 39.4
subroutine 14 14 100.0
pod 0 7 0.0
total 403 657 61.3


line stmt bran cond sub pod time code
1             #! perl
2              
3             package main;
4              
5             our $options;
6             our $config;
7              
8             package ChordPro::Output::ChordPro;
9              
10 5     5   1148 use v5.26;
  5         19  
11 5     5   37 use utf8;
  5         11  
  5         53  
12 5     5   162 use Carp;
  5         12  
  5         439  
13 5     5   38 use feature qw( signatures );
  5         18  
  5         723  
14 5     5   45 no warnings "experimental::signatures";
  5         12  
  5         286  
15              
16 5     5   35 use ChordPro::Output::Common;
  5         12  
  5         350  
17 5     5   48 use ChordPro::Utils qw( fq qquote demarkup is_true is_ttrue );
  5         12  
  5         32617  
18              
19             my $re_meta;
20              
21 23     23 0 68 sub generate_songbook ( $self, $sb ) {
  23         51  
  23         51  
  23         52  
22              
23             # Skip empty songbooks.
24 23 50       58 return [] unless eval { $sb->{songs}->[0]->{body} };
  23         155  
25              
26             # Build regex for the known metadata items.
27             $re_meta = join( '|',
28 23         134 map { quotemeta }
  322         742  
29             "title", "subtitle",
30             "artist", "composer", "lyricist", "arranger",
31             "album", "copyright", "year",
32             "key", "time", "tempo", "capo", "duration" );
33 23         421 $re_meta = qr/^($re_meta)$/;
34              
35 23         69 my @book;
36              
37 23         50 foreach my $song ( @{$sb->{songs}} ) {
  23         120  
38 26 100       103 if ( @book ) {
39 3 50       12 push(@book, "") if $options->{'backend-option'}->{tidy};
40 3         8 push(@book, "{new_song}");
41             }
42 26         61 push(@book, @{generate_song($song)});
  26         115  
43             }
44              
45 23         93 push( @book, "");
46 23         97 \@book;
47             }
48              
49             my $lyrics_only = 0;
50             my $variant = 'cho';
51             my $rechorus;
52              
53 26     26 0 51 sub upd_config () {
  26         42  
54 26         122 $rechorus = $::config->{chordpro}->{chorus}->{recall};
55 26         139 $lyrics_only = 2 * $::config->{settings}->{'lyrics-only'};
56             }
57              
58 26     26 0 49 sub generate_song ( $s ) {
  26         44  
  26         40  
59              
60 26         85 my $tidy = $options->{'backend-option'}->{tidy};
61 26   50     167 my $structured = ( $options->{'backend-option'}->{structure} // '' ) eq 'structured';
62             # $s->structurize if ++$structured;
63 26   50     137 $variant = $options->{'backend-option'}->{variant} || 'cho';
64 26         69 my $seq = $options->{'backend-option'}->{seq};
65 26         80 my $expand = $options->{'backend-option'}->{expand};
66 26         72 my $msp = $variant eq "msp";
67 26         90 upd_config();
68              
69 26         162 my @s;
70             my %imgs;
71              
72 26 50       135 if ( $s->{preamble} ) {
73 0         0 @s = @{ $s->{preamble} };
  0         0  
74             }
75              
76             push(@s, "{title: " . fq($s->{meta}->{title}->[0]) . "}")
77 26 50       205 if defined $s->{meta}->{title};
78 26 100       125 if ( defined $s->{subtitle} ) {
79 6         17 push(@s, map { +"{subtitle: ".fq($_)."}" } @{$s->{subtitle}});
  6         23  
  6         22  
80             }
81              
82 26 50       99 if ( $s->{meta} ) {
83 26 50       84 if ( $msp ) {
84 0   0     0 $s->{meta}->{source} //= [ "Lead Sheet" ];
85 0 0 0     0 $s->{meta}->{custom2} //= [ $seq ] if defined $seq;
86             }
87             # Known ones 'as is'.
88 26         55 my %used;
89 26         51 foreach my $k ( sort keys %{ $s->{meta} } ) {
  26         240  
90 130 100       454 next if $k =~ /^(?:title|subtitle)$/;
91 98 100       530 if ( $k =~ $re_meta ) {
92 11         23 push( @s, map { +"{$k: ".fq($_)."}" } @{ $s->{meta}->{$k} } );
  12         47  
  11         35  
93 11         40 $used{$k}++;
94             }
95             }
96             # Unknowns with meta prefix.
97 26         95 foreach my $k ( sort keys %{ $s->{meta} } ) {
  26         177  
98 130 100       281 next if $used{$k};
99 119 50       470 next if $k =~ /^(?:title|subtitle|songindex|key_.*|chords|numchords)$/;
100 0 0       0 next if $k =~ /^_/;
101 0         0 push( @s, map { +"{meta: $k ".fq($_)."}" } @{ $s->{meta}->{$k} } );
  0         0  
  0         0  
102             }
103             }
104              
105 26 50       140 if ( $s->{settings} ) {
106 26         67 foreach ( sort keys %{ $s->{settings} } ) {
  26         127  
107 16 100       65 if ( $_ eq "diagrams" ) {
    100          
108 3 100       13 next if $s->{settings}->{diagrampos};
109 2         7 my $v = $s->{settings}->{$_};
110 2 50       12 if ( is_ttrue($v) ) {
    50          
111 0         0 $v = "on";
112             }
113             elsif ( is_true($v) ) {
114             }
115             else {
116 2         10 $v = "off";
117             }
118 2         14 push(@s, "{diagrams: $v}");
119             }
120             elsif ( $_ eq "diagrampos" ) {
121 1         5 my $v = $s->{settings}->{$_};
122 1         5 push(@s, "{diagrams: $v}");
123             }
124             else {
125 12         56 push(@s, "{$_: " . $s->{settings}->{$_} . "}");
126             }
127             }
128             }
129              
130 26 50       91 push(@s, "") if $tidy;
131              
132             # Move a trailing list of chords to the beginning, so the chords
133             # are defined when the song is parsed.
134 26 50 33     54 if ( @{ $s->{body} } && $s->{body}->[-1]->{type} eq "diagrams"
  26   33     201  
135             && $s->{body}->[-1]->{origin} ne "__CLI__"
136             ) {
137 0         0 unshift( @{ $s->{body} }, pop( @{ $s->{body} } ) );
  0         0  
  0         0  
138             }
139              
140 26 100       96 if ( $s->{define} ) {
141 9         37 foreach my $info ( @{ $s->{define} } ) {
  9         32  
142 27         70 push( @s, define($info) );
143             }
144 9 50       46 push(@s, "") if $tidy;
145             }
146              
147 26         70 my $ctx = "";
148              
149 26         55 my @elts = @{$s->{body}};
  26         130  
150 26         87 while ( @elts ) {
151 310         566 my $elt = shift(@elts);
152              
153 310 100       762 if ( $elt->{context} ne $ctx ) {
154 30 100       131 push(@s, "{end_of_$ctx}") if $ctx;
155 30         71 $ctx = $elt->{context};
156 30 100       90 if ( $ctx ) {
157              
158 18         51 my $t = "{start_of_$ctx";
159              
160 18 100       87 if ( $elt->{type} eq "set" ) {
161 4 100       22 if ( $elt->{name} eq "gridparams" ) {
    50          
162 2         3 my @gridparams = @{ $elt->{value} };
  2         9  
163 2         5 $t .= ": ";
164 2 50       6 $t .= $gridparams[2] . "+" if $gridparams[2];
165 2         8 $t .= $gridparams[0];
166 2 50       8 $t .= "x" . $gridparams[1] if $gridparams[1];
167 2 50       4 $t .= "+" . $gridparams[3] if $gridparams[3];
168 2 50       7 if ( $gridparams[4] ) {
169 0         0 my $tag = $gridparams[4];
170 0 0       0 $t .= " " . $tag if $tag ne "";
171             }
172             }
173             elsif ( $elt->{name} eq "label" ) {
174 2         7 my $tag = $elt->{value};
175 2 50       10 $t .= ": " . $tag if $tag ne "";
176             }
177              
178             }
179 18         44 $t .= "}";
180 18         47 push( @s, $t );
181             }
182             }
183              
184 310 100       661 if ( $elt->{type} eq "empty" ) {
185 61 50       185 push(@s, "***SHOULD NOT HAPPEN***"), next
186             if $structured;
187 61         120 push( @s, "" );
188 61         121 next;
189             }
190              
191 249 100       543 if ( $elt->{type} eq "colb" ) {
192 3 50       12 next if $msp;
193 3         17 push(@s, "{column_break}");
194 3         8 next;
195             }
196              
197 246 100       481 if ( $elt->{type} eq "newpage" ) {
198 3 50       51 next if $msp;
199 3         16 push(@s, "{new_page}");
200 3         8 next;
201             }
202              
203 243 100       549 if ( $elt->{type} eq "songline" ) {
204 119         343 push(@s, songline( $s, $elt ));
205 119         440 next;
206             }
207              
208 124 100       297 if ( $elt->{type} eq "tabline" ) {
209 24         48 push(@s, $elt->{text} );
210 24         48 next;
211             }
212              
213 100 100       213 if ( $elt->{type} eq "gridline" ) {
214 4         22 push(@s, gridline( $s, $elt ));
215 4         11 next;
216             }
217              
218 96 50       210 if ( $elt->{type} eq "verse" ) {
219 0 0       0 push(@s, "") if $tidy;
220 0         0 foreach my $e ( @{$elt->{body}} ) {
  0         0  
221 0 0       0 if ( $e->{type} eq "empty" ) {
222 0 0       0 push(@s, "***SHOULD NOT HAPPEN***"), next
223             if $structured;
224             }
225 0 0       0 if ( $e->{type} eq "song" ) {
226 0         0 push(@s, songline( $s, $e ));
227 0         0 next;
228             }
229             }
230 0 0       0 push(@s, "") if $tidy;
231 0         0 next;
232             }
233              
234 96 50       208 if ( $elt->{type} eq "chorus" ) {
235 0 0       0 push(@s, "") if $tidy;
236 0         0 push(@s, "{start_of_chorus*}");
237 0         0 foreach my $e ( @{$elt->{body}} ) {
  0         0  
238 0 0       0 if ( $e->{type} eq "empty" ) {
239 0         0 push(@s, "");
240 0         0 next;
241             }
242 0 0       0 if ( $e->{type} eq "songline" ) {
243 0         0 push(@s, songline( $s, $e ));
244 0         0 next;
245             }
246             }
247 0         0 push(@s, "{end_of_chorus*}");
248 0 0       0 push(@s, "") if $tidy;
249 0         0 next;
250             }
251              
252 96 50       209 if ( $elt->{type} eq "rechorus" ) {
253 0 0 0     0 if ( $msp ) {
    0          
    0          
254 0         0 push( @s, "{chorus}" );
255             }
256             elsif ( $rechorus->{quote} ) {
257 0         0 unshift( @elts, @{ $elt->{chorus} } );
  0         0  
258             }
259             elsif ( $rechorus->{type} && $rechorus->{tag} ) {
260 0         0 push( @s, "{".$rechorus->{type}.": ".$rechorus->{tag}."}" );
261             }
262             else {
263 0         0 push( @s, "{chorus}" );
264             }
265 0         0 next;
266             }
267              
268 96 50       216 if ( $elt->{type} eq "tab" ) {
269 0 0       0 push(@s, "") if $tidy;
270 0         0 push(@s, "{start_of_tab}");
271 0         0 push(@s, @{$elt->{body}});
  0         0  
272 0         0 push(@s, "{end_of_tab}");
273 0 0       0 push(@s, "") if $tidy;
274 0         0 next;
275             }
276              
277 96 100       343 if ( $elt->{type} =~ /^comment(?:_italic|_box)?$/ ) {
278 25         64 my $type = $elt->{type};
279 25 100       83 my $text = $expand ? $elt->{text} : $elt->{orig};
280 25 50       55 if ( $msp ) {
281 0 0       0 $type = $type eq 'comment'
    0          
282             ? 'highlight'
283             : $type eq 'comment_italic'
284             ? 'comment'
285             : $type;
286             }
287             # Flatten chords/phrases.
288 25 50       72 if ( $elt->{chords} ) {
289 0         0 $text = "";
290 0         0 for ( 0..$#{ $elt->{chords} } ) {
  0         0  
291             $text .= "[" . fq(chord( $s, $elt->{chords}->[$_])) . "]"
292 0 0       0 if $elt->{chords}->[$_] ne "";
293 0         0 $text .= $elt->{phrases}->[$_];
294             }
295             }
296 25 50       58 $text = fmt_subst( $s, $text ) if $msp;
297 25 50       49 push(@s, "") if $tidy;
298 25         95 push(@s, "{$type: ".fq($text)."}");
299 25 50       69 push(@s, "") if $tidy;
300 25         87 next;
301             }
302              
303 71 50       184 if ( $elt->{type} eq "image" ) {
304 0         0 my $uri = $elt->{uri};
305 0 0 0     0 if ( $msp && $uri !~ /^id=/ ) {
306 0   0     0 $imgs{$uri} //= keys(%imgs);
307 0         0 $uri = sprintf("id=img%02d", $imgs{$uri});
308             }
309 0         0 my @args = ( "image:", $uri );
310 0         0 while ( my($k,$v) = each( %{ $elt->{opts} } ) ) {
  0         0  
311 0         0 push( @args, "$k=$v" );
312             }
313 0         0 foreach ( @args ) {
314 0 0       0 next unless /\s/;
315 0         0 $_ = '"' . $_ . '"';
316             }
317 0         0 push( @s, "{@args}" );
318 0         0 next;
319             }
320              
321 71 100       191 if ( $elt->{type} eq "diagrams" ) {
322 2         4 for ( @{$elt->{chords}} ) {
  2         5  
323 2         18 push( @s, define( $s->{chordsinfo}->{$_}, 1 ) );
324             }
325             }
326              
327 71 100       170 if ( $elt->{type} eq "set" ) {
328 30 50       181 if ( $elt->{name} eq "lyrics-only" ) {
    50          
    50          
329             $lyrics_only = $elt->{value}
330 0 0       0 unless $lyrics_only > 1;
331             }
332             elsif ( $elt->{name} eq "transpose" ) {
333             }
334             # Arbitrary config values.
335             elsif ( $elt->{name} =~ /^(chordpro\..+)/ ) {
336 0         0 my @k = split( /[.]/, $1 );
337 0         0 my $cc = {};
338 0         0 my $c = \$cc;
339 0         0 foreach ( @k ) {
340 0         0 $c = \($$c->{$_});
341             }
342 0         0 $$c = $elt->{value};
343 0         0 $config->augment($cc);
344 0         0 upd_config();
345             }
346 30         78 next;
347             }
348              
349 41 100       100 if ( $elt->{type} eq "ignore" ) {
350 39         85 push( @s, $elt->{text} );
351 39         83 next;
352             }
353              
354             }
355              
356 26 100       186 push(@s, "{end_of_$ctx}") if $ctx;
357              
358 26         73 my $did = 0;
359 26 100 100     137 if ( $s->{chords} && @{ $s->{chords}->{chords} } && $variant ne 'msp' ) {
  24   66     189  
360 23         42 for ( @{ $s->{chords}->{chords} } ) {
  23         100  
361 71 100       316 last unless $s->{chordsinfo}->{$_}->parser->has_diagrams;
362 67 100       556 push( @s, "" ) unless $did++;
363 67         200 push( @s, define( $s->{chordsinfo}->{$_}, 1 ) );
364             }
365             }
366              
367             # Process image assets.
368 26         173 foreach ( sort { $imgs{$a} <=> $imgs{$b} } keys %imgs ) {
  0         0  
369 0         0 my $url = $_;
370 0         0 my $id = $imgs{$url};
371 0         0 my $type = "jpg";
372 0 0       0 $type = lc($1) if $url =~ /\.(\w+)$/;
373 0         0 require MIME::Base64;
374 0         0 require Image::Info;
375              
376             # Slurp the image.
377 0         0 my $fd;
378 0 0       0 unless ( open( $fd, '<:raw', $url ) ) {
379 0         0 warn("$url: $!\n");
380 0         0 next;
381             }
382 0         0 my $data = do { local $/; <$fd> };
  0         0  
  0         0  
383 0         0 close($fd);
384              
385             # Get info.
386 0         0 my $info = Image::Info::image_info(\$data);
387 0 0       0 if ( $info->{error} ) {
388 0         0 do_warn($info->{error});
389 0         0 next;
390             }
391              
392             # Write in-line data.
393             push( @s,
394             sprintf( "##image: id=img%02d" .
395             " src=%s type=%s width=%d height=%d enc=base64",
396             $id, $url, $info->{file_ext},
397 0         0 $info->{width}, $info->{height} ) );
398 0         0 $data = MIME::Base64::encode($data, '');
399 0         0 my $i = 0;
400             # Note: 76 is the standard chunk size for base64 data.
401 0         0 while ( $i < length($data) ) {
402 0         0 push( @s, "# ".substr($data, $i, 76) );
403 0         0 $i += 76;
404             }
405             }
406              
407 26         257 \@s;
408             }
409              
410 119     119 0 180 sub songline ( $song, $elt ) {
  119         186  
  119         170  
  119         156  
411              
412 119 100 100     476 if ( $lyrics_only || !exists($elt->{chords}) ) {
413 40         67 return fq(join( "", @{ $elt->{phrases} } ));
  40         134  
414             }
415              
416 79         158 my $line = "";
417 79         137 foreach my $c ( 0..$#{$elt->{chords}} ) {
  79         320  
418 226         586 $line .= "[" . fq(chord( $song, $elt->{chords}->[$c])) . "]" . fq($elt->{phrases}->[$c]);
419             }
420 79         532 $line =~ s/^\[\]//;
421 79         253 $line;
422             }
423              
424 4     4 0 6 sub gridline ( $song, $elt ) {
  4         7  
  4         5  
  4         7  
425              
426 4         7 my $line = "";
427 4         5 for ( @{ $elt->{tokens} } ) {
  4         13  
428 44 100       90 $line .= " " if $line;
429 44 100       125 if ( $_->{class} eq "chord" ) {
430 10         21 $line .= chord( $song, $_->{chord} );
431             }
432             else {
433 34         61 $line .= $_->{symbol};
434             }
435             }
436              
437 4 50       15 if ( $elt->{comment} ) {
438 0 0       0 $line .= " " if $line;
439 0         0 my $res = "";
440 0         0 my $t = $elt->{comment};
441 0 0       0 if ( $t->{chords} ) {
442 0         0 for ( 0..$#{ $t->{chords} } ) {
  0         0  
443 0         0 $res .= "[" . fq(chord( $song, $t->{chords}->[$_])) . "]" . fq($t->{phrases}->[$_]);
444             }
445             }
446             else {
447 0         0 $res .= fq($t->{text});
448             }
449 0         0 $res =~ s/^\[\]//;
450 0         0 $line .= $res;
451             }
452              
453 4         10 $line;
454             }
455              
456 236     236 0 333 sub chord ( $s, $c ) {
  236         371  
  236         346  
  236         316  
457 236 100       726 return "" unless length($c);
458 176         562 local $c->info->{display} = undef;
459 176         440 local $c->info->{format} = undef;
460 176         490 my $t = $c->chord_display;
461 176 50       3106 if ( $variant ne 'msp' ) {
462 176         553 $t = demarkup($t);
463             }
464 176 100       696 return "*$t" if $c->info->is_annotation;
465 174         734 return $t;
466             }
467              
468 96     96 0 138 sub define ( $info, $is_diag = 0 ) {
  96         161  
  96         157  
  96         123  
469              
470 96 100       212 my $t = $is_diag ? "#{chord: " : "{define: ";
471 96         198 $t .= $info->{name};
472 96 100       208 unless ( $is_diag ) {
473 27 50       114 if ( $info->{copyall} ) {
    50          
474 0         0 $t .= " copyall " . qquote($info->{copyall});
475             }
476             elsif ( $info->{copy} ) {
477 0         0 $t .= " copy " . qquote($info->{copy});
478             }
479 27         52 for ( qw( display ) ) {
480 27 100       80 next unless defined $info->{$_};
481 2         29 $t .= " $_ " . qquote($info->{$_}->name );
482             }
483 27         55 for ( qw( format ) ) {
484 27 100       78 next unless defined $info->{$_};
485 9         29 my $x = qquote($info->{$_}, 1 );
486 9         23 $x =~ s/\%\{/\\%{/g;
487 9         26 $t .= " $_ $x";
488             }
489             }
490              
491 96 50       231 if ( $::config->{instrument}->{type} eq "keyboard" ) {
492             $t .= " keys " .
493 0         0 join(" ", @{$info->{keys}})
494 0 0 0     0 if $info->{keys} && @{$info->{keys}};
  0         0  
495             }
496             else {
497 96         236 $t .= " base-fret " . $info->{base};
498             $t .= " frets " .
499 576 100       1259 join(" ", map { $_ < 0 ? "N" : $_ } @{$info->{frets}})
  96         195  
500 96 50       256 if $info->{frets};
501             $t .= " fingers " .
502 36 50       76 join(" ", map { $_ < 0 ? "N" : $_ } @{$info->{fingers}})
  6         16  
503 96 100 100     329 if $info->{fingers} && @{$info->{fingers}};
  66         226  
504             }
505 96 100       210 unless ( $is_diag ) {
506 27         54 for ( qw( diagram ) ) {
507 27 100       71 next unless defined $info->{$_};
508 3         9 my $v = $info->{$_};
509 3 50       14 if ( is_true($v) ) {
510 0 0       0 if ( is_ttrue($v) ) {
511 0         0 next;
512             }
513             }
514             else {
515 3         8 $v = "off";
516             }
517 3         14 $t .= " $_ $v";
518             }
519             }
520              
521 96         347 return $t . "}";
522             }
523              
524             1;