forked from github/kensanata.oddmuse
replacement for the Sidebar module, for all Sidebar users requiring a cleaner integration of that module with other modules such as "toc.pl" and "footnotes.pl". This new module also improves Sidebar customizability by permitting users to place the "Crossbar" at any position in any page. * modules/forms.pl (FormsRules): Generalized so as to support both the Sidebar and Crossbar modules. * modules/toc.pl (RunMyRulesToc): Generalized so as to support both the Sidebar and Crossbar modules. * t/crossbar.t: New test suite, testing the Crossbar module.
307 lines
10 KiB
Perl
307 lines
10 KiB
Perl
#!/usr/bin/env perl
|
|
# ====================[ toc.pl ]====================
|
|
|
|
=head1 NAME
|
|
|
|
toc - An Oddmuse module for adding a "Table of Contents" to Oddmuse Wiki pages.
|
|
|
|
=head1 INSTALLATION
|
|
|
|
toc is easily installable; move this file into the B<wiki/modules/>
|
|
directory for your Oddmuse Wiki.
|
|
|
|
=cut
|
|
$ModulesDescription .= '<p>$Id: toc.pl,v 1.57 2008/11/23 22:13:29 leycec Exp $</p>';
|
|
|
|
# ....................{ CONFIGURATION }....................
|
|
|
|
=head1 CONFIGURATION
|
|
|
|
toc is easily configurable; set these variables in the B<wiki/config.pl> file
|
|
for your Oddmuse Wiki.
|
|
|
|
=cut
|
|
use vars qw($TocHeaderText
|
|
$TocClass
|
|
$TocAutomatic
|
|
$TocAnchorPrefix);
|
|
|
|
=head2 $TocHeaderText
|
|
|
|
The string to be displayed as the header for each page's table of contents.
|
|
|
|
=cut
|
|
$TocHeaderText = 'Contents';
|
|
|
|
=head2 $TocClass
|
|
|
|
The string to be used as the HTML class for each page's table of contents. (This
|
|
is the string with which your CSS stylesheet customizes table of contents.)
|
|
|
|
=cut
|
|
$TocClass = 'toc';
|
|
|
|
=head2 $TocAutomatic
|
|
|
|
A boolean that, if true, automatically prepends the table of contents to the
|
|
first header for a page or, if false, does not. If false, you must explicitly
|
|
add the table of contents to each page for which you'd like one by explicitly
|
|
adding the "<toc>" markup to that page.
|
|
|
|
By default, this boolean is true.
|
|
|
|
=cut
|
|
$TocAutomatic = 1;
|
|
|
|
=head2 $TocAnchorPrefix
|
|
|
|
The string for prefixing the names of toc anchor links with. The default
|
|
should be fine, generally; it creates toc anchor links resembling:
|
|
|
|
=over
|
|
|
|
=item L<http://your.wiki.com/SomePage#Heading1>. A link to the first header on
|
|
SomePage page for some wiki.
|
|
|
|
=item L<http://your.wiki.com/SomePage#Heading2>. A link to the second header on
|
|
SomePage page for some wiki.
|
|
|
|
=back
|
|
|
|
And so on. This provides Wiki users a "clean" mechanism for bookmarking,
|
|
marking, and sharing links to particular segments of a Wiki page.
|
|
|
|
=cut
|
|
$TocAnchorPrefix = 'Heading';
|
|
|
|
# ....................{ INITIALIZATION }....................
|
|
push(@MyInitVariables, \&TocInit);
|
|
|
|
# A number uniquely identifying this current header. This allows us to link each
|
|
# list entry in the table of contents to the header it refers to.
|
|
my $TocHeaderNumber;
|
|
|
|
sub TocInit {
|
|
$TocHeaderNumber = '';
|
|
}
|
|
|
|
# ....................{ MARKUP }....................
|
|
*RunMyRulesTocOld = *RunMyRules;
|
|
*RunMyRules = *RunMyRulesToc;
|
|
|
|
push(@MyRules, \&TocRule);
|
|
|
|
=head2 MARKUP
|
|
|
|
toc handles page markup resembling:
|
|
|
|
<toc header_text="$HeaderText" class="$Class">
|
|
|
|
Or, in its abbreviated form:
|
|
|
|
<toc "$HeaderText" "$Class">
|
|
|
|
Or, in its maximally abbreviated form:
|
|
|
|
<toc>
|
|
|
|
C<$HeaderText> is the header text for this table of contents: that is, text
|
|
heading the list of this table of contents. This is optional. If not specified,
|
|
it defaults to the value of the C<$TocHeaderText> variable.
|
|
|
|
C<$Class> is the HTML class for this table of contents, for CSS stylization of
|
|
that table. This is optional. If not specified, it defaults to "toc".
|
|
|
|
=cut
|
|
sub TocRule {
|
|
# <toc...> markup. This explicitly opens a new table of contents, if we
|
|
# haven't already opened one. There may be only one such table per page.
|
|
if ($bol and not $TocHeaderNumber and
|
|
m~\G<toc(/([A-Za-z\x80-\xff/]+))? # $1
|
|
(\s+(?:header_text\s*=\s*)?"(.+?)")? # $3
|
|
(\s+(?:class\s*=\s*)?"(.+?)")? # $5
|
|
>[ \t]*(\n|$)~cgx) { # $7
|
|
my ($toc_class_old, $toc_header_text, $toc_class) = ($2, $4, $6);
|
|
|
|
$TocHeaderNumber = 1;
|
|
$toc_header_text = $TocHeaderText if not defined $toc_header_text;
|
|
|
|
# A backwards-compatibility fix! Antiquated versions of this module
|
|
# accepted markup resembling:
|
|
# <toc/${CLASS_NAME_1}/${CLASS_NAME_2}/...>
|
|
#
|
|
# which this conditional converts to the more conventional:
|
|
# <toc class="${CLASS_NAME_1} ${CLASS_NAME_2} ...">
|
|
if ($toc_class_old) {
|
|
$toc_class = $toc_class_old;
|
|
$toc_class =~ tr~/~ ~;
|
|
} $toc_class = $TocClass.($toc_class ? ' '.$toc_class : '');
|
|
|
|
# If the topmost HTML tag is a paragraph, then the table of contents will
|
|
# be the first child element of that paragraph; however, embedding that
|
|
# table in a paragraph is quite unnecessary, and even obstructs our
|
|
# CSS stylization of that table elsewhere. In this case, we close this
|
|
# paragraph; this ensures that paragraph will have no content and
|
|
# therefore be removed, later, by the Oddmuse engine. This is slightly
|
|
# hacky -- but sufficiently necessary.
|
|
return ($HtmlStack[0] eq 'p' ? CloseHtmlEnvironment() : '')
|
|
.qq{<!-- toc header_text="$toc_header_text" class="$toc_class" -->}
|
|
.AddHtmlEnvironment('p');
|
|
}
|
|
return undef;
|
|
}
|
|
|
|
=head2 RunMyRulesToc
|
|
|
|
Automates insertion of the <toc ...> markup for Wiki pages not explicitly
|
|
specifying it. This searches the current page's HTML output for the first HTML
|
|
header tag for that page and, when found, automatically inserts <toc ...> markup
|
|
immediately before that tag.
|
|
|
|
=cut
|
|
sub RunMyRulesToc {
|
|
my $html = RunMyRulesTocOld(@_);
|
|
|
|
# Some markup rule converted the input Wiki markup into HTML. If this HTML is
|
|
# an HTML header tag, then we add a new "id" tag attribute to it (so as to
|
|
# uniquely identify it for later linking to from the table of contents).
|
|
if ($html) {
|
|
if ($TocAutomatic and not $TocHeaderNumber and $bol and $html =~
|
|
s~(<h[1-6][^>]*>)
|
|
~<!-- toc header_text="$TocHeaderText" class="$TocClass" -->$1~x) {
|
|
$TocHeaderNumber = 1;
|
|
}
|
|
|
|
# If we've seen at least one HTML header and we're not currently in the
|
|
# sidebar (as is the odd case when $TocPageName ne $OpenPageName), then
|
|
# add a unique identifier to all (possible) HTML headers in this string.
|
|
#
|
|
# If we are in the Sidebar, we musn't add such an identifier. HTML headers
|
|
# in the Sidebar are not vital to warrant listing in the table of contents
|
|
# for the page to which they're attached. (Hah!)
|
|
if ($TocHeaderNumber and not InElement('div', '^class="\w+bar"$')) {
|
|
# To avoid infinite substitution recursion, we avoid matching header tags
|
|
# already having id attributes. Unfortunately, I'm not as adept a regular
|
|
# expression wizard as I should be, and was unable to get a negative
|
|
# lookahead expression resembling (?!\s+id=".*?") to work. As such, I
|
|
# use a simple negative character class hack. *shrug*
|
|
while ($html =~ s~<h([1-6](\s+[^i]\w+\s+=\s+"[^"]")*)>
|
|
~<h$1 id="$TocAnchorPrefix$TocHeaderNumber">~cgx) {
|
|
$TocHeaderNumber++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $html;
|
|
}
|
|
|
|
# ....................{ MARKUP =after }....................
|
|
push(@MyAfterApplyRules, \&TocAfterApplyRule);
|
|
|
|
my $TocCommentPattern = qr~\Q<!-- toc\E.*?\Q -->\E~;
|
|
|
|
sub TocAfterApplyRule {
|
|
my ($html_, $blocks_, $flags_) = @_;
|
|
|
|
# If there are at least two HTML headers on this page, insert a table of
|
|
# contents.
|
|
if ($TocHeaderNumber > 2) {
|
|
$$html_ =~ s~\Q<!-- toc header_text="\E([^"]+)\Q" class="\E([^"]+)\Q" -->\E~
|
|
GetTocHtml($html_, $blocks_, $1, $2)~e;
|
|
}
|
|
# Otherwise, remove the table of contents placeholder comment.
|
|
else {
|
|
$$html_ =~ s~$TocCommentPattern~~;
|
|
$$blocks_ =~ s~$TocCommentPattern~~;
|
|
}
|
|
}
|
|
|
|
sub GetTocHtml {
|
|
my ($html_, $blocks_, $toc_header_text, $toc_class) = @_;
|
|
my $toc_html =
|
|
$q->start_div({-class=> $toc_class})
|
|
.$q->h2(T($toc_header_text));
|
|
|
|
# This forces evaluation of the "while ($list_depth < $header_depth) {"
|
|
# clause on the first iteration of the outer while loop. Yes: trust us.
|
|
my $list_depth = 0;
|
|
|
|
while ($$html_ =~ m~<h([1-6])[^>]* id="($TocAnchorPrefix\d+)">(.*?)</h\1>~cg) {
|
|
my ($header_depth, $header_id, $header_text) = ($1, $2, $3);
|
|
|
|
# Strip all links from header text. (They unnecessarily convolute the
|
|
# interface, since the header text is already embedded in a link to the
|
|
# appropriate header in the dialogue script's body.)
|
|
$header_text =~ s~<a[^>]*>(.*?)</a>~$1~;
|
|
|
|
# By Usemod convention, all headers begin with depth 2. This algorithm,
|
|
# however, expects headers to begin with depth 1. Thus, to "streamline"
|
|
# things, we transform it appropriately. ;-)
|
|
if (defined &UsemodRule) { $header_depth--; }
|
|
|
|
# If this is the first header and if this header's depth is deeper than 1,
|
|
# we manually clamp this header's depth to 1 so as to ensure the first list
|
|
# item in the first ordered list resides at depth 1. (Failure to do this
|
|
# produces very odd ordered lists.)
|
|
if (not $list_depth and $header_depth > 1) { $header_depth = 1; }
|
|
|
|
# Close ordered lists and list items for prior headings deeper than this
|
|
# heading's depth.
|
|
while ($list_depth > $header_depth) {
|
|
$list_depth--;
|
|
$toc_html .= '</li></ol>';
|
|
}
|
|
|
|
# If the current ordered list is at this heading's depth, add this heading
|
|
# as a list item to that list.
|
|
if ($list_depth == $header_depth) {
|
|
$toc_html .= '</li><li>';
|
|
}
|
|
# Otherwise, add ordered lists and list items until at this heading's depth.
|
|
else {
|
|
while ($list_depth < $header_depth) {
|
|
$list_depth++;
|
|
$toc_html .= '<ol><li>';
|
|
}
|
|
}
|
|
|
|
$toc_html .= "<a href=\"#$header_id\">$header_text</a>";
|
|
}
|
|
|
|
# Close ordered lists and list items for the last heading.
|
|
while ($list_depth--) { $toc_html .= '</li></ol>'; }
|
|
|
|
$toc_html .= $q->end_div();
|
|
|
|
# Lastly, perform the same replacement on the cached version of this clean
|
|
# block. (Failure to do this would ensure the first creation of this page
|
|
# would emit the proper HTML, but all subsequent refreshings of this page
|
|
# the improperly cached version.)
|
|
$$blocks_ =~ s~$TocCommentPattern~$toc_html~;
|
|
|
|
return $toc_html;
|
|
}
|
|
|
|
=head1 COPYRIGHT AND LICENSE
|
|
|
|
The information below applies to everything in this distribution,
|
|
except where noted.
|
|
|
|
Copyleft 2008 by B.w.Curry <http://www.raiazome.com>.
|
|
Copyright 2004, 2005, 2006, 2007 by Alex Schroeder <alex@emacswiki.org>.
|
|
|
|
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<http://www.gnu.org/licenses/>.
|
|
|
|
=cut
|