#
# Copyright (C) Motorola 2001 - All rights reserved
#
# This software is an extension to the TWiki system and is
# to be regarded as a derivative work covered by the same licenses
# and copyrights.
#
# TWiki extension that adds tags for the generation of tables of contents.
#
use strict;
use integer;
use wiki;
# Class of attribute sets
{ package Attrs;
# Parse a standard attribute string containing name=value pairs. The
# value may be a word or a quoted string (no escapes!)
sub new {
my ($class, $string) = @_;
my $this = {};
if (defined($string)) {
# name="value" pairs
while ($string =~ s/([a-z]+)\s*=\s*\"([^\"]*)\"//o) {
$this->{$1} = $2;
}
# name=value pairs
while ($string =~ s/([a-z]+)\s*=\s*([^\s,\}]*)//o) {
$this->{$1} = $2;
}
# simple name with no value (boolean)
while ($string =~ s/([a-z]+)//o) {
$this->{$1} = 1;
}
}
return bless $this, $class;
}
# Get an attr value; return undef if not set
sub get {
my ($this, $attr) = @_;
return $this->{$attr};
}
} # end of class Attrs
# Class of target anchors
{ package Anchor;
# Constructor
sub new {
my ($class, $type, $name, $text, $visible) = @_;
my $this = {};
$this->{TYPE} = $type;
$this->{NAME} = $name;
$this->{TEXT} = $text;
$this->{IS_VISIBLE} = $visible;
return bless($this, $class);
}
sub type {
my $this = shift;
if (@_) { $this->{TYPE} = shift; };
return $this->{TYPE};
}
sub name {
my $this = shift;
if (@_) { $this->{NAME} = shift; };
return $this->{NAME};
}
sub text {
my $this = shift;
if (@_) { $this->{TEXT} = shift; };
return $this->{TEXT};
}
sub visible {
my $this = shift;
if (@_) { $this->{IS_VISIBLE} = shift; };
return $this->{IS_VISIBLE};
}
# Generate HTML to anchor the link
sub generateTarget {
my $this = shift;
my $text = "type() . "_" . $this->name() . "\">";
if ($this->visible()) {
$text = $text . " " . $this->text();
}
$text = $text . " ";
return $text;
}
# Generate HTML to target the link
sub generateReference {
my ($this, $topic) = @_;
my $tgt = "";
if ($topic) {
$tgt = $topic->wikiName();
}
$tgt = $tgt . "#" . $this->type() . "_" . $this->name();
return " " . $this->text() . " ";
}
# Generate a string representation for debugging
sub toString {
my $this = shift;
return "#" . $this->type() . "_" . $this->name();
}
} # end of package Anchor
# A node in the tree of sections
{ package Section;
@Section::ISA = ("Anchor");
sub new {
my ($class, $level, $text) = @_;
my $this = $class->SUPER::new("Section", $level, $text, 1);
# array of subsections
$this->{SECTIONS} = [];
# depth of this section in the section hierarchy
$this->{LEVEL} = $level;
# parent section
$this->{PARENT} = undef;
# position in the parent section's subsection list
$this->{POSITION} = undef;
# link anchors targetting this section
$this->{ANCHORS} = {};
# files only - wiki name of this section
$this->{WIKINAME} = undef;
# file only - has been loaded
$this->{IS_LOADED} = undef;
return bless($this, $class);
}
sub level {
my $this = shift;
if (@_) { $this->{LEVEL} = shift; };
return $this->{LEVEL};
}
sub parent {
my $this = shift;
if (@_) { $this->{PARENT} = shift; };
return $this->{PARENT};
}
sub position {
my $this = shift;
if (@_) { $this->{POSITION} = shift; };
return $this->{POSITION};
}
sub wikiName {
my $this = shift;
if (@_) { $this->{WIKINAME} = shift; };
return $this->{WIKINAME};
}
# Get the list of anchors for a given gey
sub anchors {
my ($this, $key) = @_;
die unless defined($key);
if (defined($this->{ANCHORS}->{$key})) {
return @{$this->{ANCHORS}->{$key}};
} else {
return [];
}
}
# Generate a decorated target for this section
sub generateTarget {
my $this = shift;
my $topic = $this->_getTopic();
my $level = $this->level() - $topic->level();
my $text = "";
# For SECTION0 use
if ($level <= 0) {
$level = 1;
}
$text = $this->SUPER::generateTarget();
my $key;
foreach $key (keys %{$this->{ANCHORS}}) {
my $link;
foreach $link ( $this->anchors($key) ) {
$text = $text . $link->generateTarget() . "\n";
}
}
return "\n" . $text . "\n";
}
# add a new subsection to the end of our list of subsections
sub _push_back {
my ($this, $that) = @_;
push @{$this->{SECTIONS}}, $that;
$that->parent($this);
$that->position(scalar(@{$this->{SECTIONS}}));
}
# Get the root from within a topic
sub _getRoot {
my $this = shift;
while (defined($this->parent())) {
$this = $this->parent();
}
return $this;
}
# Get the topic that contains this section
sub _getTopic {
my $this = shift;
if (defined($this->wikiName()) || !defined($this->parent())) {
return $this;
}
return $this->parent()->_getTopic();
}
# Get the wiki name of the topic that is, or contains, this
sub _getTopicURL {
my $this = shift;
return $this->_getTopic()->wikiName();
}
# Get the most recently added subsection
sub _getLastSubsection {
my $this = shift;
return @{$this->{SECTIONS}}[$#{$this->{SECTIONS}}];
}
# add a new subsection at, or below, the level of this section.
sub _addSection {
my ($this, $newEntry) = @_;
if ($newEntry->level() == $this->level() + 1) {
# add at this level
$this->_push_back($newEntry);
} else {
# add to the last entry added
if (scalar(@{$this->{SECTIONS}}) == 0) {
# insert a pseudo-section to compensate for the
# missing section level
my $tmp = Section->new($this->level() + 1, "_missing_");
$this->_push_back($tmp);
}
$this->_getLastSubsection()->_addSection($newEntry);
}
}
# Add a reference.
sub _addAnchor {
my ($this, $type, $name, $text, $visible) = @_;
my $pretext = $this->_getSectionNumber();
if ($type ne "Section") {
my $idx = 0;
if (defined($this->{ANCHORS}->{$type})) {
$idx = @{$this->{ANCHORS}->{$type}};
}
$pretext = $type . " " . $pretext .
pack("c", ord("A") + $idx);
}
my $ref = Anchor->new($type, $name,
$pretext . " " . $text . " ", $visible);
push(@{$this->{ANCHORS}->{$type}}, $ref);
return $ref;
}
# Get the unique section number for this section.
sub _getSectionNumber {
my $this = shift;
my $parent = $this->parent();
if (!defined($parent)) {
return "";
} else {
return $parent->_getSectionNumber() . $this->position() . ".";
}
}
# Convert to a string for debugging
sub toString {
my ($this, $indent) = @_;
if (!defined($indent)) {
$indent = "";
}
my $res = $indent . $this->_getSectionNumber() . " " .
$this->text();
if (defined($this->wikiName())) {
$res = $res . " is file " . $this->_getTopicURL();
}
my $key;
foreach $key (keys %{$this->{ANCHORS}}) {
$res = $res . "\n" . $indent . "...";
my $link;
foreach $link ( @{$this->{ANCHORS}->{$key}} ) {
$res = $res . " " . $link->toString();
}
}
$res = $res . "\n";
my $child;
foreach $child ( @{$this->{SECTIONS}} ) {
$res = $res . $child->toString($indent . " ");
}
return $res;
}
# Find a link target and return the topic and the link
sub _findTarget {
my ($this, $type, $name) = @_;
# find the tag
my $link;
foreach $link ( @{$this->{ANCHORS}->{$type}} ) {
if ($link->name() eq $name) {
return ($this, $link);
}
}
my $section;
foreach $section ( @{$this->{SECTIONS}} ) {
my ($sec, $link) = $section->_findTarget($type, $name);
if ($link) { return ($sec, $link) };
}
return undef;
}
# Find a FILE topic in the TOC
sub _findTopic {
my ( $this, $wikiName ) = @_;
if (defined($this->wikiName()) && $wikiName eq $this->wikiName()) {
return $this;
}
my $section;
foreach $section ( @{$this->{SECTIONS}} ) {
my $found = $section->_findTopic($wikiName);
if (defined($found)) {
return $found;
}
}
return undef;
}
# Generate table of contents for this section
sub processTOCTag {
my ($this, $attrs) = @_;
my $attrSet = Attrs->new($attrs);
my $topic = $attrSet->get("topic");
if ($topic) {
$topic = TOC::_tocEntryToWikiName($topic);
$this = $this->_getRoot()->_findTopic($topic);
if (!$this) {
return TOC::_error("Bad topic $topic");
}
}
my $depth = $attrSet->get("depth");
return $this->_generateTOCEntry(1, $depth);
}
# Generate a prettifying indent to make the HTML readable
sub _indent {
my $level = shift;
my $text = "";
for (my $i = 0; $i < $level; $i++) {
$text = $text . " ";
}
return $text;
}
# Generate table of contents rows for this section and it's children
sub _generateTOCEntry {
my ($this, $level, $depth) = @_;
my $row = _indent($level);
if ($level > 1) {
$row = $row . $this->generateReference() . "
";
}
if (defined($depth)) {
return $row if ($level == $depth + 1);
}
if (defined($this->wikiName()) && !defined($this->{IS_LOADED})) {
$this->_loadTopic();
}
if (scalar(@{$this->{SECTIONS}})) {
$row = $row . _indent($level) . "\n";
my $section;
foreach $section ( @{$this->{SECTIONS}} ) {
$row = $row . $section->_generateTOCEntry($level + 1, $depth);
}
$row = $row . _indent($level) . "
\n";
}
return $row;
}
# Load the topic into this section
sub _parseTopicText {
my ($this, $text) = @_;
while ($text =~ s/%(SECTION[0-9]+|ANCHOR)({[^%]*})?%(.*)//o) {
my $key = $1;
my $attrs = $2;
my $title = $3;
if ($key =~ s/([0-9]+)//o) {
$this->processSECTIONTag($attrs, $1, $title);
} else {
$this->processANCHORTag($attrs, $title);
}
# recursively parse the title text
$this->_parseTopicText($title);
}
}
# Load the sections for a topic from the data directory
sub _loadTopic {
my $this = shift;
my $topic = $this->_getTopicURL();
if ($topic) {
my $text = wiki::readTopic($topic);
$text =~ s/\r//go;
$this->_parseTopicText($text);
$this->{IS_LOADED} = 1;
}
}
# Process a SECTION tag
sub processSECTIONTag {
my ($this, $attrs, $secLevel, $title) = @_;
my $attrSet = Attrs->new($attrs);
my $noTagsTitle = " " . TOC::_removeAllTags($title);
my $ne;
if ($secLevel > 0) {
my $level = $this->level() + $secLevel;
$ne = Section->new($level, $noTagsTitle);
$this->_addSection($ne);
# We know the section number now, so adjust
# the name and text.
my $sec = $ne->_getSectionNumber();
$ne->name($sec);
$ne->text($sec . " " . $ne->text());
} else {
# level 0; referring to this section
$ne = $this;
# N.B. Text after a %SECTION0% tag is ignored
}
# add an extra anchor if so requested
my $link = $attrSet->get("name");
if (defined($link)) {
$ne->_addAnchor("Section", $link, $noTagsTitle, 0);
}
return $ne;
}
# Process an ANCHOR tag
sub processANCHORTag {
my ($this, $attrs, $title) = @_;
my $attrSet = Attrs->new($attrs);
my $noTagsTitle = " " . TOC::_removeAllTags($title);
my $type = $attrSet->get("type");
my $name = $attrSet->get("name");
my $display = $attrSet->{"display"};
my $visible = (!$display || $display ne "no");
return undef unless (defined($type) && defined($name));
# Add the anchor to the last subsection under this
my $lss = $this;
while (scalar(@{$lss->{SECTIONS}})) {
$lss = $lss->_getLastSubsection();
}
return $lss->_addAnchor($type, $name, $noTagsTitle, $visible);
}
# Process a REF tag and return the topic and the link
sub processREFTag {
my ($this, $attrs, $text) = @_;
my $attrSet = Attrs->new($attrs);
my $type = $attrSet->get("type");
my $topic = $attrSet->get("topic");
my $name = $attrSet->get("name");
if ($topic) {
$topic = TOC::_tocEntryToWikiName($topic);
$this = $this->_getRoot()->_findTopic($topic);
if (!$this) {
return undef;
}
}
return $this->_findTarget($type, $name);
}
# Generate a table of contents.
# REFTABLE tag implementation
sub processREFTABLETag {
my ( $this, $attrs ) = @_;
my $attrSet = Attrs->new($attrs);
my $type = $attrSet->get("type");
return TOC::_error("Bad type in REFTABLE") unless (defined($type));
my $table = "
\n| *$type* " .
" |
\n";
$table = $table . $this->_generateRefRow($type);
return $table . "
\n";
}
sub _generateRefRow {
my ($this, $type) = @_;
my $row = "";
if (defined($this->wikiName()) && !defined($this->{IS_LOADED})) {
$this->_loadTopic();
}
# find the tag
my $topic = $this->_getTopic();
my $link;
foreach $link ( @{$this->{ANCHORS}->{$type}} ) {
$row = $row . "| " .
$link->generateReference($topic) . " |
\n";
}
my $section;
foreach $section ( @{$this->{SECTIONS}} ) {
$row = $row . $section->_generateRefRow($type);
}
return $row;
}
}
{ package TOC;
@TOC::ISA = ("Section");
sub new {
my $class = shift;
my $this = $class->SUPER::new(0, "");
$this->{CURRENT_TOPIC} = undef;
return bless($this, $class);
}
# Create a red string
sub _error {
my ( $text ) = @_;
return "$text";
}
# remove tags of a given type from a string
sub _removeTypeTags {
my ($type, $text) = @_;
$text =~ s/%$type({[^%]*})?%//go;
return $text;
}
# Remove all types of TOC tag from the string
sub _removeAllTags {
my $text = shift;
$text = _removeTypeTags("ANCHOR", $text);
$text =~ s/%SECTION[0-9]+({[^%]*})?%//go;
$text = _removeTypeTags("REFTABLE", $text);
$text = _removeTypeTags("TOC", $text);
$text = _removeTypeTags("TOCCHECK", $text);
return $text;
}
# Find files in this web which are not listed in the table of contents topic
# TOCCHECK tag implementation
sub processTOCCHECKTag {
my $this = shift;
# Get list of files in the web
chdir( "$wiki::dataDir/$wiki::webName" );
my $cmd = "$wiki::lsCmd *.txt";
my $fullText = `$cmd`;
$fullText =~ s/\.txt//go;
# ignore topics that start with "Web"
$fullText =~ s/\nWeb.*//go;
my @fullList = split( /\n/, $fullText );
my $result = "";
my $topic;
foreach $topic ( @fullList ) {
if (!$this->_findTopic($topic)) {
$result = $result . "\n" . $topic . "";
}
}
if ($result ne "") {
$result = _error("The following topics were not found " .
"in the WebOrder:\n" . $result . "\n
\n");
}
return $result;
}
# Modified version of internalLink from wiki.pm, required because the
# original is rather overenthusiastic in reformatting the link.
sub _expandOddLink {
my( $topic ) = @_;
# kill spaces and Wikify topic name
$topic =~ s/^\s*//o;
$topic =~ s/\s*$//o;
$topic =~ s/^(.)/\U$1/o;
$topic =~ s/\s([a-zA-Z0-9])/\U$1/go;
return $topic;
}
# static method to convert a toc entry to a wiki word
sub _tocEntryToWikiName {
my $t = shift;
$t =~ s/^[\s*]*//o;
$t =~ s/[\s*]*$//o;
# expand [[odd link]]
$t =~ s/\[\[([\w\s]+)\]\]/_expandOddLink($1)/eo;
return $t;
}
# Factory method to construct from a list as contained in the WebOrder
sub _constructFromTocList {
my ($class, @list) = @_;
my $root = TOC->new();
my $tocEntry;
foreach $tocEntry ( @list ) {
my $name = $tocEntry;
$name =~ s/^[\s\*]*//o;
my $level = 0;
while ($tocEntry =~ s/^(\t| )//o) {
$level++;
};
my $ne = $root->processSECTIONTag("", $level, $name);
$ne->wikiName(_tocEntryToWikiName($name));
}
return $root;
}
# Set the current topic being read and flag it as loaded; we are
# about to perform an operation that requires it.
# Must only be applied to the root.
sub _getCurrTopic {
my $this = shift;
my $currTopic = $this->{CURRENT_TOPIC};
return $currTopic if ($currTopic);
$currTopic = $this->_findTopic($wiki::topicName);
if ($currTopic) {
$currTopic->{IS_LOADED} = 1;
}
$this->{CURRENT_TOPIC} = $currTopic;
return $currTopic;
}
# Static instance; root of the table of contents
my $toc;
sub processTag {
my ($tag, @params) = @_;
if (!$toc) {
if (!&wiki::topicExists($wiki::webName, "WebOrder")) {
return _error("No WebOrder in $wiki::webName");
}
# load the table of contents topic
my $tocText = &wiki::readTopic("WebOrder");
# allow [[Odd Wiki Word]] links
my @tocNames = split( /[\n\r]/, $tocText );
# extract the bulleted list
@tocNames = grep( /^\s+\*\s/, @tocNames );
$toc = TOC->_constructFromTocList(@tocNames);
}
if ($tag eq "ANCHOR") {
if (!$toc->_getCurrTopic()) {
return _error("Bad ANCHOR: Current topic not in WebOrder");
}
my $anchor = $toc->_getCurrTopic()->processANCHORTag(@params);
if ($anchor) {
return $anchor->generateTarget();
}
} elsif ($tag eq "SECTION") {
if (!$toc->_getCurrTopic()) {
return _error("Bad SECTION: Current topic not in WebOrder");
}
my $sec = $toc->_getCurrTopic()->processSECTIONTag(@params);
if ($sec) {
return $sec->generateTarget();
}
} elsif ($tag eq "REF") {
if (!$toc->_getCurrTopic()) {
return _error("Bad REF: Current topic not in WebOrder");
}
my ($sec, $link) = $toc->_getCurrTopic()->processREFTag(@params);
if ($link) {
return $link->generateReference($sec->_getTopic());
}
} elsif ($tag eq "TOCCHECK") {
return $toc->processTOCCHECKTag(@params);
} elsif ($tag eq "TOC") {
return $toc->processTOCTag(@params);
} elsif ($tag eq "REFTABLE") {
return $toc->processREFTABLETag(@params);
}
return _error("Bad tag $tag: " . join(",", @params));
}
sub processTOCTags {
my $text = shift;
# Process wikitoc tags. Must be done in order so that anchor tags
# get added in the right places and before we generate the table
# of contents and ref tables.
while ($text =~ s/%((SECTION[0-9]+)|ANCHOR)({[^%]*})?%(.*)/\/o) {
my $tag = $1;
my $a = $3;
my $r = $4;
if ($tag =~ s/SECTION([0-9]+)//o) {
my $l = $1;
#print "SECTION $level $a $r\n";
$text =~ s/\/&processTag("SECTION",$a,$l,$r)/eo;
} else {
#print "ANCHOR $a $r\n";
$text =~ s/\/&processTag("ANCHOR",$a,$r)/eo;
}
}
# The order in which the other tags is done is irrelevant
$text =~ s/%(REF){([^%]*)}%/&processTag($1,$2)/geo;
$text =~ s/%(TOC)({[^%]*})?%/&processTag($1,$2)/geo;
$text =~ s/%(TOCCHECK)%/&processTag($1)/geo;
$text =~ s/%(REFTABLE){([^%]*)}%/&processTag($1,$2)/geo;
return $text;
}
} # end of class TOC
1;