File Coverage

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