#!/usr/bin/perl
sub
new {
my
(
$pkg
) =
@_
;
bless
{
songs
=> [ App::Music::ChordPro::Song->new ] },
$pkg
;
}
my
$def_context
=
""
;
my
$in_context
=
$def_context
;
my
$xpose
;
my
@used_chords
;
my
%used_chords
;
my
$re_meta
;
my
$diag
;
sub
parsefile {
my
(
$self
,
$filename
,
$options
) =
@_
;
my
$fh
;
if
(
ref
(
$filename
) ) {
my
$data
= encode(
"UTF-8"
,
$$filename
);
$filename
=
"__STRING__"
;
open
(
$fh
,
'<'
, \
$data
)
or croak(
"$filename: $!\n"
);
}
else
{
open
(
$fh
,
'<'
,
$filename
)
or croak(
"$filename: $!\n"
);
}
push
( @{
$self
->{songs} }, App::Music::ChordPro::Song->new )
if
exists
(
$self
->{songs}->[-1]->{body});
$self
->{songs}->[-1]->{structure} =
"linear"
;
$xpose
=
$options
->{transpose};
@used_chords
= ();
%used_chords
= ();
App::Music::ChordPro::Chords::reset_song_chords();
$diag
->{
format
} =
$options
->{diagformat}
|| $::config->{diagnostics}->{
format
};
$diag
->{file} =
$filename
;
if
( $::config->{metadata}->{
keys
} ) {
$re_meta
=
'^('
.
join
(
'|'
,
map
{
quotemeta
} @{$::config->{metadata}->{
keys
}} )
.
')$'
;
$re_meta
=
qr/$re_meta/
;
}
else
{
undef
$re_meta
;
}
while
( <
$fh
> ) {
s/[\r\n]+$//;
$diag
->{line} = $.;
$diag
->{orig} =
$_
;
my
$line
;
if
(
$options
->{encoding} ) {
$line
= decode(
$options
->{encoding},
$_
, 1 );
}
else
{
eval
{
$line
= decode(
"UTF-8"
,
$_
, 1 ) };
$line
= decode(
"iso-8859-1"
,
$_
)
if
$@;
}
$_
=
$line
;
next
if
/^
s/'/\x{2019}/g;
if
( /\{(.*)\}\s*$/ ) {
$options
->{_legacy}
?
$self
->global_directive( $1, 1 )
:
$self
->directive($1);
next
;
}
if
(
$in_context
eq
"tab"
) {
$self
->add(
type
=>
"tabline"
,
text
=>
$_
);
next
;
}
if
(
$in_context
eq
"grid"
) {
$self
->add(
type
=>
"gridline"
,
$self
->decompose_grid(
$_
) );
next
;
}
if
( /\S/ ) {
$self
->add(
type
=>
"songline"
,
$self
->decompose(
$_
) );
}
else
{
$self
->add(
type
=>
"empty"
);
}
}
my
$showgrids
;
if
(
exists
(
$self
->{songs}->[-1]->{settings}->{showgrids} ) ) {
$showgrids
=
$self
->{songs}->[-1]->{settings}->{showgrids};
$showgrids
&&= $::config->{chordgrid}->{show} ||
"all"
;
}
else
{
$showgrids
= $::config->{chordgrid}->{show};
}
if
(
$showgrids
) {
if
(
$showgrids
eq
"user"
) {
@used_chords
=
grep
{ safe_chord_info(
$_
)->{origin} == 1 }
@used_chords
;
}
elsif
(
$showgrids
eq
"all"
) {
}
else
{
@used_chords
= ();
}
if
( $::config->{chordgrid}->{sorted} ) {
@used_chords
=
sort
App::Music::ChordPro::Chords::chordcompare
@used_chords
;
}
$self
->add(
type
=>
"chord-grids"
,
origin
=>
"song"
,
chords
=> [
@used_chords
] );
}
return
1;
}
sub
add {
my
$self
=
shift
;
push
( @{
$self
->{songs}->[-1]->{body}},
{
context
=>
$in_context
,
@_
} );
}
sub
safe_chord_info {
my
(
$c
) =
@_
;
my
$info
= App::Music::ChordPro::Chords::chord_info(
$c
);
return
$info
|| {
origin
=> 0 };
}
sub
chord {
my
(
$c
) =
@_
;
return
$c
unless
length
(
$c
);
my
$parens
=
$c
=~ s/^\((.*)\)$/$1/;
if
(
exists
$used_chords
{
$c
} ) {
return
$parens
?
"($used_chords{$c})"
:
$used_chords
{
$c
};
}
my
$info
= App::Music::ChordPro::Chords::chord_info(
$c
);
unless
(
$info
) {
do_warn(
"Unknown chord: $c\n"
);
$info
= App::Music::ChordPro::Chords::add_unknown_chord(
$c
)
if
$::config->{chordgrid}->{auto};
}
my
$xc
= App::Music::ChordPro::Chords::transpose(
$c
,
$xpose
);
if
(
$xc
) {
$used_chords
{
$c
} =
$xc
;
}
else
{
$xc
=
$c
;
}
push
(
@used_chords
,
$xc
)
if
$info
;
return
$parens
?
"($xc)"
:
$xc
;
}
sub
cxpose {
my
(
$t
) =
@_
;
$t
=~ s/\[(.+?)\]/chord($1)/ge;
return
$t
;
}
sub
decompose {
my
(
$self
,
$line
) =
@_
;
$line
=~ s/\s+$//;
my
@a
=
split
(/(\[.*?\])/,
$line
, -1);
die
(
"Illegal line $.:\n$_\n"
)
unless
@a
;
if
(
@a
== 1 ) {
return
(
phrases
=> [
$line
] );
}
shift
(
@a
)
if
$a
[0] eq
""
;
unshift
(
@a
,
'[]'
)
if
$a
[0] !~ /^\[/;
my
@phrases
;
my
@chords
;
while
(
@a
) {
my
$t
=
shift
(
@a
);
$t
=~ s/^\[(.*)\]$/$1/;
push
(
@chords
, chord(
$t
));
push
(
@phrases
,
shift
(
@a
));
}
return
(
phrases
=> \
@phrases
,
chords
=> \
@chords
);
}
sub
decompose_grid {
my
(
$self
,
$line
) =
@_
;
$line
=~ s/^\s+//;
$line
=~ s/\s+$//;
my
$rest
;
my
$orig
;
if
(
$line
=~ /(.*\|\S*)\s([^\|]*)$/ ) {
$line
= $1;
$rest
= cxpose(
$orig
= $2 );
}
my
@tokens
=
map
{ chord(
$_
) }
split
(
' '
,
$line
);
return
(
tokens
=> \
@tokens
,
$rest
? (
comment
=>
$rest
,
orig
=>
$orig
) : () );
}
sub
dir_split {
my
(
$d
) =
@_
;
$d
=~ s/^[: ]+//;
$d
=~ s/\s+$//;
my
$dir
=
lc
(
$d
);
my
$arg
=
""
;
if
(
$d
=~ /^(.*?)[: ]\s*(.*)/ ) {
(
$dir
,
$arg
) = (
lc
($1), $2 );
}
$dir
=~ s/[: ]+$//;
(
$dir
,
$arg
);
}
sub
directive {
my
(
$self
,
$d
) =
@_
;
my
(
$dir
,
$arg
) = dir_split(
$d
);
if
(
$dir
eq
"soc"
) {
$dir
=
"start_of_chorus"
}
elsif
(
$dir
eq
"sot"
) {
$dir
=
"start_of_tab"
}
elsif
(
$dir
eq
"eoc"
) {
$dir
=
"end_of_chorus"
}
elsif
(
$dir
eq
"eot"
) {
$dir
=
"end_of_tab"
}
if
(
$dir
=~ /^start_of_(\w+)\s*(.*)$/ ) {
do_warn(
"Already in "
.
ucfirst
(
$in_context
) .
" context\n"
)
if
$in_context
;
$in_context
= $1;
my
$par
= $2;
if
( $1 eq
"grid"
&&
$par
&&
$par
=~ /^(\d+)(?:x(\d+))?$/ ) {
do_warn(
"Invalid grid params: $par (must be non-zero)"
),
return
unless
$1;
$self
->add(
type
=>
"control"
,
name
=>
"gridparams"
,
value
=> [ $1, $2 ] );
}
else
{
do_warn(
"Garbage in start_of_$1: $par (ignored)\n"
)
if
$par
;
}
return
;
}
if
(
$dir
=~ /^end_of_(\w+)$/ ) {
do_warn(
"Not in "
.
ucfirst
($1) .
" context\n"
)
unless
$in_context
eq $1;
$in_context
=
$def_context
;
return
;
}
if
(
$dir
=~ /^chorus$/i ) {
$self
->add(
type
=>
"rechorus"
);
return
;
}
my
$cur
=
$self
->{songs}->[-1];
if
(
$dir
=~ /^(?:colb|column_break)$/i ) {
$self
->add(
type
=>
"colb"
);
return
;
}
if
(
$dir
=~ /^(?:new_page|np|new_physical_page|npp)$/i ) {
$self
->add(
type
=>
"newpage"
);
return
;
}
if
(
$dir
=~ /^(?:new_song|ns)$/i ) {
return
unless
$self
->{songs}->[-1]->{body};
push
(@{
$self
->{songs}}, App::Music::ChordPro::Song->new );
return
;
}
if
(
$dir
=~ /^(?:comment|c|highlight)$/ ) {
$self
->add(
type
=>
"comment"
,
text
=> cxpose(
$arg
),
orig
=>
$arg
);
return
;
}
if
(
$dir
=~ /^(?:comment_italic|ci)$/ ) {
$self
->add(
type
=>
"comment_italic"
,
text
=> cxpose(
$arg
),
orig
=>
$arg
);
return
;
}
if
(
$dir
=~ /^(?:comment_box|cb)$/ ) {
$self
->add(
type
=>
"comment_box"
,
text
=> cxpose(
$arg
),
orig
=>
$arg
);
return
;
}
if
(
$dir
eq
"image"
) {
my
@args
= shellwords(
$arg
);
my
$uri
;
my
%opts
;
foreach
(
@args
) {
if
( /^(width|height|border|center)=(\d+)$/i ) {
$opts
{
lc
($1)} = $2;
}
elsif
( /^(scale)=(\d(?:\.\d+)?)$/i ) {
$opts
{
lc
($1)} = $2;
}
elsif
( /^(center|border)$/i ) {
$opts
{
lc
(
$_
)} = 1;
}
elsif
( /^(src|uri)=(.+)$/i ) {
$uri
= $2;
}
elsif
( /^(title)=(.*)$/i ) {
$opts
{title} = $1;
}
elsif
( /^(.+)=(.*)$/i ) {
do_warn(
"Unknown image attribute: $1\n"
);
next
;
}
else
{
$uri
=
$_
;
}
}
unless
(
$uri
) {
do_warn(
"Missing image source\n"
);
return
;
}
$self
->add(
type
=>
"image"
,
uri
=>
$uri
,
opts
=> \
%opts
);
return
;
}
if
(
$dir
=~ /^(?:title|t)$/ ) {
$cur
->{title} =
$arg
;
push
( @{
$self
->{songs}->[-1]->{meta}->{title} },
$arg
);
return
;
}
if
(
$dir
=~ /^(?:subtitle|st)$/ ) {
push
(@{
$cur
->{subtitle}},
$arg
);
push
( @{
$self
->{songs}->[-1]->{meta}->{subtitle} },
$arg
);
return
;
}
if
(
$re_meta
&&
$dir
=~
$re_meta
) {
if
(
$xpose
&& $1 eq
"key"
) {
$arg
= App::Music::ChordPro::Chords::transpose(
$arg
,
$xpose
);
}
push
( @{
$self
->{songs}->[-1]->{meta}->{$1} },
$arg
);
return
;
}
if
(
$dir
=~ /^(meta)$/ ) {
if
(
$arg
=~ /([^ :]+)[ :]+(.*)/ ) {
my
$key
=
lc
$1;
my
$val
= $2;
if
(
$xpose
&&
$key
eq
"key"
) {
$val
= App::Music::ChordPro::Chords::transpose(
$val
,
$xpose
);
}
if
(
$re_meta
&&
$key
=~
$re_meta
) {
push
( @{
$self
->{songs}->[-1]->{meta}->{
$key
} },
$val
);
}
elsif
( $::config->{metadata}->{strict} ) {
do_warn(
"Unknown metadata item: $key"
);
}
else
{
push
( @{
$self
->{songs}->[-1]->{meta}->{
$key
} },
$val
);
}
}
else
{
do_warn(
"Incomplete meta directive: $d\n"
);
}
return
;
}
return
if
$self
->global_directive(
$d
, 0 );
do_warn(
"Unknown directive: $d\n"
)
unless
$d
=~ /^x_/;
return
;
}
sub
global_directive {
my
(
$self
,
$d
,
$legacy
) =
@_
;
my
(
$dir
,
$arg
) = dir_split(
$d
);
my
$cur
=
$self
->{songs}->[-1];
if
(
$dir
eq
"titles"
&&
$arg
=~ /^(left|right|center|centre)$/i ) {
$cur
->{settings}->{titles} =
lc
($1) eq
"centre"
?
"center"
:
lc
($1);
return
1;
}
if
(
$dir
eq
"columns"
&&
$arg
=~ /^(\d+)$/ ) {
$cur
->{settings}->{columns} =
$arg
;
return
1;
}
if
(
$dir
eq
"pagetype"
||
$dir
eq
"pagesize"
) {
$cur
->{settings}->{papersize} =
$arg
;
return
1;
}
if
(
$dir
=~ /^(?:grid|g)$/ ) {
$cur
->{settings}->{showgrids} = 1;
return
1;
}
if
(
$dir
=~ /^(?:no_grid|ng)$/ ) {
$cur
->{settings}->{showgrids} = 0;
return
1;
}
if
(
$d
=~ /^([-+])([-\w.]+)$/i ) {
return
if
$legacy
;
$self
->add(
type
=>
"set"
,
name
=> $2,
value
=> $1 eq
"+"
? 1 : 0,
);
return
1;
}
if
(
$dir
=~ /^\+([-\w.]+)$/ ) {
return
if
$legacy
;
$self
->add(
type
=>
"set"
,
name
=> $1,
value
=>
$arg
,
);
return
1;
}
if
(
$dir
=~ /^(text|chord|tab|grid|title|footer|toc)(font|size|colou?r)$/ ) {
my
$item
= $1;
my
$prop
= $2;
my
$value
=
$arg
;
return
if
$legacy
&& ! (
$item
=~ /^(text|chord|tab)$/ &&
$prop
=~ /^(font|size)$/ );
$prop
=
"color"
if
$prop
eq
"colour"
;
if
(
$prop
eq
"size"
) {
unless
(
$value
=~ /^\d+(?:\.\d+)?\%?$/ ) {
do_warn(
"Illegal value \"$value\" for $item$prop\n"
);
return
1;
}
}
if
(
$prop
=~ /^colou?r$/ ) {
my
$v
;
unless
(
$v
= get_color(
$value
) ) {
do_warn(
"Illegal value \"$value\" for $item$prop\n"
);
return
1;
}
$value
=
$v
;
}
$self
->add(
type
=>
"control"
,
name
=>
"$item-$prop"
,
value
=>
$prop
eq
'font'
?
$value
:
lc
(
$value
) );
return
1;
}
if
(
$d
=~ /^
(define|chord) [: ]+
([^: ]+) [: ] \s*
(?: base-fret \s+ (\d+) \s+ )?
frets
((?: \s+ [0-9---xX])*
\s+ [0-9---xX])?
\s*$
/xi
) {
my
$show
= $1 eq
"chord"
;
return
if
$legacy
&&
$show
;
my
@f
=
split
(
' '
, $4||
''
);
my
$ci
= {
name
=> $2,
base
=> $3 || 1,
frets
=> [
map
{
$_
=~ /^\d+/ ?
$_
: -1 }
@f
],
};
push
( @{
$cur
->{define}},
$ci
);
if
(
@f
) {
my
$res
=
App::Music::ChordPro::Chords::add_song_chord
(
$ci
->{name},
$ci
->{base} || 1,
$ci
->{frets} );
if
(
$res
) {
do_warn(
"Invalid chord: "
,
$ci
->{name},
": "
,
$res
,
"\n"
);
$show
= 0;
}
}
else
{
App::Music::ChordPro::Chords::add_unknown_chord(
$ci
->{name} );
}
if
(
$show
) {
if
(
$self
->{songs}->[-1]->{body}->[-1]->{type} eq
"chord-grids"
) {
push
( @{
$self
->{songs}->[-1]->{body}->[-1]->{chords} },
$ci
->{name} );
}
else
{
$self
->add(
type
=>
"chord-grids"
,
show
=>
"user"
,
origin
=>
"chord"
,
chords
=> [
$ci
->{name} ] );
}
}
return
1;
}
return
;
}
sub
structurize {
my
(
$self
) =
@_
;
foreach
my
$song
( @{
$self
->{songs} } ) {
$song
->structurize;
}
}
sub
get_color {
$_
[0];
}
sub
msg {
my
$m
=
join
(
""
,
@_
);
$m
=~ s/\n+$//;
my
$t
=
$diag
->{
format
};
$t
=~ s/\
%f
/
$diag
->{file}/g;
$t
=~ s/\
%n
/
$diag
->{line}/g;
$t
=~ s/\
%l
/
$diag
->{orig}/g;
$t
=~ s/\
%m
/
$m
/g;
$t
=~ s/\\n/\n/g;
$t
=~ s/\\t/\t/g;
$t
;
}
sub
do_warn {
warn
(msg(
@_
).
"\n"
);
}
sub
new {
my
(
$pkg
,
%init
) =
@_
;
bless
{
structure
=>
"linear"
,
settings
=> {},
%init
},
$pkg
;
}
sub
structurize {
my
(
$self
) =
@_
;
return
if
$self
->{structure} eq
"structured"
;
my
@body
;
my
$context
=
$def_context
;
foreach
my
$item
( @{
$self
->{body} } ) {
if
(
$item
->{type} eq
"empty"
&&
$item
->{context} eq
$def_context
) {
$context
=
$def_context
;
next
;
}
if
(
$context
ne
$item
->{context} ) {
push
(
@body
, {
type
=>
$context
=
$item
->{context},
body
=> [] } );
}
if
(
$context
) {
push
( @{
$body
[-1]->{body} },
$item
);
}
else
{
push
(
@body
,
$item
);
}
}
$self
->{body} = [
@body
];
$self
->{structure} =
"structured"
;
}
1;