ONLamp.com    
 Published on ONLamp.com (http://www.onlamp.com/)
 See this if you're having trouble printing code examples


Slash's Wiki Plugin

by chromatic, coauthor of O'Reilly's Running Weblogs with Slash
01/17/2002

The latest stable version of Slash comes with several interesting plugins, and most future Slash development will probably involve creating new plugins. In this article, Slashdot's chromatic introduces the Slash Wiki plugin, which integrates nicely with Slash and has a lot of cool features. chromatic also gives a detailed explanation of how and why the Wiki plugin works, including Wiki theory, and its simple plugin architecture.

About Slash Plugins

The latest stable version of Slash comes with several interesting plugins, including extra blocks (pointers to remote RSS feeds), an internal and external messaging mechanism, and a user journal system. Other projects are also starting to appear, such as lead developer Chris Nandor's Slash::Gallery image gallery.

In theory, any Web application could be reimplemented as a Slash plugin. In practice, it's not terribly difficult to write something useful. The Slash plugin framework provides several nice features:

There may be more enhancements for the core storytelling Weblog feature, but most future Slash development will probably involve creating new plugins. I wouldn't be surprised to see the message board itself spun off into a plugin. Slash would then become a generic application framework. Of course, as the Slashcode project is sponsored by the owners of Slashdot, the software's primary duty is to run Slashdot. Slashteam's smart and talented enough to make things the rest of us can use, though.

Running Weblogs with Slash

Related Reading

Running Weblogs with Slash
By chromatic , Brian Aker, David Krieger

What A Wiki Is

"The nice thing about a Wiki is its simplicity."

While the story and comment format is fine for discussions, there's a whole class of collaborative work that can't be done with a standard Slash site. Occasionally, it's useful to have a brainstorming session, where getting ideas down coherently is more important than following a traditional call-and-response question format. Business people call this "synergy," but it really means something here.

Though I can't prove it at the moment, I suspect that's what lead Ward Cunningham to come up with the idea of a Wiki. It's like one of those smart whiteboards, where anyone can write anything and erase anything, but there's still a record of all revisions. The theory goes that putting simple but effective tools in the hands of smart people and staying out of the way can produce great results.

So far, it's effective. The Design Patterns and Extreme Programming folk (and they do overlap somewhat) regularly use Wikis like the Portland Pattern Repository to communicate. The editors and developers of Perlmonks keep track of site issues and create and edit site documentation on special Wiki nodes. The Perl Kwalitee Assurance team maintains a list of untested and undertested core Perl modules.

The nice thing about a Wiki is its simplicity. There are just a few formatting rules, and it's available to anyone who can manage a few HTML forms in a Web browser. They're simple to create and maintain. Perhaps best of all, it's very easy to create and to maintain links between documents. (I like similar things about the Everything Engine, though I'm partial.)

It's very easy to write notes, definitions, or even full documents in places where you have several trusted, geographically diverse people who might use the powerful automatic linking to good effect. Several free software projects have their own Wikis to answer user and developer questions, and these often become fully fledged FAQs. If there were a Slash Wiki plugin, you could even write stories or site guides or your own site FAQ.

Comment on this articleWhat's your take on future Slash development and new plugins?
Post your comments

Good news. It exists! If you have a Slash 2.2 or better site, installation is very easy. Just unpack the tarball into the plugins/ directory beneath your Slash site directory, run the Makefile.PL file, then make, make test, and make install as root, and run the install-plugins program. You'll be on your way to Wiki goodness. Things should just work, if only because Jesse Hirsh found several bugs in the beta. Thanks!

This wouldn't be much of an article on Slash, though, if it didn't go into more detail on how and why the plugin works. First, you have to know a little more about how a Wiki works.

Wiki Theory

The central component of a Wiki is the idea of a page--a single essay or a discussion on a named topic. Anyone can create or edit a page, and all of the pages on a Wiki can potentially be linked together by name. These names follow a Java-esque StudlyCaps format, and anything that looks like a page name will automatically become a link to the page of that name. This encourages extensive linking, and the ease of editing encourages people to fill in the gaps.

There are three basic types of operations that the Slash Wiki performs.

View Page

The simples and most common operation is to display a page. Generally, the user will request something like CreatingGoodLinks. The plugin searches for a page of that title, formats it if necessary, and displays it along with editing controls. If the page has not yet been created, a default message will display. The user can choose to add to the page or to follow a link to another page.

Edit Page

Editing pages is also common, though probably less so than viewing them. (The same assumption applies to the number of people who feel compelled to comment on a story versus those who merely read it.) This operation requires updating the database to add the current version of the page, to archive the previous version (for reference or abuse protection), and to display the new version.

The Slash Wiki handles this a little differently, actually storing the rendered version of the page along with the raw, Wiki-formatted version. It's a slight performance improvement. (If a page is edited once for every ten views, and if the raw version is stored to make editing easier, you can trade a little extra storage space to avoid the performance hit of re-rendering things for the nine other views. In retrospect, that would help some of the performance issues of modern Everything Engine releases.)

This is also a place to implement stricter access controls. For a site FAQ, it may be appropriate to restrict writing to users with the Author flag set, or a seclev of over 100. Admittedly, this runs counter to the trustful Wiki spirit, but it may work better for your purposes.

Special Features

There are a handful of other convenient features Wikis often have. First, it's nice to be able to search for pages by title without having to edit URI query strings. This is very simple, as the search feature is already built into the page displaying logic. Another nice feature lists the most recently modified pages, and is a simple database query. The final special feature is related, and allows users to browse the last several versions of a page.

Other Wikis have additional features, such as searching within page text and deleting and restoring versions of a page, but those aren't in SlashWiki, yet. (It would also be nice to be able to promote a page to a Story, but that's probably further down the road.)

Formatting Rules

The basic Wiki formatting rules are very simple. Newlines are handled automatically. Words of the StudlyCaps form are automatically linked. Anything wrapped in two single quotes (''libre'') is marked as emphasized, and anything wrapped in three single quotes ('''sucrose''') is marked as strong. Lists are indented one tabstop or four spaces, and list items start with an asterisk (for simple bullet points) or a number or a letter (for ordered lists). Anything else indented four spaces or one tabstop is considered to be code, or preformatted text, and will be rendered in a monospaced font as is.

Whew.

Since I already had a Wiki formatting processor lying around (from the book-sidetracked Jellybean project), I generalized it and made it into a module called Text::WikiFormat that will wing its way to the CPAN shortly. As it passes the unit tests, we'll consider this to be a solved problem. This is only important for one additional thing--extended linking semantics.

If you enable this feature (more on this in a moment), people will also be able to create links by wrapping them in square brackets, like [WebmistressHannah]. This is, admittedly, the only way I can create links to my pseudonym. Another feature shamelessly stolen from Everything is the idea of optional link titles. If there's a pipe in the brackets, everything afterwards is considered a title. A link such as [ProgrammerSunny|my best student] would link to the ProgrammerSunny page but appear in text as my best student.

Plugin Architecture

With all of that explanation out of the way, the plugin itself is really very simple. It consists of one applet file (standard Perl program that becomes a mod_perl handler), one module (the Wiki formatter), seven templates, and two SQL files.

The Database

As the Wiki pages are tied very closely to the database, the first step in creating the plugin was to define the storage schema. There are two tables, wikipage and wikitext:


		CREATE TABLE wikipage (

		title varchar(255) NOT NULL UNIQUE,

		version smallint UNSIGNED,

		wid mediumint UNSIGNED NOT NULL AUTO_INCREMENT,

		KEY (wid)

	);



		CREATE TABLE wikitext (

		wid mediumint UNSIGNED NOT NULL,

		version smallint UNSIGNED NOT NULL,

		uid mediumint UNSIGNED NOT NULL,

		raw text DEFAULT '' NOT NULL,

		date datetime,

		description varchar(255) DEFAULT '' NOT NULL,

		cooked text,

		KEY pagever (wid, version)

	);

As you can see, the wikipage table stores only the page title, the current version of the page, and a unique identifier, used to join to the wikitext table.

wikitext has the identifier, of course, and also holds the raw (Wiki-formatted) and cooked (HTML-formatted) versions of a page. In addition, it contains the uid of the user who created the particular page version (found in the user tables), a description of the version, the date at which the version was created, and a version key. Each version of a page is stored in this table, and can be retrieved with the wid and version.

The trick here was to make things flexible enough to support the basic Wiki features while not imposing arbitrary limitations and still trying to keep performance high. As Wiki pages can be 64 kilobytes in length, I tried to keep page metadata in a separate table from the text itself. (Slash uses this technique.) I ended up with two tables, one that tracks the basic information current version of the page, and another that tracks all revisions. If performance is still a problem, another table could be created just to hold raw and cooked text. Efficient normalizers will do that, but it didn't have much payoff in my very small tests, and would complicate my examples. Patches welcome.

Normally, I'd use the MySQL datestamp column type, but Slash uses the datetime type internally. While the latter isn't automatically updated, it does have the benefit of working with Slash's timeCalc() function. That's enough of a benefit to do things differently.

Thinking about database access, reading the current version of a page is easy. We just have to join the two tables on the wid field. Reading a specific version is also easy, as the version number can be a constraint on the wikitext table. Writes are a little trickier. We need to know the current version of a page and either insert a row into the wikipage table, or update a row. For a write, we'll always insert a row into the wikitext table, but we do need to know the version number. Still, it's not difficult.

The wiki.pl Applet

All access to the Wiki takes place through the wiki.pl applet. (In Slash terms, it is also called a ``program'', ``script'', or ``page''. None of these names are completely descriptive, so we'll pick the first, alphabetically speaking.)

Another ancient Slash convention is the use of the op parameter to dispatch to different operations. That is, any applet that does multiple things generally has a dispatch table keyed on the value of the op parameter to decide what to do. wiki.pl uses the %ops variable for this, and the brains of the applet are:


        %ops = (

                update  => \&update,

                list    => \&list,

                display => \&display,

        );


        main();

        return();


        sub main {

                my $form = getCurrentForm();

                my $op   = $form->{op};


                unless (exists $ops{$op}) {

                        $op = 'display';

                }


                $ops{$op}->($form);

        }

Whenever a request comes in, the applet reads the parsed CGI form variables into the $form variable with the getCurrentForm() command. These variables are parsed at an earlier stage of the request cycle. The op parameter must be verified against the operations we support. Otherwise, the default operation is to display a page. The applet calls the appropriate subroutine and then returns. The return() call exists because mod_perl will turn this into an anonymous subroutine. For something this simple, we can probably get away without it, but it's better to be explicit to avoid nasty surprises later.

The display() Function

The main guts of displaying a Wiki page live in display(). First, it must set up some variables. These are the form parameters from main(), as well as the database and the site configuration variables:


        my $form        = shift;

        my $slashdb     = getCurrentDB();

        my $constants   = getCurrentStatic();

At this point, it's possible to add access checks, turning away anonymous users (with the isAnon() function) or comparing the user's seclev against a configuration variable like $constants->{wikiReadSeclev}. By default, access is open to everyone.

Next, the sub needs to decide which page to display. If the user has specified one, it will be in the page parameter. Otherwise, use the default page specified in site configuration variables. If that doesn't exist, fall back to the default page, and print a header with the chosen page:


        my $page    = $form->{page} || $constants->{wikiDefaultPage} || 'default';

        header("SlashWiki: $page");

The only other parameter that matters is the version parameter. With it, the user is requesting a specific version of the page. Without it, the user wants to see the latest version. This is used to determine what kind of WHERE clause to use when requesting a page from the database:


    my $version = $form->{version} || '';

    $version    = 0 if $version =~ /\D/;


        my $where = "title = " . $slashdb->sqlQuote($page) .

                " AND wikipage.wid = wikitext.wid ";


        if ($version) {

                $where .= "AND wikitext.version = " . $slashdb->sqlQuote($version);

        } else {

                $where .= "AND wikipage.version = wikitext.version ";

        }

Retrieval is done with the sqlSelect() method of the Slash::DB object stored in $slashdb. As with nearly all of Slash's database operations, the first argument is the field list, the second is a table list, the third is an optional WHERE clause, and the fourth is an optional extra clause (you'll see one later):


        my %page_data = (

                title   => $page,

        );


        @page_data{qw(raw cooked nickname date wid version curver)} =

        $slashdb->sqlSelect('raw, cooked, nickname, date, wikipage.wid,' .

                'wikitext.version, wikipage.version',

                'wikipage, wikitext, users', $where

        );

This fills out the %page_data hash with fields to be passed to a template. Before that happens, two things must be changed. First, because the user might want to view an old version of a page, we need to suppress editing. It's not right to have someone modify the historical record, even accidentally. If the user requested a page version and that version isn't the same as the current version, mark the page as readonly:


        $page_data{readonly} = ($version && $version == $page_data{curver});

The final modification is to mark up the date in the user's preferred timezone and format. This is the timeCalc() subroutine mentioned earlier:


        $page_data{date} = timeCalc($page_data{date});

All that's left to be done is to display the wikiView template with the page data, and to display the page footer. Both are very simple:


        slashDisplay('wikiView', \%page_data);

        footer();

The update() Function

This function is a little brawnier. As described above, its core purpose in life is to insert something into the wikitext table and to update or to insert something into the wikipage table. There's a twist, though. Instead of forcing people to create new page revisions to correct errors that only appear after something is published (may this not be prophetic!), the editing page adds a preview button that does everything except the database work. It's a simple flag, but it does complicate things slightly.

The first thing to do is to set up some common variables. The first few are standard for all plugin programming tasks. This time, getCurrentUser() is used to fetch a hash that contains information pertaining to the current user. It'll come in handy in a moment. The other variables come from the form parameters submitted by the user.


        my $form        = shift;

        my $user        = getCurrentUser();

        my $slashdb     = getCurrentDB();

        my $constants   = getCurrentStatic();


        my ($raw, $description, $title, $wid) =

                @$form{qw( raw description title wid )};

There's a little verification of the data to prevent obviously bad submissions. The Wiki page identifier and the version numbers must be completely numeric. The title must have a word character in it. Otherwise, there will be no page displayed. This will only happen if someone's been messing with forms or if the templates are completely broken. This is terribly user unfriendly, but many better examples leave error handling as an exercise for the reader. Patches welcome. This is also the place to strip carriage returns from the raw text, as they may be introduced by certain Web browsers and tend to have deleterious effects:


    return if ($wid =~ /\D/ or $title !~ /\w/ or $version =~ /\D/);

    $raw =~ tr/\r//d;

This is also the point to add access controls, perhaps checking if the user has an Author flag again, or if her seclev meets or exceeds one stored in the mythical configuration value $constants->{wikiWriteSeclev}.

The next important job is to format the wiki text. This is done with a call to Text::WikiFormat::format(). We pass in several parameters. The first is the text to format. The second is an optional hash reference of formatting directives. As we're doing HTML for now, the defaults just work. The final argument is a hash reference of extra options, of which we have two. prefix sets the prefix for all HTML links -- this is used to create base URLs linking to other pages within the Wiki. extended allows or disallows extended linking semantics. This, along with everything else pulled out of $constants is a Slash configuration variable, and can be changed through the Web interface. By default, it's off, so if you write [CatLover|Michelle] expecting a link, you'll be disappointed. The formatting call is:


        my $cooked = Text::WikiFormat::format($raw, {}, {

                prefix      => "$constants->{rootdir}/wiki.pl?page=",

                extended    => $constants->{wikiExtendedlinks},

        });

Now that we have the raw text (in Wiki format) and the cooked text (in HTML), we have a choice to make. If the user hit the preview button, the $preview variable will be defined, and we shouldn't store everything in the database. Otherwise, we have some database action to perform.

There's yet another choice if we need to update the database. Is this a new page or an existing page? If the page already exists, we'll have a Wiki page identifier in $wid and must call sqlUpdate() on the wikipage table. Otherwise, we'll have to use sqlInsert() on wikipage.


        my ($method, $where);

        if ($wid) {

                $where      = "wid = $wid AND title = " .  

			$slashdb->sqlQuote($title);

                $method     = 'sqlUpdate';

                $version    = $slashdb->sqlSelect('version', 

			'wikipage', $where) + 1;

        } else {

                $wid        = $slashdb->sqlSelect('MAX(wid)+1','wikipage');

                $where      = '';

                $method     = 'sqlInsert';

        }


        $version ||= 1;


        my $rows = $slashdb->$method('wikipage', {

                title   => $form->Slash's Wiki Plugin,

                version => $version,

        }, $where) or warn "Could not update database\n";

There are several important things to note. First, the method name is determined dynamically. This makes the code shorter, but it can throw people who aren't used to pushing the limits of method dispatch. (Not that Damian Conway would be impressed, but he's not the target reader this time.)

Next, be aware that the code goes through a couple of contortions to make sure to get a good version number and a good wid. Since these are form the key for the wikitext table, they are very important. Because the text table keeps around several versions associated with a single wid, we can't rely on the magical auto_increment on that field in the page table. It's still clearer to leave it that way in the schema, though. Unfortunately, there's a potential race condition between the two database actions. On a busy site, this would be a candidate for some form of table locking, or perhaps a single SQL statement that could be executed in one go.

After the wikipage table is updated, it's time to insert a new row into the wikitext table. This is much easier, as all of the data is already provided:


        $slashdb->sqlInsert('wikitext', {

                raw         => $raw,

                wid         => $wid,

                uid         => $user->{uid},

                -date       => 'now()',

                cooked      => $cooked,

                version     => $version, 

                description => $description,

        }) or warn "Could not update database\n";

Only two things are slightly different. The $user->{uid} access grabs the unique identifier associated with the current user. This is, of course, used to join to the user tables to find out who created the current version. The date parameter is also unique -- it's actually a MySQL function to get the current database time. The leading dash on the parameter name tells the sqlInsert() function not to quote the value. It will be treated as a function call and not a literal string.

All that's left to do is to display information. We can reuse the wikiView template, but we're missing two pieces of information: the time of the update and the nickname of the user performing the update. That's not a problem, as they're both available:


    header("SlashWiki: $title");


        slashDisplay('wikiView', {

                raw         => $raw,

                wid         => $wid,

                date        => timeCalc($slashdb->getTime()),

                title       => $title,

                cooked      => $cooked,

                nickname    => $user->{nickname},

                description => $description,

        });


        footer();

The list() Function

By far the ugliest of the SlashWiki functions (patches welcome), list() handles two different kinds of lists. First, it lists several of the most recently modified Wiki pages. This is the default behavior. Given a valid page parameter, it also can list the various revisions of a Wiki page. Things start out as you should be expecting:


    my $form        = shift;

    my $slashdb     = getCurrentDB();

    my $constants   = getCurrentStatic();



    my $page= $form->{page} || '';

The next step is to decide which of the two behaviors to perform. This is obviously done on the truth of $page. There's no reason it couldn't also be done with a $wid parameter, but it doesn't currently do that.

If there's no page, we need to find the last several pages that have been modified. That's why we keep the date in the wikitext table. We'll need to pull in the wikipage table to get titles, and the users table to get user nicknames (to affix blame). For the sake of flexibility, we'll allow the user or the administrator to determine how many pages to display. First, we prefer a form variable named recentLimit and then a configuration variable named wikiRecentLimit. If neither is a valid positive integer, we'll use 25:


        my $recents = $form->{recentLimit} || $constants->{wikiRecentLimit};

        $recents    = 25 unless $recents and $recents !~ /\D/;

Next we display a header, reflecting what we're doing, and interpolating the number of pages. Something so small just seems so neat:


        header("SlashWiki: Latest $recents Modified Pages");

The next step is to perform a gigantic SQL selection to grab all the data we need, to sort it and to limit it to the results we want. Make sure the kids are out of the room:


        $slashdb->sqlSelectAllHashrefArray(

                'wikipage.version, title, date, description, nickname',

                'wikipage, wikitext, users',

                'wikipage.wid = wikitext.wid AND users.uid = wikitext.uid ' .

                        'AND wikipage.version = wikitext.version',

                "order by date limit $recents")

Unlike some of the other database operations, this time we want a metric boatload of results. They'll be sent to the template as an array of hash references. (The code itself performs one more manipulation to coerce the dates into the proper format with timeCalc(), but you probably really don't want to see that. It ought to be buried at sea near Innsmouth.) The results are assigned to $pagelist, which is actually a reference to the array.

The last trick is to determine which template to use. This will be important in a moment:


        $template = 'wikiRecent';

We'll return to the display briefly, but must return to the code to display a revision list. As with its cousin, the number of revisions to display will come from the revisionLimit form variable, the wikiRevisionLimit, or the sane default of 25. There's another nice interpolated header and another nasty SQL statement, which has also had surrounding Stygian nastiness edited for your protection:


        $slashdb->sqlSelectAllHashrefArray(

                'wikitext.version, title, description, date, nickname',

                'wikipage, wikitext, users',

                'wikipage.wid = wikitext.wid AND users.uid = wikitext.uid ' .

                        "AND title = " .  $slashdb->sqlQuote($page),

                "order by version DESC limit $revs>

        )

The difference here is that we're concerned about versions and a single title. This is the place to change things to pull by wid, if that's important. The results are also assigned to an anonymous array pointed to by $pagelist, after the appropriate date manipulation. A final assignment chooses the correct template:


        $template = 'wikiRevisions';

After all that work, displaying things is easy. It doesn't matter that $page may be empty, as you'll see when we discuss the templates:


        slashDisplay($template, {

                page        => $page,

                pagelist    =" $pagelist,

        });


        footer();

This function could be improved somewhat. On a site with an excessive number of revisions or of available pages, there's a potential denial of service attack, if someone were to request a huge revisionLimit or recentLimit. There are no UI widgets for this, but the potential still exists.

Also, the SQL commands can use up a lot of memory. It may be better to use sqlSelectMany() to prepare a statement handle, passing that to the templates instead of pulling all results into memory. The general Slash database philosophy is that database access is slow, so it prefers to trade memory for speed. This is worth considering, however.

Templates

The Slash Wiki comes with seven templates to customize page display. This makes it much easier to separate formatting from logic. As well, the template language is simple and effective, and really beats embedding HTML in applets all to pieces.

While the templates are very powerful, there are only two tricks to using them in Slash. The first is knowing how to use slashDisplay(). As demonstrated before, its first argument is the name of the template to use. The second argument is a reference to a hash of data to be passed to the template. The third argument is extra options, and it's not used terribly often.

The second trick is that templates can be associated with pages. That is, there's a template named header;wiki;default. Its name is header, it is associated with the wiki page (or applet), and it belongs to the default section. There's also a header associated with the misc page and another belonging to the light section. We don't have to do anything specific to call the Wiki templates, and there are no name collisions as long as no one adds another wiki.pl applet.

The header Template

This template begins the HTML page sent to a browser. It also governs the boxes down the left side of a page. For style points, the Slash Wiki adds a SlashWiki menu box. The bulk of this template is stolen from the miscellaneous header template, but a few lines are different:


        [% IF constants.run_ads && constants.wikiRunAds %]

                <!-- ad code -->

        [% END %]

This is a modification of the standard header, and is used to display banner ads on pages. The wikiNoAds variable allows ads to be suppressed. Even if you run ads, you may make the wiki available only to administrative users, and may not want the distraction of flashing monkeys. (You have the option, though, if you only run ads for the Kudra world domination fund.)


        [% UNLESS title; title = "SlashWiki Menu"; END %]

        [% contents = PROCESS wikiMenu %]

        [% PROCESS fancybox

            width = 100

        %]

At heart, it simply calls the wikiMenu template, assigning the results to the contents variable and then calls the fancybox template, passing a width parameter (which feels like a kludge). fancybox expects title and contents parameters as well, and they're visible, with a default title if none has been provided. This draws a nice little menu.

The wikiMenu Template

This template renders the menu itself. It's separate from the header so that it can be relocated elsewhere, including in a Slashbox. This is the only template not assigned to the wiki page, simply to make it available everywhere. This is mostly an HTML form, though it does offer a link to the current page's revision list, if there's a current Wiki page. If the current page is a preview of a Wiki update, there's no good reason to display revisions, as that could drop an important change. In this case, the preview parameter will evaluate to a true value:


        [% UNLESS page; page = form.page; END %]

        [% IF page and ! form.preview %]

                <a href="[% "$constants.rootdir/wiki.pl?page=$page&amp;op=list"

        %]">Revisions</a> of [% page %]

        [% END %]

The rest is mostly bare HTML, with another link to the list of recently modified Wiki pages:


        <a href="[% "$constants.rootdir/wiki.pl?op=list" %]">Recent Changes</a>

        <br />

        <form method="POST">

        Search for:

        <input type="text" name="page" />
        <input type="hidden" name="op" value="display" />

        <input type="hidden" name="version" value='' />

        <br />

        <input type="submit" value="Go!" />

        </form>

Be sure to notice the interpolation of the rootdir configuration variable into the ``Recent Changes'' link.

The wikiView Template

By far the most complex of the templates, this takes several variables and displays them somewhat nicely. Most of the code simply interpolates passed-in variables into HTML formatting constructs. It has a few complications, namely that it can display empty pages, several variables are optional, previews should be marked, and that it must suppress the editing widgets for the readonly mode described earlier.

After displaying the page title, the template checks to see if there's anything in the raw variable. If this is empty, we reasonably assume that this is a new page, and so display the contents of the wikiEmptyPage template:


        [% INCLUDE wikiEmptyPage %]

If there's something in raw (we could also check cooked, for that matter), there's Wiki page metadata to be displayed:


        <p><strong>

        [% IF nickname -%]

                by <a href="[% "$constants.rootdir/~$nickname" | fixurl %]">

                [% nickname == user.nickname ? 'you' : nickname %]</>

        [% END -%]


        on [% date %]


        [%- IF version -%]

        , version [% version %]

        [%- END -%]


        [%- IF description -%]

                (<em>[% description %]</em>)

        [% END %].</strong>


        </p>

        <hr />

        [% IF form.preview -%]

                <h1>Unsaved Preview!</h1>

        [% END %]

        [% cooked %]

If you're not familiar with Template Toolkit semantics, that looks pretty imposing, but it's almost readable as English. (Or you could read Appendix C.) If there's a nickname, we compare it to the user's nickname. If they match, we'll change the author to ``you'' instead of to the user's nickname. It's more personal that way. Whatever we display, we'll link to the user's homepage on the system. The assumption here is that my esteemed coauthor Brian's clever hack for username homepage redirections works. It's a default feature.

We expect a date parameter, and version and description will probably be there, but if they aren't, we won't display any empty fields. One final nice feature is to label previews clearly. If there's a preview parameter passed to the applet, it's probably because the user hit the preview button. We'll show a nice reminder that nothing has been saved.

Some of the template directives have extra dashes. This is simply to suppress any whitespace between the HTML and the results of the directive. If this weren't there, we'd have to smash everything together in tag soup. It looks much nicer this way.

At the very of this snippet, the cooked (HTML formatted) version of the page is displayed. It might be clearer to check for cooked in this case.

The rest of the template is a simple form, which provides the required parameters, like wid, op (set to ``update''), the page title, the raw text, and the description along with the preview and save buttons. The whole thing is wrapped in a readonly check:


        [% IF readonly %]

                <a href="[% "$constants.rootdir/wiki.pl?page=$title" %]">

				latest version</a>

        [% ELSE %]

                big old form

        [% END %]

Again, the only out of the ordinary thing is the interpolated link, and that idiom has cropped up before.

The wikiRecent Template

This template only needs to loop through the pagelist array (array reference, though Template Toolkit handles dereferencing transparently), displaying the title, the nickname of the last user to modify the page, the modification date, and, if present the description and version number of each of the changes.

To avoid presenting an empty page if something is horribly wrong, there's one little trick at the top of the template:


        [% UNLESS pagelist.0 %]

                No recent changes.  Think tabula rasa.

        [% END %]

Unless the array reference has at least one member (at index 0), nothing will be displayed by the loop. We'll refer to the theories of John Locke, who admittedly didn't have hyperlinks in mind. It beats no feedback anyway.

The wikiRevisions Template

This template is very similar to wikiRecent, as it must display similar information. There are two differences. The links from this page must take into account version numbers (so they add the version parameter), and the start of the template lists the name of the page for which it lists revisions:


        <p>The last several revisions of '[% pagelist.0.title %]' are:</p>

Provided there's at least one element of the array referenced by pagelist, that's all we need. Aside from the slightly different formatting, these two templates could be merged very easily.

It is possible that the user could request the revision list of a nonexistent page, for whatever reason -- perhaps having clicked on the "Revisions" link for an empty page. It's hard to prevent this from happening due to the way the wikiMenu is produced. Of course, it's also possible someone could construct a URI with the appropriate parameters, or there could be a database error. In any case, the template needs to handle the possibility that there are no revisions. We wrap everything so far in a case statement:


    [% IF pagelist.0.title %]

       <p>The last several revisions of '[% pagelist.0.title %]'

       are:</p>

       ( continue as before )

    [% ELSE %]

       There appear to be no revisions of this page.  It probably doesn't exist.

    [% END %]

    

The wikiEmptyPage Template

The final and simplest template is a simple message displayed when a user requests a Wiki page that doesn't yet exist. It's worth knowing that templates don't have to contain code to be useful -- they're also handy for storing changeable text (especially things that could be translated):


        This page has not yet been created.

        Here's your chance to make your mark.

Default Data

There are several pieces of data to insert into the database to get things up and running -- we need a default Wiki page, and the applet refers to several configuration variables. It's not a real problem if they're not there, but it's more professional if they are.

Since the installation process calls the schema file, creating the wikipage and wikitext tables, adding data is a snap:


        INSERT INTO wikipage VALUES('default', 1, 1);

        INSERT INTO wikitext VALUES(1, now(), 1, 2, 'this is the default Wiki page',

                'default page', 'this is the default Wiki page');

The values follow the order given in the schema. If these commands succeed, the database will have one page named ``default'', ready and raring to go.

The configuration variables are also easy. They are added to the vars table in the order name, value, and description:


        INSERT INTO vars VALUES('wikiRevisionLimit', 25, 

                'the default number of wiki page revisions to list');

The other variables include wikiRecentLimit, wikiRunAds, wikiDefaultPage, and wikiExtendedLinks. These all have sane default values, though they can easily be modified by the standard Slash Vars interface.

Conclusion

There are lots of potential enhancements, and the Slash Wiki may not be as simple architecturally as a monolithic 500-line program, but it integrates nicely with Slash and has a lot of cool features. It only took a few days to write, and data persistence, templating, user accounts, configuration variables, form parsing, and installation came, basically, for free.

Though it began as simple software to run a Web site, the current version of Slash is a powerful application platform. It'll just take a few developers to come up with a killer application to show off some amazing new technique you can't live without. Maybe it's an XML-RPC interface to the Stories List, or an NNTP gateway. Maybe it's something completely unrelated to Weblogs. Whatever it is, someone out there has it in mind, and just needs a little information and a little push to send us all off on interesting new tangents.


chromatic is the author of O'Reilly's Running Weblogs with Slash.

Copyright © 2009 O'Reilly Media, Inc.