#!/usr/bin/env perl use strict; use v5.10; # ====================[ crossbar.pl ]==================== =head1 NAME crossbar - An Oddmuse module for adding a site-wide footer, header, or other summary markup to all Oddmuse Wiki pages. =head1 SYNOPSIS crossbar is a drop-in substitute for the Sidebar module, which, as it is not entirely "backwards compatible" with the Sidebar module, is provided as a separate module and not revision of that module. crossbar provides additional functionality, including: =over =item Support for the Table of Contents and Footnotes modules. (The Sidebar module does not support these modules.) =item Support for displaying the crossbar anywhere in a page. (The Sidebar module does not permit the sidebar to be displayed anywhere except immediately after the header div and before the content div.) =back And so on. =head1 INSTALLATION crossbar is easily installable; move this file into the B directory for your Oddmuse Wiki. =cut AddModuleDescription('crossbar.pl', 'Crossbar Extension'); our ($q, $bol, $OpenPageName, @HtmlStack, @MyInitVariables, @MyRules, %AdminPages, $DeletedPage, $SidebarName, $TocIsApplyingAutomaticRules); # ....................{ CONFIGURATION }.................... our ($CrossbarPageName, $CrossbarDivIsOutsideContentDiv, $CrossbarSubstitutionPattern); =head1 CONFIGURATION crossbar is easily configurable; set these variables in the B file for your Oddmuse Wiki. =cut =head2 $CrossbarPageName The name of the page having crossbar markup. This markup will be added, automatically, to every Wiki page at the position matched by the C<$CrossbarSubstitutionPattern>, below. =cut $CrossbarPageName = 'Crossbar'; =head2 $CrossbarDivIsOutsideContentDiv A boolean that, if true, places the
...
block "outside" the
...
block; otherwise, this places it inside the
...
block. Generally, placing the crossbar div outside the content div gives a cleaner, sensibler aesthetic. (Your mileage may vary!) By default, this boolean is true. =cut $CrossbarDivIsOutsideContentDiv = 1; =head2 $CrossbarSubstitutionPattern The regular expression matching the position in each page to place the crossbar for that page. While, theoretically, this can be any pattern, it tends to be one the following two: =over =item '^'. This places the sidebar for each page immediately after that page's header and before that page's content. =item '$'. This places the sidebar for each page immediately after that page's content and before that page's footer. =back This module uses the first regular expression, by default. =cut $CrossbarSubstitutionPattern = '^'; # ....................{ INITIALIZATION }.................... push(@MyInitVariables, \&CrossbarInit); # A boolean that, if true, indicates that a crossbar has already been applied to # this page. This prevents application of a crossbar onto pages included by this # current page -- and, in general, protects against "reentrant recursion." my $CrossbarIsApplied; sub CrossbarInit { $CrossbarIsApplied = ''; $CrossbarPageName = FreeToNormal($CrossbarPageName); # spaces to underscores # Add a link to the crossbar page to the "Administration" page. $AdminPages{$CrossbarPageName} = 1; # If pulling the crossbar div outside the content div, we redefine the # default PrintPageContent() function to do this. if ($CrossbarDivIsOutsideContentDiv) { *PrintPageContentCrossbarOld = \&PrintPageContent; *PrintPageContent = \&PrintPageContentCrossbar; } # If this user is an authenticated administrator, forcefully clear the page # cache whenever saving the crossbar page. if (UserIsAdmin()) { *SaveCrossbarOld = \&Save; *Save = \&SaveCrossbar; } # If the Table of Contents module is also installed, we must prevent handling # of any Table of Contents-specific code when in the # '
...
' block. Why? Because: Table of Contents- # specific code adds unique identifiers to HTML headers, Crossbar pages may # contain HTML headers, and those HTML headers should not have unique # identifiers added to them, since adding unique identifiers to Crossbar page # headers would add those headers to the Table of Contents for //every// page. # (Trust us on this one...) if (defined &RunMyRulesToc) { *RunMyRulesCrossbarOld = \&RunMyRules; *RunMyRules = \&RunMyRulesCrossbar; } } # ....................{ MARKUP =before }.................... *OldCrossbarApplyRules = \&ApplyRules; *ApplyRules = \&NewCrossbarApplyRules; sub NewCrossbarApplyRules { my $text = shift; if (not $CrossbarIsApplied and not TextIsFile($text) and (not $SidebarName or $OpenPageName ne $SidebarName)) { my $crossbar_markup = GetPageContent($CrossbarPageName); if ($crossbar_markup and $crossbar_markup !~ m~^(\s*$|$DeletedPage)~) { $CrossbarIsApplied = 1; $text =~ s~$CrossbarSubstitutionPattern~ "\n\n<crossbar>\n\n".QuoteHtml($crossbar_markup). "\n\n</crossbar>\n\n"~e; } } return OldCrossbarApplyRules($text, @_); } # ....................{ MARKUP }.................... push(@MyRules, \&CrossbarRule); SetHtmlEnvironmentContainer('div', '^class="crossbar"$'); sub CrossbarRule { if ($bol) { if ( m~\G\<crossbar\>~cg) { return ($HtmlStack[0] eq 'p' ? CloseHtmlEnvironment() : '') .AddHtmlEnvironment ('div', 'class="crossbar"'); } elsif (m~\G\</crossbar\>~cg) { return CloseHtmlEnvironment('div', '^class="crossbar"$') # If pulling the crossbar div outside the content div, we mark the point # immediately after the close of the crossbar div with an HTML comment; # this allows us to match the contents of the div with a clean regular # expression. (A bit complicated, that one...) .($CrossbarDivIsOutsideContentDiv ? '' : ''); } } return; } =head2 RunMyRulesCrossbar Redefines the Table of Contents module's C function, when that module is installed, so as to apply a hacky, Crossbar-specific fix. =cut sub RunMyRulesCrossbar { my $TocIsApplyingAutomaticRulesOld = $TocIsApplyingAutomaticRules; $TocIsApplyingAutomaticRules = '' if InElement('div', '^class="crossbar"$'); my $html = RunMyRulesCrossbarOld(@_); $TocIsApplyingAutomaticRules = $TocIsApplyingAutomaticRulesOld; return $html; } # ....................{ BROWSING }.................... =head2 PrintPageContentCrossbar Redefines the default C function so as to extract the crossbar "" outside the content "", when so desired. =cut sub PrintPageContentCrossbar { my $html = ''; my $crossbar_pattern = '(
.*?
)'; { local *STDOUT; open( STDOUT, '>', \$html) or die "Can't open memory file: $!"; PrintPageContentCrossbarOld(@_); close STDOUT; } # If the crossbar div is placed immediately after the content div, place it # immediately before the content div. if (not ($html =~ s~(
)$crossbar_pattern~$2$1~)) { # Otherwise, if the crossbar div is placed immediately before the end of the # content div, place it immediately after the end of the content div. $html =~ s~$crossbar_pattern(.*?
)~$2$1~; } print $html; } # ....................{ EDITING }.................... *GetEditFormCrossbarOld = \&GetEditForm; *GetEditForm = \&GetEditFormCrossbar; sub GetEditFormCrossbar { my ($page_name) = @_; return ($page_name eq $CrossbarPageName ? $q->p({-class=> 'crossbar_edit_message'}, T(UserIsAdmin() ? 'You are currently logged in as an administrator. ' .'Saving this page propagates your crossbar changes to ' .'all other pages by forcefully clearing this Wiki\'s page cache.' : 'You are not currently logged in as an administrator. ' .'Saving this page only propagates your crossbar changes to newly ' .'created or edited pages — but don\'t let that deter you!')) : '') .GetEditFormCrossbarOld(@_); } # ....................{ SAVING }.................... =head2 SaveCrossbar Clears the page cache whenever a user saves the crossbar page. Why? Because the the contents of the crossbar page is injected into every other page. Consequently, when the crossbar page changes, the contents of other pages are also changed; and must have their caches forcefully cleared, to ensure they are changed. =cut sub SaveCrossbar { my ($page_name) = @_; SaveCrossbarOld(@_); if ($page_name eq $CrossbarPageName) { # Prevent the RequestLockOrError() and ReleaseLock() functions from doing # anything while in the DoClearCache() method, since the default Save() # function already obtains the lock. (We can't obtain it twice!) *RequestLockOrErrorCrossbarOld = \&RequestLockOrError; *RequestLockOrError = \&RequestLockOrErrorCrossbarNoop; *ReleaseLockCrossbarOld = \&ReleaseLock; *ReleaseLock = \&ReleaseLockCrossbarNoop; # Clear the page cache, now. Go! (Note: this prints a heap of HTML.) DoClearCache(); # Restore locking functionality. *RequestLockOrError = \&RequestLockOrErrorCrossbarOld; *ReleaseLock = \&ReleaseLockCrossbarOld; } } sub RequestLockOrErrorCrossbarNoop { } sub ReleaseLockCrossbarNoop { } =head1 COPYRIGHT AND LICENSE The information below applies to everything in this distribution, except where noted. Copyleft 2008 by B.w.Curry . This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see L. =cut