File Coverage

lib/ChordPro/Output/Markdown.pm
Criterion Covered Total %
statement 172 209 82.3
branch 47 66 71.2
condition 10 18 55.5
subroutine 22 26 84.6
pod 0 22 0.0
total 251 341 73.6


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             package main;
3              
4             our $options;
5             our $config;
6              
7             package ChordPro::Output::Markdown;
8             # Author: Johannes Rumpf / 2022
9              
10 1     1   12 use strict;
  1         2  
  1         35  
11 1     1   6 use warnings;
  1         3  
  1         30  
12 1     1   8 use ChordPro::Output::Common;
  1         1  
  1         87  
13 1     1   514 use Text::Layout::Markdown;
  1         13548  
  1         3224  
14              
15             my $single_space = 0; # suppress chords line when empty
16             my $lyrics_only = 0; # suppress all chords lines
17             my $chords_under = 0; # chords under lyrics
18             my $text_layout = Text::Layout::Markdown->new; # Text::Layout::Text->new;
19             my %line_routines = ();
20             my $tidy;
21             my $rechorus; # not implemented @todo
22             my $act_song;
23             my $cp = "\t"; # Chord-Prefix // Verbatim / Code line in Markdown
24              
25             sub upd_config {
26 10     10 0 35 $lyrics_only = $config->{settings}->{'lyrics-only'};
27 10         34 $chords_under = $config->{settings}->{'chords-under'};
28 10         42 $rechorus = $config->{text}->{chorus}->{recall};
29             }
30              
31             sub generate_songbook {
32 10     10 0 37 my ( $self, $sb ) = @_;
33 10         22 my @book;
34             # push(@book, "[TOC]"); # maybe https://metacpan.org/release/IMAGO/Markdown-TOC-0.01 to create a TOC?
35              
36 10         29 foreach my $song ( @{$sb->{songs}} ) {
  10         41  
37 10 50       39 if ( @book ) {
38 0 0       0 push(@book, "") if $options->{'backend-option'}->{tidy};
39             }
40 10         28 push(@book, @{generate_song($song)});
  10         40  
41 10         50 push(@book, "--------------- \n"); #Horizontal line between each song
42             }
43              
44 10         41 push( @book, "");
45              
46             # remove all double empty lines
47 10         19 my @new;
48 10         18 my $count = 0;
49 10         32 foreach (@book){
50 268 100       541 if ($_ =~ /.{1,}/ ){
51 173         286 push(@new, $_);
52 173         260 $count = 0
53             } else {
54 95 100       250 push(@new, $_) if $count == 0;
55 95         136 $count++;
56             }
57             }
58 10         69 \@new;
59             }
60              
61             sub generate_song {
62 10     10 0 29 my ( $s ) = @_;
63 10         5444 $act_song = $s;
64 10         69 $tidy = $options->{'backend-option'}->{tidy};
65 10         36 $single_space = $options->{'single-space'};
66              
67 10         50 upd_config();
68              
69             # asume songline a verse when no context is applied. # check https://github.com/ChordPro/chordpro/pull/211
70 10         20 foreach my $item ( @{ $s->{body} } ) {
  10         39  
71 155 100 100     494 if ( $item->{type} eq "songline" && $item->{context} eq '' ){
72 55         117 $item->{context} = 'verse';
73             }} # end of pull --
74            
75 10         145 $s->structurize;
76 10         23 my @s;
77 10 50       86 push(@s, "# " . $s->{title}) if defined $s->{title};
78 10 100       37 if ( defined $s->{subtitle} ) {
79 3         7 push(@s, map { +"## $_" } @{$s->{subtitle}});
  3         14  
  3         9  
80             }
81              
82 10 50       63 if ( $lyrics_only eq 0 ){
83 10         238 my $all_chords = "";
84             # https://chordgenerator.net/D.png?p=xx0212&s=2 # reuse of other projects (https://github.com/einaregilsson/ChordImageGenerator)?
85             # generate png-out of this project? // fingers also possible - but not set in basics.
86 10         22 foreach my $mchord (@{$s->{chords}->{chords}}){
  10         34  
87             # replace -1 with 'x' - alternative '-'
88 32 100       49 my $frets = join("", map { if($_ eq '-1'){ $_ = 'x'; } +"$_"} @{$s->{chordsinfo}->{$mchord}->{frets}});
  192         369  
  30         52  
  192         323  
  32         83  
89 32         193 $all_chords .= "![$mchord](https://chordgenerator.net/$mchord.png?p=$frets&s=2) ";
90            
91             }
92 10         47 push(@s, $all_chords);
93 10         30 push(@s, "");
94             }
95 10         64 push(@s, elt_handler($s->{body}));
96 10         98 return \@s;
97             }
98              
99             sub line_default {
100 20     20 0 47 my ( $lineobject, $ref_lineobjects ) = @_;
101 20         45 return "";
102             }
103             $line_routines{line_default} = \&line_default;
104              
105             sub chord {
106 102     102 0 209 my ( $c ) = @_;
107 102 100       323 return "" unless length($c);
108 77 100       250 return $c->key if $c->info->is_annotation;
109 76         216 $text_layout->set_markup($c->chord_display);
110 76         5149 return $text_layout->render;
111             }
112              
113             sub md_textline{
114 32     32 0 58 my ( $songline ) = @_;
115 32         85 my $empty = $songline;
116 32         39 my $textline = $songline;
117 32         46 my $nbsp = "\x{00A0}"; #unicode for nbsp sign
118 32 100       136 if($empty =~ /^\s+/){ # starts with spaces
119 14         79 $empty =~ s/^(\s+).*$/$1/; # not the elegant solution - but working - replace all spaces in the beginning of a line
120 14         34 my $replaces = $empty; #with a nbsp symbol as the intend tend to be intentional
121 14         66 $replaces =~ s/\s/$nbsp/g;
122 14         85 $textline =~ s/$empty/$replaces/;
123             }
124 32         95 $textline = $textline." "; # append two spaces to force linebreak in Markdown
125 32         129 return $textline;
126             }
127              
128             sub line_songline {
129 67     67 0 134 my ( $elt ) = @_;
130 67         111 my $t_line = "";
131 134         2244 my @phrases = map { $text_layout->set_markup($_); $text_layout->render }
  134         6421  
132 67         104 @{ $elt->{phrases} };
  67         151  
133              
134 67 50 0     2042 if ( $lyrics_only or
      33        
      33        
135             $single_space && ! ( $elt->{chords} && join( "", map { $_->raw } @{ $elt->{chords} } ) =~ /\S/ )
136             ) {
137 0         0 $t_line = join( "", @phrases );
138 0         0 return md_textline($cp.$t_line);
139             }
140              
141 67 100       760 unless ( $elt->{chords} ) { # i guess we have a line with no chords now...
142 32         108 return ($cp. md_textline( join( " ", @phrases )) );
143             }
144            
145 35 50       119 if ( my $f = $::config->{settings}->{'inline-chords'} ) {
146 0 0       0 $f = '[%s]' unless $f =~ /^[^%]*\%s[^%]*$/;
147 0         0 $f .= '%s';
148 0         0 foreach ( 0..$#{$elt->{chords}} ) {
  0         0  
149             $t_line .= sprintf( $f,
150 0         0 chord( $elt->{chords}->[$_]->raw ),
151             $phrases[$_] );
152             }
153 0         0 return ( md_textline($cp.$t_line) );
154             }
155              
156 35         287 my $c_line = "";
157 35         62 foreach ( 0..$#{$elt->{chords}} ) {
  35         158  
158 102         254 $c_line .= chord( $elt->{chords}->[$_] ) . " ";
159 102         2530 $t_line .= $phrases[$_];
160 102         275 my $d = length($c_line) - length($t_line);
161 102 100       263 $t_line .= "-" x $d if $d > 0;
162 102 100       406 $c_line .= " " x -$d if $d < 0;
163             } # this looks like setting the chords above the words.
164              
165 35         348 s/\s+$// for ( $t_line, $c_line );
166              
167             # main problem in markdown - a fixed position is only available in "Code escapes" so weather to set
168             # a tab or a double backticks (``) - i tend to the tab - so all lines with tabs are "together"
169 35 50       106 if ($c_line ne ""){ # Block-lines are not replacing initial spaces - as the are "code"
170 35         105 $t_line = $cp.$t_line." ";
171 35         87 $c_line = $cp.$c_line." ";
172             }
173             else{
174 0         0 $t_line = md_textline($cp.$t_line);
175             }
176 35 50       120 return $chords_under
177             ? ( $t_line, $c_line )
178             : ( $c_line, $t_line );
179             }
180             $line_routines{line_songline} = \&line_songline;
181              
182             sub line_newpage {
183 3     3 0 10 return "--------------- \n";
184             }
185             $line_routines{line_newpage} = \&line_newpage;
186              
187             sub line_empty {
188 0     0 0 0 return "$cp";
189             }
190             $line_routines{line_empty} = \&line_empty;
191              
192             sub line_comment {
193 19     19 0 45 my ( $elt ) = @_; # Template for comment?
194 19         30 my @s;
195 19         42 my $text = $elt->{text};
196 19 50       55 if ( $elt->{chords} ) {
197 0         0 $text = "";
198 0         0 for ( 0..$#{ $elt->{chords} } ) {
  0         0  
199             $text .= "[" . $elt->{chords}->[$_]->raw . "]"
200 0 0       0 if $elt->{chords}->[$_] ne "";
201 0         0 $text .= $elt->{phrases}->[$_];
202             }}
203 19 50       66 if ($elt->{type} =~ /italic$/) {
204 0         0 $text = "*" . $text . "* ";
205             }
206 19         73 push(@s, "> $text ");
207 19         51 return @s;
208             }
209             $line_routines{line_comment} = \&line_comment;
210              
211             sub line_comment_italic {
212 3     3 0 22 my ( $lineobject ) = @_; # Template for comment?
213 3         19 return "> *". $lineobject->{text} ."*";;
214             }
215             $line_routines{line_comment_italic} = \&line_comment_italic;
216              
217              
218             sub line_image {
219 0     0 0 0 my ( $elt ) = @_;
220 0         0 return "![](".$elt->{uri}.")";
221             }
222             $line_routines{line_image} = \&line_image;
223              
224             sub line_colb {
225 3     3 0 12 return "\n\n\n";
226             }
227             $line_routines{line_colb} = \&line_colb;
228              
229             sub body_has_chords{
230 26     26 0 55 my ( $elts ) = @_; # reference to array
231 26         38 my $has_chord = 0; # default false has no chords
232 26         38 foreach my $elt (@{ $elts }) {
  26         55  
233 51 100       133 if ($elt->{type} eq 'songline'){
234 50 100 66     124 if ((defined $elt->{chords}) && (scalar @{$elt->{chords}} > 0 )){
  18         57  
235 18         39 $has_chord = 1;
236 18         76 return $has_chord;
237             }}
238             }
239 8         37 return $has_chord;
240             }
241             sub line_chorus {
242 3     3 0 9 my ( $lineobject ) = @_; #
243 3         7 my @s;
244 3 100       13 $cp = (body_has_chords($lineobject->{body})) ? "\t" : ""; # Verbatim on Verse/Chorus because Chords are present
245 3         10 push(@s, "**Chorus**");
246 3         7 push(@s, "");
247 3         14 push(@s, elt_handler($lineobject->{body}));
248             # push(@s, "\x{00A0} "); # nbsp
249 3         11 push(@s, "--------------- \n");
250 3         15 return @s;
251             }
252             $line_routines{line_chorus} = \&line_chorus;
253              
254             sub line_verse {
255 23     23 0 61 my ( $lineobject ) = @_; #
256 23         34 my @s;
257 23 100       69 $cp = (body_has_chords($lineobject->{body})) ? "\t" : ""; # Verbatim on Verse/Chorus because Chords are present
258 23         66 push(@s, elt_handler($lineobject->{body}));
259 23         57 push(@s, "");
260             # push(@s, "\x{00A0} "); # nbsp
261 23         84 return @s;
262             }
263             $line_routines{line_verse} = \&line_verse;
264              
265             sub line_set { # potential comments in fe. Chorus or verse or .... complicated handling - potential contextsensitiv.
266 11     11 0 37 my ( $elt ) = @_;
267 11 50       66 if ( $elt->{name} eq "lyrics-only" ) {
    50          
268             $lyrics_only = $elt->{value}
269 0 0       0 unless $lyrics_only > 1;
270             }
271             # Arbitrary config values.
272             elsif ( $elt->{name} =~ /^(text\..+)/ ) {
273 0         0 my @k = split( /[.]/, $1 );
274 0         0 my $cc = {};
275 0         0 my $c = \$cc;
276 0         0 foreach ( @k ) {
277 0         0 $c = \($$c->{$_});
278             }
279 0         0 $$c = $elt->{value};
280 0         0 $config->augment($cc);
281 0         0 upd_config();
282             }
283 11         26 return "";
284             }
285             $line_routines{line_set} = \&line_set;
286              
287             sub line_tabline {
288 0     0 0 0 my ( $lineobject ) = @_;
289 0         0 return "\t".$lineobject->{text};
290             }
291             $line_routines{line_tabline} = \&line_tabline;
292              
293             sub line_tab {
294 0     0 0 0 my ( $lineobject ) = @_;
295 0         0 my @s;
296 0         0 push(@s, "**Tabulatur** "); #@todo
297 0         0 push(@s, "");
298 0         0 push(@s, map { "\t".$_ } elt_handler($lineobject->{body}) ); #maybe this need to go for code markup as well´?
  0         0  
299 0         0 return @s;
300             }
301             $line_routines{line_tab} = \&line_tab;
302              
303             sub line_grid {
304 2     2 0 8 my ( $lineobject ) = @_;
305 2         6 my @s;
306 2         6 push(@s, "**Grid** ");
307 2         4 push(@s, "");
308 2         10 push(@s, elt_handler($lineobject->{body}));
309             # push(@s, "\x{00A0} ");
310 2         5 push(@s, "");
311 2         9 return @s;
312             }
313             $line_routines{line_grid} = \&line_grid;
314              
315             sub line_gridline {
316 6     6 0 16 my ( $elt ) = @_;
317 6         10 my @a = @{ $elt->{tokens} };
  6         22  
318 6         15 @a = map { $_->{class} eq 'chord'
319             ? $_->{chord}->raw
320 78 100       198 : $_->{symbol} } @a;
321 6         37 return "\t".join("", @a);
322             }
323             $line_routines{line_gridline} = \&line_gridline;
324              
325             sub elt_handler {
326 38     38 0 76 my ( $elts ) = @_; # reference to array
327 38         78 my $cref; #command reference to subroutine
328 38         60 my $init_context = 1;
329 38         67 my $ctx = "";
330              
331 38         56 my @lines;
332 38         83 my $last_type='';
333 38         60 foreach my $elt (@{ $elts }) {
  38         71  
334 160 100 100     506 if (($elt->{type} eq 'verse') && ($last_type =~ /comment/)){
335 10         24 push(@lines, "");
336             }
337             # Gang of Four-Style - sort of command pattern
338 160         334 my $sub_type = "line_".$elt->{type}; # build command "line_"
339 160 100       404 if (defined $line_routines{$sub_type}) {
340 140         271 $cref = $line_routines{$sub_type}; #\&$sub_type; # due to use strict - we need to get an reference to the command
341 140         390 push(@lines, &$cref($elt)); # call line with actual line-object
342             }
343             else {
344 20         63 push(@lines, line_default($elt)); # default = empty line
345             }
346 160         707 $last_type = $elt->{type};
347             }
348 38         233 return @lines;
349             }
350              
351             #################
352              
353             # package Text::Layout::Text;
354              
355             # use parent 'Text::Layout';
356              
357             # # Eliminate warning when HTML backend is loaded together with Text backend.
358             # no warnings 'redefine';
359              
360             # sub new {
361             # my ( $pkg, @data ) = @_;
362             # my $self = $pkg->SUPER::new;
363             # $self;
364             # }
365              
366             # sub render {
367             # my ( $self ) = @_;
368             # my $res = "";
369             # foreach my $fragment ( @{ $self->{_content} } ) {
370             # next unless length($fragment->{text});
371             # $res .= $fragment->{text};
372             # }
373             # $res;
374             # }
375              
376              
377             1;
378             # @todo
379             # sub line_rechorus {
380             # my ( $lineobject ) = @_;
381             # if ( $rechorus->{quote} ) {
382             # unshift( @elts, @{ $elt->{chorus} } );
383             # }
384             # elsif ( $rechorus->{type} && $rechorus->{tag} ) {
385             # push( @s, "{".$rechorus->{type}.": ".$rechorus->{tag}."}" );
386             # }
387             # else {
388             # push( @s, "{chorus}" );
389             # }
390             # }
391              
392             # sub line_control {
393             # my ( $lineobject ) = @_;
394             # }