use strict; use warnings; use CGI (); use URI::Escape; use Text::CSV; use Data::Dumper; use constant DATA_FILE => 'Monsters.csv'; use constant ATTRIBUTES => ( 'Climate/Terrain', 'Frequency', 'Organization', 'Activity cycle', 'Diet', 'Intelligence', 'Treasure', 'Alignment', 'No. Appearing', 'Armor Class', 'Movement', 'Hit Dice', 'THAC0', 'No. of Attacks', 'Damage/Attack', 'Special Attacks', 'Special Defenses', 'Magic Resistance', 'Size', 'Morale', 'XP Value', ); use constant SECTIONS => ( 'Appearance', 'Combat', 'Habitat/Society', 'Ecology', 'Variants', 'Note', ); use constant FIELDS => ( 'Monster', ATTRIBUTES, SECTIONS, ); use constant SUMMARY_FIELDS => ( 'Monster', 'Climate/Terrain', 'Frequency', 'Organization', 'Activity cycle', 'Diet', 'Intelligence', 'Treasure', 'Alignment', 'No. Appearing', 'Hit Dice', ); my $q = CGI->new; my @monsters = $q->param( 'name' ); print @monsters == 1 ? monster_detail($q, $monsters[0]) : monster_summary($q, @monsters ); # -------------------------------- sub page_start { # Generate the standard HTML for the start of a page. # Returns html text. my $q = shift; # CGI Object my $title = shift; # Title of the page. my $html = < $title

$title

END_HTML return $html; } sub page_end { # Generate the standard HTML for the end of a page. # Returns html text. my $q = shift; # CGI Object my $url = $q->url(-relative=>1); my $html = <Monster List

END_HTML return $html; } sub monster_detail { # Format a monster detail page # Returns html text. my $q = shift; # CGI object my $monster = shift; # Name of monster to display my $monster_data = load_monsters( $monster ); return error_page( $q, "'$monster': Monster not found.") unless @$monster_data; my $html = join '', $q->header, page_start($q, $monster_data->[0]{Monster} ), gen_detail( $monster_data->[0], [ ATTRIBUTES ], [ SECTIONS ] ), page_end( $q ); return $html; } sub monster_summary { # Format a monster summary page. # Returns html text. my $q = shift; # CGI object my @monsters = @_; # List of monsters to summarize. my $monster_data = load_monsters( @monsters ); return error_page( $q, "No matching monsters found (@monsters)." ) unless @$monster_data; my $html = join '', $q->header, page_start($q, 'Monster Summary'), gen_table( $monster_data, [ SUMMARY_FIELDS ], { Monster => sub { my ($attr, $item) = @_; my $name = $item->{$attr}; my $esc = uri_escape( $name ); return "$name"; }, }), page_end( $q ); return $html; } =head3 load_monsters Accepts a list of monster names to load. If no list is provided, all monsters will be loaded from the data file. Returns an array ref containing hash refs, each containing data from one monster. =cut sub load_monsters { my %wanted_monster; @wanted_monster{@_} = (); my $csv = Text::CSV_XS->new ({ binary => 1, quote_char => '~', sep_char => '|', }) or die "Error creating CSV parser: ".Text::CSV->error_diag; open my $fh, "<:encoding(utf8)", DATA_FILE or die "Error opening data file: $!"; my @monsters; while (my $row = $csv->getline ($fh)) { my %monster; @monster{ FIELDS() } = @$row; push @monsters, \%monster if !%wanted_monster || exists $wanted_monster{ $monster{Monster} }; } $csv->eof or $csv->error_diag (); close $fh; return \@monsters; } sub gen_table { # Generate monster summary table. # Returns html text. my $data = shift; # Array of Monster hashes my $fields = shift; # Array of fields to use my $formatter = shift || {}; # Hash of subs used to preprocess fields my $html = ''; $html .= join '', map "", @$fields; $html .= ''; for my $item ( @$data ) { $html .= ''; $html .= join '', map "", map { exists $formatter->{$_} ? $formatter->{$_}( $_, $item ) : $item->{$_}; } @$fields; $html .= ''; } $html .= '
$_
$_
'; } sub gen_detail { # Generate monster detail view # Returns html text. my $data = shift; my $attr = shift; my $sections = shift; my $html = '
'; $html .= join '', map "
$_
$data->{$_}
", @$attr; $html .= '
'; for my $section ( @$sections ) { my @paras = split /\\/, $data->{$section}; $html .= "

$section

"; $html .= join '', map "

$_

", @paras; } return $html; }