crossbar.pl 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. #!/usr/bin/env perl
  2. use strict;
  3. use v5.10;
  4. # ====================[ crossbar.pl ]====================
  5. =head1 NAME
  6. crossbar - An Oddmuse module for adding a site-wide footer, header, or other
  7. summary markup to all Oddmuse Wiki pages.
  8. =head1 SYNOPSIS
  9. crossbar is a drop-in substitute for the Sidebar module, which, as it is not
  10. entirely "backwards compatible" with the Sidebar module, is provided as a
  11. separate module and not revision of that module.
  12. crossbar provides additional functionality, including:
  13. =over
  14. =item Support for the Table of Contents and Footnotes modules. (The Sidebar
  15. module does not support these modules.)
  16. =item Support for displaying the crossbar anywhere in a page. (The Sidebar
  17. module does not permit the sidebar to be displayed anywhere except
  18. immediately after the header div and before the content div.)
  19. =back
  20. And so on.
  21. =head1 INSTALLATION
  22. crossbar is easily installable; move this file into the B<wiki/modules/>
  23. directory for your Oddmuse Wiki.
  24. =cut
  25. AddModuleDescription('crossbar.pl', 'Crossbar Extension');
  26. our ($q, $bol, $OpenPageName, @HtmlStack, @MyInitVariables, @MyRules, %AdminPages, $DeletedPage, $SidebarName, $TocIsApplyingAutomaticRules);
  27. # ....................{ CONFIGURATION }....................
  28. our ($CrossbarPageName,
  29. $CrossbarDivIsOutsideContentDiv,
  30. $CrossbarSubstitutionPattern);
  31. =head1 CONFIGURATION
  32. crossbar is easily configurable; set these variables in the B<wiki/config.pl>
  33. file for your Oddmuse Wiki.
  34. =cut
  35. =head2 $CrossbarPageName
  36. The name of the page having crossbar markup. This markup will be added,
  37. automatically, to every Wiki page at the position matched by the
  38. C<$CrossbarSubstitutionPattern>, below.
  39. =cut
  40. $CrossbarPageName = 'Crossbar';
  41. =head2 $CrossbarDivIsOutsideContentDiv
  42. A boolean that, if true, places the <div class="crossbar">...</div> block
  43. "outside" the <div class="content browse">...</div> block; otherwise, this
  44. places it inside the <div class="content browse">...</div> block. Generally,
  45. placing the crossbar div outside the content div gives a cleaner, sensibler
  46. aesthetic. (Your mileage may vary!)
  47. By default, this boolean is true.
  48. =cut
  49. $CrossbarDivIsOutsideContentDiv = 1;
  50. =head2 $CrossbarSubstitutionPattern
  51. The regular expression matching the position in each page to place the crossbar
  52. for that page. While, theoretically, this can be any pattern, it tends to be one
  53. the following two:
  54. =over
  55. =item '^'. This places the sidebar for each page immediately after that page's
  56. header and before that page's content.
  57. =item '$'. This places the sidebar for each page immediately after that page's
  58. content and before that page's footer.
  59. =back
  60. This module uses the first regular expression, by default.
  61. =cut
  62. $CrossbarSubstitutionPattern = '^';
  63. # ....................{ INITIALIZATION }....................
  64. push(@MyInitVariables, \&CrossbarInit);
  65. # A boolean that, if true, indicates that a crossbar has already been applied to
  66. # this page. This prevents application of a crossbar onto pages included by this
  67. # current page -- and, in general, protects against "reentrant recursion."
  68. my $CrossbarIsApplied;
  69. sub CrossbarInit {
  70. $CrossbarIsApplied = '';
  71. $CrossbarPageName = FreeToNormal($CrossbarPageName); # spaces to underscores
  72. # Add a link to the crossbar page to the "Administration" page.
  73. $AdminPages{$CrossbarPageName} = 1;
  74. # If pulling the crossbar div outside the content div, we redefine the
  75. # default PrintPageContent() function to do this.
  76. if ($CrossbarDivIsOutsideContentDiv) {
  77. *PrintPageContentCrossbarOld = \&PrintPageContent;
  78. *PrintPageContent = \&PrintPageContentCrossbar;
  79. }
  80. # If this user is an authenticated administrator, forcefully clear the page
  81. # cache whenever saving the crossbar page.
  82. if (UserIsAdmin()) {
  83. *SaveCrossbarOld = \&Save;
  84. *Save = \&SaveCrossbar;
  85. }
  86. # If the Table of Contents module is also installed, we must prevent handling
  87. # of any Table of Contents-specific code when in the
  88. # '<div class="crossbar">...</div>' block. Why? Because: Table of Contents-
  89. # specific code adds unique identifiers to HTML headers, Crossbar pages may
  90. # contain HTML headers, and those HTML headers should not have unique
  91. # identifiers added to them, since adding unique identifiers to Crossbar page
  92. # headers would add those headers to the Table of Contents for //every// page.
  93. # (Trust us on this one...)
  94. if (defined &RunMyRulesToc) {
  95. *RunMyRulesCrossbarOld = \&RunMyRules;
  96. *RunMyRules = \&RunMyRulesCrossbar;
  97. }
  98. }
  99. # ....................{ MARKUP =before }....................
  100. *OldCrossbarApplyRules = \&ApplyRules;
  101. *ApplyRules = \&NewCrossbarApplyRules;
  102. sub NewCrossbarApplyRules {
  103. my $text = shift;
  104. if (not $CrossbarIsApplied
  105. and not TextIsFile($text)
  106. and (not $SidebarName or $OpenPageName ne $SidebarName)) {
  107. my $crossbar_markup = GetPageContent($CrossbarPageName);
  108. if ($crossbar_markup and $crossbar_markup !~ m~^(\s*$|$DeletedPage)~) {
  109. $CrossbarIsApplied = 1;
  110. $text =~ s~$CrossbarSubstitutionPattern~
  111. "\n\n&lt;crossbar&gt;\n\n".QuoteHtml($crossbar_markup).
  112. "\n\n&lt;/crossbar&gt;\n\n"~e;
  113. }
  114. }
  115. return OldCrossbarApplyRules($text, @_);
  116. }
  117. # ....................{ MARKUP }....................
  118. push(@MyRules, \&CrossbarRule);
  119. SetHtmlEnvironmentContainer('div', '^class="crossbar"$');
  120. sub CrossbarRule {
  121. if ($bol) {
  122. if ( m~\G\&lt;crossbar\&gt;~cg) {
  123. return ($HtmlStack[0] eq 'p' ? CloseHtmlEnvironment() : '')
  124. .AddHtmlEnvironment ('div', 'class="crossbar"');
  125. }
  126. elsif (m~\G\&lt;/crossbar\&gt;~cg) {
  127. return
  128. CloseHtmlEnvironment('div', '^class="crossbar"$')
  129. # If pulling the crossbar div outside the content div, we mark the point
  130. # immediately after the close of the crossbar div with an HTML comment;
  131. # this allows us to match the contents of the div with a clean regular
  132. # expression. (A bit complicated, that one...)
  133. .($CrossbarDivIsOutsideContentDiv ? '<!-- crossbar/-->' : '');
  134. }
  135. }
  136. return;
  137. }
  138. =head2 RunMyRulesCrossbar
  139. Redefines the Table of Contents module's C<RunMyRulesToc> function, when that
  140. module is installed, so as to apply a hacky, Crossbar-specific fix.
  141. =cut
  142. sub RunMyRulesCrossbar {
  143. my $TocIsApplyingAutomaticRulesOld = $TocIsApplyingAutomaticRules;
  144. $TocIsApplyingAutomaticRules = '' if InElement('div', '^class="crossbar"$');
  145. my $html = RunMyRulesCrossbarOld(@_);
  146. $TocIsApplyingAutomaticRules = $TocIsApplyingAutomaticRulesOld;
  147. return $html;
  148. }
  149. # ....................{ BROWSING }....................
  150. =head2 PrintPageContentCrossbar
  151. Redefines the default C<PrintPageContent> function so as to extract the
  152. crossbar "<div...>" outside the content "<div...>", when so desired.
  153. =cut
  154. sub PrintPageContentCrossbar {
  155. my $html = '';
  156. my $crossbar_pattern = '(<div class="crossbar">.*?</div>)<!-- crossbar/-->';
  157. { local *STDOUT;
  158. open( STDOUT, '>', \$html) or die "Can't open memory file: $!";
  159. PrintPageContentCrossbarOld(@_);
  160. close STDOUT; }
  161. # If the crossbar div is placed immediately after the content div, place it
  162. # immediately before the content div.
  163. if (not ($html =~ s~(<div class="content browse" lang="[a-z]*">)$crossbar_pattern~$2$1~)) {
  164. # Otherwise, if the crossbar div is placed immediately before the end of the
  165. # content div, place it immediately after the end of the content div.
  166. $html =~
  167. s~$crossbar_pattern(.*?<div class="wrapper close"></div></div>)~$2$1~;
  168. }
  169. print $html;
  170. }
  171. # ....................{ EDITING }....................
  172. *GetEditFormCrossbarOld = \&GetEditForm;
  173. *GetEditForm = \&GetEditFormCrossbar;
  174. sub GetEditFormCrossbar {
  175. my ($page_name) = @_;
  176. return
  177. ($page_name eq $CrossbarPageName ?
  178. $q->p({-class=> 'crossbar_edit_message'},
  179. T(UserIsAdmin()
  180. ? '<strong>You are currently logged in as an administrator.</strong> '
  181. .'Saving this page propagates your crossbar changes to '
  182. .'<em>all</em> other pages by forcefully clearing this Wiki\'s page cache.'
  183. : '<strong>You are not currently logged in as an administrator.</strong> '
  184. .'Saving this page only propagates your crossbar changes to <em>newly '
  185. .'created </em> or <em>edited</em> pages &#x2014; but don\'t let that deter you!')) : '')
  186. .GetEditFormCrossbarOld(@_);
  187. }
  188. # ....................{ SAVING }....................
  189. =head2 SaveCrossbar
  190. Clears the page cache whenever a user saves the crossbar page. Why?
  191. Because the the contents of the crossbar page is injected into every
  192. other page. Consequently, when the crossbar page changes, the contents
  193. of other pages are also changed; and must have their caches forcefully
  194. cleared, to ensure they are changed.
  195. =cut
  196. sub SaveCrossbar {
  197. my ($page_name) = @_;
  198. SaveCrossbarOld(@_);
  199. if ($page_name eq $CrossbarPageName) {
  200. # Prevent the RequestLockOrError() and ReleaseLock() functions from doing
  201. # anything while in the DoClearCache() method, since the default Save()
  202. # function already obtains the lock. (We can't obtain it twice!)
  203. *RequestLockOrErrorCrossbarOld = \&RequestLockOrError;
  204. *RequestLockOrError = \&RequestLockOrErrorCrossbarNoop;
  205. *ReleaseLockCrossbarOld = \&ReleaseLock;
  206. *ReleaseLock = \&ReleaseLockCrossbarNoop;
  207. # Clear the page cache, now. Go! (Note: this prints a heap of HTML.)
  208. DoClearCache();
  209. # Restore locking functionality.
  210. *RequestLockOrError = \&RequestLockOrErrorCrossbarOld;
  211. *ReleaseLock = \&ReleaseLockCrossbarOld;
  212. }
  213. }
  214. sub RequestLockOrErrorCrossbarNoop { }
  215. sub ReleaseLockCrossbarNoop { }
  216. =head1 COPYRIGHT AND LICENSE
  217. The information below applies to everything in this distribution,
  218. except where noted.
  219. Copyleft 2008 by B.w.Curry <http://www.raiazome.com>.
  220. This program is free software; you can redistribute it and/or modify
  221. it under the terms of the GNU General Public License as published by
  222. the Free Software Foundation; either version 3 of the License, or
  223. (at your option) any later version.
  224. This program is distributed in the hope that it will be useful,
  225. but WITHOUT ANY WARRANTY; without even the implied warranty of
  226. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  227. GNU General Public License for more details.
  228. You should have received a copy of the GNU General Public License
  229. along with this program. If not, see L<http://www.gnu.org/licenses/>.
  230. =cut