Beefy Boxes and Bandwidth Generously Provided by pair Networks
Just another Perl shrine

Seeing Perl in a new light: Epilog

by Lady_Aleena (Curate)
on Sep 19, 2010 at 23:17 UTC ( #860750=note: print w/replies, xml ) Need Help??

in reply to Seeing Perl in a new light

It is now almost 18 months later, and I am happy with the results of those months. My goal of getting my site off of server side includes is so very much closer to completion. That isn't to say that I don't have a long way to go, but thanks to the help of my fellow monks, I have made a lot of progress.

My blank slate is now filled with code that I use for almost everything I write. I will always be tweaking and looking for small ways to make things better, so I am including the four modules which make up the back bone of my site.

I have a lot of people that hopefully know how thankful I am for all of their help.


When I want a web page printed for my site, this comes first.

package Base::HTML; use strict; use warnings; use base 'Exporter'; our @EXPORT_OK = qw(start_html end_html print_story print_select); use CGI::Carp qw(fatalsToBrowser warningsToBrowser); use Cwd; use File::Basename; use File::Find; use HTML::Entities qw(encode_entities); use List::Util qw(first); use URI::Encode qw(uri_encode); use lib ".."; use Base::Menu qw(push_file print_menu); use Base::Nifty qw(get_hash article_sort name_sort my_sort line); use Base::Roots qw(root_directory get_data root print_styles email fil +e_text); my $full_path = cwd.'/'.basename($0); my $rootdir = root_directory; my $rootlink = root('link'); my $heading = file_text(basename($0)) ne 'index' ? file_text(basename( +$0)) : 'Home'; my $current_directory = cwd; $current_directory =~ s!$rootdir(/|)!!; my $title = join(' - ',((root('name'),map(ucfirst,split(/\//,$current_ +directory))),$heading)); $title =~ s/_/ /g; my $user = root('user'); my %off_site; my %off_site_data = ( csv => get_data('Base','other_sites.csv'), headings => [qw(site link)], ); get_hash(\%off_site,\%off_site_data); #Once the site is fully coded, the directory exclusions will be shorte +r. my %exclusions = ( directories => [qw(cgi-bin error files games personal), "Fiction/Ero +tic_fiction/unfinished", "Movies/Movie miscellany", "role_playing/X-M +en"], file_types => [qw(pl shtml)], file_names => [qw(ssi form sitemap menu textbox thankyou evansstore) +], ); my @files; sub wanted { my $directories = join '|', @{$exclusions{directories}}; my $file_types = join '|', @{$exclusions{file_types}}; my $file_names = join '|', @{$exclusions{file_names}}; my $text = $File::Find::name; $text =~ s!$rootdir/!!; if ( -f && $text =~ m!\.($file_types)$! && $text !~ m!^($directories +)! && $text !~ m!\b($file_names)!) { push @files, $text; } return; } find(\&wanted, "$rootdir"); my %site; for my $file (@files) { push_file(\%site, $file); } #print_select prints out a selection box in html using a hash to get t +he options. sub print_select { my ($action,%options) = @_; line(2,qq{<form action="$action" method="get">}); line(3,qq{<fieldset>}); line(4,qq{<legend>Display only&#8230;</legend>}); for my $select (sort keys %options) { my $options = $options{$select}; line(4,qq{<select name="$select">}); for my $option (@$options) { if ($option) { line(5,qq{<option value="$option">}.ucfirst $option.qq{</optio +n>}); } else { my $label = $select; $label =~ s/_/ /; $label =~ s/\b(\w)/\u$1/g; line(5,qq{<option value="">$label</option>}); } } line(4,qq{</select>}) } line(4,qq{<input type="submit" value="Search" class="sr">}); line(4,qq{<div><a href="$action">Start over</a></div>}); line(3,qq{</fieldset>}); line(2,qq{</form>}); } #start_html is where the printing of the html output of every module/s +cript begins. This is the template of my site. sub start_html { my ($defined_heading) = @_; print "content-type: text/html \n\n"; line(0,qq{<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://">}); line(0,qq{<html>}); line(0,qq{<head>}); line(1,qq{<title>$title</title>}); print_styles(); line(1,qq{<script type="text/javascript" src="$rootlink/files/javasc +ript/list.js"></script>}); line(0,qq{</head>}); line(0,qq{<body>}); line(1,qq{<div class="left">}); line(2,qq{<h1>Site menu</h1>}); print_menu(3,\%site,$rootdir,$rootlink,qq{ onclick="list_onclick(eve +nt)"},'no'); line(2,"<h2>".root('user')." off-site</h2>"); line(3,qq{<ul>}); for my $key (sort {lc $a cmp lc $b} keys %off_site) { line(4,qq{<li><a href="http://}.$off_site{$key}{link}.qq{">}.$off_ +site{$key}{site}.qq{</a></li>}); } line(3,qq{</ul>}); line(1,qq{</div>}); line(1,qq{<div class="right">}); if ($defined_heading eq 'no') { line(2,qq{<h1>$heading</h1>}); } } #end_html is where the printing of the html output of every module/scr +ipt ends. This is the last part of the template. sub end_html { line(2,qq(<p class="address">Contact ).email.qq(!</p>)); line(1,qq{</div>}); line(0,qq{</body>}); line(0,qq{</html>}); } #print_story is for pure text pages without any other formatting requi +red. There may be a few stray tags in the __DATA__ , #but not many hopefully. More than 6 or so, and I would write a new sc +ript for the page. sub print_story { my ($source) = @_; start_html('no'); while (my $line = <$source>) { chomp $line; if ($line =~ m/^</) { line(3,$line); } else { line(3,qq(<p>$line</p>)); } } line(3,qq(<p class="author"><small>written by $user</small></p>)); end_html; } 1;


This module gets all kinds of root information for my site or manipulates root information to get and print other things that are based on root.

package Base::Roots; use strict; use warnings; use base 'Exporter'; our @EXPORT_OK = qw(root root_directory get_data data_directory print_ +styles email file_text); use Cwd; use File::Basename; use List::Util qw(first); my $file_name = basename($0); my $full_path = getcwd; my %hosts = ( 'C:/Documents and Settings/ME/My Documents/fantasy' => { link => q(http://localhost), user => q(ME), name => q(ME's Domain), mail => q(ME@localhost), }, '/ftp/pub/www/fantasy' => { link => q(, user => q(Fantasy), name => q(Fantasy's Realm), mail => q(, }, '/home/lady_aleena/var/www' => { link => q(, user => q(Lady Aleena), name => q(Lady Aleena's Home), mail => q(, }, ); sub root_directory { my @dir = grep { $_ if $full_path =~ /^$_/ } keys %hosts; return pop @dir; } my $rootdir = root_directory(); die qq($rootdir is not on the list.) unless exists $hosts{$rootdir}; for my $host (keys %hosts) { $hosts{$host}{data} = $rootdir.'/files/data'; for my $key qw(audio css images) { $hosts{$host}{$key} = $hosts{$host}{link}.'/files/'.$key; } } #email just returns an e-mail link depending on the server it is on. sub email { return qq(<a href="mailto:$hosts{$rootdir}{mail}">$hosts{$rootdir}{u +ser}</a>); } sub root { my ($var) = @_; return $hosts{$rootdir}{$var}; } sub data_directory { my ($dir) = @_; return root('data')."/$dir/"; } my $rootlink = root('link'); my $relative_path = $full_path.'/'.$file_name; $relative_path =~ s!^$rootdir!!; $relative_path =~ s!.p[lm]$!!; #get_data searches the data directories for or takes parameters to get + a data file. sub get_data { my ($directory,$filename) = @_; my $data = $directory && $filename ? root('data')."/$directory/$file +name" : first {-e $_} map("$rootdir/files/data/$relative_path.$_",qw( +csv txt)); return $data; } #get_styles and print_styles searches the css directories for all of t +he style sheets that go with a file. my @relative_path_split = split("/",$relative_path); my @styles = (root('css').'/main.css'); sub get_styles { my ($style_dir) = @_; while (@relative_path_split) { my $var = shift @relative_path_split; if (-f ("$style_dir$var.css")) { push @styles, "$style_dir$var.css"; } get_styles("$style_dir$var/"); } } get_styles($rootdir.'/files/css'); sub print_styles { for my $style (@styles) { $style =~ s!$rootdir!$rootlink!; print qq{\t<link rel="stylesheet" type="text/css" href="$style">\n +}; } } #file_text makes the printed file name more attractive and in one case + adds a little punctuation, more may be added later. sub file_text { my ($text) = @_; $text =~ s!$rootlink/!!; $text =~ s!\.\w{2,5}?$!!; $text =~ s!&!&amp;!g; $text =~ s!_(Mr|Mrs|Ms|Dr)_!_$1._!g; $text =~ s!_! !g; return $text; } 1;


When my site is fully finished, I may fold this back into Base::HTML. Currently I am using it in two places, so a separate module was needed.

package Base::Menu; use strict; use warnings; use base 'Exporter'; our @EXPORT_OK = qw(push_file print_menu); use Cwd; use File::Basename; use File::Find; use HTML::Entities qw(encode_entities); use List::Util qw(first); use URI::Encode qw(uri_encode); use lib ".."; use Base::Roots qw(file_text); use Base::Nifty qw(line my_sort); my $full_path = cwd.'/'.basename($0); my $current_directory = cwd; #If I want to color code my file names by type, this is what I use. sub link_color { my ($var) = @_; my $color = "000"; $color = "f00" if ($var =~ m!pl$!); $color = "900" if ($var =~ m!pm$!); $color = "00f" if ($var =~ m!html$!); $color = "009" if ($var =~ m!shtml$!); $color = "003" if ($var =~ m!svg$!); $color = "060" if ($var =~ m!css$!); $color = "0f0" if ($var =~ m!csv$!); $color = "090" if ($var =~ m!txt$!); $color = "990" if ($var =~ m!zip$!); $color = "099" if ($var =~ m!js$!); $color = "c33" if ($var =~ m!pdf$!); $color = "939" if ($var =~ m!wav$!); $color = "909" if ($var =~ m!(gif|ico|jpg|png)$!); $color = "696" if ($var =~ m!xls$!); return qq( style="color:#$color"); } #push_file and print_menu are the backbones of printing my directories + and files in an html list. #written by simcop2387 in the #perlcafe on freenode. sub push_file { my $directory = shift; #get the previous directory my $file = shift; #get the file we're going to push onto the structu +re if ($file =~ m|/|) { #check if there are any more directories in our + file name my ($newdir, $newfile) = split(m|/|, $file, 2); # split the top di +rectory off $directory->{$newdir} = {} unless $directory->{$newdir}; #create t +he hash if it isn't there push_file($directory->{$newdir}, $newfile); #recurse with the file + name and the directory. } else { # we have no more / in our file name, so go ahead and just ad +d it push @{$directory->{''}}, $file; #add the file. } } sub print_menu { my ($level,$href,$dir,$link,$java,$colors) = @_; line($level,qq(<ul$java>)); for my $key (sort keys %{$href}) { if (length $key) { my $state = $current_directory =~ m/$key/ ? 'open active' : 'clo +sed'; my $key_text; if (first {-e $_} map("$dir/$key/index.$_",qw(pl shtml html))) { my $index_file = first {-e $_} map("$dir/$key/index.$_",qw(pl +shtml html)); $index_file =~ s/$dir/$link/; my $key_link_text = file_text($key); $key_text = qq(<a href="$index_file">$key_link_text</a>); } else { $key_text = file_text($key); } if (grep($_ !~ /index/,@{$href->{$key}{''}}) > 0 || (keys %{$hre +f->{$key}}) > 1) { line($level+1,qq(<li class="key $state">$key_text)); ++$level; print_menu($level+1,$href->{$key},"$dir/$key","$link/$key",'', +$colors); --$level; line($level+1,qq(</li>)); } else { line($level+1,qq(<li class="key $state">$key_text</li>)); } } else { my @files = grep($_ !~ "index",@{$href->{$key}}); if ($link =~ m/(Other_poets|Player_characters|Spellbooks)$/) { @files = sort {my_sort($a,$b,'name','-1')} @files; } else { @files = sort {my_sort($a,$b,'file')} @files; } for my $file (@files) { my $print_file = $link.'/'.uri_encode($file); $print_file =~ s/&/%26/g; my $color = $colors eq "yes" ? link_color($file) : ''; my $file_text = file_text($file); my $active = $full_path =~ m/$file/ ? 'active' : 'inactive'; line($level + 1,qq(<li class="$active"><a href="$print_file" t +itle="$file_text"$color>$file_text</a></li>)); } } } line($level,qq(</ul>)); } 1;


This is just a collection of subroutines that were too small to be on their own which I use in a lot of places all over my site.

package Base::Nifty; use strict; use warnings; use base 'Exporter'; our @EXPORT_OK = qw(get_hash commify grammatical_list line article_sor +t name_sort my_sort); #get_hash does just that for me, it gets me a hash from a text file, u +sually a .csv, which I can then use whereever. #written with rindolf in #perlcafe on freenode; golfed with the help o +f [GrandFather] of PerlMonks. sub get_hash { my ($hash,$data_hash) = @_; open(my $fh, $data_hash->{csv}) or die("can't open $data_hash $!"); while (my $value = <$fh>) { chomp $value; my @inner_array = split(/\|/,$value); my $n = 0; for my $heading (@{$data_hash->{headings}}) { $$hash{$inner_array[0]}{$heading} = $inner_array[$n]; ++$n; } } } #commify was found in the perldocs to put commas in numbers. sub commify { local $_ = shift; 1 while s/^([-+]?\d+)(\d{3})/$1,$2/; return $_; } #grammatical_list is used when I want to print a list with grammatical + correctness. #written with the help of DrForr in #perlcafe on freenode, golfed by +in #perlmonks on SlashNET. sub grammatical_list { my $conj = shift(@_) . ' '; return @_ if @_ <= 1; return join( ' '.$conj, @_ ) if @_ == 2; my $punc = grep( /,/, @_ ) ? '; ' : ', '; push @_, $conj.pop; join $punc, @_ } #tab and line are just to add tabs and newlines on the output. sub tab { my ($tab) = @_; return ("\t") x $tab; } sub line { my ($tab,$line) = @_; print tab($tab)."$line\n"; } #written mostly by kent/n in #perl on freenode. sub article_sort { my ($c,$d) = @_; for ($c,$d) { $_ =~ s{\s*\b(A|a|An|an|The|the)(_|\s)}{}xi; } return $c cmp $d; } sub name_sort { my ($c,$d,$sort_numer) = @_; return (split /_/, $c)[$sort_numer] cmp (split /_/, $d)[$sort_numer] +; } #golfed by [ikegami] on PerlMonks. sub my_sort { my ($c,$d,$type,$sort_number) = @_; my $initial = $c =~ /^index/ ? 0 : 1 <=> $d =~ /^index/ ? 0 : 1 || $c =~ /^ssi/ ? 0 : 1 <=> $d =~ /^ssi/ ? 0 : 1 || $c =~ /css$/ ? 0 : 1 <=> $d =~ /css$/ ? 0 : 1; return $initial if $initial; if ($type eq 'file') { s{\s*\b(?:an?|the)_}{}xi for $c, $d; return $c cmp $d; } elsif ($type eq 'name') { return (split /_/, $c)[$sort_number] cmp (split /_/, $d)[$sort_number]; } else { die("Bad type $type"); } } 1;

So, there it is, all of the Base modules for my site. I wouldn't have this if it weren't for the people here. Thank you all!

Have a cookie and a very nice day!
Lady Aleena

Replies are listed 'Best First'.
Re: Seeing Perl in a new light: Epilog
by TGI (Parson) on Sep 20, 2010 at 07:14 UTC

    I am writing to you about your approach for multiple similar pages. I am going to focus on the Monsters section of your site, because that is the part I've been looking at and am familiar with.

    You have a section on Role Playing, with a subsection titled "Monsters". You've got a CSV file with all the information for all the monsters you want a page for.

    Right now, it appears that each monster has its own Perl script. So there is "Dark" for a dark centaur. And "" for a Daemar and so forth. If you were to add a new monster you would add the monster to the CSV file and add a "" script.

    What makes much more sense is to make one script that handles all monster related issues. Call it Then when you want to access a list of all monsters, I've put together a CGI script that does exactly that. One script will handle all the monsters and also provide a summary page with links to detail view of specific monsters.

    With this approach, to add a new monster, you simply add it to the CSV file. Everything else happens automatically.

    Let me emphasize this key point; <bThere is no need to have a one-to-one relationship between pages displayed and scripts that generate them. A single script can produce many different pages.

    Without any other messing about, here's a script that demonstrates how a single script can handle all your monsters.

    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 = <<END_HTML; <html> <head> <title>$title</title> </head> <body> <h1>$title</h1> 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 = <<END_HTML; <p><a href='$url'>Monster List</a></p> </body> 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 "<a href='?name=$esc'>$name</a>"; }, }), 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 o +ne 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{Mo +nster} }; } $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 = '<table><tr>'; $html .= join '', map "<th>$_</th>", @$fields; $html .= '</tr>'; for my $item ( @$data ) { $html .= '<tr>'; $html .= join '', map "<td>$_</td>", map { exists $formatter->{$_} ? $formatter->{$_}( $_, $item ) : $item->{$_}; } @$fields; $html .= '</tr>'; } $html .= '</table>'; } sub gen_detail { # Generate monster detail view # Returns html text. my $data = shift; my $attr = shift; my $sections = shift; my $html = '<dl>'; $html .= join '', map "<dt>$_</dt><dd>$data->{$_}</dd>", @$attr; $html .= '</dl>'; for my $section ( @$sections ) { my @paras = split /\\/, $data->{$section}; $html .= "<h2>$section</h2>"; $html .= join '', map "<p>$_</p>", @paras; } return $html; }

    I know this code doesn't use your Base elements. I didn't want to download, configure and troubleshoot them. I thought it was more important to get this example together for you. You should be able to massage your standard infrastructure into this code.

    As you do, be aware that it has got a big, fatal bug--I didn't define an error_page routine, you'll see the error if you specify a bogus monster as the name parameter. You'll need to define an error page routine, or replace it with one that already exists in your libraries.

    The big theme of the discussion the other night was abstraction and generality. This is a good example of abstraction. Instead of one script for each monster, we have one script that can handle all the monsters by treating them as if they were identical. By doing so we can replace 20 scripts with one script.

    Start by converting the monsters to this method. Then repeat with spell books and weapons. As you build similar scripts, you might find similarities that point to opportunities for code reuse. Even better you may be able to work on an even more abstract level and simplify your code even more.

    First and foremost, you really need to move away from the SSI "one file per page" approach. Your life will be much simpler, and your stated goal of reducing your site size will be met.

    TGI says moo

      I have been thinking hard about your approach, and I don't think it will work. However, I may not have grasped the concept, so my thinking below may be completely wrong.

      You speak of creating an "all-in-one" script; and going with your example of my monsters, it would really slow up the loading of each page. Since the menu is generated solely by the directory and file structure of the site, every "all-in-one" would have to open up the data file(s) to populate that section.

      Currently, the Monster directory has a .pl file for each monster. Those are loaded into the menu script. However, a singular would require that Monster.csv be opened to create links to each monster akin to <a href="">monster</a>. For other sections, there isn't a singular data file, but a list. So, would have to go to the Spellbook data directory, read all the files there, and populate the list on the menu with <a href="">character</a>. I am not too keen on opening almost all of my data files or directories for every page loaded.

      Sample from menu

      <ul> <li class="inactive"> <a href="http://localhost/Role_playing/Monsters/Chaos_elemental-ki" title="Chaos elemental-kin">Chaos elemental-kin</a> </li> <li class="inactive"> <a href="http://localhost/Role_playing/Monsters/" title=" +Daemar">Daemar</a> </li> <li class="inactive"> <a href="http://localhost/Role_playing/Monsters/" t +itle="Dark centaur">Dark centaur</a> </li> </ul>

      After "all-in-one"

      <ul> <li class="inactive"> <a href="http://localhost/Role_playing/Monsters/ +os_elemental-kin" title="Chaos elemental-kin">Chaos elemental-kin</a> </li> <li class="inactive"> <a href="http://localhost/Role_playing/Monsters/ +mar" title="Daemar">Daemar</a> </li> <li class="inactive"> <a href="http://localhost/Role_playing/Monsters/ +k_centaur" title="Dark centaur">Dark centaur</a> </li> </ul>
      Have a cookie and a very nice day!
      Lady Aleena

        Speed is a valid concern. But before you get to worried about it, try it the change and see if it is actually a problem. It may not be as slow as you think. And as my mom always said "Don't borrow trouble." But if speed is a problem, there are ways to move forward.

        Two common solutions exist for the problem of having to open and parse too many files. 1. Use a database 2. Pre-process the files and cache the results. I'm just guessing, but I believe that you have zero interest in digging into the world of databases right now. That leaves preprocessing.

        Your current method is a form of pre-processing where you are the processor. You manually convert the list of monsters in your CSV file into a group of files. Then the menu script looks into the directory and makes the menu based on the file names. When you add or remove a monster, you manually add or remove a .pl file.

        Instead of this manual process, have a script that does whatever needs to happen to generate a data structure that represents your menu. Probably series of nested arrays. Then save that data structure to a file, using Storable is probably the simplest and best way since it is core and fast.

        Now when you want to generate the HTML for your menu, instead of redoing the work of processing all the files, simply load the data from your file and generate html.

        # Generate this by doing what you do now to make your menus. # Then save it to a file by using Storable's lock_store function: # use Storable qw( lock_store ); # # lock_store( \%menu_data, '' ); # In the code you use to render your html menu, load the data from # the file using Storable's lock_retrieve function; # use Storable qw( lock_retrieve ); # # my $menu_data = lock_retrieve( '' ); # Here is an example of a data structure that compactly represents you +r # menu. # # Item format [ 'Text', Link, children ] my $menu_data = [ [ 'About', undef, [ ['About me', 'About/'], ['About my user names', 'About/'], ['About this site', 'About/'], # And so forth ], ], [ 'Books', undef, [ ['Fiction', 'Books/'], ['Non-fiction', 'Books/'], ['Role-playing fiction', 'Books/'], # And so forth ], ], [ 'Fiction', undef, [ ['My Life as a Witch', 'Fiction/'], # And so forth [ 'Erotic fiction', undef, [ [ 'The Angel', 'Fiction/Erotic_fiction/The_Angel.p +l' ], [ 'The Assassin', 'Fiction/Erotic_fiction/The_Assa' ], # And so forth ], ], ], ] ];

        This approach should lead to a couple of benefits:

        • Less manual work - any number of updates can be handled by simply running the preprocessor script.
        • Simple HTML generation code. You have several different ways that you represent data on your account. Each of these must be processed in its own way. My guess is that each different sort of data processor directly generates HTML. This means that any changes to your desired HTML output must be repeated in many places. By generating a standard structure you can consolidate all of the code that HTML generation for menus in one place, you make changes to the final HTML easier. Also, this approach allows you to produce multiple distinct outputs from the data structure, a possible real world example would be RDF generation.
        • You can break the relationship between file names, URLs and document titles.

        I am assuming that you can write code to traverse the $menu_data structure. If not, let me know and I can demonstrate the required concepts.

        My comments here are based on my understanding of the following statement. If they don't make sense, then I probably misunderstood you.

        Currently, the Monster directory has a .pl file for each monster. Those are loaded into the menu script.

        I understand this to mean you scan the file names and use those to populate the menu.

        TGI says moo

Log In?

What's my password?
Create A New User
Node Status?
node history
Node Type: note [id://860750]
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others contemplating the Monastery: (4)
As of 2021-05-09 20:56 GMT
Find Nodes?
    Voting Booth?
    Perl 7 will be out ...

    Results (102 votes). Check out past polls.