This patch allows wiki pages to be organized into a tree.
The tree has a virtual root, called top. A page can have a single parent, and any number of children. If a page's parent is top, the page is on the top of the hierarchy. In turn, if another page's parent is this toplevel page, that page will be on the second level of the hierarchy, and so on.
The hierarchy is useful for navigation, and is displayed beside the page content. This is similar to the way content management systems work, and in fact the screenshot that you see below was inspired by Plone (http://plone.org), an open source CMS.
The following screenshot demonstrates how usemod looks after this patch is applied:
Attributes about the pages are stored inside the text of the page itself. The following attributes are used:
So, in order to make a page a toplevel page in the hierarchy, start it with the following line:
<parent top> <title Start here> <icon exclamation.gif>
This might look confusing, but it's really simple, once you get the hang of it.
The installation of this modification is a little more involved than simply applying a patch. You need to do the following steps:
action=maintain
(see AdminFeatures and Actions). This will create the metadata
in your database directory. If the metadata goes out of synch (shouldn't happen under normal circumstances), re-run maintain on your site.
The hierarchy can be as deep as desired. This does not allow usemod SubPages to be deeper than usual (2 levels), this hierarchy is independent of the subpages. This patch can be used to organize the normally "flat" wiki pages into a tree.
The patch makes usemod better suited for company knowledge bases, and in fact was developed for my current employer.
Add the following to your CSS:
.wikifooter, .spacer { clear: both; } .spacer { clear: both; } #mainColumn { margin-left: 200px; padding: 10px; } #leftColumn { width: 190px; float: left; font-size: 75%; padding: 5px; } #leftColumn UL { padding: 0; margin: 0 0 0 1em; list-style-type: none; } #leftColumn LI { padding: 0; margin: 0; } #leftColumn LI DIV { background-repeat: no-repeat; background-position: 0 0.5em; } #leftColumn A { padding: 0.5em; text-decoration: none; padding-left: 24px; display: block; } #leftColumn .currentItem A { color: #000; font-weight: bold; }
--- OriginalUsemodWiki.pl 2003-09-11 14:21:02.000000000 +0200 +++ HierarchicalUsemodWiki.pl 2006-04-27 17:58:22.000000000 +0200 @@ -63,6 +63,8 @@ $q $Now $UserID $TimeZoneOffset $ScriptName $BrowseCode $OtherCode $AnchoredLinkPattern @HeadingNumbers $TableOfContents $QuotedFullUrl $ConfigError $UploadPattern ); + +use vars qw(%Metadata $IconDir $LeftColumnTop $LeftColumnBottom $UpperLetter $LowerLetter); # == Configuration ===================================================== $DataDir = "/tmp/mywikidb"; # Main wiki directory @@ -89,6 +91,8 @@ $NotFoundPg = ""; # Page for not-found links ("" for blank pg) $EmailFrom = "Wiki"; # Text for "From: " field of email notes. $SendMail = "/usr/sbin/sendmail"; # Full path to sendmail executable +$LeftColumnTop = ''; # HTML above the navigation in the left col +$LeftColumnBottom = ''; # HTML after the navigation in the left col $FooterNote = ""; # HTML for bottom of every page $EditNote = ""; # HTML notice above buttons on edit page $MaxPost = 1024 * 210; # Maximum 210K posts (about 200K for pages) @@ -208,6 +212,7 @@ $RcOldFile = "$DataDir/oldrclog"; # Old RecentChanges logfile $IndexFile = "$DataDir/pageidx"; # List of all pages $EmailFile = "$DataDir/emails"; # Email notification lists +$IconDir = "./"; # Path to icons (must have trailing slash!) if ($RepInterMap) { push @ReplaceableFiles, $InterFile; @@ -242,7 +247,7 @@ # == Common and cache-browsing code ==================================== sub InitLinkPatterns { - my ($UpperLetter, $LowerLetter, $AnyLetter, $LpA, $LpB, $QDelim); + my ($AnyLetter, $LpA, $LpB, $QDelim); # Field separators are used in the URL-style patterns below. if ($NewFS) { @@ -416,6 +421,7 @@ %InterSite = (); $MainPage = "."; # For subpages only, the name of the top-level page $OpenPageName = ""; # Currently open page + &LoadMetadata(); &CreateDir($DataDir); # Create directory if it doesn't exist if (!-d $DataDir) { &ReportError(Ts('Could not create %s', $DataDir) . ": $!"); @@ -540,7 +546,7 @@ } $MainPage = $id; $MainPage =~ s|/.*||; # Only the main page name (remove subpage) - $fullHtml = &GetHeader($id, &QuoteHtml($id), $oldId); + $fullHtml = &GetHeader($id, '', $oldId); if ($revision ne '') { if (($revision eq $Page{'revision'}) || ($goodRevision ne '')) { $fullHtml .= '<b>' . Ts('Showing revision %s', $revision) . "</b><br>"; @@ -985,7 +991,7 @@ my ($id) = @_; my ($html, $canEdit, $row, $newText); - print &GetHeader('', Ts('History of %s', $id), '') . '<br>'; + print &GetHeader($id, Ts('History of %s', $id), '') . '<br>'; &OpenPage($id); &OpenDefaultText(); $newText = $Text{'text'}; @@ -1082,6 +1088,197 @@ return $html; } +sub GetPageMetadata +{ + my ($id) = @_; + my ($metadata, $title); + + $title = $id; + $title =~ s/($LowerLetter)($UpperLetter)/$1 $2/g; + + unless (defined $Metadata{$id}) { + return { 'title' =>$title, 'children' => [], }; + } + + $metadata = { + 'children' => [], + %{$Metadata{$id}} + }; + $metadata->{title} = $title unless $metadata->{title}; + return $metadata; +} + +sub SetPageMetadata +{ + my ($id, $text) = @_; + my ($title, $parent, $icon, $oldmetadata, $oldparent, $children); + + ($title) = $text =~ /<title\s+([^>]+)\s*>/; + ($parent) = $text =~ /<parent\s+([^>]+)\s*>/; + ($icon) = $text =~ /<icon\s+([^>]+)\s*>/; + + unless ($parent) { + ($parent) = $text =~ /\[\[([^\]]*)\]\]\s*$/s; + } + + $oldmetadata = &GetPageMetadata($id); + $oldparent = $oldmetadata->{parent}; + + unless ($oldparent eq $parent) { + if ($oldparent) { + $oldmetadata = &GetPageMetadata($oldparent); + @{$oldmetadata->{children}} = ( grep($_ ne $id, @{$oldmetadata->{children}}) ); + } + if ($parent) { + $oldmetadata = &GetPageMetadata($parent); + @{$oldmetadata->{children}} = sort ( @{$oldmetadata->{children}}, $id ); + } + } + + $Metadata{$id} = {} unless exists $Metadata{$id}; + + $Metadata{$id} = { + %{$Metadata{$id}}, + 'title' => $title, + 'parent' => $parent, + 'icon' => $icon, + }; +} + +sub WriteScalar +{ + my ($s) = @_; + + return "''" unless defined $s; + $s =~ s/'/\\'/g; + return "'$s'"; +} + +sub WriteList +{ + my ($l) = @_; + my ($item, $result); + + $result = '['; + foreach $item (@{$l}) { + $result .= &WriteScalar($item) . ','; + } + $result .= ']'; +} + +sub GetHierarchy { + my ($id) = @_; + my (%expanded, $parent); + + $parent = $id; + + while (not exists $expanded{$parent}) { + $expanded{$parent} = 1; + $parent = &GetPageMetadata($parent)->{parent}; + last unless defined $parent; + } + + return &GetHierarchyFrom('top', $id, \%expanded, 0); +} + +sub GetHierarchyListItem { + my ($id, $metadata, $iscurrent, $ishidden) = @_; + my ($result, $icon) = @_; + + $result = '<LI>'; + $icon = $metadata->{icon}; + unless ($icon) { + if ( scalar @{$metadata->{children}} ) { + $icon = 'folder_icon.gif'; + } else { + $icon = 'document_icon.gif'; + } + } + if ($icon) { + $icon = &QuoteHtml($icon); + $icon =~ s/\"/"/g; + $icon =~ s/\)/&\#41;/g; + $result .= "<div style=\"background-image: url(http://anonymouse.org/cgi-bin/anon-www.cgi/http://www.usemod.org/cgi-bin/$IconDir$icon)\">"; + } + if ($iscurrent) { + $result .= '<span class=currentItem>' . &GetPageLinkText($id, $metadata->{title}) . '</span>'; + } else { + $result .= &GetPageLinkText($id, $metadata->{title}); + } + if ($icon) { + $result .= "</div>"; + } + return $result; +} + +sub GetHierarchyFrom { + my ($id, $top, $expanded, $level) = @_; + my ($result, $metadata, $child, $children, $childmetadata, $icon); + + $result = ''; + $metadata = &GetPageMetadata($id); + $children = $metadata->{children}; + + if ( scalar @{$children} and $level < 10 ) { + $result = "<UL>\n"; + foreach $child (@{$children}) { + $childmetadata = &GetPageMetadata($child); + $result .= &GetHierarchyListItem($child, $childmetadata, $child eq $top); + $result .= &GetHierarchyFrom($child, $top, $expanded, $level + 1) if exists $expanded->{$child}; + $result .= "</LI>\n"; + } + $result .= "</UL>\n"; + } + return $result; +} + +sub LoadMetadata +{ + my ($data); + + %Metadata = (); + $data = &ReadFileOrDie("$DataDir/metadata"); + eval $data; die $@ if $@; +} + +sub SaveMetadata +{ + my ($id, $metadata); + + open (OUT, ">$DataDir/metadata") or die(T('can not write metadata')); + print OUT "\%Metadata = (\n"; + + while (($id, $metadata) = each %Metadata) { + print OUT " '$id' => {", + " 'title' => ", &WriteScalar( $metadata->{title} ), + ", 'icon' => ", &WriteScalar( $metadata->{icon} ), + ", 'parent' => ", &WriteScalar( $metadata->{parent} ), + ", 'children' => ", &WriteList( $metadata->{children} ), + " }, \n"; + } + + print OUT ");\n"; + close OUT; +} + +sub RebuildMetadata +{ + my ($id, @allpageslist); + + @allpageslist = &AllPagesList(); + %Metadata = (); + + foreach $id (@allpageslist, 'top') { + $Metadata{$id} = { 'id'=>$id, 'children' => [] }; + } + + foreach $id (@allpageslist) { + &OpenPage($id); + &OpenDefaultText(); + &SetPageMetadata($id, $Text{'text'}); + } +} + # ==== HTML and page-oriented functions ==== sub ScriptLinkChar { if ($SlashLinks) { @@ -1291,7 +1488,7 @@ if ($FreeLinks) { $title =~ s/_/ /g; # Display as spaces } - $result .= &GetHtmlHeader("$SiteName: $title"); + $result .= &GetHtmlHeader("$SiteName: " . ($title ? $title : &QuoteHtml($id))); return $result if ($embed); $result .= '<div class=wikiheader>'; @@ -1306,7 +1503,7 @@ } $header = &ScriptLink($HomePage, "<$logoImage>"); } - if ($id ne '') { + if ($id ne '' and $title eq '') { $result .= $q->h1($header . &GetBackLinksSearchLink($id)); } else { $result .= $q->h1($header . $title); @@ -1315,6 +1512,9 @@ $result .= &GetGotoBar($id) . "<hr class=wikilineheader>"; } $result .= '</div>'; + $result .= "\n<div id=leftColumn>\n$LeftColumnTop\n" . &GetHierarchy($id) . "\n$LeftColumnBottom\n</div>\n"; + $result .= "<div id=mainColumn>\n"; + return $result; } @@ -1379,14 +1579,20 @@ return $html; } +sub GetEndOfMainColumn { + return "<div class=spacer> </div>\n</div>\n"; +} + sub GetFooterText { my ($id, $rev) = @_; my $result; + $result = &GetEndOfMainColumn(); + if (&GetParam('embed', $EmbedWiki)) { - return $q->end_html; + return $result . $q->end_html; } - $result = '<div class=wikifooter>'; + $result .= '<div class=wikifooter>'; $result .= &GetFormStart(); $result .= &GetGotoBar($id); if (&UserCanEdit($id, 0)) { @@ -1443,14 +1649,16 @@ $result .= T($FooterNote); } $result .= '</div>'; - $result .= &GetMinimumFooter(); + $result .= $q->end_html; return $result; } sub GetCommonFooter { my ($html); - $html = '<hr class=wikilinefooter>' . '<div class=wikifooter>' + $html = &GetEndOfMainColumn(); + + $html .= '<hr class=wikilinefooter>' . '<div class=wikifooter>' . &GetFormStart() . &GetGotoBar('') . &GetSearchForm() . $q->endform; if ($FooterNote ne '') { @@ -1461,7 +1669,7 @@ } sub GetMinimumFooter { - return $q->end_html; + return &GetEndOfMainColumn() . $q->end_html; } sub GetFormStart { @@ -1570,6 +1778,9 @@ if ($RawHtml) { $pageText =~ s/<html>((.|\n)*?)<\/html>/&StoreRaw($1)/ige; } + $pageText =~ s/<title[^>]*>//; + $pageText =~ s/<icon[^>]*>//; + $pageText =~ s/<parent[^>]*>//; $pageText = &QuoteHtml($pageText); $pageText =~ s/\\ *\r?\n/ /g; # Join lines with backslash at end if ($ParseParas) { @@ -3182,7 +3393,7 @@ $id = &FreeToNormal($id); # Take care of users like Markus Lude :-) } if (!&UserCanEdit($id, 1)) { - print &GetHeader("", T('Editing Denied'), ""); + print &GetHeader($id, T('Editing Denied'), ""); if (&UserIsBanned()) { print T('Editing not allowed: user, ip, or network is blocked.'); print "<p>"; @@ -3217,7 +3428,7 @@ } $editRows = &GetParam("editrows", 20); $editCols = &GetParam("editcols", 65); - print &GetHeader('', &QuoteHtml($header), ''); + print &GetHeader($id, &QuoteHtml($header), ''); if ($revision ne '') { print "\n<b>" . Ts('Editing old revision %s.', $revision) . " " @@ -3300,12 +3511,13 @@ print "<h2>", T('Preview only, not yet saved'), "</h2>\n"; print '</div>'; } + print $q->endform; + print &GetEndOfMainColumn(); print '<div class=wikifooter>'; print &GetHistoryLink($id, T('View other revisions')) . "<br>\n"; print &GetGotoBar($id); - print $q->endform; print '</div>'; - print &GetMinimumFooter(); + print $q->end_html; } sub GetTextArea { @@ -3404,12 +3616,13 @@ &GetFormText('stylesheet', "", 30, 150); print '<br>', $q->submit(-name=>'Save', -value=>T('Save')), "\n"; print '</div>'; + print &GetEndOfMainColumn(); print "<hr class=wikilinefooter>\n"; print '<div class=wikifooter>'; print &GetGotoBar(''); print $q->endform; print '</div>'; - print &GetMinimumFooter(); + print $q->end_html; } sub GetFormText { @@ -3660,9 +3873,10 @@ print Ts('Login for user ID %s failed.', $uid); } print "<hr class=wikilinefooter>\n"; + print &GetEndOfMainColumn(); print &GetGotoBar(''); print $q->endform; - print &GetMinimumFooter(); + print $q->end_html; } sub GetNewUserId { @@ -3990,6 +4204,8 @@ $Section{'host'} = &GetRemoteHost(1); &SaveDefaultText(); &SavePage(); + &SetPageMetadata($id, $string); + &SaveMetadata(); &WriteRcLog($id, $summary, $isEdit, $editTime, $Section{'revision'}, $user, $Section{'host'}); if ($UseCache) { @@ -4274,6 +4490,8 @@ $status = &TrimRc(); # Consider error messages? &ReleaseLock(); } + &RebuildMetadata(); + &SaveMetadata(); print &GetCommonFooter(); } @@ -4550,6 +4768,8 @@ &UpdateLinksList($commandList, $doRC, $doText); print "<p>Finished command list."; } + &RebuildMetadata(); + &SaveMetadata(); print &GetCommonFooter(); } @@ -4613,6 +4833,9 @@ unlink($IndexFile) if ($UseIndex); &EditRecentChanges(1, $page, "") if ($doRC); # Delete page # Currently don't do anything with page text + &SetPageMetadata($page, ''); + delete $Metadata{$page}; + &SaveMetadata(); } # Given text, returns substituted text
(db) Very interesting concept.
I see you have programmed a function to create a page. Would it be possible to put the patch on the Wiki patches? This is something that could certainly be put in the next Usemod version. Excellent idea! Can we create pages in a sub-group?
My friend and I are working on a new wiki and we'll try your patch.
Since the outline on the left window can get very big, we'll put a sort of research box right on the top of the outline window in order to find a word very quickly in the outline. We'll send you the code if you are interested.
Thanks Peter!