File Coverage

blib/lib/ChordPro/Output/ChordPro.pm
Criterion Covered Total %
statement 216 335 64.4
branch 119 224 53.1
condition 14 35 40.0
subroutine 10 10 100.0
pod 0 6 0.0
total 359 610 58.8


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