File Coverage

blib/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   8 use strict;
  1         2  
  1         37  
11 1     1   5 use warnings;
  1         2  
  1         40  
12 1     1   10 use ChordPro::Output::Common;
  1         5  
  1         63  
13 1     1   504 use Text::Layout::Markdown;
  1         12869  
  1         3128  
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 39 $lyrics_only = $config->{settings}->{'lyrics-only'};
27 10         29 $chords_under = $config->{settings}->{'chords-under'};
28 10         39 $rechorus = $config->{text}->{chorus}->{recall};
29             }
30              
31             sub generate_songbook {
32 10     10 0 32 my ( $self, $sb ) = @_;
33 10         25 my @book;
34             # push(@book, "[TOC]"); # maybe https://metacpan.org/release/IMAGO/Markdown-TOC-0.01 to create a TOC?
35              
36 10         27 foreach my $song ( @{$sb->{songs}} ) {
  10         41  
37 10 50       37 if ( @book ) {
38 0 0       0 push(@book, "") if $options->{'backend-option'}->{tidy};
39             }
40 10         25 push(@book, @{generate_song($song)});
  10         36  
41 10         49 push(@book, "--------------- \n"); #Horizontal line between each song
42             }
43              
44 10         30 push( @book, "");
45              
46             # remove all double empty lines
47 10         22 my @new;
48 10         20 my $count = 0;
49 10         74 foreach (@book){
50 268 100       596 if ($_ =~ /.{1,}/ ){
51 173         270 push(@new, $_);
52 173         255 $count = 0
53             } else {
54 95 100       218 push(@new, $_) if $count == 0;
55 95         137 $count++;
56             }
57             }
58 10         51 \@new;
59             }
60              
61             sub generate_song {
62 10     10 0 28 my ( $s ) = @_;
63 10         5207 $act_song = $s;
64 10         66 $tidy = $options->{'backend-option'}->{tidy};
65 10         30 $single_space = $options->{'single-space'};
66              
67 10         55 upd_config();
68              
69             # asume songline a verse when no context is applied. # check https://github.com/ChordPro/chordpro/pull/211
70 10         25 foreach my $item ( @{ $s->{body} } ) {
  10         36  
71 155 100 100     421 if ( $item->{type} eq "songline" && $item->{context} eq '' ){
72 55         101 $item->{context} = 'verse';
73             }} # end of pull --
74            
75 10         114 $s->structurize;
76 10         28 my @s;
77 10 50       73 push(@s, "# " . $s->{title}) if defined $s->{title};
78 10 100       38 if ( defined $s->{subtitle} ) {
79 3         9 push(@s, map { +"## $_" } @{$s->{subtitle}});
  3         15  
  3         9  
80             }
81              
82 10 50       58 if ( $lyrics_only eq 0 ){
83 10         222 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         21 foreach my $mchord (@{$s->{chords}->{chords}}){
  10         45  
87             # replace -1 with 'x' - alternative '-'
88 32 100       59 my $frets = join("", map { if($_ eq '-1'){ $_ = 'x'; } +"$_"} @{$s->{chordsinfo}->{$mchord}->{frets}});
  192         350  
  30         47  
  192         368  
  32         86  
89 32         157 $all_chords .= "![$mchord](https://chordgenerator.net/$mchord.png?p=$frets&s=2) ";
90            
91             }
92 10         29 push(@s, $all_chords);
93 10         28 push(@s, "");
94             }
95 10         54 push(@s, elt_handler($s->{body}));
96 10         76 return \@s;
97             }
98              
99             sub line_default {
100 20     20 0 54 my ( $lineobject, $ref_lineobjects ) = @_;
101 20         50 return "";
102             }
103             $line_routines{line_default} = \&line_default;
104              
105             sub chord {
106 102     102 0 189 my ( $c ) = @_;
107 102 100       287 return "" unless length($c);
108 77 100       226 return $c->key if $c->info->is_annotation;
109 76         225 $text_layout->set_markup($c->chord_display);
110 76         4946 return $text_layout->render;
111             }
112              
113             sub md_textline{
114 32     32 0 57 my ( $songline ) = @_;
115 32         54 my $empty = $songline;
116 32         43 my $textline = $songline;
117 32         43 my $nbsp = "\x{00A0}"; #unicode for nbsp sign
118 32 100       107 if($empty =~ /^\s+/){ # starts with spaces
119 14         78 $empty =~ s/^(\s+).*$/$1/; # not the elegant solution - but working - replace all spaces in the beginning of a line
120 14         32 my $replaces = $empty; #with a nbsp symbol as the intend tend to be intentional
121 14         65 $replaces =~ s/\s/$nbsp/g;
122 14         86 $textline =~ s/$empty/$replaces/;
123             }
124 32         109 $textline = $textline." "; # append two spaces to force linebreak in Markdown
125 32         122 return $textline;
126             }
127              
128             sub line_songline {
129 67     67 0 124 my ( $elt ) = @_;
130 67         102 my $t_line = "";
131 134         2246 my @phrases = map { $text_layout->set_markup($_); $text_layout->render }
  134         6109  
132 67         92 @{ $elt->{phrases} };
  67         170  
133              
134 67 50 0     1968 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       729 unless ( $elt->{chords} ) { # i guess we have a line with no chords now...
142 32         104 return ($cp. md_textline( join( " ", @phrases )) );
143             }
144            
145 35 50       100 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         277 my $c_line = "";
157 35         57 foreach ( 0..$#{$elt->{chords}} ) {
  35         121  
158 102         249 $c_line .= chord( $elt->{chords}->[$_] ) . " ";
159 102         2405 $t_line .= $phrases[$_];
160 102         266 my $d = length($c_line) - length($t_line);
161 102 100       252 $t_line .= "-" x $d if $d > 0;
162 102 100       400 $c_line .= " " x -$d if $d < 0;
163             } # this looks like setting the chords above the words.
164              
165 35         350 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       109 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         93 $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 11 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 38 my ( $elt ) = @_; # Template for comment?
194 19         34 my @s;
195 19         41 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       61 if ($elt->{type} =~ /italic$/) {
204 0         0 $text = "*" . $text . "* ";
205             }
206 19         65 push(@s, "> $text ");
207 19         49 return @s;
208             }
209             $line_routines{line_comment} = \&line_comment;
210              
211             sub line_comment_italic {
212 3     3 0 9 my ( $lineobject ) = @_; # Template for comment?
213 3         12 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 50 my ( $elts ) = @_; # reference to array
231 26         43 my $has_chord = 0; # default false has no chords
232 26         44 foreach my $elt (@{ $elts }) {
  26         54  
233 51 100       114 if ($elt->{type} eq 'songline'){
234 50 100 66     125 if ((defined $elt->{chords}) && (scalar @{$elt->{chords}} > 0 )){
  18         64  
235 18         32 $has_chord = 1;
236 18         68 return $has_chord;
237             }}
238             }
239 8         28 return $has_chord;
240             }
241             sub line_chorus {
242 3     3 0 8 my ( $lineobject ) = @_; #
243 3         5 my @s;
244 3 100       12 $cp = (body_has_chords($lineobject->{body})) ? "\t" : ""; # Verbatim on Verse/Chorus because Chords are present
245 3         8 push(@s, "**Chorus**");
246 3         7 push(@s, "");
247 3         13 push(@s, elt_handler($lineobject->{body}));
248             # push(@s, "\x{00A0} "); # nbsp
249 3         10 push(@s, "--------------- \n");
250 3         16 return @s;
251             }
252             $line_routines{line_chorus} = \&line_chorus;
253              
254             sub line_verse {
255 23     23 0 46 my ( $lineobject ) = @_; #
256 23         39 my @s;
257 23 100       58 $cp = (body_has_chords($lineobject->{body})) ? "\t" : ""; # Verbatim on Verse/Chorus because Chords are present
258 23         62 push(@s, elt_handler($lineobject->{body}));
259 23         67 push(@s, "");
260             # push(@s, "\x{00A0} "); # nbsp
261 23         81 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 32 my ( $elt ) = @_;
267 11 50       54 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         24 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 7 my ( $lineobject ) = @_;
305 2         5 my @s;
306 2         4 push(@s, "**Grid** ");
307 2         5 push(@s, "");
308 2         6 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 10 my ( $elt ) = @_;
317 6         10 my @a = @{ $elt->{tokens} };
  6         24  
318 6         16 @a = map { $_->{class} eq 'chord'
319             ? $_->{chord}->raw
320 78 100       205 : $_->{symbol} } @a;
321 6         30 return "\t".join("", @a);
322             }
323             $line_routines{line_gridline} = \&line_gridline;
324              
325             sub elt_handler {
326 38     38 0 81 my ( $elts ) = @_; # reference to array
327 38         65 my $cref; #command reference to subroutine
328 38         58 my $init_context = 1;
329 38         74 my $ctx = "";
330              
331 38         55 my @lines;
332 38         60 my $last_type='';
333 38         65 foreach my $elt (@{ $elts }) {
  38         62  
334 160 100 100     463 if (($elt->{type} eq 'verse') && ($last_type =~ /comment/)){
335 10         21 push(@lines, "");
336             }
337             # Gang of Four-Style - sort of command pattern
338 160         327 my $sub_type = "line_".$elt->{type}; # build command "line_"
339 160 100       356 if (defined $line_routines{$sub_type}) {
340 140         219 $cref = $line_routines{$sub_type}; #\&$sub_type; # due to use strict - we need to get an reference to the command
341 140         337 push(@lines, &$cref($elt)); # call line with actual line-object
342             }
343             else {
344 20         64 push(@lines, line_default($elt)); # default = empty line
345             }
346 160         670 $last_type = $elt->{type};
347             }
348 38         221 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             # }