ONLamp.com
oreilly.comSafari Books Online.Conferences.

advertisement


Slash's Wiki Plugin
Pages: 1, 2, 3, 4

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();

Pages: 1, 2, 3, 4

Next Pagearrow





Sponsored by: