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.
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:
DBI::prepare(), you may
never need to use a raw statement handle. Also, as support
for databases besides MySQL improves, writing portable
plugins may be easier. (If you must create new tables, you
may have to find someone to port some SQL commands,
however.)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.
|
Related Reading Running Weblogs with Slash |
| "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.
|
| |
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.
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.
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.
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.
|
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.)
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.
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.
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.
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.
display()
FunctionThe 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();
|
update() FunctionThis 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();
list() FunctionBy 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.
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.
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.
|
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&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.
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.
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.
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 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.
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.
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.