#!/usr/bin/env perl
use strict;
use v5.10;
our ($q, $bol, @HtmlStack, @MyRules, @MyInitVariables);
# ====================[ 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
directory for your Oddmuse Wiki.
=cut
AddModuleDescription('toc.pl', 'Table of Contents Extension');
# ....................{ CONFIGURATION }....................
=head1 CONFIGURATION
toc is easily configurable; set these variables in the B file
for your Oddmuse Wiki.
=cut
our ($TocHeaderText,
$TocClass,
$TocAutomatic,
$TocAnchorPrefix,
$TocIsApplyingAutomaticRules);
=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 "" 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. A link to the first header on
SomePage page for some wiki.
=item L. 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';
=head2 $TocIsApplyingAutomaticRules
A boolean that, if true, performs a few "automatic" rules on behalf of this
extension. These are:
=over
=item Add a unique C attribute to each header tag on every page.
This ensures that every link in the table of contents, for every page,
refers to one and only one header tag in that page.
=item Add an automatic table of contents to every page, if the
C<$TocAutomatic> boolean is also enabled.
=back
By default, this boolean is true. (This is a good thing. Unless you know what
you're doing, you should probably leave this as is.)
=cut
$TocIsApplyingAutomaticRules = 1;
# ....................{ 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:
Or, in its abbreviated form:
Or, in its maximally abbreviated form:
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 {
# markup. This explicitly displays a table of contents at this point.
if ($bol and
m~\G<toc(/([A-Za-z\x{0080}-\x{fffd}/]+))? # $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:
#
#
# which this conditional converts to the more conventional:
#
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{}
.AddHtmlEnvironment('p');
} return;
}
=head2 RunMyRulesToc
Automates insertion of the 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 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).
# to the user, without embellishments or change.
if ($TocIsApplyingAutomaticRules and $html) {
if ($TocAutomatic and not $TocHeaderNumber and $bol and $html =~
s~(]*>)
~$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 ($TocHeaderNumber) {
# 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~
~