O'Reilly logo

Embedding Perl in HTML with Mason by Ken Williams, Dave Rolsky

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Using Mason Outside of Dynamic Web Sites

So far we’ve spent a lot of time telling you how to use Mason to generate spiffy web stuff on the fly, whether that be HTML, WML, or even dynamic SVG files.

But Mason can be used in lots of other contexts. For example, you could write a Mason app that recursively descends a directory tree and calls each component in turn to generate a set of static pages.

How about using Mason to generate configuration files from templates? This could be quite useful if you had to configure a lot of machines similarly but with each one slightly different (for example, a web server farm).

Generating a Static Site from Components

Many sites might be best implemented as a set of static files instead of as a set of dynamically created responses to requests. For example, if a site’s content changes only once or twice a week, generating each page dynamically upon request is probably overkill. In addition, you can often find much cheaper web hosting if you don’t need a mechanism for generating pages dynamically.

But we’d still like some of the advantages a Mason site can give us. We’d like to build the site based on a database of content. We’d also like to have a nice consistent set of headers and footers, as well as automatically generate some bits for each page from the database. And maybe, just maybe, we also want to be able to make look-and-feel changes to the site without resorting to a multi-file find-and-replace. These requirements suggest that Mason is a good choice for site implementation.

For our example in this section, we’ll consider a site of film reviews. It is similar to a site that one of the authors actually created for Hong Kong film reviews. Our example site will essentially be a set of pages that show information about films, including the film’s title, year of release, director, cast, and of course a review. We’ll generate the site from the Mason components on our home GNU/Linux box and then upload the site to the host.

First, we need a directory layout. Assuming that we’re starting in the directory /home/dave/review-site, here’s the layout:

/home/dave/review-site (top level)
  /htdocs
    - index.html
      /reviews
        - autohandler
        - Anna_Magdalena.html
        - Lost_and_Found.html
        - ... (one file for each review)

  /lib
    - header.mas
    - footer.mas
    - film_header_table.mas

The index page will be quite simple. It will look like Example 11-10.

Example 11-10. review-site/htdocs/index.html

<& /lib/header.mas, title => 'review list' &>
<h1>Pick a review</h1>
<ul>
% foreach my $title (sort keys %pages) {
 <li><a href="<% $pages{$title} | h %>"><% $title | h %></a>
% }
</li>
<%init>
 my %pages;

 local *DIR;
 my $dir = File::Spec->catfile( File::Spec->curdir, 'reviews' );
 opendir DIR, $dir
     or die "Cannot open $dir dir: $!";

 foreach my $file ( grep { /\.html$/ } readdir DIR ) {
     next if $file =~ /index\.html$/;

     my $comp = $m->fetch_comp("reviews/$file")
       or die "Cannot find reviews/$file component";

     my $title = $comp->attr('film_title');

     $pages{$title} = "reviews/$file";
 }

 closedir DIR
     or die "Cannot close $dir dir: $!";
</%init>

This component simply makes a list of the available reviews, based on the files ending in .html in the /home/dave/review-site/reviews subdirectory. We assume that the actual film title is kept as an attribute (via an <%attr> section) of the component, so we load the component and ask it for the film_title attribute. If it doesn’t have one Mason will throw an exception, which we think is better than having an empty link. If this were a dynamic web site, we might want to instead simply skip that review and go on to the next one, but here we’re assuming that this script is being executed by a human being capable of fixing the error.

We make sure to HTML-escape the filename and the film title in the <a> tag’s href attribute. It’s not unlikely that the film could contain an ampersand character (&), and we want to generate proper HTML.

Next, let’s make our autohandler for the reviews subdirectory (Example 11-11), which will take care of all the repeated work that goes into displaying a review.

Example 11-11. review-site/htdocs/reviews/autohandler

<& /lib/header.mas, title => $film_title &>
<& /lib/film_header_table.mas, comp => $m->base_comp &>
% $m->call_next;
<& /lib/footer.mas &>

<%init>
 my $film_title = $m->base_comp->attr('film_title');
</%init>

Again, a very simple page. We grab the film title so we can pass it to the header component. Then we call the film_header_table.mas component, which will use attributes from the component it is passed to generate a table containing the film’s title, year of release, cast, and director.

Then we call the review component itself via call_next( ) and finish up with the footer.

Our header (Example 11-12) is quite straightforward.

Example 11-12. review-site/lib/header.mas

<html>
<head>
<title><% $real_title | h %></title>
</head>
<body>

<%args>
 $title
</%args>
<%init>
 my $real_title = "Dave's Reviews - $title";
</%init>

This is a nice, simple header that generates the basic HTML pieces every page needs. Its only special feature is that it will make sure to incorporate a unique title, based on what is passed in the $title argument.

The footer (Example 11-13) is the simplest of all.

Example 11-13. review-site/lib/footer.mas

<p>
<em>Copyright &copy; David Rolsky, 1996-2002</em>.
</p>

<p>
<em>All rights reserved.  No part of the review may be reproduced or
transmitted in any form or by any means, electronic or mechanical,
including photocopying, recording, or by any information storage and
retrieval system, without permission in writing from the copyright
owner.</em>
</p>

</body>
</html>

There’s one last building block piece left before we get to the reviews, the /lib/film_header_table.mas component (Example 11-14).

Example 11-14. review-site/lib/film_header_table.mas

<table width="100%">
 <tr>
  <td colspan="2" align="center"><h1><% $film_title | h %></h1></td>
 </tr>
% foreach my $field ( grep { exists $data{$_} } @optional ) {
 <tr>
  <td><strong><% ucfirst $field %></strong>:</td>
  <td><% $data{$field} | h %></td>
 </tr>
% }
</table>

<%args>
 $comp
</%args>

<%init>
 my %data;
 my $film_title = $comp->attr('film_title');
 my @optional = qw( year director cast );
 foreach my $field (@optional)  {
     my $data = $comp->attr_if_exists($field);
     next unless defined $data;
     $data{$field} = ref $data ? join ', ', @$data : $data;
 }
</%init>

This component just builds a table based on the attributes of the component passed to it. The required attribute is the film’s title, but we can accommodate the year, director(s), and cast.

There are only two slightly complex lines.

The first is:

% foreach my $field ( grep { exists $data{$_} } @optional ) {

Here we are iterating through the fields in @optional that have matching keys in %data. We could have simply called keys %data, but we want to display things in a specific order while still skipping nonexistent keys.

The other line that bears some explaining is:

$data{$field} = ref $data ? join ', ', @$data : $data;

We check whether the value is a reference so that the attribute can contain an array reference, which is useful for something like the cast, which is probably going to have more than one person in it. If it is an array, we join all its elements together into a comma-separated list. Otherwise, we simply use it as-is.

Let’s take a look at what one of the review components might look like:

<%attr>
 film_title => 'Lost and Found'
 year => 1996
 director => 'Lee Chi-Ngai'
 cast => [ 'Kelly Chan Wai-Lan', 'Takeshi Kaneshiro', 'Michael Wong Man-Tak' ]
</%attr>

<p>
 Takeshi Kaneshiro plays a man who runs a business called Lost and
 Found, which specializes in searching for lost things and people. In
 the subtitles, his name is shown as That Worm, though that seems
 like a fairly odd name, cultural barriers notwithstanding. Near the
 beginning of the film, he runs into Kelly Chan. During their first
 conversation, she says that she has lost something. What she says
 she has lost is hope. We soon find out that she has leukemia and
 that the hope she seeks seems to be Michael Wong, a sailor who works
 for her father's shipping company.
</p>

<p>
 blah blah blah...
</p>

This makes writing new reviews really easy. All we do is type in the review and a small number of attributes, and the rest of the framework is built automatically.

A more complex version of this site might store some or all of the data, including the reviews, in a database, which would make it easier to reuse the information in another context. But this is certainly good enough for a first pass.

All that’s left is the script that will generate the static HTML files. See Example 11-15.

Example 11-15. review-site/generate_html.pl

#!/usr/bin/perl -w

use strict;  # Always use strict!

use Cwd;
use File::Basename;
use File::Find;
use File::Path;
use File::Spec;
use HTML::Mason;

# These are directories.  The canonpath method removes any cruft
# like doubled slashes.
my ($source, $target) = map { File::Spec->canonpath($_) } @ARGV;

die "Need a source and target\n"
    unless defined $source && defined $target;

# Make target absolute because File::Find changes the current working
# directory as it runs.
$target = File::Spec->rel2abs($target);

my $interp =
    HTML::Mason::Interp->new( comp_root => File::Spec->rel2abs(cwd) );

find( \&convert, $source );

sub convert {
    # We don't want to try to convert our autohandler or .mas
    # components.  $_ contains the filename
    return unless /\.html$/;

    my $buffer;
    # This will save the component's output in $buffer
    $interp->out_method(\$buffer);

    # We want to split the path to the file into its components and
    # join them back together with a forward slash in order to make
    # a component path for Mason
    #
    # $File::Find::name has the path to the file we are looking at,
    # relative to the starting directory
    my $comp_path = join '/', File::Spec->splitdir($File::Find::name);

    $interp->exec("/$comp_path");
    # Strip off leading part of path that matches source directory
    my $name = $File::Find::name;
    $name =~ s/^$source//;

    # Generate absolute path to output file
    my $out_file = File::Spec->catfile( $target, $name );
    # In case the directory doesn't exist, we make it
    mkpath(dirname($out_file));

    local *RESULT;
    open RESULT, "> $out_file" or die "Cannot write to $out_file: $!";
    print RESULT $buffer or die "Cannot write to $out_file: $!";
    close RESULT or die "Cannot close $out_file: $!";
}

We take advantage of the File::Find module included with Perl, which can recursively descend a directory structure and invoke a callback for each file found. We simply have our callback (the convert( ) subroutine) call the HTML::Mason::Interp object’s exec( ) method for each file ending in .html. We then write the results of the component call out to disk in the target directory.

We also use a number of other modules, including Cwd, File::Basename, File::Path, and File::Spec. These modules are distributed as part of the Perl core and provide useful functions for dealing with the filesystem in a cross-platform-compatible manner.

You may have noticed in Example 9-1 that when we invoked the Interpreter’s exec( ) method directly, it didn’t attempt to handle any of the web-specific elements of the request.

The same method is employed again here in our HTML generation script, and this same methodology could be applied in other situations that have little or nothing to do with the web.

Generating Config Files

Config files are a good candidate for Mason. For example, your production and staging web server config files might differ in only a few areas. Changes to one usually will need to be propagated to another. This is especially true with mod_perl, where web server configuration can basically be part of a web-based application.

And if you adopt the per-developer server solution discussed earlier, a template-driven config file generator becomes even more appealing.

Example 11-16 is a simple script to drive this generation.

Example 11-16. config_maker.pl

#!/usr/bin/perl -w

use strict;

use Cwd;
use File::Spec;
use HTML::Mason;
use User::pwent;

my $comp_root =
    File::Spec->rel2abs( File::Spec->catfile( cwd( ), 'config' ) );

my $output;
my $interp =
    HTML::Mason::Interp->new( comp_root  => $comp_root,
                      out_method => \$output,
                      );

my $user = getpwuid($<);

$interp->exec( '/httpd.conf.mas', user => $user );

my $file =  File::Spec->catfile( $user->dir, 'etc', 'httpd.conf' );
open FILE, ">$file" or die "Cannot open $file: $!";
print FILE $output;
close FILE;

An httpd.conf.mas from the component might look like Example 11-17.

Example 11-17. config/httpd.conf.mas

ServerRoot <% $user->dir %>
PidFile <% File::Spec->catfile( $user->dir, 'logs', 'httpd.pid' ) %>
LockFile <% File::Spec->catfile( $user->dir, 'logs', 'httpd.lock' ) %>
Port <% $user->uid + 5000 %>

# loads Apache modules, defines content type handling, etc.
<& standard_apache_config.mas &>

<Perl>
 use lib <% File::Spec->catfile( $user->dir, 'project', 'lib' ) %>;
</Perl>

DocumentRoot <% File::Spec->catfile( $user->dir, 'project', 'htdocs' ) %>

PerlSetVar MasonCompRoot <% File::Spec->catfile( $user->dir, 'project', 'htdocs' ) %>
PerlSetVar MasonDataDir <% File::Spec->catfile( $user->dir, 'mason' ) %>
PerlModule HTML::Mason::ApacheHandler

<FilesMatch "\.html$">
 SetHandler perl-script
 PerlHandler HTML::Mason::ApacheHandler
</FilesMatch>

<%args>
$user
</%args>

This points the server’s document root to the developer’s working directory. Similarly, it adds the project/lib directory the Perl’s @INC via use lib so that the user’s working copy of the project’s modules are seen first. The server will listen on a port equal to the user’s user id plus 5,000.

Obviously, this is an incomplete example. It doesn’t specify where logs, or other necessary config items, will go. It also doesn’t handle generating the config file for a server intended to be run by the root user on a standard port.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required