File Coverage

blib/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   1012 use v5.26;
  5         20  
11 5     5   31 use utf8;
  5         12  
  5         33  
12 5     5   181 use Carp;
  5         12  
  5         394  
13 5     5   38 use feature qw( signatures );
  5         10  
  5         617  
14 5     5   43 no warnings "experimental::signatures";
  5         12  
  5         307  
15              
16 5     5   45 use ChordPro::Output::Common;
  5         12  
  5         324  
17 5     5   34 use ChordPro::Utils qw( fq qquote demarkup is_true is_ttrue );
  5         24  
  5         31019  
18              
19             my $re_meta;
20              
21 23     23 0 70 sub generate_songbook ( $self, $sb ) {
  23         57  
  23         62  
  23         60  
22              
23             # Skip empty songbooks.
24 23 50       63 return [] unless eval { $sb->{songs}->[0]->{body} };
  23         110  
25              
26             # Build regex for the known metadata items.
27             $re_meta = join( '|',
28 23         119 map { quotemeta }
  322         685  
29             "title", "subtitle",
30             "artist", "composer", "lyricist", "arranger",
31             "album", "copyright", "year",
32             "key", "time", "tempo", "capo", "duration" );
33 23         409 $re_meta = qr/^($re_meta)$/;
34              
35 23         62 my @book;
36              
37 23         60 foreach my $song ( @{$sb->{songs}} ) {
  23         100  
38 26 100       78 if ( @book ) {
39 3 50       10 push(@book, "") if $options->{'backend-option'}->{tidy};
40 3         6 push(@book, "{new_song}");
41             }
42 26         60 push(@book, @{generate_song($song)});
  26         91  
43             }
44              
45 23         89 push( @book, "");
46 23         100 \@book;
47             }
48              
49             my $lyrics_only = 0;
50             my $variant = 'cho';
51             my $rechorus;
52              
53 26     26 0 56 sub upd_config () {
  26         45  
54 26         120 $rechorus = $::config->{chordpro}->{chorus}->{recall};
55 26         149 $lyrics_only = 2 * $::config->{settings}->{'lyrics-only'};
56             }
57              
58 26     26 0 45 sub generate_song ( $s ) {
  26         46  
  26         43  
59              
60 26         95 my $tidy = $options->{'backend-option'}->{tidy};
61 26   50     146 my $structured = ( $options->{'backend-option'}->{structure} // '' ) eq 'structured';
62             # $s->structurize if ++$structured;
63 26   50     144 $variant = $options->{'backend-option'}->{variant} || 'cho';
64 26         73 my $seq = $options->{'backend-option'}->{seq};
65 26         68 my $expand = $options->{'backend-option'}->{expand};
66 26         74 my $msp = $variant eq "msp";
67 26         91 upd_config();
68              
69 26         167 my @s;
70             my %imgs;
71              
72 26 50       93 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       204 if defined $s->{meta}->{title};
78 26 100       100 if ( defined $s->{subtitle} ) {
79 6         15 push(@s, map { +"{subtitle: ".fq($_)."}" } @{$s->{subtitle}});
  6         20  
  6         19  
80             }
81              
82 26 50       90 if ( $s->{meta} ) {
83 26 50       85 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         73 my %used;
89 26         61 foreach my $k ( sort keys %{ $s->{meta} } ) {
  26         264  
90 130 100       493 next if $k =~ /^(?:title|subtitle)$/;
91 98 100       470 if ( $k =~ $re_meta ) {
92 11         25 push( @s, map { +"{$k: ".fq($_)."}" } @{ $s->{meta}->{$k} } );
  12         44  
  11         30  
93 11         54 $used{$k}++;
94             }
95             }
96             # Unknowns with meta prefix.
97 26         101 foreach my $k ( sort keys %{ $s->{meta} } ) {
  26         139  
98 130 100       289 next if $used{$k};
99 119 50       533 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       108 if ( $s->{settings} ) {
106 26         54 foreach ( sort keys %{ $s->{settings} } ) {
  26         132  
107 16 100       69 if ( $_ eq "diagrams" ) {
    100          
108 3 100       15 next if $s->{settings}->{diagrampos};
109 2         5 my $v = $s->{settings}->{$_};
110 2 50       11 if ( is_ttrue($v) ) {
    50          
111 0         0 $v = "on";
112             }
113             elsif ( is_true($v) ) {
114             }
115             else {
116 2         7 $v = "off";
117             }
118 2         10 push(@s, "{diagrams: $v}");
119             }
120             elsif ( $_ eq "diagrampos" ) {
121 1         4 my $v = $s->{settings}->{$_};
122 1         6 push(@s, "{diagrams: $v}");
123             }
124             else {
125 12         52 push(@s, "{$_: " . $s->{settings}->{$_} . "}");
126             }
127             }
128             }
129              
130 26 50       89 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     40 if ( @{ $s->{body} } && $s->{body}->[-1]->{type} eq "diagrams"
  26   33     195  
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       93 if ( $s->{define} ) {
141 9         21 foreach my $info ( @{ $s->{define} } ) {
  9         31  
142 27         74 push( @s, define($info) );
143             }
144 9 50       46 push(@s, "") if $tidy;
145             }
146              
147 26         70 my $ctx = "";
148              
149 26         52 my @elts = @{$s->{body}};
  26         137  
150 26         94 while ( @elts ) {
151 310         512 my $elt = shift(@elts);
152              
153 310 100       715 if ( $elt->{context} ne $ctx ) {
154 30 100       118 push(@s, "{end_of_$ctx}") if $ctx;
155 30         70 $ctx = $elt->{context};
156 30 100       73 if ( $ctx ) {
157              
158 18         44 my $t = "{start_of_$ctx";
159              
160 18 100       62 if ( $elt->{type} eq "set" ) {
161 4 100       16 if ( $elt->{name} eq "gridparams" ) {
    50          
162 2         5 my @gridparams = @{ $elt->{value} };
  2         9  
163 2         5 $t .= ": ";
164 2 50       7 $t .= $gridparams[2] . "+" if $gridparams[2];
165 2         5 $t .= $gridparams[0];
166 2 50       7 $t .= "x" . $gridparams[1] if $gridparams[1];
167 2 50       7 $t .= "+" . $gridparams[3] if $gridparams[3];
168 2 50       8 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         5 my $tag = $elt->{value};
175 2 50       9 $t .= ": " . $tag if $tag ne "";
176             }
177              
178             }
179 18         79 $t .= "}";
180 18         51 push( @s, $t );
181             }
182             }
183              
184 310 100       683 if ( $elt->{type} eq "empty" ) {
185 61 50       132 push(@s, "***SHOULD NOT HAPPEN***"), next
186             if $structured;
187 61         112 push( @s, "" );
188 61         130 next;
189             }
190              
191 249 100       520 if ( $elt->{type} eq "colb" ) {
192 3 50       12 next if $msp;
193 3         6 push(@s, "{column_break}");
194 3         7 next;
195             }
196              
197 246 100       466 if ( $elt->{type} eq "newpage" ) {
198 3 50       20 next if $msp;
199 3         9 push(@s, "{new_page}");
200 3         7 next;
201             }
202              
203 243 100       503 if ( $elt->{type} eq "songline" ) {
204 119         285 push(@s, songline( $s, $elt ));
205 119         338 next;
206             }
207              
208 124 100       305 if ( $elt->{type} eq "tabline" ) {
209 24         49 push(@s, $elt->{text} );
210 24         43 next;
211             }
212              
213 100 100       214 if ( $elt->{type} eq "gridline" ) {
214 4         12 push(@s, gridline( $s, $elt ));
215 4         11 next;
216             }
217              
218 96 50       203 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       232 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       211 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       302 if ( $elt->{type} =~ /^comment(?:_italic|_box)?$/ ) {
278 25         71 my $type = $elt->{type};
279 25 100       73 my $text = $expand ? $elt->{text} : $elt->{orig};
280 25 50       62 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 .= "[" . $elt->{chords}->[$_] . "]"
292 0 0       0 if $elt->{chords}->[$_] ne "";
293 0         0 $text .= $elt->{phrases}->[$_];
294             }
295             }
296 25 50       54 $text = fmt_subst( $s, $text ) if $msp;
297 25 50       89 push(@s, "") if $tidy;
298 25         85 push(@s, "{$type: ".fq($text)."}");
299 25 50       87 push(@s, "") if $tidy;
300 25         75 next;
301             }
302              
303 71 50       169 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       231 if ( $elt->{type} eq "diagrams" ) {
322 2         10 for ( @{$elt->{chords}} ) {
  2         19  
323 2         9 push( @s, define( $s->{chordsinfo}->{$_}, 1 ) );
324             }
325             }
326              
327 71 100       165 if ( $elt->{type} eq "set" ) {
328 30 50       159 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         230 next;
347             }
348              
349 41 100       107 if ( $elt->{type} eq "ignore" ) {
350 39         77 push( @s, $elt->{text} );
351 39         74 next;
352             }
353              
354             }
355              
356 26 100       107 push(@s, "{end_of_$ctx}") if $ctx;
357              
358 26         73 my $did = 0;
359 26 100 100     120 if ( $s->{chords} && @{ $s->{chords}->{chords} } && $variant ne 'msp' ) {
  24   66     179  
360 23         49 for ( @{ $s->{chords}->{chords} } ) {
  23         84  
361 71 100       295 last unless $s->{chordsinfo}->{$_}->parser->has_diagrams;
362 67 100       510 push( @s, "" ) unless $did++;
363 67         192 push( @s, define( $s->{chordsinfo}->{$_}, 1 ) );
364             }
365             }
366              
367             # Process image assets.
368 26         189 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         277 \@s;
408             }
409              
410 119     119 0 177 sub songline ( $song, $elt ) {
  119         184  
  119         167  
  119         169  
411              
412 119 100 100     462 if ( $lyrics_only || !exists($elt->{chords}) ) {
413 40         65 return fq(join( "", @{ $elt->{phrases} } ));
  40         130  
414             }
415              
416 79         144 my $line = "";
417 79         129 foreach my $c ( 0..$#{$elt->{chords}} ) {
  79         268  
418 226         559 $line .= "[" . fq(chord( $song, $elt->{chords}->[$c])) . "]" . fq($elt->{phrases}->[$c]);
419             }
420 79         435 $line =~ s/^\[\]//;
421 79         261 $line;
422             }
423              
424 4     4 0 7 sub gridline ( $song, $elt ) {
  4         7  
  4         5  
  4         9  
425              
426 4         7 my $line = "";
427 4         7 for ( @{ $elt->{tokens} } ) {
  4         24  
428 44 100       91 $line .= " " if $line;
429 44 100       92 if ( $_->{class} eq "chord" ) {
430 10         22 $line .= chord( $song, $_->{chord} );
431             }
432             else {
433 34         61 $line .= $_->{symbol};
434             }
435             }
436              
437 4 50       13 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         13 $line;
454             }
455              
456 236     236 0 324 sub chord ( $s, $c ) {
  236         344  
  236         387  
  236         309  
457 236 100       711 return "" unless length($c);
458 176         486 local $c->info->{display} = undef;
459 176         458 local $c->info->{format} = undef;
460 176         508 my $t = $c->chord_display;
461 176 50       2655 if ( $variant ne 'msp' ) {
462 176         546 $t = demarkup($t);
463             }
464 176 100       617 return "*$t" if $c->info->is_annotation;
465 174         694 return $t;
466             }
467              
468 96     96 0 146 sub define ( $info, $is_diag = 0 ) {
  96         142  
  96         154  
  96         128  
469              
470 96 100       237 my $t = $is_diag ? "#{chord: " : "{define: ";
471 96         205 $t .= $info->{name};
472 96 100       215 unless ( $is_diag ) {
473 27 50       90 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         53 for ( qw( display ) ) {
480 27 100       80 next unless defined $info->{$_};
481 2         12 $t .= " $_ " . qquote($info->{$_}->name );
482             }
483 27         68 for ( qw( format ) ) {
484 27 100       84 next unless defined $info->{$_};
485 9         45 my $x = qquote($info->{$_}, 1 );
486 9         26 $x =~ s/\%\{/\\%{/g;
487 9         28 $t .= " $_ $x";
488             }
489             }
490              
491 96 50       269 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         245 $t .= " base-fret " . $info->{base};
498             $t .= " frets " .
499 576 100       1313 join(" ", map { $_ < 0 ? "N" : $_ } @{$info->{frets}})
  96         236  
500 96 50       264 if $info->{frets};
501             $t .= " fingers " .
502 36 50       77 join(" ", map { $_ < 0 ? "N" : $_ } @{$info->{fingers}})
  6         13  
503 96 100 100     355 if $info->{fingers} && @{$info->{fingers}};
  66         230  
504             }
505 96 100       210 unless ( $is_diag ) {
506 27         58 for ( qw( diagram ) ) {
507 27 100       82 next unless defined $info->{$_};
508 3         7 my $v = $info->{$_};
509 3 50       13 if ( is_true($v) ) {
510 0 0       0 if ( is_ttrue($v) ) {
511 0         0 next;
512             }
513             }
514             else {
515 3         6 $v = "off";
516             }
517 3         18 $t .= " $_ $v";
518             }
519             }
520              
521 96         370 return $t . "}";
522             }
523              
524             1;