App-Music-ChordPro

 view release on metacpan or  search on metacpan

lib/ChordPro/Output/LaTeX.pm  view on Meta::CPAN

#!/usr/bin/perl

package ChordPro::Output::LaTeX;
# Author: Johannes Rumpf / 2022
# relevant Latex packages - still using the template module would make it possible 
# to create any form of textual output. 
# delivered example will work with songs-package - any other package needed to be 
# evaluated / tested. But should work
# http://songs.sourceforge.net/songsdoc/songs.html
# https://www.ctan.org/pkg/songs
# https://www.ctan.org/pkg/guitar
# https://www.ctan.org/pkg/songbook
# https://www.ctan.org/pkg/gchords

use strict;
use warnings;
use ChordPro::Paths;
use ChordPro::Output::Common;
use Template;
use LaTeX::Encode;

our $CHORDPRO_LIBRARY;

my $single_space = 0;		# suppress chords line when empty
my $lyrics_only = 0;		# suppress all chords lines
my %line_routines = ();
my $gtemplate;
my $gcfg;

my $newpage_tag = "[% newpage_tag %]" ;
my $emptyline_tag = "[% emptyline_tag %]";
my $columnbreak_tag = "[% columnbreak_tag %]";
my $beginchorus_tag = "[% beginchorus_tag %]";
my $endchorus_tag = "[% endchorus_tag %]";
my $beginverse_tag = "[% beginverse_tag %]";
my $endverse_tag = "[% endverse_tag %]";
my $beginabc_tag = "[% beginabc_tag %]";
my $endabc_tag = "[% endabc_tag %]";
my $beginlilypond_tag = "[% beginlilypond_tag %]";
my $endlilypond_tag = "[% endlilypond_tag %]";
my $begingrid_tag = "[% begingrid_tag %]";
my $endgrid_tag = "[% endgrid_tag %]";
my $begintab_tag = "[% begintab_tag %]";
my $endtab_tag = "[% endtab_tag %]";
my $gchordstart_tag = "[% gchordstart_tag %]";
my $gchordend_tag = "[% gchordend_tag %]"; 
my $chorded_line = "[% chorded_line %]";
my $unchorded_line = "[% unchorded_line %]";
my $start_spaces_songline = "[% start_spaces_songline %]";
my $eol = "[% eol %]";

sub generate_songbook {
    my ( $self, $sb ) = @_;
    my @songs;
    $gcfg = $::config->{latex};
    $gtemplate = Template->new
      ({ INCLUDE_PATH => [@{$gcfg->{template_include_path}},
			  CP->findres("templates"), $CHORDPRO_LIBRARY],
        INTERPOLATE  => 1,
    }) || die "$Template::ERROR\n";

    foreach my $song ( @{$sb->{songs}} ) {
    	push( @songs, generate_song($song) );
    }
    my $songbook = '';
    my %vars = ();
    $vars{songs} = [@songs] ;
    $gtemplate->process($gcfg->{templates}->{songbook}, \%vars, $::options->{output} )
        || die $gtemplate->error();
 # i like it more to handle output through template module - but its possible to result it as array.
 #   return split(/\n/, $songbook); 
    $::options->{output} = '-';
    return [];
}

# some not implemented feature is requested. will be removed.
sub line_default {
    my ( $lineobject, $ref_lineobjects ) = @_;
    return "";
}
$line_routines{line_default} = \&line_default;

sub get_firstphrase{
   my ( $elts ) = @_; # reference to array
   my $line = "";
   foreach my $elt (@{ $elts }) {
       if($elt->{type} eq 'songline'){
          foreach my $phrase (@{$elt->{phrases}}){
                $line .=  $phrase;
            }
        return my_latex_encode($line);
       }
   }
}

sub line_songline {
    my ( $lineobject ) = @_;
    my $index = 0;
    my $line = "";
    my $chord = "";
    my $has_chord = 0;
    foreach my $phrase (@{$lineobject->{phrases}}){
        if(defined $lineobject->{chords}){
            if (@{$lineobject->{chords}}[$index] ne '' ){
                $chord = $gchordstart_tag.@{$lineobject->{chords}}[$index]->key .$gchordend_tag; #songbook format \\[chord]
                $has_chord = 1;
        }}
        $line .=  $chord . latex_encode($phrase);
        $index += 1; 
        $chord = "";
    }

	my $empty = $line;
    my $textline = $line;
    my $nbsp = $start_spaces_songline; #unicode for nbsp sign # start_spaces_songline
    if($empty =~ /^\s+/){ # starts with spaces
	    $empty =~ s/^(\s+).*$/$1/; # not the elegant solution - but working - replace all spaces in the beginning of a line
        my $replaces = $empty;  #with a nbsp symbol as the intend tend to be intentional
        $replaces =~ s/\s+/$nbsp/g;
        $textline =~ s/$empty/$replaces/;
    }
    $line = $textline;
    if ($has_chord) { $line = $chorded_line . $line; } else { $line = $unchorded_line . $line; }
    return $line.$eol;
}
$line_routines{line_songline} = \&line_songline;

sub line_newpage {
    my ( $lineobject ) = @_;
    return $newpage_tag;
}
$line_routines{line_newpage} = \&line_newpage;

sub line_empty {
    my ( $lineobject ) = @_;
    return $emptyline_tag;
}
$line_routines{line_empty} = \&line_empty;

sub line_comment {
    my ( $lineobject ) = @_; # Template for comment?
    my $vars = {
        comment => latex_encode($lineobject->{text})
    };
    my $comment = '';
    $gtemplate->process($gcfg->{templates}->{comment}, $vars, \$comment) || die $gtemplate->error();
    return $comment ;
}
$line_routines{line_comment} = \&line_comment;

sub line_comment_italic {
    my ( $lineobject ) = @_; # Template for comment?
    my $vars = {
        comment => "\\textit{". latex_encode($lineobject->{text}) ."}"
    };
    my $comment = '';
    $gtemplate->process($gcfg->{templates}->{comment}, $vars, \$comment) || die $gtemplate->error();
    return $comment;
}
$line_routines{line_comment_italic} = \&line_comment_italic;

sub line_image {
    my ( $lineobject ) = @_;
    my $image = '';
    $gtemplate->process($gcfg->{templates}->{image}, $lineobject, \$image)|| die $gtemplate->error();
    return $image;
}
$line_routines{line_image} = \&line_image;

sub line_colb {
    my ( $lineobject ) = @_; # Template for comment?
    return $columnbreak_tag;
}
$line_routines{line_colb} = \&line_colb;

sub line_chorus {
    my ( $lineobject ) = @_; #
   return $beginchorus_tag ."\n". 
          elt_handler($lineobject->{body}) . 
          $endchorus_tag . "\n";
}
$line_routines{line_chorus} = \&line_chorus;

sub line_verse {
    my ( $lineobject ) = @_; #
   return $beginverse_tag ."\n". 
        elt_handler($lineobject->{body}) 
        .$endverse_tag ."\n";
}
$line_routines{line_verse} = \&line_verse;

sub line_set { # potential comments in fe. Chorus or verse or .... complicated handling - potential contextsensitiv.
    my ( $lineobject ) = @_;
    return '';
}
$line_routines{line_set} = \&line_set;

sub line_tabline {
    my ( $lineobject ) = @_;
    return $lineobject->{text}.$eol;
}
$line_routines{line_tabline} = \&line_tabline;

sub line_tab {
    my ( $lineobject ) = @_;
    return $begintab_tag."\n". 
           elt_handler($lineobject->{body}) .
           $endtab_tag ."\n";
}
$line_routines{line_tab} = \&line_tab;

sub line_grid {
    my ( $lineobject ) = @_;
    return $begingrid_tag."\n".
           elt_handler($lineobject->{body}) 
           .$endgrid_tag ."\n";
}
$line_routines{line_grid} = \&line_grid;

sub line_gridline {
    my ( $lineobject ) = @_;
    my $line = '';
    if(defined $lineobject->{margin}){
        $line .= $lineobject->{margin}->{text} . "\t";
    }
    else {
        $line .= "\t\t";
    }
    foreach my $token (@{ $lineobject->{tokens} }){
        if ($token->{class} eq 'chord'){
            $line .= $token->{chord}->key;
        }
        else {
           $line .= $token->{symbol};
        }
    }
    if(defined $lineobject->{comment}){
        $line .= $lineobject->{comment}->{text};
    }
    return $line. $eol;
}
$line_routines{line_gridline} = \&line_gridline;

sub elt_handler {
    my ( $elts ) = @_; # reference to array
    my $cref; #command reference to subroutine

    my $lines = "";
    foreach my $elt (@{ $elts }) {
    # Gang of Four-Style - sort of command pattern 
    my $sub_type = "line_".$elt->{type}; # build command "line_<linetype>"
  #  if (exists &{$sub_type}) { #check if sub is implemented / maybe hash is -would be- faster...
     if (defined $line_routines{$sub_type}) {
        $cref = $line_routines{$sub_type}; #\&$sub_type; # due to use strict - we need to get an reference to the command 
        $lines .= &$cref($elt); # call line with actual line-object
    }
    else {
        $lines .= line_default($elt); # default = empty line
        
    }
  }
  return $lines;
}

sub my_latex_encode{
    my ( $val ) = @_;
    if ((ref($val) eq 'SCALAR') or ( ref($val) eq '' )) { return latex_encode($val); }
    if (ref($val) eq 'ARRAY'){
        my @array_return;
        foreach my $array_val (@{$val}){
            push(@array_return, my_latex_encode($array_val));
        }
        return \@array_return;
    }
    if (ref($val) eq 'HASH'){
        my %hash_return = ();
        foreach my $hash_key (keys( % {$val } )){
            $hash_return{$hash_key} = my_latex_encode( $val->{$hash_key} );
        }
        return \%hash_return;
    }
}

sub generate_song {
    my ( $s ) = @_;
    my %gtemplatatevar = ();

    if ( defined $s->{meta} ) {
		$gtemplatatevar{meta} = my_latex_encode($s->{meta});
    }
    $gtemplatatevar{meta}->{index} = get_firstphrase($s->{body}); # needs unstructured data - .. redesign?
    
  # asume songline a verse when no context is applied. # check https://github.com/ChordPro/chordpro/pull/211
  # Songbook needs to have a verse otherwise the chords-makro is not in the right context
	foreach my $item ( @{ $s->{body} } ) {
	if ( $item->{type} eq "songline" &&  $item->{context} eq '' ){
		$item->{context} = 'verse';
	}} # end of pull -- 
    $s->structurize; # removes empty lines 



( run in 0.817 second using v1.01-cache-2.11-cpan-8f98c5d2c55 )