| Chess-PGN-Filter documentation | Contained in the Chess-PGN-Filter distribution. |
Chess::PGN::Filter - Perl extension for converting PGN files to other formats.
use Chess::PGN::Filter; filter(source => $pgn,filtertype => 'XML');
OR
my %substitutions = (
hsmyers => 'Myers, Hugh S (ID)',
);
my @exclude = qw(
WhiteElo
BlackElo
EventDate
);
filter(
source => $pgn,
filtertype => 'TEXT',
substitutions => \%substitutions,
nags => 'yes',
exclude => \@exclude,
);
OR
filter(
source => $pgn,
filtertype => 'DOM',
);
OR
$dom = filter(
source => $pgn,
filtertype => 'DOM',
verbose => 0,
);
This is a typical text in one side, different text out the otherside filter module. There are as of this writing, the following supported choices:
$VAR1 = {
'Tags' => {
'Site' => 'Boise (ID)',
'Event' => 'Cabin Fever Open',
'Round' => '1',
'ECO' => '?',
'Date' => '1997.??.??',
'White' => 'Barrett Curtis',
'Black' => 'Myers Hugh S',
'Result' => '1-0'
},
'Gametext' => [
{
'Movenumber' => '1',
'Epd' => 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3',
'Movetext' => 'e4'
},
{
'Movenumber' => '2',
'Epd' => 'rnbqkbnr/ppp1pppp/8/3p4/4P3/8/PPPP1PPP/RNBQKBNR w KQkq d6',
'Movetext' => 'd5'
},
{
'Movenumber' => '3',
'Epd' => 'rnbqkbnr/ppp1pppp/8/3pP3/8/8/PPPP1PPP/RNBQKBNR b KQkq -',
'Movetext' => 'e5'
},
{
'Movenumber' => '4',
'Comment' => 'Playing ...Bf5 before closing the c8-h3 diagonal has some positive features.',
'Epd' => 'rnbqkbnr/ppp2ppp/4p3/3pP3/8/8/PPPP1PPP/RNBQKBNR w KQkq -',
'Movetext' => 'e6'
},
{
'Movenumber' => '5',
'Epd' => 'rnbqkbnr/ppp2ppp/4p3/3pP3/3P4/8/PPP2PPP/RNBQKBNR b KQkq d3',
'Movetext' => 'd4'
},
{
'Movenumber' => '6',
'Comment' => 'Time to think like a Frenchie - c7-c5!',
'Epd' => 'r1bqkbnr/ppp2ppp/2n1p3/3pP3/3P4/8/PPP2PPP/RNBQKBNR w KQkq -',
'Movetext' => 'Nc6',
'Rav' => [
{
'Movenumber' => '6',
'Epd' => 'rnbqkbnr/pp3ppp/4p3/2ppP3/3P4/8/PPP2PPP/RNBQKBNR w KQkq c6',
'Movetext' => 'c5'
}
]
},
.
.
.
{
'Movenumber' => '29',
'Comment' => ' (Bxe5) Black could still kick for a while if he had played ...Bxe5.',
'Epd' => 'r1bq1rk1/2p1npb1/2n1p2P/pp1pP1p1/3P2P1/2P4Q/PP2BP2/RNB1K2R b KQ -',
'Movetext' => 'h6'
}
]
};
Briefly, the DOM is a multiply nested data structure of hashes and arrays. In a sort of outline form, it more or less follows this schematic:
The 'extra' comments have not yet been implemented. See the TODO list.
Owing to a dearth of imagination, there is but one exported routine in the module:
There are however, a small host of known keys for parameter_hash and they are as follows:
This is actually a hash reference where the hash reffered to has the form of (text_to_change => text_to_change_to). For instance:
my %substitutions = (
hsmyers => 'Myers, Hugh S (ID)'
);
as used in the SYNOPSIS example would expand my user name into a full version for any tag the former might occur in.
This is an array reference where the referent has the form of (tag_to_exclude_1..tag_to_exclude_n), i.e.:
my @exclude = qw(
WhiteElo
BlackElo
EventDate
);
again, as used in the SYNOPSIS example, this would eliminate the 'WhiteElo', 'BlackElo' and 'EventDate' tags from the .pgn file being processed.
Possible values are:
Following list shows font name, font designer. They are available from http://www.enpassant.dk/chess/fonteng.htm
Note -- not all fonts support all combinations of 'border', 'corner' and 'legend'. No warnings or errors will be generated by unsupported options, you get the best a font can do, no more!
None known; Unknown? Of course, though I try to be neat...
Hugh S. Myers
| Chess-PGN-Filter documentation | Contained in the Chess-PGN-Filter distribution. |
package Parms; use strict; use warnings; use Chess::PGN::EPD; use vars '$AUTOLOAD'; use Carp; sub new { my ($class,%arg) = @_; bless { _verbose => defined $arg{verbose} ? $arg{verbose} : 1, #default legacy verbose _filtertype => $arg{filtertype}, _source => $arg{source}, _fen => $arg{fen} || 'no', _position => $arg{position} || 'yes', _type => $arg{type} || ($arg{font} || 'marroquin'), _border => $arg{single} || 'single', _corner => $arg{square} || 'square', _legend => $arg{legend} || 'no', _size => $arg{size} || '5', _font => $arg{font} || 'Chess Kingdom', _ECO => $arg{ECO} || 'yes', _NIC => $arg{NIC} || 'no', _Opening => $arg{Opening} || 'yes', _substitutions => $arg{substitutions}, _exclude => $arg{exclude}, _comments => $arg{comments} || 'yes', _nags => $arg{nags} || 'yes', _ravs => $arg{ravs} || 'yes', _sticky => $arg{sticky} || 'yes', _autoround => $arg{autoround} || 'yes', _event => '', _site => '', _date => '', _round => '', }, $class; } sub AUTOLOAD { my ($self,$newval) = @_; $AUTOLOAD =~ /.*::get(_\w+)/ and return $self->{$1}; $AUTOLOAD =~ /.*::set(_\w+)/ and do { $self->{$1} = $newval; return }; $AUTOLOAD =~ /.*::if(_\w+)/ and return ($self->{$1} eq 'yes'); croak "No such method: $AUTOLOAD\n"; } sub DESTROY { } package Chess::PGN::Filter; use 5.006; use strict; use warnings; use Chess::PGN::Parse; use Chess::PGN::EPD; use Text::DelimMatch; use Data::Dumper; require Exporter; our @ISA = qw(Exporter); our @EXPORT = qw( &filter ); our $VERSION = '0.15'; sub filter { my $parameters = new Parms(@_); my $filtered; if ($parameters->get_filtertype() eq 'XML') { $filtered = filterXML(@_); } elsif ($parameters->get_filtertype() eq 'TEXT') { $filtered = filterTEXT(@_); } elsif ($parameters->get_filtertype() eq 'DOM') { $filtered = [ filterDOM(@_) ]; } else { die "Unknown filtertype: '$parameters->get_filtertype()' not supported.\n"; } if ($parameters->get_verbose()) { print $parameters->get_filtertype() eq 'DOM' ? Dumper(@$filtered) : $filtered; } else { return $filtered; } } sub filterDOM { my $parms = new Parms(@_); my $file = $parms->get_source(); my $filetext; { local $/ = undef; open(FILE,$file) or die "Couldn't open file:$file $!\n"; $filetext = <FILE>; close(FILE); } return getDOM($filetext); } sub filterTEXT { my $parms = new Parms(@_); my $file = $parms->get_source(); my @DOM; my $filetext; my $text = ''; { local $/ = undef; open(FILE,$file) or die "Couldn't open file:$file $!\n"; $filetext = <FILE>; close(FILE); } @DOM = getDOM($filetext); foreach (@DOM) { my $termination = ($_->{'Tags'}->{'Result'} =~ /^1-0|0-1|1\/2-1\/2|\*$/ ? $_->{'Tags'}->{'Result'} : '*'); my $movetext; my $move = 1; domExTags($parms,$_->{'Tags'}) if $parms->get_exclude(); domSticky($parms,$_->{'Tags'}) if $parms->if_sticky(); domAutoround($parms,$_->{'Tags'}) if $parms->if_autoround(); domTaxonomy($parms,$_->{'Gametext'},$_->{'Tags'}) if doTax($parms); foreach my $key ('Event','Site','Date','Round','White','Black','Result') { if ($_->{'Tags'}->{$key}) { if ($parms->get_substitutions()) { while (my ($one,$another) = each(%{$parms->get_substitutions()})) { if ($_->{'Tags'}->{$key} =~ /$one/) { $_->{'Tags'}->{$key} =~ s/$one/$another/; } } } $text .= "[$key \"$_->{'Tags'}->{$key}\"]\n"; delete($_->{'Tags'}->{$key}); } } foreach my $key (sort keys %{$_->{'Tags'}}) { $text .= "[$key \"$_->{'Tags'}->{$key}\"]\n"; } $text .= "\n"; $movetext = domTEXTGametext($parms,$move,$_->{'Gametext'}); $text .= join("\n",paragraph($movetext . $termination,78))."\n\n"; } return $text; } sub doTax { my $parms = shift; return ($parms->if_ECO() or $parms->if_NIC() or $parms->if_Opening()); } sub domExTags { my $parms = shift; my $tag = shift; my $array = $parms->get_exclude(); foreach (@$array) { if (exists($tag->{$_})) { delete($tag->{$_}); } } } sub domAutoround { my $parms = shift; my $tag = shift; if (exists($tag->{'Round'})) { my $value = $tag->{'Round'}; if ($value eq '' or $value eq '?') { my $round = $parms->get_round(); $tag->{'Round'} = ++$round; $parms->set_round($round); } else { $parms->set_round($value); } } } sub domSticky { my $parms = shift; my $tag = shift; if (exists($tag->{'Event'})) { my $value = $tag->{'Event'}; if ($value eq '' or $value eq '?') { $tag->{'Event'} = $parms->get_event(); } else { $parms->set_event($value); } } if (exists($tag->{'Site'})) { my $value = $tag->{'Site'}; if ($value eq '' or $value eq '?') { $tag->{'Site'} = $parms->get_site(); } else { $parms->set_site($value); } } if (exists($tag->{'Date'})) { my $value = $tag->{'Date'}; if ($value eq '' or $value eq '??.??.??') { $tag->{'Date'} = $parms->get_date(); } else { $parms->set_date($value); } } } sub domTaxonomy { my $parms = shift; my $gametext = shift; my $tag = shift; my @epd = reverse domEPD($gametext); if (exists($tag->{'ECO'})) { my $tax = $tag->{'ECO'}; if ($tax eq '?' or $tax eq '') { $tag->{'ECO'} = epdcode('ECO',\@epd) } } else { $tag->{'ECO'} = epdcode('ECO',\@epd) if $parms->if_ECO(); } if (exists($tag->{'NIC'})) { my $tax = $tag->{'NIC'}; if ($tax eq '?' or $tax eq '') { $tag->{'NIC'} = epdcode('NIC',\@epd) } } else { $tag->{'NIC'} = epdcode('NIC',\@epd) if $parms->if_NIC(); } if (exists($tag->{'Opening'})) { my $tax = $tag->{'Opening'}; if ($tax eq '?' or $tax eq '') { $tag->{'Opening'} = epdcode('Opening',\@epd) } } else { $tag->{'Opening'} = epdcode('Opening',\@epd) if $parms->if_Opening(); } } sub domTEXTGametext { my $parms = shift; my $move = shift; my $Gametext = shift; my $movetext = ''; foreach my $element (@{$Gametext}) { if ($element->{'Movenumber'} % 2) { $movetext .= "$move. "; } else { $move++; } $movetext .= $element->{'Movetext'} . " "; if ($element->{'Comment'}) { $movetext =~ s/\s$//; $movetext .= '(' . $element->{'Comment'} . ')' if $parms->if_comments(); } if ($element->{'Nag'}) { my $s = $element->{'Nag'}; if ($s) { if ($s < 6) { $movetext =~ s/\s$//; $movetext .= NAG($s); } else { $movetext =~ s/\s$//; $movetext .= "\$$s" if $parms->if_nags(); } } } if ($element->{'Rav'}) { my $ravtext = domTEXTGametext($parms,$move,$element->{'Rav'}); $ravtext =~ s/\s$//; $movetext =~ s/\s$//; $movetext .= "{$ravtext}"; } } return $movetext; } sub domEPD { my $Gametext = shift; my @epd; foreach my $element (@{$Gametext}) { push(@epd,$element->{'Epd'}); } return @epd; } sub paragraph { my $s = shift; my $n = shift; my $m = $n; while ($m < length($s)) { while(substr($s,$m,1) ne ' ') { $m--; } if (substr($s,$m - 1,1) eq '.') { $m--; next; } substr($s,$m,1) = '|'; $m += $n; } return split(/\|/,$s); } sub deLIMIT { my $t = shift; my $startdelim = shift; my $enddelim = shift; my $escape = shift; my $mc = new Text::DelimMatch($startdelim,$enddelim,$escape); my ($prefix,$match,$remainder) = $mc->match(' ' . $t . ' '); if ($match) { $match =~ s/^$startdelim//; $match =~ s/$enddelim$//; return ($prefix or '') . ($remainder or ''),$match; } else { return $t,''; } } sub parseComments { my $rcomments = shift; my $rravs = shift; my $rnags = shift; foreach (keys %$rcomments) { my $t = $rcomments->{$_}; my $NAG; my $RAV; my $COMMENT; ($t,$COMMENT) = deLIMIT($t,'\{','\}'); ($t,$RAV) = deLIMIT($t,'\(','\)','{}'); ($t,$NAG) = deLIMIT($t,'\$','\D'); if ($RAV) { $rravs->{$_} = $RAV; } if ($NAG and $NAG ne '') { $rnags->{$_} = $NAG; } if ($COMMENT) { $rcomments->{$_} = $COMMENT; } else { delete($rcomments->{$_}); } } } sub filterXML { my $parms = new Parms(@_); my $file = $parms->get_source(); my @DOM; my $filetext; my $text = ''; { local $/ = undef; open(FILE,$file) or die "Couldn't open file:$file $!\n"; $filetext = <FILE>; close(FILE); } @DOM = getDOM($filetext); $file =~ s/.pgn//; $file = uc($file); #----------------------------------------------------------------------------- $text .= <<"HEADER"; <?xml version="1.0" encoding="UTF-8"?> <?xml-stylesheet type="text/xsl" href="pgn.xsl"?> <!DOCTYPE CHESSGAMES SYSTEM "pgn.dtd"> <CHESSGAMES NAME="$file Games"> HEADER #----------------------------------------------------------------------------- $text .= dom2XML($parms,@DOM); $text .= "</CHESSGAMES>\n"; return $text; } sub dom2XML { my $parms = shift; my @DOM = @_; my $level = 0; my $result; my $text = ''; foreach (@DOM) { $text .= "\t<GAME>\n"; $text .= "\t\t<TAGLIST>\n"; foreach my $key ('Event','Site','Date','Round','White','Black','Result') { if ($_->{'Tags'}->{$key}) { if ($key eq 'Result') { $result = $_->{'Tags'}->{$key}; if ($result eq '1-0') { $result = 'WHITEWIN'; } elsif ($result eq '0-1') { $result = 'BLACKWIN'; } elsif ($result eq '1/2-1/2') { $result = 'DRAW'; } else { $result = 'UNKNOWN'; } $text .= "\t\t\t<Result GAMERESULT=\"$result\"/>\n"; } elsif ($key eq 'Date') { my @date = split(/\./,$_->{'Tags'}->{$key}); $text .= "\t\t\t<Date YEAR=\"$date[0]\" MONTH=\"$date[1]\" DAY=\"$date[2]\"/>\n"; } else { $text .= "\t\t\t<$key>$_->{'Tags'}->{$key}</$key>\n"; } delete($_->{'Tags'}->{$key}); } } foreach my $key (sort keys %{$_->{'Tags'}}) { $text .= "\t\t\t<$key>$_->{Tags}->{$key}</$key>\n"; } $text .= "\t\t</TAGLIST>\n"; $text .= dom2XMLGametext($parms,$level,$result,$_->{'Gametext'}); $text .= "\t</GAME>\n"; } return $text; } sub dom2XMLGametext { my $parms = shift; my $level = shift; my $result = shift; my $Gametext = shift; my $tabs = "\t" x 2 . "\t" x $level; my $text = ''; my $diagram = sub { my $element = shift; my @rows = epdstr( epd => $element, type => $parms->get_type(), border => $parms->get_border(), corner => $parms->get_corner(), legend => $parms->get_legend() ); my $text = ''; $text .= qq|$tabs\t<POSITION FONT="|.$parms->get_font().'" SIZE="'.($parms->get_size() - 2).qq|">\n|; foreach my $row (@rows) { $text .= "$tabs\t\t<ROW>$row</ROW>\n"; } $text .= "$tabs\t</POSITION>\n"; return $text; }; $text .= qq|$tabs<GAMETEXT LEVEL="$level">\n|; foreach my $element (@{$Gametext}) { $text .= "$tabs\t<MOVENUMBER>$element->{'Movenumber'}</MOVENUMBER>\n"; $text .= "$tabs\t<MOVE>$element->{'Movetext'}</MOVE>\n"; if ($element->{'Rav'}) { $text .= dom2XMLGametext($parms,$level + 1,'UNKNOWN',$element->{'Rav'}); } $text .= "$tabs\t<COMMENT>$element->{'Comment'}</COMMENT>\n" if $element->{'Comment'}; if ($element->{'Nag'}) { my $s = $element->{'Nag'}; if ($s eq '0' and ($parms->if_position() or $parms->get_position() eq 'nag')) { $text .= &$diagram($element->{'Epd'}); } else { $text .= "$tabs\t<NAG>$element->{'Nag'}</NAG>\n"; } } $text .= "$tabs\t<FENstr>$element->{'Epd'}</FENstr>\n" if $parms->if_fen(); } $text .= qq|$tabs\t<GAMETERMINATION GAMERESULT="$result"/>\n|; $text .= "$tabs</GAMETEXT>\n"; if ($level == 0 and ($parms->if_position() or $parms->get_position() eq 'end')) { chop($tabs); $text .= &$diagram(@{$Gametext}[-1]->{'Epd'}); } return $text; } sub getDOM { my $s = shift; my $pgn = new Chess::PGN::Parse undef, $s; my $games_ref = $pgn->read_all({save_comments => 'yes'}); my @DOM; foreach (@{$games_ref}) { my %comments; my %ravs; my %nags; my @moves; my @epd; my %tags; my %game; my @movelist; my @movesmade; my $position = 0; my $movenumber = 1; push(@DOM,\%game); foreach my $key (keys %{$_}) { my $ref = $_->{$key}; if (ref($ref)) { if ($key eq 'GameMoves') { @moves = @{$ref}; @epd = epdlist(@moves); } elsif ($key eq 'GameComments') { %comments = %{$ref}; parseComments(\%comments,\%ravs,\%nags); } } elsif ($ref) { if ($key ne 'Game') { $tags{$key} = $ref; } } } $game{'Tags'} = \%tags; $game{'Gametext'} = \@movelist; foreach (@moves) { my %move; my $ckey; $move{'Movetext'} = $_; $move{'Movenumber'} = $position; push(@movesmade,$_); if ($position % 2) { $ckey = "${movenumber}b"; $movenumber++; } else { $ckey = "${movenumber}w"; } $move{'Comment'} = $comments{$ckey} if (%comments and exists($comments{$ckey})); $move{'Nag'} = $nags{$ckey} if (%nags and exists($nags{$ckey})); if (%ravs and exists($ravs{$ckey})) { my $n = scalar(@movesmade) - 2; my @ravDOM = getDOM("[Result \"*\"]\n\n" . join(' ',@movesmade[0..$n++]) . " $ravs{$ckey}"); delete($ravDOM[0]->{'Tags'}); splice(@{$ravDOM[0]->{'Gametext'}},0,$n); $move{'Rav'} = $ravDOM[0]->{'Gametext'}; } $move{'Epd'} = $epd[$position++]; $move{'Movenumber'} = $position; push(@movelist,\%move); } } return @DOM; } 1; __END__